aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--patches/server/0822-fixup-MC-Utils.patch1263
-rw-r--r--patches/server/0822-fixup-Paper-config-files.patch (renamed from patches/server/0824-fixup-Paper-config-files.patch)4
-rw-r--r--patches/server/0823-fixup-MC-Utils.patch1318
-rw-r--r--patches/server/0824-Rewrite-dataconverter-system.patch (renamed from patches/server/0825-Rewrite-dataconverter-system.patch)0
-rw-r--r--patches/server/0825-fixup-Optimize-BlockPosition-helper-methods.patch (renamed from patches/server/0827-fixup-Optimize-BlockPosition-helper-methods.patch)0
-rw-r--r--patches/server/0826-Moonrise-optimisation-patches.patch8263
-rw-r--r--patches/server/0828-fixup-Moonrise-optimisation-patches.patch13349
7 files changed, 6788 insertions, 17409 deletions
diff --git a/patches/server/0822-fixup-MC-Utils.patch b/patches/server/0822-fixup-MC-Utils.patch
deleted file mode 100644
index cc014ce7e8..0000000000
--- a/patches/server/0822-fixup-MC-Utils.patch
+++ /dev/null
@@ -1,1263 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <[email protected]>
-Date: Mon, 21 Oct 2024 12:21:54 -0700
-Subject: [PATCH] fixup! MC Utils
-
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..3c5ed66328ccf94c4744a191a7c63562dd08158d
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
-@@ -0,0 +1,115 @@
-+package ca.spottedleaf.moonrise.common;
-+
-+import com.mojang.datafixers.DataFixer;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.GenerationChunkHolder;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.util.datafix.DataFixTypes;
-+import net.minecraft.world.entity.Entity;
-+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.LevelChunk;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import net.minecraft.world.level.chunk.storage.SerializableChunkData;
-+import net.minecraft.world.level.entity.EntityTypeTest;
-+import net.minecraft.world.phys.AABB;
-+import java.util.List;
-+import java.util.ServiceLoader;
-+import java.util.function.Predicate;
-+
-+public interface PlatformHooks {
-+ public static PlatformHooks get() {
-+ return Holder.INSTANCE;
-+ }
-+
-+ public String getBrand();
-+
-+ public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos);
-+
-+ public Predicate<BlockState> maybeHasLightEmission();
-+
-+ public boolean hasCurrentlyLoadingChunk();
-+
-+ public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder);
-+
-+ public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk);
-+
-+ public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original);
-+
-+ public boolean allowAsyncTicketUpdates();
-+
-+ public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel);
-+
-+ public void chunkUnloadFromWorld(final LevelChunk chunk);
-+
-+ public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data);
-+
-+ public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player);
-+
-+ public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player);
-+
-+ public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate,
-+ final List<Entity> into);
-+
-+ public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest,
-+ final AABB boundingBox, final Predicate<? super T> predicate,
-+ final List<? super T> into, final int maxCount);
-+
-+ 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();
-+
-+ public double configPlayerMaxLoadRate();
-+
-+ public double configPlayerMaxGenRate();
-+
-+ public double configPlayerMaxSendRate();
-+
-+ public int configPlayerMaxConcurrentLoads();
-+
-+ public int configPlayerMaxConcurrentGens();
-+
-+ public long configAutoSaveInterval();
-+
-+ public int configMaxAutoSavePerTick();
-+
-+ public boolean configFixMC159283();
-+
-+ // support for CB chunk mustNotSave
-+ public boolean forceNoSave(final ChunkAccess chunk);
-+
-+ public CompoundTag convertNBT(final DataFixTypes type, final DataFixer dataFixer, final CompoundTag nbt,
-+ final int fromVersion, final int toVersion);
-+
-+ public boolean hasMainChunkLoadHook();
-+
-+ public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData);
-+
-+ public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities);
-+
-+ public void unloadEntity(final Entity entity);
-+
-+ public int modifyEntityTrackingRange(final Entity entity, final int currentRange);
-+
-+ public static final class Holder {
-+ private Holder() {
-+ }
-+
-+ private static final PlatformHooks INSTANCE;
-+
-+ static {
-+ INSTANCE = ServiceLoader.load(PlatformHooks.class, PlatformHooks.class.getClassLoader()).findFirst()
-+ .orElseThrow(() -> new RuntimeException("Failed to locate PlatformHooks"));
-+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
-index ba68998f6ef57b24c72fd833bd7de440de9501cc..7fed43a1e7bcf35c4d7fd3224837a47fedd59860 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
-@@ -13,15 +13,15 @@ import java.util.NoSuchElementException;
- */
- public final class EntityList implements Iterable<Entity> {
-
-- protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
-+ private final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
- {
- this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE);
- }
-
-- protected static final Entity[] EMPTY_LIST = new Entity[0];
-+ private static final Entity[] EMPTY_LIST = new Entity[0];
-
-- protected Entity[] entities = EMPTY_LIST;
-- protected int count;
-+ private Entity[] entities = EMPTY_LIST;
-+ private int count;
-
- public int size() {
- return this.count;
-@@ -94,10 +94,9 @@ public final class EntityList implements Iterable<Entity> {
-
- @Override
- public Iterator<Entity> iterator() {
-- return new Iterator<Entity>() {
--
-- Entity lastRet;
-- int current;
-+ return new Iterator<>() {
-+ private Entity lastRet;
-+ private int current;
-
- @Override
- public boolean hasNext() {
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
-deleted file mode 100644
-index fcfbca333234c09f7c056bbfcd9ac8860b20a8db..0000000000000000000000000000000000000000
---- a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
-+++ /dev/null
-@@ -1,125 +0,0 @@
--package ca.spottedleaf.moonrise.common.list;
--
--import it.unimi.dsi.fastutil.longs.LongIterator;
--import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap;
--import java.util.Arrays;
--import net.minecraft.world.level.block.Block;
--import net.minecraft.world.level.block.state.BlockState;
--import net.minecraft.world.level.chunk.GlobalPalette;
--
--public final class IBlockDataList {
--
-- private static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
--
-- // map of location -> (index | (location << 16) | (palette id << 32))
-- private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f);
-- {
-- this.map.defaultReturnValue(Long.MAX_VALUE);
-- }
--
-- private static final long[] EMPTY_LIST = new long[0];
--
-- private long[] byIndex = EMPTY_LIST;
-- private int size;
--
-- public static int getLocationKey(final int x, final int y, final int z) {
-- return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4));
-- }
--
-- public static BlockState getBlockDataFromRaw(final long raw) {
-- return GLOBAL_PALETTE.valueFor((int)(raw >>> 32));
-- }
--
-- public static int getIndexFromRaw(final long raw) {
-- return (int)(raw & 0xFFFF);
-- }
--
-- public static int getLocationFromRaw(final long raw) {
-- return (int)((raw >>> 16) & 0xFFFF);
-- }
--
-- public static long getRawFromValues(final int index, final int location, final BlockState data) {
-- return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32);
-- }
--
-- public static long setIndexRawValues(final long value, final int index) {
-- return value & ~(0xFFFF) | (index);
-- }
--
-- public long add(final int x, final int y, final int z, final BlockState data) {
-- return this.add(getLocationKey(x, y, z), data);
-- }
--
-- public long add(final int location, final BlockState data) {
-- final long curr = this.map.get((short)location);
--
-- if (curr == Long.MAX_VALUE) {
-- final int index = this.size++;
-- final long raw = getRawFromValues(index, location, data);
-- this.map.put((short)location, raw);
--
-- if (index >= this.byIndex.length) {
-- this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L));
-- }
--
-- this.byIndex[index] = raw;
-- return raw;
-- } else {
-- final int index = getIndexFromRaw(curr);
-- final long raw = this.byIndex[index] = getRawFromValues(index, location, data);
--
-- this.map.put((short)location, raw);
--
-- return raw;
-- }
-- }
--
-- public long remove(final int x, final int y, final int z) {
-- return this.remove(getLocationKey(x, y, z));
-- }
--
-- public long remove(final int location) {
-- final long ret = this.map.remove((short)location);
-- final int index = getIndexFromRaw(ret);
-- if (ret == Long.MAX_VALUE) {
-- return ret;
-- }
--
-- // move the entry at the end to this index
-- final int endIndex = --this.size;
-- final long end = this.byIndex[endIndex];
-- if (index != endIndex) {
-- // not empty after this call
-- this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index));
-- }
-- this.byIndex[index] = end;
-- this.byIndex[endIndex] = 0L;
--
-- return ret;
-- }
--
-- public int size() {
-- return this.size;
-- }
--
-- public long getRaw(final int index) {
-- return this.byIndex[index];
-- }
--
-- public int getLocation(final int index) {
-- return getLocationFromRaw(this.getRaw(index));
-- }
--
-- public BlockState getData(final int index) {
-- return getBlockDataFromRaw(this.getRaw(index));
-- }
--
-- public void clear() {
-- this.size = 0;
-- this.map.clear();
-- }
--
-- public LongIterator getRawIterator() {
-- return this.map.values().iterator();
-- }
--}
-\ No newline at end of file
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..9f3b25bb2439f283f878db93973a02fcdcd14eed
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
-@@ -0,0 +1,77 @@
-+package ca.spottedleaf.moonrise.common.list;
-+
-+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
-+import java.util.Arrays;
-+
-+public final class IntList {
-+
-+ private final Int2IntOpenHashMap map = new Int2IntOpenHashMap();
-+ {
-+ this.map.defaultReturnValue(Integer.MIN_VALUE);
-+ }
-+
-+ private static final int[] EMPTY_LIST = new int[0];
-+
-+ private int[] byIndex = EMPTY_LIST;
-+ private int count;
-+
-+ public int size() {
-+ return this.count;
-+ }
-+
-+ public void setMinCapacity(final int len) {
-+ final int[] byIndex = this.byIndex;
-+ if (byIndex.length < len) {
-+ this.byIndex = Arrays.copyOf(byIndex, len);
-+ }
-+ }
-+
-+ public int getRaw(final int index) {
-+ return this.byIndex[index];
-+ }
-+
-+ public boolean add(final int value) {
-+ final int count = this.count;
-+ final int currIndex = this.map.putIfAbsent(value, count);
-+
-+ if (currIndex != Integer.MIN_VALUE) {
-+ return false; // already in this list
-+ }
-+
-+ int[] list = this.byIndex;
-+
-+ if (list.length == count) {
-+ // resize required
-+ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
-+ }
-+
-+ list[count] = value;
-+ this.count = count + 1;
-+
-+ return true;
-+ }
-+
-+ public boolean remove(final int value) {
-+ final int index = this.map.remove(value);
-+ if (index == Integer.MIN_VALUE) {
-+ return false;
-+ }
-+
-+ // move the entry at the end to this index
-+ final int endIndex = --this.count;
-+ final int end = this.byIndex[endIndex];
-+ if (index != endIndex) {
-+ // not empty after this call
-+ this.map.put(end, index);
-+ }
-+ this.byIndex[index] = end;
-+ this.byIndex[endIndex] = 0;
-+
-+ return true;
-+ }
-+
-+ public void clear() {
-+ this.count = 0;
-+ this.map.clear();
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..2bae9949ef325d0001aa638150fbbdf968367e75
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java
-@@ -0,0 +1,77 @@
-+package ca.spottedleaf.moonrise.common.list;
-+
-+import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap;
-+import java.util.Arrays;
-+
-+public final class ShortList {
-+
-+ private final Short2ShortOpenHashMap map = new Short2ShortOpenHashMap();
-+ {
-+ this.map.defaultReturnValue(Short.MIN_VALUE);
-+ }
-+
-+ private static final short[] EMPTY_LIST = new short[0];
-+
-+ private short[] byIndex = EMPTY_LIST;
-+ private short count;
-+
-+ public int size() {
-+ return (int)this.count;
-+ }
-+
-+ public short getRaw(final int index) {
-+ return this.byIndex[index];
-+ }
-+
-+ public void setMinCapacity(final int len) {
-+ final short[] byIndex = this.byIndex;
-+ if (byIndex.length < len) {
-+ this.byIndex = Arrays.copyOf(byIndex, len);
-+ }
-+ }
-+
-+ public boolean add(final short value) {
-+ final int count = (int)this.count;
-+ final short currIndex = this.map.putIfAbsent(value, (short)count);
-+
-+ if (currIndex != Short.MIN_VALUE) {
-+ return false; // already in this list
-+ }
-+
-+ short[] list = this.byIndex;
-+
-+ if (list.length == count) {
-+ // resize required
-+ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
-+ }
-+
-+ list[count] = value;
-+ this.count = (short)(count + 1);
-+
-+ return true;
-+ }
-+
-+ public boolean remove(final short value) {
-+ final short index = this.map.remove(value);
-+ if (index == Short.MIN_VALUE) {
-+ return false;
-+ }
-+
-+ // move the entry at the end to this index
-+ final short endIndex = --this.count;
-+ final short end = this.byIndex[endIndex];
-+ if (index != endIndex) {
-+ // not empty after this call
-+ this.map.put(end, index);
-+ }
-+ this.byIndex[(int)index] = end;
-+ this.byIndex[(int)endIndex] = (short)0;
-+
-+ return true;
-+ }
-+
-+ public void clear() {
-+ this.count = (short)0;
-+ this.map.clear();
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..c2d917c2eac55b8a4411a6e159f177f9428b1150
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java
-@@ -0,0 +1,22 @@
-+package ca.spottedleaf.moonrise.common.misc;
-+
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import java.lang.invoke.VarHandle;
-+
-+public final class LazyRunnable implements Runnable {
-+
-+ private volatile Runnable toRun;
-+ private static final VarHandle TO_RUN_HANDLE = ConcurrentUtil.getVarHandle(LazyRunnable.class, "toRun", Runnable.class);
-+
-+ public void setRunnable(final Runnable run) {
-+ final Runnable prev = (Runnable)TO_RUN_HANDLE.compareAndExchange(this, (Runnable)null, run);
-+ if (prev != null) {
-+ throw new IllegalStateException("Runnable already set");
-+ }
-+ }
-+
-+ @Override
-+ public void run() {
-+ ((Runnable)TO_RUN_HANDLE.getVolatile(this)).run();
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
-index ab093b0e8ac6f762921eb1d15f5217345c4eba05..bb44de17a37082e57f2292a4f470740be1d09b11 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
-@@ -4,13 +4,17 @@ 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 {
-
-@@ -20,7 +24,27 @@ public final class NearbyPlayers {
- GENERAL_REALLY_SMALL,
- TICK_VIEW_DISTANCE,
- VIEW_DISTANCE,
-- SPAWN_RANGE, // Moonrise - chunk tick iteration
-+ // 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();
-@@ -37,6 +61,12 @@ public final class NearbyPlayers {
- 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;
-@@ -70,6 +100,16 @@ public final class NearbyPlayers {
- }
- }
-
-+ 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) {
-@@ -94,38 +134,41 @@ public final class NearbyPlayers {
- return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
- }
-
-- public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
-- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
-+ public TrackedChunk getChunk(final int chunkX, final int chunkZ) {
-+ return this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ }
-
-- return chunk == null ? null : chunk.players[type.ordinal()];
-+ 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) {
-- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
--
-- return chunk == null ? null : chunk.players[type.ordinal()];
-+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos));
- }
-
- public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
-- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
-- return chunk == null ? null : chunk.players[type.ordinal()];
-+ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
- }
-
- public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
-- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
--
-- return chunk == null ? null : chunk.players[type.ordinal()];
-+ 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;
- }
-@@ -145,7 +188,9 @@ public final class NearbyPlayers {
- final ReferenceList<ServerPlayer> list = this.players[idx];
- if (list == null) {
- ++this.nonEmptyLists;
-- (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY)).add(player);
-+ final ReferenceList<ServerPlayer> players = (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY));
-+ this.nearbyPlayers.directByChunk[idx].put(this.chunkKey, players);
-+ players.add(player);
- return;
- }
-
-@@ -169,6 +214,7 @@ public final class NearbyPlayers {
-
- if (list.size() == 0) {
- this.players[idx] = null;
-+ this.nearbyPlayers.directByChunk[idx].remove(this.chunkKey);
- --this.nonEmptyLists;
- }
- }
-@@ -187,9 +233,19 @@ public final class NearbyPlayers {
- protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
- final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-
-- NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> {
-- return new TrackedChunk();
-- }).addPlayer(parameter, this.type);
-+ 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
-@@ -201,10 +257,16 @@ public final class NearbyPlayers {
- throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey));
- }
-
-- chunk.removePlayer(parameter, this.type);
-+ 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/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-index da323a1105347d5cf4b946df10ded78a953236f2..94bba2b71918d79f54b3e28c35e76098ba0afd8c 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-@@ -1,6 +1,7 @@
- package ca.spottedleaf.moonrise.common.util;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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;
-@@ -24,15 +25,15 @@ public final class ChunkSystem {
- }
-
- public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
-- scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
-+ scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
- }
-
-- public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ 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 PrioritisedExecutor.Priority priority,
-+ final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
- final Consumer<ChunkAccess> onComplete) {
- if (gen) {
- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-@@ -59,7 +60,7 @@ public final class ChunkSystem {
-
- private static long chunkLoadCounter = 0L;
- public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
-- final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
-+ final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) {
- if (!org.bukkit.Bukkit.isPrimaryThread()) {
- scheduleChunkTask(level, chunkX, chunkZ, () -> {
- scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
-@@ -113,13 +114,13 @@ public final class ChunkSystem {
- }
- loadCallback.accept(result.orElse(null));
- }, (final Runnable r) -> {
-- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
-+ 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 PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
-+ 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");
-@@ -196,7 +197,7 @@ public final class ChunkSystem {
- }
- loadCallback.accept(result.orElse(null));
- }, (final Runnable r) -> {
-- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
-+ scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
- });
- }
-
-@@ -220,7 +221,10 @@ public final class ChunkSystem {
- return getUpdatingChunkHolderCount(level) != 0;
- }
-
-- public static boolean screenEntity(final ServerLevel level, final Entity entity) {
-+ 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;
- }
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
-index ac6f284ee4469d16c5655328b2488d7612832353..97848869df61648fc415e4d39f409f433202c274 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
-@@ -3,8 +3,12 @@ package ca.spottedleaf.moonrise.common.util;
- public final class MixinWorkarounds {
-
- // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs
-+ // https://github.com/FabricMC/Mixin/pull/147
- public static long[] clone(final long[] values) {
- return values.clone();
- }
-
-+ public static byte[] clone(final byte[] values) {
-+ return values.clone();
-+ }
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
-index 3abe0bd2a820352b85306d554bf14a4cf6123091..c125c70a68130be373acc989053a6c0e487be924 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
-@@ -1,45 +1,100 @@
- package ca.spottedleaf.moonrise.common.util;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
-+import ca.spottedleaf.concurrentutil.executor.thread.PrioritisedThreadPool;
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
--import java.io.File;
-+import java.util.concurrent.TimeUnit;
-+import java.util.concurrent.atomic.AtomicInteger;
-+import java.util.function.Consumer;
-
- public final class MoonriseCommon {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseCommon.class);
-
-- // Paper start
-- public static PrioritisedThreadPool WORKER_POOL;
-- public static int WORKER_THREADS;
-- public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
-- // Paper end
-+ public static final PrioritisedThreadPool WORKER_POOL = new PrioritisedThreadPool(
-+ new Consumer<>() {
-+ private final AtomicInteger idGenerator = new AtomicInteger();
-+
-+ @Override
-+ public void accept(Thread thread) {
-+ thread.setDaemon(true);
-+ thread.setName(PlatformHooks.get().getBrand() + " Common Worker #" + this.idGenerator.getAndIncrement());
-+ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
-+ @Override
-+ public void uncaughtException(final Thread thread, final Throwable throwable) {
-+ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
-+ }
-+ });
-+ }
-+ }
-+ );
-+ public static final long WORKER_QUEUE_HOLD_TIME = (long)(20.0e6); // 20ms
-+ public static final int CLIENT_DIVISION = 0;
-+ public static final PrioritisedThreadPool.ExecutorGroup RENDER_EXECUTOR_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(CLIENT_DIVISION, 0);
-+ public static final int SERVER_DIVISION = 1;
-+ public static final PrioritisedThreadPool.ExecutorGroup PARALLEL_GEN_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
-+ public static final PrioritisedThreadPool.ExecutorGroup RADIUS_AWARE_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
-+ public static final PrioritisedThreadPool.ExecutorGroup LOAD_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
-+
-+ public static void adjustWorkerThreads(final int configWorkerThreads, final int configIoThreads) {
- int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2;
- if (defaultWorkerThreads <= 4) {
- defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2;
- } else {
- defaultWorkerThreads = defaultWorkerThreads / 2;
- }
-- defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); // Paper
-+ defaultWorkerThreads = Integer.getInteger(PlatformHooks.get().getBrand() + ".WorkerThreadCount", Integer.valueOf(defaultWorkerThreads));
-
-- int workerThreads = chunkSystem.workerThreads; // Paper
-+ int workerThreads = configWorkerThreads;
-
- if (workerThreads <= 0) {
- workerThreads = defaultWorkerThreads;
- }
-
-- WORKER_POOL = new PrioritisedThreadPool(
-- "Paper Worker Pool", workerThreads, // Paper
-- (final Thread thread, final Integer id) -> {
-- thread.setName("Paper Common Worker #" + id.intValue()); // Paper
-+ final int ioThreads = Math.max(1, configIoThreads);
-+
-+ WORKER_POOL.adjustThreadCount(workerThreads);
-+ IO_POOL.adjustThreadCount(ioThreads);
-+
-+ LOGGER.info(PlatformHooks.get().getBrand() + " is using " + workerThreads + " worker threads, " + ioThreads + " I/O threads");
-+ }
-+
-+ public static final PrioritisedThreadPool IO_POOL = new PrioritisedThreadPool(
-+ new Consumer<>() {
-+ private final AtomicInteger idGenerator = new AtomicInteger();
-+
-+ @Override
-+ public void accept(final Thread thread) {
-+ thread.setDaemon(true);
-+ thread.setName(PlatformHooks.get().getBrand() + " I/O Worker #" + this.idGenerator.getAndIncrement());
- thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
- @Override
- public void uncaughtException(final Thread thread, final Throwable throwable) {
- LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
- }
- });
-- }, (long)(20.0e6)); // 20ms
-- WORKER_THREADS = workerThreads;
-+ }
-+ }
-+ );
-+ public static final long IO_QUEUE_HOLD_TIME = (long)(100.0e6); // 100ms
-+ public static final PrioritisedThreadPool.ExecutorGroup CLIENT_PROFILER_IO_GROUP = IO_POOL.createExecutorGroup(CLIENT_DIVISION, 0);
-+ public static final PrioritisedThreadPool.ExecutorGroup SERVER_REGION_IO_GROUP = IO_POOL.createExecutorGroup(SERVER_DIVISION, 0);
-+
-+ public static void haltExecutors() {
-+ MoonriseCommon.WORKER_POOL.shutdown(false);
-+ LOGGER.info("Awaiting termination of worker pool for up to 60s...");
-+ if (!MoonriseCommon.WORKER_POOL.join(TimeUnit.SECONDS.toMillis(60L))) {
-+ LOGGER.error("Worker pool did not shut down in time!");
-+ MoonriseCommon.WORKER_POOL.halt(false);
-+ }
-+
-+ MoonriseCommon.IO_POOL.shutdown(false);
-+ LOGGER.info("Awaiting termination of I/O pool for up to 60s...");
-+ if (!MoonriseCommon.IO_POOL.join(TimeUnit.SECONDS.toMillis(60L))) {
-+ LOGGER.error("I/O pool did not shut down in time!");
-+ MoonriseCommon.IO_POOL.halt(false);
-+ }
- }
-
- private MoonriseCommon() {}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
-index 1cf32d7d1bbc8a0a3f7cb9024c793f6744199f64..559c959aff3c9deef867b9e425fba3e2e669cac6 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
-@@ -1,8 +1,10 @@
- package ca.spottedleaf.moonrise.common.util;
-
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+
- public final class MoonriseConstants {
-
-- public static final int MAX_VIEW_DISTANCE = 32;
-+ public static final int MAX_VIEW_DISTANCE = Integer.getInteger(PlatformHooks.get().getBrand() + ".MaxViewDistance", 32);
-
- private MoonriseConstants() {}
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a9ff1c1a70faf4b7a64b265932f07a8b8f00c1ff
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java
-@@ -0,0 +1,52 @@
-+package ca.spottedleaf.moonrise.common.util;
-+
-+import net.minecraft.world.level.levelgen.LegacyRandomSource;
-+
-+/**
-+ * Avoid costly CAS of superclass
-+ */
-+public final class SimpleRandom extends LegacyRandomSource {
-+
-+ private static final long MULTIPLIER = 25214903917L;
-+ private static final long ADDEND = 11L;
-+ private static final int BITS = 48;
-+ private static final long MASK = (1L << BITS) - 1;
-+
-+ private long value;
-+
-+ public SimpleRandom(final long seed) {
-+ super(0L);
-+ this.value = seed;
-+ }
-+
-+ @Override
-+ public void setSeed(final long seed) {
-+ this.value = (seed ^ MULTIPLIER) & MASK;
-+ }
-+
-+ private long advanceSeed() {
-+ return this.value = ((this.value * MULTIPLIER) + ADDEND) & MASK;
-+ }
-+
-+ @Override
-+ public int next(final int bits) {
-+ return (int)(this.advanceSeed() >>> (BITS - bits));
-+ }
-+
-+ @Override
-+ public int nextInt() {
-+ final long seed = this.advanceSeed();
-+ return (int)(seed >>> (BITS - Integer.SIZE));
-+ }
-+
-+ @Override
-+ public int nextInt(final int bound) {
-+ if (bound <= 0) {
-+ throw new IllegalArgumentException();
-+ }
-+
-+ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
-+ final long value = this.advanceSeed() >>> (BITS - Integer.SIZE);
-+ return (int)((value * (long)bound) >>> Integer.SIZE);
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
-index 11b7f15755dde766140c29bedca456c80d53293f..217d1f908a36a5177ba3cbb80a33f73d4dab0fa0 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
-@@ -77,11 +77,15 @@ public class TickThread extends Thread {
- }
-
- public TickThread(final Runnable run, final String name) {
-- this(run, name, ID_GENERATOR.incrementAndGet());
-+ this(null, run, name);
- }
-
-- private TickThread(final Runnable run, final String name, final int id) {
-- super(run, name);
-+ public TickThread(final ThreadGroup group, final Runnable run, final String name) {
-+ this(group, run, name, ID_GENERATOR.incrementAndGet());
-+ }
-+
-+ private TickThread(final ThreadGroup group, final Runnable run, final String name, final int id) {
-+ super(group, run, name);
- this.id = id;
- }
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
-index af9623240ff2d389aa7090623f507720e7dbab7d..efda2688ae1254a82ba7f6bf8bf597ef224cbb86 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
-@@ -8,11 +8,19 @@ public final class WorldUtil {
- // min, max are inclusive
-
- public static int getMaxSection(final LevelHeightAccessor world) {
-- return world.getMaxSection() - 1; // getMaxSection() is exclusive
-+ return world.getMaxSectionY();
-+ }
-+
-+ public static int getMaxSection(final Level world) {
-+ return world.getMaxSectionY();
- }
-
- public static int getMinSection(final LevelHeightAccessor world) {
-- return world.getMinSection();
-+ return world.getMinSectionY();
-+ }
-+
-+ public static int getMinSection(final Level world) {
-+ return world.getMinSectionY();
- }
-
- public static int getMaxLightSection(final LevelHeightAccessor world) {
-diff --git a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..6f2dc0900dbf13a02410682eecda56cea4481346
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-@@ -0,0 +1,199 @@
-+package ca.spottedleaf.moonrise.paper;
-+
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
-+import com.mojang.datafixers.DataFixer;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.GenerationChunkHolder;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.util.datafix.DataFixTypes;
-+import net.minecraft.world.entity.Entity;
-+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.LevelChunk;
-+import net.minecraft.world.level.chunk.ProtoChunk;
-+import net.minecraft.world.level.chunk.storage.SerializableChunkData;
-+import net.minecraft.world.level.entity.EntityTypeTest;
-+import net.minecraft.world.phys.AABB;
-+import java.util.List;
-+import java.util.function.Predicate;
-+
-+public final class PaperHooks implements PlatformHooks {
-+
-+ @Override
-+ public String getBrand() {
-+ return "Paper";
-+ }
-+
-+ @Override
-+ public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos) {
-+ return blockState.getLightEmission();
-+ }
-+
-+ @Override
-+ public Predicate<BlockState> maybeHasLightEmission() {
-+ return (final BlockState state) -> {
-+ return state.getLightEmission() != 0;
-+ };
-+ }
-+
-+ @Override
-+ public boolean hasCurrentlyLoadingChunk() {
-+ return false;
-+ }
-+
-+ @Override
-+ public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder) {
-+ return null;
-+ }
-+
-+ @Override
-+ public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk) {
-+
-+ }
-+
-+ @Override
-+ public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original) {
-+
-+ }
-+
-+ @Override
-+ public boolean allowAsyncTicketUpdates() {
-+ return true;
-+ }
-+
-+ @Override
-+ public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel) {
-+
-+ }
-+
-+ @Override
-+ public void chunkUnloadFromWorld(final LevelChunk chunk) {
-+
-+ }
-+
-+ @Override
-+ public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data) {
-+
-+ }
-+
-+ @Override
-+ public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player) {
-+
-+ }
-+
-+ @Override
-+ public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player) {
-+
-+ }
-+
-+ @Override
-+ public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate, final List<Entity> into) {
-+
-+ }
-+
-+ @Override
-+ public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest, final AABB boundingBox, final Predicate<? super T> predicate, final List<? super T> into, final int maxCount) {
-+
-+ }
-+
-+ @Override
-+ public void entityMove(final Entity entity, final long oldSection, final long newSection) {
-+
-+ }
-+
-+ @Override
-+ public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event) {
-+ return true;
-+ }
-+
-+ @Override
-+ public boolean configFixMC224294() {
-+ return true;
-+ }
-+
-+ @Override
-+ public boolean configAutoConfigSendDistance() {
-+
-+ }
-+
-+ @Override
-+ public double configPlayerMaxLoadRate() {
-+
-+ }
-+
-+ @Override
-+ public double configPlayerMaxGenRate() {
-+
-+ }
-+
-+ @Override
-+ public double configPlayerMaxSendRate() {
-+
-+ }
-+
-+ @Override
-+ public int configPlayerMaxConcurrentLoads() {
-+
-+ }
-+
-+ @Override
-+ public int configPlayerMaxConcurrentGens() {
-+
-+ }
-+
-+ @Override
-+ public long configAutoSaveInterval() {
-+
-+ }
-+
-+ @Override
-+ public int configMaxAutoSavePerTick() {
-+
-+ }
-+
-+ @Override
-+ public boolean configFixMC159283() {
-+ return true;
-+ }
-+
-+ @Override
-+ public boolean forceNoSave(final ChunkAccess chunk) {
-+ return chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave;
-+ }
-+
-+ @Override
-+ public CompoundTag convertNBT(final DataFixTypes type, final DataFixer dataFixer, final CompoundTag nbt, final int fromVersion, final int toVersion) {
-+ return type.update(dataFixer, nbt, fromVersion, toVersion);
-+ }
-+
-+ @Override
-+ public boolean hasMainChunkLoadHook() {
-+ return false;
-+ }
-+
-+ @Override
-+ public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData) {
-+
-+ }
-+
-+ @Override
-+ public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities) {
-+ return entities;
-+ }
-+
-+ @Override
-+ public void unloadEntity(final Entity entity) {
-+ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, org.bukkit.event.entity.EntityRemoveEvent.Cause.UNLOAD);
-+ }
-+
-+ @Override
-+ public int modifyEntityTrackingRange(final Entity entity, final int currentRange) {
-+ return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
-+ }
-+}
-diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-index 6baa313b8201ed23193d7885c85606b0899ade3c..3eb38271b6ca26099b2da04c2d969e32fd72b2af 100644
---- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-+++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-@@ -94,15 +94,13 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
- private boolean addEntity(T entity, boolean existing) {
- org.spigotmc.AsyncCatcher.catchOp("Entity add"); // Paper
- // Paper start - chunk system hooks
-- if (existing) {
-- // 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);
-- if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
-- // removed by callback
-- return false;
-- }
-+ // 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, existing, false);
-+ if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
-+ // removed by callback
-+ return false;
- }
- // Paper end - chunk system hooks
- if (!this.addEntityUuid(entity)) {
-diff --git a/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks b/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks
-new file mode 100644
-index 0000000000000000000000000000000000000000..e57c3ca79677b1dfe7cf3db36f0406de7ea5bd0a
---- /dev/null
-+++ b/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks
-@@ -0,0 +1 @@
-+ca.spottedleaf.moonrise.paper.PaperHooks
diff --git a/patches/server/0824-fixup-Paper-config-files.patch b/patches/server/0822-fixup-Paper-config-files.patch
index a5c6d251af..cdf7e80f80 100644
--- a/patches/server/0824-fixup-Paper-config-files.patch
+++ b/patches/server/0822-fixup-Paper-config-files.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] fixup! Paper config files
diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
-index 73e8a524925ed6f2580d3bd01616646fabafda78..4bfe7e987450afa433fcad1847f6130654769416 100644
+index 73e8a524925ed6f2580d3bd01616646fabafda78..61893f8216ddaedd899b573322f3ad0088074ac5 100644
--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
@@ -30,6 +30,45 @@ public class GlobalConfiguration extends ConfigurationPart {
@@ -81,7 +81,7 @@ index 73e8a524925ed6f2580d3bd01616646fabafda78..4bfe7e987450afa433fcad1847f61306
@PostProcess
private void postProcess() {
- //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this);
-+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(this);
++
}
}
diff --git a/patches/server/0823-fixup-MC-Utils.patch b/patches/server/0823-fixup-MC-Utils.patch
index ea91946273..54f6874194 100644
--- a/patches/server/0823-fixup-MC-Utils.patch
+++ b/patches/server/0823-fixup-MC-Utils.patch
@@ -1,94 +1,1257 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <[email protected]>
-Date: Wed, 23 Oct 2024 22:13:41 -0700
+Date: Mon, 21 Oct 2024 12:21:54 -0700
Subject: [PATCH] fixup! MC Utils
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
-index 3c5ed66328ccf94c4744a191a7c63562dd08158d..deb64f7ebebcf6de91ffe0542d6b449a4db64da0 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..deb64f7ebebcf6de91ffe0542d6b449a4db64da0
+--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
-@@ -1,5 +1,6 @@
- package ca.spottedleaf.moonrise.common;
-
+@@ -0,0 +1,117 @@
++package ca.spottedleaf.moonrise.common;
++
+import com.mojang.datafixers.DSL;
- import com.mojang.datafixers.DataFixer;
++import com.mojang.datafixers.DataFixer;
++import net.minecraft.core.BlockPos;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.GenerationChunkHolder;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++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.LevelChunk;
++import net.minecraft.world.level.chunk.ProtoChunk;
++import net.minecraft.world.level.chunk.storage.SerializableChunkData;
++import net.minecraft.world.level.entity.EntityTypeTest;
++import net.minecraft.world.phys.AABB;
++import java.util.List;
++import java.util.ServiceLoader;
++import java.util.function.Predicate;
++
++public interface PlatformHooks {
++ public static PlatformHooks get() {
++ return Holder.INSTANCE;
++ }
++
++ public String getBrand();
++
++ public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos);
++
++ public Predicate<BlockState> maybeHasLightEmission();
++
++ public boolean hasCurrentlyLoadingChunk();
++
++ public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder);
++
++ public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk);
++
++ public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original);
++
++ public boolean allowAsyncTicketUpdates();
++
++ public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel);
++
++ public void chunkUnloadFromWorld(final LevelChunk chunk);
++
++ public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data);
++
++ public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player);
++
++ public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player);
++
++ public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate,
++ final List<Entity> into);
++
++ public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest,
++ final AABB boundingBox, final Predicate<? super T> predicate,
++ final List<? super T> into, final int maxCount);
++
++ 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();
++
++ public double configPlayerMaxLoadRate();
++
++ public double configPlayerMaxGenRate();
++
++ public double configPlayerMaxSendRate();
++
++ public int configPlayerMaxConcurrentLoads();
++
++ public int configPlayerMaxConcurrentGens();
++
++ public long configAutoSaveInterval();
++
++ public int configMaxAutoSavePerTick();
++
++ public boolean configFixMC159283();
++
++ // support for CB chunk mustNotSave
++ public boolean forceNoSave(final ChunkAccess chunk);
++
++ public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
++ final int fromVersion, final int toVersion);
++
++ public boolean hasMainChunkLoadHook();
++
++ public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData);
++
++ public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities);
++
++ public void unloadEntity(final Entity entity);
++
++ public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk);
++
++ public int modifyEntityTrackingRange(final Entity entity, final int currentRange);
++
++ public static final class Holder {
++ private Holder() {
++ }
++
++ private static final PlatformHooks INSTANCE;
++
++ static {
++ INSTANCE = ServiceLoader.load(PlatformHooks.class, PlatformHooks.class.getClassLoader()).findFirst()
++ .orElseThrow(() -> new RuntimeException("Failed to locate PlatformHooks"));
++ }
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
+index ba68998f6ef57b24c72fd833bd7de440de9501cc..7fed43a1e7bcf35c4d7fd3224837a47fedd59860 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
+@@ -13,15 +13,15 @@ import java.util.NoSuchElementException;
+ */
+ public final class EntityList implements Iterable<Entity> {
+
+- protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
++ private final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
+ {
+ this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE);
+ }
+
+- protected static final Entity[] EMPTY_LIST = new Entity[0];
++ private static final Entity[] EMPTY_LIST = new Entity[0];
+
+- protected Entity[] entities = EMPTY_LIST;
+- protected int count;
++ private Entity[] entities = EMPTY_LIST;
++ private int count;
+
+ public int size() {
+ return this.count;
+@@ -94,10 +94,9 @@ public final class EntityList implements Iterable<Entity> {
+
+ @Override
+ public Iterator<Entity> iterator() {
+- return new Iterator<Entity>() {
+-
+- Entity lastRet;
+- int current;
++ return new Iterator<>() {
++ private Entity lastRet;
++ private int current;
+
+ @Override
+ public boolean hasNext() {
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
+deleted file mode 100644
+index fcfbca333234c09f7c056bbfcd9ac8860b20a8db..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
++++ /dev/null
+@@ -1,125 +0,0 @@
+-package ca.spottedleaf.moonrise.common.list;
+-
+-import it.unimi.dsi.fastutil.longs.LongIterator;
+-import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap;
+-import java.util.Arrays;
+-import net.minecraft.world.level.block.Block;
+-import net.minecraft.world.level.block.state.BlockState;
+-import net.minecraft.world.level.chunk.GlobalPalette;
+-
+-public final class IBlockDataList {
+-
+- private static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
+-
+- // map of location -> (index | (location << 16) | (palette id << 32))
+- private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f);
+- {
+- this.map.defaultReturnValue(Long.MAX_VALUE);
+- }
+-
+- private static final long[] EMPTY_LIST = new long[0];
+-
+- private long[] byIndex = EMPTY_LIST;
+- private int size;
+-
+- public static int getLocationKey(final int x, final int y, final int z) {
+- return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4));
+- }
+-
+- public static BlockState getBlockDataFromRaw(final long raw) {
+- return GLOBAL_PALETTE.valueFor((int)(raw >>> 32));
+- }
+-
+- public static int getIndexFromRaw(final long raw) {
+- return (int)(raw & 0xFFFF);
+- }
+-
+- public static int getLocationFromRaw(final long raw) {
+- return (int)((raw >>> 16) & 0xFFFF);
+- }
+-
+- public static long getRawFromValues(final int index, final int location, final BlockState data) {
+- return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32);
+- }
+-
+- public static long setIndexRawValues(final long value, final int index) {
+- return value & ~(0xFFFF) | (index);
+- }
+-
+- public long add(final int x, final int y, final int z, final BlockState data) {
+- return this.add(getLocationKey(x, y, z), data);
+- }
+-
+- public long add(final int location, final BlockState data) {
+- final long curr = this.map.get((short)location);
+-
+- if (curr == Long.MAX_VALUE) {
+- final int index = this.size++;
+- final long raw = getRawFromValues(index, location, data);
+- this.map.put((short)location, raw);
+-
+- if (index >= this.byIndex.length) {
+- this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L));
+- }
+-
+- this.byIndex[index] = raw;
+- return raw;
+- } else {
+- final int index = getIndexFromRaw(curr);
+- final long raw = this.byIndex[index] = getRawFromValues(index, location, data);
+-
+- this.map.put((short)location, raw);
+-
+- return raw;
+- }
+- }
+-
+- public long remove(final int x, final int y, final int z) {
+- return this.remove(getLocationKey(x, y, z));
+- }
+-
+- public long remove(final int location) {
+- final long ret = this.map.remove((short)location);
+- final int index = getIndexFromRaw(ret);
+- if (ret == Long.MAX_VALUE) {
+- return ret;
+- }
+-
+- // move the entry at the end to this index
+- final int endIndex = --this.size;
+- final long end = this.byIndex[endIndex];
+- if (index != endIndex) {
+- // not empty after this call
+- this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index));
+- }
+- this.byIndex[index] = end;
+- this.byIndex[endIndex] = 0L;
+-
+- return ret;
+- }
+-
+- public int size() {
+- return this.size;
+- }
+-
+- public long getRaw(final int index) {
+- return this.byIndex[index];
+- }
+-
+- public int getLocation(final int index) {
+- return getLocationFromRaw(this.getRaw(index));
+- }
+-
+- public BlockState getData(final int index) {
+- return getBlockDataFromRaw(this.getRaw(index));
+- }
+-
+- public void clear() {
+- this.size = 0;
+- this.map.clear();
+- }
+-
+- public LongIterator getRawIterator() {
+- return this.map.values().iterator();
+- }
+-}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..9f3b25bb2439f283f878db93973a02fcdcd14eed
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
++import java.util.Arrays;
++
++public final class IntList {
++
++ private final Int2IntOpenHashMap map = new Int2IntOpenHashMap();
++ {
++ this.map.defaultReturnValue(Integer.MIN_VALUE);
++ }
++
++ private static final int[] EMPTY_LIST = new int[0];
++
++ private int[] byIndex = EMPTY_LIST;
++ private int count;
++
++ public int size() {
++ return this.count;
++ }
++
++ public void setMinCapacity(final int len) {
++ final int[] byIndex = this.byIndex;
++ if (byIndex.length < len) {
++ this.byIndex = Arrays.copyOf(byIndex, len);
++ }
++ }
++
++ public int getRaw(final int index) {
++ return this.byIndex[index];
++ }
++
++ public boolean add(final int value) {
++ final int count = this.count;
++ final int currIndex = this.map.putIfAbsent(value, count);
++
++ if (currIndex != Integer.MIN_VALUE) {
++ return false; // already in this list
++ }
++
++ int[] list = this.byIndex;
++
++ if (list.length == count) {
++ // resize required
++ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ }
++
++ list[count] = value;
++ this.count = count + 1;
++
++ return true;
++ }
++
++ public boolean remove(final int value) {
++ final int index = this.map.remove(value);
++ if (index == Integer.MIN_VALUE) {
++ return false;
++ }
++
++ // move the entry at the end to this index
++ final int endIndex = --this.count;
++ final int end = this.byIndex[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.map.put(end, index);
++ }
++ this.byIndex[index] = end;
++ this.byIndex[endIndex] = 0;
++
++ return true;
++ }
++
++ public void clear() {
++ this.count = 0;
++ this.map.clear();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2bae9949ef325d0001aa638150fbbdf968367e75
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java
+@@ -0,0 +1,77 @@
++package ca.spottedleaf.moonrise.common.list;
++
++import it.unimi.dsi.fastutil.shorts.Short2ShortOpenHashMap;
++import java.util.Arrays;
++
++public final class ShortList {
++
++ private final Short2ShortOpenHashMap map = new Short2ShortOpenHashMap();
++ {
++ this.map.defaultReturnValue(Short.MIN_VALUE);
++ }
++
++ private static final short[] EMPTY_LIST = new short[0];
++
++ private short[] byIndex = EMPTY_LIST;
++ private short count;
++
++ public int size() {
++ return (int)this.count;
++ }
++
++ public short getRaw(final int index) {
++ return this.byIndex[index];
++ }
++
++ public void setMinCapacity(final int len) {
++ final short[] byIndex = this.byIndex;
++ if (byIndex.length < len) {
++ this.byIndex = Arrays.copyOf(byIndex, len);
++ }
++ }
++
++ public boolean add(final short value) {
++ final int count = (int)this.count;
++ final short currIndex = this.map.putIfAbsent(value, (short)count);
++
++ if (currIndex != Short.MIN_VALUE) {
++ return false; // already in this list
++ }
++
++ short[] list = this.byIndex;
++
++ if (list.length == count) {
++ // resize required
++ list = this.byIndex = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
++ }
++
++ list[count] = value;
++ this.count = (short)(count + 1);
++
++ return true;
++ }
++
++ public boolean remove(final short value) {
++ final short index = this.map.remove(value);
++ if (index == Short.MIN_VALUE) {
++ return false;
++ }
++
++ // move the entry at the end to this index
++ final short endIndex = --this.count;
++ final short end = this.byIndex[endIndex];
++ if (index != endIndex) {
++ // not empty after this call
++ this.map.put(end, index);
++ }
++ this.byIndex[(int)index] = end;
++ this.byIndex[(int)endIndex] = (short)0;
++
++ return true;
++ }
++
++ public void clear() {
++ this.count = (short)0;
++ this.map.clear();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c2d917c2eac55b8a4411a6e159f177f9428b1150
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java
+@@ -0,0 +1,22 @@
++package ca.spottedleaf.moonrise.common.misc;
++
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import java.lang.invoke.VarHandle;
++
++public final class LazyRunnable implements Runnable {
++
++ private volatile Runnable toRun;
++ private static final VarHandle TO_RUN_HANDLE = ConcurrentUtil.getVarHandle(LazyRunnable.class, "toRun", Runnable.class);
++
++ public void setRunnable(final Runnable run) {
++ final Runnable prev = (Runnable)TO_RUN_HANDLE.compareAndExchange(this, (Runnable)null, run);
++ if (prev != null) {
++ throw new IllegalStateException("Runnable already set");
++ }
++ }
++
++ @Override
++ public void run() {
++ ((Runnable)TO_RUN_HANDLE.getVolatile(this)).run();
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+index ab093b0e8ac6f762921eb1d15f5217345c4eba05..bb44de17a37082e57f2292a4f470740be1d09b11 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+@@ -4,13 +4,17 @@ 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.nbt.CompoundTag;
-@@ -7,7 +8,6 @@ import net.minecraft.server.level.ChunkHolder;
- import net.minecraft.server.level.GenerationChunkHolder;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
--import net.minecraft.util.datafix.DataFixTypes;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.ChunkPos;
-@@ -88,7 +88,7 @@ public interface PlatformHooks {
- // support for CB chunk mustNotSave
- public boolean forceNoSave(final ChunkAccess chunk);
++import java.util.ArrayList;
-- public CompoundTag convertNBT(final DataFixTypes type, final DataFixer dataFixer, final CompoundTag nbt,
-+ public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
- final int fromVersion, final int toVersion);
+ public final class NearbyPlayers {
+
+@@ -20,7 +24,27 @@ public final class NearbyPlayers {
+ GENERAL_REALLY_SMALL,
+ TICK_VIEW_DISTANCE,
+ VIEW_DISTANCE,
+- SPAWN_RANGE, // Moonrise - chunk tick iteration
++ // 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) {
++
++ }
+ }
- public boolean hasMainChunkLoadHook();
-@@ -99,6 +99,8 @@ public interface PlatformHooks {
+ private static final NearbyMapType[] MAP_TYPES = NearbyMapType.values();
+@@ -37,6 +61,12 @@ public final class NearbyPlayers {
+ 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 void unloadEntity(final Entity entity);
+ public NearbyPlayers(final ServerLevel world) {
+ this.world = world;
+@@ -70,6 +100,16 @@ public final class NearbyPlayers {
+ }
+ }
-+ public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk);
++ 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) {
+@@ -94,38 +134,41 @@ public final class NearbyPlayers {
+ return this.byChunk.get(CoordinateUtils.getChunkKey(pos));
+ }
+
+- public ReferenceList<ServerPlayer> getPlayers(final BlockPos pos, final NearbyMapType type) {
+- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
++ public TrackedChunk getChunk(final int chunkX, final int chunkZ) {
++ return this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ }
+
+- return chunk == null ? null : chunk.players[type.ordinal()];
++ 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) {
+- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(pos));
+-
+- return chunk == null ? null : chunk.players[type.ordinal()];
++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(pos));
+ }
+
+ public ReferenceList<ServerPlayer> getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) {
+- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+-
+- return chunk == null ? null : chunk.players[type.ordinal()];
++ return this.directByChunk[type.ordinal()].get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+ }
+
+ public ReferenceList<ServerPlayer> getPlayersByBlock(final int blockX, final int blockZ, final NearbyMapType type) {
+- final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(blockX >> 4, blockZ >> 4));
+-
+- return chunk == null ? null : chunk.players[type.ordinal()];
++ 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;
+ }
+@@ -145,7 +188,9 @@ public final class NearbyPlayers {
+ final ReferenceList<ServerPlayer> list = this.players[idx];
+ if (list == null) {
+ ++this.nonEmptyLists;
+- (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY)).add(player);
++ final ReferenceList<ServerPlayer> players = (this.players[idx] = new ReferenceList<>(EMPTY_PLAYERS_ARRAY));
++ this.nearbyPlayers.directByChunk[idx].put(this.chunkKey, players);
++ players.add(player);
+ return;
+ }
+
+@@ -169,6 +214,7 @@ public final class NearbyPlayers {
+
+ if (list.size() == 0) {
+ this.players[idx] = null;
++ this.nearbyPlayers.directByChunk[idx].remove(this.chunkKey);
+ --this.nonEmptyLists;
+ }
+ }
+@@ -187,9 +233,19 @@ public final class NearbyPlayers {
+ protected void addCallback(final ServerPlayer parameter, final int chunkX, final int chunkZ) {
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+- NearbyPlayers.this.byChunk.computeIfAbsent(chunkKey, (final long keyInMap) -> {
+- return new TrackedChunk();
+- }).addPlayer(parameter, this.type);
++ 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
+@@ -201,10 +257,16 @@ public final class NearbyPlayers {
+ throw new IllegalStateException("Chunk should exist at " + new ChunkPos(chunkKey));
+ }
+
+- chunk.removePlayer(parameter, this.type);
++ 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/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+index da323a1105347d5cf4b946df10ded78a953236f2..94bba2b71918d79f54b3e28c35e76098ba0afd8c 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+@@ -1,6 +1,7 @@
+ package ca.spottedleaf.moonrise.common.util;
+
+-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++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;
+@@ -24,15 +25,15 @@ public final class ChunkSystem {
+ }
+
+ public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
+- scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
++ scheduleChunkTask(level, chunkX, chunkZ, run, Priority.NORMAL);
+ }
+
+- public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
++ 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 PrioritisedExecutor.Priority priority,
++ final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
+ final Consumer<ChunkAccess> onComplete) {
+ if (gen) {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+@@ -59,7 +60,7 @@ public final class ChunkSystem {
+
+ private static long chunkLoadCounter = 0L;
+ public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+- final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
++ final boolean addTicket, final Priority priority, final Consumer<ChunkAccess> onComplete) {
+ if (!org.bukkit.Bukkit.isPrimaryThread()) {
+ scheduleChunkTask(level, chunkX, chunkZ, () -> {
+ scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+@@ -113,13 +114,13 @@ public final class ChunkSystem {
+ }
+ loadCallback.accept(result.orElse(null));
+ }, (final Runnable r) -> {
+- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
++ 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 PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
++ 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");
+@@ -196,7 +197,7 @@ public final class ChunkSystem {
+ }
+ loadCallback.accept(result.orElse(null));
+ }, (final Runnable r) -> {
+- scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
++ scheduleChunkTask(level, chunkX, chunkZ, r, Priority.HIGHEST);
+ });
+ }
+
+@@ -220,7 +221,10 @@ public final class ChunkSystem {
+ return getUpdatingChunkHolderCount(level) != 0;
+ }
+
+- public static boolean screenEntity(final ServerLevel level, final Entity entity) {
++ 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;
+ }
+
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+index ac6f284ee4469d16c5655328b2488d7612832353..97848869df61648fc415e4d39f409f433202c274 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+@@ -3,8 +3,12 @@ package ca.spottedleaf.moonrise.common.util;
+ public final class MixinWorkarounds {
+
+ // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs
++ // https://github.com/FabricMC/Mixin/pull/147
+ public static long[] clone(final long[] values) {
+ return values.clone();
+ }
+
++ public static byte[] clone(final byte[] values) {
++ return values.clone();
++ }
+ }
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+index 3abe0bd2a820352b85306d554bf14a4cf6123091..c125c70a68130be373acc989053a6c0e487be924 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+@@ -1,45 +1,100 @@
+ package ca.spottedleaf.moonrise.common.util;
+
+-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
++import ca.spottedleaf.concurrentutil.executor.thread.PrioritisedThreadPool;
++import ca.spottedleaf.moonrise.common.PlatformHooks;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+-import java.io.File;
++import java.util.concurrent.TimeUnit;
++import java.util.concurrent.atomic.AtomicInteger;
++import java.util.function.Consumer;
+
+ public final class MoonriseCommon {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseCommon.class);
+
+- // Paper start
+- public static PrioritisedThreadPool WORKER_POOL;
+- public static int WORKER_THREADS;
+- public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
+- // Paper end
++ public static final PrioritisedThreadPool WORKER_POOL = new PrioritisedThreadPool(
++ new Consumer<>() {
++ private final AtomicInteger idGenerator = new AtomicInteger();
++
++ @Override
++ public void accept(Thread thread) {
++ thread.setDaemon(true);
++ thread.setName(PlatformHooks.get().getBrand() + " Common Worker #" + this.idGenerator.getAndIncrement());
++ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
++ @Override
++ public void uncaughtException(final Thread thread, final Throwable throwable) {
++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
++ }
++ });
++ }
++ }
++ );
++ public static final long WORKER_QUEUE_HOLD_TIME = (long)(20.0e6); // 20ms
++ public static final int CLIENT_DIVISION = 0;
++ public static final PrioritisedThreadPool.ExecutorGroup RENDER_EXECUTOR_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(CLIENT_DIVISION, 0);
++ public static final int SERVER_DIVISION = 1;
++ public static final PrioritisedThreadPool.ExecutorGroup PARALLEL_GEN_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
++ public static final PrioritisedThreadPool.ExecutorGroup RADIUS_AWARE_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
++ public static final PrioritisedThreadPool.ExecutorGroup LOAD_GROUP = MoonriseCommon.WORKER_POOL.createExecutorGroup(SERVER_DIVISION, 0);
++
++ public static void adjustWorkerThreads(final int configWorkerThreads, final int configIoThreads) {
+ int defaultWorkerThreads = Runtime.getRuntime().availableProcessors() / 2;
+ if (defaultWorkerThreads <= 4) {
+ defaultWorkerThreads = defaultWorkerThreads <= 3 ? 1 : 2;
+ } else {
+ defaultWorkerThreads = defaultWorkerThreads / 2;
+ }
+- defaultWorkerThreads = Integer.getInteger("Paper.WorkerThreadCount", Integer.valueOf(defaultWorkerThreads)); // Paper
++ defaultWorkerThreads = Integer.getInteger(PlatformHooks.get().getBrand() + ".WorkerThreadCount", Integer.valueOf(defaultWorkerThreads));
+
+- int workerThreads = chunkSystem.workerThreads; // Paper
++ int workerThreads = configWorkerThreads;
+
+ if (workerThreads <= 0) {
+ workerThreads = defaultWorkerThreads;
+ }
+
+- WORKER_POOL = new PrioritisedThreadPool(
+- "Paper Worker Pool", workerThreads, // Paper
+- (final Thread thread, final Integer id) -> {
+- thread.setName("Paper Common Worker #" + id.intValue()); // Paper
++ final int ioThreads = Math.max(1, configIoThreads);
++
++ WORKER_POOL.adjustThreadCount(workerThreads);
++ IO_POOL.adjustThreadCount(ioThreads);
++
++ LOGGER.info(PlatformHooks.get().getBrand() + " is using " + workerThreads + " worker threads, " + ioThreads + " I/O threads");
++ }
++
++ public static final PrioritisedThreadPool IO_POOL = new PrioritisedThreadPool(
++ new Consumer<>() {
++ private final AtomicInteger idGenerator = new AtomicInteger();
++
++ @Override
++ public void accept(final Thread thread) {
++ thread.setDaemon(true);
++ thread.setName(PlatformHooks.get().getBrand() + " I/O Worker #" + this.idGenerator.getAndIncrement());
+ thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(final Thread thread, final Throwable throwable) {
+ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
+ }
+ });
+- }, (long)(20.0e6)); // 20ms
+- WORKER_THREADS = workerThreads;
++ }
++ }
++ );
++ public static final long IO_QUEUE_HOLD_TIME = (long)(100.0e6); // 100ms
++ public static final PrioritisedThreadPool.ExecutorGroup CLIENT_PROFILER_IO_GROUP = IO_POOL.createExecutorGroup(CLIENT_DIVISION, 0);
++ public static final PrioritisedThreadPool.ExecutorGroup SERVER_REGION_IO_GROUP = IO_POOL.createExecutorGroup(SERVER_DIVISION, 0);
++
++ public static void haltExecutors() {
++ MoonriseCommon.WORKER_POOL.shutdown(false);
++ LOGGER.info("Awaiting termination of worker pool for up to 60s...");
++ if (!MoonriseCommon.WORKER_POOL.join(TimeUnit.SECONDS.toMillis(60L))) {
++ LOGGER.error("Worker pool did not shut down in time!");
++ MoonriseCommon.WORKER_POOL.halt(false);
++ }
+
- public int modifyEntityTrackingRange(final Entity entity, final int currentRange);
++ MoonriseCommon.IO_POOL.shutdown(false);
++ LOGGER.info("Awaiting termination of I/O pool for up to 60s...");
++ if (!MoonriseCommon.IO_POOL.join(TimeUnit.SECONDS.toMillis(60L))) {
++ LOGGER.error("I/O pool did not shut down in time!");
++ MoonriseCommon.IO_POOL.halt(false);
++ }
+ }
+
+ private MoonriseCommon() {}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+index 1cf32d7d1bbc8a0a3f7cb9024c793f6744199f64..559c959aff3c9deef867b9e425fba3e2e669cac6 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+@@ -1,8 +1,10 @@
+ package ca.spottedleaf.moonrise.common.util;
+
++import ca.spottedleaf.moonrise.common.PlatformHooks;
++
+ public final class MoonriseConstants {
+
+- public static final int MAX_VIEW_DISTANCE = 32;
++ public static final int MAX_VIEW_DISTANCE = Integer.getInteger(PlatformHooks.get().getBrand() + ".MaxViewDistance", 32);
+
+ private MoonriseConstants() {}
- public static final class Holder {
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a9ff1c1a70faf4b7a64b265932f07a8b8f00c1ff
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java
+@@ -0,0 +1,52 @@
++package ca.spottedleaf.moonrise.common.util;
++
++import net.minecraft.world.level.levelgen.LegacyRandomSource;
++
++/**
++ * Avoid costly CAS of superclass
++ */
++public final class SimpleRandom extends LegacyRandomSource {
++
++ private static final long MULTIPLIER = 25214903917L;
++ private static final long ADDEND = 11L;
++ private static final int BITS = 48;
++ private static final long MASK = (1L << BITS) - 1;
++
++ private long value;
++
++ public SimpleRandom(final long seed) {
++ super(0L);
++ this.value = seed;
++ }
++
++ @Override
++ public void setSeed(final long seed) {
++ this.value = (seed ^ MULTIPLIER) & MASK;
++ }
++
++ private long advanceSeed() {
++ return this.value = ((this.value * MULTIPLIER) + ADDEND) & MASK;
++ }
++
++ @Override
++ public int next(final int bits) {
++ return (int)(this.advanceSeed() >>> (BITS - bits));
++ }
++
++ @Override
++ public int nextInt() {
++ final long seed = this.advanceSeed();
++ return (int)(seed >>> (BITS - Integer.SIZE));
++ }
++
++ @Override
++ public int nextInt(final int bound) {
++ if (bound <= 0) {
++ throw new IllegalArgumentException();
++ }
++
++ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
++ final long value = this.advanceSeed() >>> (BITS - Integer.SIZE);
++ return (int)((value * (long)bound) >>> Integer.SIZE);
++ }
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+index 11b7f15755dde766140c29bedca456c80d53293f..217d1f908a36a5177ba3cbb80a33f73d4dab0fa0 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+@@ -77,11 +77,15 @@ public class TickThread extends Thread {
+ }
+
+ public TickThread(final Runnable run, final String name) {
+- this(run, name, ID_GENERATOR.incrementAndGet());
++ this(null, run, name);
+ }
+
+- private TickThread(final Runnable run, final String name, final int id) {
+- super(run, name);
++ public TickThread(final ThreadGroup group, final Runnable run, final String name) {
++ this(group, run, name, ID_GENERATOR.incrementAndGet());
++ }
++
++ private TickThread(final ThreadGroup group, final Runnable run, final String name, final int id) {
++ super(group, run, name);
+ this.id = id;
+ }
+
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+index af9623240ff2d389aa7090623f507720e7dbab7d..efda2688ae1254a82ba7f6bf8bf597ef224cbb86 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+@@ -8,11 +8,19 @@ public final class WorldUtil {
+ // min, max are inclusive
+
+ public static int getMaxSection(final LevelHeightAccessor world) {
+- return world.getMaxSection() - 1; // getMaxSection() is exclusive
++ return world.getMaxSectionY();
++ }
++
++ public static int getMaxSection(final Level world) {
++ return world.getMaxSectionY();
+ }
+
+ public static int getMinSection(final LevelHeightAccessor world) {
+- return world.getMinSection();
++ return world.getMinSectionY();
++ }
++
++ public static int getMinSection(final Level world) {
++ return world.getMinSectionY();
+ }
+
+ public static int getMaxLightSection(final LevelHeightAccessor world) {
diff --git a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-index 6f2dc0900dbf13a02410682eecda56cea4481346..ee514a767f69de69d86e1e88d70fe37c4ab84277 100644
---- a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..ee514a767f69de69d86e1e88d70fe37c4ab84277
+--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java
-@@ -1,14 +1,16 @@
- package ca.spottedleaf.moonrise.paper;
-
- import ca.spottedleaf.moonrise.common.PlatformHooks;
+@@ -0,0 +1,209 @@
++package ca.spottedleaf.moonrise.paper;
++
++import ca.spottedleaf.moonrise.common.PlatformHooks;
+import com.mojang.datafixers.DSL;
- import com.mojang.datafixers.DataFixer;
++import com.mojang.datafixers.DataFixer;
+import com.mojang.serialization.Dynamic;
- import net.minecraft.core.BlockPos;
- import net.minecraft.nbt.CompoundTag;
++import net.minecraft.core.BlockPos;
++import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtOps;
- import net.minecraft.server.level.ChunkHolder;
- import net.minecraft.server.level.GenerationChunkHolder;
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.server.level.ServerPlayer;
--import net.minecraft.util.datafix.DataFixTypes;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.level.BlockGetter;
- import net.minecraft.world.level.ChunkPos;
-@@ -168,8 +170,11 @@ public final class PaperHooks implements PlatformHooks {
- }
-
- @Override
-- public CompoundTag convertNBT(final DataFixTypes type, final DataFixer dataFixer, final CompoundTag nbt, final int fromVersion, final int toVersion) {
-- return type.update(dataFixer, nbt, fromVersion, toVersion);
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.GenerationChunkHolder;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++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.LevelChunk;
++import net.minecraft.world.level.chunk.ProtoChunk;
++import net.minecraft.world.level.chunk.storage.SerializableChunkData;
++import net.minecraft.world.level.entity.EntityTypeTest;
++import net.minecraft.world.phys.AABB;
++import java.util.List;
++import java.util.function.Predicate;
++
++public final class PaperHooks implements PlatformHooks {
++
++ @Override
++ public String getBrand() {
++ return "Paper";
++ }
++
++ @Override
++ public int getLightEmission(final BlockState blockState, final BlockGetter world, final BlockPos pos) {
++ return blockState.getLightEmission();
++ }
++
++ @Override
++ public Predicate<BlockState> maybeHasLightEmission() {
++ return (final BlockState state) -> {
++ return state.getLightEmission() != 0;
++ };
++ }
++
++ @Override
++ public boolean hasCurrentlyLoadingChunk() {
++ return false;
++ }
++
++ @Override
++ public LevelChunk getCurrentlyLoadingChunk(final GenerationChunkHolder holder) {
++ return null;
++ }
++
++ @Override
++ public void setCurrentlyLoading(final GenerationChunkHolder holder, final LevelChunk levelChunk) {
++
++ }
++
++ @Override
++ public void chunkFullStatusComplete(final LevelChunk newChunk, final ProtoChunk original) {
++
++ }
++
++ @Override
++ public boolean allowAsyncTicketUpdates() {
++ return true;
++ }
++
++ @Override
++ public void onChunkHolderTicketChange(final ServerLevel world, final ChunkHolder holder, final int oldLevel, final int newLevel) {
++
++ }
++
++ @Override
++ public void chunkUnloadFromWorld(final LevelChunk chunk) {
++
++ }
++
++ @Override
++ public void chunkSyncSave(final ServerLevel world, final ChunkAccess chunk, final SerializableChunkData data) {
++
++ }
++
++ @Override
++ public void onChunkWatch(final ServerLevel world, final LevelChunk chunk, final ServerPlayer player) {
++
++ }
++
++ @Override
++ public void onChunkUnWatch(final ServerLevel world, final ChunkPos chunk, final ServerPlayer player) {
++
++ }
++
++ @Override
++ public void addToGetEntities(final Level world, final Entity entity, final AABB boundingBox, final Predicate<? super Entity> predicate, final List<Entity> into) {
++
++ }
++
++ @Override
++ public <T extends Entity> void addToGetEntities(final Level world, final EntityTypeTest<Entity, T> entityTypeTest, final AABB boundingBox, final Predicate<? super T> predicate, final List<? super T> into, final int maxCount) {
++
++ }
++
++ @Override
++ public void entityMove(final Entity entity, final long oldSection, final long newSection) {
++
++ }
++
++ @Override
++ public boolean screenEntity(final ServerLevel world, final Entity entity, final boolean fromDisk, final boolean event) {
++ return true;
++ }
++
++ @Override
++ public boolean configFixMC224294() {
++ return true;
++ }
++
++ @Override
++ public boolean configAutoConfigSendDistance() {
++
++ }
++
++ @Override
++ public double configPlayerMaxLoadRate() {
++
++ }
++
++ @Override
++ public double configPlayerMaxGenRate() {
++
++ }
++
++ @Override
++ public double configPlayerMaxSendRate() {
++
++ }
++
++ @Override
++ public int configPlayerMaxConcurrentLoads() {
++
++ }
++
++ @Override
++ public int configPlayerMaxConcurrentGens() {
++
++ }
++
++ @Override
++ public long configAutoSaveInterval() {
++
++ }
++
++ @Override
++ public int configMaxAutoSavePerTick() {
++
++ }
++
++ @Override
++ public boolean configFixMC159283() {
++ return true;
++ }
++
++ @Override
++ public boolean forceNoSave(final ChunkAccess chunk) {
++ return chunk instanceof LevelChunk levelChunk && levelChunk.mustNotSave;
++ }
++
++ @Override
+ public CompoundTag convertNBT(final DSL.TypeReference type, final DataFixer dataFixer, final CompoundTag nbt,
+ final int fromVersion, final int toVersion) {
+ return (CompoundTag)dataFixer.update(
+ type, new Dynamic<>(NbtOps.INSTANCE, nbt), fromVersion, toVersion
+ ).getValue();
- }
-
- @Override
-@@ -192,6 +197,11 @@ public final class PaperHooks implements PlatformHooks {
- entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, org.bukkit.event.entity.EntityRemoveEvent.Cause.UNLOAD);
- }
-
++ }
++
++ @Override
++ public boolean hasMainChunkLoadHook() {
++ return false;
++ }
++
++ @Override
++ public void mainChunkLoad(final ChunkAccess chunk, final SerializableChunkData chunkData) {
++
++ }
++
++ @Override
++ public List<Entity> modifySavedEntities(final ServerLevel world, final int chunkX, final int chunkZ, final List<Entity> entities) {
++ return entities;
++ }
++
++ @Override
++ public void unloadEntity(final Entity entity) {
++ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, org.bukkit.event.entity.EntityRemoveEvent.Cause.UNLOAD);
++ }
++
+ @Override
+ public void postLoadProtoChunk(final ServerLevel world, final ProtoChunk chunk) {
+ net.minecraft.world.level.chunk.status.ChunkStatusTasks.postLoadProtoChunk(world, chunk.getEntities());
+ }
+
- @Override
- public int modifyEntityTrackingRange(final Entity entity, final int currentRange) {
- return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
++ @Override
++ public int modifyEntityTrackingRange(final Entity entity, final int currentRange) {
++ return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
++ }
++}
+diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+index 61893f8216ddaedd899b573322f3ad0088074ac5..36b96e0ed5c0d25068ec4678eddd8a19a020d345 100644
+--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+@@ -242,7 +242,7 @@ public class GlobalConfiguration extends ConfigurationPart {
+
+ @PostProcess
+ private void postProcess() {
+-
++ ca.spottedleaf.moonrise.common.util.MoonriseCommon.adjustWorkerThreads(this.workerThreads, this.ioThreads);
+ }
+ }
+
diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
index f18c2b85ed9541f646f157184221e333d0ae58bd..aff4c3d63a97d5bbde004a616f7e14fca59b5ab9 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java
@@ -103,15 +1266,36 @@ index f18c2b85ed9541f646f157184221e333d0ae58bd..aff4c3d63a97d5bbde004a616f7e14fc
// 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) -> {
diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-index 3eb38271b6ca26099b2da04c2d969e32fd72b2af..5aa74c00a61282830d82359eae2b114e2a48b6d9 100644
+index 6baa313b8201ed23193d7885c85606b0899ade3c..5aa74c00a61282830d82359eae2b114e2a48b6d9 100644
--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
-@@ -97,7 +97,7 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
- // 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, existing, false);
+@@ -94,15 +94,13 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
+ private boolean addEntity(T entity, boolean existing) {
+ org.spigotmc.AsyncCatcher.catchOp("Entity add"); // Paper
+ // Paper start - chunk system hooks
+- if (existing) {
+- // 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);
+- if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
+- // removed by callback
+- return false;
+- }
++ // 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, existing, true);
- if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
- // removed by callback
- return false;
++ if ((!wasRemoved && entityCasted.isRemoved()) || !screened) {
++ // removed by callback
++ return false;
+ }
+ // Paper end - chunk system hooks
+ if (!this.addEntityUuid(entity)) {
+diff --git a/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks b/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks
+new file mode 100644
+index 0000000000000000000000000000000000000000..e57c3ca79677b1dfe7cf3db36f0406de7ea5bd0a
+--- /dev/null
++++ b/src/main/resources/META-INF/services/ca.spottedleaf.moonrise.common.PlatformHooks
+@@ -0,0 +1 @@
++ca.spottedleaf.moonrise.paper.PaperHooks
diff --git a/patches/server/0825-Rewrite-dataconverter-system.patch b/patches/server/0824-Rewrite-dataconverter-system.patch
index 173d97a5b2..173d97a5b2 100644
--- a/patches/server/0825-Rewrite-dataconverter-system.patch
+++ b/patches/server/0824-Rewrite-dataconverter-system.patch
diff --git a/patches/server/0827-fixup-Optimize-BlockPosition-helper-methods.patch b/patches/server/0825-fixup-Optimize-BlockPosition-helper-methods.patch
index 5a70b45e95..5a70b45e95 100644
--- a/patches/server/0827-fixup-Optimize-BlockPosition-helper-methods.patch
+++ b/patches/server/0825-fixup-Optimize-BlockPosition-helper-methods.patch
diff --git a/patches/server/0826-Moonrise-optimisation-patches.patch b/patches/server/0826-Moonrise-optimisation-patches.patch
index 452ab610f1..59a89d7268 100644
--- a/patches/server/0826-Moonrise-optimisation-patches.patch
+++ b/patches/server/0826-Moonrise-optimisation-patches.patch
@@ -17,10 +17,10 @@ Currently includes:
See https://github.com/Tuinity/Moonrise
diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-index 94bba2b71918d79f54b3e28c35e76098ba0afd8c..b61611351bf23efc1e90bab8a850ebbe6ffdd516 100644
+index 94bba2b71918d79f54b3e28c35e76098ba0afd8c..fc029c8fb22a7c8eeb23bfc171812f6da91c60fa 100644
--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-@@ -2,6 +2,10 @@ package ca.spottedleaf.moonrise.common.util;
+@@ -2,11 +2,17 @@ package ca.spottedleaf.moonrise.common.util;
import ca.spottedleaf.concurrentutil.util.Priority;
import ca.spottedleaf.moonrise.common.PlatformHooks;
@@ -28,10 +28,17 @@ index 94bba2b71918d79f54b3e28c35e76098ba0afd8c..b61611351bf23efc1e90bab8a850ebbe
+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;
-@@ -18,203 +22,46 @@ import java.util.function.Consumer;
+ 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();
@@ -243,17 +250,26 @@ index 94bba2b71918d79f54b3e28c35e76098ba0afd8c..b61611351bf23efc1e90bab8a850ebbe
}
public static boolean hasAnyChunkHolders(final ServerLevel level) {
-@@ -236,52 +83,85 @@ public final class ChunkSystem {
-
+@@ -233,55 +82,96 @@ public final class ChunkSystem {
}
-- public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+ 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) {
+ public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+-
+ ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add(
+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+ );
@@ -278,10 +294,11 @@ index 94bba2b71918d79f54b3e28c35e76098ba0afd8c..b61611351bf23efc1e90bab8a850ebbe
+ ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+ );
+ if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
-+ chunk.postProcessGeneration();
++ 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) {
@@ -289,6 +306,7 @@ index 94bba2b71918d79f54b3e28c35e76098ba0afd8c..b61611351bf23efc1e90bab8a850ebbe
+ ((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) {
@@ -361,65 +379,318 @@ index be60439c43b887f0143e7713689fd2773066ba73..dc17aa5a0937c13d431e41779f241f2e
@Override
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
new file mode 100644
-index 0000000000000000000000000000000000000000..aef4fc0d3c272febe675d1ac846b88e58b4e7533
+index 0000000000000000000000000000000000000000..93bc56daec4526f373c84763b8c7ccb4a30e800b
--- /dev/null
+++ b/src/main/java/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.ints.IntArrayList;
++import it.unimi.dsi.fastutil.shorts.ShortArrayList;
+
+public interface BlockCountingBitStorage {
+
-+ public Int2ObjectOpenHashMap<IntArrayList> moonrise$countEntries();
++ public Int2ObjectOpenHashMap<ShortArrayList> moonrise$countEntries();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a08ddb0598d44368af5b6bace971ee31edf9919e
+index 0000000000000000000000000000000000000000..0d1443a113c07d7655e7b927a899447f70db8fa9
--- /dev/null
+++ b/src/main/java/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.IBlockDataList;
++import ca.spottedleaf.moonrise.common.list.ShortList;
+
+public interface BlockCountingChunkSection {
+
-+ public int moonrise$getSpecialCollidingBlocks();
++ public boolean moonrise$hasSpecialCollidingBlocks();
+
-+ public IBlockDataList moonrise$getTickingBlockList();
++ public ShortList moonrise$getTickingBlockList();
+
+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
new file mode 100644
-index 0000000000000000000000000000000000000000..08338917dc61c856eaba0b76e05c1497c458399d
+index 0000000000000000000000000000000000000000..89e75b454695e174c5619104eeb15eb923a2d9a7
--- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.chunk_getblock;
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
+@@ -0,0 +1,12 @@
++package ca.spottedleaf.moonrise.patches.blockstate_propertyaccess;
+
-+import net.minecraft.world.level.block.state.BlockState;
++public interface PropertyAccess<T> {
+
-+public interface GetBlockChunk {
++ public int moonrise$getId();
+
-+ public BlockState moonrise$getBlock(final int x, final int y, final int z);
++ public int moonrise$getIdFor(final T value);
++
++ public T moonrise$getById(final int id);
++
++ public void moonrise$setById(final T[] values);
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..01da52b9e8a786824f199a057b62ce0431ecbc43
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..b5335a2a8cb5dc7637c7112c8f7193389d726489
+--- /dev/null
++++ b/src/main/java/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.ReferenceOpenHashSet;
++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 ReferenceOpenHashSet<>(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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
new file mode 100644
-index 0000000000000000000000000000000000000000..49160a30b8e19e5c5ada811fbcae2a05959524f3
+index 0000000000000000000000000000000000000000..44bb25554634af2ec0b2e9b3d9231304d5dff034
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
-@@ -0,0 +1,38 @@
+@@ -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.DataFixTypes;
++import net.minecraft.util.datafix.fixes.References;
+
+public final class ChunkSystemConverters {
+
@@ -440,84 +711,26 @@ index 0000000000000000000000000000000000000000..49160a30b8e19e5c5ada811fbcae2a05
+ public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) {
+ final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION);
+
-+ return DataFixTypes.POI_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
++ 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 DataFixTypes.ENTITY_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
++ return PlatformHooks.get().convertNBT(References.ENTITY_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
+ }
+
+ private ChunkSystemConverters() {}
+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..67f6dd9a4855611cfe242c2e37e90f6d27d4c823
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
-@@ -0,0 +1,36 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system;
-+
-+import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+
-+public final class ChunkSystemFeatures {
-+
-+ public static boolean supportsAsyncChunkSave() {
-+ // uncertain how to properly pass AsyncSaveData to ChunkSerializer#write
-+ // additionally, there may be mods hooking into the write() call which may not be thread-safe to call
-+ return true;
-+ }
-+
-+ public static AsyncChunkSaveData getAsyncSaveData(final ServerLevel world, final ChunkAccess chunk) {
-+ return net.minecraft.world.level.chunk.storage.ChunkSerializer.getAsyncSaveData(world, chunk);
-+ }
-+
-+ public static CompoundTag saveChunkAsync(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData) {
-+ return net.minecraft.world.level.chunk.storage.ChunkSerializer.saveChunk(world, chunk, asyncSaveData);
-+ }
-+
-+ public static boolean forceNoSave(final ChunkAccess chunk) {
-+ // support for CB chunk mustNotSave
-+ return chunk instanceof net.minecraft.world.level.chunk.LevelChunk levelChunk && levelChunk.mustNotSave;
-+ }
-+
-+ public static boolean supportsAsyncChunkDeserialization() {
-+ // as it stands, the current problem with supporting this in Moonrise is that we are unsure that any mods
-+ // hooking into ChunkSerializer#read() are thread-safe to call
-+ return true;
-+ }
-+
-+ private ChunkSystemFeatures() {}
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..becd1c6d54ed6c912aee3a9178a970e2751d3694
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
-@@ -0,0 +1,11 @@
-+package ca.spottedleaf.moonrise.patches.chunk_system.async_save;
-+
-+import net.minecraft.nbt.ListTag;
-+import net.minecraft.nbt.Tag;
-+
-+public record AsyncChunkSaveData(
-+ Tag blockTickList, // non-null if we had to go to the server's tick list
-+ Tag fluidTickList, // non-null if we had to go to the server's tick list
-+ ListTag blockEntities,
-+ long worldTime
-+) {}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
new file mode 100644
-index 0000000000000000000000000000000000000000..2c279854bdf214538380fa354e4298ec4bd9ac4e
+index 0000000000000000000000000000000000000000..c7da23900228aab3a5673eb5adfada5091140319
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
-@@ -0,0 +1,39 @@
+@@ -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;
@@ -537,6 +750,10 @@ index 0000000000000000000000000000000000000000..2c279854bdf214538380fa354e4298ec
+
+ 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);
@@ -557,12 +774,13 @@ index 0000000000000000000000000000000000000000..2c279854bdf214538380fa354e4298ec
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
new file mode 100644
-index 0000000000000000000000000000000000000000..73df26b27146bbad2106d57b22dd3c792ed3dd1d
+index 0000000000000000000000000000000000000000..a814512fcfb85312474ae2c2c21443843bf57831
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
-@@ -0,0 +1,14 @@
+@@ -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;
+
@@ -574,63 +792,71 @@ index 0000000000000000000000000000000000000000..73df26b27146bbad2106d57b22dd3c79
+
+ 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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
new file mode 100644
-index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d8da6b215
+index 0000000000000000000000000000000000000000..99f6f3e58b11b8967e6f1c3391c190d9a860ab7f
--- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
-@@ -0,0 +1,1240 @@
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
+@@ -0,0 +1,1707 @@
+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.standard.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue;
++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.ChunkPos;
+import net.minecraft.world.level.chunk.storage.RegionFile;
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
++import java.io.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;
-+import java.util.function.Function;
+
-+/**
-+ * Prioritised RegionFile I/O executor, responsible for all RegionFile access.
-+ * <p>
-+ * All functions provided are MT-Safe, however certain ordering constraints are recommended:
-+ * <li>
-+ * Chunk saves may not occur for unloaded chunks.
-+ * </li>
-+ * <li>
-+ * Tasks must be scheduled on the chunk scheduler thread.
-+ * </li>
-+ * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems.
-+ * </p>
-+ */
-+public final class RegionFileIOThread extends PrioritisedQueueExecutorThread {
++public final class MoonriseRegionFileIO {
+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(RegionFileIOThread.class);
++ private static final int REGION_FILE_SHIFT = 5;
++ private static final Logger LOGGER = LoggerFactory.getLogger(MoonriseRegionFileIO.class);
+
+ /**
-+ * The kinds of region files controlled by the region file thread. Add more when needed, and ensure
-+ * getControllerFor is updated.
++ * The types of RegionFiles controlled by the I/O thread(s).
+ */
+ public static enum RegionFileType {
+ CHUNK_DATA,
@@ -638,9 +864,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ ENTITY_DATA;
+ }
+
-+ private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values();
-+
-+ public static ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) {
++ public static RegionDataController getControllerFor(final ServerLevel world, final RegionFileType type) {
+ switch (type) {
+ case CHUNK_DATA:
+ return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController();
@@ -653,8 +877,10 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ }
+ }
+
++ private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values();
++
+ /**
-+ * Collects regionfile data for a certain chunk.
++ * Collects RegionFile data for a certain chunk.
+ */
+ public static final class RegionFileData {
+
@@ -663,13 +889,13 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ 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.
++ * 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 type The RegionFile type.
+ * @param data The result to set.
+ */
-+ public void setData(final RegionFileType type, final CompoundTag data) {
++ public void setData(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) {
+ final int index = type.ordinal();
+
+ if (this.hasResult[index]) {
@@ -680,13 +906,13 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ }
+
+ /**
-+ * Sets the result associated with the specified regionfile type. Note that
-+ * results can only be set once per regionfile type.
++ * 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 type The RegionFile type.
+ * @param throwable The result to set.
+ */
-+ public void setThrowable(final RegionFileType type, final Throwable throwable) {
++ public void setThrowable(final MoonriseRegionFileIO.RegionFileType type, final Throwable throwable) {
+ final int index = type.ordinal();
+
+ if (this.hasResult[index]) {
@@ -697,26 +923,26 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ }
+
+ /**
-+ * Returns whether there is a result for the specified regionfile type.
++ * Returns whether there is a result for the specified RegionFile type.
+ *
-+ * @param type Specified regionfile type.
++ * @param type Specified RegionFile type.
+ *
+ * @return Whether a result exists for {@code type}.
+ */
-+ public boolean hasResult(final RegionFileType type) {
++ public boolean hasResult(final MoonriseRegionFileIO.RegionFileType type) {
+ return this.hasResult[type.ordinal()];
+ }
+
+ /**
-+ * Returns the data result for the regionfile type.
++ * Returns the data result for the RegionFile type.
+ *
-+ * @param type Specified regionfile type.
++ * @param type Specified RegionFile type.
+ *
+ * @throws IllegalArgumentException If the result has not been set for {@code type}.
+ * @return The data result for the specified type. If the result is a {@code Throwable},
+ * then returns {@code null}.
+ */
-+ public CompoundTag getData(final RegionFileType type) {
++ public CompoundTag getData(final MoonriseRegionFileIO.RegionFileType type) {
+ final int index = type.ordinal();
+
+ if (!this.hasResult[index]) {
@@ -727,15 +953,15 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ }
+
+ /**
-+ * Returns the throwable result for the regionfile type.
++ * Returns the throwable result for the RegionFile type.
+ *
-+ * @param type Specified regionfile type.
++ * @param type Specified RegionFile type.
+ *
+ * @throws IllegalArgumentException If the result has not been set for {@code type}.
+ * @return The throwable result for the specified type. If the result is an {@code CompoundTag},
+ * then returns {@code null}.
+ */
-+ public Throwable getThrowable(final RegionFileType type) {
++ public Throwable getThrowable(final MoonriseRegionFileIO.RegionFileType type) {
+ final int index = type.ordinal();
+
+ if (!this.hasResult[index]) {
@@ -746,134 +972,55 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ }
+ }
+
-+ private static final Object INIT_LOCK = new Object();
-+
-+ static RegionFileIOThread[] threads;
-+
-+ /* needs to be consistent given a set of parameters */
-+ static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-+ if (threads == null) {
-+ throw new IllegalStateException("Threads not initialised");
-+ }
-+
-+ final int regionX = chunkX >> 5;
-+ final int regionZ = chunkZ >> 5;
-+ final int typeOffset = type.ordinal();
-+
-+ return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length];
-+ }
-+
-+ /**
-+ * Shuts down the I/O executor(s). Watis for all tasks to complete if specified.
-+ * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted.
-+ *
-+ * @param wait Whether to wait until all tasks have completed.
-+ */
-+ public static void close(final boolean wait) {
-+ for (int i = 0, len = threads.length; i < len; ++i) {
-+ threads[i].close(false, true);
-+ }
-+ if (wait) {
-+ RegionFileIOThread.flush();
-+ }
-+ }
-+
-+ public static long[] getExecutedTasks() {
-+ final long[] ret = new long[threads.length];
-+ for (int i = 0, len = threads.length; i < len; ++i) {
-+ ret[i] = threads[i].getTotalTasksExecuted();
++ public static void flushRegionStorages(final ServerLevel world) throws IOException {
++ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
++ flushRegionStorages(world, type);
+ }
-+
-+ return ret;
+ }
+
-+ public static long[] getTasksScheduled() {
-+ final long[] ret = new long[threads.length];
-+ for (int i = 0, len = threads.length; i < len; ++i) {
-+ ret[i] = threads[i].getTotalTasksScheduled();
-+ }
-+ return ret;
++ public static void flushRegionStorages(final ServerLevel world, final RegionFileType type) throws IOException {
++ getControllerFor(world, type).getCache().flush();
+ }
+
-+ public static void flush() {
-+ for (int i = 0, len = threads.length; i < len; ++i) {
-+ threads[i].waitUntilAllExecuted();
++ public static void flush(final MinecraftServer server) {
++ for (final ServerLevel world : server.getAllLevels()) {
++ flush(world);
+ }
+ }
+
-+ public static void flushRegionStorages(final ServerLevel world) throws IOException {
-+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-+ getControllerFor(world, type).getCache().flush();
++ public static void flush(final ServerLevel world) {
++ for (final RegionFileType regionFileType : CACHED_REGIONFILE_TYPES) {
++ flush(world, regionFileType);
+ }
+ }
+
-+ public static void partialFlush(final int totalTasksRemaining) {
-+ long failures = 1L; // start out at 0.25ms
++ public static void flush(final ServerLevel world, final RegionFileType type) {
++ final RegionDataController taskController = getControllerFor(world, type);
+
-+ for (;;) {
-+ final long[] executed = getExecutedTasks();
-+ final long[] scheduled = getTasksScheduled();
++ long failures = 1L; // start at 0.13ms
+
-+ long sum = 0;
-+ for (int i = 0; i < executed.length; ++i) {
-+ sum += scheduled[i] - executed[i];
-+ }
-+
-+ if (sum <= totalTasksRemaining) {
-+ break;
-+ }
-+
-+ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms
++ while (taskController.hasTasks()) {
++ Thread.yield();
++ failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms
+ }
+ }
+
-+ /**
-+ * Inits the executor with the specified number of threads.
-+ *
-+ * @param threads Specified number of threads.
-+ */
-+ public static void init(final int threads) {
-+ synchronized (INIT_LOCK) {
-+ if (RegionFileIOThread.threads != null) {
-+ throw new IllegalStateException("Already initialised threads");
++ 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();
+ }
+
-+ RegionFileIOThread.threads = new RegionFileIOThread[threads];
-+
-+ for (int i = 0; i < threads; ++i) {
-+ RegionFileIOThread.threads[i] = new RegionFileIOThread(i);
-+ RegionFileIOThread.threads[i].start();
++ if (totalTasks > (long)tasksRemaining) {
++ Thread.yield();
++ failures = ConcurrentUtil.linearLongBackoff(failures, 125_000L, 5_000_000L); // 125us, 5ms
++ } else {
++ return;
+ }
+ }
+ }
+
-+ public static void deinit() {
-+ if (true) { // Paper
-+ // TODO does this cause issues with mods? how to implement
-+ close(true);
-+ synchronized (INIT_LOCK) {
-+ RegionFileIOThread.threads = null;
-+ }
-+ } else { RegionFileIOThread.flush(); }
-+ }
-+
-+ private RegionFileIOThread(final int threadNumber) {
-+ super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time
-+ this.setName("RegionFile I/O Thread #" + threadNumber);
-+ this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us
-+ this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> {
-+ LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr);
-+ });
-+ }
-+
-+ /**
-+ * Returns whether the current thread is a regionfile I/O executor.
-+ * @return Whether the current thread is a regionfile I/O executor.
-+ */
-+ public static boolean isRegionFileThread() {
-+ return Thread.currentThread() instanceof RegionFileIOThread;
-+ }
-+
+ /**
+ * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid
+ * dumb plugins from taking away priority from threads we consider crucial.
@@ -887,35 +1034,6 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ }
+
+ /**
-+ * Returns the current {@code CompoundTag} pending for write for the specified chunk & regionfile type.
-+ * Note that this does not copy the result, so do not modify the result returned.
-+ *
-+ * @param world Specified world.
-+ * @param chunkX Specified chunk x.
-+ * @param chunkZ Specified chunk z.
-+ * @param type Specified regionfile type.
-+ *
-+ * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending.
-+ */
-+ public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-+ return thread.getPendingWriteInternal(world, chunkX, chunkZ, type);
-+ }
-+
-+ CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-+ final ChunkDataController taskController = getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+
-+ if (task == null) {
-+ return null;
-+ }
-+
-+ final CompoundTag ret = task.inProgressWrite;
-+
-+ return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret;
-+ }
-+
-+ /**
+ * Returns the priority for the specified regionfile type for the specified chunk.
+ * @param world Specified world.
+ * @param chunkX Specified chunk x.
@@ -924,19 +1042,14 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ * @return The priority for the chunk
+ */
+ public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-+ return thread.getPriorityInternal(world, chunkX, chunkZ, type);
-+ }
-+
-+ Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-+ final ChunkDataController taskController = getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ final RegionDataController taskController = getControllerFor(world, type);
++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task == null) {
+ return Priority.COMPLETING;
+ }
+
-+ return task.prioritisedTask.getPriority();
++ return task.getPriority();
+ }
+
+ /**
@@ -957,7 +1070,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Priority priority) {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-+ RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority);
++ MoonriseRegionFileIO.setPriority(world, chunkX, chunkZ, type, priority);
+ }
+ }
+
@@ -979,17 +1092,11 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ */
+ public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) {
-+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-+ thread.setPriorityInternal(world, chunkX, chunkZ, type, priority);
-+ }
-+
-+ void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final Priority priority) {
-+ final ChunkDataController taskController = getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ final RegionDataController taskController = getControllerFor(world, type);
++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task != null) {
-+ task.prioritisedTask.setPriority(priority);
++ task.setPriority(priority);
+ }
+ }
+
@@ -1009,7 +1116,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Priority priority) {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-+ RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority);
++ MoonriseRegionFileIO.raisePriority(world, chunkX, chunkZ, type, priority);
+ }
+ }
+
@@ -1029,17 +1136,11 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ */
+ public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) {
-+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-+ thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority);
-+ }
-+
-+ void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final Priority priority) {
-+ final ChunkDataController taskController = getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ final RegionDataController taskController = getControllerFor(world, type);
++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task != null) {
-+ task.prioritisedTask.raisePriority(priority);
++ task.raisePriority(priority);
+ }
+ }
+
@@ -1059,7 +1160,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Priority priority) {
+ for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-+ RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority);
++ MoonriseRegionFileIO.lowerPriority(world, chunkX, chunkZ, type, priority);
+ }
+ }
+
@@ -1079,17 +1180,11 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ */
+ public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
+ final Priority priority) {
-+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-+ thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority);
-+ }
-+
-+ void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-+ final Priority priority) {
-+ final ChunkDataController taskController = getControllerFor(world, type);
-+ final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++ final RegionDataController taskController = getControllerFor(world, type);
++ final ChunkIOTask task = taskController.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
+
+ if (task != null) {
-+ task.prioritisedTask.lowerPriority(priority);
++ task.lowerPriority(priority);
+ }
+ }
+
@@ -1116,7 +1211,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ */
+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
+ final RegionFileType type) {
-+ RegionFileIOThread.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL);
++ MoonriseRegionFileIO.scheduleSave(world, chunkX, chunkZ, data, type, Priority.NORMAL);
+ }
+
+ /**
@@ -1143,37 +1238,112 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ */
+ public static void scheduleSave(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
+ final RegionFileType type, final Priority priority) {
-+ final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-+ thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority);
++ 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);
+ }
+
-+ void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
-+ final RegionFileType type, final Priority priority) {
-+ final ChunkDataController taskController = getControllerFor(world, type);
++ 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 long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final ChunkDataTask task = taskController.tasks.compute(key, (final long keyInMap, final ChunkDataTask taskRunning) -> {
-+ if (taskRunning == null || taskRunning.failedWrite) {
-+ // no task is scheduled or the previous write failed - meaning we need to overwrite it
++ 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()
++ );
+
-+ // create task
-+ final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority);
-+ newTask.inProgressWrite = data;
-+ created[0] = true;
++ newTask.pushPendingWrite(write);
+
-+ return newTask;
-+ }
++ created[0] = true;
+
-+ taskRunning.inProgressWrite = data;
++ return newTask;
++ }
+
-+ return taskRunning;
-+ });
++ taskRunning.pushPendingWrite(write);
++
++ return taskRunning;
++ }
++ );
++
++ write.schedule(task, scheduler);
+
+ if (created[0]) {
-+ task.prioritisedTask.queue();
++ taskController.startTask(task);
++ task.scheduleWriteCompress();
+ } else {
-+ task.prioritisedTask.raisePriority(priority);
++ task.raisePriority(priority);
+ }
+ }
+
@@ -1206,7 +1376,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ */
+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock) {
-+ return RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL);
++ return MoonriseRegionFileIO.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL);
+ }
+
+ /**
@@ -1240,7 +1410,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ public static Cancellable loadAllChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
+ final Priority priority) {
-+ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES);
++ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES);
+ }
+
+ /**
@@ -1274,7 +1444,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ public static Cancellable loadChunkData(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Consumer<RegionFileData> onComplete, final boolean intendingToBlock,
+ final RegionFileType... types) {
-+ return RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types);
++ return MoonriseRegionFileIO.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types);
+ }
+
+ /**
@@ -1324,7 +1494,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+
+ for (int i = 0; i < expectedCompletions; ++i) {
+ final RegionFileType type = types[i];
-+ reads[i] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type,
++ reads[i] = MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type,
+ (final CompoundTag data, final Throwable throwable) -> {
+ if (throwable != null) {
+ ret.setThrowable(type, throwable);
@@ -1370,7 +1540,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ 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 RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL);
++ return MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL);
+ }
+
+ /**
@@ -1403,57 +1573,62 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ 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 RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-+ return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority);
-+ }
-+
-+ Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
-+ final boolean intendingToBlock, final Priority priority) {
-+ final ChunkDataController taskController = getControllerFor(world, type);
++ final RegionDataController taskController = getControllerFor(world, type);
+
+ final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion();
+
+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+ final BiLong1Function<ChunkDataTask, ChunkDataTask> compute = (final long keyInMap, final ChunkDataTask running) -> {
++ final BiLong1Function<ChunkIOTask, ChunkIOTask> compute = (final long keyInMap, final ChunkIOTask running) -> {
+ if (running == null) {
+ // not scheduled
+
+ // set up task
-+ final ChunkDataTask newTask = new ChunkDataTask(
-+ world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority
++ final ChunkIOTask newTask = new ChunkIOTask(
++ world, taskController, chunkX, chunkZ, priority, new ChunkIOTask.InProgressRead()
+ );
-+ newTask.inProgressRead = new InProgressRead();
+ newTask.inProgressRead.addToAsyncWaiters(onComplete);
+
-+ callbackInfo.tasksNeedsScheduling = true;
++ callbackInfo.tasksNeedReadScheduling = true;
+ return newTask;
+ }
+
-+ final CompoundTag pendingWrite = running.inProgressWrite;
++ final ChunkIOTask.InProgressWrite pendingWrite = running.inProgressWrite;
+
-+ if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) {
++ 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
-+ callbackInfo.data = pendingWrite;
-+ callbackInfo.throwable = null;
-+ callbackInfo.completeNow = true;
++
++ 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 ChunkDataTask ret = taskController.tasks.compute(key, compute);
++ final ChunkIOTask ret = taskController.chunkTasks.compute(key, compute);
+
+ // needs to be scheduled
-+ if (callbackInfo.tasksNeedsScheduling) {
-+ ret.prioritisedTask.queue();
++ if (callbackInfo.tasksNeedReadScheduling) {
++ taskController.startTask(ret);
++ ret.scheduleReadIO();
+ } else if (callbackInfo.completeNow) {
+ try {
+ onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable);
@@ -1462,10 +1637,21 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ }
+ } else {
+ // we're waiting on a task we didn't schedule, so raise its priority to what we want
-+ ret.prioritisedTask.raisePriority(priority);
++ ret.raisePriority(priority);
+ }
+
-+ return new CancellableRead(onComplete, ret);
++ 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;
++
+ }
+
+ /**
@@ -1485,7 +1671,7 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ final Priority priority) throws IOException {
+ final CompletableFuture<CompoundTag> ret = new CompletableFuture<>();
+
-+ RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> {
++ MoonriseRegionFileIO.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> {
+ if (thr != null) {
+ ret.completeExceptionally(thr);
+ } else {
@@ -1499,52 +1685,53 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ throw new IOException(ex);
+ }
+ }
-+
-+ private static final class ImmediateCallbackCompletion {
-+
-+ public CompoundTag data;
-+ public Throwable throwable;
-+ public boolean completeNow;
-+ public boolean tasksNeedsScheduling;
-+
-+ }
-+
++
+ private static final class CancellableRead implements Cancellable {
+
+ private BiConsumer<CompoundTag, Throwable> callback;
-+ private ChunkDataTask task;
++ private ChunkIOTask.InProgressRead read;
++ private ChunkIOTask.InProgressWrite write;
+
-+ CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, final ChunkDataTask task) {
++ private CancellableRead(final BiConsumer<CompoundTag, Throwable> callback,
++ final ChunkIOTask.InProgressRead read,
++ final ChunkIOTask.InProgressWrite write) {
+ this.callback = callback;
-+ this.task = task;
++ this.read = read;
++ this.write = write;
+ }
+
+ @Override
+ public boolean cancel() {
+ final BiConsumer<CompoundTag, Throwable> callback = this.callback;
-+ final ChunkDataTask task = this.task;
++ final ChunkIOTask.InProgressRead read = this.read;
++ final ChunkIOTask.InProgressWrite write = this.write;
+
-+ if (callback == null || task == null) {
++ if (callback == null || (read == null && write == null)) {
+ return false;
+ }
+
+ this.callback = null;
-+ this.task = null;
++ this.read = null;
++ this.write = null;
+
-+ final InProgressRead read = task.inProgressRead;
++ if (read != null) {
++ return read.cancel(callback);
++ }
++ if (write != null) {
++ return write.cancel(callback);
++ }
+
-+ // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't)
-+ return read != null && read.cancel(callback);
++ // 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);
+
-+ CancellableReads(final Cancellable[] reads) {
++ private CancellableReads(final Cancellable[] reads) {
+ this.reads = reads;
+ }
+
@@ -1566,286 +1753,802 @@ index 0000000000000000000000000000000000000000..3218cbf84f54daf06e84442d5eb1a36d
+ }
+ }
+
-+ private static final class InProgressRead {
++ private static final class ChunkIOTask {
+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class);
++ private final ServerLevel world;
++ private final RegionDataController regionDataController;
++ private final int chunkX;
++ private final int chunkZ;
++ private Priority priority;
++ private PrioritisedExecutor.PrioritisedTask currentTask;
+
-+ private CompoundTag value;
-+ private Throwable throwable;
-+ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>();
++ 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);
+
-+ public boolean hasNoWaiters() {
-+ return this.callbacks.isEmpty();
++ return true;
++ }
+ }
+
-+ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) {
-+ return this.callbacks.add(callback);
++ public boolean raisePriority(final Priority priority) {
++ synchronized (this) {
++ if (this.priority.isHigherOrEqualPriority(priority)) {
++ return false;
++ }
++
++ this.updatePriority(priority);
++
++ return true;
++ }
+ }
+
-+ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) {
-+ return this.callbacks.remove(callback);
++ public boolean lowerPriority(final Priority priority) {
++ synchronized (this) {
++ if (this.priority.isLowerOrEqualPriority(priority)) {
++ return false;
++ }
++
++ this.updatePriority(priority);
++
++ return true;
++ }
+ }
+
-+ public void complete(final ChunkDataTask task, final CompoundTag value, final Throwable throwable) {
-+ this.value = value;
-+ this.throwable = throwable;
++ 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;
+
-+ BiConsumer<CompoundTag, Throwable> consumer;
-+ while ((consumer = this.callbacks.pollOrBlockAdds()) != null) {
+ try {
-+ consumer.accept(value == null ? null : value.copy(), throwable);
++ readData = this.regionDataController.readData(this.chunkX, this.chunkZ);
+ } catch (final Throwable thr) {
-+ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data for task " + task.toString(), 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();
+ }
+ }
-+ }
+
-+ public static abstract class ChunkDataController {
++ private void scheduleReadDecompress() {
++ final PrioritisedExecutor.PrioritisedTask task;
++ synchronized (this) {
++ task = this.regionDataController.compressionExecutor.createTask(this::performReadDecompress, this.priority);
++ this.currentTask = task;
++ }
++ task.queue();
++ }
+
-+ // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding.
-+ private final ConcurrentLong2ReferenceChainedHashTable<ChunkDataTask> tasks = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(8192, 0.5f);
++ private void performReadDecompress() {
++ final RegionDataController.ReadData readData = this.readData;
++ this.readData = null;
+
-+ public final RegionFileType type;
++ CompoundTag compoundTag = null;
++ Throwable throwable = null;
+
-+ public ChunkDataController(final RegionFileType type) {
-+ this.type = type;
++ 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();
++ }
+ }
+
-+ public abstract RegionFileStorage getCache();
++ private void finishRead(final CompoundTag compoundTag, final Throwable throwable) {
++ this.inProgressRead.complete(this, compoundTag, throwable);
++ }
+
-+ public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException;
++ public void scheduleWriteCompress() {
++ final InProgressWrite inProgressWrite = this.inProgressWrite;
+
-+ public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException;
++ final PrioritisedExecutor.PrioritisedTask task;
++ synchronized (this) {
++ task = this.regionDataController.compressionExecutor.createTask(() -> {
++ ChunkIOTask.this.performWriteCompress(inProgressWrite);
++ }, this.priority);
++ this.currentTask = task;
++ }
+
-+ public boolean hasTasks() {
-+ return !this.tasks.isEmpty();
++ inProgressWrite.addToWaiters(this, (final CompoundTag data, final Throwable throwable) -> {
++ task.queue();
++ });
+ }
+
-+ public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) {
-+ return ((ChunkSystemRegionFileStorage)(Object)this.getCache()).moonrise$doesRegionFileNotExistNoIO(chunkX, chunkZ);
-+ }
++ 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!");
++ }
+
-+ public <T> T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function<RegionFile, T> function) {
-+ final RegionFileStorage cache = this.getCache();
-+ final RegionFile regionFile;
-+ synchronized (cache) {
-+ try {
-+ if (existingOnly) {
-+ regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfExists(chunkX, chunkZ);
-+ } else {
-+ regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly);
++ if (valueInMap.inProgressWrite != null) {
++ return valueInMap;
+ }
-+ } catch (final IOException ex) {
-+ throw new RuntimeException(ex);
-+ }
+
-+ return function.apply(regionFile);
++ 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;
+ }
+
-+ public <T> T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function<RegionFile, T> function) {
-+ final RegionFileStorage cache = this.getCache();
-+ final RegionFile regionFile;
++ 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) {
++ // TODO implement this?
++ /*if (thr instanceof RegionFileStorage.RegionFileSizeException) {
++ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024);
++ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk.");
++ } else */
++ {
++ failedWrite = thr instanceof IOException;
++ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr);
++ }
++ }
++
++ if (writeData == null) {
++ // null if a throwable was encountered
+
-+ synchronized (cache) {
-+ regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfLoaded(chunkX, chunkZ);
++ // we cannot continue to the I/O stage here, so try to complete
+
-+ return function.apply(regionFile);
++ 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 static final class ChunkDataTask implements Runnable {
++ 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 static final CompoundTag NOTHING_TO_WRITE = new CompoundTag();
++ private void runWriteIO(final InProgressWrite inProgressWrite) {
++ RegionDataController.WriteData writeData = this.writeData;
++ this.writeData = null;
+
-+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkDataTask.class);
++ boolean failedWrite = false;
+
-+ private InProgressRead inProgressRead;
-+ private volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release
++ 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);
++ }
+
-+ private boolean failedWrite;
++ if (!this.tryCompleteWrite(inProgressWrite, failedWrite)) {
++ // fetch new data and try again
++ this.scheduleWriteCompress();
++ }
++ return;
++ }
+
-+ private final ServerLevel world;
-+ private final int chunkX;
-+ private final int chunkZ;
-+ private final ChunkDataController taskController;
++ private boolean tryCompleteWrite(final InProgressWrite written, final boolean failedWrite) {
++ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ);
+
-+ private final PrioritisedTask prioritisedTask;
++ final boolean[] done = new boolean[] { false };
+
-+ /*
-+ * IO thread will perform reads before writes for a given chunk x and z
-+ *
-+ * How reads/writes are scheduled:
-+ *
-+ * If read is scheduled while scheduling write, take no special action and just schedule write
-+ * If read is scheduled while scheduling read and no write is scheduled, chain the read task
-+ *
-+ *
-+ * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled)
-+ * If write is scheduled while scheduling write (ignore read in progress), overwrite the write in progress data
-+ *
-+ * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
-+ * it fails to properly propagate write failures thanks to writes overwriting each other
-+ */
++ 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;
++ });
+
-+ public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkDataController taskController,
-+ final PrioritisedExecutor executor, final Priority priority) {
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.taskController = taskController;
-+ this.prioritisedTask = executor.createTask(this, priority);
++ 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.taskController.type.name() + ", hash: " + this.hashCode();
++ return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + ","
++ + this.chunkZ + ") type: " + this.regionDataController.type.name() + ", hash: " + this.hashCode();
+ }
+
-+ @Override
-+ public void run() {
-+ final InProgressRead read = this.inProgressRead;
-+ final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ);
++ private static final class InProgressRead {
+
-+ if (read != null) {
-+ final boolean[] canRead = new boolean[] { true };
++ private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class);
+
-+ if (read.hasNoWaiters()) {
-+ // cancelled read? go to task controller to confirm
-+ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
-+ }
-+ if (valueInMap != ChunkDataTask.this) {
-+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-+ }
++ private CompoundTag value;
++ private Throwable throwable;
++ private final MultiThreadedQueue<BiConsumer<CompoundTag, Throwable>> callbacks = new MultiThreadedQueue<>();
+
-+ if (!read.hasNoWaiters()) {
-+ return valueInMap;
-+ } else {
-+ canRead[0] = false;
-+ }
++ public boolean hasNoWaiters() {
++ return this.callbacks.isEmpty();
++ }
+
-+ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap;
-+ });
++ public boolean addToAsyncWaiters(final BiConsumer<CompoundTag, Throwable> callback) {
++ return this.callbacks.add(callback);
++ }
+
-+ if (inMap == null) {
-+ // read is cancelled - and no write pending, so we're done
-+ return;
-+ }
-+ // if there is a write in progress, we don't actually have to worry about waiters gaining new entries -
-+ // the readers will just use the in progress write, so the value in canRead is good to use without
-+ // further synchronisation.
-+ }
++ public boolean cancel(final BiConsumer<CompoundTag, Throwable> callback) {
++ return this.callbacks.remove(callback);
++ }
+
-+ if (canRead[0]) {
-+ CompoundTag compound = null;
-+ Throwable throwable = null;
++ 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 {
-+ compound = this.taskController.readData(this.chunkX, this.chunkZ);
++ consumer.accept(value == null ? null : value.copy(), throwable);
+ } catch (final Throwable thr) {
-+ throwable = thr;
-+ LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr);
++ LOGGER.error("Callback " + ConcurrentUtil.genericToString(consumer) + " failed to handle chunk data (read) for task " + task.toString(), thr);
+ }
-+ read.complete(this, compound, throwable);
+ }
+ }
++ }
+
-+ CompoundTag write = this.inProgressWrite;
++ private static final class InProgressWrite {
+
-+ if (write == NOTHING_TO_WRITE) {
-+ final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
++ 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");
+ }
-+ if (valueInMap != ChunkDataTask.this) {
-+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
++ ++tasks.executedTasks;
++ if (tasks.createdTasks != tasks.executedTasks) {
++ return tasks;
+ }
-+ return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap;
++
++ if (tasks.task != null) {
++ throw new IllegalStateException("Task may not be null when created==executed");
++ }
++
++ return null;
+ });
++ }
++ }
+
-+ if (inMap == null) {
-+ return; // set the task value to null, indicating we're done
-+ } // else: inProgressWrite changed, so now we have something to write
++ private final class WrappedTask implements PrioritisedExecutor.PrioritisedTask {
++
++ private final PrioritisedExecutor.PrioritisedTask wrapped;
++
++ public WrappedTask(final PrioritisedExecutor.PrioritisedTask wrap) {
++ this.wrapped = wrap;
+ }
+
-+ for (;;) {
-+ write = this.inProgressWrite;
-+ final CompoundTag dataWritten = write;
++ @Override
++ public PrioritisedExecutor getExecutor() {
++ return RegionIOTasks.this.ioScheduler.executor;
++ }
+
-+ boolean failedWrite = false;
++ @Override
++ public boolean queue() {
++ synchronized (RegionIOTasks.this) {
++ if (this.wrapped.queue()) {
++ RegionIOTasks.this.adjustTaskPriority();
++ return true;
++ }
++ return false;
++ }
++ }
+
-+ try {
-+ this.taskController.writeData(this.chunkX, this.chunkZ, write);
-+ } catch (final Throwable thr) {
-+ if (thr instanceof RegionFileStorage.RegionFileSizeException) {
-+ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024);
-+ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk.");
-+ } else {
-+ failedWrite = thr instanceof IOException;
-+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr);
++ @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;
+ }
++ }
+
-+ final boolean finalFailWrite = failedWrite;
-+ final boolean[] done = new boolean[] { 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;
++ }
++ }
+
-+ this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
-+ if (valueInMap == null) {
-+ throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
++ @Override
++ public boolean lowerPriority(final Priority priority) {
++ synchronized (RegionIOTasks.this) {
++ if (this.wrapped.lowerPriority(priority) && this.wrapped.isQueued()) {
++ RegionIOTasks.this.adjustTaskPriority();
++ return true;
+ }
-+ if (valueInMap != ChunkDataTask.this) {
-+ throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
++ 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;
+ }
-+ if (valueInMap.inProgressWrite == dataWritten) {
-+ valueInMap.failedWrite = finalFailWrite;
-+ done[0] = true;
-+ // keep the data in map if we failed the write so we can try to prevent data loss
-+ return finalFailWrite ? valueInMap : null;
++ 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;
+ }
-+ // different data than expected, means we need to retry write
-+ return valueInMap;
-+ });
++ return false;
++ }
++ }
+
-+ if (done[0]) {
-+ return;
++ @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;
+ }
++ }
+
-+ // fetch & write new data
-+ continue;
++ @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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c35e0c29700be48dda3e53e7d2db224766ef17b7
+index 0000000000000000000000000000000000000000..a36ab89f5c37f5f9ab0152f087bb4cf3560f8581
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
-@@ -0,0 +1,56 @@
+@@ -0,0 +1,50 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
+
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.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.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+
-+public final class ChunkDataController extends RegionFileIOThread.ChunkDataController {
++public final class ChunkDataController extends MoonriseRegionFileIO.RegionDataController {
+
+ private final ServerLevel world;
+
-+ public ChunkDataController(final ServerLevel world) {
-+ super(RegionFileIOThread.RegionFileType.CHUNK_DATA);
++ public ChunkDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
++ super(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor);
+ this.world = world;
+ }
+
@@ -1855,43 +2558,37 @@ index 0000000000000000000000000000000000000000..c35e0c29700be48dda3e53e7d2db2247
+ }
+
+ @Override
-+ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
-+ final CompletableFuture<Void> future = this.world.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound);
++ public WriteData startWrite(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
++ return ((ChunkSystemRegionFileStorage)this.getCache()).moonrise$startWrite(chunkX, chunkZ, compound);
++ }
+
-+ try {
-+ if (future != null) {
-+ // rets non-null when sync writing (i.e. future should be completed here)
-+ future.join();
-+ }
-+ } catch (final CompletionException ex) {
-+ if (ex.getCause() instanceof IOException ioException) {
-+ throw ioException;
-+ }
-+ throw ex;
-+ }
++ @Override
++ public 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 CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
-+ try {
-+ return this.world.getChunkSource().chunkMap.read(new ChunkPos(chunkX, chunkZ)).join().orElse(null);
-+ } catch (final CompletionException ex) {
-+ if (ex.getCause() instanceof IOException ioException) {
-+ throw ioException;
-+ }
-+ throw ex;
-+ }
++ 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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
new file mode 100644
-index 0000000000000000000000000000000000000000..fdd189ef056187941d43809c5d61cab717aecf60
+index 0000000000000000000000000000000000000000..828c868f68c2a20bf90d0f7ec253fdeb591f15f6
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
-@@ -0,0 +1,55 @@
+@@ -0,0 +1,73 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
+
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.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;
@@ -1900,12 +2597,12 @@ index 0000000000000000000000000000000000000000..fdd189ef056187941d43809c5d61cab7
+import java.io.IOException;
+import java.nio.file.Path;
+
-+public final class EntityDataController extends RegionFileIOThread.ChunkDataController {
++public final class EntityDataController extends MoonriseRegionFileIO.RegionDataController {
+
+ private final EntityRegionFileStorage storage;
+
-+ public EntityDataController(final EntityRegionFileStorage storage) {
-+ super(RegionFileIOThread.RegionFileType.ENTITY_DATA);
++ public EntityDataController(final EntityRegionFileStorage storage, final ChunkTaskScheduler taskScheduler) {
++ super(MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor);
+ this.storage = storage;
+ }
+
@@ -1915,13 +2612,35 @@ index 0000000000000000000000000000000000000000..fdd189ef056187941d43809c5d61cab7
+ }
+
+ @Override
-+ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
-+ this.storage.write(new ChunkPos(chunkX, chunkZ), compound);
++ 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 CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
-+ return this.storage.read(new ChunkPos(chunkX, chunkZ));
++ 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 {
@@ -1933,38 +2652,34 @@ index 0000000000000000000000000000000000000000..fdd189ef056187941d43809c5d61cab7
+
+ @Override
+ public void write(final ChunkPos pos, final CompoundTag nbt) throws IOException {
-+ final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt);
-+ if (nbtPos != null && !pos.equals(nbtPos)) {
-+ throw new IllegalArgumentException(
-+ "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString()
-+ + " but compound says coordinate is " + nbtPos + " for world: " + this
-+ );
-+ }
++ checkPosition(pos, nbt);
+ super.write(pos, nbt);
+ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
new file mode 100644
-index 0000000000000000000000000000000000000000..af867f8fedd0bb8f675e94243aa1a3f17363483b
+index 0000000000000000000000000000000000000000..bd0d782852f9cfe5bc0b5339ecf4d82c10332ec9
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
-@@ -0,0 +1,33 @@
+@@ -0,0 +1,45 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
+
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.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 RegionFileIOThread.ChunkDataController {
++public final class PoiDataController extends MoonriseRegionFileIO.RegionDataController {
+
+ private final ServerLevel world;
+
-+ public PoiDataController(final ServerLevel world) {
-+ super(RegionFileIOThread.RegionFileType.POI_DATA);
++ public PoiDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
++ super(MoonriseRegionFileIO.RegionFileType.POI_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor);
+ this.world = world;
+ }
+
@@ -1974,23 +2689,50 @@ index 0000000000000000000000000000000000000000..af867f8fedd0bb8f675e94243aa1a3f1
+ }
+
+ @Override
-+ public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
-+ ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$write(chunkX, chunkZ, compound);
++ 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 CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
-+ return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$read(chunkX, chunkZ);
++ 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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..47a4d3376d08dde94a39254bec21473ff27f53e6
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
new file mode 100644
-index 0000000000000000000000000000000000000000..efcd9057f008f0b9cf0d22b2b21d1851205841e5
+index 0000000000000000000000000000000000000000..5d4d650186b18eb00782429d53d861564d8e4ba9
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
-@@ -0,0 +1,22 @@
+@@ -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;
@@ -2010,6 +2752,16 @@ index 0000000000000000000000000000000000000000..efcd9057f008f0b9cf0d22b2b21d1851
+
+ 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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevelReader.java
new file mode 100644
@@ -2029,19 +2781,20 @@ index 0000000000000000000000000000000000000000..0b58701342d573fa43cdd06681534854
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
new file mode 100644
-index 0000000000000000000000000000000000000000..b8a87b7e6505feb76ce1bd58c84615256cf6faa6
+index 0000000000000000000000000000000000000000..9d46482476f9ed9032a2b0f89afc20e03ed42dbb
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
-@@ -0,0 +1,61 @@
+@@ -0,0 +1,64 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level;
+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++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.RegionFileIOThread;
++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;
@@ -2052,32 +2805,34 @@ index 0000000000000000000000000000000000000000..b8a87b7e6505feb76ce1bd58c8461525
+
+ public ChunkTaskScheduler moonrise$getChunkTaskScheduler();
+
-+ public RegionFileIOThread.ChunkDataController moonrise$getChunkDataController();
++ public MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController();
+
-+ public RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController();
++ public MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController();
+
-+ public RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController();
++ public MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController();
+
+ public int moonrise$getRegionChunkShift();
+
-+ // Paper - marked closing not needed on CB
++ public boolean moonrise$isMarkedClosing();
++
++ public void moonrise$setMarkedClosing(final boolean value);
+
+ public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader();
+
+ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
-+ final PrioritisedExecutor.Priority priority,
++ final Priority priority,
+ final Consumer<List<ChunkAccess>> onLoad);
+
+ public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
-+ final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority,
++ 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 PrioritisedExecutor.Priority priority,
++ 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 PrioritisedExecutor.Priority priority,
++ final ChunkStatus chunkStatus, final Priority priority,
+ final Consumer<List<ChunkAccess>> onLoad);
+
+ public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
@@ -2094,6 +2849,33 @@ index 0000000000000000000000000000000000000000..b8a87b7e6505feb76ce1bd58c8461525
+
+ public ReferenceList<ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks();
+}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemChunkHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d049d750df88762566f13a9c4fc7574a2df4825
@@ -2160,18 +2942,21 @@ index 0000000000000000000000000000000000000000..f4bc44bb266763345c4e6f859c89352c
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
new file mode 100644
-index 0000000000000000000000000000000000000000..883fe6401f1b9711fa544d18a815b4d638f580df
+index 0000000000000000000000000000000000000000..aacd543f03b35908011d0c2891e978cc093ebcf5
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
-@@ -0,0 +1,9 @@
+@@ -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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemLevelChunk.java
new file mode 100644
@@ -2194,13 +2979,15 @@ index 0000000000000000000000000000000000000000..5b092bca7027e37aeee8f4b852ad896d
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
new file mode 100644
-index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548cc65efa8e
+index 0000000000000000000000000000000000000000..5ed6599d1f9a2edf8c904f3602b06d26d857600c
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
-@@ -0,0 +1,819 @@
+@@ -0,0 +1,798 @@
+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;
@@ -2213,6 +3000,7 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+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;
@@ -2226,7 +3014,6 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Predicate;
-+import org.bukkit.event.entity.EntityRemoveEvent;
+
+public final class ChunkEntitySlices {
+
@@ -2243,6 +3030,7 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+ private final EntityList entities = new EntityList();
+
+ public FullChunkStatus status;
++ public final ChunkData chunkData;
+
+ private boolean isTransient;
+
@@ -2255,7 +3043,7 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+ }
+
+ public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status,
-+ final int minSection, final int maxSection) { // inclusive, inclusive
++ final ChunkData chunkData, final int minSection, final int maxSection) { // inclusive, inclusive
+ this.minSection = minSection;
+ this.maxSection = maxSection;
+ this.chunkX = chunkX;
@@ -2268,11 +3056,12 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+ 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).collect(ImmutableList.toImmutableList());
++ return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world, EntitySpawnReason.LOAD).collect(ImmutableList.toImmutableList());
+ }
+
+ // Paper start - rewrite chunk system
@@ -2300,7 +3089,7 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+ }
+
+ final ListTag entitiesTag = new ListTag();
-+ for (final Entity entity : entities) {
++ for (final Entity entity : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, entities)) {
+ CompoundTag compoundTag = new CompoundTag();
+ if (entity.save(compoundTag)) {
+ entitiesTag.add(compoundTag);
@@ -2347,12 +3136,12 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+ continue;
+ }
+ if (entity.shouldBeSaved()) {
-+ entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD);
++ 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()) {
-+ passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD);
++ PlatformHooks.get().unloadEntity(passenger);
+ }
+ }
+ }
@@ -2361,34 +3150,7 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+ return this.entities.size() != 0;
+ }
+
-+ // Paper start
-+ public org.bukkit.entity.Entity[] getChunkEntities() {
-+ List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>();
-+ final Entity[] entities = this.entities.getRawData();
-+ for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
-+ final Entity entity = entities[i];
-+ if (entity == null) {
-+ continue;
-+ }
-+ final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity();
-+ if (bukkit != null && bukkit.isValid()) {
-+ ret.add(bukkit);
-+ }
-+ }
-+
-+ return ret.toArray(new org.bukkit.entity.Entity[0]);
-+ }
-+
-+ public void callEntitiesLoadEvent() {
-+ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesLoadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities());
-+ }
-+
-+ public void callEntitiesUnloadEvent() {
-+ org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesUnloadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities());
-+ }
-+ // Paper end
-+
-+ private List<Entity> getAllEntities() {
++ public List<Entity> getAllEntities() {
+ final int len = this.entities.size();
+ if (len == 0) {
+ return new ArrayList<>();
@@ -2451,6 +3213,7 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+ 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);
@@ -2484,6 +3247,7 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+ return false;
+ }
+ ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null);
++ ((ChunkSystemEntity)entity).moonrise$setChunkData(null);
+ final int sectionIndex = chunkSection - this.minSection;
+
+ this.allEntities.removeEntity(entity, sectionIndex);
@@ -3019,7 +3783,7 @@ index 0000000000000000000000000000000000000000..997b05167c19472acb98edac32d4548c
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
new file mode 100644
-index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc3b2c810c
+index 0000000000000000000000000000000000000000..93335de8cf514dc8417e4b9b2d495663deda2904
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
@@ -0,0 +1,1083 @@
@@ -3071,8 +3835,6 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+
+ protected final SWMRLong2ObjectHashTable<ChunkSlicesRegion> regions = new SWMRLong2ObjectHashTable<>(128, 0.5f);
+
-+ protected final int minSection; // inclusive
-+ protected final int maxSection; // inclusive
+ protected final LevelCallback<Entity> worldCallback;
+
+ protected final ConcurrentLong2ReferenceChainedHashTable<Entity> entityById = new ConcurrentLong2ReferenceChainedHashTable<>();
@@ -3081,8 +3843,6 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+
+ public EntityLookup(final Level world, final LevelCallback<Entity> worldCallback) {
+ this.world = world;
-+ this.minSection = WorldUtil.getMinSection(world);
-+ this.maxSection = WorldUtil.getMaxSection(world);
+ this.worldCallback = worldCallback;
+ }
+
@@ -3116,7 +3876,7 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+
+ protected abstract void entityEndTicking(final Entity entity);
+
-+ protected abstract boolean screenEntity(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) {
@@ -3372,7 +4132,7 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+ }
+
+ protected void addRecursivelySafe(final Entity root, final boolean fromDisk) {
-+ if (!this.addEntity(root, 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;
@@ -3411,7 +4171,11 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+ }
+
+ public boolean addNewEntity(final Entity entity) {
-+ return this.addEntity(entity, false);
++ 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) {
@@ -3422,10 +4186,10 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+ return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus);
+ }
+
-+ protected boolean addEntity(final Entity entity, final boolean fromDisk) {
++ 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, this.minSection, this.maxSection);
++ 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");
+
@@ -3439,7 +4203,7 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+ return false;
+ }
+
-+ if (!this.screenEntity(entity)) {
++ if (!this.screenEntity(entity, fromDisk, event)) {
+ return false;
+ }
+
@@ -3544,7 +4308,7 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+ final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
+ final BlockPos newPos = entity.blockPosition();
+ final int newSectionX = newPos.getX() >> 4;
-+ final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection);
++ final int 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) {
@@ -3984,7 +4748,7 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+
+ public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) {
+ final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-+ ChunkEntitySlices ret;
++ final ChunkEntitySlices ret;
+ if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) {
+ return this.createEntityChunk(chunkX, chunkZ, true);
+ }
@@ -4082,7 +4846,7 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+ @Override
+ public void onRemove(final Entity.RemovalReason reason) {
+ final Entity entity = this.entity;
-+ EntityLookup.this.checkThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system
++ EntityLookup.this.checkThread(entity, "Cannot remove entity off-main");
+ final Visibility tickingState = EntityLookup.getEntityStatus(entity);
+
+ EntityLookup.this.removeEntity(entity);
@@ -4106,15 +4870,15 @@ index 0000000000000000000000000000000000000000..efc0c1acc8239dd7b00211a1d3bfd3fc
+ public void onRemove(final Entity.RemovalReason reason) {}
+ }
+}
-\ No newline at end of file
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
new file mode 100644
-index 0000000000000000000000000000000000000000..edcde00206d068bd79175fea33efa05b0e8c1562
+index 0000000000000000000000000000000000000000..a038215156a163b0b1cbc870ada5b4ac85ed1335
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
-@@ -0,0 +1,123 @@
+@@ -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;
@@ -4160,7 +4924,8 @@ index 0000000000000000000000000000000000000000..edcde00206d068bd79175fea33efa05b
+
+ final ChunkEntitySlices ret = new ChunkEntitySlices(
+ this.world, chunkX, chunkZ,
-+ ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
++ ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, null,
++ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
+ );
+
+ // note: not handled by superclass
@@ -4178,7 +4943,11 @@ index 0000000000000000000000000000000000000000..edcde00206d068bd79175fea33efa05b
+ 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
@@ -4212,7 +4981,7 @@ index 0000000000000000000000000000000000000000..edcde00206d068bd79175fea33efa05b
+ }
+
+ @Override
-+ protected boolean screenEntity(final Entity entity) {
++ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
+ return true;
+ }
+
@@ -4238,7 +5007,7 @@ index 0000000000000000000000000000000000000000..edcde00206d068bd79175fea33efa05b
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
new file mode 100644
-index 0000000000000000000000000000000000000000..465469e44346c50f30f3abd6b44f4173ccfcf248
+index 0000000000000000000000000000000000000000..2ff58cf753c60913ee73aae015182e9c5560d529
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
@@ -0,0 +1,114 @@
@@ -4276,7 +5045,7 @@ index 0000000000000000000000000000000000000000..465469e44346c50f30f3abd6b44f4173
+ protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
+ final ChunkEntitySlices ret = new ChunkEntitySlices(
+ this.world, chunkX, chunkZ, FullChunkStatus.FULL,
-+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
++ null, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
+ );
+
+ // note: not handled by superclass
@@ -4328,7 +5097,7 @@ index 0000000000000000000000000000000000000000..465469e44346c50f30f3abd6b44f4173
+ }
+
+ @Override
-+ protected boolean screenEntity(final Entity entity) {
++ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
+ return true;
+ }
+
@@ -4358,13 +5127,15 @@ index 0000000000000000000000000000000000000000..465469e44346c50f30f3abd6b44f4173
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
new file mode 100644
-index 0000000000000000000000000000000000000000..dacf2b2988ce603879fe525a3418ac77f8a663f7
+index 0000000000000000000000000000000000000000..58d9187adc188b693b6becc400f766e069bf1bf5
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-@@ -0,0 +1,113 @@
+@@ -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;
@@ -4381,7 +5152,6 @@ index 0000000000000000000000000000000000000000..dacf2b2988ce603879fe525a3418ac77
+
+ private final ServerLevel serverWorld;
+ public final ReferenceList<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
-+ public final ReferenceList<Entity> trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
+
+ public ServerEntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) {
+ super(world, worldCallback);
@@ -4427,6 +5197,11 @@ index 0000000000000000000000000000000000000000..dacf2b2988ce603879fe525a3418ac77
+ 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
@@ -4441,14 +5216,12 @@ index 0000000000000000000000000000000000000000..dacf2b2988ce603879fe525a3418ac77
+ if (entity instanceof ServerPlayer player) {
+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().removePlayer(player);
+ }
-+ this.trackerUnloadedEntities.remove(entity); // Moonrise - entity tracker
+ }
+
+ @Override
+ protected void entityStartLoaded(final Entity entity) {
+ // Moonrise start - entity tracker
+ this.trackerEntities.add(entity);
-+ this.trackerUnloadedEntities.remove(entity);
+ // Moonrise end - entity tracker
+ }
+
@@ -4456,7 +5229,6 @@ index 0000000000000000000000000000000000000000..dacf2b2988ce603879fe525a3418ac77
+ protected void entityEndLoaded(final Entity entity) {
+ // Moonrise start - entity tracker
+ this.trackerEntities.remove(entity);
-+ this.trackerUnloadedEntities.add(entity);
+ // Moonrise end - entity tracker
+ }
+
@@ -4471,8 +5243,8 @@ index 0000000000000000000000000000000000000000..dacf2b2988ce603879fe525a3418ac77
+ }
+
+ @Override
-+ protected boolean screenEntity(final Entity entity) {
-+ return ChunkSystem.screenEntity(this.serverWorld, entity);
++ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
++ return ChunkSystem.screenEntity(this.serverWorld, entity, fromDisk, event);
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/ChunkSystemPoiManager.java
@@ -4518,16 +5290,15 @@ index 0000000000000000000000000000000000000000..89b956b8fdf1a0d862a843104511005e
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
new file mode 100644
-index 0000000000000000000000000000000000000000..fd35e4db0c8fec8f86b8743bcc2b15ed2e7433f1
+index 0000000000000000000000000000000000000000..bbf9d6c1c9525d97160806819a57be03eca290f1
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
-@@ -0,0 +1,212 @@
+@@ -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.Codec;
+import com.mojang.serialization.DataResult;
+import net.minecraft.SharedConstants;
+import net.minecraft.nbt.CompoundTag;
@@ -4647,7 +5418,6 @@ index 0000000000000000000000000000000000000000..fd35e4db0c8fec8f86b8743bcc2b15ed
+ ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion());
+
+ final ServerLevel world = this.world;
-+ final PoiManager poiManager = world.getPoiManager();
+ final int chunkX = this.chunkX;
+ final int chunkZ = this.chunkZ;
+
@@ -4657,13 +5427,8 @@ index 0000000000000000000000000000000000000000..fd35e4db0c8fec8f86b8743bcc2b15ed
+ continue;
+ }
+
-+ final long key = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ);
-+ // codecs are honestly such a fucking disaster. What the fuck is this trash?
-+ final Codec<PoiSection> codec = PoiSection.codec(() -> {
-+ poiManager.setDirty(key);
-+ });
-+
-+ final DataResult<Tag> serializedResult = codec.encodeStart(registryOps, section);
++ // 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);
@@ -4707,19 +5472,18 @@ index 0000000000000000000000000000000000000000..fd35e4db0c8fec8f86b8743bcc2b15ed
+ continue;
+ }
+
-+ final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ);
-+ // codecs are honestly such a fucking disaster. What the fuck is this trash?
-+ final Codec<PoiSection> codec = PoiSection.codec(() -> {
-+ poiManager.setDirty(coordinateKey);
-+ });
-+
+ final CompoundTag section = sections.getCompound(key);
-+ final DataResult<PoiSection> deserializeResult = codec.parse(registryOps, section);
++ final DataResult<PoiSection.Packed> deserializeResult = PoiSection.Packed.CODEC.parse(registryOps, section);
+ final int finalSectionY = sectionY;
-+ final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> {
++ 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;
@@ -4736,19 +5500,15 @@ index 0000000000000000000000000000000000000000..fd35e4db0c8fec8f86b8743bcc2b15ed
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
new file mode 100644
-index 0000000000000000000000000000000000000000..fb87d7ece6ebccfd0ffd2f1a609b45a0d2461d9e
+index 0000000000000000000000000000000000000000..524752744e37a2db0e3ea089468bdf497129bfef
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
-@@ -0,0 +1,17 @@
+@@ -0,0 +1,13 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.level.storage;
+
-+import com.mojang.serialization.Dynamic;
+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.Tag;
+import net.minecraft.world.level.chunk.storage.RegionFileStorage;
+import java.io.IOException;
-+import java.util.Optional;
-+import java.util.concurrent.CompletableFuture;
+
+public interface ChunkSystemSectionStorage {
+
@@ -4780,14 +5540,15 @@ index 0000000000000000000000000000000000000000..003a857e70ead858e8437e3c1bfaf22f
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
new file mode 100644
-index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235efa8a969
+index 0000000000000000000000000000000000000000..b2fa9883aefb07f64bb5db7e0052218d2ad09aba
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-@@ -0,0 +1,1082 @@
+@@ -0,0 +1,1085 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.player;
+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+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;
@@ -5198,7 +5959,11 @@ index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235
+ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
+ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager
+ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player);
-+ PlayerChunkSender.sendChunk(this.player.connection, this.world, ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ));
++
++ 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();
@@ -5212,17 +5977,12 @@ index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235
+ }
+
+ 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);
-+ final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
-+ this.player.connection.send(new ClientboundForgetLevelChunkPacket(chunkPos));
-+ // Paper start - PlayerChunkUnloadEvent
-+ if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) {
-+ new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(this.world.getWorld().getChunkAt(chunkPos.longKey), this.player.getBukkitEntity()).callEvent();
-+ }
-+ // Paper end - PlayerChunkUnloadEvent
++ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ)));
+ }
+
+ private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) {
@@ -5315,7 +6075,7 @@ index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235
+ final int playerSendViewDistance, final int worldSendViewDistance) {
+ return Math.min(
+ loadViewDistance - 1,
-+ playerSendViewDistance < 0 ? (!io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance
++ playerSendViewDistance < 0 ? (!PlatformHooks.get().configAutoConfigSendDistance() || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance
+ );
+ }
+
@@ -5340,26 +6100,26 @@ index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235
+ }
+
+ private double getMaxChunkLoadRate() {
-+ final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
++ 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 = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
++ 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 = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
++ 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 = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
++ 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);
@@ -5373,7 +6133,7 @@ index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235
+
+ private long getMaxChunkGenerates() {
+ final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
-+ long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
++ 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);
@@ -5495,7 +6255,7 @@ index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235
+ final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk);
+ final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk);
+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
-+ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null
++ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, Priority.NORMAL, null
+ );
+ if (this.removed) {
+ return;
@@ -5611,7 +6371,7 @@ index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235
+ }
+ 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();
++ chunk.postProcessGeneration(this.world);
+ // check if there was any recursive action
+ if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) {
+ return;
@@ -5650,7 +6410,6 @@ index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235
+ final int clientViewDistance = getClientViewDistance(this.player);
+ final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
+
-+ // TODO check PlayerList diff in paper chunk system patch
+ // send view distances
+ this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
+ this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
@@ -5864,6 +6623,10 @@ index 0000000000000000000000000000000000000000..852d75a73dae7448cbe1e2f5e164b235
+
+ // now all tickets should be removed, which is all of our external state
+ }
++
++ public LongOpenHashSet getSentChunksRaw() {
++ return this.sentChunks;
++ }
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
@@ -6019,21 +6782,21 @@ index 0000000000000000000000000000000000000000..7eafc5b7cba23d8dec92ecc1050afe3f
\ No newline at end of file
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
new file mode 100644
-index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b58a8b5ef
+index 0000000000000000000000000000000000000000..f98df65eaed2abedc66f3a49790e0cfb65354ed9
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-@@ -0,0 +1,1428 @@
+@@ -0,0 +1,1455 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
++import ca.spottedleaf.concurrentutil.util.Priority;
++import ca.spottedleaf.moonrise.common.PlatformHooks;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
+import ca.spottedleaf.moonrise.common.util.TickThread;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import ca.spottedleaf.moonrise.common.util.ChunkSystem;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++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;
@@ -6045,7 +6808,6 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+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;
@@ -6065,7 +6827,9 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+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 org.slf4j.LoggerFactory;
+import java.io.IOException;
+import java.text.DecimalFormat;
+import java.util.ArrayDeque;
@@ -6083,7 +6847,7 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+
+public final class ChunkHolderManager {
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkHolderManager.class);
+
+ public static final int FULL_LOADED_TICKET_LEVEL = ChunkLevel.FULL_CHUNK_LEVEL;
+ public static final int BLOCK_TICKING_TICKET_LEVEL = ChunkLevel.BLOCK_TICKING_LEVEL;
@@ -6214,7 +6978,7 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ if (halt) {
+ LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
+ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
-+ LOGGER.warn("Failed to halt world generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
++ 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) + "'");
+ }
@@ -6224,21 +6988,21 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ this.saveAllChunks(true, true, true);
+ }
+
-+ boolean hasTasks = false;
-+ for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) {
-+ if (RegionFileIOThread.getControllerFor(this.world, type).hasTasks()) {
-+ hasTasks = true;
-+ break;
++ 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) + "'");
+ }
+ }
-+ if (hasTasks) {
-+ RegionFileIOThread.flush();
-+ }
+
+ // kill regionfile cache
-+ for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) {
++ for (final MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) {
+ try {
-+ RegionFileIOThread.getControllerFor(this.world, type).getCache().close();
++ 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);
+ }
@@ -6255,8 +7019,8 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ public void autoSave() {
+ final List<NewChunkHolder> reschedule = new ArrayList<>();
+ final long currentTick = this.currentTick;
-+ final long maxSaveTime = currentTick - Math.max(1L, this.world.paperConfig().chunks.autoSaveInterval.value());
-+ final int maxToSave = this.world.paperConfig().chunks.maxAutoSaveChunksPerTick;
++ final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval());
++ final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick();
+ for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
+ final NewChunkHolder holder = this.autoSaveQueue.first();
+
@@ -6296,55 +7060,74 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+
+ long start = System.nanoTime();
+ long lastLog = start;
-+ boolean needsFlush = false;
-+ final int flushInterval = 50;
++ 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) {
-+ ++saved;
-+ needsFlush = flush;
+ 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 (needsFlush && (saved % flushInterval) == 0) {
-+ needsFlush = false;
-+ RegionFileIOThread.partialFlush(flushInterval / 2);
++ 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 " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + WorldUtil.getWorldName(this.world) + "'");
++ 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) {
-+ RegionFileIOThread.flush();
++ MoonriseRegionFileIO.flush(this.world);
+ try {
-+ RegionFileIOThread.flushRegionStorages(this.world);
++ 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");
++ 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"
++ );
+ }
+ }
+
@@ -6823,21 +7606,21 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ return this.chunkHolders.get(position);
+ }
+
-+ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
++ 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority 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);
@@ -6920,7 +7703,7 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask();
+
+ if (entityLoad != null) {
-+ entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++ entityLoad.raisePriority(Priority.BLOCKING);
+ }
+ }
+ }
@@ -6996,7 +7779,7 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask();
+
+ if (poiLoad != null) {
-+ poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++ poiLoad.raisePriority(Priority.BLOCKING);
+ }
+ }
+ } finally {
@@ -7043,7 +7826,7 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ }
+
+ ChunkHolderManager.this.processPendingFullUpdate();
-+ }, PrioritisedExecutor.Priority.HIGHEST);
++ }, Priority.HIGHEST);
+ } else {
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+ for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
@@ -7053,11 +7836,10 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ }
+
+ private void removeChunkHolder(final NewChunkHolder holder) {
-+ holder.markUnloaded();
++ 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
@@ -7338,6 +8120,9 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ 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;
+
@@ -7353,10 +8138,15 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+ }
+ changedFullStatus = new ArrayList<>();
+
-+ ret |= this.ticketLevelPropagator.performUpdates(
-+ this.ticketLockArea, this.taskScheduler.schedulingLockArea,
-+ scheduledTasks, changedFullStatus
-+ );
++ this.blockTicketUpdates();
++ try {
++ ret |= this.ticketLevelPropagator.performUpdates(
++ this.ticketLockArea, this.taskScheduler.schedulingLockArea,
++ scheduledTasks, changedFullStatus
++ );
++ } finally {
++ this.unblockTicketUpdates(Boolean.FALSE);
++ }
+ }
+
+ if (changedFullStatus != null) {
@@ -7453,23 +8243,24 @@ index 0000000000000000000000000000000000000000..58d3d1a47e9f2423c467bb329c2d5f4b
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
new file mode 100644
-index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf01815f64c
+index 0000000000000000000000000000000000000000..120ce31729dc8d4bba0901ca06d3212f3158d089
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-@@ -0,0 +1,1041 @@
+@@ -0,0 +1,1037 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue;
++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.config.moonrise.MoonriseConfig;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.JsonUtil;
+import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
+import ca.spottedleaf.moonrise.common.util.TickThread;
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
+import ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer;
@@ -7482,7 +8273,6 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+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.mojang.logging.LogUtils;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import net.minecraft.CrashReport;
@@ -7507,6 +8297,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+import net.minecraft.world.level.chunk.status.ChunkStep;
+import net.minecraft.world.phys.Vec3;
+import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
+import java.io.File;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
@@ -7522,51 +8313,14 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+
+public final class ChunkTaskScheduler {
+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
-+
-+ static int newChunkSystemIOThreads;
-+ static int newChunkSystemGenParallelism;
-+ static int newChunkSystemGenPopulationParallelism;
-+ static int newChunkSystemLoadParallelism;
-+
-+ private static boolean initialised = false;
-+
-+ public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
-+ if (initialised) {
-+ return;
-+ }
-+ initialised = true;
-+ MoonriseCommon.init(chunkSystem); // Paper
-+ newChunkSystemIOThreads = chunkSystem.ioThreads;
-+ if (newChunkSystemIOThreads <= 0) {
-+ newChunkSystemIOThreads = 1;
-+ } else {
-+ newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads);
-+ }
-+
-+ String newChunkSystemGenParallelism = chunkSystem.genParallelism;
-+ if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) {
-+ newChunkSystemGenParallelism = "true";
-+ }
++ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkTaskScheduler.class);
+
-+ boolean useParallelGen;
-+ if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled")
-+ || newChunkSystemGenParallelism.equalsIgnoreCase("true")) {
-+ useParallelGen = true;
-+ } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled")
-+ || newChunkSystemGenParallelism.equalsIgnoreCase("false")) {
-+ useParallelGen = false;
-+ } else {
-+ throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]");
++ public static void init(final boolean useParallelGen) {
++ for (final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor executor : MoonriseCommon.RADIUS_AWARE_GROUP.getAllExecutors()) {
++ executor.setMaxParallelism(useParallelGen ? -1 : 1);
+ }
+
-+ ChunkTaskScheduler.newChunkSystemGenParallelism = MoonriseCommon.WORKER_THREADS;
-+ ChunkTaskScheduler.newChunkSystemGenPopulationParallelism = useParallelGen ? MoonriseCommon.WORKER_THREADS : 1;
-+ ChunkTaskScheduler.newChunkSystemLoadParallelism = MoonriseCommon.WORKER_THREADS;
-+
-+ RegionFileIOThread.init(newChunkSystemIOThreads);
-+
-+ LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + MoonriseCommon.WORKER_THREADS + " worker threads, and population gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenPopulationParallelism + " threads");
++ 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);
@@ -7610,13 +8364,15 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ }
+
+ public final ServerLevel world;
-+ public final PrioritisedThreadPool workers;
+ public final RadiusAwarePrioritisedExecutor radiusAwareScheduler;
-+ public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor;
-+ private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor;
-+ public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor;
++ 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 PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue();
++ private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue();
+
+ public final ChunkHolderManager chunkHolderManager;
+
@@ -7765,9 +8521,8 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ return this.lockShift;
+ }
+
-+ public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) {
++ public ChunkTaskScheduler(final ServerLevel world) {
+ this.world = world;
-+ this.workers = workers;
+ // 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
@@ -7776,11 +8531,14 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ this.lockShift = Math.max(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT);
+ this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift());
+
-+ final String worldName = WorldUtil.getWorldName(world);
-+ this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenParallelism));
-+ this.radiusAwareGenExecutor = workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenPopulationParallelism));
-+ this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", 1, newChunkSystemLoadParallelism);
-+ this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(2, 1 + newChunkSystemGenPopulationParallelism));
++ 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);
+ }
+
@@ -7819,7 +8577,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ };
+
+ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
-+ this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING);
++ 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));
+ }
@@ -7829,20 +8587,20 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ return this.mainThreadExecutor.executeTask();
+ }
+
-+ public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
++ 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority priority,
++ final boolean addTicket, final Priority priority,
+ final Consumer<LevelChunk> onComplete) {
+ final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING
+
@@ -7938,7 +8696,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ }
+
+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket,
-+ final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
++ final Priority priority, final Consumer<ChunkAccess> onComplete) {
+ if (gen) {
+ this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+ return;
@@ -7962,7 +8720,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+
+ // only appropriate to use with syncLoadNonFull
+ public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
-+ final PrioritisedExecutor.Priority priority) {
++ final Priority priority) {
+ final int accessRadius = getAccessRadius(toStatus);
+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+ final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
@@ -8007,6 +8765,11 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ 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;
@@ -8017,7 +8780,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
+ this.chunkHolderManager.processTicketUpdates();
+
-+ this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, PrioritisedExecutor.Priority.BLOCKING);
++ 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
@@ -8037,7 +8800,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ }
+
+ public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket,
-+ final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
++ 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);
@@ -8129,7 +8892,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+
+ private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk,
+ final NewChunkHolder chunkHolder, final StaticCache2D<GenerationChunkHolder> neighbours,
-+ final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) {
++ final ChunkStatus toStatus, final Priority initialPriority) {
+ if (toStatus == ChunkStatus.EMPTY) {
+ return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority);
+ }
@@ -8145,7 +8908,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+
+ 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(PrioritisedExecutor.Priority.NORMAL));
++ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(Priority.NORMAL));
+ }
+
+ // rets new task scheduled for the _specified_ chunk
@@ -8154,7 +8917,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ // 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 PrioritisedExecutor.Priority minPriority) {
++ final Priority minPriority) {
+ if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) {
+ throw new IllegalStateException("Not holding scheduling lock");
+ }
@@ -8164,8 +8927,8 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ return null;
+ }
+
-+ final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max(
-+ minPriority, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
++ final Priority requestedPriority = Priority.max(
++ minPriority, chunkHolder.getEffectivePriority(Priority.NORMAL)
+ );
+ final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus();
+ final ChunkAccess chunk = chunkHolder.getCurrentChunk();
@@ -8202,7 +8965,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+
+ final int neighbourReadRadius = Math.max(
+ 0,
-+ chunkPyramid.getStepTo(toStatus).getAccumulatedRadiusOf(ChunkStatus.EMPTY)
++ chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY)
+ );
+
+ boolean unGeneratedNeighbours = false;
@@ -8242,7 +9005,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+
+ final ChunkProgressionTask task = this.createTask(
+ chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus,
-+ chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
++ chunkHolder.getEffectivePriority(Priority.NORMAL)
+ );
+ allTasks.add(task);
+
@@ -8253,7 +9016,7 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+
+ // 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 PrioritisedExecutor.Priority minPriority) {
++ final List<ChunkProgressionTask> tasks, final Priority minPriority) {
+ final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
+
+ if (chunkHolder == null) {
@@ -8289,41 +9052,41 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ */
+ @Deprecated
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) {
-+ return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL);
++ 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 PrioritisedExecutor.Priority priority) {
-+ return this.mainThreadExecutor.queueRunnable(run, priority);
++ 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, PrioritisedExecutor.Priority.NORMAL);
++ return this.createChunkTask(chunkX, chunkZ, run, Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
-+ final PrioritisedExecutor.Priority priority) {
++ 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, PrioritisedExecutor.Priority.NORMAL);
++ return this.scheduleChunkTask(chunkX, chunkZ, run, Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
-+ final PrioritisedExecutor.Priority priority) {
-+ return this.mainThreadExecutor.queueRunnable(run, priority);
++ 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();
-+ final long time = System.nanoTime();
+ if (sync) {
++ final long time = System.nanoTime();
+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
+ if (
+ !this.radiusAwareGenExecutor.isActive() &&
@@ -8341,6 +9104,29 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+ 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 {
@@ -8500,25 +9286,27 @@ index 0000000000000000000000000000000000000000..8671a90e969d16c7a57ddc38fedb7cf0
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
new file mode 100644
-index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7e451416f
+index 0000000000000000000000000000000000000000..381631e405895ba3eede1cd2e1011c64aadbd662
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-@@ -0,0 +1,2035 @@
+@@ -0,0 +1,1998 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
+
-+import ca.spottedleaf.concurrentutil.completable.Completable;
++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
+import ca.spottedleaf.concurrentutil.executor.Cancellable;
-+import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++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.ChunkSystemFeatures;
-+import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++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;
@@ -8542,13 +9330,14 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+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.ChunkSerializer;
++import net.minecraft.world.level.chunk.storage.SerializableChunkData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
@@ -8564,6 +9353,8 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+
+ 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;
@@ -8595,7 +9386,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ if (this.entityChunk == null) {
+ ret = this.entityChunk = new ChunkEntitySlices(
+ this.world, this.chunkX, this.chunkZ, this.getChunkStatus(),
-+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
++ this.holderData, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
+ );
+
+ ret.setTransient(transientChunk);
@@ -8679,7 +9470,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ // no tasks to schedule _for_
+ } else {
+ entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
+ );
+ entityDataLoadTask.addCallback(this::completeEntityLoad);
+ // need one schedule() per waiter
@@ -8726,7 +9517,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+
+ if (this.entityDataLoadTask == null) {
+ this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
+ );
+ this.entityDataLoadTask.addCallback(this::completeEntityLoad);
+ this.entityDataLoadTaskWaiters = new ArrayList<>();
@@ -8800,7 +9591,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ // no tasks to schedule _for_
+ } else {
+ poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
+ );
+ poiDataLoadTask.addCallback(this::completePoiLoad);
+ // need one schedule() per waiter
@@ -8846,7 +9637,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+
+ if (this.poiDataLoadTask == null) {
+ this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
++ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
+ );
+ this.poiDataLoadTask.addCallback(this::completePoiLoad);
+ this.poiDataLoadTaskWaiters = new ArrayList<>();
@@ -9025,15 +9816,15 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ // priority state
+
+ // the target priority for this chunk to generate at
-+ private PrioritisedExecutor.Priority priority = null;
++ private Priority priority = null;
+ private boolean priorityLocked;
+
+ // the priority neighbouring chunks have requested this chunk generate at
-+ private PrioritisedExecutor.Priority neighbourRequestedPriority = null;
++ private Priority neighbourRequestedPriority = null;
+
-+ public PrioritisedExecutor.Priority getEffectivePriority(final PrioritisedExecutor.Priority dfl) {
-+ final PrioritisedExecutor.Priority neighbour = this.neighbourRequestedPriority;
-+ final PrioritisedExecutor.Priority us = this.priority;
++ public Priority getEffectivePriority(final Priority dfl) {
++ final Priority neighbour = this.neighbourRequestedPriority;
++ final Priority us = this.priority;
+
+ if (neighbour == null) {
+ return us == null ? dfl : us;
@@ -9042,7 +9833,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ return neighbour;
+ }
+
-+ return PrioritisedExecutor.Priority.max(us, neighbour);
++ return Priority.max(us, neighbour);
+ }
+
+ private void recalculateNeighbourRequestedPriority() {
@@ -9051,18 +9842,18 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ return;
+ }
+
-+ PrioritisedExecutor.Priority max = null;
++ Priority max = null;
+
+ for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) {
-+ final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(null);
++ final Priority neighbourPriority = holder.getEffectivePriority(null);
+ if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) {
+ max = neighbourPriority;
+ }
+ }
+
-+ final PrioritisedExecutor.Priority current = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
++ final Priority current = this.getEffectivePriority(Priority.NORMAL);
+ this.neighbourRequestedPriority = max;
-+ final PrioritisedExecutor.Priority next = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
++ final Priority next = this.getEffectivePriority(Priority.NORMAL);
+
+ if (current == next) {
+ return;
@@ -9084,7 +9875,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ }
+
+ // must hold scheduling lock
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ public void raisePriority(final Priority priority) {
+ if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) {
+ return;
+ }
@@ -9097,13 +9888,13 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ }
+
+ // must hold scheduling lock
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ public void setPriority(final Priority priority) {
+ if (this.priorityLocked) {
+ return;
+ }
-+ final PrioritisedExecutor.Priority old = this.getEffectivePriority(null);
++ final Priority old = this.getEffectivePriority(null);
+ this.priority = priority;
-+ final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
++ final Priority newPriority = this.getEffectivePriority(Priority.NORMAL);
+
+ if (old != newPriority) {
+ if (this.generationTask != null) {
@@ -9115,7 +9906,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ }
+
+ // must hold scheduling lock
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ public void lowerPriority(final Priority priority) {
+ if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) {
+ return;
+ }
@@ -9138,7 +9929,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ }
+
+ // ticket level state
-+ public int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
++ private int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
+ private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
+
+ public int getTicketLevel() {
@@ -9157,6 +9948,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ 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() {
@@ -9256,8 +10048,9 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ /** Unloaded from chunk map */
+ private boolean unloaded;
+
-+ void markUnloaded() {
++ void onUnload() {
+ this.unloaded = true;
++ ((ChunkSystemLevel)this.world).moonrise$releaseChunkData(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ));
+ }
+
+ private boolean inUnloadQueue = false;
@@ -9294,9 +10087,10 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ private UnloadTask entityDataUnload;
+ private UnloadTask poiDataUnload;
+
-+ public static final record UnloadTask(Completable<CompoundTag> completable, DelayedPrioritisedTask task) {}
++ public static final record UnloadTask(CallbackCompletable<CompoundTag> completable, PrioritisedExecutor.PrioritisedTask task,
++ LazyRunnable toRun) {}
+
-+ public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) {
++ public UnloadTask getUnloadTask(final MoonriseRegionFileIO.RegionFileType type) {
+ switch (type) {
+ case CHUNK_DATA:
+ return this.chunkDataUnload;
@@ -9309,7 +10103,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ }
+ }
+
-+ private void removeUnloadTask(final RegionFileIOThread.RegionFileType type) {
++ private void removeUnloadTask(final MoonriseRegionFileIO.RegionFileType type) {
+ switch (type) {
+ case CHUNK_DATA: {
+ this.chunkDataUnload = null;
@@ -9342,10 +10136,10 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ // chunk state
+ this.currentChunk = null;
+ this.currentGenStatus = null;
-+ this.lastChunkCompletion = null;
+ for (int i = 0; i < this.chunkCompletions.length; ++i) {
-+ CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, (ChunkCompletion)null);
++ CHUNK_COMPLETION_ARRAY_HANDLE.setRelease(this.chunkCompletions, i, (ChunkCompletion)null);
+ }
++ this.lastChunkCompletion = null;
+ // entity chunk state
+ this.entityChunk = null;
+ this.pendingEntityChunk = null;
@@ -9357,22 +10151,23 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ this.priorityLocked = false;
+
+ if (chunk != null) {
-+ this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL));
++ 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 Completable<>(), null);
++ this.poiDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null);
+ }
+ if (entityChunk != null) {
-+ this.entityDataUnload = new UnloadTask(new Completable<>(), 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 RegionFileIOThread.RegionFileType type, final CompoundTag data) {
++ void completeAsyncUnloadDataSave(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) {
+ if (data != null) {
-+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type);
++ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type);
+ }
+
+ this.getUnloadTask(type).completable().complete(data);
@@ -9392,18 +10187,19 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ final ChunkEntitySlices entityChunk = state.entityChunk();
+ final PoiChunk poiChunk = state.poiChunk();
+
-+ final boolean shouldLevelChunkNotSave = ChunkSystemFeatures.forceNoSave(chunk);
++ 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(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
++ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null);
+ }
+
+ if (chunk instanceof LevelChunk levelChunk) {
@@ -9572,6 +10368,9 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ if (oldUnloaded != newUnloaded) {
+ this.checkUnload();
+ }
++
++ // Don't really have a choice but to place this hook here
++ PlatformHooks.get().onChunkHolderTicketChange(this.world, this, oldLevel, newLevel);
+ }
+
+ static final int NEIGHBOUR_RADIUS = 2;
@@ -9617,24 +10416,6 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1);
+ private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2);
+
-+ public static boolean areNeighboursFullLoaded(final long bitset, final int radius) {
-+ switch (radius) {
-+ case 0: {
-+ return (bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0;
-+ }
-+ case 1: {
-+ return (bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1;
-+ }
-+ case 2: {
-+ return (bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2;
-+ }
-+
-+ default: {
-+ throw new IllegalArgumentException("Radius not recognized: " + radius);
-+ }
-+ }
-+ }
-+
+ // 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
@@ -9869,6 +10650,17 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ }
+
+ 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);
@@ -9892,7 +10684,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
+ }
-+ }, PrioritisedExecutor.Priority.HIGHEST);
++ }, Priority.HIGHEST);
+ }
+
+ private final Reference2ObjectOpenHashMap<FullChunkStatus, List<Consumer<LevelChunk>>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>();
@@ -9920,7 +10712,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ LOGGER.error("Failed to process chunk status callback", thr);
+ }
+ }
-+ }, PrioritisedExecutor.Priority.HIGHEST);
++ }, Priority.HIGHEST);
+ }
+
+ // note: must hold scheduling lock
@@ -10176,6 +10968,8 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+
+ 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");
+
@@ -10183,6 +10977,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ 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
@@ -10192,17 +10987,22 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ poi = this.unloadState.poiChunk();
+ entities = this.unloadState.entityChunk();
+ }
-+ final UnloadTask chunkUnloadTask = this.chunkDataUnload;
-+ final DelayedPrioritisedTask chunkDataUnloadTask = chunkUnloadTask == null ? null : chunkUnloadTask.task();
-+ if (chunkDataUnloadTask != null) {
-+ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnloadTask.getTask();
-+ if (unloadTask != null) {
-+ executedUnloadTask = unloadTask.execute();
++ 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 = ChunkSystemFeatures.forceNoSave(chunk);
++ 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()));
@@ -10223,106 +11023,55 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ }
+ }
+
-+ return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null;
-+ }
-+
-+ static final class AsyncChunkSerializeTask implements Runnable {
-+
-+ private final ServerLevel world;
-+ private final ChunkAccess chunk;
-+ private final AsyncChunkSaveData asyncSaveData;
-+ private final NewChunkHolder toComplete;
-+
-+ public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData,
-+ final NewChunkHolder toComplete) {
-+ this.world = world;
-+ this.chunk = chunk;
-+ this.asyncSaveData = asyncSaveData;
-+ this.toComplete = toComplete;
-+ }
-+
-+ @Override
-+ public void run() {
-+ final CompoundTag toSerialize;
-+ try {
-+ toSerialize = ChunkSystemFeatures.saveChunkAsync(this.world, this.chunk, this.asyncSaveData);
-+ } catch (final Throwable throwable) {
-+ LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", throwable);
-+ final ChunkPos pos = this.chunk.getPos();
-+ ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> {
-+ final CompoundTag synchronousSave;
-+ try {
-+ synchronousSave = ChunkSystemFeatures.saveChunkAsync(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData);
-+ } catch (final Throwable throwable2) {
-+ LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "', chunk data will be lost", throwable2);
-+ AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
-+ return;
-+ }
-+
-+ AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, synchronousSave);
-+ LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "' synchronously");
-+
-+ }, PrioritisedExecutor.Priority.HIGHEST);
-+ return;
-+ }
-+ this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, toSerialize);
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "AsyncChunkSerializeTask{" +
-+ "chunk={pos=" + this.chunk.getPos() + ",world=\"" + WorldUtil.getWorldName(this.world) + "\"}" +
-+ "}";
-+ }
++ 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(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
++ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null);
+ }
+ return false;
+ }
-+ boolean completing = false;
-+ boolean failedAsyncPrepare = false;
+ try {
-+ if (unloading && ChunkSystemFeatures.supportsAsyncChunkSave()) {
-+ try {
-+ final AsyncChunkSaveData asyncSaveData = ChunkSystemFeatures.getAsyncSaveData(this.world, chunk);
++ final SerializableChunkData chunkData = SerializableChunkData.copyOf(this.world, chunk);
++ PlatformHooks.get().chunkSyncSave(this.world, chunk, chunkData);
+
-+ final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this));
++ chunk.tryMarkSaved();
+
-+ this.chunkDataUnload.task().setTask(task);
++ final CallbackCompletable<CompoundTag> completable = new CallbackCompletable<>();
+
-+ chunk.setUnsaved(false);
++ final Runnable run = () -> {
++ final CompoundTag data = chunkData.write();
+
-+ task.queue();
++ completable.complete(data);
+
-+ return true;
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", thr);
-+ failedAsyncPrepare = true;
-+ // fall through to synchronous save
++ if (unloading) {
++ NewChunkHolder.this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, data);
+ }
-+ }
-+
-+ final CompoundTag save = ChunkSerializer.write(this.world, chunk);
++ };
+
++ final PrioritisedExecutor.PrioritisedTask task;
+ if (unloading) {
-+ completing = true;
-+ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, save);
-+ if (failedAsyncPrepare) {
-+ LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "' synchronously");
-+ }
++ this.chunkDataUnload.toRun().setRunnable(run);
++ task = this.chunkDataUnload.task();
+ } else {
-+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA);
++ task = this.scheduler.saveExecutor.createTask(run);
+ }
-+ chunk.setUnsaved(false);
++
++ 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);
-+ if (unloading && !completing) {
-+ this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
-+ }
+ }
+
+ return true;
@@ -10340,7 +11089,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ return false;
+ }
+ try {
-+ mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING);
++ 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);
+ }
@@ -10359,7 +11108,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ return false;
+ }
+
-+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA);
++ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA);
+ this.lastEntitySaveNull = save == null;
+ if (unloading) {
+ this.lastEntityUnload = save;
@@ -10383,7 +11132,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ return false;
+ }
+
-+ RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA);
++ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.POI_DATA);
+ this.lastPoiSaveNull = save == null;
+ if (unloading) {
+ this.poiDataUnload.completable().complete(save);
@@ -10430,7 +11179,7 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString());
+ }
+
-+ private static JsonObject serializeCompletable(final Completable<?> completable) {
++ private static JsonObject serializeCompletable(final CallbackCompletable<?> completable) {
+ final JsonObject ret = new JsonObject();
+
+ if (completable == null) {
@@ -10525,13 +11274,13 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+ ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable()));
+ ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable()));
+
-+ final DelayedPrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task();
++ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task();
+ if (unloadTask == null) {
+ ret.addProperty("unload_task_priority", "null");
-+ ret.addProperty("unload_task_priority_raw", "null");
++ ret.addProperty("unload_task_suborder", Long.valueOf(0L));
+ } else {
+ ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority()));
-+ ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal()));
++ ret.addProperty("unload_task_suborder", Long.valueOf(unloadTask.getSubOrder()));
+ }
+
+ ret.addProperty("killed", Boolean.valueOf(this.unloaded));
@@ -10541,14 +11290,14 @@ index 0000000000000000000000000000000000000000..45eda96fd8a1acb87dbb69ce5495fec7
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
new file mode 100644
-index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c7aa6a3a8
+index 0000000000000000000000000000000000000000..6b468c621b74449a6218391f6477cf63cfc98c7c
--- /dev/null
+++ b/src/main/java/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.executor.standard.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Priority;
+import java.lang.invoke.VarHandle;
+
+public abstract class PriorityHolder {
@@ -10575,8 +11324,8 @@ index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c
+ PRIORITY_HANDLE.set((PriorityHolder)this, (int)val);
+ }
+
-+ protected PriorityHolder(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ protected PriorityHolder(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.setPriorityPlain(priority.priority);
@@ -10616,7 +11365,7 @@ index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c
+ return;
+ }
+
-+ this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority));
++ this.scheduleTask(Priority.getPriority(priority));
+
+ int failures = 0;
+ for (;;) {
@@ -10633,7 +11382,7 @@ index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c
+ return;
+ }
+
-+ this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority));
++ this.setPriorityScheduled(Priority.getPriority(priority));
+
+ ++failures;
+ for (int i = 0; i < failures; ++i) {
@@ -10642,19 +11391,19 @@ index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c
+ }
+ }
+
-+ public final PrioritisedExecutor.Priority getPriority() {
++ public final Priority getPriority() {
+ final int ret = this.getPriorityVolatile();
+ if ((ret & PRIORITY_EXECUTED) != 0) {
-+ return PrioritisedExecutor.Priority.COMPLETING;
++ return Priority.COMPLETING;
+ }
+ if ((ret & PRIORITY_SCHEDULED) != 0) {
+ return this.getScheduledPriority();
+ }
-+ return PrioritisedExecutor.Priority.getPriority(ret);
++ return Priority.getPriority(ret);
+ }
+
-+ public final void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public final void lowerPriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
@@ -10686,8 +11435,8 @@ index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c
+ }
+ }
+
-+ public final void setPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public final void setPriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
@@ -10715,8 +11464,8 @@ index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c
+ }
+ }
+
-+ public final void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public final void raisePriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
@@ -10750,15 +11499,15 @@ index 0000000000000000000000000000000000000000..261e09454f49d04eb159c984ec695d7c
+
+ protected abstract void cancelScheduled();
+
-+ protected abstract PrioritisedExecutor.Priority getScheduledPriority();
++ protected abstract Priority getScheduledPriority();
+
-+ protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority);
++ protected abstract void scheduleTask(final Priority priority);
+
-+ protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority);
++ protected abstract void lowerPriorityScheduled(final Priority priority);
+
-+ protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority);
++ protected abstract void setPriorityScheduled(final Priority priority);
+
-+ protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority);
++ protected abstract void raisePriorityScheduled(final Priority priority);
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ThreadedTicketLevelPropagator.java
new file mode 100644
@@ -12225,17 +12974,17 @@ index 0000000000000000000000000000000000000000..310a8f80debadd64c2d962ebf83b7d05
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
new file mode 100644
-index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373ba811ba8b
+index 0000000000000000000000000000000000000000..5f4b99d8c5453f8ad2e600a57ea4e7dafa2d45f8
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
-@@ -0,0 +1,668 @@
+@@ -0,0 +1,729 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor;
+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++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;
@@ -12247,17 +12996,38 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ return Long.compare(t1.id, t2.id);
+ };
+
-+ private final DependencyTree[] queues = new DependencyTree[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES];
++ 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, 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;
+ }
@@ -12287,7 +13057,7 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ return null;
+ }
+
-+ private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final PrioritisedExecutor.Priority priority) {
++ private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final Priority priority) {
+ final int priorityId = priority.priority;
+ final DependencyTree queue = this.queues[priorityId];
+
@@ -12310,7 +13080,7 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ return null;
+ }
+
-+ if (PrioritisedExecutor.Priority.isHigherPriority(priorityId, this.selectedQueue)) {
++ if (Priority.isHigherPriority(priorityId, this.selectedQueue)) {
+ // prevent the lower priority tree from queueing more tasks
+ this.canQueueTasks = false;
+ return null;
@@ -12321,7 +13091,7 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius,
-+ final Runnable run, final PrioritisedExecutor.Priority priority) {
++ final Runnable run, final Priority priority) {
+ if (radius < 0) {
+ throw new IllegalArgumentException("Radius must be > 0: " + radius);
+ }
@@ -12330,11 +13100,11 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+
+ public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius,
+ final Runnable run) {
-+ return this.createTask(chunkX, chunkZ, radius, run, PrioritisedExecutor.Priority.NORMAL);
++ 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 PrioritisedExecutor.Priority priority) {
++ final Runnable run, final Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority);
+
+ ret.queue();
@@ -12351,15 +13121,15 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ return ret;
+ }
+
-+ public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ 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, PrioritisedExecutor.Priority.NORMAL);
++ return this.createInfiniteRadiusTask(run, Priority.NORMAL);
+ }
+
-+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority);
+
+ ret.queue();
@@ -12368,20 +13138,27 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) {
-+ final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL);
++ 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 final int maxToSchedule;
-+ private final int treeIndex;
++ private int maxToSchedule;
+
+ private int currentlyExecuting;
+ private long idGenerator;
@@ -12394,11 +13171,10 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ private final Long2ReferenceOpenHashMap<DependencyNode> nodeByPosition = new Long2ReferenceOpenHashMap<>();
+
+ public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor,
-+ final int maxToSchedule, final int treeIndex) {
++ final int maxToSchedule) {
+ this.scheduler = scheduler;
+ this.executor = executor;
+ this.maxToSchedule = maxToSchedule;
-+ this.treeIndex = treeIndex;
+ }
+
+ public boolean hasWaitingTasks() {
@@ -12643,13 +13419,13 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ private final int chunkZ;
+ private final int radius;
+ private Runnable run;
-+ private PrioritisedExecutor.Priority priority;
++ 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 PrioritisedExecutor.Priority priority) {
++ final Runnable run, final Priority priority) {
+ this.scheduler = scheduler;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
@@ -12672,14 +13448,6 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ run.run();
+ }
+
-+ 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();
-+ }
-+ }
-+ }
-+
+ private void returnNode() {
+ final List<PrioritisedExecutor.PrioritisedTask> toSchedule;
+ synchronized (this.scheduler) {
@@ -12692,6 +13460,11 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ }
+
+ @Override
++ public PrioritisedExecutor getExecutor() {
++ return this.scheduler.executor;
++ }
++
++ @Override
+ public void run() {
+ final Runnable run = this.run;
+ this.run = null;
@@ -12706,7 +13479,7 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ public boolean queue() {
+ final List<PrioritisedExecutor.PrioritisedTask> toSchedule;
+ synchronized (this.scheduler) {
-+ if (this.queuedTask != null || this.dependencyNode != null || this.priority == PrioritisedExecutor.Priority.COMPLETING) {
++ if (this.queuedTask != null || this.dependencyNode != null || this.priority == Priority.COMPLETING) {
+ return false;
+ }
+
@@ -12718,15 +13491,22 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ }
+
+ @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 == PrioritisedExecutor.Priority.COMPLETING) {
++ if (this.priority == Priority.COMPLETING) {
+ return false;
+ }
+
-+ this.priority = PrioritisedExecutor.Priority.COMPLETING;
++ this.priority = Priority.COMPLETING;
+ if (this.dependencyNode != null) {
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
@@ -12750,11 +13530,11 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ final PrioritisedExecutor.PrioritisedTask task;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
-+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
++ if (this.priority == Priority.COMPLETING) {
+ return false;
+ }
+
-+ this.priority = PrioritisedExecutor.Priority.COMPLETING;
++ this.priority = Priority.COMPLETING;
+ if (this.dependencyNode != null) {
+ this.dependencyNode.purged = true;
+ this.dependencyNode = null;
@@ -12774,7 +13554,7 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ }
+
+ @Override
-+ public PrioritisedExecutor.Priority getPriority() {
++ public Priority getPriority() {
+ final PrioritisedExecutor.PrioritisedTask task;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
@@ -12786,8 +13566,8 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ }
+
+ @Override
-+ public boolean setPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public boolean setPriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
@@ -12795,7 +13575,7 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
-+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
++ if (this.priority == Priority.COMPLETING) {
+ return false;
+ }
+
@@ -12823,8 +13603,8 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ }
+
+ @Override
-+ public boolean raisePriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public boolean raisePriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
@@ -12832,7 +13612,7 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
-+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
++ if (this.priority == Priority.COMPLETING) {
+ return false;
+ }
+
@@ -12860,8 +13640,8 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ }
+
+ @Override
-+ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public boolean lowerPriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
@@ -12869,7 +13649,7 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+ List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
+ synchronized (this.scheduler) {
+ if ((task = this.queuedTask) == null) {
-+ if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
++ if (this.priority == Priority.COMPLETING) {
+ return false;
+ }
+
@@ -12895,19 +13675,50 @@ index 0000000000000000000000000000000000000000..e0b26ccb63596748b80fc6a5e47e373b
+
+ 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);
++ }
+ }
+}
-\ No newline at end of file
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
new file mode 100644
-index 0000000000000000000000000000000000000000..fbdf721e8b4cfe6cef4ee60c53c680cbfc858d88
+index 0000000000000000000000000000000000000000..6ab353b0d2465c3680bb3c8d0852ba0f65c00fd2
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
-@@ -0,0 +1,142 @@
+@@ -0,0 +1,151 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++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;
@@ -12935,7 +13746,7 @@ index 0000000000000000000000000000000000000000..fbdf721e8b4cfe6cef4ee60c53c680cb
+ 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 PrioritisedExecutor.Priority priority) {
++ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final Priority priority) {
+ super(scheduler, world, chunkX, chunkZ);
+ this.chunkHolder = chunkHolder;
+ this.fromChunk = fromChunk;
@@ -12949,6 +13760,8 @@ index 0000000000000000000000000000000000000000..fbdf721e8b4cfe6cef4ee60c53c680cb
+
+ @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 {
@@ -12967,7 +13780,7 @@ index 0000000000000000000000000000000000000000..fbdf721e8b4cfe6cef4ee60c53c680cb
+ final ServerLevel world = this.world;
+ final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk;
+ chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> {
-+ ChunkStatusTasks.postLoadProtoChunk(world, protoChunk.getEntities(), protoChunk.getPos()); // Paper - pass chunk pos
++ PlatformHooks.get().postLoadProtoChunk(world, protoChunk);
+ });
+ this.chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false));
+ }
@@ -12977,16 +13790,21 @@ index 0000000000000000000000000000000000000000..fbdf721e8b4cfe6cef4ee60c53c680cb
+ final NewChunkHolder chunkHolder = this.chunkHolder;
+
+ chunk.setFullStatus(chunkHolder::getChunkStatus);
-+ 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);
-+
-+ // we don't need the entitiesInLevel, not sure why it's there
-+ chunk.setLoaded(true);
-+ chunk.registerAllBlockEntitiesAfterLevelLoad();
-+ chunk.registerTickContainerInLevel(this.world);
++ 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;
@@ -13018,29 +13836,29 @@ index 0000000000000000000000000000000000000000..fbdf721e8b4cfe6cef4ee60c53c680cb
+ }
+
+ @Override
-+ public PrioritisedExecutor.Priority getPriority() {
++ public Priority getPriority() {
+ return this.convertToFullTask.getPriority();
+ }
+
+ @Override
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ 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 PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ 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 PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public void raisePriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.convertToFullTask.raisePriority(priority);
@@ -13048,13 +13866,13 @@ index 0000000000000000000000000000000000000000..fbdf721e8b4cfe6cef4ee60c53c680cb
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
new file mode 100644
-index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8a938922f
+index 0000000000000000000000000000000000000000..4538ccfaea83d217ed85eaf16e82393c7f286489
--- /dev/null
+++ b/src/main/java/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.executor.standard.PrioritisedExecutor;
++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;
@@ -13079,9 +13897,9 @@ index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8
+ private final LightTaskPriorityHolder priorityHolder;
+
+ public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
-+ final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) {
++ final ChunkAccess chunk, final Priority priority) {
+ super(scheduler, world, chunkX, chunkZ);
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.priorityHolder = new LightTaskPriorityHolder(priority, this);
@@ -13109,22 +13927,22 @@ index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8
+ }
+
+ @Override
-+ public PrioritisedExecutor.Priority getPriority() {
++ public Priority getPriority() {
+ return this.priorityHolder.getPriority();
+ }
+
+ @Override
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ public void lowerPriority(final Priority priority) {
+ this.priorityHolder.raisePriority(priority);
+ }
+
+ @Override
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ public void setPriority(final Priority priority) {
+ this.priorityHolder.setPriority(priority);
+ }
+
+ @Override
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ public void raisePriority(final Priority priority) {
+ this.priorityHolder.raisePriority(priority);
+ }
+
@@ -13132,7 +13950,7 @@ index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8
+
+ private final ChunkLightTask task;
+
-+ private LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) {
++ private LightTaskPriorityHolder(final Priority priority, final ChunkLightTask task) {
+ super(priority);
+ this.task = task;
+ }
@@ -13144,13 +13962,13 @@ index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8
+ }
+
+ @Override
-+ protected PrioritisedExecutor.Priority getScheduledPriority() {
++ 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 PrioritisedExecutor.Priority priority) {
++ 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();
@@ -13159,7 +13977,7 @@ index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8
+ }
+
+ @Override
-+ protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) {
++ 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();
@@ -13167,7 +13985,7 @@ index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8
+ }
+
+ @Override
-+ protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) {
++ 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();
@@ -13175,7 +13993,7 @@ index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8
+ }
+
+ @Override
-+ protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) {
++ 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();
@@ -13235,19 +14053,20 @@ index 0000000000000000000000000000000000000000..7c2e6752228fac175c4aa97fa3d817b8
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
new file mode 100644
-index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13091425f8
+index 0000000000000000000000000000000000000000..e0a88615a8b6d58191f29b1ff1a26427f0a4c1a6
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
-@@ -0,0 +1,487 @@
+@@ -0,0 +1,494 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++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.ChunkSystemFeatures;
-+import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
++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;
@@ -13259,7 +14078,7 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+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.ChunkSerializer;
++import net.minecraft.world.level.chunk.storage.SerializableChunkData;
+import net.minecraft.world.level.levelgen.blending.BlendingData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
@@ -13282,7 +14101,7 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ 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 PrioritisedExecutor.Priority priority) {
++ final NewChunkHolder chunkHolder, final Priority priority) {
+ super(scheduler, world, chunkX, chunkZ);
+ this.chunkHolder = chunkHolder;
+ this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority);
@@ -13411,12 +14230,12 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ }
+
+ @Override
-+ public PrioritisedExecutor.Priority getPriority() {
++ public Priority getPriority() {
+ return this.loadTask.getPriority();
+ }
+
+ @Override
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ public void lowerPriority(final Priority priority) {
+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
+ if (entityLoad != null) {
+ entityLoad.lowerPriority(priority);
@@ -13432,7 +14251,7 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ }
+
+ @Override
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ public void setPriority(final Priority priority) {
+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
+ if (entityLoad != null) {
+ entityLoad.setPriority(priority);
@@ -13448,7 +14267,7 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ }
+
+ @Override
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ public void raisePriority(final Priority priority) {
+ final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
+ if (entityLoad != null) {
+ entityLoad.raisePriority(priority);
@@ -13472,8 +14291,8 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ 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 RegionFileIOThread.RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
++ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type,
++ final Priority priority) {
+ super(scheduler, world, chunkX, chunkZ, type, priority);
+ }
+
@@ -13513,10 +14332,13 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ }
+ }
+
-+ private static final class ChunkDataLoadTask extends CallbackDataLoadTask<CompoundTag, ChunkAccess> {
++
++ 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 PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority);
++ final int chunkZ, final Priority priority) {
++ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, priority);
+ }
+
+ @Override
@@ -13530,40 +14352,42 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ }
+
+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ 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 PrioritisedExecutor.Priority priority) {
++ 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 CompoundTag data, final Throwable throwable) {
++ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final ReadChunk data, final Throwable throwable) {
+ if (throwable != null) {
+ return new TaskResult<>(null, throwable);
+ }
-+ if (data == null) {
++
++ if (data == null || data.protoChunk() == null) {
+ return new TaskResult<>(this.getEmptyChunk(), null);
+ }
+
-+ if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) {
-+ return this.deserialize(data);
++ if (!PlatformHooks.get().hasMainChunkLoadHook()) {
++ return new TaskResult<>(data.protoChunk(), null);
+ }
-+ // need to deserialize on main thread
++
++ // 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().registryOrThrow(Registries.BIOME), (BlendingData)null
++ this.world.registryAccess().lookupOrThrow(Registries.BIOME), (BlendingData)null
+ );
+ }
+
+ @Override
-+ protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
++ 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);
@@ -13575,42 +14399,43 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+
+ try {
+ // run converters
-+ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new net.minecraft.world.level.ChunkPos(this.chunkX, this.chunkZ));
++ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data);
+
-+ return new TaskResult<>(converted, 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);
-+ }
-+ }
++ // unpack the data
++ final SerializableChunkData chunkData = SerializableChunkData.parse(
++ this.world, this.world.registryAccess(), converted
++ );
+
-+ private TaskResult<ChunkAccess, Throwable> deserialize(final CompoundTag data) {
-+ try {
-+ final ChunkAccess deserialized = ChunkSerializer.read(
-+ this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), new ChunkPos(this.chunkX, this.chunkZ), data
++ 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<>(deserialized, null);
++
++ 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<>(this.getEmptyChunk(), null);
++ return new TaskResult<>(null, null);
+ }
+ }
+
+ @Override
-+ protected TaskResult<ChunkAccess, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) {
-+ // data != null && throwable == null
-+ if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) {
-+ throw new UnsupportedOperationException();
-+ }
-+ return this.deserialize(data);
++ 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 PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority);
++ final int chunkZ, final Priority priority) {
++ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.POI_DATA, priority);
+ }
+
+ @Override
@@ -13624,12 +14449,12 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ }
+
+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ 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 PrioritisedExecutor.Priority priority) {
++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) {
+ throw new UnsupportedOperationException();
+ }
+
@@ -13671,8 +14496,8 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ public static final class EntityDataLoadTask extends CallbackDataLoadTask<CompoundTag, CompoundTag> {
+
+ public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final PrioritisedExecutor.Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority);
++ final int chunkZ, final Priority priority) {
++ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, priority);
+ }
+
+ @Override
@@ -13686,12 +14511,12 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+ }
+
+ @Override
-+ protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
++ 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 PrioritisedExecutor.Priority priority) {
++ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) {
+ throw new UnsupportedOperationException();
+ }
+
@@ -13728,15 +14553,15 @@ index 0000000000000000000000000000000000000000..1ab93f219246d0b4dcdfd0f685f47c13
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
new file mode 100644
-index 0000000000000000000000000000000000000000..70e900b0f9c131900bf8b3f3ecbfbd5df5361205
+index 0000000000000000000000000000000000000000..002ee365aa70d8e6a6e6bd5c95988bd17db4395a
--- /dev/null
+++ b/src/main/java/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.executor.standard.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.scheduling.ChunkTaskScheduler;
+import net.minecraft.server.level.ServerLevel;
@@ -13780,15 +14605,15 @@ index 0000000000000000000000000000000000000000..70e900b0f9c131900bf8b3f3ecbfbd5d
+ /* May be called multiple times */
+ public abstract void cancel();
+
-+ public abstract PrioritisedExecutor.Priority getPriority();
++ public abstract Priority getPriority();
+
+ /* Schedule lock is always held for the priority update calls */
+
-+ public abstract void lowerPriority(final PrioritisedExecutor.Priority priority);
++ public abstract void lowerPriority(final Priority priority);
+
-+ public abstract void setPriority(final PrioritisedExecutor.Priority priority);
++ public abstract void setPriority(final Priority priority);
+
-+ public abstract void raisePriority(final PrioritisedExecutor.Priority priority);
++ public abstract void raisePriority(final Priority priority);
+
+ public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) {
+ if (!this.waiters.add(onComplete)) {
@@ -13836,14 +14661,15 @@ index 0000000000000000000000000000000000000000..70e900b0f9c131900bf8b3f3ecbfbd5d
\ No newline at end of file
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
new file mode 100644
-index 0000000000000000000000000000000000000000..2c17d5589f15f1155be08be670d29acbe954a8fa
+index 0000000000000000000000000000000000000000..25d8da4773dcee5096053e7e3788bfc224d705a7
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
-@@ -0,0 +1,217 @@
+@@ -0,0 +1,218 @@
+package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
+
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++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;
@@ -13878,9 +14704,9 @@ index 0000000000000000000000000000000000000000..2c17d5589f15f1155be08be670d29acb
+
+ 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 PrioritisedExecutor.Priority priority) {
++ final ChunkStatus toStatus, final Priority priority) {
+ super(scheduler, world, chunkX, chunkZ);
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.fromChunk = chunk;
@@ -14029,29 +14855,29 @@ index 0000000000000000000000000000000000000000..2c17d5589f15f1155be08be670d29acb
+ }
+
+ @Override
-+ public PrioritisedExecutor.Priority getPriority() {
++ public Priority getPriority() {
+ return this.generateTask.getPriority();
+ }
+
+ @Override
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ 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 PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ 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 PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public void raisePriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.generateTask.raisePriority(priority);
@@ -14059,19 +14885,20 @@ index 0000000000000000000000000000000000000000..2c17d5589f15f1155be08be670d29acb
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
new file mode 100644
-index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be314b876c
+index 0000000000000000000000000000000000000000..bdcd1879457bafcca4e76523aac0555968f37c0b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
-@@ -0,0 +1,673 @@
+@@ -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.standard.DelayedPrioritisedTask;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++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.RegionFileIOThread;
++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;
@@ -14112,11 +14939,11 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ protected final ServerLevel world;
+ protected final int chunkX;
+ protected final int chunkZ;
-+ protected final RegionFileIOThread.RegionFileType type;
++ protected final MoonriseRegionFileIO.RegionFileType type;
+
+ public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-+ final int chunkZ, final RegionFileIOThread.RegionFileType type,
-+ final PrioritisedExecutor.Priority priority) {
++ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type,
++ final Priority priority) {
+ this.scheduler = scheduler;
+ this.world = world;
+ this.chunkX = chunkX;
@@ -14154,9 +14981,9 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+
+ protected abstract boolean hasOnMain();
+
-+ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority);
++ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority);
+
-+ protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.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);
+
@@ -14173,7 +15000,7 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ ", type: " + this.type.toString() + "}";
+ }
+
-+ public PrioritisedExecutor.Priority getPriority() {
++ public Priority getPriority() {
+ if (this.processOnMain != null) {
+ return this.processOnMain.getPriority();
+ } else {
@@ -14181,7 +15008,7 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ }
+ }
+
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ 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);
@@ -14191,7 +15018,7 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ }
+ }
+
-+ public void setPriority(final PrioritisedExecutor.Priority 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) {
@@ -14202,7 +15029,7 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ }
+ }
+
-+ public void raisePriority(final PrioritisedExecutor.Priority 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) {
@@ -14447,10 +15274,10 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ private final int chunkX;
+ private final int chunkZ;
+
-+ private final RegionFileIOThread.RegionFileType type;
++ private final MoonriseRegionFileIO.RegionFileType type;
+ private Cancellable dataLoadTask;
+ private Cancellable dataUnloadCancellable;
-+ private DelayedPrioritisedTask dataUnloadTask;
++ private PrioritisedExecutor.PrioritisedTask dataUnloadTask;
+
+ private final BiConsumer<CompoundTag, Throwable> onComplete;
+ private final AtomicBoolean scheduled = new AtomicBoolean();
@@ -14458,10 +15285,10 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ // 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 RegionFileIOThread.RegionFileType type,
++ final MoonriseRegionFileIO.RegionFileType type,
+ final BiConsumer<CompoundTag, Throwable> onComplete,
-+ final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+ this.world = world;
@@ -14491,8 +15318,8 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
+ }
+
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public void lowerPriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
@@ -14504,7 +15331,7 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ }
+
+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-+ RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
++ MoonriseRegionFileIO.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
+ return;
+ }
+
@@ -14532,8 +15359,8 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ }
+ }
+
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public void setPriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
@@ -14545,7 +15372,7 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ }
+
+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
++ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
+ return;
+ }
+
@@ -14569,8 +15396,8 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ }
+ }
+
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++ public void raisePriority(final Priority priority) {
++ if (!Priority.isValidPriority(priority)) {
+ throw new IllegalArgumentException("Invalid priority " + priority);
+ }
+
@@ -14582,7 +15409,7 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ }
+
+ if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-+ RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
++ MoonriseRegionFileIO.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
+ return;
+ }
+
@@ -14648,7 +15475,7 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ } // else: cancelled
+ };
+
-+ final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority);
++ final Priority initialPriority = Priority.getPriority(priority);
+ boolean scheduledUnload = false;
+
+ final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ);
@@ -14658,13 +15485,13 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ consumer.accept(data, null);
+ } else {
+ // need to schedule task
-+ LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS));
++ 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 Completable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable();
++ final CallbackCompletable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable();
+ if (unloadCompletable != null) {
+ unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer);
+ if (unloadCancellable == null) {
@@ -14687,7 +15514,7 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ this.schedule(scheduledUnload, consumer, initialPriority);
+ }
+
-+ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final PrioritisedExecutor.Priority initialPriority) {
++ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final Priority initialPriority) {
+ int priority = this.getPriorityVolatile();
+
+ if ((priority & PRIORITY_EXECUTED) != 0) {
@@ -14696,9 +15523,9 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+ }
+
+ if (!scheduledUnload) {
-+ this.dataLoadTask = RegionFileIOThread.loadDataAsync(
++ this.dataLoadTask = MoonriseRegionFileIO.loadDataAsync(
+ this.world, this.chunkX, this.chunkZ, this.type, consumer,
-+ initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority
++ initialPriority.isHigherPriority(Priority.NORMAL), initialPriority
+ );
+ }
+
@@ -14722,10 +15549,10 @@ index 0000000000000000000000000000000000000000..7a65d351b448873c6f2c145c975c92be
+
+ if (scheduledUnload) {
+ if (this.dataUnloadTask != null) {
-+ this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
++ this.dataUnloadTask.setPriority(Priority.getPriority(priority & ~PRIORITY_FLAGS));
+ }
+ } else {
-+ RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
++ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, Priority.getPriority(priority & ~PRIORITY_FLAGS));
+ }
+
+ ++failures;
@@ -14766,6 +15593,24 @@ index 0000000000000000000000000000000000000000..ea759ce6f10f2a5a4e107ab7528030fe
+ public ChunkStatus moonrise$getRequiredStatusAtRadius(final int radius);
+
+}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..51c126735ace8fdde89ad97b5cab62f244212db0
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796ce55e1875
@@ -14781,6 +15626,24 @@ index 0000000000000000000000000000000000000000..129a35ff2db5b3bb6736810fc180796c
+ public RegionFileStorage moonrise$getRegionStorage();
+
+}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..3bd1b59250dbab15097a64d515999b278636795a
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ticket/ChunkSystemTicket.java
new file mode 100644
index 0000000000000000000000000000000000000000..786e6ad17cd6216ef0aadaa7cf10044a0c19c933
@@ -14834,13 +15697,14 @@ index 0000000000000000000000000000000000000000..ce3bb903c9ccb7efa0f004cf79b291dc
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
new file mode 100644
-index 0000000000000000000000000000000000000000..3a9a564edfdb99e006e4816cb8821bd1e9ecff43
+index 0000000000000000000000000000000000000000..93fd23027c00cef76562098306737272fda1350a
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
-@@ -0,0 +1,320 @@
+@@ -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;
@@ -14853,7 +15717,7 @@ index 0000000000000000000000000000000000000000..3a9a564edfdb99e006e4816cb8821bd1
+
+ // 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[64+2+1][];
++ 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
@@ -15158,6 +16022,49 @@ index 0000000000000000000000000000000000000000..3a9a564edfdb99e006e4816cb8821bd1
+ return ret.elements();
+ }
+}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..7ef3dcca89ed7578c6c0f5565131889110063056
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/world/ChunkSystemEntityGetter.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea6b6ed27b212719feb31610faac974899688839
@@ -15230,22 +16137,65 @@ index 0000000000000000000000000000000000000000..f28fd0e01e2bdda0daf9d775e514a725
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
new file mode 100644
-index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a27385126
+index 0000000000000000000000000000000000000000..3abd4ad6379c383c3a31931255292b42d9435694
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-@@ -0,0 +1,1853 @@
+@@ -0,0 +1,2189 @@
+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.item.Item;
++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 it.unimi.dsi.fastutil.doubles.DoubleArrayList ZERO_ONE = it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(new double[] { 0.0, 1.0 });
++ 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() == net.minecraft.world.level.block.Blocks.MOVING_PISTON;
++ return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON;
+ }
+
-+ public static boolean isEmpty(final net.minecraft.world.phys.AABB aabb) {
++ 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;
+ }
+
@@ -15254,11 +16204,11 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON;
+ }
+
-+ public static net.minecraft.world.phys.AABB getBoxForChunk(final int chunkX, final int chunkZ) {
++ 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 net.minecraft.world.phys.AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON,
++ 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));
+ }
+
@@ -15279,21 +16229,21 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON;
+ }
+
-+ public static boolean voxelShapeIntersect(final net.minecraft.world.phys.AABB box, final double minX, final double minY, final double minZ,
++ 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 net.minecraft.world.phys.AABB box1, final net.minecraft.world.phys.AABB box2) {
++ 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 net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) {
++ 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) {
@@ -15314,7 +16264,7 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+
+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideY(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) {
++ 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) {
@@ -15335,7 +16285,7 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+
+ // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideZ(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) {
++ 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) {
@@ -15357,7 +16307,8 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+
+ // startIndex and endIndex inclusive
+ // assumes indices are in range of array
-+ private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) {
++ public static int findFloor(final double[] values, 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];
@@ -15372,7 +16323,217 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return startIndex - 1;
+ }
+
-+ public static boolean voxelShapeIntersectNoEmpty(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.AABB aabb) {
++ 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;
+ }
@@ -15380,15 +16541,15 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ // 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetX();
-+ final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetY();
-+ final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetZ();
++ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ();
++ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
+
+ // note: size = coords.length - 1
+ final int size_x = cached_shape_data.sizeX();
@@ -15482,23 +16643,23 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+
+ // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON
-+ public static double collideX(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) {
-+ final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation();
++ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX();
-+ final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY();
-+ final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ();
++ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ();
++ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData();
++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData();
+
+ // note: size = coords.length - 1
+ final int size_x = cached_shape_data.sizeX();
@@ -15640,23 +16801,23 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+ }
+
-+ public static double collideY(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) {
-+ final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation();
++ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX();
-+ final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY();
-+ final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ();
++ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ();
++ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData();
++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData();
+
+ // note: size = coords.length - 1
+ final int size_x = cached_shape_data.sizeX();
@@ -15798,23 +16959,23 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+ }
+
-+ public static double collideZ(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) {
-+ final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation();
++ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX();
-+ final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY();
-+ final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ();
++ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ();
++ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData();
++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData();
+
+ // note: size = coords.length - 1
+ final int size_x = cached_shape_data.sizeX();
@@ -15957,13 +17118,13 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+
+ // does not use epsilon
-+ public static boolean strictlyContains(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.Vec3 point) {
++ 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 net.minecraft.world.phys.shapes.VoxelShape voxel, double x, double y, double z) {
-+ final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation();
++ public static boolean strictlyContains(final VoxelShape voxel, double x, double y, double z) {
++ final AABB single_aabb = ((CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation();
+ if (single_aabb != null) {
+ return single_aabb.contains(x, y, z);
+ }
@@ -15974,15 +17135,15 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+
+ // offset input
-+ x -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetX();
-+ y -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetY();
-+ z -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetZ();
++ x -= ((CollisionVoxelShape)voxel).moonrise$offsetX();
++ y -= ((CollisionVoxelShape)voxel).moonrise$offsetY();
++ z -= ((CollisionVoxelShape)voxel).moonrise$offsetZ();
+
-+ final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesX();
-+ final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesY();
-+ final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ();
++ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
++ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
+
+ // note: size = coords.length - 1
+ final int size_x = cached_shape_data.sizeX();
@@ -16024,10 +17185,10 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3);
+ }
+
-+ private static net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape merge(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst, final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond,
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX, final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY,
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ,
-+ final int booleanOp) {
++ 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;
@@ -16042,7 +17203,7 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY();
+
+ // note: indices may contain -1, but nothing > size
-+ final net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape ret = new net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ);
++ final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ);
+
+ boolean empty = true;
+
@@ -16059,10 +17220,11 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ final int s1z = mergedZ.firstIndices[idxZ];
+ final int s2z = mergedZ.secondIndices[idxZ];
+
-+ int idx;
++ int idx1;
++ int idx2;
+
-+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L);
-+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L);
++ 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
@@ -16097,9 +17259,9 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return empty ? null : ret;
+ }
+
-+ private static boolean isMergeEmpty(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst, final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond,
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX, final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY,
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ,
++ 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;
@@ -16125,10 +17287,11 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ final int s1z = mergedZ.firstIndices[idxZ];
+ final int s2z = mergedZ.secondIndices[idxZ];
+
-+ int idx;
++ int idx1;
++ int idx2;
+
-+ final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L);
-+ final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L);
++ 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
@@ -16147,11 +17310,11 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return true;
+ }
+
-+ public static net.minecraft.world.phys.shapes.VoxelShape joinOptimized(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) {
++ public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
+ return joinUnoptimized(first, second, operator).optimize();
+ }
+
-+ public static net.minecraft.world.phys.shapes.VoxelShape joinUnoptimized(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) {
++ 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
@@ -16161,23 +17324,23 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ final boolean tt = operator.apply(true, true);
+
+ if (first == second) {
-+ return tt ? first : net.minecraft.world.phys.shapes.Shapes.empty();
++ 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 : net.minecraft.world.phys.shapes.Shapes.empty();
++ return ft ? second : Shapes.empty();
+ }
+ if (second.isEmpty()) {
-+ return tf ? first : net.minecraft.world.phys.shapes.Shapes.empty();
++ return tf ? first : Shapes.empty();
+ }
+
+ if (!tt) {
+ // try to check for no intersection, since tt = false
-+ final net.minecraft.world.phys.AABB aabbF = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
-+ final net.minecraft.world.phys.AABB aabbS = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
++ final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
++ final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
+
+ final boolean intersect;
+
@@ -16198,7 +17361,7 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+
+ if (!intersect) {
+ if (!tf & !ft) {
-+ return net.minecraft.world.phys.shapes.Shapes.empty();
++ return Shapes.empty();
+ }
+ if (!tf | !ft) {
+ return tf ? first : second;
@@ -16206,50 +17369,50 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+ }
+
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetX(),
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetX(),
++ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
-+ return net.minecraft.world.phys.shapes.Shapes.empty();
++ if (mergedX == null) {
++ return Shapes.empty();
+ }
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetY(),
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetY(),
++ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
-+ return net.minecraft.world.phys.shapes.Shapes.empty();
++ if (mergedY == null) {
++ return Shapes.empty();
+ }
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetZ(),
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetZ(),
++ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
-+ return net.minecraft.world.phys.shapes.Shapes.empty();
++ if (mergedZ == null) {
++ return Shapes.empty();
+ }
+
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getCachedVoxelData();
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getCachedVoxelData();
++ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData();
++ final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData();
+
-+ final net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape mergedShape = merge(
++ final BitSetDiscreteVoxelShape mergedShape = merge(
+ shapeDataFirst, shapeDataSecond,
+ mergedX, mergedY, mergedZ,
+ makeBitset(ft, tf, tt)
+ );
+
+ if (mergedShape == null) {
-+ return net.minecraft.world.phys.shapes.Shapes.empty();
++ return Shapes.empty();
+ }
+
-+ return new net.minecraft.world.phys.shapes.ArrayVoxelShape(
++ return new ArrayVoxelShape(
+ mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords()
+ );
+ }
+
-+ public static boolean isJoinNonEmpty(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) {
++ 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
@@ -16271,8 +17434,8 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ final boolean tf = operator.apply(true, false);
+
+ // try to check intersection
-+ final net.minecraft.world.phys.AABB aabbF = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
-+ final net.minecraft.world.phys.AABB aabbS = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
++ final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
++ final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
+
+ final boolean intersect;
+
@@ -16304,33 +17467,33 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+ }
+
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetX(),
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetX(),
++ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
++ if (mergedX == null) {
+ return false;
+ }
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetY(),
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetY(),
++ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
++ if (mergedY == null) {
+ return false;
+ }
-+ final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetZ(),
-+ ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetZ(),
++ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
++ if (mergedZ == null) {
+ return false;
+ }
+
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getCachedVoxelData();
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getCachedVoxelData();
++ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData();
++ final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData();
+
+ return !isMergeEmpty(
+ shapeDataFirst, shapeDataSecond,
@@ -16348,10 +17511,6 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+ }
+
-+ private static final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList EMPTY = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(
-+ new double[] { 0.0 }, 0.0, new int[0], new int[0], 0
-+ );
-+
+ private static int[] getIndices(final int length) {
+ final int[] ret = new int[length];
+
@@ -16378,25 +17537,25 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ this.voxels = voxels;
+ }
+
-+ public it.unimi.dsi.fastutil.doubles.DoubleList wrapCoords() {
++ public DoubleList wrapCoords() {
+ if (this.coordinateOffset == 0.0) {
-+ return it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(this.coordinates, this.voxels + 1);
++ return DoubleArrayList.wrap(this.coordinates, this.voxels + 1);
+ }
-+ return new net.minecraft.world.phys.shapes.OffsetDoubleList(it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset);
++ return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset);
+ }
+
+ // assume coordinates.length > 1
-+ public static ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) {
++ 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 ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels);
++ return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels);
+ }
+
+ // assume coordinates.length > 1
-+ public static ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset,
-+ final double[] secondCoordinates, final double secondOffset,
-+ final boolean ft, final boolean tf) {
++ 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);
+ }
@@ -16486,13 +17645,13 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+ }
+
-+ return resultSize <= 1 ? EMPTY : new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1);
++ return resultSize <= 1 ? null : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1);
+ }
+ }
+
-+ public static boolean equals(final net.minecraft.world.phys.shapes.DiscreteVoxelShape shape1, final net.minecraft.world.phys.shapes.DiscreteVoxelShape shape2) {
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData1 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData();
-+ final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData2 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData();
++ 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();
@@ -16501,7 +17660,7 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return true;
+ } else if (isEmpty1 ^ isEmpty2) {
+ return false;
-+ }
++ } // else: isEmpty1 = isEmpty2 = false
+
+ if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) {
+ return false;
@@ -16517,153 +17676,237 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return false;
+ }
+
-+ return java.util.Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet());
++ return Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet());
+ }
+
+ // useful only for testing
-+ public static boolean equals(final net.minecraft.world.phys.shapes.VoxelShape shape1, final net.minecraft.world.phys.shapes.VoxelShape shape2) {
++ 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(net.minecraft.core.Direction.Axis.X).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.X)) &&
-+ shape1.getCoords(net.minecraft.core.Direction.Axis.Y).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.Y)) &&
-+ shape1.getCoords(net.minecraft.core.Direction.Axis.Z).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.Z));
++ 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 net.minecraft.world.phys.AABB offsetX(final net.minecraft.world.phys.AABB box, final double dx) {
-+ return new net.minecraft.world.phys.AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ);
++ 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 net.minecraft.world.phys.AABB offsetY(final net.minecraft.world.phys.AABB box, final double dy) {
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, box.maxZ);
++ 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 net.minecraft.world.phys.AABB offsetZ(final net.minecraft.world.phys.AABB box, final double dz) {
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz);
++ 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 net.minecraft.world.phys.AABB expandRight(final net.minecraft.world.phys.AABB box, final double dx) { // dx > 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ);
++ 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 net.minecraft.world.phys.AABB expandLeft(final net.minecraft.world.phys.AABB box, final double dx) { // dx < 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX - dx, box.minY, box.minZ, box.maxX, 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 net.minecraft.world.phys.AABB expandUpwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy > 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, 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 net.minecraft.world.phys.AABB expandDownwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy < 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, 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 net.minecraft.world.phys.AABB expandForwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz > 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz);
++ 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 net.minecraft.world.phys.AABB expandBackwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz < 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ);
++ 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 net.minecraft.world.phys.AABB cutRight(final net.minecraft.world.phys.AABB box, final double dx) { // dx > 0.0
-+ return new net.minecraft.world.phys.AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, 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 net.minecraft.world.phys.AABB cutLeft(final net.minecraft.world.phys.AABB box, final double dx) { // dx < 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX + dx, box.minY, box.minZ, box.minX, 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 net.minecraft.world.phys.AABB cutUpwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy > 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, 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 net.minecraft.world.phys.AABB cutDownwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy < 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, 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 net.minecraft.world.phys.AABB cutForwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz > 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz);
++ 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 net.minecraft.world.phys.AABB cutBackwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz < 0.0
-+ return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ);
++ 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 net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) {
++ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
+ if (Math.abs(value) < COLLISION_EPSILON) {
+ return 0.0;
+ }
-+ final net.minecraft.world.phys.AABB target = potentialCollisions.get(i);
++ final AABB target = potentialCollisions.get(i);
+ value = collideX(target, currentBoundingBox, value);
+ }
+
-+ return value;
++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
+ }
+
-+ public static double performAABBCollisionsY(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) {
++ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
+ if (Math.abs(value) < COLLISION_EPSILON) {
+ return 0.0;
+ }
-+ final net.minecraft.world.phys.AABB target = potentialCollisions.get(i);
++ final AABB target = potentialCollisions.get(i);
+ value = collideY(target, currentBoundingBox, value);
+ }
+
-+ return value;
++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
+ }
+
-+ public static double performAABBCollisionsZ(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) {
++ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
+ if (Math.abs(value) < COLLISION_EPSILON) {
+ return 0.0;
+ }
-+ final net.minecraft.world.phys.AABB target = potentialCollisions.get(i);
++ final AABB target = potentialCollisions.get(i);
+ value = collideZ(target, currentBoundingBox, value);
+ }
+
-+ return value;
++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
+ }
+
-+ public static double performVoxelCollisionsX(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) {
++ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
+ if (Math.abs(value) < COLLISION_EPSILON) {
+ return 0.0;
+ }
-+ final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i);
++ final VoxelShape target = potentialCollisions.get(i);
+ value = collideX(target, currentBoundingBox, value);
+ }
+
-+ return value;
++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
+ }
+
-+ public static double performVoxelCollisionsY(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) {
++ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
+ if (Math.abs(value) < COLLISION_EPSILON) {
+ return 0.0;
+ }
-+ final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i);
++ final VoxelShape target = potentialCollisions.get(i);
+ value = collideY(target, currentBoundingBox, value);
+ }
+
-+ return value;
++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
+ }
+
-+ public static double performVoxelCollisionsZ(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) {
++ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
+ for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
+ if (Math.abs(value) < COLLISION_EPSILON) {
+ return 0.0;
+ }
-+ final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i);
++ final VoxelShape target = potentialCollisions.get(i);
+ value = collideZ(target, currentBoundingBox, value);
+ }
+
-+ return value;
++ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
+ }
+
-+ public static net.minecraft.world.phys.Vec3 performVoxelCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) {
++ 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;
@@ -16695,10 +17938,10 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
+ }
+
-+ return new net.minecraft.world.phys.Vec3(x, y, z);
++ return new Vec3(x, y, z);
+ }
+
-+ public static net.minecraft.world.phys.Vec3 performAABBCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) {
++ 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;
@@ -16730,12 +17973,12 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
+ }
+
-+ return new net.minecraft.world.phys.Vec3(x, y, z);
++ return new Vec3(x, y, z);
+ }
+
-+ public static net.minecraft.world.phys.Vec3 performCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb,
-+ final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> voxels,
-+ final java.util.List<net.minecraft.world.phys.AABB> aabbs) {
++ 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);
@@ -16776,14 +18019,14 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
+ }
+
-+ return new net.minecraft.world.phys.Vec3(x, y, z);
++ return new Vec3(x, y, z);
+ }
+
-+ public static boolean isCollidingWithBorder(final net.minecraft.world.level.border.WorldBorder worldborder, final net.minecraft.world.phys.AABB boundingBox) {
++ 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 net.minecraft.world.level.border.WorldBorder worldborder,
++ 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
@@ -16793,8 +18036,8 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z
+
+ // inverted check for world border enclosing the specified box expanded by -EPSILON
-+ return (borderMinX - boxMinX) > ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON ||
-+ (borderMinZ - boxMinZ) > ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_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 */
@@ -16811,38 +18054,38 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ 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 net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB aabb,
-+ final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> intoVoxel, final java.util.List<net.minecraft.world.phys.AABB> intoAABB,
-+ final int collisionFlags, final java.util.function.BiPredicate<net.minecraft.world.level.block.state.BlockState, net.minecraft.core.BlockPos> predicate) {
++ 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 net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder();
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) {
++ final WorldBorder worldBorder = world.getWorldBorder();
++ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) {
+ if (checkOnly) {
+ return true;
+ } else {
-+ final net.minecraft.world.phys.shapes.VoxelShape borderShape = worldBorder.getCollisionShape();
++ final VoxelShape borderShape = worldBorder.getCollisionShape();
+ intoVoxel.add(borderShape);
+ ret = true;
+ }
+ }
+ }
+
-+ final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)world).moonrise$getMinSection();
++ final int minSection = WorldUtil.getMinSection(world);
+
-+ final int minBlockX = net.minecraft.util.Mth.floor(aabb.minX - COLLISION_EPSILON) - 1;
-+ final int maxBlockX = net.minecraft.util.Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1;
++ 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, net.minecraft.util.Mth.floor(aabb.minY - COLLISION_EPSILON) - 1);
-+ final int maxBlockY = Math.min((((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)world).moonrise$getMaxSection() << 4) + 16, net.minecraft.util.Mth.floor(aabb.maxY + 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 = net.minecraft.util.Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
-+ final int maxBlockZ = net.minecraft.util.Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
++ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
++ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
+
-+ final net.minecraft.core.BlockPos.MutableBlockPos mutablePos = new net.minecraft.core.BlockPos.MutableBlockPos();
-+ final net.minecraft.world.phys.shapes.CollisionContext collisionShape = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity);
++ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
++ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity);
+
+ // special cases:
+ if (minBlockY > maxBlockY) {
@@ -16860,11 +18103,11 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ final int maxChunkZ = maxBlockZ >> 4;
+
+ final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0;
-+ final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource();
++ final ChunkSource chunkSource = world.getChunkSource();
+
+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-+ final net.minecraft.world.level.chunk.ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, loadChunks);
++ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks);
+
+ if (chunk == null) {
+ if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) {
@@ -16878,7 +18121,7 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ continue;
+ }
+
-+ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections();
++ final LevelChunkSection[] sections = chunk.getSections();
+
+ // bound y
+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
@@ -16886,16 +18129,16 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
+ continue;
+ }
-+ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
-+ if (section == null || section.hasOnlyAir()) {
++ final LevelChunkSection section = sections[sectionIdx];
++ if (section.hasOnlyAir()) {
+ // empty
+ continue;
+ }
+
-+ final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getSpecialCollidingBlocks() != 0;
++ final boolean hasSpecial = ((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 PalettedContainer<BlockState> blocks = section.states;
+
+ final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0;
+ final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15;
@@ -16919,21 +18162,21 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ continue;
+ }
+
-+ final net.minecraft.world.level.block.state.BlockState blockData = blocks.get(localBlockIndex);
++ final BlockState blockData = blocks.get(localBlockIndex);
+
-+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyCollisionShape()) {
++ if (((CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) {
+ continue;
+ }
+
-+ net.minecraft.world.phys.shapes.VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantCollisionShape();
++ VoxelShape blockCollision = ((CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape();
+
-+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == net.minecraft.world.level.block.Blocks.MOVING_PISTON))) {
++ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) {
+ if (blockCollision == null) {
+ mutablePos.set(blockX, blockY, blockZ);
+ blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape);
+ }
+
-+ net.minecraft.world.phys.AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation();
++ AABB singleAABB = ((CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation();
+ if (singleAABB != null) {
+ singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ);
+ if (!voxelShapeIntersect(aabb, singleAABB)) {
@@ -16960,7 +18203,7 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ continue;
+ }
+
-+ final net.minecraft.world.phys.shapes.VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ);
++ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ);
+
+ if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) {
+ continue;
@@ -16991,8 +18234,8 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return ret;
+ }
+
-+ public static boolean getEntityHardCollisions(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, net.minecraft.world.phys.AABB aabb,
-+ final java.util.List<net.minecraft.world.phys.AABB> into, final int collisionFlags, final java.util.function.Predicate<net.minecraft.world.entity.Entity> predicate) {
++ 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;
@@ -17001,15 +18244,15 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ // 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 java.util.List<net.minecraft.world.entity.Entity> entities;
-+ if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) {
++ final List<Entity> entities;
++ if (entity != null && ((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
+ entities = world.getEntities(entity, aabb, predicate);
+ } else {
-+ entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate);
++ entities = ((ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate);
+ }
+
+ for (int i = 0, len = entities.size(); i < len; ++i) {
-+ final net.minecraft.world.entity.Entity otherEntity = entities.get(i);
++ final Entity otherEntity = entities.get(i);
+
+ if (otherEntity.isSpectator()) {
+ continue;
@@ -17028,10 +18271,10 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return ret;
+ }
+
-+ public static boolean getCollisions(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB aabb,
-+ final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> intoVoxel, final java.util.List<net.minecraft.world.phys.AABB> intoAABB, final int collisionFlags,
-+ final java.util.function.BiPredicate<net.minecraft.world.level.block.state.BlockState, net.minecraft.core.BlockPos> blockPredicate,
-+ final java.util.function.Predicate<net.minecraft.world.entity.Entity> entityPredicate) {
++ 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);
@@ -17041,12 +18284,12 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+ }
+
-+ public static final class LazyEntityCollisionContext extends net.minecraft.world.phys.shapes.EntityCollisionContext {
++ public static final class LazyEntityCollisionContext extends EntityCollisionContext {
+
-+ private net.minecraft.world.phys.shapes.CollisionContext delegate;
++ private CollisionContext delegate;
+ private boolean delegated;
+
-+ public LazyEntityCollisionContext(final net.minecraft.world.entity.Entity entity) {
++ public LazyEntityCollisionContext(final Entity entity) {
+ super(false, 0.0, null, null, entity);
+ }
+
@@ -17056,10 +18299,10 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ return delegated;
+ }
+
-+ public net.minecraft.world.phys.shapes.CollisionContext getDelegate() {
++ public CollisionContext getDelegate() {
+ this.delegated = true;
-+ final net.minecraft.world.entity.Entity entity = this.getEntity();
-+ return this.delegate == null ? this.delegate = (entity == null ? net.minecraft.world.phys.shapes.CollisionContext.empty() : net.minecraft.world.phys.shapes.CollisionContext.of(entity)) : this.delegate;
++ final Entity entity = this.getEntity();
++ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate;
+ }
+
+ @Override
@@ -17068,17 +18311,17 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+ }
+
+ @Override
-+ public boolean isAbove(final net.minecraft.world.phys.shapes.VoxelShape shape, final net.minecraft.core.BlockPos pos, final boolean defaultValue) {
++ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) {
+ return this.getDelegate().isAbove(shape, pos, defaultValue);
+ }
+
+ @Override
-+ public boolean isHoldingItem(final net.minecraft.world.item.Item item) {
++ public boolean isHoldingItem(final Item item) {
+ return this.getDelegate().isHoldingItem(item);
+ }
+
+ @Override
-+ public boolean canStandOnFluid(final net.minecraft.world.level.material.FluidState state, final net.minecraft.world.level.material.FluidState fluidState) {
++ public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) {
+ return this.getDelegate().canStandOnFluid(state, fluidState);
+ }
+ }
@@ -17089,25 +18332,30 @@ index 0000000000000000000000000000000000000000..748ab4d637ce463272bae4fdbab6842a
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
new file mode 100644
-index 0000000000000000000000000000000000000000..eb7200657d5c7ac37ee93868ba43be0aefecac6d
+index 0000000000000000000000000000000000000000..35c8aaf0bfa42717f45eed1d1072e1614874de91
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
-@@ -0,0 +1,23 @@
+@@ -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 net.minecraft.core.BlockPos immutablePos;
-+ public final net.minecraft.world.level.block.state.BlockState blockState;
-+ public final net.minecraft.world.level.material.FluidState fluidState;
++ 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 net.minecraft.world.phys.shapes.VoxelShape cachedCollisionShape;
++ public VoxelShape cachedCollisionShape;
+
-+ public ExplosionBlockCache(final long key, final net.minecraft.core.BlockPos immutablePos, final net.minecraft.world.level.block.state.BlockState blockState,
-+ final net.minecraft.world.level.material.FluidState fluidState, final float resistance, final boolean outOfWorld) {
++ 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;
@@ -17118,12 +18366,14 @@ index 0000000000000000000000000000000000000000..eb7200657d5c7ac37ee93868ba43be0a
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
new file mode 100644
-index 0000000000000000000000000000000000000000..02b29c563a298e06186de010de68a716bccba494
+index 0000000000000000000000000000000000000000..a38ab583200ebf68ca68fdddf2d12077720b72b7
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
-@@ -0,0 +1,26 @@
+@@ -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())
@@ -17133,6 +18383,9 @@ index 0000000000000000000000000000000000000000..02b29c563a298e06186de010de68a716
+ // 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();
+
@@ -17144,9 +18397,7 @@ index 0000000000000000000000000000000000000000..02b29c563a298e06186de010de68a716
+ // value is still unique
+ public int moonrise$uniqueId2();
+
-+ public net.minecraft.world.phys.shapes.VoxelShape moonrise$getConstantCollisionShape();
-+
-+ public net.minecraft.world.phys.AABB moonrise$getConstantCollisionAABB();
++ public VoxelShape moonrise$getConstantContextCollisionShape();
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedShapeData.java
new file mode 100644
@@ -17166,34 +18417,38 @@ index 0000000000000000000000000000000000000000..5a6b16be4b8c0cc92d017bc592bc4818
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
new file mode 100644
-index 0000000000000000000000000000000000000000..5fe1dad9dad368911aedbe6ba7fcd8f9b0189d32
+index 0000000000000000000000000000000000000000..9d33ead3a97d86b371e4d9ad9fed80d789bed844
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
-@@ -0,0 +1,35 @@
+@@ -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(
-+ java.util.List<net.minecraft.world.phys.AABB> aabbs,
++ List<AABB> aabbs,
+ boolean isOffset,
+ double offX, double offY, double offZ
+) {
+
-+ public ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs removeOffset() {
-+ final java.util.List<net.minecraft.world.phys.AABB> toOffset = this.aabbs;
++ 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 java.util.List<net.minecraft.world.phys.AABB> ret = new java.util.ArrayList<>(toOffset.size());
++ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
++ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
+ }
+
-+ public static ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs offset(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cache, final double offX, final double offY, final double offZ) {
++ 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;
+ }
@@ -17202,12 +18457,12 @@ index 0000000000000000000000000000000000000000..5fe1dad9dad368911aedbe6ba7fcd8f9
+ final double resY = cache.offY + offY;
+ final double resZ = cache.offZ + offZ;
+
-+ return new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(cache.aabbs, true, resX, resY, resZ);
++ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ);
+ }
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
new file mode 100644
-index 0000000000000000000000000000000000000000..a09efadea9b733840bbe69830dd8f2a303fe656f
+index 0000000000000000000000000000000000000000..07fe5e02c2d0a27d2fe37bb45761654dc2d02e5d
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
@@ -0,0 +1,7 @@
@@ -17215,17 +18470,21 @@ index 0000000000000000000000000000000000000000..a09efadea9b733840bbe69830dd8f2a3
+
+public interface CollisionDiscreteVoxelShape {
+
-+ public ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getOrCreateCachedShapeData();
++ public CachedShapeData moonrise$getOrCreateCachedShapeData();
+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
new file mode 100644
-index 0000000000000000000000000000000000000000..70371eb87c11a106e8513cdbc8d938dda088f745
+index 0000000000000000000000000000000000000000..05d7b3f9d8659c259f3ed0537c57e6e43eb6e288
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
-@@ -0,0 +1,36 @@
+@@ -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();
@@ -17240,16 +18499,16 @@ index 0000000000000000000000000000000000000000..70371eb87c11a106e8513cdbc8d938dd
+
+ public double[] moonrise$rootCoordinatesZ();
+
-+ public ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getCachedVoxelData();
++ public CachedShapeData moonrise$getCachedVoxelData();
+
+ // rets null if not possible to represent this shape as one AABB
-+ public net.minecraft.world.phys.AABB moonrise$getSingleAABBRepresentation();
++ 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 net.minecraft.world.phys.shapes.VoxelShape moonrise$getFaceShapeClamped(final net.minecraft.core.Direction direction);
++ public VoxelShape moonrise$getFaceShapeClamped(final Direction direction);
+
+ public boolean moonrise$isFullBlock();
+
@@ -17258,19 +18517,21 @@ index 0000000000000000000000000000000000000000..70371eb87c11a106e8513cdbc8d938dd
+ public boolean moonrise$occludesFullBlockIfCached();
+
+ // uses a cache internally
-+ public net.minecraft.world.phys.shapes.VoxelShape moonrise$orUnoptimized(final net.minecraft.world.phys.shapes.VoxelShape other);
++ public VoxelShape moonrise$orUnoptimized(final VoxelShape other);
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
new file mode 100644
-index 0000000000000000000000000000000000000000..4217426d3eca5e5cd2bc37e509f84da1d6fed0b2
+index 0000000000000000000000000000000000000000..44831fc18efb7534dc6e4822f3c9b5cdc4dcc33e
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
-@@ -0,0 +1,8 @@
+@@ -0,0 +1,10 @@
+package ca.spottedleaf.moonrise.patches.collisions.shape;
+
++import net.minecraft.world.phys.shapes.VoxelShape;
++
+public record MergedORCache(
-+ net.minecraft.world.phys.shapes.VoxelShape key,
-+ net.minecraft.world.phys.shapes.VoxelShape result
++ VoxelShape key,
++ VoxelShape result
+) {
+
+}
@@ -17289,261 +18550,18 @@ index 0000000000000000000000000000000000000000..f62359e5d6aa9a9cdb015441dbdb6182
+ public int moonrise$uniqueId();
+
+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..673103f160cbe577c6e05f998706af4e6850011b
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java
-@@ -0,0 +1,225 @@
-+package ca.spottedleaf.moonrise.patches.collisions.util;
-+
-+import java.util.Iterator;
-+import java.util.Optional;
-+import java.util.Spliterator;
-+import java.util.stream.Stream;
-+
-+public final class EmptyStreamForMoveCall<T> implements java.util.stream.Stream<T> {
-+
-+ public static final ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall INSTANCE = new ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall();
-+
-+ @Override
-+ public boolean noneMatch(java.util.function.Predicate<? super T> predicate) {
-+ return false; // important: ret false so the branch is never taken by mojang code
-+ }
-+
-+ @Override
-+ public java.util.stream.Stream<T> filter(java.util.function.Predicate<? super T> predicate) {
-+ return null;
-+ }
-+
-+ @Override
-+ public <R> java.util.stream.Stream<R> map(java.util.function.Function<? super T, ? extends R> mapper) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.IntStream mapToInt(java.util.function.ToIntFunction<? super T> mapper) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.LongStream mapToLong(java.util.function.ToLongFunction<? super T> mapper) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.DoubleStream mapToDouble(java.util.function.ToDoubleFunction<? super T> mapper) {
-+ return null;
-+ }
-+
-+ @Override
-+ public <R> java.util.stream.Stream<R> flatMap(java.util.function.Function<? super T, ? extends java.util.stream.Stream<? extends R>> mapper) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.IntStream flatMapToInt(java.util.function.Function<? super T, ? extends java.util.stream.IntStream> mapper) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.LongStream flatMapToLong(java.util.function.Function<? super T, ? extends java.util.stream.LongStream> mapper) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.DoubleStream flatMapToDouble(java.util.function.Function<? super T, ? extends java.util.stream.DoubleStream> mapper) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.Stream<T> distinct() {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.Stream<T> sorted() {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.Stream<T> sorted(java.util.Comparator<? super T> comparator) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.Stream<T> peek(java.util.function.Consumer<? super T> action) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.Stream<T> limit(long maxSize) {
-+ return null;
-+ }
-+
-+ @Override
-+ public java.util.stream.Stream<T> skip(long n) {
-+ return null;
-+ }
-+
-+ @Override
-+ public void forEach(java.util.function.Consumer<? super T> action) {
-+
-+ }
-+
-+ @Override
-+ public void forEachOrdered(java.util.function.Consumer<? super T> action) {
-+
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Object[] toArray() {
-+ return new Object[0];
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public <A> A[] toArray(java.util.function.IntFunction<A[]> generator) {
-+ return null;
-+ }
-+
-+ @Override
-+ public T reduce(T identity, java.util.function.BinaryOperator<T> accumulator) {
-+ return null;
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Optional<T> reduce(java.util.function.BinaryOperator<T> accumulator) {
-+ return java.util.Optional.empty();
-+ }
-+
-+ @Override
-+ public <U> U reduce(U identity, java.util.function.BiFunction<U, ? super T, U> accumulator, java.util.function.BinaryOperator<U> combiner) {
-+ return null;
-+ }
-+
-+ @Override
-+ public <R> R collect(java.util.function.Supplier<R> supplier, java.util.function.BiConsumer<R, ? super T> accumulator, java.util.function.BiConsumer<R, R> combiner) {
-+ return null;
-+ }
-+
-+ @Override
-+ public <R, A> R collect(java.util.stream.Collector<? super T, A, R> collector) {
-+ return null;
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Optional<T> min(java.util.Comparator<? super T> comparator) {
-+ return java.util.Optional.empty();
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Optional<T> max(java.util.Comparator<? super T> comparator) {
-+ return java.util.Optional.empty();
-+ }
-+
-+ @Override
-+ public long count() {
-+ return 0;
-+ }
-+
-+ @Override
-+ public boolean anyMatch(java.util.function.Predicate<? super T> predicate) {
-+ return false;
-+ }
-+
-+ @Override
-+ public boolean allMatch(java.util.function.Predicate<? super T> predicate) {
-+ return false;
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Optional<T> findFirst() {
-+ return java.util.Optional.empty();
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Optional<T> findAny() {
-+ return java.util.Optional.empty();
-+ }
-+
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Iterator<T> iterator() {
-+ return null;
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Spliterator<T> spliterator() {
-+ return null;
-+ }
-+
-+ @Override
-+ public boolean isParallel() {
-+ return false;
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Stream<T> sequential() {
-+ return null;
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Stream<T> parallel() {
-+ return null;
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Stream<T> unordered() {
-+ return null;
-+ }
-+
-+ @org.jetbrains.annotations.NotNull
-+ @Override
-+ public Stream<T> onClose(Runnable closeHandler) {
-+ return null;
-+ }
-+
-+ @Override
-+ public void close() {
-+
-+ }
-+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
new file mode 100644
-index 0000000000000000000000000000000000000000..128267ff40b38c7b3ea0feb5133825cc6aae075b
+index 0000000000000000000000000000000000000000..cf9ffdeff6bf0b62a45f7a44dbfe0dd7d17dc4f4
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
-@@ -0,0 +1,4 @@
+@@ -0,0 +1,7 @@
+package ca.spottedleaf.moonrise.patches.collisions.util;
+
-+public record FluidOcclusionCacheKey(net.minecraft.world.level.block.state.BlockState first, net.minecraft.world.level.block.state.BlockState second, net.minecraft.core.Direction direction, boolean result) {
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..e851e81e13edbad6316df63fcb7095d48f85c5b0
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java
-@@ -0,0 +1,9 @@
-+package ca.spottedleaf.moonrise.patches.collisions.world;
-+
-+public interface CollisionLevel {
-+
-+ public int moonrise$getMinSection();
-+
-+ public int moonrise$getMaxSection();
++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/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerEntity.java
new file mode 100644
@@ -17564,10 +18582,10 @@ index 0000000000000000000000000000000000000000..5f5734c00ce8245a1ff69b2d4c303657
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
new file mode 100644
-index 0000000000000000000000000000000000000000..1fa07bef57d82c6d5242aaaf66011f0913515231
+index 0000000000000000000000000000000000000000..8e7472157a98de607c03769a91f64c8369fd3ea6
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
-@@ -0,0 +1,13 @@
+@@ -0,0 +1,15 @@
+package ca.spottedleaf.moonrise.patches.entity_tracker;
+
+import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
@@ -17580,21 +18598,77 @@ index 0000000000000000000000000000000000000000..1fa07bef57d82c6d5242aaaf66011f09
+
+ public void moonrise$clearPlayers();
+
++ public boolean moonrise$hasPlayers();
++
++}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4a7abd239a9c59aa98947e7993962d75e9051902
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..4503f3495846a7d7ed082b9e24636044e4fbccd1
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..107c97089354edd35f330582f5e0c8a18e792a6e
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..540c14a6d2c216cd3ef2a9c4056e15712bf8cb8c
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
new file mode 100644
-index 0000000000000000000000000000000000000000..2bfdf3721db9a45e36538d71cbefcb1d339e6c58
+index 0000000000000000000000000000000000000000..8e6d79b7c10ef25f5478b72c53c555423d615a2f
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
-@@ -0,0 +1,9 @@
+@@ -0,0 +1,7 @@
+package ca.spottedleaf.moonrise.patches.starlight.blockstate;
+
+public interface StarlightAbstractBlockState {
+
+ public boolean starlight$isConditionallyFullOpaque();
+
-+ public int starlight$getOpacityIfCached();
-+
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/chunk/StarlightChunk.java
new file mode 100644
@@ -17622,15 +18696,17 @@ index 0000000000000000000000000000000000000000..ed80017c8f257b981d626a37ffc5480d
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
new file mode 100644
-index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d638b213aeb
+index 0000000000000000000000000000000000000000..fa7b784a89626e8528c249d7889a598bd7ee3d49
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
-@@ -0,0 +1,277 @@
+@@ -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;
@@ -17719,7 +18795,7 @@ index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d63
+
+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
+ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ);
-+ final int emittedLevel = blockState.getLightEmission() & emittedMask;
++ 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
@@ -17747,37 +18823,32 @@ index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d63
+ }
+
+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
+
+ @Override
+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
+ final int expect) {
++ this.recalcCenterPos.set(worldX, worldY, worldZ);
++
+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+ int level = centerState.getLightEmission() & 0xF;
++ 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 sectionOffset = this.chunkSectionIndexOffset;
-+ final BlockState conditionallyOpaqueState;
-+ int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached();
-+
-+ if (opacity == -1) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+ opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos);
-+ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ } else if (opacity >= 15) {
++ 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;
+ }
-+ opacity = Math.max(1, opacity);
+
++ final int sectionOffset = this.chunkSectionIndexOffset;
+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
+ final int offX = worldX + direction.x;
+ final int offY = worldY + direction.y;
@@ -17797,9 +18868,8 @@ index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d63
+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
+ // we don't read the blockstate because most of the time this is false, so using the faster
+ // known transparency lookup results in a net win
-+ this.recalcNeighbourPos.set(offX, offY, offZ);
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
++ 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;
@@ -17833,30 +18903,34 @@ index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d63
+ 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 == null || section.hasOnlyAir()) {
++ if (section.hasOnlyAir()) {
+ // no sources in empty sections
+ continue;
+ }
-+ if (!section.maybeHas((final BlockState state) -> {
-+ return state.getLightEmission() > 0;
-+ })) {
++ 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);
-+ if (state.getLightEmission() <= 0) {
++ 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(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
++ sources.add(mutablePos.immutable());
+ }
+ }
+
@@ -17866,12 +18940,15 @@ index 0000000000000000000000000000000000000000..154443ac1ee1d6d18b8ff0f40a307d63
+ @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 = blockState.getLightEmission() & emittedMask;
++ final int emittedLight = platformHooks.getLightEmission(blockState, world, pos) & emittedMask;
+
+ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) {
+ // some other source is brighter
@@ -18351,10 +19428,10 @@ index 0000000000000000000000000000000000000000..4ca68a903e67606fc4ef0bfa9862a737
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
new file mode 100644
-index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def568142a4
+index 0000000000000000000000000000000000000000..f9aef289e9a2d6f63c98c72c56ef32b8793f57f4
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
-@@ -0,0 +1,711 @@
+@@ -0,0 +1,681 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
@@ -18647,9 +19724,6 @@ index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def
+ );
+ }
+
-+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
-+
+ @Override
+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
+ final int expect) {
@@ -18659,20 +19733,13 @@ index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def
+
+ final int sectionOffset = this.chunkSectionIndexOffset;
+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+ int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached();
+
+ final BlockState conditionallyOpaqueState;
-+ if (opacity < 0) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos));
-+ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
++ final int opacity = Math.max(1, centerState.getLightBlock());
++ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
++ conditionallyOpaqueState = centerState;
+ } else {
+ conditionallyOpaqueState = null;
-+ opacity = Math.max(1, opacity);
+ }
+
+ int level = 0;
@@ -18697,9 +19764,8 @@ index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def
+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
+ // we don't read the blockstate because most of the time this is false, so using the faster
+ // known transparency lookup results in a net win
-+ this.recalcNeighbourPos.set(offX, offY, offZ);
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
++ 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;
@@ -18967,7 +20033,6 @@ index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def
+ // clobbering the light values will result in broken propagation)
+ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ,
+ final boolean extrudeInitialised, final boolean delayLightSet) {
-+ final BlockPos.MutableBlockPos mutablePos = this.mutablePos3;
+ final int encodeOffset = this.coordinateOffset;
+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards.
+
@@ -18989,8 +20054,7 @@ index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def
+
+ final VoxelShape fromShape;
+ if (((StarlightAbstractBlockState)above).starlight$isConditionallyFullOpaque()) {
-+ this.mutablePos2.set(worldX, startY + 1, worldZ);
-+ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms);
++ fromShape = above.getFaceOcclusionShape(AxisDirection.NEGATIVE_Y.nms);
+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ // above wont let us propagate
+ break;
@@ -18999,49 +20063,32 @@ index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def
+ fromShape = Shapes.empty();
+ }
+
-+ final int opacityIfCached = ((StarlightAbstractBlockState)current).starlight$getOpacityIfCached();
+ // does light propagate from the top down?
-+ if (opacityIfCached != -1) {
-+ if (opacityIfCached != 0) {
-+ // we cannot propagate 15 through this
-+ break;
-+ }
-+ // most of the time it falls here.
-+ // add to propagate
-+ // light set delayed until we determine if this nibble section is null
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ );
-+ } else {
-+ mutablePos.set(worldX, startY, worldZ);
-+ long flags = 0L;
-+ if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms);
++ 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(world, mutablePos);
-+ if (opacity > 0) {
-+ // let the queued value (if any) handle it from here.
++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
++ // can't propagate here, we're done on this column.
+ break;
+ }
++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
++ }
+
-+ // 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
-+ );
++ 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) {
@@ -19068,13 +20115,14 @@ index 0000000000000000000000000000000000000000..fdbc015f498164c9d2c578cd84a73def
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
new file mode 100644
-index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017c58191e2
+index 0000000000000000000000000000000000000000..8aeb5fb87f94a35659347a09a638420699b52a6f
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
-@@ -0,0 +1,1573 @@
+@@ -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;
@@ -19117,9 +20165,9 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+ protected static enum AxisDirection {
+
+ // Declaration order is important and relied upon. Do not change without modifying propagation code.
-+ POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0),
-+ POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1),
-+ POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0);
++ 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;
@@ -19136,11 +20184,11 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+ public final long everythingButThisDirection;
+ public final long everythingButTheOppositeDirection;
+
-+ AxisDirection(final int x, final int y, final int z) {
++ AxisDirection(final int x, final int y, final int z, final Direction nms) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
-+ this.nms = Direction.fromDelta(x, y, 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)));
@@ -19180,9 +20228,7 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+ // index = x + (z * 5)
+ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
+
-+ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos();
++ protected final BlockPos.MutableBlockPos lightEmissionPos = new BlockPos.MutableBlockPos();
+
+ protected int encodeOffsetX;
+ protected int encodeOffsetY;
@@ -20224,69 +21270,46 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+ if (blockState == null) {
+ continue;
+ }
-+ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-+ if (targetLevel > currentLevel) {
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
++ 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(world, this.mutablePos1);
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
++ 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);
++ 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);
++ if (targetLevel > 1) {
++ if (queueLength >= queue.length) {
++ queue = this.resizeIncreaseQueue();
+ }
-+ continue;
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
++ | (flags);
+ }
++ continue;
+ }
+ } else {
+ // we actually need to worry about our state here
+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ this.mutablePos2.set(posX, posY, posZ);
+ for (final AxisDirection propagate : checkDirections) {
+ final int offX = posX + propagate.x;
+ final int offY = posY + propagate.y;
+ final int offZ = posZ + propagate.z;
+
-+ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
++ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty();
+
+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ continue;
@@ -20306,58 +21329,36 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+ if (blockState == null) {
+ continue;
+ }
-+ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-+ if (targetLevel > currentLevel) {
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
++ long flags = 0;
++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
++ 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);
++ 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);
++ if (targetLevel > 1) {
++ if (queueLength >= queue.length) {
++ queue = this.resizeIncreaseQueue();
+ }
-+ continue;
++ 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;
+ }
+ }
+ }
@@ -20378,6 +21379,8 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+ final int sectionOffset = this.chunkSectionIndexOffset;
+ final int emittedMask = this.emittedLightMask;
+
++ final PlatformHooks platformHooks = PlatformHooks.get();
++
+ while (queueReadIndex < queueLength) {
+ final long queueValue = queue[queueReadIndex++];
+
@@ -20409,109 +21412,63 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+ if (blockState == null) {
+ continue;
+ }
-+ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_RECHECK_LEVEL;
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // 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))
-+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
++ this.lightEmissionPos.set(offX, offY, offZ);
++ long flags = 0;
++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
+
-+ 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));
++ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
+ continue;
+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
++ }
+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
++ final int 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();
+ }
-+ final int emittedLight = blockState.getLightEmission() & 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);
++ 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);
++ 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;
++ if (targetLevel > 0) {
++ if (queueLength >= queue.length) {
++ queue = this.resizeDecreaseQueue();
+ }
-+ continue;
++ queue[queueLength++] =
++ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
++ | ((targetLevel & 0xFL) << (6 + 6 + 16))
++ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
++ | flags;
+ }
++ continue;
+ }
+ } else {
+ // we actually need to worry about our state here
+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ this.mutablePos2.set(posX, posY, posZ);
+ for (final AxisDirection propagate : checkDirections) {
+ final int offX = posX + propagate.x;
+ final int offY = posY + propagate.y;
@@ -20520,7 +21477,7 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+ 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(world, this.mutablePos2, propagate.nms) : Shapes.empty();
++ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty();
+
+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
+ continue;
@@ -20538,104 +21495,59 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+ if (blockState == null) {
+ continue;
+ }
-+ final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_RECHECK_LEVEL;
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // 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))
-+ | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
-+ }
++ this.lightEmissionPos.set(offX, offY, offZ);
++ long flags = 0;
++ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
++ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
+
-+ 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));
++ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
+ continue;
+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
++ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
++ }
+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
++ final int 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();
+ }
-+ final int emittedLight = blockState.getLightEmission() & 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);
++ 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);
++ 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;
++ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
++ if (queueLength >= queue.length) {
++ queue = this.resizeDecreaseQueue();
+ }
-+ continue;
++ 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;
+ }
+ }
+ }
@@ -20647,15 +21559,16 @@ index 0000000000000000000000000000000000000000..382c9e445af0d6ad2428fc22d0f63017
+}
diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
new file mode 100644
-index 0000000000000000000000000000000000000000..c64ab41198a5e0c7cbcbe6452af11f82f5938862
+index 0000000000000000000000000000000000000000..571db5f9bf94745a8afe2cd313e593fb15db5e37
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
-@@ -0,0 +1,930 @@
+@@ -0,0 +1,931 @@
+package ca.spottedleaf.moonrise.patches.starlight.light;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++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;
@@ -21398,34 +22311,34 @@ index 0000000000000000000000000000000000000000..c64ab41198a5e0c7cbcbe6452af11f82
+ super(lightInterface);
+ }
+
-+ public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
++ 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) {
++ 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 PrioritisedExecutor.Priority.COMPLETING;
++ return Priority.COMPLETING;
+ }
+
+ @Override
@@ -21469,7 +22382,7 @@ index 0000000000000000000000000000000000000000..c64ab41198a5e0c7cbcbe6452af11f82
+ return ret;
+ }
+
-+ public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) {
++ 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(
@@ -21532,11 +22445,11 @@ index 0000000000000000000000000000000000000000..c64ab41198a5e0c7cbcbe6452af11f82
+
+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
+ final ServerLightQueue queue) {
-+ this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL);
++ this(chunkCoordinate, lightEngine, queue, Priority.NORMAL);
+ }
+
+ public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
-+ final ServerLightQueue queue, final PrioritisedExecutor.Priority priority) {
++ 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),
@@ -21556,19 +22469,19 @@ index 0000000000000000000000000000000000000000..c64ab41198a5e0c7cbcbe6452af11f82
+ return this.task.cancel();
+ }
+
-+ public PrioritisedExecutor.Priority getPriority() {
++ public Priority getPriority() {
+ return this.task.getPriority();
+ }
+
-+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++ public void lowerPriority(final Priority priority) {
+ this.task.lowerPriority(priority);
+ }
+
-+ public void setPriority(final PrioritisedExecutor.Priority priority) {
++ public void setPriority(final Priority priority) {
+ this.task.setPriority(priority);
+ }
+
-+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
++ public void raisePriority(final Priority priority) {
+ this.task.raisePriority(priority);
+ }
+
@@ -21616,12 +22529,31 @@ index 0000000000000000000000000000000000000000..7fe59ab70557aa6a484a02db2b2007fd
+ }
+
+}
+diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..40d004afdc6449530f5bb2d7c7638b8ee3e3a577
+--- /dev/null
++++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
new file mode 100644
-index 0000000000000000000000000000000000000000..57692a503e147a00ac4e1586cd78e12b71a80d3f
+index 0000000000000000000000000000000000000000..689ce367164e79e0426eeecb81dbbc521d4bc742
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
-@@ -0,0 +1,188 @@
+@@ -0,0 +1,189 @@
+package ca.spottedleaf.moonrise.patches.starlight.util;
+
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
@@ -21638,19 +22570,20 @@ index 0000000000000000000000000000000000000000..57692a503e147a00ac4e1586cd78e12b
+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();
+
-+ private static final int STARLIGHT_LIGHT_VERSION = 9;
++ public static final int STARLIGHT_LIGHT_VERSION = 9;
+
+ public static int getLightVersion() {
+ return STARLIGHT_LIGHT_VERSION;
+ }
+
-+ private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state";
-+ private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state";
-+ private static final String STARLIGHT_VERSION_TAG = "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 {
@@ -22229,83 +23162,30 @@ index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de14
+ }
+}
diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
-index b3f3408a986ae513c06e3b16b82e1c80d4604cd2..9bd509915b391e9d382fe47798e2c345b6e59a9a 100644
+index 36b96e0ed5c0d25068ec4678eddd8a19a020d345..b59613e3d97a9ca7d11bda28508021a5e9a9e92f 100644
--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
-@@ -30,6 +30,45 @@ public class GlobalConfiguration extends ConfigurationPart {
- public static GlobalConfiguration get() {
- return instance;
- }
-+
-+ public ChunkLoadingBasic chunkLoadingBasic;
-+
-+ public class ChunkLoadingBasic extends ConfigurationPart {
-+ @Comment("The maximum rate in chunks per second that the server will send to any individual player. Set to -1 to disable this limit.")
-+ public double playerMaxChunkSendRate = 75.0;
-+
-+ @Comment(
-+ "The maximum rate at which chunks will load for any individual player. " +
-+ "Note that this setting also affects chunk generations, since a chunk load is always first issued to test if a" +
-+ "chunk is already generated. Set to -1 to disable this limit."
-+ )
-+ public double playerMaxChunkLoadRate = 100.0;
-+
-+ @Comment("The maximum rate at which chunks will generate for any individual player. Set to -1 to disable this limit.")
-+ public double playerMaxChunkGenerateRate = -1.0;
-+ }
-+
-+ public ChunkLoadingAdvanced chunkLoadingAdvanced;
-+
-+ public class ChunkLoadingAdvanced extends ConfigurationPart {
-+ @Comment(
-+ "Set to true if the server will match the chunk send radius that clients have configured" +
-+ "in their view distance settings if the client is less-than the server's send distance."
-+ )
-+ public boolean autoConfigSendDistance = true;
-+
-+ @Comment(
-+ "Specifies the maximum amount of concurrent chunk loads that an individual player can have." +
-+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
-+ )
-+ public int playerMaxConcurrentChunkLoads = 0;
-+
-+ @Comment(
-+ "Specifies the maximum amount of concurrent chunk generations that an individual player can have." +
-+ "Set to 0 to let the server configure it automatically per player, or set it to -1 to disable the limit."
-+ )
-+ public int playerMaxConcurrentChunkGenerates = 0;
-+ }
- static void set(GlobalConfiguration instance) {
- GlobalConfiguration.instance = instance;
- }
-@@ -145,21 +184,6 @@ public class GlobalConfiguration extends ConfigurationPart {
- public int incomingPacketThreshold = 300;
- }
-
-- public ChunkLoading chunkLoading;
--
-- public class ChunkLoading extends ConfigurationPart {
-- public int minLoadRadius = 2;
-- public int maxConcurrentSends = 2;
-- public boolean autoconfigSendDistance = true;
-- public double targetPlayerChunkSendRate = 100.0;
-- public double globalMaxChunkSendRate = -1.0;
-- public boolean enableFrustumPriority = false;
-- public double globalMaxChunkLoadRate = -1.0;
-- public double playerMaxConcurrentLoads = 20.0;
-- public double globalMaxConcurrentLoads = 500.0;
-- public double playerMaxChunkLoadRate = -1.0;
-- }
--
- public UnsupportedSettings unsupportedSettings;
-
- public class UnsupportedSettings extends ConfigurationPart {
-@@ -218,7 +242,7 @@ public class GlobalConfiguration extends ConfigurationPart {
-
+@@ -243,6 +243,23 @@ public class GlobalConfiguration extends ConfigurationPart {
@PostProcess
private void postProcess() {
-- //io.papermc.paper.chunk.system.scheduling.ChunkTaskScheduler.init(this);
-+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(this);
+ ca.spottedleaf.moonrise.common.util.MoonriseCommon.adjustWorkerThreads(this.workerThreads, this.ioThreads);
++
++ String newChunkSystemGenParallelism = this.genParallelism;
++ if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) {
++ newChunkSystemGenParallelism = "true";
++ }
++
++ boolean useParallelGen;
++ if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled")
++ || newChunkSystemGenParallelism.equalsIgnoreCase("true")) {
++ useParallelGen = true;
++ } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled")
++ || newChunkSystemGenParallelism.equalsIgnoreCase("false")) {
++ useParallelGen = false;
++ } else {
++ throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]");
++ }
++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(useParallelGen);
}
}
@@ -22434,6 +23314,38 @@ index 3fde5abde736b2c19d8819d9aec0397a0245ccd1..6548302d4983bf48cc6bc2b7f4833dc7
+ }
+ // Paper end - optimise collisions
}
+diff --git a/src/main/java/net/minecraft/core/MappedRegistry.java b/src/main/java/net/minecraft/core/MappedRegistry.java
+index 71e04e5c1bc0722abf8ca2e0738bd60b6d7ae21c..8e0dfaf02343d74ce786e4fc647bc4c1d73c0014 100644
+--- a/src/main/java/net/minecraft/core/MappedRegistry.java
++++ b/src/main/java/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/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
index fc6ce3485dc890f5105a37fe3e344a1460867556..e114e687f2f4503546687fd6792226a643af8793 100644
--- a/src/main/java/net/minecraft/server/Main.java
@@ -22447,7 +23359,7 @@ index fc6ce3485dc890f5105a37fe3e344a1460867556..e114e687f2f4503546687fd6792226a6
DedicatedServer dedicatedserver1 = new DedicatedServer(optionset, worldLoader.get(), thread, convertable_conversionsession, resourcepackrepository, worldstem, dedicatedserversettings, DataFixers.getDataFixer(), services, LoggerChunkProgressListener::createFromGameruleRadius);
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 53c9be615a0f2939cd989e24e304e81e6e27f39d..7c388e230c4a88edf6212dd8990e8238d3265ebf 100644
+index 53c9be615a0f2939cd989e24e304e81e6e27f39d..8a66012b7f2396031840c8c718f49f8aab716ee0 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -205,7 +205,7 @@ import org.bukkit.event.server.ServerLoadEvent;
@@ -22606,15 +23518,22 @@ index 53c9be615a0f2939cd989e24e304e81e6e27f39d..7c388e230c4a88edf6212dd8990e8238
this.isSaving = false;
this.resources.close();
-@@ -1042,6 +1106,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -1042,6 +1106,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
// Spigot end
-+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.deinit(); // Paper - rewrite chunk system
++ // 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() {
-@@ -1212,6 +1277,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -1212,6 +1284,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.tickServer(flag ? () -> {
return false;
} : this::haveTime);
@@ -22628,7 +23547,7 @@ index 53c9be615a0f2939cd989e24e304e81e6e27f39d..7c388e230c4a88edf6212dd8990e8238
this.tickFrame.end();
gameprofilerfiller.popPush("nextTickWait");
this.mayHaveDelayedTasks = true;
-@@ -1428,6 +1500,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -1428,6 +1507,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private boolean pollTaskInternal() {
if (super.pollTask()) {
@@ -22636,7 +23555,7 @@ index 53c9be615a0f2939cd989e24e304e81e6e27f39d..7c388e230c4a88edf6212dd8990e8238
return true;
} else {
boolean ret = false; // Paper - force execution of all worlds, do not just bias the first
-@@ -2696,6 +2769,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+@@ -2696,6 +2776,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
@@ -22689,7 +23608,7 @@ index eb9dab7be7da11ab1c4046a7fc4a29d5bddf31d2..c066e93994f4f9ab2ac00163e25d4bb1
}
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index b9ab241b930edc63a39dbbcf14cd0b5edacb9ea9..8e2c0def0d158bdacfa0d455be82db24cc4bdf64 100644
+index b9ab241b930edc63a39dbbcf14cd0b5edacb9ea9..3b5b6799b6e7832d7cc7d73afb5f2ca04fee6060 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -32,46 +32,125 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
@@ -22937,7 +23856,7 @@ index b9ab241b930edc63a39dbbcf14cd0b5edacb9ea9..8e2c0def0d158bdacfa0d455be82db24
} else {
ichunkaccess.markUnsaved();
- LevelChunk chunk = this.getTickingChunk();
-+ LevelChunk chunk = this.getChunkToSend(); // Paper - rewrite chunk system
++ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
if (chunk == null) {
return false;
@@ -23189,7 +24108,7 @@ index d9ad32acdf46a43a649334a3b736aeb7b3af21d1..fae17a075d7efaf24d916877dd5968eb
public static final int RADIUS_AROUND_FULL_CHUNK = FULL_CHUNK_STEP.accumulatedDependencies().getRadius();
public static final int MAX_LEVEL = 33 + RADIUS_AROUND_FULL_CHUNK;
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc55303852070107f1736e 100644
+index ec19eb88705a07db45f1a3541571fb7f43efb5a9..64b9738584fe2efd1ce4a3d7e2c75e091adc2504 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -125,10 +125,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
@@ -23801,7 +24720,7 @@ index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc553038520701
}
public int getTickingGenerated() {
-@@ -858,143 +502,83 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -858,144 +502,84 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
private boolean saveChunkIfNeeded(ChunkHolder chunkHolder, long currentTime) {
@@ -23955,8 +24874,8 @@ index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc553038520701
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) {
@@ -23992,11 +24911,12 @@ index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc553038520701
+ @Override
+ public void flushWorker() {
+ ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.flush();
- }
++ }
+ // Paper end - rewrite chunk system
-
++
@Nullable
public LevelChunk getChunkToSend(long pos) {
+ ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos);
@@ -1061,7 +645,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
@@ -24152,7 +25072,7 @@ index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc553038520701
playerchunkmap_entitytracker.updatePlayers(this.level.players());
if (entity instanceof ServerPlayer) {
ServerPlayer entityplayer = (ServerPlayer) entity;
-@@ -1362,16 +905,49 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1362,16 +905,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
playerchunkmap_entitytracker1.broadcastRemoved();
}
@@ -24163,7 +25083,6 @@ index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc553038520701
- Iterator iterator = this.playerMap.getAllPlayers().iterator();
+ // Paper start - optimise entity tracker
+ private void newTrackerTick() {
-+ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers();
+ 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()) {
@@ -24176,38 +25095,28 @@ index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc553038520701
+ if (tracker == null) {
+ continue;
+ }
-+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition()));
-+ tracker.serverEntity.sendChanges();
-+ }
-+
-+ // process unloads
-+ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> unloadedEntities = entityLookup.trackerUnloadedEntities;
-+ final Entity[] unloadedEntitiesRaw = java.util.Arrays.copyOf(unloadedEntities.getRawDataUnchecked(), unloadedEntities.size());
-+ unloadedEntities.clear();
-
-- this.updateChunkTracking(entityplayer);
-+ for (final Entity entity : unloadedEntitiesRaw) {
-+ 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();
+ }
-+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$clearPlayers();
- }
++ }
+ }
+ // 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();
-@@ -1478,27 +1054,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1478,27 +1043,25 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
public void waitForLightBeforeSending(ChunkPos centerPos, int radius) {
@@ -24245,7 +25154,7 @@ index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc553038520701
}
@Nullable
-@@ -1514,7 +1088,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1514,7 +1077,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
}
@@ -24254,7 +25163,7 @@ index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc553038520701
public final ServerEntity serverEntity;
final Entity entity;
-@@ -1522,6 +1096,84 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+@@ -1522,6 +1085,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
@@ -24334,11 +25243,51 @@ index ec19eb88705a07db45f1a3541571fb7f43efb5a9..60adb0caf3d6f03a57fc553038520701
+ 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;
+@@ -1610,20 +1256,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 = ca.spottedleaf.moonrise.common.PlatformHooks.get().modifyEntityTrackingRange(entity, 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/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
index f7c2c03749d6be25bf33afd61e1da120770b3432..64da6726634fc223c0e6dcab4d83a6c8997ff196 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -25152,7 +26101,7 @@ index 65206fdfa5b94eaca139e433b4865c16b16641f3..bf4463bcb5dc439ac5a3fa08dd60845a
}
}
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815d46bff2e 100644
+index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..23a13bfd23514cde6dcf8d59ba3b43d84f266aad 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -52,7 +52,7 @@ import net.minecraft.world.level.storage.DimensionDataStorage;
@@ -25164,7 +26113,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
private static final Logger LOGGER = LogUtils.getLogger();
private final DistanceManager distanceManager;
-@@ -78,6 +78,62 @@ public class ServerChunkCache extends ChunkSource {
+@@ -78,6 +78,71 @@ public class ServerChunkCache extends ChunkSource {
private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
long chunkFutureAwaitCounter;
// Paper end
@@ -25189,7 +26138,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
+ 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.executor.standard.PrioritisedExecutor.Priority.BLOCKING,
++ chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.util.Priority.BLOCKING,
+ completable::complete
+ );
+
@@ -25220,6 +26169,15 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
+ 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
@@ -25227,7 +26185,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
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;
-@@ -109,13 +165,7 @@ public class ServerChunkCache extends ChunkSource {
+@@ -109,13 +174,7 @@ public class ServerChunkCache extends ChunkSource {
}
// CraftBukkit end
// Paper start
@@ -25242,7 +26200,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
@Nullable
public ChunkAccess getChunkAtImmediately(int x, int z) {
-@@ -186,63 +236,25 @@ public class ServerChunkCache extends ChunkSource {
+@@ -186,63 +245,42 @@ public class ServerChunkCache extends ChunkSource {
@Nullable
@Override
public ChunkAccess getChunk(int x, int z, ChunkStatus leastStatus, boolean create) {
@@ -25261,14 +26219,14 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
-
- 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];
+ // Paper start - rewrite chunk system
+ if (leastStatus == ChunkStatus.FULL) {
+ final LevelChunk ret = this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(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;
- }
@@ -25311,12 +26269,28 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
- return null;
- } else {
- return this.getChunkAtIfLoadedMainThread(chunkX, chunkZ); // Paper - Perf: Optimise getChunkAt calls for loaded chunks
-- }
-+ return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); // Paper - rewrite chunk system
++ // 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() {
-@@ -273,56 +285,59 @@ public class ServerChunkCache extends ChunkSource {
+@@ -273,56 +311,59 @@ public class ServerChunkCache extends ChunkSource {
}
private CompletableFuture<ChunkResult<ChunkAccess>> getChunkFutureMainThread(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
@@ -25374,7 +26348,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
+
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
+ chunkX, chunkZ, leastStatus, true,
-+ ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER,
+ complete
+ );
@@ -25414,7 +26388,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
}
@Override
-@@ -335,16 +350,7 @@ public class ServerChunkCache extends ChunkSource {
+@@ -335,16 +376,7 @@ public class ServerChunkCache extends ChunkSource {
}
public boolean runDistanceManagerUpdates() { // Paper - public
@@ -25432,7 +26406,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
}
// Paper start
-@@ -354,17 +360,14 @@ public class ServerChunkCache extends ChunkSource {
+@@ -354,17 +386,14 @@ public class ServerChunkCache extends ChunkSource {
// Paper end
public boolean isPositionTicking(long pos) {
@@ -25455,7 +26429,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
try (co.aikar.timings.Timing timed = level.timings.chunkSaveData.startTiming()) { // Paper - Timings
this.chunkMap.saveAllChunks(flush);
} // Paper - Timings
-@@ -377,17 +380,15 @@ public class ServerChunkCache extends ChunkSource {
+@@ -377,17 +406,15 @@ public class ServerChunkCache extends ChunkSource {
}
public void close(boolean save) throws IOException {
@@ -25476,7 +26450,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
ProfilerFiller gameprofilerfiller = Profiler.get();
gameprofilerfiller.push("purge");
-@@ -415,6 +416,7 @@ public class ServerChunkCache extends ChunkSource {
+@@ -415,6 +442,7 @@ public class ServerChunkCache extends ChunkSource {
gameprofilerfiller.popPush("chunks");
if (tickChunks) {
this.level.timings.chunks.startTiming(); // Paper - timings
@@ -25484,7 +26458,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
this.tickChunks();
this.level.timings.chunks.stopTiming(); // Paper - timings
this.chunkMap.tick();
-@@ -546,11 +548,12 @@ public class ServerChunkCache extends ChunkSource {
+@@ -546,11 +574,13 @@ public class ServerChunkCache extends ChunkSource {
}
private void getFullChunk(long pos, Consumer<LevelChunk> chunkConsumer) {
@@ -25493,7 +26467,8 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
- if (playerchunk != null) {
- ((ChunkResult) playerchunk.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).ifSuccess(chunkConsumer);
+ // Paper start - rewrite chunk system
-+ final LevelChunk fullChunk = this.getChunkNow(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos));
++ // note: bypass currentlyLoaded from getChunkNow
++ final LevelChunk fullChunk = this.fullChunks.get(pos);
+ if (fullChunk != null) {
+ chunkConsumer.accept(fullChunk);
}
@@ -25501,7 +26476,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
}
-@@ -644,6 +647,12 @@ public class ServerChunkCache extends ChunkSource {
+@@ -644,6 +674,12 @@ public class ServerChunkCache extends ChunkSource {
this.chunkMap.setServerViewDistance(watchDistance);
}
@@ -25514,7 +26489,7 @@ index 7e5714fea4cda68b9ae21031c0e0d39061b07e2f..22157cbc4e4bb1e4010116bdf7429815
public void setSimulationDistance(int simulationDistance) {
this.distanceManager.updateSimulationDistance(simulationDistance);
}
-@@ -735,21 +744,19 @@ public class ServerChunkCache extends ChunkSource {
+@@ -735,21 +771,19 @@ public class ServerChunkCache extends ChunkSource {
@Override
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
public boolean pollTask() {
@@ -25559,7 +26534,7 @@ index bc0f1aa61e68d2a8638d89c10bc5c71922d057f9..79c88b315481fe70f037bae834f2b072
if (!list.equals(this.lastPassengers)) {
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9fb00a037 100644
+index 509a67aff07bcdcad47eb77e923d442349a4f20c..c7523387f0e9bbfe952abd237a936c8319f10200 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -187,7 +187,7 @@ import org.bukkit.event.weather.LightningStrikeEvent;
@@ -25634,7 +26609,9 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
+ public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
+ return this.chunkSource.getChunkNow(chunkX, chunkZ);
+ }
-+
+
+- int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
+- int[] loadedChunks = new int[1];
+ @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));
@@ -25644,7 +26621,8 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
+ final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder.ChunkCompletion lastCompletion = newChunkHolder.getLastChunkCompletion();
+ return lastCompletion == null ? null : lastCompletion.chunk();
+ }
-+
+
+- Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
+ @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);
@@ -25658,20 +26636,18 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
+ public final void moonrise$midTickTasks() {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer)this.server).moonrise$executeMidTickTasks();
+ }
-+
+
+- java.util.function.Consumer<net.minecraft.world.level.chunk.ChunkAccess> consumer = (net.minecraft.world.level.chunk.ChunkAccess chunk) -> {
+ @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);
+ }
-
-- int requiredChunks = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1);
-- int[] loadedChunks = new int[1];
++
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler moonrise$getChunkTaskScheduler() {
+ return this.chunkTaskScheduler;
+ }
-
-- Long holderIdentifier = Long.valueOf(chunkProvider.chunkFutureAwaitCounter++);
++
+ @Override
+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getChunkDataController() {
+ return this.chunkDataController;
@@ -25681,8 +26657,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
+ public final ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController() {
+ return this.poiDataController;
+ }
-
-- 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.RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController() {
+ return this.entityDataController;
@@ -25798,8 +26773,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
+ @Override
+ public final long moonrise$getLastMidTickFailure() {
+ return this.lastMidTickFailure;
- }
-- // Paper end - optimise getPlayerByUUID
++ }
+
+ @Override
+ public final void moonrise$setLastMidTickFailure(final long time) {
@@ -25814,7 +26788,8 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
+ @Override
+ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks() {
+ return this.loadedChunks;
-+ }
+ }
+- // Paper end - optimise getPlayerByUUID
+
+ @Override
+ public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getTickingChunks() {
@@ -25895,16 +26870,18 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
}
protected void tickTime() {
-@@ -613,6 +774,63 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -613,7 +774,60 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
});
}
+ // Paper start - optimise random ticking
++ private final ca.spottedleaf.moonrise.common.util.SimpleRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleRandom(0L);
++
+ 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 RandomSource random = this.random;
-+ final boolean tickFluids = false; // Paper - not configurable - MC-224294
++ final ca.spottedleaf.moonrise.common.util.SimpleRandom simpleRandom = this.simpleRandom;
++ final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294();
+
+ final ChunkPos cpos = chunk.getPos();
+ final int offsetX = cpos.x << 4;
@@ -25914,39 +26891,32 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
+ 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 == null || !section.isRandomlyTickingBlocks()) {
++ if (!section.isRandomlyTickingBlocks()) {
+ continue;
+ }
+
-+ final ca.spottedleaf.moonrise.common.list.IBlockDataList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList();
-+ if (tickList.size() == 0) {
-+ 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 = random.nextInt() & ((16 * 16 * 16) - 1);
++ final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1);
+
+ if (index >= tickingBlocks) {
+ // most of the time we fall here
+ continue;
+ }
+
-+ final long raw = tickList.getRaw(index);
-+ final int location = ca.spottedleaf.moonrise.common.list.IBlockDataList.getLocationFromRaw(raw);
-+ final int randomX = (location & 15);
-+ final int randomY = ((location >>> (4 + 4)) & 255);
-+ final int randomZ = ((location >>> 4) & 15);
-+ final BlockState state = states.get(randomX | (randomZ << 4) | (randomY << 8));
++ 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(randomX | offsetX, randomY | offsetY, randomZ | offsetZ);
++ final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ);
+
-+ state.randomTick((ServerLevel)(Object)this, pos, random);
-+ if (tickFluids) {
++ state.randomTick((ServerLevel)(Object)this, pos, simpleRandom);
++ if (doubleTickFluids) {
+ final FluidState fluidState = state.getFluidState();
+ if (fluidState.isRandomlyTicking()) {
-+ fluidState.randomTick((ServerLevel)(Object)this, pos, random);
++ fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom);
+ }
+ }
+ }
@@ -25957,9 +26927,29 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
+ // Paper end - optimise random ticking
+
public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
++ final ca.spottedleaf.moonrise.common.util.SimpleRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking
ChunkPos chunkcoordintpair = chunk.getPos();
boolean flag = this.isRaining();
-@@ -662,35 +880,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+ int j = chunkcoordintpair.getMinBlockX();
+@@ -621,7 +835,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)) {
+@@ -653,7 +867,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));
+ }
+ }
+@@ -662,35 +876,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
gameprofilerfiller.popPush("tickBlocks");
timings.chunkTicksBlocks.startTiming(); // Paper
if (randomTickSpeed > 0) {
@@ -25996,7 +26986,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
}
timings.chunkTicksBlocks.stopTiming(); // Paper
-@@ -964,6 +1154,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -964,6 +1150,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
if (fluid1.is(fluid)) {
fluid1.tick(this, pos, iblockdata);
}
@@ -26008,7 +26998,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
}
-@@ -973,6 +1168,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -973,6 +1164,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
if (iblockdata.is(block)) {
iblockdata.tick(this, pos, this.random);
}
@@ -26020,7 +27010,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
}
-@@ -1049,6 +1249,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1049,6 +1245,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
public void save(@Nullable ProgressListener progressListener, boolean flush, boolean savingDisabled) {
@@ -26032,7 +27022,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
ServerChunkCache chunkproviderserver = this.getChunkSource();
if (!savingDisabled) {
-@@ -1064,16 +1269,21 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1064,16 +1265,21 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
timings.worldSaveChunks.startTiming(); // Paper
@@ -26060,7 +27050,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
// CraftBukkit start - moved from MinecraftServer.saveChunks
ServerLevel worldserver1 = this;
-@@ -1213,7 +1423,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1213,7 +1419,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
this.removePlayerImmediately((ServerPlayer) entity, Entity.RemovalReason.DISCARDED);
}
@@ -26069,7 +27059,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
}
// CraftBukkit start
-@@ -1243,7 +1453,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1243,7 +1449,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
// CraftBukkit end
@@ -26078,7 +27068,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
}
}
-@@ -1254,11 +1464,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1254,11 +1460,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
public boolean tryAddFreshEntityWithPassengers(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
// CraftBukkit end
@@ -26091,7 +27081,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
return false;
} else {
this.addFreshEntityWithPassengers(entity, reason); // CraftBukkit
-@@ -1891,7 +2097,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1891,7 +2093,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
}
@@ -26100,7 +27090,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
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()));
-@@ -1940,7 +2146,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1940,7 +2142,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
BufferedWriter bufferedwriter2 = Files.newBufferedWriter(path1);
try {
@@ -26109,7 +27099,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
} catch (Throwable throwable4) {
if (bufferedwriter2 != null) {
try {
-@@ -1961,7 +2167,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -1961,7 +2163,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
BufferedWriter bufferedwriter3 = Files.newBufferedWriter(path2);
try {
@@ -26118,7 +27108,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
} catch (Throwable throwable6) {
if (bufferedwriter3 != null) {
try {
-@@ -2103,7 +2309,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -2103,7 +2305,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@VisibleForTesting
public String getWatchdogStats() {
@@ -26127,7 +27117,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
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());
}
-@@ -2133,15 +2339,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -2133,15 +2335,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public LevelEntityGetter<Entity> getEntities() {
org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
@@ -26156,7 +27146,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
}
public void startTickingChunk(LevelChunk chunk) {
-@@ -2161,34 +2377,47 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -2161,34 +2373,47 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public void close() throws IOException {
super.close();
@@ -26211,7 +27201,7 @@ index 509a67aff07bcdcad47eb77e923d442349a4f20c..af27a004f618912d6a5f1d2c82c8e7e9
}
@Override
-@@ -2234,7 +2463,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
+@@ -2234,7 +2459,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
CrashReportCategory crashreportsystemdetails = super.fillReportDetails(report);
crashreportsystemdetails.setDetail("Loaded entity count", () -> {
@@ -26715,8 +27705,21 @@ index cdd66e6ce96e2613afe7f06ca8da3cfaa6704b2d..32634e45ac8433648e49e47e20081e15
handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.getLightEngine(), null, null));
// Paper start - PlayerChunkLoadEvent
if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) {
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index 4fe3024e26b56c2d796acf703a1bc200ff309f09..7529b3d90e65036c7bf869af30475932d547b3ab 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -1407,7 +1407,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()) {
diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java
-index 68648c5a5e3ff079f832092af0f2f801c42d1ede..19661e106612b8e4e152085fb398db7bd06acc23 100644
+index 68648c5a5e3ff079f832092af0f2f801c42d1ede..e4e153cb8899e70273aa150b8ea26907cf68b15c 100644
--- a/src/main/java/net/minecraft/util/BitStorage.java
+++ b/src/main/java/net/minecraft/util/BitStorage.java
@@ -2,7 +2,7 @@ package net.minecraft.util;
@@ -26736,60 +27739,234 @@ index 68648c5a5e3ff079f832092af0f2f801c42d1ede..19661e106612b8e4e152085fb398db7b
+ // 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.ints.IntArrayList> moonrise$countEntries() {
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>();
++ 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.ints.IntArrayList();
-+ }).add(index);
++ return new it.unimi.dsi.fastutil.shorts.ShortArrayList();
++ }).add((short)index);
+ }
+
+ return ret;
+ }
+ // Paper end - block counting
}
+diff --git a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
+index 61dee55417bc802e25b9ba2f271d32d8c12844a9..a8a260a3caaa8e5004069b833ecc8b17b2fc8db5 100644
+--- a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
++++ b/src/main/java/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/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java
-index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..8acf2f2491a8d9d13392c5e89b2bd5c9918285e1 100644
+index 9f438d9c6eb05e43d24e4af68188a3d4c46a938c..d99ec470b4653beab630999a5b2c1a6428b20c38 100644
--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java
+++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java
-@@ -362,6 +362,40 @@ public class SimpleBitStorage implements BitStorage {
+@@ -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.ints.IntArrayList> moonrise$countEntries() {
++ 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 = this.mask;
++ final long mask = (1L << bits) - 1L;
+ final int size = this.size;
+
-+ // we may be backed by global palette, so limit bits for init capacity
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(
-+ 1 << Math.min(6, bits)
-+ );
++ 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;
++ int index = 0;
+
-+ for (long value : this.data) {
-+ int li = 0;
-+ do {
-+ final int paletteIdx = (int)(value & mask);
-+ value >>= bits;
++ 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.ints.IntArrayList();
-+ }).add(index);
++ 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);
++ }
+
-+ ++li;
-+ ++index;
-+ } 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
++ );
+
-+ return ret;
++ 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
+
@@ -26892,7 +28069,7 @@ index ea72dcb064a35bc6245bc5c94d592efedd8faf41..87ee8e51dfa7657ed7d83fcbceef48bf
this.comparator = comparator;
if (initialCapacity < 0) {
diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java
-index 50040c497a819cd1229042ab3cb057d34a32cacc..15c5164d0ef41a978c16ee317fa73e97f2480207 100644
+index 50040c497a819cd1229042ab3cb057d34a32cacc..1f9c436a632e4f110be61cf76fcfc3b7eb80334e 100644
--- a/src/main/java/net/minecraft/util/ZeroBitStorage.java
+++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java
@@ -62,4 +62,22 @@ public class ZeroBitStorage implements BitStorage {
@@ -26902,24 +28079,24 @@ index 50040c497a819cd1229042ab3cb057d34a32cacc..15c5164d0ef41a978c16ee317fa73e97
+
+ // Paper start - block counting
+ @Override
-+ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() {
++ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() {
+ final int size = this.size;
+
-+ final int[] raw = new int[size];
++ final short[] raw = new short[size];
+ for (int i = 0; i < size; ++i) {
-+ raw[i] = i;
++ raw[i] = (short)i;
+ }
+
-+ final it.unimi.dsi.fastutil.ints.IntArrayList coordinates = it.unimi.dsi.fastutil.ints.IntArrayList.wrap(raw, size);
++ 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.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1);
++ 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/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
-index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cfc5a31a89 100644
+index 8cdef637f6343119fc77f87e7478ee23e9b8efab..a3b0363fbc207ed9edc8a4d6619b6fff9389a9c7 100644
--- a/src/main/java/net/minecraft/world/entity/Entity.java
+++ b/src/main/java/net/minecraft/world/entity/Entity.java
@@ -175,7 +175,7 @@ import org.bukkit.event.player.PlayerTeleportEvent;
@@ -26931,13 +28108,14 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
// CraftBukkit start
private static final int CURRENT_LEVEL = 2;
-@@ -445,6 +445,97 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -445,6 +445,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;
@@ -26959,6 +28137,16 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
+ }
+
+ @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;
+ }
@@ -27006,6 +28194,54 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
+ 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;
+
@@ -27029,7 +28265,7 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
public Entity(EntityType<?> type, Level world) {
this.id = Entity.ENTITY_COUNTER.incrementAndGet();
-@@ -1303,41 +1394,82 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -1303,41 +1453,76 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
private Vec3 collide(Vec3 movement) {
@@ -27061,85 +28297,80 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
- float[] afloat = Entity.collectCandidateStepUpHeights(axisalignedbb1, list1, this.maxUpStep(), f);
- float[] afloat1 = afloat;
- int i = afloat.length;
-+ final Level world = this.level;
-+ final AABB currBoundingBox = this.getBoundingBox();
++ final AABB currentBox = this.getBoundingBox();
- 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);
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(currBoundingBox)) {
-+ return movement;
-+ }
++ final List<VoxelShape> potentialCollisionsVoxel = new ArrayList<>();
++ final List<AABB> potentialCollisionsBB = new ArrayList<>();
- if (vec3d2.horizontalDistanceSqr() > vec3d1.horizontalDistanceSqr()) {
- double d0 = axisalignedbb.minY - axisalignedbb1.minY;
-+ final List<AABB> potentialCollisionsBB = new ArrayList<>();
-+ final List<VoxelShape> potentialCollisionsVoxel = new ArrayList<>();
-+ final double stepHeight = (double)this.maxUpStep();
-+ final AABB collisionBox;
-+ final boolean onGround = this.onGround;
-
-- return vec3d2.add(0.0D, -d0, 0.0D);
++ final AABB initialCollisionBox;
+ if (xZero & zZero) {
-+ if (movement.y > 0.0) {
-+ collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutUpwards(currBoundingBox, movement.y);
-+ } else {
-+ collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutDownwards(currBoundingBox, movement.y);
-+ }
++ // 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 {
-+ // note: xZero == false or zZero == false
-+ if (stepHeight > 0.0 && (onGround || (movement.y < 0.0))) {
-+ // don't bother getting the collisions if we don't need them.
-+ if (movement.y <= 0.0) {
-+ collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(movement.x, movement.y, movement.z), stepHeight);
-+ } else {
-+ collisionBox = currBoundingBox.expandTowards(movement.x, Math.max(stepHeight, movement.y), movement.z);
- }
-+ } else {
-+ collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z);
- }
- }
++ initialCollisionBox = currentBox.expandTowards(movement);
++ }
-- return vec3d1;
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisions(
-+ world, (Entity)(Object)this, collisionBox, potentialCollisionsVoxel, potentialCollisionsBB,
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
-+ null, null
+- return vec3d2.add(0.0D, -d0, 0.0D);
+- }
++ final List<AABB> entityAABBs = new ArrayList<>();
++ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions(
++ this.level, (Entity)(Object)this, initialCollisionBox, entityAABBs, 0, null
+ );
+
-+ if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) {
-+ return movement;
-+ }
-+
-+ final Vec3 limitedMoveVector = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB);
++ 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);
+
-+ if (stepHeight > 0.0
-+ && (onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0))
-+ && (limitedMoveVector.x != movement.x || limitedMoveVector.z != movement.z)) {
-+ Vec3 vec3d2 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, stepHeight, movement.z), currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB);
-+ final Vec3 vec3d3 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisionsVoxel, potentialCollisionsBB);
++ final boolean collidedX = collided.x != movement.x;
++ final boolean collidedY = collided.y != movement.y;
++ final boolean collidedZ = collided.z != movement.z;
+
-+ if (vec3d3.y < stepHeight) {
-+ final Vec3 vec3d4 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, 0.0D, movement.z), currBoundingBox.move(vec3d3), potentialCollisionsVoxel, potentialCollisionsBB).add(vec3d3);
++ final boolean collidedDownwards = collidedY && movement.y < 0.0;
+
-+ if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) {
-+ vec3d2 = vec3d4;
-+ }
-+ }
++ final double stepHeight;
+
-+ if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) {
-+ return vec3d2.add(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisionsVoxel, potentialCollisionsBB));
-+ }
++ if ((!collidedDownwards && !this.onGround) || (!collidedX && !collidedZ) || (stepHeight = (double)this.maxUpStep()) <= 0.0) {
++ return collided;
++ }
+
-+ return limitedMoveVector;
-+ } else {
-+ return limitedMoveVector;
++ 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) {
-@@ -2699,18 +2831,75 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -2699,18 +2884,110 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
public boolean isInWall() {
@@ -27153,76 +28384,111 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
- return BlockPos.betweenClosedStream(axisalignedbb).anyMatch((blockposition) -> {
- BlockState iblockdata = this.level().getBlockState(blockposition);
-+ final float reducedWith = this.dimensions.width() * 0.8F;
-+ final AABB box = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith);
++ 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(box)) {
++ 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 BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos();
++ final int maxBlockX = Mth.floor(boundingBox.maxX);
++ final int maxBlockY = Mth.floor(boundingBox.maxY);
++ final int maxBlockZ = Mth.floor(boundingBox.maxZ);
+
-+ final int minX = Mth.floor(box.minX);
-+ final int minY = Mth.floor(box.minY);
-+ final int minZ = Mth.floor(box.minZ);
-+ final int maxX = Mth.floor(box.maxX);
-+ final int maxY = Mth.floor(box.maxY);
-+ final int maxZ = Mth.floor(box.maxZ);
++ final int minChunkX = minBlockX >> 4;
++ final int minChunkY = minBlockY >> 4;
++ final int minChunkZ = minBlockZ >> 4;
+
-+ final net.minecraft.world.level.chunk.ChunkSource chunkProvider = this.level.getChunkSource();
++ final int maxChunkX = maxBlockX >> 4;
++ final int maxChunkY = maxBlockY >> 4;
++ final int maxChunkZ = maxBlockZ >> 4;
+
-+ long lastChunkKey = ChunkPos.INVALID_CHUNK_POS;
-+ net.minecraft.world.level.chunk.LevelChunk lastChunk = null;
-+ for (int fz = minZ; fz <= maxZ; ++fz) {
-+ tempPos.setZ(fz);
-+ for (int fx = minX; fx <= maxX; ++fx) {
-+ final int newChunkX = fx >> 4;
-+ final int newChunkZ = fz >> 4;
-+ final net.minecraft.world.level.chunk.LevelChunk chunk = lastChunkKey == (lastChunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ?
-+ lastChunk : (lastChunk = (net.minecraft.world.level.chunk.LevelChunk)chunkProvider.getChunk(newChunkX, newChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true));
-+ tempPos.setX(fx);
-+ for (int fy = minY; fy <= maxY; ++fy) {
-+ tempPos.setY(fy);
++ 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();
+
-+ final BlockState state = chunk.getBlockState(tempPos);
++ 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();
+
-+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$emptyCollisionShape() || !state.isSuffocating(this.level, tempPos)) {
++ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
++ final int sectionIdx = currChunkY - minSection;
++ if (sectionIdx < 0 || sectionIdx >= sections.length) {
+ continue;
+ }
-+
-+ // Yes, it does not use the Entity context stuff.
-+ final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos);
-+
-+ if (collisionShape.isEmpty()) {
++ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
++ if (section.hasOnlyAir()) {
++ // empty
+ continue;
+ }
+
-+ final AABB toCollide = box.move(-(double)fx, -(double)fy, -(double)fz);
++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states;
+
-+ 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;
-+ }
++ 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;
+
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) {
-+ return true;
++ 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;
++ }
++ }
+ }
-+ continue;
+ }
+ }
- }
++ }
+
+ return false;
+ // Paper end - optimise collisions
}
public InteractionResult interact(Player player, InteractionHand hand) {
-@@ -4180,14 +4369,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4180,14 +4457,17 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
public Iterable<Entity> getIndirectPassengers() {
@@ -27247,7 +28513,202 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
}
private Iterable<Entity> getIndirectPassengers_old() {
// Paper end - Optimize indirect passenger iteration
-@@ -4543,6 +4735,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4316,82 +4596,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;
++
++ 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;
++ }
++
++ mutablePos.set(currX | (currChunkX << 4), currY | (currChunkY << 4), currZ | (currChunkZ << 4));
+
+- vec3d = vec3d.add(vec3d1);
+- ++k1;
++ final double height = (double)((float)mutablePos.getY() + fluidState.getHeight(world, mutablePos));
++ final double diff = height - boundingBox.minY;
++
++ if (diff < 0.0) {
++ continue;
+ }
+- // CraftBukkit start - store last lava contact location
+- if (tag == FluidTags.LAVA) {
+- this.lastLavaContact = blockposition_mutableblockposition.immutable();
++
++ 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);
+@@ -4543,6 +4877,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) {
@@ -27263,7 +28724,7 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
if (!checkPosition(this, x, y, z)) {
return;
}
-@@ -4672,6 +4873,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4672,6 +5015,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Override
public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
@@ -27276,7 +28737,7 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
CraftEventFactory.callEntityRemoveEvent(this, cause);
// CraftBukkit end
if (this.removalReason == null) {
-@@ -4682,7 +4889,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4682,7 +5031,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.stopRiding();
}
@@ -27285,7 +28746,7 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
this.levelCallback.onRemove(entity_removalreason);
this.onRemoval(entity_removalreason);
}
-@@ -4698,7 +4905,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
+@@ -4698,7 +5047,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Override
public boolean shouldBeSaved() {
@@ -27295,10 +28756,10 @@ index 8cdef637f6343119fc77f87e7478ee23e9b8efab..5410a0380c44629f1c9b4f0a8e6017cf
@Override
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-index 96bc0ba60195e5e666d47b3a0b943b733986d96a..4cf6cb0abfeb7065c6d9381fb4194371c0cddc35 100644
+index 96bc0ba60195e5e666d47b3a0b943b733986d96a..5930a430983061afddf20e3208ff2462ca1b78cd 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-@@ -38,12 +38,130 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
+@@ -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;
@@ -27338,8 +28799,7 @@ index 96bc0ba60195e5e666d47b3a0b943b733986d96a..4cf6cb0abfeb7065c6d9381fb4194371
+
+ 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);
++ 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);
+ }
@@ -27393,9 +28853,13 @@ index 96bc0ba60195e5e666d47b3a0b943b733986d96a..4cf6cb0abfeb7065c6d9381fb4194371
+ 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 section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
-+ final long sectionPos = SectionPos.asLong(chunkX, section, chunkZ);
++ for (int sectionY = minY; sectionY <= maxY; ++sectionY) {
++ final long sectionPos = SectionPos.asLong(chunkX, sectionY, chunkZ);
+ this.updateDistanceTracking(sectionPos);
+ }
+ }
@@ -27404,8 +28868,12 @@ index 96bc0ba60195e5e666d47b3a0b943b733986d96a..4cf6cb0abfeb7065c6d9381fb4194371
+ 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 = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) {
++ 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));
@@ -27430,7 +28898,7 @@ index 96bc0ba60195e5e666d47b3a0b943b733986d96a..4cf6cb0abfeb7065c6d9381fb4194371
public PoiManager(
RegionStorageInfo storageKey,
Path directory,
-@@ -64,6 +182,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
+@@ -64,6 +189,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
world
);
this.distanceTracker = new PoiManager.DistanceTracker();
@@ -27438,18 +28906,20 @@ index 96bc0ba60195e5e666d47b3a0b943b733986d96a..4cf6cb0abfeb7065c6d9381fb4194371
}
public void add(BlockPos pos, Holder<PoiType> type) {
-@@ -197,8 +316,8 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
+@@ -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());
-+ this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
-+ return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - rewrite chunk system
++ // 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 +331,26 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
+@@ -212,19 +340,26 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
@Override
public void tick(BooleanSupplier shouldKeepTicking) {
@@ -27482,7 +28952,7 @@ index 96bc0ba60195e5e666d47b3a0b943b733986d96a..4cf6cb0abfeb7065c6d9381fb4194371
}
public void checkConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection chunkSection) {
-@@ -263,7 +389,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> {
+@@ -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())
@@ -27492,7 +28962,7 @@ index 96bc0ba60195e5e666d47b3a0b943b733986d96a..4cf6cb0abfeb7065c6d9381fb4194371
}
diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-index b9e0bc8f1e948614d986335de1f3d2df199eea81..f6f0d7c21ee81ff33d4af350c4d39aadfbe140df 100644
+index b9e0bc8f1e948614d986335de1f3d2df199eea81..712cbfc100e8aaf612d1d651dae64f57f892a768 100644
--- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
@@ -23,13 +23,27 @@ import net.minecraft.core.SectionPos;
@@ -27508,7 +28978,7 @@ index b9e0bc8f1e948614d986335de1f3d2df199eea81..f6f0d7c21ee81ff33d4af350c4d39aad
private boolean isValid;
+ // Paper start - rewrite chunk system
-+ private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this);;
++ private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this);
+
+ @Override
+ public final boolean moonrise$isEmpty() {
@@ -27675,7 +29145,7 @@ index e185a33b5b1f8e8e0a0e666b24ba3e9186a8a7ff..5d7a6e4b73f032db356e7ec369b15001
// Paper start - Affects Spawning API
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
-index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e86a982f8 100644
+index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..40fe47c7c145587ac81f0f15c237ed72ea9c094d 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -83,6 +83,7 @@ import net.minecraft.world.level.storage.LevelData;
@@ -27695,7 +29165,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
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"));
-@@ -190,6 +191,483 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -190,7 +191,584 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
public abstract ResourceKey<LevelStem> getTypeKey();
@@ -27772,26 +29242,13 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ }
+ // Paper end - rewrite chunk system
+ // Paper start - optimise collisions
-+ private final int minSection;
-+ private final int maxSection;
-+
-+ @Override
-+ public final int moonrise$getMinSection() {
-+ return this.minSection;
-+ }
-+
-+ @Override
-+ public final int moonrise$getMaxSection() {
-+ return this.maxSection;
-+ }
-+
+ /**
+ * Route to faster lookup.
+ * See {@link EntityGetter#isUnobstructed(Entity, VoxelShape)} for expected behavior
+ * @author Spottedleaf
+ */
+ @Override
-+ public final boolean isUnobstructed(final Entity entity) {
++ public boolean isUnobstructed(final Entity entity) {
+ final AABB boundingBox = entity.getBoundingBox();
+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) {
+ return false;
@@ -27821,7 +29278,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ final Vec3 to = clipContext.getTo();
+ final Vec3 from = clipContext.getFrom();
+
-+ return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z));
++ 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();
@@ -27875,7 +29332,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ int lastChunkY = Integer.MIN_VALUE;
+ int lastChunkZ = Integer.MIN_VALUE;
+
-+ final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)level).moonrise$getMinSection();
++ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level);
+
+ for (;;) {
+ currPos.set(currX, currY, currZ);
@@ -27958,7 +29415,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ * @author Spottedleaf
+ */
+ @Override
-+ public final net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) {
++ 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);
+ }
@@ -27968,7 +29425,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ * @author Spottedleaf
+ */
+ @Override
-+ public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) {
++ 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) -> {
@@ -27994,8 +29451,8 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ * @author Spottedleaf
+ */
+ @Override
-+ public final 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) {
++ 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();
+ }
@@ -28054,103 +29511,139 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ * @author Spottedleaf
+ */
+ @Override
-+ public final java.util.Optional<net.minecraft.core.BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) {
++ 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 = Mth.floor(aabb.minY - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1;
-+ final int maxBlockY = Mth.floor(aabb.maxY + 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;
+
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext collisionContext = null;
-+
-+ final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
++ 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();
+
-+ LevelChunk lastChunk = null;
-+ int lastChunkX = Integer.MIN_VALUE;
-+ int lastChunkZ = Integer.MIN_VALUE;
++ // special cases:
++ if (minBlockY > maxBlockY) {
++ // no point in checking
++ return java.util.Optional.empty();
++ }
+
-+ final ChunkSource chunkSource = this.getChunkSource();
++ final int minChunkX = minBlockX >> 4;
++ final int maxChunkX = maxBlockX >> 4;
+
-+ for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) {
-+ pos.setZ(currZ);
-+ for (int currX = minBlockX; currX <= maxBlockX; ++currX) {
-+ pos.setX(currX);
++ final int minChunkY = minBlockY >> 4;
++ final int maxChunkY = maxBlockY >> 4;
+
-+ final int newChunkX = currX >> 4;
-+ final int newChunkZ = currZ >> 4;
++ final int minChunkZ = minBlockZ >> 4;
++ final int maxChunkZ = maxBlockZ >> 4;
+
-+ if (((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)) != 0) {
-+ lastChunkX = newChunkX;
-+ lastChunkZ = newChunkZ;
-+ lastChunk = (LevelChunk)chunkSource.getChunk(newChunkX, newChunkZ, ChunkStatus.FULL, false);
-+ }
++ final ChunkSource chunkSource = this.getChunkSource();
+
-+ if (lastChunk == null) {
++ 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;
+ }
-+ for (int currY = minBlockY; currY <= maxBlockY; ++currY) {
-+ int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) +
-+ ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) +
-+ ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0);
-+ if (edgeCount == 3) {
-+ continue;
-+ }
+
-+ pos.setY(currY);
++ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections();
+
-+ final double distance = pos.distToCenterSqr(entityPos);
-+ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) {
++ // bound y
++ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
++ final int sectionIdx = currChunkY - minSection;
++ if (sectionIdx < 0 || sectionIdx >= sections.length) {
+ continue;
+ }
-+
-+ final BlockState state = ((ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk)lastChunk).moonrise$getBlock(currX, currY, currZ);
-+ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$emptyCollisionShape()) {
++ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
++ if (section.hasOnlyAir()) {
++ // empty
+ continue;
+ }
+
-+ VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$getConstantCollisionShape();
++ final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$hasSpecialCollidingBlocks();
++ final int sectionAdjust = !hasSpecial ? 1 : 0;
+
-+ if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) {
-+ if (collisionContext == null) {
-+ collisionContext = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity);
-+ }
++ final net.minecraft.world.level.chunk.PalettedContainer<net.minecraft.world.level.block.state.BlockState> blocks = section.states;
+
-+ if (blockCollision == null) {
-+ blockCollision = state.getCollisionShape((Level)(Object)this, pos, collisionContext);
-+ }
++ 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;
+
-+ if (blockCollision.isEmpty()) {
-+ continue;
-+ }
++ 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);
+
-+ // avoid VoxelShape#move by shifting the entity collision shape instead
-+ final AABB shiftedAABB = aabb.move(-(double)currX, -(double)currY, -(double)currZ);
++ 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 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;
-+ }
++ final double distance = mutablePos.distToCenterSqr(entityPos);
++ if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(mutablePos) >= 0)) {
++ continue;
++ }
+
-+ selected = pos.immutable();
-+ selectedDistance = distance;
-+ continue;
-+ }
++ final BlockState blockData = blocks.get(localBlockIndex);
+
-+ if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) {
-+ continue;
-+ }
++ if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) {
++ continue;
++ }
+
-+ selected = pos.immutable();
-+ selectedDistance = distance;
-+ 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;
++ }
++ }
++ }
+ }
+ }
+ }
@@ -28159,6 +29652,74 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ 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);
@@ -28177,9 +29738,19 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ // 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) { // Paper - create paper world config
++ // 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
-@@ -271,6 +749,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+ this.generator = gen;
+@@ -271,6 +849,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
this.timings = new co.aikar.timings.WorldTimingsHandler(this); // Paper - code below can generate new world and access timings
this.entityLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.entityMaxTickTime);
this.tileLimiter = new org.spigotmc.TickLimiter(this.spigotConfig.tileMaxTickTime);
@@ -28191,7 +29762,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
}
// Paper start - Cancel hit for vanished players
-@@ -535,7 +1018,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -535,7 +1118,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
}
@@ -28200,7 +29771,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
}
-@@ -800,6 +1283,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -800,6 +1383,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
// Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
boolean flag = this.tickRateManager().runsNormally();
@@ -28209,7 +29780,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
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
-@@ -815,6 +1300,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -815,6 +1400,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
// Spigot end
} else if (flag && this.shouldTickBlocksAt(tickingblockentity.getPos())) {
tickingblockentity.tick();
@@ -28221,7 +29792,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
}
}
this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075
-@@ -837,12 +1327,20 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -837,12 +1427,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
}
@@ -28243,7 +29814,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
}
// Paper end - Option to prevent armor stands from doing entity lookups
-@@ -894,7 +1392,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -894,7 +1492,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
}
// Paper end - Perf: Optimize capturedTileEntities lookup
// CraftBukkit end
@@ -28252,7 +29823,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
}
public void setBlockEntity(BlockEntity blockEntity) {
-@@ -986,26 +1484,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -986,26 +1584,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
Profiler.get().incrementCounter("getEntities");
List<Entity> list = Lists.newArrayList();
@@ -28260,17 +29831,17 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
- if (entity1 != except && predicate.test(entity1)) {
- list.add(entity1);
- }
-+ // Paper start - rewrite chunk system
-+ final List<Entity> ret = new java.util.ArrayList<>();
-
+-
- if (entity1 instanceof EnderDragon) {
- EnderDragonPart[] aentitycomplexpart = ((EnderDragon) entity1).getSubEntities();
- int i = aentitycomplexpart.length;
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate);
++ // Paper start - rewrite chunk system
++ final List<Entity> ret = new java.util.ArrayList<>();
- for (int j = 0; j < i; ++j) {
- EnderDragonPart entitycomplexpart = aentitycomplexpart[j];
--
++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities(except, box, ret, predicate);
+
- if (entity1 != except && predicate.test(entitycomplexpart)) {
- list.add(entitycomplexpart);
- }
@@ -28284,7 +29855,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
}
@Override
-@@ -1020,36 +1505,77 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
+@@ -1020,36 +1605,86 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
this.getEntities(filter, box, predicate, result, Integer.MAX_VALUE);
}
@@ -28359,7 +29930,7 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ } else {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel)this).moonrise$getEntityLookup().getEntities((Entity)null, boundingBox, (List)into, (Predicate)modifiedPredicate);
+ 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);
@@ -28370,15 +29941,24 @@ index d048d0e4b16459b5bad44ebfa3c6a8f336f6762b..332dc7e6bdfb5b3741764d4877185a2e
+ }
+ }
+ }
-
-- 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];
+ }
-+ return slices.getChunkEntities();
++
++ 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 AbortableIterationConsumer.Continuation.CONTINUE;
+- });
++ return ret.toArray(new org.bukkit.entity.Entity[0]);
}
+ // Paper end - rewrite chunk system
@@ -28434,6 +30014,20 @@ index 8590de51b572c0f73d45aee60313d466e4671da5..b725eea9d3ca81d2ef7802f5d0346d92
}
public boolean shouldFreeze(LevelReader world, BlockPos blockPos) {
+diff --git a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java b/src/main/java/net/minecraft/world/level/biome/BiomeManager.java
+index 01352cc83b25eb0e30b7e0ff521fc7c1b3d5155b..90f8360f547ce709fd13ee34f8e67d8bfa94b498 100644
+--- a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java
index 4d140bd83ca0e1554afad80ec4fc6186188a79d8..3dd236d39535cfce866eb73673f8d7f1b6dc535c 100644
--- a/src/main/java/net/minecraft/world/level/block/Block.java
@@ -28448,7 +30042,7 @@ index 4d140bd83ca0e1554afad80ec4fc6186188a79d8..3dd236d39535cfce866eb73673f8d7f1
public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) {}
diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-index 0665ca48fe2f8ab1ce1c0306b11be19b06445f74..a4b4fd83d201fff005c738c84fa5c1bc55d670bd 100644
+index 0665ca48fe2f8ab1ce1c0306b11be19b06445f74..8631655a181735df53f8a02c9eb98f0cc13f55bb 100644
--- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
@@ -793,7 +793,7 @@ public abstract class BlockBehaviour implements FeatureElement {
@@ -28465,18 +30059,12 @@ index 0665ca48fe2f8ab1ce1c0306b11be19b06445f74..a4b4fd83d201fff005c738c84fa5c1bc
private int lightBlock;
+ // Paper start - rewrite chunk system
-+ private int opacityIfCached;
+ private boolean isConditionallyFullOpaque;
+
+ @Override
+ public final boolean starlight$isConditionallyFullOpaque() {
+ return this.isConditionallyFullOpaque;
+ }
-+
-+ @Override
-+ public final int starlight$getOpacityIfCached() {
-+ return this.opacityIfCached;
-+ }
+ // Paper end - rewrite chunk system
+ // Paper start - optimise collisions
+ private static final int RANDOM_OFFSET = 704237939;
@@ -28486,16 +30074,22 @@ index 0665ca48fe2f8ab1ce1c0306b11be19b06445f74..a4b4fd83d201fff005c738c84fa5c1bc
+ 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 AABB constantAABBCollision;
+
-+ private static void initCaches(final VoxelShape shape) {
++ 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
@@ -28514,6 +30108,11 @@ index 0665ca48fe2f8ab1ce1c0306b11be19b06445f74..a4b4fd83d201fff005c738c84fa5c1bc
+ }
+
+ @Override
++ public final boolean moonrise$emptyContextCollisionShape() {
++ return this.emptyConstantCollisionShape;
++ }
++
++ @Override
+ public final int moonrise$uniqueId1() {
+ return this.id1;
+ }
@@ -28524,63 +30123,399 @@ index 0665ca48fe2f8ab1ce1c0306b11be19b06445f74..a4b4fd83d201fff005c738c84fa5c1bc
+ }
+
+ @Override
-+ public final VoxelShape moonrise$getConstantCollisionShape() {
++ public final VoxelShape moonrise$getConstantContextCollisionShape() {
+ return this.constantCollisionShape;
+ }
-+
-+ @Override
-+ public final AABB moonrise$getConstantCollisionAABB() {
-+ return this.constantAABBCollision;
-+ }
+ // Paper end - optimise collisions
+
protected BlockStateBase(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) {
super(block, propertyMap, codec);
this.fluidState = Fluids.EMPTY.defaultFluidState();
-@@ -921,6 +991,43 @@ public abstract class BlockBehaviour implements FeatureElement {
+@@ -921,6 +991,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;
-+ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque ? -1 : this.cache.lightBlock;
+ // Paper end - rewrite chunk system
+ // Paper start - optimise collisions
+ if (this.cache != null) {
+ final VoxelShape collisionShape = this.cache.collisionShape;
+ try {
+ this.constantCollisionShape = this.getCollisionShape(null, null, null);
-+ this.constantAABBCollision = this.constantCollisionShape == null ? null : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this.constantCollisionShape).moonrise$getSingleAABBRepresentation();
+ } catch (final Throwable throwable) {
+ this.constantCollisionShape = null;
-+ this.constantAABBCollision = 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);
-+ if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) {
-+ for (final Direction direction : DIRECTIONS_CACHED) {
-+ // initialise the directional face shape cache as well
-+ final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction);
-+ initCaches(shape);
-+ }
-+ }
-+ if (this.cache.occlusionShapes != null) {
-+ for (final VoxelShape shape : this.cache.occlusionShapes) {
-+ initCaches(shape);
-+ }
++ initCaches(collisionShape, true);
++ if (this.constantCollisionShape != null) {
++ initCaches(this.constantCollisionShape, true);
+ }
+ } else {
+ this.occludesFullBlock = false;
+ this.emptyCollisionShape = false;
++ this.emptyConstantCollisionShape = false;
+ this.constantCollisionShape = null;
-+ this.constantAABBCollision = 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/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java
+index 422b364764e0df16ca250b4939d7b226e69c0840..2df28ffc731bd77e0d7af3541cfd3741aa5af83b 100644
+--- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java
+index ea76aa490358e9e1d13350ba0ea246ec2c423894..98058505d36baf74008da08339afc196713b14a7 100644
+--- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java
+index 85a197232be9377c0313ec00e8f935551e2c60e0..30b2fce9e47ffcc3de1542b1d0f073f5640127a7 100644
+--- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java
+index 55a87592a99105dbf57b26fb6ccba695295fce24..986365acc9983331a7982ea2e1eac2b0efe1506d 100644
+--- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java
+index fcf04c5c58ff35d38c5bf0df562ae2f8dc98a0ee..0b116160924300a9d62ad5948bfaf276f0386e4d 100644
+--- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
index 37795b9e264c571efe9c718fa9996197dca4ed54..0601f454758cb1447cca2cbff4ef5fd7633fece5 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
@@ -28816,6 +30751,31 @@ index dcc0acd259920463a4464213b9a5e793603852f9..ef4161884574d3d137e12591d983dc95
@Override
public BlockState getBlockState(BlockPos pos) {
return Blocks.VOID_AIR.defaultBlockState();
+diff --git a/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java b/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java
+index 98dbeaf8bde15940e5b5d5d1f13fd4bb32f0a10d..7beea075b5a7ef738a4ac0558b99f4c5708f2c4a 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
index 7cce66d4c6efe6fd3cc22a6acf72878c964c61ae..30ee3df2278d0d9bd7478b49eda5fff27b8a504c 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
@@ -28879,7 +30839,7 @@ index 7cce66d4c6efe6fd3cc22a6acf72878c964c61ae..30ee3df2278d0d9bd7478b49eda5fff2
@Override
public BlockEntity getBlockEntity(BlockPos pos) {
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-index 7181acfafad91aa5f6ab7ce663d9be4a1b65b02a..a61294befc2f855fcecb2336a2d5444ce60e0a3a 100644
+index 7181acfafad91aa5f6ab7ce663d9be4a1b65b02a..a0e51681731dc7b487d5b14ae0d44a881bd5cb09 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -54,7 +54,7 @@ import net.minecraft.world.ticks.LevelChunkTicks;
@@ -28887,7 +30847,7 @@ index 7181acfafad91aa5f6ab7ce663d9be4a1b65b02a..a61294befc2f855fcecb2336a2d5444c
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.chunk_getblock.GetBlockChunk { // Paper - rewrite chunk system // Paper - get block chunk optimisation
++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() {
@@ -28994,7 +30954,7 @@ index 7181acfafad91aa5f6ab7ce663d9be4a1b65b02a..a61294befc2f855fcecb2336a2d5444c
*/
org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration));
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system
++ 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) {
try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper
@@ -29004,7 +30964,7 @@ index 7181acfafad91aa5f6ab7ce663d9be4a1b65b02a..a61294befc2f855fcecb2336a2d5444c
public void unloadCallback() {
+ if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper
org.bukkit.Server server = this.level.getCraftServer();
-+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesUnloadEvent(); // Paper - rewrite chunk system
++ 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
@@ -29054,7 +31014,7 @@ index 7181acfafad91aa5f6ab7ce663d9be4a1b65b02a..a61294befc2f855fcecb2336a2d5444c
@Nullable
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-index 52f44f14bbda60fe771c351e01e6ff470d7371e6..161211124f3f8390530af7ab21f3a0f1025209c5 100644
+index 52f44f14bbda60fe771c351e01e6ff470d7371e6..4167ed830382c6a76bb281e9d753919925c6bd00 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.Blocks;
@@ -29066,20 +31026,22 @@ index 52f44f14bbda60fe771c351e01e6ff470d7371e6..161211124f3f8390530af7ab21f3a0f1
public static final int SECTION_WIDTH = 16;
public static final int SECTION_HEIGHT = 16;
-@@ -25,6 +25,28 @@ public class LevelChunkSection {
+@@ -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.ints.IntArrayList FULL_LIST = new it.unimi.dsi.fastutil.ints.IntArrayList(16*16*16);
++ private static final it.unimi.dsi.fastutil.shorts.ShortArrayList FULL_LIST = new it.unimi.dsi.fastutil.shorts.ShortArrayList(16*16*16);
+ static {
-+ for (int i = 0; i < (16*16*16); ++i) {
++ for (short i = 0; i < (16*16*16); ++i) {
+ FULL_LIST.add(i);
+ }
+ }
+
-+ private int specialCollidingBlocks;
-+ private final ca.spottedleaf.moonrise.common.list.IBlockDataList tickingBlocks = new ca.spottedleaf.moonrise.common.list.IBlockDataList();
++ 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 int moonrise$getSpecialCollidingBlocks() {
@@ -29087,7 +31049,7 @@ index 52f44f14bbda60fe771c351e01e6ff470d7371e6..161211124f3f8390530af7ab21f3a0f1
+ }
+
+ @Override
-+ public final ca.spottedleaf.moonrise.common.list.IBlockDataList moonrise$getTickingBlockList() {
++ public final ca.spottedleaf.moonrise.common.list.ShortList moonrise$getTickingBlockList() {
+ return this.tickingBlocks;
+ }
+ // Paper end - block counting
@@ -29095,30 +31057,76 @@ index 52f44f14bbda60fe771c351e01e6ff470d7371e6..161211124f3f8390530af7ab21f3a0f1
private LevelChunkSection(LevelChunkSection section) {
this.nonEmptyBlockCount = section.nonEmptyBlockCount;
this.tickingBlockCount = section.tickingBlockCount;
-@@ -98,6 +120,22 @@ public class LevelChunkSection {
- ++this.tickingFluidCount;
- }
+@@ -64,6 +88,45 @@ public class LevelChunkSection {
+ return this.setBlockState(x, y, z, state, true);
+ }
-+ // Paper start - block counting
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(iblockdata1)) {
-+ --this.specialCollidingBlocks;
++ // 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 (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) {
-+ ++this.specialCollidingBlocks;
++
++ if (this.isClient) {
++ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(newState)) {
++ this.specialCollidingBlocks = CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS;
++ }
++ return;
+ }
+
-+ if (iblockdata1.isRandomlyTicking()) {
-+ this.tickingBlocks.remove(x, y, z);
++ 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;
++ }
+ }
-+ if (state.isRandomlyTicking()) {
-+ this.tickingBlocks.add(x, y, z, state);
++
++ 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
++ }
++ // Paper end - block counting
++
+ public BlockState setBlockState(int x, int y, int z, BlockState state, boolean lock) {
+ BlockState iblockdata1;
+
+@@ -83,7 +146,7 @@ public class LevelChunkSection {
+ }
+ }
+
+- if (!fluid.isEmpty()) {
++ if (!!fluid.isRandomlyTicking()) { // Paper - block counting
+ --this.tickingFluidCount;
+ }
+
+@@ -94,10 +157,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;
}
-@@ -118,40 +156,65 @@ public class LevelChunkSection {
+@@ -118,40 +183,70 @@ public class LevelChunkSection {
}
public void recalcBlockCounts() {
@@ -29137,47 +31145,52 @@ index 52f44f14bbda60fe771c351e01e6ff470d7371e6..161211124f3f8390530af7ab21f3a0f1
+ final int paletteSize = palette.getSize();
+ final net.minecraft.util.BitStorage storage = data.storage();
+
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> counts;
++ 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;
-+ for (final java.util.Iterator<it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.ints.IntArrayList>> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-+ final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.ints.IntArrayList> entry = iterator.next();
-+ final int paletteIdx = entry.getIntKey();
-+ final it.unimi.dsi.fastutil.ints.IntArrayList coordinates = entry.getValue();
-+ final int paletteCount = coordinates.size();
-
-- a(final LevelChunkSection chunksection) {}
+ final BlockState state = palette.valueFor(paletteIdx);
-- public void accept(BlockState iblockdata, int i) {
-- FluidState fluid = iblockdata.getFluidState();
+- 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;
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) {
-+ this.specialCollidingBlocks += paletteCount;
-+ }
-+ this.nonEmptyBlockCount += paletteCount;
-+ if (state.isRandomlyTicking()) {
-+ this.tickingBlockCount += paletteCount;
-+ final int[] raw = coordinates.elements();
-+
+ java.util.Objects.checkFromToIndex(0, paletteCount, raw.length);
+ for (int i = 0; i < paletteCount; ++i) {
-+ this.tickingBlocks.add(raw[i], state);
++ tickingBlocks.add(raw[i]);
}
}
@@ -29185,10 +31198,10 @@ index 52f44f14bbda60fe771c351e01e6ff470d7371e6..161211124f3f8390530af7ab21f3a0f1
+
if (!fluid.isEmpty()) {
- this.nonEmptyBlockCount += i;
-+ //this.nonEmptyBlockCount += count; // fix vanilla bug: make non empty block count correct
++ //this.nonEmptyBlockCount += count; // fix vanilla bug: make non-empty block count correct
if (fluid.isRandomlyTicking()) {
- this.tickingFluidCount += i;
-+ this.tickingFluidCount += paletteCount;
++ this.tickingFluidCount += (short)paletteCount;
}
}
-
@@ -29205,16 +31218,59 @@ index 52f44f14bbda60fe771c351e01e6ff470d7371e6..161211124f3f8390530af7ab21f3a0f1
}
public PalettedContainer<BlockState> getStates() {
-@@ -169,6 +232,7 @@ public class LevelChunkSection {
+@@ -169,6 +264,11 @@ public class LevelChunkSection {
datapaletteblock.read(buf);
this.biomes = datapaletteblock;
-+ this.recalcBlockCounts(); // Paper - block counting
++ // 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/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java b/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java
+index bc4d9452bbeb05a691fd285603e49491f41d3ad2..f8d9892970c9092f7cc84434d4fbf34354ce1195 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/Palette.java b/src/main/java/net/minecraft/world/level/chunk/Palette.java
+index b8922e4a13df535cdc5701e893a6e460b33ff90d..100807f8b8337f56f49cdb818ccc75be2f08ecd1 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/Palette.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-index 112d1259dd37743076ff6c67ffd711d084ba8698..b46c58c952e183bd74854c3eb70d64979af70f18 100644
+index 112d1259dd37743076ff6c67ffd711d084ba8698..533167eaa8bd39006fb1c7e193c81359973da9af 100644
--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
@@ -28,7 +28,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
@@ -29226,15 +31282,156 @@ index 112d1259dd37743076ff6c67ffd711d084ba8698..b46c58c952e183bd74854c3eb70d6497
private final PalettedContainer.Strategy strategy;
// private final ThreadingDetector threadingDetector = new ThreadingDetector("PalettedContainer"); // Paper - unused
-@@ -161,7 +161,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+@@ -71,6 +71,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
++
+ public PalettedContainer(
+ IdMap<T> idList,
+ PalettedContainer.Strategy paletteProvider,
+@@ -81,12 +108,14 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+ this.registry = idList;
+ this.strategy = paletteProvider;
+ this.data = new PalettedContainer.Data<>(dataProvider, storage, dataProvider.factory().create(dataProvider.bits(), idList, this, paletteEntries));
++ this.updateData(this.data); // Paper - optimise palette reads
+ }
+
+ private PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data<T> data) {
+ this.registry = idList;
+ this.strategy = paletteProvider;
+ this.data = data;
++ this.updateData(this.data); // Paper - optimise palette reads
+ }
+
+ private PalettedContainer(PalettedContainer<T> container) {
+@@ -100,6 +129,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) {
+@@ -115,6 +145,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+ PalettedContainer.Data<T> data2 = this.createOrReuseData(data, newBits);
+ data2.copyFrom(data.palette, data.storage);
+ this.data = data2;
++ this.updateData(this.data); // Paper - optimise palette reads
+ return data2.palette.idFor(object);
+ }
+
+@@ -136,9 +167,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) {
+@@ -161,9 +195,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
- PalettedContainer.Data<T> data = this.data;
- return data.palette.valueFor(data.storage.get(index));
++ // 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
+@@ -183,6 +219,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+ data.palette.read(buf);
+ buf.readLongArray(data.storage.getRaw());
+ this.data = data;
++ this.updateData(this.data); // Paper - optimise palette reads
+ } finally {
+ this.release();
+ }
+@@ -323,7 +360,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/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
index 5321109ca638036572df9a7e17eafcef2b4f5112..5304254587372465c8ce821d7aa38b39a979f46b 100644
--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
@@ -29248,6 +31445,60 @@ index 5321109ca638036572df9a7e17eafcef2b4f5112..5304254587372465c8ce821d7aa38b39
this.lightEngine.checkBlock(pos);
}
}
+diff --git a/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java b/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java
+index a45e6410600afc5464e5d29932c193786ce0a6fb..a1ba68c95c2cdebdc0d7782cce7895529918073c 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
index b1058bf0dcda544a074f4d3772d7899b94f98927..b7bf82f6b6023bd628d3e7ea84d2d6755a0d931a 100644
--- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkPyramid.java
@@ -29647,8 +31898,100 @@ index cb823d342e41b5861adfc847a313c265fb702a4c..2b1ea97199d5976e5ff4bd049c1e6c8b
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/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+index f1237f6fd6414900ffbad0caee31aa83310eeef4..8071ce70d66909bb4bda45792bf329a939d6f918 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
++++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
+@@ -25,7 +25,7 @@ import net.minecraft.util.profiling.jfr.JvmProfiler;
+ 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;
+@@ -49,6 +49,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.getSelected(), dsync);
+ }
+@@ -220,6 +235,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])) {
+@@ -443,10 +468,29 @@ public class RegionFile implements AutoCloseable {
+ }
+
+ public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Paper - don't write garbage data to disk if writing serialization fails
+- 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);
+@@ -480,7 +524,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/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-index 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e77fe5c37c 100644
+index 4c1212c6ef48594e766fa9e35a6e15916602d587..9dbc9e2f9d5aab71720bb81803efe76e2f361f04 100644
--- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -17,7 +17,7 @@ import net.minecraft.nbt.StreamTagVisitor;
@@ -29660,15 +32003,15 @@ index 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e7
public static final String ANVIL_EXTENSION = ".mca";
private static final int MAX_CACHE_SIZE = 256;
-@@ -26,33 +26,122 @@ public final class RegionFileStorage implements AutoCloseable {
+@@ -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 * 64;
-+ private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(MAX_NON_EXISTING_CACHE+1);
++ 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";
+ }
@@ -29743,6 +32086,97 @@ index 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e7
+
+ 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
@@ -29754,6 +32188,17 @@ index 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e7
- 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) {
@@ -29761,12 +32206,7 @@ index 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e7
+ }
+ synchronized (this) {
+ final long key = ChunkPos.asLong(chunkcoordintpair.x >> REGION_SHIFT, chunkcoordintpair.z >> REGION_SHIFT);
-
-- 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();
++
+ RegionFile ret = this.regionCache.getAndMoveToFirst(key);
+ if (ret != null) {
+ return ret;
@@ -29799,7 +32239,7 @@ index 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e7
}
@Nullable
-@@ -132,8 +221,14 @@ public final class RegionFileStorage implements AutoCloseable {
+@@ -132,8 +318,14 @@ public final class RegionFileStorage implements AutoCloseable {
}
@@ -29816,7 +32256,7 @@ index 4c1212c6ef48594e766fa9e35a6e15916602d587..18054304e08c8a6346c0135a0e6a68e7
// Paper start - Chunk save reattempt
int attempts = 0;
Exception lastException = null;
-@@ -182,30 +277,37 @@ public final class RegionFileStorage implements AutoCloseable {
+@@ -182,30 +374,37 @@ public final class RegionFileStorage implements AutoCloseable {
}
public void close() throws IOException {
@@ -30393,6 +32833,257 @@ index 8d90e783967280025d711c709facbcc87f611f8a..987e3397503cd07d3a2f172cede34129
}
public int getLightSectionCount() {
+diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
+index 261e5994d13f8bc30490b86691c80c0a21e7640a..f4fbcbb8ff6d2677af1a02a0801a323c06dce9b1 100644
+--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/level/material/FluidState.java b/src/main/java/net/minecraft/world/level/material/FluidState.java
+index 87adfe152abd1b8b4d547034576883c5d1cdf134..2d50d72bf026d0cf9c546a3c6fc1859379bfd805 100644
+--- a/src/main/java/net/minecraft/world/level/material/FluidState.java
++++ b/src/main/java/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/src/main/java/net/minecraft/world/phys/AABB.java b/src/main/java/net/minecraft/world/phys/AABB.java
index 5dc2674b537f4a61b2e21a21bdb2e8dc090d3a3c..6cf6d4ec7b9e43c7b2b4c0e2fb080964ff588130 100644
--- a/src/main/java/net/minecraft/world/phys/AABB.java
@@ -30602,7 +33293,7 @@ index d812949c7329ae2696b38dc792fa011ba87decb9..7743495c7ec3fc5e17947144457cef7b
@Override
diff --git a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-index 01693ba050b12b9debcdaefceeff9cbcd503b369..1d36f8dcffd22cf844448d3d8351fb8718cf5227 100644
+index 01693ba050b12b9debcdaefceeff9cbcd503b369..fbe0c4b0fdbb992b7002f6afe1e74d63cbb420f2 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
@@ -3,12 +3,79 @@ package net.minecraft.world.phys.shapes;
@@ -30664,7 +33355,7 @@ index 01693ba050b12b9debcdaefceeff9cbcd503b369..1d36f8dcffd22cf844448d3d8351fb87
+ }
+ }
+
-+ final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0);
++ 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);
@@ -30702,7 +33393,7 @@ index 7ec02a7849437a18860aa0df7d9ddd71b2447d4c..5e45e49ab09344cb95736f4124b1c6e0
public OffsetDoubleList(DoubleList oldList, double offset) {
this.delegate = oldList;
diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-index 5a0b0b47da3d796c391ac15eb573af25a1dfcd32..672a2038c6d8b31090403766460c6149a75adf8b 100644
+index 5a0b0b47da3d796c391ac15eb573af25a1dfcd32..513bed7f11aee667c87046db4cf912b80e8f3638 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
@@ -16,9 +16,15 @@ public final class Shapes {
@@ -30858,13 +33549,13 @@ index 5a0b0b47da3d796c391ac15eb573af25a1dfcd32..672a2038c6d8b31090403766460c6149
+ final VoxelShape first = tmp[i];
+ final VoxelShape second = tmp[next];
+
-+ tmp[newSize++] = Shapes.or(first, second);
++ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR);
+ }
+ }
+ size = newSize;
+ }
+
-+ return tmp[0];
++ return tmp[0].optimize();
+ // Paper end - optimise collisions
}
@@ -31108,10 +33799,10 @@ index b07f1c58e00d232e7c83e6df3499e4b677645609..b88c71f27996d24d29048e06a69a0046
private static DiscreteVoxelShape makeSlice(DiscreteVoxelShape voxelSet, Direction.Axis axis, int sliceWidth) {
diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f3d5d69e8 100644
+index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..3f8e7e29c3e52211a29e6f0a32890f6b53bfd9a8 100644
--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-@@ -15,38 +15,505 @@ import net.minecraft.world.phys.AABB;
+@@ -15,61 +15,546 @@ import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
@@ -31264,13 +33955,13 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
+
+ if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) {
+ if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) {
-+ ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1));
++ 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 = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0));
++ ret = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, 0);
+ } else {
+ ret = Shapes.empty();
+ }
@@ -31281,23 +33972,6 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
+ return ret;
+ }
+
-+ private static VoxelShape tryForceBlock(final VoxelShape other) {
-+ if (other == Shapes.block()) {
-+ return other;
-+ }
-+
-+ final AABB otherAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)other).moonrise$getSingleAABBRepresentation();
-+ if (otherAABB == null) {
-+ return other;
-+ }
-+
-+ if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)Shapes.block()).moonrise$getSingleAABBRepresentation().equals(otherAABB)) {
-+ return Shapes.block();
-+ }
-+
-+ return other;
-+ }
-+
+ private boolean computeOccludesFullBlock() {
+ if (this.isEmpty) {
+ this.occludesFullBlock = Boolean.FALSE;
@@ -31395,18 +34069,21 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
+ return result;
+ }
+
-+ private static DoubleList offsetList(final DoubleList src, final double by) {
-+ if (src instanceof OffsetDoubleList offsetDoubleList) {
-+ return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset);
++ 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(src, by);
++ return new OffsetDoubleList(wrap, by);
+ }
+
+ private List<AABB> toAabbsUncached() {
-+ final List<AABB> ret = new java.util.ArrayList<>();
++ 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;
@@ -31528,6 +34205,26 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
+ 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, (positiveDir ? (1.0 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) : (0.0 + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) - offset,
++ 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) {
@@ -31634,7 +34331,45 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
}
public VoxelShape singleEncompassing() {
-@@ -69,7 +536,7 @@ public abstract class VoxelShape {
+- 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() {
@@ -31643,7 +34378,7 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
}
public VoxelShape move(Vec3 vec3d) {
-@@ -77,24 +544,91 @@ public abstract class VoxelShape {
+@@ -77,24 +562,96 @@ public abstract class VoxelShape {
}
public VoxelShape move(double x, double y, double z) {
@@ -31662,9 +34397,9 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
+
+ final ArrayVoxelShape ret = new ArrayVoxelShape(
+ this.shape,
-+ offsetList(this.getCoords(Direction.Axis.X), x),
-+ offsetList(this.getCoords(Direction.Axis.Y), y),
-+ offsetList(this.getCoords(Direction.Axis.Z), z)
++ 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;
@@ -31696,6 +34431,11 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
+
+ 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);
@@ -31750,7 +34490,7 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
}
public void forAllEdges(Shapes.DoubleLineConsumer consumer) {
-@@ -131,9 +665,24 @@ public abstract class VoxelShape {
+@@ -131,9 +688,24 @@ public abstract class VoxelShape {
}
public List<AABB> toAabbs() {
@@ -31778,7 +34518,37 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
}
public double min(Direction.Axis axis, double from, double to) {
-@@ -159,42 +708,63 @@ public abstract class VoxelShape {
+@@ -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, value - this.offsetX, 0, values.length - 1
++ );
++ }
++ case Y: {
++ final double[] values = this.rootCoordinatesY;
++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor(
++ values, value - this.offsetY, 0, values.length - 1
++ );
++ }
++ case Z: {
++ final double[] values = this.rootCoordinatesZ;
++ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor(
++ values, value - this.offsetZ, 0, values.length - 1
++ );
++ }
++ default: {
++ throw new IllegalStateException("Unknown axis: " + axis);
++ }
++ }
++ // Paper end - optimise collisions
}
@Nullable
@@ -31817,13 +34587,13 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
+ final AABB singleAABB = this.singleAABBRepresentation;
+ if (singleAABB != null) {
+ if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
-+ return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true);
++ 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.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true);
++ 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);
@@ -31871,7 +34641,44 @@ index bcb79462c8b3309ae8701cba4753b27a9d22eb2e..d850a7de74150a04622da71d9614320f
}
public VoxelShape getFaceShape(Direction facing) {
-@@ -249,9 +819,30 @@ public abstract class VoxelShape {
+@@ -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);
}
diff --git a/patches/server/0828-fixup-Moonrise-optimisation-patches.patch b/patches/server/0828-fixup-Moonrise-optimisation-patches.patch
deleted file mode 100644
index e2197234c7..0000000000
--- a/patches/server/0828-fixup-Moonrise-optimisation-patches.patch
+++ /dev/null
@@ -1,13349 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <[email protected]>
-Date: Mon, 21 Oct 2024 11:06:24 -0700
-Subject: [PATCH] fixup! Moonrise optimisation patches
-
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-index b61611351bf23efc1e90bab8a850ebbe6ffdd516..fc029c8fb22a7c8eeb23bfc171812f6da91c60fa 100644
---- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
-@@ -6,11 +6,13 @@ 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;
-@@ -80,7 +82,13 @@ 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) {
-@@ -112,16 +120,18 @@ public final class ChunkSystem {
- ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
- );
- if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
-- chunk.postProcessGeneration();
-+ 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) {
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
-index aef4fc0d3c272febe675d1ac846b88e58b4e7533..93bc56daec4526f373c84763b8c7ccb4a30e800b 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
-@@ -1,10 +1,10 @@
- package ca.spottedleaf.moonrise.patches.block_counting;
-
- import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
--import it.unimi.dsi.fastutil.ints.IntArrayList;
-+import it.unimi.dsi.fastutil.shorts.ShortArrayList;
-
- public interface BlockCountingBitStorage {
-
-- public Int2ObjectOpenHashMap<IntArrayList> moonrise$countEntries();
-+ public Int2ObjectOpenHashMap<ShortArrayList> moonrise$countEntries();
-
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
-index a08ddb0598d44368af5b6bace971ee31edf9919e..0d1443a113c07d7655e7b927a899447f70db8fa9 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java
-@@ -1,11 +1,11 @@
- package ca.spottedleaf.moonrise.patches.block_counting;
-
--import ca.spottedleaf.moonrise.common.list.IBlockDataList;
-+import ca.spottedleaf.moonrise.common.list.ShortList;
-
- public interface BlockCountingChunkSection {
-
-- public int moonrise$getSpecialCollidingBlocks();
-+ public boolean moonrise$hasSpecialCollidingBlocks();
-
-- public IBlockDataList moonrise$getTickingBlockList();
-+ public ShortList moonrise$getTickingBlockList();
-
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..89e75b454695e174c5619104eeb15eb923a2d9a7
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..01da52b9e8a786824f199a057b62ce0431ecbc43
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..b5335a2a8cb5dc7637c7112c8f7193389d726489
---- /dev/null
-+++ b/src/main/java/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.ReferenceOpenHashSet;
-+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 ReferenceOpenHashSet<>(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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
-index 49160a30b8e19e5c5ada811fbcae2a05959524f3..44bb25554634af2ec0b2e9b3d9231304d5dff034 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemConverters.java
-@@ -1,10 +1,11 @@
- 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.DataFixTypes;
-+import net.minecraft.util.datafix.fixes.References;
-
- public final class ChunkSystemConverters {
-
-@@ -25,13 +26,13 @@ public final class ChunkSystemConverters {
- public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) {
- final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION);
-
-- return DataFixTypes.POI_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
-+ 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 DataFixTypes.ENTITY_CHUNK.update(world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
-+ return PlatformHooks.get().convertNBT(References.ENTITY_CHUNK, world.getServer().getFixerUpper(), data, dataVersion, getCurrentVersion());
- }
-
- private ChunkSystemConverters() {}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
-deleted file mode 100644
-index 67f6dd9a4855611cfe242c2e37e90f6d27d4c823..0000000000000000000000000000000000000000
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
-+++ /dev/null
-@@ -1,36 +0,0 @@
--package ca.spottedleaf.moonrise.patches.chunk_system;
--
--import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData;
--import net.minecraft.nbt.CompoundTag;
--import net.minecraft.server.level.ServerLevel;
--import net.minecraft.world.level.chunk.ChunkAccess;
--
--public final class ChunkSystemFeatures {
--
-- public static boolean supportsAsyncChunkSave() {
-- // uncertain how to properly pass AsyncSaveData to ChunkSerializer#write
-- // additionally, there may be mods hooking into the write() call which may not be thread-safe to call
-- return true;
-- }
--
-- public static AsyncChunkSaveData getAsyncSaveData(final ServerLevel world, final ChunkAccess chunk) {
-- return net.minecraft.world.level.chunk.storage.ChunkSerializer.getAsyncSaveData(world, chunk);
-- }
--
-- public static CompoundTag saveChunkAsync(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData) {
-- return net.minecraft.world.level.chunk.storage.ChunkSerializer.saveChunk(world, chunk, asyncSaveData);
-- }
--
-- public static boolean forceNoSave(final ChunkAccess chunk) {
-- // support for CB chunk mustNotSave
-- return chunk instanceof net.minecraft.world.level.chunk.LevelChunk levelChunk && levelChunk.mustNotSave;
-- }
--
-- public static boolean supportsAsyncChunkDeserialization() {
-- // as it stands, the current problem with supporting this in Moonrise is that we are unsure that any mods
-- // hooking into ChunkSerializer#read() are thread-safe to call
-- return true;
-- }
--
-- private ChunkSystemFeatures() {}
--}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
-deleted file mode 100644
-index becd1c6d54ed6c912aee3a9178a970e2751d3694..0000000000000000000000000000000000000000
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
-+++ /dev/null
-@@ -1,11 +0,0 @@
--package ca.spottedleaf.moonrise.patches.chunk_system.async_save;
--
--import net.minecraft.nbt.ListTag;
--import net.minecraft.nbt.Tag;
--
--public record AsyncChunkSaveData(
-- Tag blockTickList, // non-null if we had to go to the server's tick list
-- Tag fluidTickList, // non-null if we had to go to the server's tick list
-- ListTag blockEntities,
-- long worldTime
--) {}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
-index 2c279854bdf214538380fa354e4298ec4bd9ac4e..c7da23900228aab3a5673eb5adfada5091140319 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/entity/ChunkSystemEntity.java
-@@ -1,5 +1,6 @@
- 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;
-@@ -19,6 +20,10 @@ public interface ChunkSystemEntity {
-
- 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);
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
-index 73df26b27146bbad2106d57b22dd3c792ed3dd1d..a814512fcfb85312474ae2c2c21443843bf57831 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java
-@@ -1,5 +1,6 @@
- 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;
-
-@@ -11,4 +12,20 @@ public interface ChunkSystemRegionFileStorage {
-
- 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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..99f6f3e58b11b8967e6f1c3391c190d9a860ab7f
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
-@@ -0,0 +1,1707 @@
-+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) {
-+ // TODO implement this?
-+ /*if (thr instanceof RegionFileStorage.RegionFileSizeException) {
-+ final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024);
-+ LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk.");
-+ } else */
-+ {
-+ failedWrite = thr instanceof IOException;
-+ LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr);
-+ }
-+ }
-+
-+ 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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
-deleted file mode 100644
-index 3218cbf84f54daf06e84442d5eb1a36d8da6b215..0000000000000000000000000000000000000000
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
-+++ /dev/null
-@@ -1,1240 +0,0 @@
--package ca.spottedleaf.moonrise.patches.chunk_system.io;
--
--import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
--import ca.spottedleaf.concurrentutil.executor.Cancellable;
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedQueueExecutorThread;
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue;
--import ca.spottedleaf.concurrentutil.function.BiLong1Function;
--import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
--import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
--import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
--import ca.spottedleaf.moonrise.common.util.TickThread;
--import ca.spottedleaf.moonrise.common.util.WorldUtil;
--import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
--import net.minecraft.nbt.CompoundTag;
--import net.minecraft.server.level.ServerLevel;
--import net.minecraft.world.level.ChunkPos;
--import net.minecraft.world.level.chunk.storage.RegionFile;
--import net.minecraft.world.level.chunk.storage.RegionFileStorage;
--import org.slf4j.Logger;
--import org.slf4j.LoggerFactory;
--import java.io.IOException;
--import java.lang.invoke.VarHandle;
--import java.util.concurrent.CompletableFuture;
--import java.util.concurrent.CompletionException;
--import java.util.concurrent.atomic.AtomicInteger;
--import java.util.function.BiConsumer;
--import java.util.function.Consumer;
--import java.util.function.Function;
--
--/**
-- * Prioritised RegionFile I/O executor, responsible for all RegionFile access.
-- * <p>
-- * All functions provided are MT-Safe, however certain ordering constraints are recommended:
-- * <li>
-- * Chunk saves may not occur for unloaded chunks.
-- * </li>
-- * <li>
-- * Tasks must be scheduled on the chunk scheduler thread.
-- * </li>
-- * By following these constraints, no chunk data loss should occur with the exception of underlying I/O problems.
-- * </p>
-- */
--public final class RegionFileIOThread extends PrioritisedQueueExecutorThread {
--
-- private static final Logger LOGGER = LoggerFactory.getLogger(RegionFileIOThread.class);
--
-- /**
-- * The kinds of region files controlled by the region file thread. Add more when needed, and ensure
-- * getControllerFor is updated.
-- */
-- public static enum RegionFileType {
-- CHUNK_DATA,
-- POI_DATA,
-- ENTITY_DATA;
-- }
--
-- private static final RegionFileType[] CACHED_REGIONFILE_TYPES = RegionFileType.values();
--
-- public static ChunkDataController getControllerFor(final ServerLevel world, final RegionFileType type) {
-- switch (type) {
-- case CHUNK_DATA:
-- return ((ChunkSystemServerLevel)world).moonrise$getChunkDataController();
-- case POI_DATA:
-- return ((ChunkSystemServerLevel)world).moonrise$getPoiChunkDataController();
-- case ENTITY_DATA:
-- return ((ChunkSystemServerLevel)world).moonrise$getEntityChunkDataController();
-- default:
-- throw new IllegalStateException("Unknown controller type " + type);
-- }
-- }
--
-- /**
-- * Collects regionfile data for a certain chunk.
-- */
-- public static final class RegionFileData {
--
-- private final boolean[] hasResult = new boolean[CACHED_REGIONFILE_TYPES.length];
-- private final CompoundTag[] data = new CompoundTag[CACHED_REGIONFILE_TYPES.length];
-- private final Throwable[] throwables = new Throwable[CACHED_REGIONFILE_TYPES.length];
--
-- /**
-- * Sets the result associated with the specified regionfile type. Note that
-- * results can only be set once per regionfile type.
-- *
-- * @param type The regionfile type.
-- * @param data The result to set.
-- */
-- public void setData(final RegionFileType type, final CompoundTag data) {
-- final int index = type.ordinal();
--
-- if (this.hasResult[index]) {
-- throw new IllegalArgumentException("Result already exists for type " + type);
-- }
-- this.hasResult[index] = true;
-- this.data[index] = data;
-- }
--
-- /**
-- * Sets the result associated with the specified regionfile type. Note that
-- * results can only be set once per regionfile type.
-- *
-- * @param type The regionfile type.
-- * @param throwable The result to set.
-- */
-- public void setThrowable(final RegionFileType type, final Throwable throwable) {
-- final int index = type.ordinal();
--
-- if (this.hasResult[index]) {
-- throw new IllegalArgumentException("Result already exists for type " + type);
-- }
-- this.hasResult[index] = true;
-- this.throwables[index] = throwable;
-- }
--
-- /**
-- * Returns whether there is a result for the specified regionfile type.
-- *
-- * @param type Specified regionfile type.
-- *
-- * @return Whether a result exists for {@code type}.
-- */
-- public boolean hasResult(final RegionFileType type) {
-- return this.hasResult[type.ordinal()];
-- }
--
-- /**
-- * Returns the data result for the regionfile type.
-- *
-- * @param type Specified regionfile type.
-- *
-- * @throws IllegalArgumentException If the result has not been set for {@code type}.
-- * @return The data result for the specified type. If the result is a {@code Throwable},
-- * then returns {@code null}.
-- */
-- public CompoundTag getData(final RegionFileType type) {
-- final int index = type.ordinal();
--
-- if (!this.hasResult[index]) {
-- throw new IllegalArgumentException("Result does not exist for type " + type);
-- }
--
-- return this.data[index];
-- }
--
-- /**
-- * Returns the throwable result for the regionfile type.
-- *
-- * @param type Specified regionfile type.
-- *
-- * @throws IllegalArgumentException If the result has not been set for {@code type}.
-- * @return The throwable result for the specified type. If the result is an {@code CompoundTag},
-- * then returns {@code null}.
-- */
-- public Throwable getThrowable(final RegionFileType type) {
-- final int index = type.ordinal();
--
-- if (!this.hasResult[index]) {
-- throw new IllegalArgumentException("Result does not exist for type " + type);
-- }
--
-- return this.throwables[index];
-- }
-- }
--
-- private static final Object INIT_LOCK = new Object();
--
-- static RegionFileIOThread[] threads;
--
-- /* needs to be consistent given a set of parameters */
-- static RegionFileIOThread selectThread(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-- if (threads == null) {
-- throw new IllegalStateException("Threads not initialised");
-- }
--
-- final int regionX = chunkX >> 5;
-- final int regionZ = chunkZ >> 5;
-- final int typeOffset = type.ordinal();
--
-- return threads[(System.identityHashCode(world) + regionX + regionZ + typeOffset) % threads.length];
-- }
--
-- /**
-- * Shuts down the I/O executor(s). Watis for all tasks to complete if specified.
-- * Tasks queued during this call might not be accepted, and tasks queued after will not be accepted.
-- *
-- * @param wait Whether to wait until all tasks have completed.
-- */
-- public static void close(final boolean wait) {
-- for (int i = 0, len = threads.length; i < len; ++i) {
-- threads[i].close(false, true);
-- }
-- if (wait) {
-- RegionFileIOThread.flush();
-- }
-- }
--
-- public static long[] getExecutedTasks() {
-- final long[] ret = new long[threads.length];
-- for (int i = 0, len = threads.length; i < len; ++i) {
-- ret[i] = threads[i].getTotalTasksExecuted();
-- }
--
-- return ret;
-- }
--
-- public static long[] getTasksScheduled() {
-- final long[] ret = new long[threads.length];
-- for (int i = 0, len = threads.length; i < len; ++i) {
-- ret[i] = threads[i].getTotalTasksScheduled();
-- }
-- return ret;
-- }
--
-- public static void flush() {
-- for (int i = 0, len = threads.length; i < len; ++i) {
-- threads[i].waitUntilAllExecuted();
-- }
-- }
--
-- public static void flushRegionStorages(final ServerLevel world) throws IOException {
-- for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-- getControllerFor(world, type).getCache().flush();
-- }
-- }
--
-- public static void partialFlush(final int totalTasksRemaining) {
-- long failures = 1L; // start out at 0.25ms
--
-- for (;;) {
-- final long[] executed = getExecutedTasks();
-- final long[] scheduled = getTasksScheduled();
--
-- long sum = 0;
-- for (int i = 0; i < executed.length; ++i) {
-- sum += scheduled[i] - executed[i];
-- }
--
-- if (sum <= totalTasksRemaining) {
-- break;
-- }
--
-- failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms
-- }
-- }
--
-- /**
-- * Inits the executor with the specified number of threads.
-- *
-- * @param threads Specified number of threads.
-- */
-- public static void init(final int threads) {
-- synchronized (INIT_LOCK) {
-- if (RegionFileIOThread.threads != null) {
-- throw new IllegalStateException("Already initialised threads");
-- }
--
-- RegionFileIOThread.threads = new RegionFileIOThread[threads];
--
-- for (int i = 0; i < threads; ++i) {
-- RegionFileIOThread.threads[i] = new RegionFileIOThread(i);
-- RegionFileIOThread.threads[i].start();
-- }
-- }
-- }
--
-- public static void deinit() {
-- if (true) { // Paper
-- // TODO does this cause issues with mods? how to implement
-- close(true);
-- synchronized (INIT_LOCK) {
-- RegionFileIOThread.threads = null;
-- }
-- } else { RegionFileIOThread.flush(); }
-- }
--
-- private RegionFileIOThread(final int threadNumber) {
-- super(new PrioritisedThreadedTaskQueue(), (int)(1.0e6)); // 1.0ms spinwait time
-- this.setName("RegionFile I/O Thread #" + threadNumber);
-- this.setPriority(Thread.NORM_PRIORITY - 2); // we keep priority close to normal because threads can wait on us
-- this.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> {
-- LOGGER.error("Uncaught exception thrown from I/O thread, report this! Thread: " + thread.getName(), thr);
-- });
-- }
--
-- /**
-- * Returns whether the current thread is a regionfile I/O executor.
-- * @return Whether the current thread is a regionfile I/O executor.
-- */
-- public static boolean isRegionFileThread() {
-- return Thread.currentThread() instanceof RegionFileIOThread;
-- }
--
-- /**
-- * Returns the priority associated with blocking I/O based on the current thread. The goal is to avoid
-- * dumb plugins from taking away priority from threads we consider crucial.
-- * @return The priroity to use with blocking I/O on the current thread.
-- */
-- public static Priority getIOBlockingPriorityForCurrentThread() {
-- if (TickThread.isTickThread()) {
-- return Priority.BLOCKING;
-- }
-- return Priority.HIGHEST;
-- }
--
-- /**
-- * Returns the current {@code CompoundTag} pending for write for the specified chunk & regionfile type.
-- * Note that this does not copy the result, so do not modify the result returned.
-- *
-- * @param world Specified world.
-- * @param chunkX Specified chunk x.
-- * @param chunkZ Specified chunk z.
-- * @param type Specified regionfile type.
-- *
-- * @return The compound tag associated for the specified chunk. {@code null} if no write was pending, or if {@code null} is the write pending.
-- */
-- public static CompoundTag getPendingWrite(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-- return thread.getPendingWriteInternal(world, chunkX, chunkZ, type);
-- }
--
-- CompoundTag getPendingWriteInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-- final ChunkDataController taskController = getControllerFor(world, type);
-- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
-- if (task == null) {
-- return null;
-- }
--
-- final CompoundTag ret = task.inProgressWrite;
--
-- return ret == ChunkDataTask.NOTHING_TO_WRITE ? null : ret;
-- }
--
-- /**
-- * Returns the priority for the specified regionfile type for the specified chunk.
-- * @param world Specified world.
-- * @param chunkX Specified chunk x.
-- * @param chunkZ Specified chunk z.
-- * @param type Specified regionfile type.
-- * @return The priority for the chunk
-- */
-- public static Priority getPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-- return thread.getPriorityInternal(world, chunkX, chunkZ, type);
-- }
--
-- Priority getPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type) {
-- final ChunkDataController taskController = getControllerFor(world, type);
-- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
-- if (task == null) {
-- return Priority.COMPLETING;
-- }
--
-- return task.prioritisedTask.getPriority();
-- }
--
-- /**
-- * Sets the priority for all regionfile types for the specified chunk. Note that great care should
-- * be taken using this method, as there can be multiple tasks tied to the same chunk that want different
-- * priorities.
-- *
-- * @param world Specified world.
-- * @param chunkX Specified chunk x.
-- * @param chunkZ Specified chunk z.
-- * @param priority New priority.
-- *
-- * @see #raisePriority(ServerLevel, int, int, Priority)
-- * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
-- * @see #lowerPriority(ServerLevel, int, int, Priority)
-- * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
-- */
-- public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ,
-- final Priority priority) {
-- for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-- RegionFileIOThread.setPriority(world, chunkX, chunkZ, type, priority);
-- }
-- }
--
-- /**
-- * Sets the priority for the specified regionfile type for the specified chunk. Note that great care should
-- * be taken using this method, as there can be multiple tasks tied to the same chunk that want different
-- * priorities.
-- *
-- * @param world Specified world.
-- * @param chunkX Specified chunk x.
-- * @param chunkZ Specified chunk z.
-- * @param type Specified regionfile type.
-- * @param priority New priority.
-- *
-- * @see #raisePriority(ServerLevel, int, int, Priority)
-- * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
-- * @see #lowerPriority(ServerLevel, int, int, Priority)
-- * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
-- */
-- public static void setPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-- final Priority priority) {
-- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-- thread.setPriorityInternal(world, chunkX, chunkZ, type, priority);
-- }
--
-- void setPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-- final Priority priority) {
-- final ChunkDataController taskController = getControllerFor(world, type);
-- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
-- if (task != null) {
-- task.prioritisedTask.setPriority(priority);
-- }
-- }
--
-- /**
-- * Raises the priority for all regionfile types for the specified chunk.
-- *
-- * @param world Specified world.
-- * @param chunkX Specified chunk x.
-- * @param chunkZ Specified chunk z.
-- * @param priority New priority.
-- *
-- * @see #setPriority(ServerLevel, int, int, Priority)
-- * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
-- * @see #lowerPriority(ServerLevel, int, int, Priority)
-- * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
-- */
-- public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ,
-- final Priority priority) {
-- for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-- RegionFileIOThread.raisePriority(world, chunkX, chunkZ, type, priority);
-- }
-- }
--
-- /**
-- * Raises the priority for the specified regionfile type for the specified chunk.
-- *
-- * @param world Specified world.
-- * @param chunkX Specified chunk x.
-- * @param chunkZ Specified chunk z.
-- * @param type Specified regionfile type.
-- * @param priority New priority.
-- *
-- * @see #setPriority(ServerLevel, int, int, Priority)
-- * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
-- * @see #lowerPriority(ServerLevel, int, int, Priority)
-- * @see #lowerPriority(ServerLevel, int, int, RegionFileType, Priority)
-- */
-- public static void raisePriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-- final Priority priority) {
-- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-- thread.raisePriorityInternal(world, chunkX, chunkZ, type, priority);
-- }
--
-- void raisePriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-- final Priority priority) {
-- final ChunkDataController taskController = getControllerFor(world, type);
-- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
-- if (task != null) {
-- task.prioritisedTask.raisePriority(priority);
-- }
-- }
--
-- /**
-- * Lowers the priority for all regionfile types for the specified chunk.
-- *
-- * @param world Specified world.
-- * @param chunkX Specified chunk x.
-- * @param chunkZ Specified chunk z.
-- * @param priority New priority.
-- *
-- * @see #raisePriority(ServerLevel, int, int, Priority)
-- * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
-- * @see #setPriority(ServerLevel, int, int, Priority)
-- * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
-- */
-- public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ,
-- final Priority priority) {
-- for (final RegionFileType type : CACHED_REGIONFILE_TYPES) {
-- RegionFileIOThread.lowerPriority(world, chunkX, chunkZ, type, priority);
-- }
-- }
--
-- /**
-- * Lowers the priority for the specified regionfile type for the specified chunk.
-- *
-- * @param world Specified world.
-- * @param chunkX Specified chunk x.
-- * @param chunkZ Specified chunk z.
-- * @param type Specified regionfile type.
-- * @param priority New priority.
-- *
-- * @see #raisePriority(ServerLevel, int, int, Priority)
-- * @see #raisePriority(ServerLevel, int, int, RegionFileType, Priority)
-- * @see #setPriority(ServerLevel, int, int, Priority)
-- * @see #setPriority(ServerLevel, int, int, RegionFileType, Priority)
-- */
-- public static void lowerPriority(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-- final Priority priority) {
-- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-- thread.lowerPriorityInternal(world, chunkX, chunkZ, type, priority);
-- }
--
-- void lowerPriorityInternal(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-- final Priority priority) {
-- final ChunkDataController taskController = getControllerFor(world, type);
-- final ChunkDataTask task = taskController.tasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
--
-- if (task != null) {
-- task.prioritisedTask.lowerPriority(priority);
-- }
-- }
--
-- /**
-- * Schedules the chunk data to be written asynchronously.
-- * <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) {
-- RegionFileIOThread.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) {
-- final RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-- thread.scheduleSaveInternal(world, chunkX, chunkZ, data, type, priority);
-- }
--
-- void scheduleSaveInternal(final ServerLevel world, final int chunkX, final int chunkZ, final CompoundTag data,
-- final RegionFileType type, final Priority priority) {
-- final ChunkDataController taskController = getControllerFor(world, type);
--
-- final boolean[] created = new boolean[1];
-- final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-- final ChunkDataTask task = taskController.tasks.compute(key, (final long keyInMap, final ChunkDataTask taskRunning) -> {
-- if (taskRunning == null || taskRunning.failedWrite) {
-- // no task is scheduled or the previous write failed - meaning we need to overwrite it
--
-- // create task
-- final ChunkDataTask newTask = new ChunkDataTask(world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority);
-- newTask.inProgressWrite = data;
-- created[0] = true;
--
-- return newTask;
-- }
--
-- taskRunning.inProgressWrite = data;
--
-- return taskRunning;
-- });
--
-- if (created[0]) {
-- task.prioritisedTask.queue();
-- } else {
-- task.prioritisedTask.raisePriority(priority);
-- }
-- }
--
-- /**
-- * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call
-- * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
-- * for single load.
-- * <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 RegionFileIOThread.loadAllChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL);
-- }
--
-- /**
-- * Schedules a load to be executed asynchronously. This task will load all regionfile types, and then call
-- * {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
-- * for single load.
-- * <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 RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, priority, CACHED_REGIONFILE_TYPES);
-- }
--
-- /**
-- * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and
-- * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean)}
-- * for single load.
-- * <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 RegionFileIOThread.loadChunkData(world, chunkX, chunkZ, onComplete, intendingToBlock, Priority.NORMAL, types);
-- }
--
-- /**
-- * Schedules a load to be executed asynchronously. This task will load data for the specified regionfile type(s), and
-- * then call {@code onComplete}. This is a bulk load operation, see {@link #loadDataAsync(ServerLevel, int, int, RegionFileType, BiConsumer, boolean, Priority)}
-- * for single load.
-- * <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] = RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type,
-- (final CompoundTag data, final Throwable throwable) -> {
-- if (throwable != null) {
-- ret.setThrowable(type, throwable);
-- } else {
-- ret.setData(type, data);
-- }
--
-- if (completions.incrementAndGet() == expectedCompletions) {
-- onComplete.accept(ret);
-- }
-- }, intendingToBlock, priority);
-- }
--
-- return new CancellableReads(reads);
-- }
--
-- /**
-- * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
-- * {@code onComplete}.
-- * <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 RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, onComplete, intendingToBlock, Priority.NORMAL);
-- }
--
-- /**
-- * Schedules a load to be executed asynchronously. This task will load the specified regionfile type, and then call
-- * {@code onComplete}.
-- * <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 RegionFileIOThread thread = RegionFileIOThread.selectThread(world, chunkX, chunkZ, type);
-- return thread.loadDataAsyncInternal(world, chunkX, chunkZ, type, onComplete, intendingToBlock, priority);
-- }
--
-- Cancellable loadDataAsyncInternal(final ServerLevel world, final int chunkX, final int chunkZ,
-- final RegionFileType type, final BiConsumer<CompoundTag, Throwable> onComplete,
-- final boolean intendingToBlock, final Priority priority) {
-- final ChunkDataController taskController = getControllerFor(world, type);
--
-- final ImmediateCallbackCompletion callbackInfo = new ImmediateCallbackCompletion();
--
-- final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-- final BiLong1Function<ChunkDataTask, ChunkDataTask> compute = (final long keyInMap, final ChunkDataTask running) -> {
-- if (running == null) {
-- // not scheduled
--
-- // set up task
-- final ChunkDataTask newTask = new ChunkDataTask(
-- world, chunkX, chunkZ, taskController, RegionFileIOThread.this, priority
-- );
-- newTask.inProgressRead = new InProgressRead();
-- newTask.inProgressRead.addToAsyncWaiters(onComplete);
--
-- callbackInfo.tasksNeedsScheduling = true;
-- return newTask;
-- }
--
-- final CompoundTag pendingWrite = running.inProgressWrite;
--
-- if (pendingWrite == ChunkDataTask.NOTHING_TO_WRITE) {
-- // need to add to waiters here, because the regionfile thread will use compute() to lock and check for cancellations
-- if (!running.inProgressRead.addToAsyncWaiters(onComplete)) {
-- callbackInfo.data = running.inProgressRead.value;
-- callbackInfo.throwable = running.inProgressRead.throwable;
-- callbackInfo.completeNow = true;
-- }
-- return running;
-- }
--
-- // at this stage we have to use the in progress write's data to avoid an order issue
-- callbackInfo.data = pendingWrite;
-- callbackInfo.throwable = null;
-- callbackInfo.completeNow = true;
-- return running;
-- };
--
-- final ChunkDataTask ret = taskController.tasks.compute(key, compute);
--
-- // needs to be scheduled
-- if (callbackInfo.tasksNeedsScheduling) {
-- ret.prioritisedTask.queue();
-- } else if (callbackInfo.completeNow) {
-- try {
-- onComplete.accept(callbackInfo.data == null ? null : callbackInfo.data.copy(), callbackInfo.throwable);
-- } catch (final Throwable thr) {
-- LOGGER.error("Callback " + ConcurrentUtil.genericToString(onComplete) + " synchronously failed to handle chunk data for task " + ret.toString(), thr);
-- }
-- } else {
-- // we're waiting on a task we didn't schedule, so raise its priority to what we want
-- ret.prioritisedTask.raisePriority(priority);
-- }
--
-- return new CancellableRead(onComplete, ret);
-- }
--
-- /**
-- * Schedules a load task to be executed asynchronously, and blocks on that task.
-- *
-- * @param world Chunk's world
-- * @param chunkX Chunk's x coordinate
-- * @param chunkZ Chunk's z coordinate
-- * @param type Regionfile type
-- * @param priority Minimum priority to load the data at.
-- *
-- * @return The chunk data for the chunk. Note that a {@code null} result means the chunk or regionfile does not exist on disk.
-- *
-- * @throws IOException If the load fails for any reason
-- */
-- public static CompoundTag loadData(final ServerLevel world, final int chunkX, final int chunkZ, final RegionFileType type,
-- final Priority priority) throws IOException {
-- final CompletableFuture<CompoundTag> ret = new CompletableFuture<>();
--
-- RegionFileIOThread.loadDataAsync(world, chunkX, chunkZ, type, (final CompoundTag compound, final Throwable thr) -> {
-- if (thr != null) {
-- ret.completeExceptionally(thr);
-- } else {
-- ret.complete(compound);
-- }
-- }, true, priority);
--
-- try {
-- return ret.join();
-- } catch (final CompletionException ex) {
-- throw new IOException(ex);
-- }
-- }
--
-- private static final class ImmediateCallbackCompletion {
--
-- public CompoundTag data;
-- public Throwable throwable;
-- public boolean completeNow;
-- public boolean tasksNeedsScheduling;
--
-- }
--
-- private static final class CancellableRead implements Cancellable {
--
-- private BiConsumer<CompoundTag, Throwable> callback;
-- private ChunkDataTask task;
--
-- CancellableRead(final BiConsumer<CompoundTag, Throwable> callback, final ChunkDataTask task) {
-- this.callback = callback;
-- this.task = task;
-- }
--
-- @Override
-- public boolean cancel() {
-- final BiConsumer<CompoundTag, Throwable> callback = this.callback;
-- final ChunkDataTask task = this.task;
--
-- if (callback == null || task == null) {
-- return false;
-- }
--
-- this.callback = null;
-- this.task = null;
--
-- final InProgressRead read = task.inProgressRead;
--
-- // read can be null if no read was scheduled (i.e no regionfile existed or chunk in regionfile didn't)
-- return read != null && read.cancel(callback);
-- }
-- }
--
-- private static final class CancellableReads implements Cancellable {
--
-- private Cancellable[] reads;
--
-- private static final VarHandle READS_HANDLE = ConcurrentUtil.getVarHandle(CancellableReads.class, "reads", Cancellable[].class);
--
-- CancellableReads(final Cancellable[] reads) {
-- this.reads = reads;
-- }
--
-- @Override
-- public boolean cancel() {
-- final Cancellable[] reads = (Cancellable[])READS_HANDLE.getAndSet((CancellableReads)this, (Cancellable[])null);
--
-- if (reads == null) {
-- return false;
-- }
--
-- boolean ret = false;
--
-- for (final Cancellable read : reads) {
-- ret |= read.cancel();
-- }
--
-- return ret;
-- }
-- }
--
-- private static final class InProgressRead {
--
-- private static final Logger LOGGER = LoggerFactory.getLogger(InProgressRead.class);
--
-- private CompoundTag value;
-- private Throwable throwable;
-- private final MultiThreadedQueue<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 ChunkDataTask 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 for task " + task.toString(), thr);
-- }
-- }
-- }
-- }
--
-- public static abstract class ChunkDataController {
--
-- // ConcurrentHashMap synchronizes per chain, so reduce the chance of task's hashes colliding.
-- private final ConcurrentLong2ReferenceChainedHashTable<ChunkDataTask> tasks = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(8192, 0.5f);
--
-- public final RegionFileType type;
--
-- public ChunkDataController(final RegionFileType type) {
-- this.type = type;
-- }
--
-- public abstract RegionFileStorage getCache();
--
-- public abstract void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException;
--
-- public abstract CompoundTag readData(final int chunkX, final int chunkZ) throws IOException;
--
-- public boolean hasTasks() {
-- return !this.tasks.isEmpty();
-- }
--
-- public boolean doesRegionFileNotExist(final int chunkX, final int chunkZ) {
-- return ((ChunkSystemRegionFileStorage)(Object)this.getCache()).moonrise$doesRegionFileNotExistNoIO(chunkX, chunkZ);
-- }
--
-- public <T> T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function<RegionFile, T> function) {
-- final RegionFileStorage cache = this.getCache();
-- final RegionFile regionFile;
-- synchronized (cache) {
-- try {
-- if (existingOnly) {
-- regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfExists(chunkX, chunkZ);
-- } else {
-- regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly);
-- }
-- } catch (final IOException ex) {
-- throw new RuntimeException(ex);
-- }
--
-- return function.apply(regionFile);
-- }
-- }
--
-- public <T> T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function<RegionFile, T> function) {
-- final RegionFileStorage cache = this.getCache();
-- final RegionFile regionFile;
--
-- synchronized (cache) {
-- regionFile = ((ChunkSystemRegionFileStorage)(Object)cache).moonrise$getRegionFileIfLoaded(chunkX, chunkZ);
--
-- return function.apply(regionFile);
-- }
-- }
-- }
--
-- private static final class ChunkDataTask implements Runnable {
--
-- private static final CompoundTag NOTHING_TO_WRITE = new CompoundTag();
--
-- private static final Logger LOGGER = LoggerFactory.getLogger(ChunkDataTask.class);
--
-- private InProgressRead inProgressRead;
-- private volatile CompoundTag inProgressWrite = NOTHING_TO_WRITE; // only needs to be acquire/release
--
-- private boolean failedWrite;
--
-- private final ServerLevel world;
-- private final int chunkX;
-- private final int chunkZ;
-- private final ChunkDataController taskController;
--
-- private final PrioritisedTask prioritisedTask;
--
-- /*
-- * IO thread will perform reads before writes for a given chunk x and z
-- *
-- * How reads/writes are scheduled:
-- *
-- * If read is scheduled while scheduling write, take no special action and just schedule write
-- * If read is scheduled while scheduling read and no write is scheduled, chain the read task
-- *
-- *
-- * If write is scheduled while scheduling read, use the pending write data and ret immediately (so no read is scheduled)
-- * If write is scheduled while scheduling write (ignore read in progress), overwrite the write in progress data
-- *
-- * This allows the reads and writes to act as if they occur synchronously to the thread scheduling them, however
-- * it fails to properly propagate write failures thanks to writes overwriting each other
-- */
--
-- public ChunkDataTask(final ServerLevel world, final int chunkX, final int chunkZ, final ChunkDataController taskController,
-- final PrioritisedExecutor executor, final Priority priority) {
-- this.world = world;
-- this.chunkX = chunkX;
-- this.chunkZ = chunkZ;
-- this.taskController = taskController;
-- this.prioritisedTask = executor.createTask(this, priority);
-- }
--
-- @Override
-- public String toString() {
-- return "Task for world: '" + WorldUtil.getWorldName(this.world) + "' at (" + this.chunkX + "," + this.chunkZ +
-- ") type: " + this.taskController.type.name() + ", hash: " + this.hashCode();
-- }
--
-- @Override
-- public void run() {
-- final InProgressRead read = this.inProgressRead;
-- final long chunkKey = CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ);
--
-- if (read != null) {
-- final boolean[] canRead = new boolean[] { true };
--
-- if (read.hasNoWaiters()) {
-- // cancelled read? go to task controller to confirm
-- final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
-- if (valueInMap == null) {
-- throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
-- }
-- if (valueInMap != ChunkDataTask.this) {
-- throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-- }
--
-- if (!read.hasNoWaiters()) {
-- return valueInMap;
-- } else {
-- canRead[0] = false;
-- }
--
-- return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap;
-- });
--
-- if (inMap == null) {
-- // read is cancelled - and no write pending, so we're done
-- return;
-- }
-- // if there is a write in progress, we don't actually have to worry about waiters gaining new entries -
-- // the readers will just use the in progress write, so the value in canRead is good to use without
-- // further synchronisation.
-- }
--
-- if (canRead[0]) {
-- CompoundTag compound = null;
-- Throwable throwable = null;
--
-- try {
-- compound = this.taskController.readData(this.chunkX, this.chunkZ);
-- } catch (final Throwable thr) {
-- throwable = thr;
-- LOGGER.error("Failed to read chunk data for task: " + this.toString(), thr);
-- }
-- read.complete(this, compound, throwable);
-- }
-- }
--
-- CompoundTag write = this.inProgressWrite;
--
-- if (write == NOTHING_TO_WRITE) {
-- final ChunkDataTask inMap = this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
-- if (valueInMap == null) {
-- throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
-- }
-- if (valueInMap != ChunkDataTask.this) {
-- throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-- }
-- return valueInMap.inProgressWrite == NOTHING_TO_WRITE ? null : valueInMap;
-- });
--
-- if (inMap == null) {
-- return; // set the task value to null, indicating we're done
-- } // else: inProgressWrite changed, so now we have something to write
-- }
--
-- for (;;) {
-- write = this.inProgressWrite;
-- final CompoundTag dataWritten = write;
--
-- boolean failedWrite = false;
--
-- try {
-- this.taskController.writeData(this.chunkX, this.chunkZ, write);
-- } catch (final Throwable thr) {
-- if (thr instanceof RegionFileStorage.RegionFileSizeException) {
-- final int maxSize = RegionFile.MAX_CHUNK_SIZE / (1024 * 1024);
-- LOGGER.error("Chunk at (" + this.chunkX + "," + this.chunkZ + ") in '" + WorldUtil.getWorldName(this.world) + "' exceeds max size of " + maxSize + "MiB, it has been deleted from disk.");
-- } else {
-- failedWrite = thr instanceof IOException;
-- LOGGER.error("Failed to write chunk data for task: " + this.toString(), thr);
-- }
-- }
--
-- final boolean finalFailWrite = failedWrite;
-- final boolean[] done = new boolean[] { false };
--
-- this.taskController.tasks.compute(chunkKey, (final long keyInMap, final ChunkDataTask valueInMap) -> {
-- if (valueInMap == null) {
-- throw new IllegalStateException("Write completed concurrently, expected this task: " + ChunkDataTask.this.toString() + ", report this!");
-- }
-- if (valueInMap != ChunkDataTask.this) {
-- throw new IllegalStateException("Chunk task mismatch, expected this task: " + ChunkDataTask.this.toString() + ", got: " + valueInMap.toString() + ", report this!");
-- }
-- if (valueInMap.inProgressWrite == dataWritten) {
-- valueInMap.failedWrite = finalFailWrite;
-- done[0] = true;
-- // keep the data in map if we failed the write so we can try to prevent data loss
-- return finalFailWrite ? valueInMap : null;
-- }
-- // different data than expected, means we need to retry write
-- return valueInMap;
-- });
--
-- if (done[0]) {
-- return;
-- }
--
-- // fetch & write new data
-- continue;
-- }
-- }
-- }
--}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
-index c35e0c29700be48dda3e53e7d2db224766ef17b7..a36ab89f5c37f5f9ab0152f087bb4cf3560f8581 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/ChunkDataController.java
-@@ -1,22 +1,24 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
-
--import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
-+import ca.spottedleaf.moonrise.patches.chunk_system.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.Optional;
- import java.util.concurrent.CompletableFuture;
- import java.util.concurrent.CompletionException;
-
--public final class ChunkDataController extends RegionFileIOThread.ChunkDataController {
-+public final class ChunkDataController extends MoonriseRegionFileIO.RegionDataController {
-
- private final ServerLevel world;
-
-- public ChunkDataController(final ServerLevel world) {
-- super(RegionFileIOThread.RegionFileType.CHUNK_DATA);
-+ public ChunkDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
-+ super(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor);
- this.world = world;
- }
-
-@@ -26,31 +28,23 @@ public final class ChunkDataController extends RegionFileIOThread.ChunkDataContr
- }
-
- @Override
-- public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
-- final CompletableFuture<Void> future = this.world.getChunkSource().chunkMap.write(new ChunkPos(chunkX, chunkZ), compound);
--
-- try {
-- if (future != null) {
-- // rets non-null when sync writing (i.e. future should be completed here)
-- future.join();
-- }
-- } catch (final CompletionException ex) {
-- if (ex.getCause() instanceof IOException ioException) {
-- throw ioException;
-- }
-- throw ex;
-- }
-+ 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 CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
-- try {
-- return this.world.getChunkSource().chunkMap.read(new ChunkPos(chunkX, chunkZ)).join().orElse(null);
-- } catch (final CompletionException ex) {
-- if (ex.getCause() instanceof IOException ioException) {
-- throw ioException;
-- }
-- throw ex;
-- }
-+ 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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
-index fdd189ef056187941d43809c5d61cab717aecf60..828c868f68c2a20bf90d0f7ec253fdeb591f15f6 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/EntityDataController.java
-@@ -1,6 +1,8 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
-
--import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
-+import ca.spottedleaf.moonrise.patches.chunk_system.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;
-@@ -9,12 +11,12 @@ import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
- import java.io.IOException;
- import java.nio.file.Path;
-
--public final class EntityDataController extends RegionFileIOThread.ChunkDataController {
-+public final class EntityDataController extends MoonriseRegionFileIO.RegionDataController {
-
- private final EntityRegionFileStorage storage;
-
-- public EntityDataController(final EntityRegionFileStorage storage) {
-- super(RegionFileIOThread.RegionFileType.ENTITY_DATA);
-+ public EntityDataController(final EntityRegionFileStorage storage, final ChunkTaskScheduler taskScheduler) {
-+ super(MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor);
- this.storage = storage;
- }
-
-@@ -24,13 +26,35 @@ public final class EntityDataController extends RegionFileIOThread.ChunkDataCont
- }
-
- @Override
-- public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
-- this.storage.write(new ChunkPos(chunkX, chunkZ), compound);
-+ 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 readData(final int chunkX, final int chunkZ) throws IOException {
-- return this.storage.read(new ChunkPos(chunkX, chunkZ));
-+ 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 {
-@@ -42,13 +66,7 @@ public final class EntityDataController extends RegionFileIOThread.ChunkDataCont
-
- @Override
- public void write(final ChunkPos pos, final CompoundTag nbt) throws IOException {
-- final ChunkPos nbtPos = nbt == null ? null : EntityStorage.readChunkPos(nbt);
-- if (nbtPos != null && !pos.equals(nbtPos)) {
-- throw new IllegalArgumentException(
-- "Entity chunk coordinate and serialized data do not have matching coordinates, trying to serialize coordinate " + pos.toString()
-- + " but compound says coordinate is " + nbtPos + " for world: " + this
-- );
-- }
-+ checkPosition(pos, nbt);
- super.write(pos, nbt);
- }
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
-index af867f8fedd0bb8f675e94243aa1a3f17363483b..bd0d782852f9cfe5bc0b5339ecf4d82c10332ec9 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/datacontroller/PoiDataController.java
-@@ -1,18 +1,20 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller;
-
--import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
-+import ca.spottedleaf.moonrise.patches.chunk_system.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 RegionFileIOThread.ChunkDataController {
-+public final class PoiDataController extends MoonriseRegionFileIO.RegionDataController {
-
- private final ServerLevel world;
-
-- public PoiDataController(final ServerLevel world) {
-- super(RegionFileIOThread.RegionFileType.POI_DATA);
-+ public PoiDataController(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
-+ super(MoonriseRegionFileIO.RegionFileType.POI_DATA, taskScheduler.ioExecutor, taskScheduler.compressionExecutor);
- this.world = world;
- }
-
-@@ -22,12 +24,22 @@ public final class PoiDataController extends RegionFileIOThread.ChunkDataControl
- }
-
- @Override
-- public void writeData(final int chunkX, final int chunkZ, final CompoundTag compound) throws IOException {
-- ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$write(chunkX, chunkZ, compound);
-+ 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 CompoundTag readData(final int chunkX, final int chunkZ) throws IOException {
-- return ((ChunkSystemSectionStorage)this.world.getPoiManager()).moonrise$read(chunkX, chunkZ);
-+ 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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..47a4d3376d08dde94a39254bec21473ff27f53e6
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
-index efcd9057f008f0b9cf0d22b2b21d1851205841e5..5d4d650186b18eb00782429d53d861564d8e4ba9 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemLevel.java
-@@ -1,5 +1,6 @@
- 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;
-@@ -19,4 +20,14 @@ public interface ChunkSystemLevel {
-
- 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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
-index b8a87b7e6505feb76ce1bd58c84615256cf6faa6..9d46482476f9ed9032a2b0f89afc20e03ed42dbb 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemServerLevel.java
-@@ -1,12 +1,13 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.level;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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.RegionFileIOThread;
-+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;
-@@ -17,32 +18,34 @@ public interface ChunkSystemServerLevel extends ChunkSystemLevel {
-
- public ChunkTaskScheduler moonrise$getChunkTaskScheduler();
-
-- public RegionFileIOThread.ChunkDataController moonrise$getChunkDataController();
-+ public MoonriseRegionFileIO.RegionDataController moonrise$getChunkDataController();
-
-- public RegionFileIOThread.ChunkDataController moonrise$getPoiChunkDataController();
-+ public MoonriseRegionFileIO.RegionDataController moonrise$getPoiChunkDataController();
-
-- public RegionFileIOThread.ChunkDataController moonrise$getEntityChunkDataController();
-+ public MoonriseRegionFileIO.RegionDataController moonrise$getEntityChunkDataController();
-
- public int moonrise$getRegionChunkShift();
-
-- // Paper - marked closing not needed on CB
-+ public boolean moonrise$isMarkedClosing();
-+
-+ public void moonrise$setMarkedClosing(final boolean value);
-
- public RegionizedPlayerChunkLoader moonrise$getPlayerChunkLoader();
-
- public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
-- final PrioritisedExecutor.Priority priority,
-+ final Priority priority,
- final Consumer<List<ChunkAccess>> onLoad);
-
- public void moonrise$loadChunksAsync(final BlockPos pos, final int radiusBlocks,
-- final ChunkStatus chunkStatus, final PrioritisedExecutor.Priority priority,
-+ 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 PrioritisedExecutor.Priority priority,
-+ 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 PrioritisedExecutor.Priority priority,
-+ final ChunkStatus chunkStatus, final Priority priority,
- final Consumer<List<ChunkAccess>> onLoad);
-
- public RegionizedPlayerChunkLoader.ViewDistanceHolder moonrise$getViewDistanceHolder();
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
-index 883fe6401f1b9711fa544d18a815b4d638f580df..aacd543f03b35908011d0c2891e978cc093ebcf5 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkSystemDistanceManager.java
-@@ -1,9 +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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
-index 997b05167c19472acb98edac32d4548cc65efa8e..5ed6599d1f9a2edf8c904f3602b06d26d857600c 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java
-@@ -1,6 +1,8 @@
- 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;
-@@ -13,6 +15,7 @@ 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;
-@@ -26,7 +29,6 @@ import java.util.Arrays;
- import java.util.Iterator;
- import java.util.List;
- import java.util.function.Predicate;
--import org.bukkit.event.entity.EntityRemoveEvent;
-
- public final class ChunkEntitySlices {
-
-@@ -43,6 +45,7 @@ public final class ChunkEntitySlices {
- private final EntityList entities = new EntityList();
-
- public FullChunkStatus status;
-+ public final ChunkData chunkData;
-
- private boolean isTransient;
-
-@@ -55,7 +58,7 @@ public final class ChunkEntitySlices {
- }
-
- public ChunkEntitySlices(final Level world, final int chunkX, final int chunkZ, final FullChunkStatus status,
-- final int minSection, final int maxSection) { // inclusive, inclusive
-+ final ChunkData chunkData, final int minSection, final int maxSection) { // inclusive, inclusive
- this.minSection = minSection;
- this.maxSection = maxSection;
- this.chunkX = chunkX;
-@@ -68,11 +71,12 @@ public final class ChunkEntitySlices {
- 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).collect(ImmutableList.toImmutableList());
-+ return EntityType.loadEntitiesRecursive(compoundTag.getList("Entities", 10), world, EntitySpawnReason.LOAD).collect(ImmutableList.toImmutableList());
- }
-
- // Paper start - rewrite chunk system
-@@ -100,7 +104,7 @@ public final class ChunkEntitySlices {
- }
-
- final ListTag entitiesTag = new ListTag();
-- for (final Entity entity : entities) {
-+ for (final Entity entity : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, entities)) {
- CompoundTag compoundTag = new CompoundTag();
- if (entity.save(compoundTag)) {
- entitiesTag.add(compoundTag);
-@@ -147,12 +151,12 @@ public final class ChunkEntitySlices {
- continue;
- }
- if (entity.shouldBeSaved()) {
-- entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD);
-+ 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()) {
-- passenger.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK, EntityRemoveEvent.Cause.UNLOAD);
-+ PlatformHooks.get().unloadEntity(passenger);
- }
- }
- }
-@@ -161,34 +165,7 @@ public final class ChunkEntitySlices {
- return this.entities.size() != 0;
- }
-
-- // Paper start
-- public org.bukkit.entity.Entity[] getChunkEntities() {
-- List<org.bukkit.entity.Entity> ret = new java.util.ArrayList<>();
-- final Entity[] entities = this.entities.getRawData();
-- for (int i = 0, size = Math.min(entities.length, this.entities.size()); i < size; ++i) {
-- final Entity entity = entities[i];
-- if (entity == null) {
-- continue;
-- }
-- final org.bukkit.entity.Entity bukkit = entity.getBukkitEntity();
-- if (bukkit != null && bukkit.isValid()) {
-- ret.add(bukkit);
-- }
-- }
--
-- return ret.toArray(new org.bukkit.entity.Entity[0]);
-- }
--
-- public void callEntitiesLoadEvent() {
-- org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesLoadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities());
-- }
--
-- public void callEntitiesUnloadEvent() {
-- org.bukkit.craftbukkit.event.CraftEventFactory.callEntitiesUnloadEvent(this.world, new ChunkPos(this.chunkX, this.chunkZ), this.getAllEntities());
-- }
-- // Paper end
--
-- private List<Entity> getAllEntities() {
-+ public List<Entity> getAllEntities() {
- final int len = this.entities.size();
- if (len == 0) {
- return new ArrayList<>();
-@@ -251,6 +228,7 @@ public final class ChunkEntitySlices {
- 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);
-@@ -284,6 +262,7 @@ public final class ChunkEntitySlices {
- return false;
- }
- ((ChunkSystemEntity)entity).moonrise$setChunkStatus(null);
-+ ((ChunkSystemEntity)entity).moonrise$setChunkData(null);
- final int sectionIndex = chunkSection - this.minSection;
-
- this.allEntities.removeEntity(entity, sectionIndex);
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
-index efc0c1acc8239dd7b00211a1d3bfd3fc3b2c810c..93335de8cf514dc8417e4b9b2d495663deda2904 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
-@@ -46,8 +46,6 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
-
- protected final SWMRLong2ObjectHashTable<ChunkSlicesRegion> regions = new SWMRLong2ObjectHashTable<>(128, 0.5f);
-
-- protected final int minSection; // inclusive
-- protected final int maxSection; // inclusive
- protected final LevelCallback<Entity> worldCallback;
-
- protected final ConcurrentLong2ReferenceChainedHashTable<Entity> entityById = new ConcurrentLong2ReferenceChainedHashTable<>();
-@@ -56,8 +54,6 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
-
- public EntityLookup(final Level world, final LevelCallback<Entity> worldCallback) {
- this.world = world;
-- this.minSection = WorldUtil.getMinSection(world);
-- this.maxSection = WorldUtil.getMaxSection(world);
- this.worldCallback = worldCallback;
- }
-
-@@ -91,7 +87,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
-
- protected abstract void entityEndTicking(final Entity entity);
-
-- protected abstract boolean screenEntity(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) {
-@@ -347,7 +343,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
- }
-
- protected void addRecursivelySafe(final Entity root, final boolean fromDisk) {
-- if (!this.addEntity(root, 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;
-@@ -386,7 +382,11 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
- }
-
- public boolean addNewEntity(final Entity entity) {
-- return this.addEntity(entity, false);
-+ 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) {
-@@ -397,10 +397,10 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
- return Visibility.fromFullChunkStatus(entityStatus == null ? FullChunkStatus.INACCESSIBLE : entityStatus);
- }
-
-- protected boolean addEntity(final Entity entity, final boolean fromDisk) {
-+ 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, this.minSection, this.maxSection);
-+ 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");
-
-@@ -414,7 +414,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
- return false;
- }
-
-- if (!this.screenEntity(entity)) {
-+ if (!this.screenEntity(entity, fromDisk, event)) {
- return false;
- }
-
-@@ -519,7 +519,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
- final int sectionZ = ((ChunkSystemEntity)entity).moonrise$getSectionZ();
- final BlockPos newPos = entity.blockPosition();
- final int newSectionX = newPos.getX() >> 4;
-- final int newSectionY = Mth.clamp(newPos.getY() >> 4, this.minSection, this.maxSection);
-+ final int 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) {
-@@ -959,7 +959,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
-
- public ChunkEntitySlices getOrCreateChunk(final int chunkX, final int chunkZ) {
- final ChunkSlicesRegion region = this.getRegion(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
-- ChunkEntitySlices ret;
-+ final ChunkEntitySlices ret;
- if (region == null || (ret = region.get((chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT))) == null) {
- return this.createEntityChunk(chunkX, chunkZ, true);
- }
-@@ -1057,7 +1057,7 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
- @Override
- public void onRemove(final Entity.RemovalReason reason) {
- final Entity entity = this.entity;
-- EntityLookup.this.checkThread(entity, "Cannot remove entity off-main"); // Paper - rewrite chunk system
-+ EntityLookup.this.checkThread(entity, "Cannot remove entity off-main");
- final Visibility tickingState = EntityLookup.getEntityStatus(entity);
-
- EntityLookup.this.removeEntity(entity);
-@@ -1080,4 +1080,4 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
- @Override
- public void onRemove(final Entity.RemovalReason reason) {}
- }
--}
-\ No newline at end of file
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
-index edcde00206d068bd79175fea33efa05b0e8c1562..a038215156a163b0b1cbc870ada5b4ac85ed1335 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/client/ClientEntityLookup.java
-@@ -1,5 +1,6 @@
- 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;
-@@ -45,7 +46,8 @@ public final class ClientEntityLookup extends EntityLookup {
-
- final ChunkEntitySlices ret = new ChunkEntitySlices(
- this.world, chunkX, chunkZ,
-- ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
-+ ticking ? FullChunkStatus.ENTITY_TICKING : FullChunkStatus.FULL, null,
-+ WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
- );
-
- // note: not handled by superclass
-@@ -63,7 +65,11 @@ public final class ClientEntityLookup extends EntityLookup {
- 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
-@@ -97,7 +103,7 @@ public final class ClientEntityLookup extends EntityLookup {
- }
-
- @Override
-- protected boolean screenEntity(final Entity entity) {
-+ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
- return true;
- }
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
-index 465469e44346c50f30f3abd6b44f4173ccfcf248..2ff58cf753c60913ee73aae015182e9c5560d529 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/dfl/DefaultEntityLookup.java
-@@ -32,7 +32,7 @@ public final class DefaultEntityLookup extends EntityLookup {
- protected ChunkEntitySlices createEntityChunk(final int chunkX, final int chunkZ, final boolean transientChunk) {
- final ChunkEntitySlices ret = new ChunkEntitySlices(
- this.world, chunkX, chunkZ, FullChunkStatus.FULL,
-- WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
-+ null, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
- );
-
- // note: not handled by superclass
-@@ -84,7 +84,7 @@ public final class DefaultEntityLookup extends EntityLookup {
- }
-
- @Override
-- protected boolean screenEntity(final Entity entity) {
-+ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
- return true;
- }
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-index dacf2b2988ce603879fe525a3418ac77f8a663f7..58d9187adc188b693b6becc400f766e069bf1bf5 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-@@ -1,6 +1,8 @@
- 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;
-@@ -17,7 +19,6 @@ public final class ServerEntityLookup extends EntityLookup {
-
- private final ServerLevel serverWorld;
- public final ReferenceList<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
-- public final ReferenceList<Entity> trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
-
- public ServerEntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) {
- super(world, worldCallback);
-@@ -63,6 +64,11 @@ public final class ServerEntityLookup extends EntityLookup {
- 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
-@@ -77,14 +83,12 @@ public final class ServerEntityLookup extends EntityLookup {
- if (entity instanceof ServerPlayer player) {
- ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().removePlayer(player);
- }
-- this.trackerUnloadedEntities.remove(entity); // Moonrise - entity tracker
- }
-
- @Override
- protected void entityStartLoaded(final Entity entity) {
- // Moonrise start - entity tracker
- this.trackerEntities.add(entity);
-- this.trackerUnloadedEntities.remove(entity);
- // Moonrise end - entity tracker
- }
-
-@@ -92,7 +96,6 @@ public final class ServerEntityLookup extends EntityLookup {
- protected void entityEndLoaded(final Entity entity) {
- // Moonrise start - entity tracker
- this.trackerEntities.remove(entity);
-- this.trackerUnloadedEntities.add(entity);
- // Moonrise end - entity tracker
- }
-
-@@ -107,7 +110,7 @@ public final class ServerEntityLookup extends EntityLookup {
- }
-
- @Override
-- protected boolean screenEntity(final Entity entity) {
-- return ChunkSystem.screenEntity(this.serverWorld, entity);
-+ protected boolean screenEntity(final Entity entity, final boolean fromDisk, final boolean event) {
-+ return ChunkSystem.screenEntity(this.serverWorld, entity, fromDisk, event);
- }
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
-index fd35e4db0c8fec8f86b8743bcc2b15ed2e7433f1..bbf9d6c1c9525d97160806819a57be03eca290f1 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/poi/PoiChunk.java
-@@ -3,7 +3,6 @@ package ca.spottedleaf.moonrise.patches.chunk_system.level.poi;
- import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
- import ca.spottedleaf.moonrise.common.util.TickThread;
- import ca.spottedleaf.moonrise.common.util.WorldUtil;
--import com.mojang.serialization.Codec;
- import com.mojang.serialization.DataResult;
- import net.minecraft.SharedConstants;
- import net.minecraft.nbt.CompoundTag;
-@@ -123,7 +122,6 @@ public final class PoiChunk {
- ret.putInt("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion());
-
- final ServerLevel world = this.world;
-- final PoiManager poiManager = world.getPoiManager();
- final int chunkX = this.chunkX;
- final int chunkZ = this.chunkZ;
-
-@@ -133,13 +131,8 @@ public final class PoiChunk {
- continue;
- }
-
-- final long key = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ);
-- // codecs are honestly such a fucking disaster. What the fuck is this trash?
-- final Codec<PoiSection> codec = PoiSection.codec(() -> {
-- poiManager.setDirty(key);
-- });
--
-- final DataResult<Tag> serializedResult = codec.encodeStart(registryOps, section);
-+ // 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);
-@@ -183,19 +176,18 @@ public final class PoiChunk {
- continue;
- }
-
-- final long coordinateKey = CoordinateUtils.getChunkSectionKey(chunkX, sectionY, chunkZ);
-- // codecs are honestly such a fucking disaster. What the fuck is this trash?
-- final Codec<PoiSection> codec = PoiSection.codec(() -> {
-- poiManager.setDirty(coordinateKey);
-- });
--
- final CompoundTag section = sections.getCompound(key);
-- final DataResult<PoiSection> deserializeResult = codec.parse(registryOps, section);
-+ final DataResult<PoiSection.Packed> deserializeResult = PoiSection.Packed.CODEC.parse(registryOps, section);
- final int finalSectionY = sectionY;
-- final PoiSection deserialized = deserializeResult.resultOrPartial((final String description) -> {
-+ 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;
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
-index fb87d7ece6ebccfd0ffd2f1a609b45a0d2461d9e..524752744e37a2db0e3ea089468bdf497129bfef 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/storage/ChunkSystemSectionStorage.java
-@@ -1,12 +1,8 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.level.storage;
-
--import com.mojang.serialization.Dynamic;
- import net.minecraft.nbt.CompoundTag;
--import net.minecraft.nbt.Tag;
- import net.minecraft.world.level.chunk.storage.RegionFileStorage;
- import java.io.IOException;
--import java.util.Optional;
--import java.util.concurrent.CompletableFuture;
-
- public interface ChunkSystemSectionStorage {
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-index 852d75a73dae7448cbe1e2f5e164b235efa8a969..b2fa9883aefb07f64bb5db7e0052218d2ad09aba 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-@@ -1,7 +1,8 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.player;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
- 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;
-@@ -412,7 +413,11 @@ public final class RegionizedPlayerChunkLoader {
- if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) {
- ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager
- .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player);
-- PlayerChunkSender.sendChunk(this.player.connection, this.world, ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ));
-+
-+ 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();
-@@ -426,17 +431,12 @@ public final class RegionizedPlayerChunkLoader {
- }
-
- 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);
-- final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
-- this.player.connection.send(new ClientboundForgetLevelChunkPacket(chunkPos));
-- // Paper start - PlayerChunkUnloadEvent
-- if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) {
-- new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(this.world.getWorld().getChunkAt(chunkPos.longKey), this.player.getBukkitEntity()).callEvent();
-- }
-- // Paper end - PlayerChunkUnloadEvent
-+ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ)));
- }
-
- private final SingleUserAreaMap<PlayerChunkLoaderData> broadcastMap = new SingleUserAreaMap<>(this) {
-@@ -529,7 +529,7 @@ public final class RegionizedPlayerChunkLoader {
- final int playerSendViewDistance, final int worldSendViewDistance) {
- return Math.min(
- loadViewDistance - 1,
-- playerSendViewDistance < 0 ? (!io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.autoConfigSendDistance || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance
-+ playerSendViewDistance < 0 ? (!PlatformHooks.get().configAutoConfigSendDistance() || clientViewDistance < 0 ? (worldSendViewDistance < 0 ? (loadViewDistance - 1) : worldSendViewDistance) : clientViewDistance + 1) : playerSendViewDistance
- );
- }
-
-@@ -554,26 +554,26 @@ public final class RegionizedPlayerChunkLoader {
- }
-
- private double getMaxChunkLoadRate() {
-- final double configRate = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkLoadRate;
-+ 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 = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkGenerateRate;
-+ 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 = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingBasic.playerMaxChunkSendRate;
-+ 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 = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkLoads;
-+ 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);
-@@ -587,7 +587,7 @@ public final class RegionizedPlayerChunkLoader {
-
- private long getMaxChunkGenerates() {
- final long radiusChunks = (2L * this.lastLoadDistance + 1L) * (2L * this.lastLoadDistance + 1L);
-- long configLimit = io.papermc.paper.configuration.GlobalConfiguration.get().chunkLoadingAdvanced.playerMaxConcurrentChunkGenerates;
-+ 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);
-@@ -709,7 +709,7 @@ public final class RegionizedPlayerChunkLoader {
- final int queuedChunkX = CoordinateUtils.getChunkX(queuedLoadChunk);
- final int queuedChunkZ = CoordinateUtils.getChunkZ(queuedLoadChunk);
- ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
-- queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, PrioritisedExecutor.Priority.NORMAL, null
-+ queuedChunkX, queuedChunkZ, ChunkStatus.EMPTY, false, Priority.NORMAL, null
- );
- if (this.removed) {
- return;
-@@ -825,7 +825,7 @@ public final class RegionizedPlayerChunkLoader {
- }
- 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();
-+ chunk.postProcessGeneration(this.world);
- // check if there was any recursive action
- if (this.removed || this.sendQueue.isEmpty() || this.sendQueue.firstLong() != pendingSend) {
- return;
-@@ -864,7 +864,6 @@ public final class RegionizedPlayerChunkLoader {
- final int clientViewDistance = getClientViewDistance(this.player);
- final int sendViewDistance = getSendViewDistance(loadViewDistance, clientViewDistance, playerDistances.sendViewDistance, worldDistances.sendViewDistance);
-
-- // TODO check PlayerList diff in paper chunk system patch
- // send view distances
- this.player.connection.send(this.updateClientChunkRadius(sendViewDistance));
- this.player.connection.send(this.updateClientSimulationDistance(tickViewDistance));
-@@ -1078,5 +1077,9 @@ public final class RegionizedPlayerChunkLoader {
-
- // now all tickets should be removed, which is all of our external state
- }
-+
-+ public LongOpenHashSet getSentChunksRaw() {
-+ return this.sentChunks;
-+ }
- }
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-index 58d3d1a47e9f2423c467bb329c2d5f4b58a8b5ef..f98df65eaed2abedc66f3a49790e0cfb65354ed9 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-@@ -1,14 +1,14 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
- import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
- import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.PlatformHooks;
- import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
--import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
- import ca.spottedleaf.moonrise.common.util.TickThread;
- import ca.spottedleaf.moonrise.common.util.WorldUtil;
- import ca.spottedleaf.moonrise.common.util.ChunkSystem;
--import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
-+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;
-@@ -20,7 +20,6 @@ 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;
-@@ -40,7 +39,9 @@ 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 org.slf4j.LoggerFactory;
- import java.io.IOException;
- import java.text.DecimalFormat;
- import java.util.ArrayDeque;
-@@ -58,7 +59,7 @@ import java.util.function.Predicate;
-
- public final class ChunkHolderManager {
-
-- private static final Logger LOGGER = LogUtils.getClassLogger();
-+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkHolderManager.class);
-
- public static final int FULL_LOADED_TICKET_LEVEL = ChunkLevel.FULL_CHUNK_LEVEL;
- public static final int BLOCK_TICKING_TICKET_LEVEL = ChunkLevel.BLOCK_TICKING_LEVEL;
-@@ -189,7 +190,7 @@ public final class ChunkHolderManager {
- if (halt) {
- LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
- if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
-- LOGGER.warn("Failed to halt world generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
-+ 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) + "'");
- }
-@@ -199,21 +200,21 @@ public final class ChunkHolderManager {
- this.saveAllChunks(true, true, true);
- }
-
-- boolean hasTasks = false;
-- for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) {
-- if (RegionFileIOThread.getControllerFor(this.world, type).hasTasks()) {
-- hasTasks = true;
-- break;
-+ 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) + "'");
- }
- }
-- if (hasTasks) {
-- RegionFileIOThread.flush();
-- }
-
- // kill regionfile cache
-- for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) {
-+ for (final MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) {
- try {
-- RegionFileIOThread.getControllerFor(this.world, type).getCache().close();
-+ 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);
- }
-@@ -230,8 +231,8 @@ public final class ChunkHolderManager {
- public void autoSave() {
- final List<NewChunkHolder> reschedule = new ArrayList<>();
- final long currentTick = this.currentTick;
-- final long maxSaveTime = currentTick - Math.max(1L, this.world.paperConfig().chunks.autoSaveInterval.value());
-- final int maxToSave = this.world.paperConfig().chunks.maxAutoSaveChunksPerTick;
-+ final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval());
-+ final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick();
- for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
- final NewChunkHolder holder = this.autoSaveQueue.first();
-
-@@ -271,55 +272,74 @@ public final class ChunkHolderManager {
-
- long start = System.nanoTime();
- long lastLog = start;
-- boolean needsFlush = false;
-- final int flushInterval = 50;
-+ 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) {
-- ++saved;
-- needsFlush = flush;
- 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 (needsFlush && (saved % flushInterval) == 0) {
-- needsFlush = false;
-- RegionFileIOThread.partialFlush(flushInterval / 2);
-+ 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 " + saved + " chunks (" + format.format((double)(i+1)/(double)len * 100.0) + "%) in world '" + WorldUtil.getWorldName(this.world) + "'");
-+ 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) {
-- RegionFileIOThread.flush();
-+ MoonriseRegionFileIO.flush(this.world);
- try {
-- RegionFileIOThread.flushRegionStorages(this.world);
-+ 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");
-+ 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"
-+ );
- }
- }
-
-@@ -798,21 +818,21 @@ public final class ChunkHolderManager {
- return this.chunkHolders.get(position);
- }
-
-- public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
-+ 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority 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);
-@@ -895,7 +915,7 @@ public final class ChunkHolderManager {
- final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask();
-
- if (entityLoad != null) {
-- entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
-+ entityLoad.raisePriority(Priority.BLOCKING);
- }
- }
- }
-@@ -971,7 +991,7 @@ public final class ChunkHolderManager {
- final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask();
-
- if (poiLoad != null) {
-- poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
-+ poiLoad.raisePriority(Priority.BLOCKING);
- }
- }
- } finally {
-@@ -1018,7 +1038,7 @@ public final class ChunkHolderManager {
- }
-
- ChunkHolderManager.this.processPendingFullUpdate();
-- }, PrioritisedExecutor.Priority.HIGHEST);
-+ }, Priority.HIGHEST);
- } else {
- final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-@@ -1028,11 +1048,10 @@ public final class ChunkHolderManager {
- }
-
- private void removeChunkHolder(final NewChunkHolder holder) {
-- holder.markUnloaded();
-+ 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
-@@ -1313,6 +1332,9 @@ public final class ChunkHolderManager {
- 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;
-
-@@ -1328,10 +1350,15 @@ public final class ChunkHolderManager {
- }
- changedFullStatus = new ArrayList<>();
-
-- ret |= this.ticketLevelPropagator.performUpdates(
-- this.ticketLockArea, this.taskScheduler.schedulingLockArea,
-- scheduledTasks, changedFullStatus
-- );
-+ this.blockTicketUpdates();
-+ try {
-+ ret |= this.ticketLevelPropagator.performUpdates(
-+ this.ticketLockArea, this.taskScheduler.schedulingLockArea,
-+ scheduledTasks, changedFullStatus
-+ );
-+ } finally {
-+ this.unblockTicketUpdates(Boolean.FALSE);
-+ }
- }
-
- if (changedFullStatus != null) {
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-index 8671a90e969d16c7a57ddc38fedb7cf01815f64c..120ce31729dc8d4bba0901ca06d3212f3158d089 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-@@ -1,16 +1,17 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadedTaskQueue;
-+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.config.moonrise.MoonriseConfig;
- import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
- import ca.spottedleaf.moonrise.common.util.JsonUtil;
- import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
- import ca.spottedleaf.moonrise.common.util.TickThread;
- import ca.spottedleaf.moonrise.common.util.WorldUtil;
--import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
- import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
- import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkStatus;
- import ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer;
-@@ -23,7 +24,6 @@ import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkUpgrade
- 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.mojang.logging.LogUtils;
- import com.google.gson.JsonArray;
- import com.google.gson.JsonObject;
- import net.minecraft.CrashReport;
-@@ -48,6 +48,7 @@ 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 org.slf4j.LoggerFactory;
- import java.io.File;
- import java.time.LocalDateTime;
- import java.time.format.DateTimeFormatter;
-@@ -63,51 +64,14 @@ import java.util.function.Consumer;
-
- public final class ChunkTaskScheduler {
-
-- private static final Logger LOGGER = LogUtils.getClassLogger();
-+ private static final Logger LOGGER = LoggerFactory.getLogger(ChunkTaskScheduler.class);
-
-- static int newChunkSystemIOThreads;
-- static int newChunkSystemGenParallelism;
-- static int newChunkSystemGenPopulationParallelism;
-- static int newChunkSystemLoadParallelism;
--
-- private static boolean initialised = false;
--
-- public static void init(io.papermc.paper.configuration.GlobalConfiguration.ChunkSystem chunkSystem) {
-- if (initialised) {
-- return;
-- }
-- initialised = true;
-- MoonriseCommon.init(chunkSystem); // Paper
-- newChunkSystemIOThreads = chunkSystem.ioThreads;
-- if (newChunkSystemIOThreads <= 0) {
-- newChunkSystemIOThreads = 1;
-- } else {
-- newChunkSystemIOThreads = Math.max(1, newChunkSystemIOThreads);
-- }
--
-- String newChunkSystemGenParallelism = chunkSystem.genParallelism;
-- if (newChunkSystemGenParallelism.equalsIgnoreCase("default")) {
-- newChunkSystemGenParallelism = "true";
-- }
--
-- boolean useParallelGen;
-- if (newChunkSystemGenParallelism.equalsIgnoreCase("on") || newChunkSystemGenParallelism.equalsIgnoreCase("enabled")
-- || newChunkSystemGenParallelism.equalsIgnoreCase("true")) {
-- useParallelGen = true;
-- } else if (newChunkSystemGenParallelism.equalsIgnoreCase("off") || newChunkSystemGenParallelism.equalsIgnoreCase("disabled")
-- || newChunkSystemGenParallelism.equalsIgnoreCase("false")) {
-- useParallelGen = false;
-- } else {
-- throw new IllegalStateException("Invalid option for gen-parallelism: must be one of [on, off, enabled, disabled, true, false, default]");
-+ public static void init(final boolean useParallelGen) {
-+ for (final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor executor : MoonriseCommon.RADIUS_AWARE_GROUP.getAllExecutors()) {
-+ executor.setMaxParallelism(useParallelGen ? -1 : 1);
- }
-
-- ChunkTaskScheduler.newChunkSystemGenParallelism = MoonriseCommon.WORKER_THREADS;
-- ChunkTaskScheduler.newChunkSystemGenPopulationParallelism = useParallelGen ? MoonriseCommon.WORKER_THREADS : 1;
-- ChunkTaskScheduler.newChunkSystemLoadParallelism = MoonriseCommon.WORKER_THREADS;
--
-- RegionFileIOThread.init(newChunkSystemIOThreads);
--
-- LOGGER.info("Chunk system is using " + newChunkSystemIOThreads + " I/O threads, " + MoonriseCommon.WORKER_THREADS + " worker threads, and population gen parallelism of " + ChunkTaskScheduler.newChunkSystemGenPopulationParallelism + " threads");
-+ 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);
-@@ -151,13 +115,15 @@ public final class ChunkTaskScheduler {
- }
-
- public final ServerLevel world;
-- public final PrioritisedThreadPool workers;
- public final RadiusAwarePrioritisedExecutor radiusAwareScheduler;
-- public final PrioritisedThreadPool.PrioritisedPoolExecutor parallelGenExecutor;
-- private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor;
-- public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor;
-+ 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 PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue();
-+ private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue();
-
- public final ChunkHolderManager chunkHolderManager;
-
-@@ -306,9 +272,8 @@ public final class ChunkTaskScheduler {
- return this.lockShift;
- }
-
-- public ChunkTaskScheduler(final ServerLevel world, final PrioritisedThreadPool workers) {
-+ public ChunkTaskScheduler(final ServerLevel world) {
- this.world = world;
-- this.workers = workers;
- // 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
-@@ -317,11 +282,14 @@ public final class ChunkTaskScheduler {
- this.lockShift = Math.max(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift(), ThreadedTicketLevelPropagator.SECTION_SHIFT);
- this.schedulingLockArea = new ReentrantAreaLock(this.getChunkSystemLockShift());
-
-- final String worldName = WorldUtil.getWorldName(world);
-- this.parallelGenExecutor = workers.createExecutor("Chunk parallel generation executor for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenParallelism));
-- this.radiusAwareGenExecutor = workers.createExecutor("Chunk radius aware generator for world '" + worldName + "'", 1, Math.max(1, newChunkSystemGenPopulationParallelism));
-- this.loadExecutor = workers.createExecutor("Chunk load executor for world '" + worldName + "'", 1, newChunkSystemLoadParallelism);
-- this.radiusAwareScheduler = new RadiusAwarePrioritisedExecutor(this.radiusAwareGenExecutor, Math.max(2, 1 + newChunkSystemGenPopulationParallelism));
-+ 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);
- }
-
-@@ -360,7 +328,7 @@ public final class ChunkTaskScheduler {
- };
-
- // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
-- this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING);
-+ 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));
- }
-@@ -370,20 +338,20 @@ public final class ChunkTaskScheduler {
- return this.mainThreadExecutor.executeTask();
- }
-
-- public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) {
-+ 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority priority,
-+ final boolean addTicket, final Priority priority,
- final Consumer<LevelChunk> onComplete) {
- final int radius = toStatus.ordinal() - 1; // 0 -> BORDER, 1 -> TICKING, 2 -> ENTITY_TICKING
-
-@@ -479,7 +447,7 @@ public final class ChunkTaskScheduler {
- }
-
- public void scheduleChunkLoad(final int chunkX, final int chunkZ, final boolean gen, final ChunkStatus toStatus, final boolean addTicket,
-- final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
-+ final Priority priority, final Consumer<ChunkAccess> onComplete) {
- if (gen) {
- this.scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
- return;
-@@ -503,7 +471,7 @@ public final class ChunkTaskScheduler {
-
- // only appropriate to use with syncLoadNonFull
- public boolean beginChunkLoadForNonFullSync(final int chunkX, final int chunkZ, final ChunkStatus toStatus,
-- final PrioritisedExecutor.Priority priority) {
-+ final Priority priority) {
- final int accessRadius = getAccessRadius(toStatus);
- final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ);
- final int minLevel = ChunkTaskScheduler.getTicketLevel(toStatus);
-@@ -548,6 +516,11 @@ public final class ChunkTaskScheduler {
- 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;
-@@ -558,7 +531,7 @@ public final class ChunkTaskScheduler {
- this.chunkHolderManager.addTicketAtLevel(NON_FULL_CHUNK_LOAD, chunkX, chunkZ, ticketLevel, ticketId);
- this.chunkHolderManager.processTicketUpdates();
-
-- this.beginChunkLoadForNonFullSync(chunkX, chunkZ, status, PrioritisedExecutor.Priority.BLOCKING);
-+ 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
-@@ -578,7 +551,7 @@ public final class ChunkTaskScheduler {
- }
-
- public void scheduleChunkLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus, final boolean addTicket,
-- final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
-+ 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);
-@@ -670,7 +643,7 @@ public final class ChunkTaskScheduler {
-
- private ChunkProgressionTask createTask(final int chunkX, final int chunkZ, final ChunkAccess chunk,
- final NewChunkHolder chunkHolder, final StaticCache2D<GenerationChunkHolder> neighbours,
-- final ChunkStatus toStatus, final PrioritisedExecutor.Priority initialPriority) {
-+ final ChunkStatus toStatus, final Priority initialPriority) {
- if (toStatus == ChunkStatus.EMPTY) {
- return new ChunkLoadTask(this, this.world, chunkX, chunkZ, chunkHolder, initialPriority);
- }
-@@ -686,7 +659,7 @@ public final class ChunkTaskScheduler {
-
- 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(PrioritisedExecutor.Priority.NORMAL));
-+ return this.schedule(chunkX, chunkZ, targetStatus, chunkHolder, allTasks, chunkHolder.getEffectivePriority(Priority.NORMAL));
- }
-
- // rets new task scheduled for the _specified_ chunk
-@@ -695,7 +668,7 @@ public final class ChunkTaskScheduler {
- // 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 PrioritisedExecutor.Priority minPriority) {
-+ final Priority minPriority) {
- if (!this.schedulingLockArea.isHeldByCurrentThread(chunkX, chunkZ, getAccessRadius(targetStatus))) {
- throw new IllegalStateException("Not holding scheduling lock");
- }
-@@ -705,8 +678,8 @@ public final class ChunkTaskScheduler {
- return null;
- }
-
-- final PrioritisedExecutor.Priority requestedPriority = PrioritisedExecutor.Priority.max(
-- minPriority, chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
-+ final Priority requestedPriority = Priority.max(
-+ minPriority, chunkHolder.getEffectivePriority(Priority.NORMAL)
- );
- final ChunkStatus currentGenStatus = chunkHolder.getCurrentGenStatus();
- final ChunkAccess chunk = chunkHolder.getCurrentChunk();
-@@ -743,7 +716,7 @@ public final class ChunkTaskScheduler {
-
- final int neighbourReadRadius = Math.max(
- 0,
-- chunkPyramid.getStepTo(toStatus).getAccumulatedRadiusOf(ChunkStatus.EMPTY)
-+ chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY)
- );
-
- boolean unGeneratedNeighbours = false;
-@@ -783,7 +756,7 @@ public final class ChunkTaskScheduler {
-
- final ChunkProgressionTask task = this.createTask(
- chunkX, chunkZ, chunk, chunkHolder, neighbours, toStatus,
-- chunkHolder.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
-+ chunkHolder.getEffectivePriority(Priority.NORMAL)
- );
- allTasks.add(task);
-
-@@ -794,7 +767,7 @@ public final class ChunkTaskScheduler {
-
- // 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 PrioritisedExecutor.Priority minPriority) {
-+ final List<ChunkProgressionTask> tasks, final Priority minPriority) {
- final NewChunkHolder chunkHolder = this.chunkHolderManager.getChunkHolder(chunkX, chunkZ);
-
- if (chunkHolder == null) {
-@@ -830,41 +803,41 @@ public final class ChunkTaskScheduler {
- */
- @Deprecated
- public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) {
-- return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL);
-+ 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 PrioritisedExecutor.Priority priority) {
-- return this.mainThreadExecutor.queueRunnable(run, priority);
-+ 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, PrioritisedExecutor.Priority.NORMAL);
-+ return this.createChunkTask(chunkX, chunkZ, run, Priority.NORMAL);
- }
-
- public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
-- final PrioritisedExecutor.Priority priority) {
-+ 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, PrioritisedExecutor.Priority.NORMAL);
-+ return this.scheduleChunkTask(chunkX, chunkZ, run, Priority.NORMAL);
- }
-
- public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
-- final PrioritisedExecutor.Priority priority) {
-- return this.mainThreadExecutor.queueRunnable(run, priority);
-+ 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();
-- final long time = System.nanoTime();
- if (sync) {
-+ final long time = System.nanoTime();
- for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
- if (
- !this.radiusAwareGenExecutor.isActive() &&
-@@ -882,6 +855,29 @@ public final class ChunkTaskScheduler {
- 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 {
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-index 45eda96fd8a1acb87dbb69ce5495fec7e451416f..381631e405895ba3eede1cd2e1011c64aadbd662 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-@@ -1,18 +1,20 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
-
--import ca.spottedleaf.concurrentutil.completable.Completable;
-+import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
- import ca.spottedleaf.concurrentutil.executor.Cancellable;
--import ca.spottedleaf.concurrentutil.executor.standard.DelayedPrioritisedTask;
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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.ChunkSystemFeatures;
--import ca.spottedleaf.moonrise.patches.chunk_system.async_save.AsyncChunkSaveData;
--import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
-+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;
-@@ -36,13 +38,14 @@ 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.ChunkSerializer;
-+import net.minecraft.world.level.chunk.storage.SerializableChunkData;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import java.lang.invoke.VarHandle;
-@@ -58,6 +61,8 @@ 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;
-@@ -89,7 +94,7 @@ public final class NewChunkHolder {
- if (this.entityChunk == null) {
- ret = this.entityChunk = new ChunkEntitySlices(
- this.world, this.chunkX, this.chunkZ, this.getChunkStatus(),
-- WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
-+ this.holderData, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)
- );
-
- ret.setTransient(transientChunk);
-@@ -173,7 +178,7 @@ public final class NewChunkHolder {
- // no tasks to schedule _for_
- } else {
- entityDataLoadTask = this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
-- this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
- );
- entityDataLoadTask.addCallback(this::completeEntityLoad);
- // need one schedule() per waiter
-@@ -220,7 +225,7 @@ public final class NewChunkHolder {
-
- if (this.entityDataLoadTask == null) {
- this.entityDataLoadTask = new ChunkLoadTask.EntityDataLoadTask(
-- this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
- );
- this.entityDataLoadTask.addCallback(this::completeEntityLoad);
- this.entityDataLoadTaskWaiters = new ArrayList<>();
-@@ -294,7 +299,7 @@ public final class NewChunkHolder {
- // no tasks to schedule _for_
- } else {
- poiDataLoadTask = this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
-- this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
- );
- poiDataLoadTask.addCallback(this::completePoiLoad);
- // need one schedule() per waiter
-@@ -340,7 +345,7 @@ public final class NewChunkHolder {
-
- if (this.poiDataLoadTask == null) {
- this.poiDataLoadTask = new ChunkLoadTask.PoiDataLoadTask(
-- this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL)
-+ this.scheduler, this.world, this.chunkX, this.chunkZ, this.getEffectivePriority(Priority.NORMAL)
- );
- this.poiDataLoadTask.addCallback(this::completePoiLoad);
- this.poiDataLoadTaskWaiters = new ArrayList<>();
-@@ -519,15 +524,15 @@ public final class NewChunkHolder {
- // priority state
-
- // the target priority for this chunk to generate at
-- private PrioritisedExecutor.Priority priority = null;
-+ private Priority priority = null;
- private boolean priorityLocked;
-
- // the priority neighbouring chunks have requested this chunk generate at
-- private PrioritisedExecutor.Priority neighbourRequestedPriority = null;
-+ private Priority neighbourRequestedPriority = null;
-
-- public PrioritisedExecutor.Priority getEffectivePriority(final PrioritisedExecutor.Priority dfl) {
-- final PrioritisedExecutor.Priority neighbour = this.neighbourRequestedPriority;
-- final PrioritisedExecutor.Priority us = this.priority;
-+ public Priority getEffectivePriority(final Priority dfl) {
-+ final Priority neighbour = this.neighbourRequestedPriority;
-+ final Priority us = this.priority;
-
- if (neighbour == null) {
- return us == null ? dfl : us;
-@@ -536,7 +541,7 @@ public final class NewChunkHolder {
- return neighbour;
- }
-
-- return PrioritisedExecutor.Priority.max(us, neighbour);
-+ return Priority.max(us, neighbour);
- }
-
- private void recalculateNeighbourRequestedPriority() {
-@@ -545,18 +550,18 @@ public final class NewChunkHolder {
- return;
- }
-
-- PrioritisedExecutor.Priority max = null;
-+ Priority max = null;
-
- for (final NewChunkHolder holder : this.neighboursWaitingForUs.keySet()) {
-- final PrioritisedExecutor.Priority neighbourPriority = holder.getEffectivePriority(null);
-+ final Priority neighbourPriority = holder.getEffectivePriority(null);
- if (neighbourPriority != null && (max == null || neighbourPriority.isHigherPriority(max))) {
- max = neighbourPriority;
- }
- }
-
-- final PrioritisedExecutor.Priority current = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
-+ final Priority current = this.getEffectivePriority(Priority.NORMAL);
- this.neighbourRequestedPriority = max;
-- final PrioritisedExecutor.Priority next = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
-+ final Priority next = this.getEffectivePriority(Priority.NORMAL);
-
- if (current == next) {
- return;
-@@ -578,7 +583,7 @@ public final class NewChunkHolder {
- }
-
- // must hold scheduling lock
-- public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ public void raisePriority(final Priority priority) {
- if (this.priority != null && this.priority.isHigherOrEqualPriority(priority)) {
- return;
- }
-@@ -591,13 +596,13 @@ public final class NewChunkHolder {
- }
-
- // must hold scheduling lock
-- public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ public void setPriority(final Priority priority) {
- if (this.priorityLocked) {
- return;
- }
-- final PrioritisedExecutor.Priority old = this.getEffectivePriority(null);
-+ final Priority old = this.getEffectivePriority(null);
- this.priority = priority;
-- final PrioritisedExecutor.Priority newPriority = this.getEffectivePriority(PrioritisedExecutor.Priority.NORMAL);
-+ final Priority newPriority = this.getEffectivePriority(Priority.NORMAL);
-
- if (old != newPriority) {
- if (this.generationTask != null) {
-@@ -609,7 +614,7 @@ public final class NewChunkHolder {
- }
-
- // must hold scheduling lock
-- public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ public void lowerPriority(final Priority priority) {
- if (this.priority != null && this.priority.isLowerOrEqualPriority(priority)) {
- return;
- }
-@@ -632,7 +637,7 @@ public final class NewChunkHolder {
- }
-
- // ticket level state
-- public int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
-+ private int oldTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
- private int currentTicketLevel = ChunkHolderManager.MAX_TICKET_LEVEL + 1;
-
- public int getTicketLevel() {
-@@ -651,6 +656,7 @@ public final class NewChunkHolder {
- 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() {
-@@ -750,8 +756,9 @@ public final class NewChunkHolder {
- /** Unloaded from chunk map */
- private boolean unloaded;
-
-- void markUnloaded() {
-+ void onUnload() {
- this.unloaded = true;
-+ ((ChunkSystemLevel)this.world).moonrise$releaseChunkData(CoordinateUtils.getChunkKey(this.chunkX, this.chunkZ));
- }
-
- private boolean inUnloadQueue = false;
-@@ -788,9 +795,10 @@ public final class NewChunkHolder {
- private UnloadTask entityDataUnload;
- private UnloadTask poiDataUnload;
-
-- public static final record UnloadTask(Completable<CompoundTag> completable, DelayedPrioritisedTask task) {}
-+ public static final record UnloadTask(CallbackCompletable<CompoundTag> completable, PrioritisedExecutor.PrioritisedTask task,
-+ LazyRunnable toRun) {}
-
-- public UnloadTask getUnloadTask(final RegionFileIOThread.RegionFileType type) {
-+ public UnloadTask getUnloadTask(final MoonriseRegionFileIO.RegionFileType type) {
- switch (type) {
- case CHUNK_DATA:
- return this.chunkDataUnload;
-@@ -803,7 +811,7 @@ public final class NewChunkHolder {
- }
- }
-
-- private void removeUnloadTask(final RegionFileIOThread.RegionFileType type) {
-+ private void removeUnloadTask(final MoonriseRegionFileIO.RegionFileType type) {
- switch (type) {
- case CHUNK_DATA: {
- this.chunkDataUnload = null;
-@@ -836,10 +844,10 @@ public final class NewChunkHolder {
- // chunk state
- this.currentChunk = null;
- this.currentGenStatus = null;
-- this.lastChunkCompletion = null;
- for (int i = 0; i < this.chunkCompletions.length; ++i) {
-- CHUNK_COMPLETION_ARRAY_HANDLE.setVolatile(this.chunkCompletions, i, (ChunkCompletion)null);
-+ CHUNK_COMPLETION_ARRAY_HANDLE.setRelease(this.chunkCompletions, i, (ChunkCompletion)null);
- }
-+ this.lastChunkCompletion = null;
- // entity chunk state
- this.entityChunk = null;
- this.pendingEntityChunk = null;
-@@ -851,22 +859,23 @@ public final class NewChunkHolder {
- this.priorityLocked = false;
-
- if (chunk != null) {
-- this.chunkDataUnload = new UnloadTask(new Completable<>(), new DelayedPrioritisedTask(PrioritisedExecutor.Priority.NORMAL));
-+ 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 Completable<>(), null);
-+ this.poiDataUnload = new UnloadTask(new CallbackCompletable<>(), null, null);
- }
- if (entityChunk != null) {
-- this.entityDataUnload = new UnloadTask(new Completable<>(), 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 RegionFileIOThread.RegionFileType type, final CompoundTag data) {
-+ void completeAsyncUnloadDataSave(final MoonriseRegionFileIO.RegionFileType type, final CompoundTag data) {
- if (data != null) {
-- RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type);
-+ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, data, type);
- }
-
- this.getUnloadTask(type).completable().complete(data);
-@@ -886,18 +895,19 @@ public final class NewChunkHolder {
- final ChunkEntitySlices entityChunk = state.entityChunk();
- final PoiChunk poiChunk = state.poiChunk();
-
-- final boolean shouldLevelChunkNotSave = ChunkSystemFeatures.forceNoSave(chunk);
-+ 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(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
-+ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null);
- }
-
- if (chunk instanceof LevelChunk levelChunk) {
-@@ -1066,6 +1076,9 @@ public final class NewChunkHolder {
- if (oldUnloaded != newUnloaded) {
- this.checkUnload();
- }
-+
-+ // Don't really have a choice but to place this hook here
-+ PlatformHooks.get().onChunkHolderTicketChange(this.world, this, oldLevel, newLevel);
- }
-
- static final int NEIGHBOUR_RADIUS = 2;
-@@ -1111,24 +1124,6 @@ public final class NewChunkHolder {
- private static final long CHUNK_LOADED_MASK_RAD1 = getLoadedMask(1);
- private static final long CHUNK_LOADED_MASK_RAD2 = getLoadedMask(2);
-
-- public static boolean areNeighboursFullLoaded(final long bitset, final int radius) {
-- switch (radius) {
-- case 0: {
-- return (bitset & CHUNK_LOADED_MASK_RAD0) == CHUNK_LOADED_MASK_RAD0;
-- }
-- case 1: {
-- return (bitset & CHUNK_LOADED_MASK_RAD1) == CHUNK_LOADED_MASK_RAD1;
-- }
-- case 2: {
-- return (bitset & CHUNK_LOADED_MASK_RAD2) == CHUNK_LOADED_MASK_RAD2;
-- }
--
-- default: {
-- throw new IllegalArgumentException("Radius not recognized: " + radius);
-- }
-- }
-- }
--
- // 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
-@@ -1363,6 +1358,17 @@ public final class NewChunkHolder {
- }
-
- 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);
-@@ -1386,7 +1392,7 @@ public final class NewChunkHolder {
- LOGGER.error("Failed to process chunk status callback", thr);
- }
- }
-- }, PrioritisedExecutor.Priority.HIGHEST);
-+ }, Priority.HIGHEST);
- }
-
- private final Reference2ObjectOpenHashMap<FullChunkStatus, List<Consumer<LevelChunk>>> fullStatusWaiters = new Reference2ObjectOpenHashMap<>();
-@@ -1414,7 +1420,7 @@ public final class NewChunkHolder {
- LOGGER.error("Failed to process chunk status callback", thr);
- }
- }
-- }, PrioritisedExecutor.Priority.HIGHEST);
-+ }, Priority.HIGHEST);
- }
-
- // note: must hold scheduling lock
-@@ -1670,6 +1676,8 @@ public final class NewChunkHolder {
-
- 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");
-
-@@ -1677,6 +1685,7 @@ public final class NewChunkHolder {
- 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
-@@ -1686,17 +1695,22 @@ public final class NewChunkHolder {
- poi = this.unloadState.poiChunk();
- entities = this.unloadState.entityChunk();
- }
-- final UnloadTask chunkUnloadTask = this.chunkDataUnload;
-- final DelayedPrioritisedTask chunkDataUnloadTask = chunkUnloadTask == null ? null : chunkUnloadTask.task();
-- if (chunkDataUnloadTask != null) {
-- final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnloadTask.getTask();
-- if (unloadTask != null) {
-- executedUnloadTask = unloadTask.execute();
-+ 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 = ChunkSystemFeatures.forceNoSave(chunk);
-+ 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()));
-@@ -1717,106 +1731,55 @@ public final class NewChunkHolder {
- }
- }
-
-- return executedUnloadTask | canSaveChunk | canSaveEntities | canSavePOI ? new SaveStat(executedUnloadTask || canSaveChunk, canSaveEntities, canSavePOI): null;
-- }
--
-- static final class AsyncChunkSerializeTask implements Runnable {
--
-- private final ServerLevel world;
-- private final ChunkAccess chunk;
-- private final AsyncChunkSaveData asyncSaveData;
-- private final NewChunkHolder toComplete;
--
-- public AsyncChunkSerializeTask(final ServerLevel world, final ChunkAccess chunk, final AsyncChunkSaveData asyncSaveData,
-- final NewChunkHolder toComplete) {
-- this.world = world;
-- this.chunk = chunk;
-- this.asyncSaveData = asyncSaveData;
-- this.toComplete = toComplete;
-- }
--
-- @Override
-- public void run() {
-- final CompoundTag toSerialize;
-- try {
-- toSerialize = ChunkSystemFeatures.saveChunkAsync(this.world, this.chunk, this.asyncSaveData);
-- } catch (final Throwable throwable) {
-- LOGGER.error("Failed to asynchronously save chunk " + this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", throwable);
-- final ChunkPos pos = this.chunk.getPos();
-- ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().scheduleChunkTask(pos.x, pos.z, () -> {
-- final CompoundTag synchronousSave;
-- try {
-- synchronousSave = ChunkSystemFeatures.saveChunkAsync(AsyncChunkSerializeTask.this.world, AsyncChunkSerializeTask.this.chunk, AsyncChunkSerializeTask.this.asyncSaveData);
-- } catch (final Throwable throwable2) {
-- LOGGER.error("Failed to synchronously save chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "', chunk data will be lost", throwable2);
-- AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
-- return;
-- }
--
-- AsyncChunkSerializeTask.this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, synchronousSave);
-- LOGGER.info("Successfully serialized chunk " + AsyncChunkSerializeTask.this.chunk.getPos() + " for world '" + WorldUtil.getWorldName(AsyncChunkSerializeTask.this.world) + "' synchronously");
--
-- }, PrioritisedExecutor.Priority.HIGHEST);
-- return;
-- }
-- this.toComplete.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, toSerialize);
-- }
--
-- @Override
-- public String toString() {
-- return "AsyncChunkSerializeTask{" +
-- "chunk={pos=" + this.chunk.getPos() + ",world=\"" + WorldUtil.getWorldName(this.world) + "\"}" +
-- "}";
-- }
-+ 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(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
-+ this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, null);
- }
- return false;
- }
-- boolean completing = false;
-- boolean failedAsyncPrepare = false;
- try {
-- if (unloading && ChunkSystemFeatures.supportsAsyncChunkSave()) {
-- try {
-- final AsyncChunkSaveData asyncSaveData = ChunkSystemFeatures.getAsyncSaveData(this.world, chunk);
-+ final SerializableChunkData chunkData = SerializableChunkData.copyOf(this.world, chunk);
-+ PlatformHooks.get().chunkSyncSave(this.world, chunk, chunkData);
-
-- final PrioritisedExecutor.PrioritisedTask task = this.scheduler.loadExecutor.createTask(new AsyncChunkSerializeTask(this.world, chunk, asyncSaveData, this));
-+ chunk.tryMarkSaved();
-
-- this.chunkDataUnload.task().setTask(task);
-+ final CallbackCompletable<CompoundTag> completable = new CallbackCompletable<>();
-
-- chunk.setUnsaved(false);
-+ final Runnable run = () -> {
-+ final CompoundTag data = chunkData.write();
-
-- task.queue();
-+ completable.complete(data);
-
-- return true;
-- } catch (final Throwable thr) {
-- LOGGER.error("Failed to prepare async chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "', falling back to synchronous save", thr);
-- failedAsyncPrepare = true;
-- // fall through to synchronous save
-+ if (unloading) {
-+ NewChunkHolder.this.completeAsyncUnloadDataSave(MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, data);
- }
-- }
--
-- final CompoundTag save = ChunkSerializer.write(this.world, chunk);
-+ };
-
-+ final PrioritisedExecutor.PrioritisedTask task;
- if (unloading) {
-- completing = true;
-- this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, save);
-- if (failedAsyncPrepare) {
-- LOGGER.info("Successfully serialized chunk data (" + this.chunkX + "," + this.chunkZ + ") in world '" + WorldUtil.getWorldName(this.world) + "' synchronously");
-- }
-+ this.chunkDataUnload.toRun().setRunnable(run);
-+ task = this.chunkDataUnload.task();
- } else {
-- RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.CHUNK_DATA);
-+ task = this.scheduler.saveExecutor.createTask(run);
- }
-- chunk.setUnsaved(false);
-+
-+ 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);
-- if (unloading && !completing) {
-- this.completeAsyncUnloadDataSave(RegionFileIOThread.RegionFileType.CHUNK_DATA, null);
-- }
- }
-
- return true;
-@@ -1834,7 +1797,7 @@ public final class NewChunkHolder {
- return false;
- }
- try {
-- mergeFrom = RegionFileIOThread.loadData(this.world, this.chunkX, this.chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, PrioritisedExecutor.Priority.BLOCKING);
-+ 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);
- }
-@@ -1853,7 +1816,7 @@ public final class NewChunkHolder {
- return false;
- }
-
-- RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.ENTITY_DATA);
-+ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA);
- this.lastEntitySaveNull = save == null;
- if (unloading) {
- this.lastEntityUnload = save;
-@@ -1877,7 +1840,7 @@ public final class NewChunkHolder {
- return false;
- }
-
-- RegionFileIOThread.scheduleSave(this.world, this.chunkX, this.chunkZ, save, RegionFileIOThread.RegionFileType.POI_DATA);
-+ MoonriseRegionFileIO.scheduleSave(this.world, this.chunkX, this.chunkZ, save, MoonriseRegionFileIO.RegionFileType.POI_DATA);
- this.lastPoiSaveNull = save == null;
- if (unloading) {
- this.poiDataUnload.completable().complete(save);
-@@ -1924,7 +1887,7 @@ public final class NewChunkHolder {
- return element == null ? JsonNull.INSTANCE : new JsonPrimitive(element.toString());
- }
-
-- private static JsonObject serializeCompletable(final Completable<?> completable) {
-+ private static JsonObject serializeCompletable(final CallbackCompletable<?> completable) {
- final JsonObject ret = new JsonObject();
-
- if (completable == null) {
-@@ -2019,13 +1982,13 @@ public final class NewChunkHolder {
- ret.add("poi_unload_completable", serializeCompletable(poiDataUnload == null ? null : poiDataUnload.completable()));
- ret.add("chunk_unload_completable", serializeCompletable(chunkDataUnload == null ? null : chunkDataUnload.completable()));
-
-- final DelayedPrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task();
-+ final PrioritisedExecutor.PrioritisedTask unloadTask = chunkDataUnload == null ? null : chunkDataUnload.task();
- if (unloadTask == null) {
- ret.addProperty("unload_task_priority", "null");
-- ret.addProperty("unload_task_priority_raw", "null");
-+ ret.addProperty("unload_task_suborder", Long.valueOf(0L));
- } else {
- ret.addProperty("unload_task_priority", Objects.toString(unloadTask.getPriority()));
-- ret.addProperty("unload_task_priority_raw", Integer.valueOf(unloadTask.getPriorityInternal()));
-+ ret.addProperty("unload_task_suborder", Long.valueOf(unloadTask.getSubOrder()));
- }
-
- ret.addProperty("killed", Boolean.valueOf(this.unloaded));
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
-index 261e09454f49d04eb159c984ec695d7c7aa6a3a8..6b468c621b74449a6218391f6477cf63cfc98c7c 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/PriorityHolder.java
-@@ -1,7 +1,7 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
- import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
- import java.lang.invoke.VarHandle;
-
- public abstract class PriorityHolder {
-@@ -28,8 +28,8 @@ public abstract class PriorityHolder {
- PRIORITY_HANDLE.set((PriorityHolder)this, (int)val);
- }
-
-- protected PriorityHolder(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ protected PriorityHolder(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
- this.setPriorityPlain(priority.priority);
-@@ -69,7 +69,7 @@ public abstract class PriorityHolder {
- return;
- }
-
-- this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority));
-+ this.scheduleTask(Priority.getPriority(priority));
-
- int failures = 0;
- for (;;) {
-@@ -86,7 +86,7 @@ public abstract class PriorityHolder {
- return;
- }
-
-- this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority));
-+ this.setPriorityScheduled(Priority.getPriority(priority));
-
- ++failures;
- for (int i = 0; i < failures; ++i) {
-@@ -95,19 +95,19 @@ public abstract class PriorityHolder {
- }
- }
-
-- public final PrioritisedExecutor.Priority getPriority() {
-+ public final Priority getPriority() {
- final int ret = this.getPriorityVolatile();
- if ((ret & PRIORITY_EXECUTED) != 0) {
-- return PrioritisedExecutor.Priority.COMPLETING;
-+ return Priority.COMPLETING;
- }
- if ((ret & PRIORITY_SCHEDULED) != 0) {
- return this.getScheduledPriority();
- }
-- return PrioritisedExecutor.Priority.getPriority(ret);
-+ return Priority.getPriority(ret);
- }
-
-- public final void lowerPriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public final void lowerPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
-
-@@ -139,8 +139,8 @@ public abstract class PriorityHolder {
- }
- }
-
-- public final void setPriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public final void setPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
-
-@@ -168,8 +168,8 @@ public abstract class PriorityHolder {
- }
- }
-
-- public final void raisePriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public final void raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
-
-@@ -203,13 +203,13 @@ public abstract class PriorityHolder {
-
- protected abstract void cancelScheduled();
-
-- protected abstract PrioritisedExecutor.Priority getScheduledPriority();
-+ protected abstract Priority getScheduledPriority();
-
-- protected abstract void scheduleTask(final PrioritisedExecutor.Priority priority);
-+ protected abstract void scheduleTask(final Priority priority);
-
-- protected abstract void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority);
-+ protected abstract void lowerPriorityScheduled(final Priority priority);
-
-- protected abstract void setPriorityScheduled(final PrioritisedExecutor.Priority priority);
-+ protected abstract void setPriorityScheduled(final Priority priority);
-
-- protected abstract void raisePriorityScheduled(final PrioritisedExecutor.Priority priority);
-+ protected abstract void raisePriorityScheduled(final Priority priority);
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
-index e0b26ccb63596748b80fc6a5e47e373ba811ba8b..5f4b99d8c5453f8ad2e600a57ea4e7dafa2d45f8 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/executor/RadiusAwarePrioritisedExecutor.java
-@@ -1,10 +1,10 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.executor;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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;
-@@ -16,15 +16,36 @@ public class RadiusAwarePrioritisedExecutor {
- return Long.compare(t1.id, t2.id);
- };
-
-- private final DependencyTree[] queues = new DependencyTree[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES];
-+ 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, 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() {
-@@ -56,7 +77,7 @@ public class RadiusAwarePrioritisedExecutor {
- return null;
- }
-
-- private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final PrioritisedExecutor.Priority priority) {
-+ private List<PrioritisedExecutor.PrioritisedTask> queue(final Task task, final Priority priority) {
- final int priorityId = priority.priority;
- final DependencyTree queue = this.queues[priorityId];
-
-@@ -79,7 +100,7 @@ public class RadiusAwarePrioritisedExecutor {
- return null;
- }
-
-- if (PrioritisedExecutor.Priority.isHigherPriority(priorityId, this.selectedQueue)) {
-+ if (Priority.isHigherPriority(priorityId, this.selectedQueue)) {
- // prevent the lower priority tree from queueing more tasks
- this.canQueueTasks = false;
- return null;
-@@ -90,7 +111,7 @@ public class RadiusAwarePrioritisedExecutor {
- }
-
- public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius,
-- final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ final Runnable run, final Priority priority) {
- if (radius < 0) {
- throw new IllegalArgumentException("Radius must be > 0: " + radius);
- }
-@@ -99,11 +120,11 @@ public class RadiusAwarePrioritisedExecutor {
-
- public PrioritisedExecutor.PrioritisedTask createTask(final int chunkX, final int chunkZ, final int radius,
- final Runnable run) {
-- return this.createTask(chunkX, chunkZ, radius, run, PrioritisedExecutor.Priority.NORMAL);
-+ 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 PrioritisedExecutor.Priority priority) {
-+ final Runnable run, final Priority priority) {
- final PrioritisedExecutor.PrioritisedTask ret = this.createTask(chunkX, chunkZ, radius, run, priority);
-
- ret.queue();
-@@ -120,15 +141,15 @@ public class RadiusAwarePrioritisedExecutor {
- return ret;
- }
-
-- public PrioritisedExecutor.PrioritisedTask createInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ 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, PrioritisedExecutor.Priority.NORMAL);
-+ return this.createInfiniteRadiusTask(run, Priority.NORMAL);
- }
-
-- public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run, final Priority priority) {
- final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, priority);
-
- ret.queue();
-@@ -137,20 +158,27 @@ public class RadiusAwarePrioritisedExecutor {
- }
-
- public PrioritisedExecutor.PrioritisedTask queueInfiniteRadiusTask(final Runnable run) {
-- final PrioritisedExecutor.PrioritisedTask ret = this.createInfiniteRadiusTask(run, PrioritisedExecutor.Priority.NORMAL);
-+ 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 final int maxToSchedule;
-- private final int treeIndex;
-+ private int maxToSchedule;
-
- private int currentlyExecuting;
- private long idGenerator;
-@@ -163,11 +191,10 @@ public class RadiusAwarePrioritisedExecutor {
- private final Long2ReferenceOpenHashMap<DependencyNode> nodeByPosition = new Long2ReferenceOpenHashMap<>();
-
- public DependencyTree(final RadiusAwarePrioritisedExecutor scheduler, final PrioritisedExecutor executor,
-- final int maxToSchedule, final int treeIndex) {
-+ final int maxToSchedule) {
- this.scheduler = scheduler;
- this.executor = executor;
- this.maxToSchedule = maxToSchedule;
-- this.treeIndex = treeIndex;
- }
-
- public boolean hasWaitingTasks() {
-@@ -412,13 +439,13 @@ public class RadiusAwarePrioritisedExecutor {
- private final int chunkZ;
- private final int radius;
- private Runnable run;
-- private PrioritisedExecutor.Priority priority;
-+ 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 PrioritisedExecutor.Priority priority) {
-+ final Runnable run, final Priority priority) {
- this.scheduler = scheduler;
- this.chunkX = chunkX;
- this.chunkZ = chunkZ;
-@@ -441,14 +468,6 @@ public class RadiusAwarePrioritisedExecutor {
- run.run();
- }
-
-- 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();
-- }
-- }
-- }
--
- private void returnNode() {
- final List<PrioritisedExecutor.PrioritisedTask> toSchedule;
- synchronized (this.scheduler) {
-@@ -460,6 +479,11 @@ public class RadiusAwarePrioritisedExecutor {
- scheduleTasks(toSchedule);
- }
-
-+ @Override
-+ public PrioritisedExecutor getExecutor() {
-+ return this.scheduler.executor;
-+ }
-+
- @Override
- public void run() {
- final Runnable run = this.run;
-@@ -475,7 +499,7 @@ public class RadiusAwarePrioritisedExecutor {
- public boolean queue() {
- final List<PrioritisedExecutor.PrioritisedTask> toSchedule;
- synchronized (this.scheduler) {
-- if (this.queuedTask != null || this.dependencyNode != null || this.priority == PrioritisedExecutor.Priority.COMPLETING) {
-+ if (this.queuedTask != null || this.dependencyNode != null || this.priority == Priority.COMPLETING) {
- return false;
- }
-
-@@ -486,16 +510,23 @@ public class RadiusAwarePrioritisedExecutor {
- 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 == PrioritisedExecutor.Priority.COMPLETING) {
-+ if (this.priority == Priority.COMPLETING) {
- return false;
- }
-
-- this.priority = PrioritisedExecutor.Priority.COMPLETING;
-+ this.priority = Priority.COMPLETING;
- if (this.dependencyNode != null) {
- this.dependencyNode.purged = true;
- this.dependencyNode = null;
-@@ -519,11 +550,11 @@ public class RadiusAwarePrioritisedExecutor {
- final PrioritisedExecutor.PrioritisedTask task;
- synchronized (this.scheduler) {
- if ((task = this.queuedTask) == null) {
-- if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
-+ if (this.priority == Priority.COMPLETING) {
- return false;
- }
-
-- this.priority = PrioritisedExecutor.Priority.COMPLETING;
-+ this.priority = Priority.COMPLETING;
- if (this.dependencyNode != null) {
- this.dependencyNode.purged = true;
- this.dependencyNode = null;
-@@ -543,7 +574,7 @@ public class RadiusAwarePrioritisedExecutor {
- }
-
- @Override
-- public PrioritisedExecutor.Priority getPriority() {
-+ public Priority getPriority() {
- final PrioritisedExecutor.PrioritisedTask task;
- synchronized (this.scheduler) {
- if ((task = this.queuedTask) == null) {
-@@ -555,8 +586,8 @@ public class RadiusAwarePrioritisedExecutor {
- }
-
- @Override
-- public boolean setPriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public boolean setPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
-
-@@ -564,7 +595,7 @@ public class RadiusAwarePrioritisedExecutor {
- List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
- synchronized (this.scheduler) {
- if ((task = this.queuedTask) == null) {
-- if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
-+ if (this.priority == Priority.COMPLETING) {
- return false;
- }
-
-@@ -592,8 +623,8 @@ public class RadiusAwarePrioritisedExecutor {
- }
-
- @Override
-- public boolean raisePriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public boolean raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
-
-@@ -601,7 +632,7 @@ public class RadiusAwarePrioritisedExecutor {
- List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
- synchronized (this.scheduler) {
- if ((task = this.queuedTask) == null) {
-- if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
-+ if (this.priority == Priority.COMPLETING) {
- return false;
- }
-
-@@ -629,8 +660,8 @@ public class RadiusAwarePrioritisedExecutor {
- }
-
- @Override
-- public boolean lowerPriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public boolean lowerPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
-
-@@ -638,7 +669,7 @@ public class RadiusAwarePrioritisedExecutor {
- List<PrioritisedExecutor.PrioritisedTask> toSchedule = null;
- synchronized (this.scheduler) {
- if ((task = this.queuedTask) == null) {
-- if (this.priority == PrioritisedExecutor.Priority.COMPLETING) {
-+ if (this.priority == Priority.COMPLETING) {
- return false;
- }
-
-@@ -664,5 +695,35 @@ public class RadiusAwarePrioritisedExecutor {
-
- 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);
-+ }
- }
--}
-\ No newline at end of file
-+}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
-index fbdf721e8b4cfe6cef4ee60c53c680cbfc858d88..6ab353b0d2465c3680bb3c8d0852ba0f65c00fd2 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkFullTask.java
-@@ -1,7 +1,9 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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;
-@@ -29,7 +31,7 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl
- 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 PrioritisedExecutor.Priority priority) {
-+ final NewChunkHolder chunkHolder, final ChunkAccess fromChunk, final Priority priority) {
- super(scheduler, world, chunkX, chunkZ);
- this.chunkHolder = chunkHolder;
- this.fromChunk = fromChunk;
-@@ -43,6 +45,8 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl
-
- @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 {
-@@ -61,7 +65,7 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl
- final ServerLevel world = this.world;
- final ProtoChunk protoChunk = (ProtoChunk)this.fromChunk;
- chunk = new LevelChunk(this.world, protoChunk, (final LevelChunk unused) -> {
-- ChunkStatusTasks.postLoadProtoChunk(world, protoChunk.getEntities(), protoChunk.getPos()); // Paper - pass chunk pos
-+ PlatformHooks.get().postLoadProtoChunk(world, protoChunk);
- });
- this.chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false));
- }
-@@ -71,16 +75,21 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl
- final NewChunkHolder chunkHolder = this.chunkHolder;
-
- chunk.setFullStatus(chunkHolder::getChunkStatus);
-- 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);
--
-- // we don't need the entitiesInLevel, not sure why it's there
-- chunk.setLoaded(true);
-- chunk.registerAllBlockEntitiesAfterLevelLoad();
-- chunk.registerTickContainerInLevel(this.world);
-+ 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;
-@@ -112,29 +121,29 @@ public final class ChunkFullTask extends ChunkProgressionTask implements Runnabl
- }
-
- @Override
-- public PrioritisedExecutor.Priority getPriority() {
-+ public Priority getPriority() {
- return this.convertToFullTask.getPriority();
- }
-
- @Override
-- public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ 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 PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ 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 PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public void raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
- this.convertToFullTask.raisePriority(priority);
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
-index 7c2e6752228fac175c4aa97fa3d817b8a938922f..4538ccfaea83d217ed85eaf16e82393c7f286489 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLightTask.java
-@@ -1,6 +1,6 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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;
-@@ -25,9 +25,9 @@ public final class ChunkLightTask extends ChunkProgressionTask {
- private final LightTaskPriorityHolder priorityHolder;
-
- public ChunkLightTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX, final int chunkZ,
-- final ChunkAccess chunk, final PrioritisedExecutor.Priority priority) {
-+ final ChunkAccess chunk, final Priority priority) {
- super(scheduler, world, chunkX, chunkZ);
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
- this.priorityHolder = new LightTaskPriorityHolder(priority, this);
-@@ -55,22 +55,22 @@ public final class ChunkLightTask extends ChunkProgressionTask {
- }
-
- @Override
-- public PrioritisedExecutor.Priority getPriority() {
-+ public Priority getPriority() {
- return this.priorityHolder.getPriority();
- }
-
- @Override
-- public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ public void lowerPriority(final Priority priority) {
- this.priorityHolder.raisePriority(priority);
- }
-
- @Override
-- public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ public void setPriority(final Priority priority) {
- this.priorityHolder.setPriority(priority);
- }
-
- @Override
-- public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ public void raisePriority(final Priority priority) {
- this.priorityHolder.raisePriority(priority);
- }
-
-@@ -78,7 +78,7 @@ public final class ChunkLightTask extends ChunkProgressionTask {
-
- private final ChunkLightTask task;
-
-- private LightTaskPriorityHolder(final PrioritisedExecutor.Priority priority, final ChunkLightTask task) {
-+ private LightTaskPriorityHolder(final Priority priority, final ChunkLightTask task) {
- super(priority);
- this.task = task;
- }
-@@ -90,13 +90,13 @@ public final class ChunkLightTask extends ChunkProgressionTask {
- }
-
- @Override
-- protected PrioritisedExecutor.Priority getScheduledPriority() {
-+ 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 PrioritisedExecutor.Priority priority) {
-+ 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();
-@@ -105,7 +105,7 @@ public final class ChunkLightTask extends ChunkProgressionTask {
- }
-
- @Override
-- protected void lowerPriorityScheduled(final PrioritisedExecutor.Priority priority) {
-+ 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();
-@@ -113,7 +113,7 @@ public final class ChunkLightTask extends ChunkProgressionTask {
- }
-
- @Override
-- protected void setPriorityScheduled(final PrioritisedExecutor.Priority priority) {
-+ 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();
-@@ -121,7 +121,7 @@ public final class ChunkLightTask extends ChunkProgressionTask {
- }
-
- @Override
-- protected void raisePriorityScheduled(final PrioritisedExecutor.Priority priority) {
-+ 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();
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
-index 1ab93f219246d0b4dcdfd0f685f47c13091425f8..e0a88615a8b6d58191f29b1ff1a26427f0a4c1a6 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkLoadTask.java
-@@ -1,12 +1,13 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-
- import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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.ChunkSystemFeatures;
--import ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread;
-+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;
-@@ -18,7 +19,7 @@ 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.ChunkSerializer;
-+import net.minecraft.world.level.chunk.storage.SerializableChunkData;
- import net.minecraft.world.level.levelgen.blending.BlendingData;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-@@ -41,7 +42,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- 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 PrioritisedExecutor.Priority priority) {
-+ final NewChunkHolder chunkHolder, final Priority priority) {
- super(scheduler, world, chunkX, chunkZ);
- this.chunkHolder = chunkHolder;
- this.loadTask = new ChunkDataLoadTask(scheduler, world, chunkX, chunkZ, priority);
-@@ -170,12 +171,12 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- }
-
- @Override
-- public PrioritisedExecutor.Priority getPriority() {
-+ public Priority getPriority() {
- return this.loadTask.getPriority();
- }
-
- @Override
-- public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ public void lowerPriority(final Priority priority) {
- final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
- if (entityLoad != null) {
- entityLoad.lowerPriority(priority);
-@@ -191,7 +192,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- }
-
- @Override
-- public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ public void setPriority(final Priority priority) {
- final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
- if (entityLoad != null) {
- entityLoad.setPriority(priority);
-@@ -207,7 +208,7 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- }
-
- @Override
-- public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ public void raisePriority(final Priority priority) {
- final EntityDataLoadTask entityLoad = this.chunkHolder.getEntityDataLoadTask();
- if (entityLoad != null) {
- entityLoad.raisePriority(priority);
-@@ -231,8 +232,8 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- 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 RegionFileIOThread.RegionFileType type,
-- final PrioritisedExecutor.Priority priority) {
-+ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type,
-+ final Priority priority) {
- super(scheduler, world, chunkX, chunkZ, type, priority);
- }
-
-@@ -272,10 +273,13 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- }
- }
-
-- private static final class ChunkDataLoadTask extends CallbackDataLoadTask<CompoundTag, ChunkAccess> {
-+
-+ 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 PrioritisedExecutor.Priority priority) {
-- super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.CHUNK_DATA, priority);
-+ final int chunkZ, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.CHUNK_DATA, priority);
- }
-
- @Override
-@@ -289,40 +293,42 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- }
-
- @Override
-- protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ 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 PrioritisedExecutor.Priority priority) {
-+ 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 CompoundTag data, final Throwable throwable) {
-+ protected TaskResult<ChunkAccess, Throwable> completeOnMainOffMain(final ReadChunk data, final Throwable throwable) {
- if (throwable != null) {
- return new TaskResult<>(null, throwable);
- }
-- if (data == null) {
-+
-+ if (data == null || data.protoChunk() == null) {
- return new TaskResult<>(this.getEmptyChunk(), null);
- }
-
-- if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) {
-- return this.deserialize(data);
-+ if (!PlatformHooks.get().hasMainChunkLoadHook()) {
-+ return new TaskResult<>(data.protoChunk(), null);
- }
-- // need to deserialize on main thread
-+
-+ // 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().registryOrThrow(Registries.BIOME), (BlendingData)null
-+ this.world.registryAccess().lookupOrThrow(Registries.BIOME), (BlendingData)null
- );
- }
-
- @Override
-- protected TaskResult<CompoundTag, Throwable> runOffMain(final CompoundTag data, final Throwable throwable) {
-+ 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);
-@@ -334,42 +340,43 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
-
- try {
- // run converters
-- final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data, new net.minecraft.world.level.ChunkPos(this.chunkX, this.chunkZ));
-+ final CompoundTag converted = this.world.getChunkSource().chunkMap.upgradeChunkTag(data);
-
-- return new TaskResult<>(converted, 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);
-- }
-- }
-+ // unpack the data
-+ final SerializableChunkData chunkData = SerializableChunkData.parse(
-+ this.world, this.world.registryAccess(), converted
-+ );
-
-- private TaskResult<ChunkAccess, Throwable> deserialize(final CompoundTag data) {
-- try {
-- final ChunkAccess deserialized = ChunkSerializer.read(
-- this.world, this.world.getPoiManager(), this.world.getChunkSource().chunkMap.storageInfo(), new ChunkPos(this.chunkX, this.chunkZ), data
-+ 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<>(deserialized, null);
-+
-+ 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<>(this.getEmptyChunk(), null);
-+ return new TaskResult<>(null, null);
- }
- }
-
- @Override
-- protected TaskResult<ChunkAccess, Throwable> runOnMain(final CompoundTag data, final Throwable throwable) {
-- // data != null && throwable == null
-- if (ChunkSystemFeatures.supportsAsyncChunkDeserialization()) {
-- throw new UnsupportedOperationException();
-- }
-- return this.deserialize(data);
-+ 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 PrioritisedExecutor.Priority priority) {
-- super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.POI_DATA, priority);
-+ final int chunkZ, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.POI_DATA, priority);
- }
-
- @Override
-@@ -383,12 +390,12 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- }
-
- @Override
-- protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ 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 PrioritisedExecutor.Priority priority) {
-+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) {
- throw new UnsupportedOperationException();
- }
-
-@@ -430,8 +437,8 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- public static final class EntityDataLoadTask extends CallbackDataLoadTask<CompoundTag, CompoundTag> {
-
- public EntityDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-- final int chunkZ, final PrioritisedExecutor.Priority priority) {
-- super(scheduler, world, chunkX, chunkZ, RegionFileIOThread.RegionFileType.ENTITY_DATA, priority);
-+ final int chunkZ, final Priority priority) {
-+ super(scheduler, world, chunkX, chunkZ, MoonriseRegionFileIO.RegionFileType.ENTITY_DATA, priority);
- }
-
- @Override
-@@ -445,12 +452,12 @@ public final class ChunkLoadTask extends ChunkProgressionTask {
- }
-
- @Override
-- protected PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority) {
-+ 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 PrioritisedExecutor.Priority priority) {
-+ protected PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final Priority priority) {
- throw new UnsupportedOperationException();
- }
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
-index 70e900b0f9c131900bf8b3f3ecbfbd5df5361205..002ee365aa70d8e6a6e6bd5c95988bd17db4395a 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkProgressionTask.java
-@@ -1,8 +1,8 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-
- import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
--import ca.spottedleaf.concurrentutil.executor.standard.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.scheduling.ChunkTaskScheduler;
- import net.minecraft.server.level.ServerLevel;
-@@ -46,15 +46,15 @@ public abstract class ChunkProgressionTask {
- /* May be called multiple times */
- public abstract void cancel();
-
-- public abstract PrioritisedExecutor.Priority getPriority();
-+ public abstract Priority getPriority();
-
- /* Schedule lock is always held for the priority update calls */
-
-- public abstract void lowerPriority(final PrioritisedExecutor.Priority priority);
-+ public abstract void lowerPriority(final Priority priority);
-
-- public abstract void setPriority(final PrioritisedExecutor.Priority priority);
-+ public abstract void setPriority(final Priority priority);
-
-- public abstract void raisePriority(final PrioritisedExecutor.Priority priority);
-+ public abstract void raisePriority(final Priority priority);
-
- public final void onComplete(final BiConsumer<ChunkAccess, Throwable> onComplete) {
- if (!this.waiters.add(onComplete)) {
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
-index 2c17d5589f15f1155be08be670d29acbe954a8fa..25d8da4773dcee5096053e7e3788bfc224d705a7 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/ChunkUpgradeGenericStatusTask.java
-@@ -1,7 +1,8 @@
- package ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task;
-
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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;
-@@ -36,9 +37,9 @@ public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask im
-
- 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 PrioritisedExecutor.Priority priority) {
-+ final ChunkStatus toStatus, final Priority priority) {
- super(scheduler, world, chunkX, chunkZ);
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
- this.fromChunk = chunk;
-@@ -187,29 +188,29 @@ public final class ChunkUpgradeGenericStatusTask extends ChunkProgressionTask im
- }
-
- @Override
-- public PrioritisedExecutor.Priority getPriority() {
-+ public Priority getPriority() {
- return this.generateTask.getPriority();
- }
-
- @Override
-- public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ 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 PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ 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 PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public void raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
- this.generateTask.raisePriority(priority);
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
-index 7a65d351b448873c6f2c145c975c92be314b876c..bdcd1879457bafcca4e76523aac0555968f37c0b 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/task/GenericDataLoadTask.java
-@@ -1,12 +1,13 @@
- 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.standard.DelayedPrioritisedTask;
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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.RegionFileIOThread;
-+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;
-@@ -47,11 +48,11 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- protected final ServerLevel world;
- protected final int chunkX;
- protected final int chunkZ;
-- protected final RegionFileIOThread.RegionFileType type;
-+ protected final MoonriseRegionFileIO.RegionFileType type;
-
- public GenericDataLoadTask(final ChunkTaskScheduler scheduler, final ServerLevel world, final int chunkX,
-- final int chunkZ, final RegionFileIOThread.RegionFileType type,
-- final PrioritisedExecutor.Priority priority) {
-+ final int chunkZ, final MoonriseRegionFileIO.RegionFileType type,
-+ final Priority priority) {
- this.scheduler = scheduler;
- this.world = world;
- this.chunkX = chunkX;
-@@ -89,9 +90,9 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
-
- protected abstract boolean hasOnMain();
-
-- protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final PrioritisedExecutor.Priority priority);
-+ protected abstract PrioritisedExecutor.PrioritisedTask createOffMain(final Runnable run, final Priority priority);
-
-- protected abstract PrioritisedExecutor.PrioritisedTask createOnMain(final Runnable run, final PrioritisedExecutor.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);
-
-@@ -108,7 +109,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- ", type: " + this.type.toString() + "}";
- }
-
-- public PrioritisedExecutor.Priority getPriority() {
-+ public Priority getPriority() {
- if (this.processOnMain != null) {
- return this.processOnMain.getPriority();
- } else {
-@@ -116,7 +117,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- }
- }
-
-- public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ 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);
-@@ -126,7 +127,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- }
- }
-
-- public void setPriority(final PrioritisedExecutor.Priority 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) {
-@@ -137,7 +138,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- }
- }
-
-- public void raisePriority(final PrioritisedExecutor.Priority 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) {
-@@ -382,10 +383,10 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- private final int chunkX;
- private final int chunkZ;
-
-- private final RegionFileIOThread.RegionFileType type;
-+ private final MoonriseRegionFileIO.RegionFileType type;
- private Cancellable dataLoadTask;
- private Cancellable dataUnloadCancellable;
-- private DelayedPrioritisedTask dataUnloadTask;
-+ private PrioritisedExecutor.PrioritisedTask dataUnloadTask;
-
- private final BiConsumer<CompoundTag, Throwable> onComplete;
- private final AtomicBoolean scheduled = new AtomicBoolean();
-@@ -393,10 +394,10 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- // 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 RegionFileIOThread.RegionFileType type,
-+ final MoonriseRegionFileIO.RegionFileType type,
- final BiConsumer<CompoundTag, Throwable> onComplete,
-- final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
- this.world = world;
-@@ -426,8 +427,8 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- return (this.getPriorityVolatile() & PRIORITY_EXECUTED) != 0;
- }
-
-- public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public void lowerPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
-
-@@ -439,7 +440,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- }
-
- if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-- RegionFileIOThread.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
-+ MoonriseRegionFileIO.lowerPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
- return;
- }
-
-@@ -467,8 +468,8 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- }
- }
-
-- public void setPriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public void setPriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
-
-@@ -480,7 +481,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- }
-
- if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-- RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
-+ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
- return;
- }
-
-@@ -504,8 +505,8 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- }
- }
-
-- public void raisePriority(final PrioritisedExecutor.Priority priority) {
-- if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
-+ public void raisePriority(final Priority priority) {
-+ if (!Priority.isValidPriority(priority)) {
- throw new IllegalArgumentException("Invalid priority " + priority);
- }
-
-@@ -517,7 +518,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- }
-
- if ((curr & PRIORITY_LOAD_SCHEDULED) != 0) {
-- RegionFileIOThread.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
-+ MoonriseRegionFileIO.raisePriority(this.world, this.chunkX, this.chunkZ, this.type, priority);
- return;
- }
-
-@@ -583,7 +584,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- } // else: cancelled
- };
-
-- final PrioritisedExecutor.Priority initialPriority = PrioritisedExecutor.Priority.getPriority(priority);
-+ final Priority initialPriority = Priority.getPriority(priority);
- boolean scheduledUnload = false;
-
- final NewChunkHolder holder = ((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.chunkX, this.chunkZ);
-@@ -593,13 +594,13 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- consumer.accept(data, null);
- } else {
- // need to schedule task
-- LoadDataFromDiskTask.this.schedule(false, consumer, PrioritisedExecutor.Priority.getPriority(LoadDataFromDiskTask.this.getPriorityVolatile() & ~PRIORITY_FLAGS));
-+ 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 Completable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable();
-+ final CallbackCompletable<CompoundTag> unloadCompletable = unloadTask == null ? null : unloadTask.completable();
- if (unloadCompletable != null) {
- unloadCancellable = unloadCompletable.addAsynchronousWaiter(unloadConsumer);
- if (unloadCancellable == null) {
-@@ -622,7 +623,7 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- this.schedule(scheduledUnload, consumer, initialPriority);
- }
-
-- private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final PrioritisedExecutor.Priority initialPriority) {
-+ private void schedule(final boolean scheduledUnload, final BiConsumer<CompoundTag, Throwable> consumer, final Priority initialPriority) {
- int priority = this.getPriorityVolatile();
-
- if ((priority & PRIORITY_EXECUTED) != 0) {
-@@ -631,9 +632,9 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
- }
-
- if (!scheduledUnload) {
-- this.dataLoadTask = RegionFileIOThread.loadDataAsync(
-+ this.dataLoadTask = MoonriseRegionFileIO.loadDataAsync(
- this.world, this.chunkX, this.chunkZ, this.type, consumer,
-- initialPriority.isHigherPriority(PrioritisedExecutor.Priority.NORMAL), initialPriority
-+ initialPriority.isHigherPriority(Priority.NORMAL), initialPriority
- );
- }
-
-@@ -657,10 +658,10 @@ public abstract class GenericDataLoadTask<OnMain,FinalCompletion> {
-
- if (scheduledUnload) {
- if (this.dataUnloadTask != null) {
-- this.dataUnloadTask.setPriority(PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
-+ this.dataUnloadTask.setPriority(Priority.getPriority(priority & ~PRIORITY_FLAGS));
- }
- } else {
-- RegionFileIOThread.setPriority(this.world, this.chunkX, this.chunkZ, this.type, PrioritisedExecutor.Priority.getPriority(priority & ~PRIORITY_FLAGS));
-+ MoonriseRegionFileIO.setPriority(this.world, this.chunkX, this.chunkZ, this.type, Priority.getPriority(priority & ~PRIORITY_FLAGS));
- }
-
- ++failures;
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..51c126735ace8fdde89ad97b5cab62f244212db0
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..3bd1b59250dbab15097a64d515999b278636795a
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
-index 3a9a564edfdb99e006e4816cb8821bd1e9ecff43..93fd23027c00cef76562098306737272fda1350a 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/ParallelSearchRadiusIteration.java
-@@ -1,6 +1,7 @@
- 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;
-@@ -13,7 +14,7 @@ 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[64+2+1][];
-+ 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
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..7ef3dcca89ed7578c6c0f5565131889110063056
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-index 748ab4d637ce463272bae4fdbab6842a27385126..3abd4ad6379c383c3a31931255292b42d9435694 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-@@ -1,15 +1,58 @@
- 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.item.Item;
-+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 it.unimi.dsi.fastutil.doubles.DoubleArrayList ZERO_ONE = it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(new double[] { 0.0, 1.0 });
-+ 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() == net.minecraft.world.level.block.Blocks.MOVING_PISTON;
-+ return block.hasLargeCollisionShape() || block.getBlock() == Blocks.MOVING_PISTON;
- }
-
-- public static boolean isEmpty(final net.minecraft.world.phys.AABB aabb) {
-+ 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;
- }
-
-@@ -18,11 +61,11 @@ public final class CollisionUtil {
- return (maxX - minX) < COLLISION_EPSILON || (maxY - minY) < COLLISION_EPSILON || (maxZ - minZ) < COLLISION_EPSILON;
- }
-
-- public static net.minecraft.world.phys.AABB getBoxForChunk(final int chunkX, final int chunkZ) {
-+ 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 net.minecraft.world.phys.AABB(x - 3*COLLISION_EPSILON, Double.NEGATIVE_INFINITY, z - 3*COLLISION_EPSILON,
-+ 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));
- }
-
-@@ -43,21 +86,21 @@ public final class CollisionUtil {
- (minZ1 - maxZ2) < -COLLISION_EPSILON && (maxZ1 - minZ2) > COLLISION_EPSILON;
- }
-
-- public static boolean voxelShapeIntersect(final net.minecraft.world.phys.AABB box, final double minX, final double minY, final double minZ,
-+ 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 net.minecraft.world.phys.AABB box1, final net.minecraft.world.phys.AABB box2) {
-+ 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 net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) {
-+ 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) {
-@@ -78,7 +121,7 @@ public final class CollisionUtil {
- }
-
- // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-- public static double collideY(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) {
-+ 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) {
-@@ -99,7 +142,7 @@ public final class CollisionUtil {
- }
-
- // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON
-- public static double collideZ(final net.minecraft.world.phys.AABB target, final net.minecraft.world.phys.AABB source, final double source_move) {
-+ 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) {
-@@ -121,7 +164,8 @@ public final class CollisionUtil {
-
- // startIndex and endIndex inclusive
- // assumes indices are in range of array
-- private static int findFloor(final double[] values, final double value, int startIndex, int endIndex) {
-+ public static int findFloor(final double[] values, 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];
-@@ -136,7 +180,217 @@ public final class CollisionUtil {
- return startIndex - 1;
- }
-
-- public static boolean voxelShapeIntersectNoEmpty(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.AABB aabb) {
-+ 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;
- }
-@@ -144,15 +398,15 @@ public final class CollisionUtil {
- // 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetX();
-- final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetY();
-- final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetZ();
-+ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesX();
-- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesY();
-- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ();
-+ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
-
- // note: size = coords.length - 1
- final int size_x = cached_shape_data.sizeX();
-@@ -246,23 +500,23 @@ public final class CollisionUtil {
- }
-
- // assume !target.isEmpty() && abs(source_move) >= COLLISION_EPSILON
-- public static double collideX(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) {
-- final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation();
-+ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX();
-- final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY();
-- final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ();
-+ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX();
-- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY();
-- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ();
-+ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData();
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData();
-
- // note: size = coords.length - 1
- final int size_x = cached_shape_data.sizeX();
-@@ -404,23 +658,23 @@ public final class CollisionUtil {
- }
- }
-
-- public static double collideY(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) {
-- final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation();
-+ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX();
-- final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY();
-- final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ();
-+ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX();
-- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY();
-- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ();
-+ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData();
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData();
-
- // note: size = coords.length - 1
- final int size_x = cached_shape_data.sizeX();
-@@ -562,23 +816,23 @@ public final class CollisionUtil {
- }
- }
-
-- public static double collideZ(final net.minecraft.world.phys.shapes.VoxelShape target, final net.minecraft.world.phys.AABB source, final double source_move) {
-- final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getSingleAABBRepresentation();
-+ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetX();
-- final double off_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetY();
-- final double off_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$offsetZ();
-+ 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 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesX();
-- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesY();
-- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$rootCoordinatesZ();
-+ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)target).moonrise$getCachedVoxelData();
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)target).moonrise$getCachedVoxelData();
-
- // note: size = coords.length - 1
- final int size_x = cached_shape_data.sizeX();
-@@ -721,13 +975,13 @@ public final class CollisionUtil {
- }
-
- // does not use epsilon
-- public static boolean strictlyContains(final net.minecraft.world.phys.shapes.VoxelShape voxel, final net.minecraft.world.phys.Vec3 point) {
-+ 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 net.minecraft.world.phys.shapes.VoxelShape voxel, double x, double y, double z) {
-- final net.minecraft.world.phys.AABB single_aabb = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation();
-+ public static boolean strictlyContains(final VoxelShape voxel, double x, double y, double z) {
-+ final AABB single_aabb = ((CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation();
- if (single_aabb != null) {
- return single_aabb.contains(x, y, z);
- }
-@@ -738,15 +992,15 @@ public final class CollisionUtil {
- }
-
- // offset input
-- x -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetX();
-- y -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetY();
-- z -= ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$offsetZ();
-+ x -= ((CollisionVoxelShape)voxel).moonrise$offsetX();
-+ y -= ((CollisionVoxelShape)voxel).moonrise$offsetY();
-+ z -= ((CollisionVoxelShape)voxel).moonrise$offsetZ();
-
-- final double[] coords_x = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesX();
-- final double[] coords_y = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesY();
-- final double[] coords_z = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$rootCoordinatesZ();
-+ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cached_shape_data = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
-+ final CachedShapeData cached_shape_data = ((CollisionVoxelShape)voxel).moonrise$getCachedVoxelData();
-
- // note: size = coords.length - 1
- final int size_x = cached_shape_data.sizeX();
-@@ -788,10 +1042,10 @@ public final class CollisionUtil {
- return ((ft ? 1 : 0) << 1) | ((tf ? 1 : 0) << 2) | ((tt ? 1 : 0) << 3);
- }
-
-- private static net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape merge(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst, final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond,
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX, final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY,
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ,
-- final int booleanOp) {
-+ 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;
-@@ -806,7 +1060,7 @@ public final class CollisionUtil {
- final int s2Mul2 = s2Mul1 * shapeDataSecond.sizeY();
-
- // note: indices may contain -1, but nothing > size
-- final net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape ret = new net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ);
-+ final BitSetDiscreteVoxelShape ret = new BitSetDiscreteVoxelShape(sizeX, sizeY, sizeZ);
-
- boolean empty = true;
-
-@@ -823,10 +1077,11 @@ public final class CollisionUtil {
- final int s1z = mergedZ.firstIndices[idxZ];
- final int s2z = mergedZ.secondIndices[idxZ];
-
-- int idx;
-+ int idx1;
-+ int idx2;
-
-- final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L);
-- final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L);
-+ 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
-@@ -861,9 +1116,9 @@ public final class CollisionUtil {
- return empty ? null : ret;
- }
-
-- private static boolean isMergeEmpty(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst, final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond,
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX, final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY,
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ,
-+ 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;
-@@ -889,10 +1144,11 @@ public final class CollisionUtil {
- final int s1z = mergedZ.firstIndices[idxZ];
- final int s2z = mergedZ.secondIndices[idxZ];
-
-- int idx;
-+ int idx1;
-+ int idx2;
-
-- final int isS1Full = (s1x | s1y | s1z) < 0 ? 0 : (int)((s1Voxels[(idx = s1z + s1y*s1Mul1 + s1x*s1Mul2) >>> 6] >>> idx) & 1L);
-- final int isS2Full = (s2x | s2y | s2z) < 0 ? 0 : (int)((s2Voxels[(idx = s2z + s2y*s2Mul1 + s2x*s2Mul2) >>> 6] >>> idx) & 1L);
-+ 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
-@@ -911,11 +1167,11 @@ public final class CollisionUtil {
- return true;
- }
-
-- public static net.minecraft.world.phys.shapes.VoxelShape joinOptimized(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) {
-+ public static VoxelShape joinOptimized(final VoxelShape first, final VoxelShape second, final BooleanOp operator) {
- return joinUnoptimized(first, second, operator).optimize();
- }
-
-- public static net.minecraft.world.phys.shapes.VoxelShape joinUnoptimized(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) {
-+ 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
-@@ -925,23 +1181,23 @@ public final class CollisionUtil {
- final boolean tt = operator.apply(true, true);
-
- if (first == second) {
-- return tt ? first : net.minecraft.world.phys.shapes.Shapes.empty();
-+ 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 : net.minecraft.world.phys.shapes.Shapes.empty();
-+ return ft ? second : Shapes.empty();
- }
- if (second.isEmpty()) {
-- return tf ? first : net.minecraft.world.phys.shapes.Shapes.empty();
-+ return tf ? first : Shapes.empty();
- }
-
- if (!tt) {
- // try to check for no intersection, since tt = false
-- final net.minecraft.world.phys.AABB aabbF = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
-- final net.minecraft.world.phys.AABB aabbS = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
-+ final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
-+ final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
-
- final boolean intersect;
-
-@@ -962,7 +1218,7 @@ public final class CollisionUtil {
-
- if (!intersect) {
- if (!tf & !ft) {
-- return net.minecraft.world.phys.shapes.Shapes.empty();
-+ return Shapes.empty();
- }
- if (!tf | !ft) {
- return tf ? first : second;
-@@ -970,50 +1226,50 @@ public final class CollisionUtil {
- }
- }
-
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetX(),
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetX(),
-+ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
-- return net.minecraft.world.phys.shapes.Shapes.empty();
-+ if (mergedX == null) {
-+ return Shapes.empty();
- }
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetY(),
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetY(),
-+ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
-- return net.minecraft.world.phys.shapes.Shapes.empty();
-+ if (mergedY == null) {
-+ return Shapes.empty();
- }
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetZ(),
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetZ(),
-+ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
-- return net.minecraft.world.phys.shapes.Shapes.empty();
-+ if (mergedZ == null) {
-+ return Shapes.empty();
- }
-
-- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getCachedVoxelData();
-- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getCachedVoxelData();
-+ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData();
-+ final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData();
-
-- final net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape mergedShape = merge(
-+ final BitSetDiscreteVoxelShape mergedShape = merge(
- shapeDataFirst, shapeDataSecond,
- mergedX, mergedY, mergedZ,
- makeBitset(ft, tf, tt)
- );
-
- if (mergedShape == null) {
-- return net.minecraft.world.phys.shapes.Shapes.empty();
-+ return Shapes.empty();
- }
-
-- return new net.minecraft.world.phys.shapes.ArrayVoxelShape(
-+ return new ArrayVoxelShape(
- mergedShape, mergedX.wrapCoords(), mergedY.wrapCoords(), mergedZ.wrapCoords()
- );
- }
-
-- public static boolean isJoinNonEmpty(final net.minecraft.world.phys.shapes.VoxelShape first, final net.minecraft.world.phys.shapes.VoxelShape second, final net.minecraft.world.phys.shapes.BooleanOp operator) {
-+ 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
-@@ -1035,8 +1291,8 @@ public final class CollisionUtil {
- final boolean tf = operator.apply(true, false);
-
- // try to check intersection
-- final net.minecraft.world.phys.AABB aabbF = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
-- final net.minecraft.world.phys.AABB aabbS = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
-+ final AABB aabbF = ((CollisionVoxelShape)first).moonrise$getSingleAABBRepresentation();
-+ final AABB aabbS = ((CollisionVoxelShape)second).moonrise$getSingleAABBRepresentation();
-
- final boolean intersect;
-
-@@ -1068,33 +1324,33 @@ public final class CollisionUtil {
- }
- }
-
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedX = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetX(),
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesX(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetX(),
-+ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
-+ if (mergedX == null) {
- return false;
- }
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedY = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetY(),
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesY(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetY(),
-+ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
-+ if (mergedY == null) {
- return false;
- }
-- final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList mergedZ = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.merge(
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$offsetZ(),
-- ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$rootCoordinatesZ(), ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$offsetZ(),
-+ 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 == ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList.EMPTY) {
-+ if (mergedZ == null) {
- return false;
- }
-
-- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataFirst = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)first).moonrise$getCachedVoxelData();
-- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData shapeDataSecond = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)second).moonrise$getCachedVoxelData();
-+ final CachedShapeData shapeDataFirst = ((CollisionVoxelShape)first).moonrise$getCachedVoxelData();
-+ final CachedShapeData shapeDataSecond = ((CollisionVoxelShape)second).moonrise$getCachedVoxelData();
-
- return !isMergeEmpty(
- shapeDataFirst, shapeDataSecond,
-@@ -1112,10 +1368,6 @@ public final class CollisionUtil {
- }
- }
-
-- private static final ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList EMPTY = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(
-- new double[] { 0.0 }, 0.0, new int[0], new int[0], 0
-- );
--
- private static int[] getIndices(final int length) {
- final int[] ret = new int[length];
-
-@@ -1142,25 +1394,25 @@ public final class CollisionUtil {
- this.voxels = voxels;
- }
-
-- public it.unimi.dsi.fastutil.doubles.DoubleList wrapCoords() {
-+ public DoubleList wrapCoords() {
- if (this.coordinateOffset == 0.0) {
-- return it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(this.coordinates, this.voxels + 1);
-+ return DoubleArrayList.wrap(this.coordinates, this.voxels + 1);
- }
-- return new net.minecraft.world.phys.shapes.OffsetDoubleList(it.unimi.dsi.fastutil.doubles.DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset);
-+ return new OffsetDoubleList(DoubleArrayList.wrap(this.coordinates, this.voxels + 1), this.coordinateOffset);
- }
-
- // assume coordinates.length > 1
-- public static ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList getForSingle(final double[] coordinates, final double offset) {
-+ 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 ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels);
-+ return new MergedVoxelCoordinateList(coordinates, offset, indices, indices, voxels);
- }
-
- // assume coordinates.length > 1
-- public static ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList merge(final double[] firstCoordinates, final double firstOffset,
-- final double[] secondCoordinates, final double secondOffset,
-- final boolean ft, final boolean tf) {
-+ 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);
- }
-@@ -1250,13 +1502,13 @@ public final class CollisionUtil {
- }
- }
-
-- return resultSize <= 1 ? EMPTY : new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1);
-+ return resultSize <= 1 ? null : new MergedVoxelCoordinateList(coordinates, 0.0, firstIndices, secondIndices, resultSize - 1);
- }
- }
-
-- public static boolean equals(final net.minecraft.world.phys.shapes.DiscreteVoxelShape shape1, final net.minecraft.world.phys.shapes.DiscreteVoxelShape shape2) {
-- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData1 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)shape1).moonrise$getOrCreateCachedShapeData();
-- final ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData cachedShapeData2 = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape)shape2).moonrise$getOrCreateCachedShapeData();
-+ 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();
-@@ -1265,7 +1517,7 @@ public final class CollisionUtil {
- return true;
- } else if (isEmpty1 ^ isEmpty2) {
- return false;
-- }
-+ } // else: isEmpty1 = isEmpty2 = false
-
- if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) {
- return false;
-@@ -1281,153 +1533,237 @@ public final class CollisionUtil {
- return false;
- }
-
-- return java.util.Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet());
-+ return Arrays.equals(cachedShapeData1.voxelSet(), cachedShapeData2.voxelSet());
- }
-
- // useful only for testing
-- public static boolean equals(final net.minecraft.world.phys.shapes.VoxelShape shape1, final net.minecraft.world.phys.shapes.VoxelShape shape2) {
-+ 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(net.minecraft.core.Direction.Axis.X).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.X)) &&
-- shape1.getCoords(net.minecraft.core.Direction.Axis.Y).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.Y)) &&
-- shape1.getCoords(net.minecraft.core.Direction.Axis.Z).equals(shape2.getCoords(net.minecraft.core.Direction.Axis.Z));
-+ 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 net.minecraft.world.phys.AABB offsetX(final net.minecraft.world.phys.AABB box, final double dx) {
-- return new net.minecraft.world.phys.AABB(box.minX + dx, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ);
-+ 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 net.minecraft.world.phys.AABB offsetY(final net.minecraft.world.phys.AABB box, final double dy) {
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.maxY + dy, 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 net.minecraft.world.phys.AABB offsetZ(final net.minecraft.world.phys.AABB box, final double dz) {
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.maxZ + dz);
-+ 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 net.minecraft.world.phys.AABB expandRight(final net.minecraft.world.phys.AABB box, final double dx) { // dx > 0.0
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX + dx, box.maxY, box.maxZ);
-+ 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 net.minecraft.world.phys.AABB expandLeft(final net.minecraft.world.phys.AABB box, final double dx) { // dx < 0.0
-- return new net.minecraft.world.phys.AABB(box.minX - dx, box.minY, box.minZ, box.maxX, 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 net.minecraft.world.phys.AABB expandUpwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy > 0.0
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY + dy, 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 net.minecraft.world.phys.AABB expandDownwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy < 0.0
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY - dy, box.minZ, box.maxX, box.maxY, 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 net.minecraft.world.phys.AABB expandForwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz > 0.0
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ + dz);
-+ 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 net.minecraft.world.phys.AABB expandBackwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz < 0.0
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ - dz, box.maxX, box.maxY, box.maxZ);
-+ 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 net.minecraft.world.phys.AABB cutRight(final net.minecraft.world.phys.AABB box, final double dx) { // dx > 0.0
-- return new net.minecraft.world.phys.AABB(box.maxX, box.minY, box.minZ, box.maxX + dx, 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 net.minecraft.world.phys.AABB cutLeft(final net.minecraft.world.phys.AABB box, final double dx) { // dx < 0.0
-- return new net.minecraft.world.phys.AABB(box.minX + dx, box.minY, box.minZ, box.minX, 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 net.minecraft.world.phys.AABB cutUpwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy > 0.0
-- return new net.minecraft.world.phys.AABB(box.minX, box.maxY, box.minZ, box.maxX, box.maxY + dy, 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 net.minecraft.world.phys.AABB cutDownwards(final net.minecraft.world.phys.AABB box, final double dy) { // dy < 0.0
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY + dy, box.minZ, box.maxX, box.minY, 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 net.minecraft.world.phys.AABB cutForwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz > 0.0
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.maxZ, box.maxX, box.maxY, box.maxZ + dz);
-+ 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 net.minecraft.world.phys.AABB cutBackwards(final net.minecraft.world.phys.AABB box, final double dz) { // dz < 0.0
-- return new net.minecraft.world.phys.AABB(box.minX, box.minY, box.minZ + dz, box.maxX, box.maxY, box.minZ);
-+ 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 net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) {
-+ public static double performAABBCollisionsX(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
- for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
- if (Math.abs(value) < COLLISION_EPSILON) {
- return 0.0;
- }
-- final net.minecraft.world.phys.AABB target = potentialCollisions.get(i);
-+ final AABB target = potentialCollisions.get(i);
- value = collideX(target, currentBoundingBox, value);
- }
-
-- return value;
-+ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
- }
-
-- public static double performAABBCollisionsY(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) {
-+ public static double performAABBCollisionsY(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
- for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
- if (Math.abs(value) < COLLISION_EPSILON) {
- return 0.0;
- }
-- final net.minecraft.world.phys.AABB target = potentialCollisions.get(i);
-+ final AABB target = potentialCollisions.get(i);
- value = collideY(target, currentBoundingBox, value);
- }
-
-- return value;
-+ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
- }
-
-- public static double performAABBCollisionsZ(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) {
-+ public static double performAABBCollisionsZ(final AABB currentBoundingBox, double value, final List<AABB> potentialCollisions) {
- for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
- if (Math.abs(value) < COLLISION_EPSILON) {
- return 0.0;
- }
-- final net.minecraft.world.phys.AABB target = potentialCollisions.get(i);
-+ final AABB target = potentialCollisions.get(i);
- value = collideZ(target, currentBoundingBox, value);
- }
-
-- return value;
-+ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
- }
-
-- public static double performVoxelCollisionsX(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) {
-+ public static double performVoxelCollisionsX(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
- for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
- if (Math.abs(value) < COLLISION_EPSILON) {
- return 0.0;
- }
-- final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i);
-+ final VoxelShape target = potentialCollisions.get(i);
- value = collideX(target, currentBoundingBox, value);
- }
-
-- return value;
-+ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
- }
-
-- public static double performVoxelCollisionsY(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) {
-+ public static double performVoxelCollisionsY(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
- for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
- if (Math.abs(value) < COLLISION_EPSILON) {
- return 0.0;
- }
-- final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i);
-+ final VoxelShape target = potentialCollisions.get(i);
- value = collideY(target, currentBoundingBox, value);
- }
-
-- return value;
-+ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
- }
-
-- public static double performVoxelCollisionsZ(final net.minecraft.world.phys.AABB currentBoundingBox, double value, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) {
-+ public static double performVoxelCollisionsZ(final AABB currentBoundingBox, double value, final List<VoxelShape> potentialCollisions) {
- for (int i = 0, len = potentialCollisions.size(); i < len; ++i) {
- if (Math.abs(value) < COLLISION_EPSILON) {
- return 0.0;
- }
-- final net.minecraft.world.phys.shapes.VoxelShape target = potentialCollisions.get(i);
-+ final VoxelShape target = potentialCollisions.get(i);
- value = collideZ(target, currentBoundingBox, value);
- }
-
-- return value;
-+ return Math.abs(value) < COLLISION_EPSILON ? 0.0 : value;
- }
-
-- public static net.minecraft.world.phys.Vec3 performVoxelCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> potentialCollisions) {
-+ 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;
-@@ -1459,10 +1795,10 @@ public final class CollisionUtil {
- z = performVoxelCollisionsZ(axisalignedbb, z, potentialCollisions);
- }
-
-- return new net.minecraft.world.phys.Vec3(x, y, z);
-+ return new Vec3(x, y, z);
- }
-
-- public static net.minecraft.world.phys.Vec3 performAABBCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb, final java.util.List<net.minecraft.world.phys.AABB> potentialCollisions) {
-+ 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;
-@@ -1494,12 +1830,12 @@ public final class CollisionUtil {
- z = performAABBCollisionsZ(axisalignedbb, z, potentialCollisions);
- }
-
-- return new net.minecraft.world.phys.Vec3(x, y, z);
-+ return new Vec3(x, y, z);
- }
-
-- public static net.minecraft.world.phys.Vec3 performCollisions(final net.minecraft.world.phys.Vec3 moveVector, net.minecraft.world.phys.AABB axisalignedbb,
-- final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> voxels,
-- final java.util.List<net.minecraft.world.phys.AABB> aabbs) {
-+ 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);
-@@ -1540,14 +1876,14 @@ public final class CollisionUtil {
- z = performVoxelCollisionsZ(axisalignedbb, z, voxels);
- }
-
-- return new net.minecraft.world.phys.Vec3(x, y, z);
-+ return new Vec3(x, y, z);
- }
-
-- public static boolean isCollidingWithBorder(final net.minecraft.world.level.border.WorldBorder worldborder, final net.minecraft.world.phys.AABB boundingBox) {
-+ 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 net.minecraft.world.level.border.WorldBorder worldborder,
-+ 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
-@@ -1557,8 +1893,8 @@ public final class CollisionUtil {
- final double borderMaxZ = Math.ceil(worldborder.getMaxZ()); // +Z
-
- // inverted check for world border enclosing the specified box expanded by -EPSILON
-- return (borderMinX - boxMinX) > ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || (borderMaxX - boxMaxX) < -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON ||
-- (borderMinZ - boxMinZ) > ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON || (borderMaxZ - boxMaxZ) < -ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_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 */
-@@ -1575,38 +1911,38 @@ public final class CollisionUtil {
- 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 net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB aabb,
-- final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> intoVoxel, final java.util.List<net.minecraft.world.phys.AABB> intoAABB,
-- final int collisionFlags, final java.util.function.BiPredicate<net.minecraft.world.level.block.state.BlockState, net.minecraft.core.BlockPos> predicate) {
-+ 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 net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder();
-- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) {
-+ final WorldBorder worldBorder = world.getWorldBorder();
-+ if (CollisionUtil.isCollidingWithBorder(worldBorder, aabb) && entity != null && worldBorder.isInsideCloseToBorder(entity, aabb)) {
- if (checkOnly) {
- return true;
- } else {
-- final net.minecraft.world.phys.shapes.VoxelShape borderShape = worldBorder.getCollisionShape();
-+ final VoxelShape borderShape = worldBorder.getCollisionShape();
- intoVoxel.add(borderShape);
- ret = true;
- }
- }
- }
-
-- final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)world).moonrise$getMinSection();
-+ final int minSection = WorldUtil.getMinSection(world);
-
-- final int minBlockX = net.minecraft.util.Mth.floor(aabb.minX - COLLISION_EPSILON) - 1;
-- final int maxBlockX = net.minecraft.util.Mth.floor(aabb.maxX + COLLISION_EPSILON) + 1;
-+ 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, net.minecraft.util.Mth.floor(aabb.minY - COLLISION_EPSILON) - 1);
-- final int maxBlockY = Math.min((((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)world).moonrise$getMaxSection() << 4) + 16, net.minecraft.util.Mth.floor(aabb.maxY + 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 = net.minecraft.util.Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
-- final int maxBlockZ = net.minecraft.util.Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
-+ final int minBlockZ = Mth.floor(aabb.minZ - COLLISION_EPSILON) - 1;
-+ final int maxBlockZ = Mth.floor(aabb.maxZ + COLLISION_EPSILON) + 1;
-
-- final net.minecraft.core.BlockPos.MutableBlockPos mutablePos = new net.minecraft.core.BlockPos.MutableBlockPos();
-- final net.minecraft.world.phys.shapes.CollisionContext collisionShape = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity);
-+ final BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
-+ final CollisionContext collisionShape = new LazyEntityCollisionContext(entity);
-
- // special cases:
- if (minBlockY > maxBlockY) {
-@@ -1624,11 +1960,11 @@ public final class CollisionUtil {
- final int maxChunkZ = maxBlockZ >> 4;
-
- final boolean loadChunks = (collisionFlags & COLLISION_FLAG_LOAD_CHUNKS) != 0;
-- final net.minecraft.world.level.chunk.ChunkSource chunkSource = world.getChunkSource();
-+ final ChunkSource chunkSource = world.getChunkSource();
-
- for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
- for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-- final net.minecraft.world.level.chunk.ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, loadChunks);
-+ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks);
-
- if (chunk == null) {
- if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) {
-@@ -1642,7 +1978,7 @@ public final class CollisionUtil {
- continue;
- }
-
-- final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections();
-+ final LevelChunkSection[] sections = chunk.getSections();
-
- // bound y
- for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
-@@ -1650,16 +1986,16 @@ public final class CollisionUtil {
- if (sectionIdx < 0 || sectionIdx >= sections.length) {
- continue;
- }
-- final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
-- if (section == null || section.hasOnlyAir()) {
-+ final LevelChunkSection section = sections[sectionIdx];
-+ if (section.hasOnlyAir()) {
- // empty
- continue;
- }
-
-- final boolean hasSpecial = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getSpecialCollidingBlocks() != 0;
-+ final boolean hasSpecial = ((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 PalettedContainer<BlockState> blocks = section.states;
-
- final int minXIterate = currChunkX == minChunkX ? (minBlockX & 15) + sectionAdjust : 0;
- final int maxXIterate = currChunkX == maxChunkX ? (maxBlockX & 15) - sectionAdjust : 15;
-@@ -1683,21 +2019,21 @@ public final class CollisionUtil {
- continue;
- }
-
-- final net.minecraft.world.level.block.state.BlockState blockData = blocks.get(localBlockIndex);
-+ final BlockState blockData = blocks.get(localBlockIndex);
-
-- if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$emptyCollisionShape()) {
-+ if (((CollisionBlockState)blockData).moonrise$emptyContextCollisionShape()) {
- continue;
- }
-
-- net.minecraft.world.phys.shapes.VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)blockData).moonrise$getConstantCollisionShape();
-+ VoxelShape blockCollision = ((CollisionBlockState)blockData).moonrise$getConstantContextCollisionShape();
-
-- if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == net.minecraft.world.level.block.Blocks.MOVING_PISTON))) {
-+ if (edgeCount == 0 || ((edgeCount != 1 || blockData.hasLargeCollisionShape()) && (edgeCount != 2 || blockData.getBlock() == Blocks.MOVING_PISTON))) {
- if (blockCollision == null) {
- mutablePos.set(blockX, blockY, blockZ);
- blockCollision = blockData.getCollisionShape(world, mutablePos, collisionShape);
- }
-
-- net.minecraft.world.phys.AABB singleAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation();
-+ AABB singleAABB = ((CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation();
- if (singleAABB != null) {
- singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ);
- if (!voxelShapeIntersect(aabb, singleAABB)) {
-@@ -1724,7 +2060,7 @@ public final class CollisionUtil {
- continue;
- }
-
-- final net.minecraft.world.phys.shapes.VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ);
-+ final VoxelShape blockCollisionOffset = blockCollision.move((double)blockX, (double)blockY, (double)blockZ);
-
- if (!voxelShapeIntersectNoEmpty(blockCollisionOffset, aabb)) {
- continue;
-@@ -1755,8 +2091,8 @@ public final class CollisionUtil {
- return ret;
- }
-
-- public static boolean getEntityHardCollisions(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, net.minecraft.world.phys.AABB aabb,
-- final java.util.List<net.minecraft.world.phys.AABB> into, final int collisionFlags, final java.util.function.Predicate<net.minecraft.world.entity.Entity> predicate) {
-+ 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;
-@@ -1765,15 +2101,15 @@ public final class CollisionUtil {
- // 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 java.util.List<net.minecraft.world.entity.Entity> entities;
-- if (entity != null && ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$isHardColliding()) {
-+ final List<Entity> entities;
-+ if (entity != null && ((ChunkSystemEntity)entity).moonrise$isHardColliding()) {
- entities = world.getEntities(entity, aabb, predicate);
- } else {
-- entities = ((ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate);
-+ entities = ((ChunkSystemEntityGetter)world).moonrise$getHardCollidingEntities(entity, aabb, predicate);
- }
-
- for (int i = 0, len = entities.size(); i < len; ++i) {
-- final net.minecraft.world.entity.Entity otherEntity = entities.get(i);
-+ final Entity otherEntity = entities.get(i);
-
- if (otherEntity.isSpectator()) {
- continue;
-@@ -1792,10 +2128,10 @@ public final class CollisionUtil {
- return ret;
- }
-
-- public static boolean getCollisions(final net.minecraft.world.level.Level world, final net.minecraft.world.entity.Entity entity, final net.minecraft.world.phys.AABB aabb,
-- final java.util.List<net.minecraft.world.phys.shapes.VoxelShape> intoVoxel, final java.util.List<net.minecraft.world.phys.AABB> intoAABB, final int collisionFlags,
-- final java.util.function.BiPredicate<net.minecraft.world.level.block.state.BlockState, net.minecraft.core.BlockPos> blockPredicate,
-- final java.util.function.Predicate<net.minecraft.world.entity.Entity> entityPredicate) {
-+ 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);
-@@ -1805,12 +2141,12 @@ public final class CollisionUtil {
- }
- }
-
-- public static final class LazyEntityCollisionContext extends net.minecraft.world.phys.shapes.EntityCollisionContext {
-+ public static final class LazyEntityCollisionContext extends EntityCollisionContext {
-
-- private net.minecraft.world.phys.shapes.CollisionContext delegate;
-+ private CollisionContext delegate;
- private boolean delegated;
-
-- public LazyEntityCollisionContext(final net.minecraft.world.entity.Entity entity) {
-+ public LazyEntityCollisionContext(final Entity entity) {
- super(false, 0.0, null, null, entity);
- }
-
-@@ -1820,10 +2156,10 @@ public final class CollisionUtil {
- return delegated;
- }
-
-- public net.minecraft.world.phys.shapes.CollisionContext getDelegate() {
-+ public CollisionContext getDelegate() {
- this.delegated = true;
-- final net.minecraft.world.entity.Entity entity = this.getEntity();
-- return this.delegate == null ? this.delegate = (entity == null ? net.minecraft.world.phys.shapes.CollisionContext.empty() : net.minecraft.world.phys.shapes.CollisionContext.of(entity)) : this.delegate;
-+ final Entity entity = this.getEntity();
-+ return this.delegate == null ? this.delegate = (entity == null ? CollisionContext.empty() : CollisionContext.of(entity)) : this.delegate;
- }
-
- @Override
-@@ -1832,17 +2168,17 @@ public final class CollisionUtil {
- }
-
- @Override
-- public boolean isAbove(final net.minecraft.world.phys.shapes.VoxelShape shape, final net.minecraft.core.BlockPos pos, final boolean defaultValue) {
-+ public boolean isAbove(final VoxelShape shape, final BlockPos pos, final boolean defaultValue) {
- return this.getDelegate().isAbove(shape, pos, defaultValue);
- }
-
- @Override
-- public boolean isHoldingItem(final net.minecraft.world.item.Item item) {
-+ public boolean isHoldingItem(final Item item) {
- return this.getDelegate().isHoldingItem(item);
- }
-
- @Override
-- public boolean canStandOnFluid(final net.minecraft.world.level.material.FluidState state, final net.minecraft.world.level.material.FluidState fluidState) {
-+ public boolean canStandOnFluid(final FluidState state, final FluidState fluidState) {
- return this.getDelegate().canStandOnFluid(state, fluidState);
- }
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
-index eb7200657d5c7ac37ee93868ba43be0aefecac6d..35c8aaf0bfa42717f45eed1d1072e1614874de91 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
-@@ -1,18 +1,23 @@
- 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 net.minecraft.core.BlockPos immutablePos;
-- public final net.minecraft.world.level.block.state.BlockState blockState;
-- public final net.minecraft.world.level.material.FluidState fluidState;
-+ 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 net.minecraft.world.phys.shapes.VoxelShape cachedCollisionShape;
-+ public VoxelShape cachedCollisionShape;
-
-- public ExplosionBlockCache(final long key, final net.minecraft.core.BlockPos immutablePos, final net.minecraft.world.level.block.state.BlockState blockState,
-- final net.minecraft.world.level.material.FluidState fluidState, final float resistance, final boolean outOfWorld) {
-+ 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;
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
-index 02b29c563a298e06186de010de68a716bccba494..a38ab583200ebf68ca68fdddf2d12077720b72b7 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/block/CollisionBlockState.java
-@@ -1,5 +1,7 @@
- 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())
-@@ -9,6 +11,9 @@ public interface CollisionBlockState {
- // 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();
-
-@@ -20,7 +25,5 @@ public interface CollisionBlockState {
- // value is still unique
- public int moonrise$uniqueId2();
-
-- public net.minecraft.world.phys.shapes.VoxelShape moonrise$getConstantCollisionShape();
--
-- public net.minecraft.world.phys.AABB moonrise$getConstantCollisionAABB();
-+ public VoxelShape moonrise$getConstantContextCollisionShape();
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
-index 5fe1dad9dad368911aedbe6ba7fcd8f9b0189d32..9d33ead3a97d86b371e4d9ad9fed80d789bed844 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CachedToAABBs.java
-@@ -1,27 +1,31 @@
- package ca.spottedleaf.moonrise.patches.collisions.shape;
-
-+import net.minecraft.world.phys.AABB;
-+import java.util.ArrayList;
-+import java.util.List;
-+
- public record CachedToAABBs(
-- java.util.List<net.minecraft.world.phys.AABB> aabbs,
-+ List<AABB> aabbs,
- boolean isOffset,
- double offX, double offY, double offZ
- ) {
-
-- public ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs removeOffset() {
-- final java.util.List<net.minecraft.world.phys.AABB> toOffset = this.aabbs;
-+ 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 java.util.List<net.minecraft.world.phys.AABB> ret = new java.util.ArrayList<>(toOffset.size());
-+ 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 ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
-+ return new CachedToAABBs(ret, false, 0.0, 0.0, 0.0);
- }
-
-- public static ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs offset(final ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs cache, final double offX, final double offY, final double offZ) {
-+ 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;
- }
-@@ -30,6 +34,6 @@ public record CachedToAABBs(
- final double resY = cache.offY + offY;
- final double resZ = cache.offZ + offZ;
-
-- return new ca.spottedleaf.moonrise.patches.collisions.shape.CachedToAABBs(cache.aabbs, true, resX, resY, resZ);
-+ return new CachedToAABBs(cache.aabbs, true, resX, resY, resZ);
- }
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
-index a09efadea9b733840bbe69830dd8f2a303fe656f..07fe5e02c2d0a27d2fe37bb45761654dc2d02e5d 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionDiscreteVoxelShape.java
-@@ -2,6 +2,6 @@ package ca.spottedleaf.moonrise.patches.collisions.shape;
-
- public interface CollisionDiscreteVoxelShape {
-
-- public ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getOrCreateCachedShapeData();
-+ public CachedShapeData moonrise$getOrCreateCachedShapeData();
-
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
-index 70371eb87c11a106e8513cdbc8d938dda088f745..05d7b3f9d8659c259f3ed0537c57e6e43eb6e288 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/CollisionVoxelShape.java
-@@ -1,5 +1,9 @@
- 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();
-@@ -14,16 +18,16 @@ public interface CollisionVoxelShape {
-
- public double[] moonrise$rootCoordinatesZ();
-
-- public ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData moonrise$getCachedVoxelData();
-+ public CachedShapeData moonrise$getCachedVoxelData();
-
- // rets null if not possible to represent this shape as one AABB
-- public net.minecraft.world.phys.AABB moonrise$getSingleAABBRepresentation();
-+ 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 net.minecraft.world.phys.shapes.VoxelShape moonrise$getFaceShapeClamped(final net.minecraft.core.Direction direction);
-+ public VoxelShape moonrise$getFaceShapeClamped(final Direction direction);
-
- public boolean moonrise$isFullBlock();
-
-@@ -32,5 +36,5 @@ public interface CollisionVoxelShape {
- public boolean moonrise$occludesFullBlockIfCached();
-
- // uses a cache internally
-- public net.minecraft.world.phys.shapes.VoxelShape moonrise$orUnoptimized(final net.minecraft.world.phys.shapes.VoxelShape other);
-+ public VoxelShape moonrise$orUnoptimized(final VoxelShape other);
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
-index 4217426d3eca5e5cd2bc37e509f84da1d6fed0b2..44831fc18efb7534dc6e4822f3c9b5cdc4dcc33e 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/shape/MergedORCache.java
-@@ -1,8 +1,10 @@
- package ca.spottedleaf.moonrise.patches.collisions.shape;
-
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+
- public record MergedORCache(
-- net.minecraft.world.phys.shapes.VoxelShape key,
-- net.minecraft.world.phys.shapes.VoxelShape result
-+ VoxelShape key,
-+ VoxelShape result
- ) {
-
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java
-deleted file mode 100644
-index 673103f160cbe577c6e05f998706af4e6850011b..0000000000000000000000000000000000000000
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java
-+++ /dev/null
-@@ -1,225 +0,0 @@
--package ca.spottedleaf.moonrise.patches.collisions.util;
--
--import java.util.Iterator;
--import java.util.Optional;
--import java.util.Spliterator;
--import java.util.stream.Stream;
--
--public final class EmptyStreamForMoveCall<T> implements java.util.stream.Stream<T> {
--
-- public static final ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall INSTANCE = new ca.spottedleaf.moonrise.patches.collisions.util.EmptyStreamForMoveCall();
--
-- @Override
-- public boolean noneMatch(java.util.function.Predicate<? super T> predicate) {
-- return false; // important: ret false so the branch is never taken by mojang code
-- }
--
-- @Override
-- public java.util.stream.Stream<T> filter(java.util.function.Predicate<? super T> predicate) {
-- return null;
-- }
--
-- @Override
-- public <R> java.util.stream.Stream<R> map(java.util.function.Function<? super T, ? extends R> mapper) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.IntStream mapToInt(java.util.function.ToIntFunction<? super T> mapper) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.LongStream mapToLong(java.util.function.ToLongFunction<? super T> mapper) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.DoubleStream mapToDouble(java.util.function.ToDoubleFunction<? super T> mapper) {
-- return null;
-- }
--
-- @Override
-- public <R> java.util.stream.Stream<R> flatMap(java.util.function.Function<? super T, ? extends java.util.stream.Stream<? extends R>> mapper) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.IntStream flatMapToInt(java.util.function.Function<? super T, ? extends java.util.stream.IntStream> mapper) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.LongStream flatMapToLong(java.util.function.Function<? super T, ? extends java.util.stream.LongStream> mapper) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.DoubleStream flatMapToDouble(java.util.function.Function<? super T, ? extends java.util.stream.DoubleStream> mapper) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.Stream<T> distinct() {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.Stream<T> sorted() {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.Stream<T> sorted(java.util.Comparator<? super T> comparator) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.Stream<T> peek(java.util.function.Consumer<? super T> action) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.Stream<T> limit(long maxSize) {
-- return null;
-- }
--
-- @Override
-- public java.util.stream.Stream<T> skip(long n) {
-- return null;
-- }
--
-- @Override
-- public void forEach(java.util.function.Consumer<? super T> action) {
--
-- }
--
-- @Override
-- public void forEachOrdered(java.util.function.Consumer<? super T> action) {
--
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Object[] toArray() {
-- return new Object[0];
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public <A> A[] toArray(java.util.function.IntFunction<A[]> generator) {
-- return null;
-- }
--
-- @Override
-- public T reduce(T identity, java.util.function.BinaryOperator<T> accumulator) {
-- return null;
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Optional<T> reduce(java.util.function.BinaryOperator<T> accumulator) {
-- return java.util.Optional.empty();
-- }
--
-- @Override
-- public <U> U reduce(U identity, java.util.function.BiFunction<U, ? super T, U> accumulator, java.util.function.BinaryOperator<U> combiner) {
-- return null;
-- }
--
-- @Override
-- public <R> R collect(java.util.function.Supplier<R> supplier, java.util.function.BiConsumer<R, ? super T> accumulator, java.util.function.BiConsumer<R, R> combiner) {
-- return null;
-- }
--
-- @Override
-- public <R, A> R collect(java.util.stream.Collector<? super T, A, R> collector) {
-- return null;
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Optional<T> min(java.util.Comparator<? super T> comparator) {
-- return java.util.Optional.empty();
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Optional<T> max(java.util.Comparator<? super T> comparator) {
-- return java.util.Optional.empty();
-- }
--
-- @Override
-- public long count() {
-- return 0;
-- }
--
-- @Override
-- public boolean anyMatch(java.util.function.Predicate<? super T> predicate) {
-- return false;
-- }
--
-- @Override
-- public boolean allMatch(java.util.function.Predicate<? super T> predicate) {
-- return false;
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Optional<T> findFirst() {
-- return java.util.Optional.empty();
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Optional<T> findAny() {
-- return java.util.Optional.empty();
-- }
--
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Iterator<T> iterator() {
-- return null;
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Spliterator<T> spliterator() {
-- return null;
-- }
--
-- @Override
-- public boolean isParallel() {
-- return false;
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Stream<T> sequential() {
-- return null;
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Stream<T> parallel() {
-- return null;
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Stream<T> unordered() {
-- return null;
-- }
--
-- @org.jetbrains.annotations.NotNull
-- @Override
-- public Stream<T> onClose(Runnable closeHandler) {
-- return null;
-- }
--
-- @Override
-- public void close() {
--
-- }
--}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
-index 128267ff40b38c7b3ea0feb5133825cc6aae075b..cf9ffdeff6bf0b62a45f7a44dbfe0dd7d17dc4f4 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/FluidOcclusionCacheKey.java
-@@ -1,4 +1,7 @@
- package ca.spottedleaf.moonrise.patches.collisions.util;
-
--public record FluidOcclusionCacheKey(net.minecraft.world.level.block.state.BlockState first, net.minecraft.world.level.block.state.BlockState second, net.minecraft.core.Direction direction, boolean result) {
-+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/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java
-deleted file mode 100644
-index e851e81e13edbad6316df63fcb7095d48f85c5b0..0000000000000000000000000000000000000000
---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java
-+++ /dev/null
-@@ -1,9 +0,0 @@
--package ca.spottedleaf.moonrise.patches.collisions.world;
--
--public interface CollisionLevel {
--
-- public int moonrise$getMinSection();
--
-- public int moonrise$getMaxSection();
--
--}
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
-index 1fa07bef57d82c6d5242aaaf66011f0913515231..8e7472157a98de607c03769a91f64c8369fd3ea6 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/entity_tracker/EntityTrackerTrackedEntity.java
-@@ -10,4 +10,6 @@ public interface EntityTrackerTrackedEntity {
-
- public void moonrise$clearPlayers();
-
-+ public boolean moonrise$hasPlayers();
-+
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4a7abd239a9c59aa98947e7993962d75e9051902
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4503f3495846a7d7ed082b9e24636044e4fbccd1
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java b/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..107c97089354edd35f330582f5e0c8a18e792a6e
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
-similarity index 75%
-rename from src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java
-rename to src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
-index 08338917dc61c856eaba0b76e05c1497c458399d..540c14a6d2c216cd3ef2a9c4056e15712bf8cb8c 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
-@@ -1,4 +1,4 @@
--package ca.spottedleaf.moonrise.patches.chunk_getblock;
-+package ca.spottedleaf.moonrise.patches.getblock;
-
- import net.minecraft.world.level.block.state.BlockState;
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
-index 2bfdf3721db9a45e36538d71cbefcb1d339e6c58..8e6d79b7c10ef25f5478b72c53c555423d615a2f 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/blockstate/StarlightAbstractBlockState.java
-@@ -4,6 +4,4 @@ public interface StarlightAbstractBlockState {
-
- public boolean starlight$isConditionallyFullOpaque();
-
-- public int starlight$getOpacityIfCached();
--
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
-index 154443ac1ee1d6d18b8ff0f40a307d638b213aeb..fa7b784a89626e8528c249d7889a598bd7ee3d49 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/BlockStarLightEngine.java
-@@ -1,8 +1,10 @@
- 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;
-@@ -91,7 +93,7 @@ public final class BlockStarLightEngine extends StarLightEngine {
-
- final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
- final BlockState blockState = this.getBlockState(worldX, worldY, worldZ);
-- final int emittedLevel = blockState.getLightEmission() & emittedMask;
-+ 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
-@@ -119,37 +121,32 @@ public final class BlockStarLightEngine extends StarLightEngine {
- }
-
- protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-- protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
-
- @Override
- protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
- final int expect) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+
- final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-- int level = centerState.getLightEmission() & 0xF;
-+ 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 sectionOffset = this.chunkSectionIndexOffset;
-- final BlockState conditionallyOpaqueState;
-- int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached();
--
-- if (opacity == -1) {
-- this.recalcCenterPos.set(worldX, worldY, worldZ);
-- opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos);
-- if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
-- conditionallyOpaqueState = centerState;
-- } else {
-- conditionallyOpaqueState = null;
-- }
-- } else if (opacity >= 15) {
-+ 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;
- }
-- opacity = Math.max(1, opacity);
-
-+ final int sectionOffset = this.chunkSectionIndexOffset;
- for (final AxisDirection direction : AXIS_DIRECTIONS) {
- final int offX = worldX + direction.x;
- final int offY = worldY + direction.y;
-@@ -169,9 +166,8 @@ public final class BlockStarLightEngine extends StarLightEngine {
- // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
- // we don't read the blockstate because most of the time this is false, so using the faster
- // known transparency lookup results in a net win
-- this.recalcNeighbourPos.set(offX, offY, offZ);
-- final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-- final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
-+ 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;
-@@ -205,30 +201,34 @@ public final class BlockStarLightEngine extends StarLightEngine {
- 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 == null || section.hasOnlyAir()) {
-+ if (section.hasOnlyAir()) {
- // no sources in empty sections
- continue;
- }
-- if (!section.maybeHas((final BlockState state) -> {
-- return state.getLightEmission() > 0;
-- })) {
-+ 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);
-- if (state.getLightEmission() <= 0) {
-+ 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(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
-+ sources.add(mutablePos.immutable());
- }
- }
-
-@@ -238,12 +238,15 @@ public final class BlockStarLightEngine extends StarLightEngine {
- @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 = blockState.getLightEmission() & emittedMask;
-+ final int emittedLight = platformHooks.getLightEmission(blockState, world, pos) & emittedMask;
-
- if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) {
- // some other source is brighter
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
-index fdbc015f498164c9d2c578cd84a73def568142a4..f9aef289e9a2d6f63c98c72c56ef32b8793f57f4 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/SkyStarLightEngine.java
-@@ -290,9 +290,6 @@ public final class SkyStarLightEngine extends StarLightEngine {
- );
- }
-
-- protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-- protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
--
- @Override
- protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
- final int expect) {
-@@ -302,20 +299,13 @@ public final class SkyStarLightEngine extends StarLightEngine {
-
- final int sectionOffset = this.chunkSectionIndexOffset;
- final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-- int opacity = ((StarlightAbstractBlockState)centerState).starlight$getOpacityIfCached();
-
- final BlockState conditionallyOpaqueState;
-- if (opacity < 0) {
-- this.recalcCenterPos.set(worldX, worldY, worldZ);
-- opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos));
-- if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
-- conditionallyOpaqueState = centerState;
-- } else {
-- conditionallyOpaqueState = null;
-- }
-+ final int opacity = Math.max(1, centerState.getLightBlock());
-+ if (((StarlightAbstractBlockState)centerState).starlight$isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
- } else {
- conditionallyOpaqueState = null;
-- opacity = Math.max(1, opacity);
- }
-
- int level = 0;
-@@ -340,9 +330,8 @@ public final class SkyStarLightEngine extends StarLightEngine {
- // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
- // we don't read the blockstate because most of the time this is false, so using the faster
- // known transparency lookup results in a net win
-- this.recalcNeighbourPos.set(offX, offY, offZ);
-- final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-- final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
-+ 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;
-@@ -610,7 +599,6 @@ public final class SkyStarLightEngine extends StarLightEngine {
- // clobbering the light values will result in broken propagation)
- protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ,
- final boolean extrudeInitialised, final boolean delayLightSet) {
-- final BlockPos.MutableBlockPos mutablePos = this.mutablePos3;
- final int encodeOffset = this.coordinateOffset;
- final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards.
-
-@@ -632,8 +620,7 @@ public final class SkyStarLightEngine extends StarLightEngine {
-
- final VoxelShape fromShape;
- if (((StarlightAbstractBlockState)above).starlight$isConditionallyFullOpaque()) {
-- this.mutablePos2.set(worldX, startY + 1, worldZ);
-- fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms);
-+ fromShape = above.getFaceOcclusionShape(AxisDirection.NEGATIVE_Y.nms);
- if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
- // above wont let us propagate
- break;
-@@ -642,49 +629,32 @@ public final class SkyStarLightEngine extends StarLightEngine {
- fromShape = Shapes.empty();
- }
-
-- final int opacityIfCached = ((StarlightAbstractBlockState)current).starlight$getOpacityIfCached();
- // does light propagate from the top down?
-- if (opacityIfCached != -1) {
-- if (opacityIfCached != 0) {
-- // we cannot propagate 15 through this
-- break;
-- }
-- // most of the time it falls here.
-- // add to propagate
-- // light set delayed until we determine if this nibble section is null
-- this.appendToIncreaseQueue(
-- ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-- | (15L << (6 + 6 + 16)) // we know we're at full lit here
-- | (propagateDirection << (6 + 6 + 16 + 4))
-- );
-- } else {
-- mutablePos.set(worldX, startY, worldZ);
-- long flags = 0L;
-- if (((StarlightAbstractBlockState)current).starlight$isConditionallyFullOpaque()) {
-- final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms);
-+ 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(world, mutablePos);
-- if (opacity > 0) {
-- // let the queued value (if any) handle it from here.
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ // can't propagate here, we're done on this column.
- break;
- }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-
-- // 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
-- );
-+ 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) {
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
-index 382c9e445af0d6ad2428fc22d0f63017c58191e2..8aeb5fb87f94a35659347a09a638420699b52a6f 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightEngine.java
-@@ -1,6 +1,7 @@
- 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;
-@@ -43,9 +44,9 @@ public abstract class StarLightEngine {
- protected static enum AxisDirection {
-
- // Declaration order is important and relied upon. Do not change without modifying propagation code.
-- POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0),
-- POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1),
-- POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0);
-+ 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;
-@@ -62,11 +63,11 @@ public abstract class StarLightEngine {
- public final long everythingButThisDirection;
- public final long everythingButTheOppositeDirection;
-
-- AxisDirection(final int x, final int y, final int z) {
-+ AxisDirection(final int x, final int y, final int z, final Direction nms) {
- this.x = x;
- this.y = y;
- this.z = z;
-- this.nms = Direction.fromDelta(x, y, 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)));
-@@ -106,9 +107,7 @@ public abstract class StarLightEngine {
- // index = x + (z * 5)
- protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
-
-- protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos();
-- protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos();
-- protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos lightEmissionPos = new BlockPos.MutableBlockPos();
-
- protected int encodeOffsetX;
- protected int encodeOffsetY;
-@@ -1150,69 +1149,46 @@ public abstract class StarLightEngine {
- if (blockState == null) {
- continue;
- }
-- final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
-- if (opacityCached != -1) {
-- final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-- if (targetLevel > currentLevel) {
-- currentNibble.set(localIndex, targetLevel);
-- this.postLightUpdate(offX, offY, offZ);
--
-- if (targetLevel > 1) {
-- if (queueLength >= queue.length) {
-- queue = this.resizeIncreaseQueue();
-- }
-- queue[queueLength++] =
-- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-- | ((targetLevel & 0xFL) << (6 + 6 + 16))
-- | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-- continue;
-- }
-- }
-- continue;
-- } else {
-- this.mutablePos1.set(offX, offY, offZ);
-- long flags = 0;
-- if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-- final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+ 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(world, this.mutablePos1);
-- final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-- if (targetLevel <= currentLevel) {
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
- continue;
- }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-
-- currentNibble.set(localIndex, targetLevel);
-- this.postLightUpdate(offX, offY, offZ);
-+ final int opacity = blockState.getLightBlock();
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-
-- 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);
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
- }
-- continue;
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
-+ | (flags);
- }
-+ continue;
- }
- } else {
- // we actually need to worry about our state here
- final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-- this.mutablePos2.set(posX, posY, posZ);
- for (final AxisDirection propagate : checkDirections) {
- final int offX = posX + propagate.x;
- final int offY = posY + propagate.y;
- final int offZ = posZ + propagate.z;
-
-- final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
-+ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty();
-
- if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
- continue;
-@@ -1232,58 +1208,36 @@ public abstract class StarLightEngine {
- if (blockState == null) {
- continue;
- }
-- final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
-- if (opacityCached != -1) {
-- final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-- if (targetLevel > currentLevel) {
-- currentNibble.set(localIndex, targetLevel);
-- this.postLightUpdate(offX, offY, offZ);
--
-- if (targetLevel > 1) {
-- if (queueLength >= queue.length) {
-- queue = this.resizeIncreaseQueue();
-- }
-- queue[queueLength++] =
-- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-- | ((targetLevel & 0xFL) << (6 + 6 + 16))
-- | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-- continue;
-- }
-- }
-- continue;
-- } else {
-- this.mutablePos1.set(offX, offY, offZ);
-- long flags = 0;
-- if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-- final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
--
-- if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-- continue;
-- }
-- flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-- }
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
-
-- final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-- final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-- if (targetLevel <= currentLevel) {
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
- continue;
- }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-
-- currentNibble.set(localIndex, targetLevel);
-- this.postLightUpdate(offX, offY, offZ);
-+ final int opacity = blockState.getLightBlock();
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-
-- 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);
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
- }
-- continue;
-+ 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;
- }
- }
- }
-@@ -1304,6 +1258,8 @@ public abstract class StarLightEngine {
- final int sectionOffset = this.chunkSectionIndexOffset;
- final int emittedMask = this.emittedLightMask;
-
-+ final PlatformHooks platformHooks = PlatformHooks.get();
-+
- while (queueReadIndex < queueLength) {
- final long queueValue = queue[queueReadIndex++];
-
-@@ -1335,109 +1291,63 @@ public abstract class StarLightEngine {
- if (blockState == null) {
- continue;
- }
-- final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
-- if (opacityCached != -1) {
-- final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-- if (lightLevel > targetLevel) {
-- // it looks like another source propagated here, so re-propagate it
-- if (increaseQueueLength >= increaseQueue.length) {
-- increaseQueue = this.resizeIncreaseQueue();
-- }
-- increaseQueue[increaseQueueLength++] =
-- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-- | ((lightLevel & 0xFL) << (6 + 6 + 16))
-- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-- | FLAG_RECHECK_LEVEL;
-- continue;
-- }
-- final int emittedLight = blockState.getLightEmission() & emittedMask;
-- if (emittedLight != 0) {
-- // re-propagate source
-- // 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))
-- | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
-- }
--
-- currentNibble.set(localIndex, 0);
-- this.postLightUpdate(offX, offY, offZ);
-+ this.lightEmissionPos.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
-
-- 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));
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
- continue;
- }
-- continue;
-- } else {
-- this.mutablePos1.set(offX, offY, offZ);
-- long flags = 0;
-- if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-- final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
--
-- if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-- continue;
-- }
-- flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-- }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-
-- final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-- final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-- if (lightLevel > targetLevel) {
-- // it looks like another source propagated here, so re-propagate it
-- if (increaseQueueLength >= increaseQueue.length) {
-- increaseQueue = this.resizeIncreaseQueue();
-- }
-- increaseQueue[increaseQueueLength++] =
-- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-- | ((lightLevel & 0xFL) << (6 + 6 + 16))
-- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-- | (FLAG_RECHECK_LEVEL | flags);
-- continue;
-+ final int 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();
- }
-- final int emittedLight = blockState.getLightEmission() & 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);
-+ 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);
-+ 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;
-+ if (targetLevel > 0) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
- }
-- continue;
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
-+ | flags;
- }
-+ continue;
- }
- } else {
- // we actually need to worry about our state here
- final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-- this.mutablePos2.set(posX, posY, posZ);
- for (final AxisDirection propagate : checkDirections) {
- final int offX = posX + propagate.x;
- final int offY = posY + propagate.y;
-@@ -1446,7 +1356,7 @@ public abstract class StarLightEngine {
- 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(world, this.mutablePos2, propagate.nms) : Shapes.empty();
-+ final VoxelShape fromShape = (((StarlightAbstractBlockState)fromBlock).starlight$isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(propagate.nms) : Shapes.empty();
-
- if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
- continue;
-@@ -1464,104 +1374,59 @@ public abstract class StarLightEngine {
- if (blockState == null) {
- continue;
- }
-- final int opacityCached = ((StarlightAbstractBlockState)blockState).starlight$getOpacityIfCached();
-- if (opacityCached != -1) {
-- final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-- if (lightLevel > targetLevel) {
-- // it looks like another source propagated here, so re-propagate it
-- if (increaseQueueLength >= increaseQueue.length) {
-- increaseQueue = this.resizeIncreaseQueue();
-- }
-- increaseQueue[increaseQueueLength++] =
-- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-- | ((lightLevel & 0xFL) << (6 + 6 + 16))
-- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-- | FLAG_RECHECK_LEVEL;
-- continue;
-- }
-- final int emittedLight = blockState.getLightEmission() & emittedMask;
-- if (emittedLight != 0) {
-- // re-propagate source
-- // 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))
-- | (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
-- }
--
-- currentNibble.set(localIndex, 0);
-- this.postLightUpdate(offX, offY, offZ);
-+ this.lightEmissionPos.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(propagate.getOpposite().nms);
-
-- 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));
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
- continue;
- }
-- continue;
-- } else {
-- this.mutablePos1.set(offX, offY, offZ);
-- long flags = 0;
-- if (((StarlightAbstractBlockState)blockState).starlight$isConditionallyFullOpaque()) {
-- final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
--
-- if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-- continue;
-- }
-- flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-- }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-
-- final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-- final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-- if (lightLevel > targetLevel) {
-- // it looks like another source propagated here, so re-propagate it
-- if (increaseQueueLength >= increaseQueue.length) {
-- increaseQueue = this.resizeIncreaseQueue();
-- }
-- increaseQueue[increaseQueueLength++] =
-- ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-- | ((lightLevel & 0xFL) << (6 + 6 + 16))
-- | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-- | (FLAG_RECHECK_LEVEL | flags);
-- continue;
-+ final int 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();
- }
-- final int emittedLight = blockState.getLightEmission() & 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);
-+ 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);
-+ 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;
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
- }
-- continue;
-+ 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;
- }
- }
- }
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
-index c64ab41198a5e0c7cbcbe6452af11f82f5938862..571db5f9bf94745a8afe2cd313e593fb15db5e37 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/light/StarLightInterface.java
-@@ -1,8 +1,9 @@
- package ca.spottedleaf.moonrise.patches.starlight.light;
-
- import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
--import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
-+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;
-@@ -745,34 +746,34 @@ public final class StarLightInterface {
- super(lightInterface);
- }
-
-- public void lowerPriority(final int chunkX, final int chunkZ, final PrioritisedExecutor.Priority priority) {
-+ 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority 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 PrioritisedExecutor.Priority getPriority(final int chunkX, final int chunkZ) {
-+ 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 PrioritisedExecutor.Priority.COMPLETING;
-+ return Priority.COMPLETING;
- }
-
- @Override
-@@ -816,7 +817,7 @@ public final class StarLightInterface {
- return ret;
- }
-
-- public ServerChunkTasks queueChunkLightTask(final ChunkPos pos, final BooleanSupplier lightTask, final PrioritisedExecutor.Priority priority) {
-+ 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(
-@@ -879,11 +880,11 @@ public final class StarLightInterface {
-
- public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
- final ServerLightQueue queue) {
-- this(chunkCoordinate, lightEngine, queue, PrioritisedExecutor.Priority.NORMAL);
-+ this(chunkCoordinate, lightEngine, queue, Priority.NORMAL);
- }
-
- public ServerChunkTasks(final long chunkCoordinate, final StarLightInterface lightEngine,
-- final ServerLightQueue queue, final PrioritisedExecutor.Priority priority) {
-+ 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),
-@@ -903,19 +904,19 @@ public final class StarLightInterface {
- return this.task.cancel();
- }
-
-- public PrioritisedExecutor.Priority getPriority() {
-+ public Priority getPriority() {
- return this.task.getPriority();
- }
-
-- public void lowerPriority(final PrioritisedExecutor.Priority priority) {
-+ public void lowerPriority(final Priority priority) {
- this.task.lowerPriority(priority);
- }
-
-- public void setPriority(final PrioritisedExecutor.Priority priority) {
-+ public void setPriority(final Priority priority) {
- this.task.setPriority(priority);
- }
-
-- public void raisePriority(final PrioritisedExecutor.Priority priority) {
-+ public void raisePriority(final Priority priority) {
- this.task.raisePriority(priority);
- }
-
-diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..40d004afdc6449530f5bb2d7c7638b8ee3e3a577
---- /dev/null
-+++ b/src/main/java/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/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
-index 57692a503e147a00ac4e1586cd78e12b71a80d3f..689ce367164e79e0426eeecb81dbbc521d4bc742 100644
---- a/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
-+++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/util/SaveUtil.java
-@@ -14,19 +14,20 @@ 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();
-
-- private static final int STARLIGHT_LIGHT_VERSION = 9;
-+ public static final int STARLIGHT_LIGHT_VERSION = 9;
-
- public static int getLightVersion() {
- return STARLIGHT_LIGHT_VERSION;
- }
-
-- private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state";
-- private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state";
-- private static final String STARLIGHT_VERSION_TAG = "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 {
-diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
-index 9bd509915b391e9d382fe47798e2c345b6e59a9a..65c7e7a1b1a5472ee03149bb4ccd9f7d0229069b 100644
---- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
-+++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
-@@ -242,6 +242,7 @@ public class GlobalConfiguration extends ConfigurationPart {
-
- @PostProcess
- private void postProcess() {
-+ ca.spottedleaf.moonrise.common.util.MoonriseCommon.adjustWorkerThreads(this.workerThreads, this.ioThreads);
- ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.init(this);
- }
- }
-diff --git a/src/main/java/net/minecraft/core/MappedRegistry.java b/src/main/java/net/minecraft/core/MappedRegistry.java
-index 71e04e5c1bc0722abf8ca2e0738bd60b6d7ae21c..8e0dfaf02343d74ce786e4fc647bc4c1d73c0014 100644
---- a/src/main/java/net/minecraft/core/MappedRegistry.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 7c388e230c4a88edf6212dd8990e8238d3265ebf..8a66012b7f2396031840c8c718f49f8aab716ee0 100644
---- a/src/main/java/net/minecraft/server/MinecraftServer.java
-+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -1106,7 +1106,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
- }
- // Spigot end
-
-- ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.deinit(); // Paper - rewrite chunk system
-+ // 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() {
-diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index 8e2c0def0d158bdacfa0d455be82db24cc4bdf64..3b5b6799b6e7832d7cc7d73afb5f2ca04fee6060 100644
---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-@@ -252,7 +252,7 @@ public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf
- return false;
- } else {
- ichunkaccess.markUnsaved();
-- LevelChunk chunk = this.getChunkToSend(); // Paper - rewrite chunk system
-+ LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
-
- if (chunk == null) {
- return false;
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 60adb0caf3d6f03a57fc55303852070107f1736e..64b9738584fe2efd1ce4a3d7e2c75e091adc2504 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -910,7 +910,6 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-
- // Paper start - optimise entity tracker
- private void newTrackerTick() {
-- final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers();
- 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();;
-
- final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = entityLookup.trackerEntities;
-@@ -921,21 +920,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- if (tracker == null) {
- continue;
- }
-- ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition()));
-- tracker.serverEntity.sendChanges();
-- }
--
-- // process unloads
-- final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> unloadedEntities = entityLookup.trackerUnloadedEntities;
-- final Entity[] unloadedEntitiesRaw = java.util.Arrays.copyOf(unloadedEntities.getRawDataUnchecked(), unloadedEntities.size());
-- unloadedEntities.clear();
--
-- for (final Entity entity : unloadedEntitiesRaw) {
-- 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();
- }
-- ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$clearPlayers();
- }
- }
- // Paper end - optimise entity tracker
-@@ -1172,6 +1161,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- 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) {
-@@ -1262,20 +1256,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 = ca.spottedleaf.moonrise.common.PlatformHooks.get().modifyEntityTrackingRange(entity, 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/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-index 22157cbc4e4bb1e4010116bdf7429815d46bff2e..23a13bfd23514cde6dcf8d59ba3b43d84f266aad 100644
---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
-@@ -99,7 +99,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
- 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.executor.standard.PrioritisedExecutor.Priority.BLOCKING,
-+ chunkX, chunkZ, toStatus, true, ca.spottedleaf.concurrentutil.util.Priority.BLOCKING,
- completable::complete
- );
-
-@@ -130,6 +130,15 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
- 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
-@@ -254,7 +263,24 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
- @Nullable
- @Override
- public LevelChunk getChunkNow(int chunkX, int chunkZ) {
-- return this.fullChunks.get(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); // Paper - rewrite chunk system
-+ // 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() {
-@@ -311,7 +337,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
-
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(
- chunkX, chunkZ, leastStatus, true,
-- ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER,
-+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER,
- complete
- );
-
-@@ -549,7 +575,8 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
-
- private void getFullChunk(long pos, Consumer<LevelChunk> chunkConsumer) {
- // Paper start - rewrite chunk system
-- final LevelChunk fullChunk = this.getChunkNow(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos));
-+ // note: bypass currentlyLoaded from getChunkNow
-+ final LevelChunk fullChunk = this.fullChunks.get(pos);
- if (fullChunk != null) {
- chunkConsumer.accept(fullChunk);
- }
-diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
-index af27a004f618912d6a5f1d2c82c8e7e9fb00a037..c7523387f0e9bbfe952abd237a936c8319f10200 100644
---- a/src/main/java/net/minecraft/server/level/ServerLevel.java
-+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
-@@ -775,11 +775,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- }
-
- // Paper start - optimise random ticking
-+ private final ca.spottedleaf.moonrise.common.util.SimpleRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleRandom(0L);
-+
- 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 RandomSource random = this.random;
-- final boolean tickFluids = false; // Paper - not configurable - MC-224294
-+ final ca.spottedleaf.moonrise.common.util.SimpleRandom simpleRandom = this.simpleRandom;
-+ final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294();
-
- final ChunkPos cpos = chunk.getPos();
- final int offsetX = cpos.x << 4;
-@@ -789,39 +791,32 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- 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 == null || !section.isRandomlyTickingBlocks()) {
-+ if (!section.isRandomlyTickingBlocks()) {
- continue;
- }
-
-- final ca.spottedleaf.moonrise.common.list.IBlockDataList tickList = ((ca.spottedleaf.moonrise.patches.block_counting.BlockCountingChunkSection)section).moonrise$getTickingBlockList();
-- if (tickList.size() == 0) {
-- 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 = random.nextInt() & ((16 * 16 * 16) - 1);
-+ final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1);
-
- if (index >= tickingBlocks) {
- // most of the time we fall here
- continue;
- }
-
-- final long raw = tickList.getRaw(index);
-- final int location = ca.spottedleaf.moonrise.common.list.IBlockDataList.getLocationFromRaw(raw);
-- final int randomX = (location & 15);
-- final int randomY = ((location >>> (4 + 4)) & 255);
-- final int randomZ = ((location >>> 4) & 15);
-- final BlockState state = states.get(randomX | (randomZ << 4) | (randomY << 8));
-+ 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(randomX | offsetX, randomY | offsetY, randomZ | offsetZ);
-+ final BlockPos pos = new BlockPos((location & 15) | offsetX, ((location >>> (4 + 4)) & 15) | offsetY, ((location >>> 4) & 15) | offsetZ);
-
-- state.randomTick((ServerLevel)(Object)this, pos, random);
-- if (tickFluids) {
-+ state.randomTick((ServerLevel)(Object)this, pos, simpleRandom);
-+ if (doubleTickFluids) {
- final FluidState fluidState = state.getFluidState();
- if (fluidState.isRandomlyTicking()) {
-- fluidState.randomTick((ServerLevel)(Object)this, pos, random);
-+ fluidState.randomTick((ServerLevel)(Object)this, pos, simpleRandom);
- }
- }
- }
-@@ -832,6 +827,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
- // Paper end - optimise random ticking
-
- public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
-+ final ca.spottedleaf.moonrise.common.util.SimpleRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking
- ChunkPos chunkcoordintpair = chunk.getPos();
- boolean flag = this.isRaining();
- int j = chunkcoordintpair.getMinBlockX();
-@@ -839,7 +835,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)) {
-@@ -871,7 +867,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));
- }
- }
-diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
-index 4fe3024e26b56c2d796acf703a1bc200ff309f09..7529b3d90e65036c7bf869af30475932d547b3ab 100644
---- a/src/main/java/net/minecraft/server/players/PlayerList.java
-+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
-@@ -1407,7 +1407,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()) {
-diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java
-index 19661e106612b8e4e152085fb398db7bd06acc23..e4e153cb8899e70273aa150b8ea26907cf68b15c 100644
---- a/src/main/java/net/minecraft/util/BitStorage.java
-+++ b/src/main/java/net/minecraft/util/BitStorage.java
-@@ -24,15 +24,15 @@ public interface BitStorage extends ca.spottedleaf.moonrise.patches.block_counti
- // 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.ints.IntArrayList> moonrise$countEntries() {
-- final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>();
-+ 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.ints.IntArrayList();
-- }).add(index);
-+ return new it.unimi.dsi.fastutil.shorts.ShortArrayList();
-+ }).add((short)index);
- }
-
- return ret;
-diff --git a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java b/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
-index 61dee55417bc802e25b9ba2f271d32d8c12844a9..a8a260a3caaa8e5004069b833ecc8b17b2fc8db5 100644
---- a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java
-index 8acf2f2491a8d9d13392c5e89b2bd5c9918285e1..d99ec470b4653beab630999a5b2c1a6428b20c38 100644
---- a/src/main/java/net/minecraft/util/SimpleBitStorage.java
-+++ b/src/main/java/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
-@@ -364,35 +408,62 @@ public class SimpleBitStorage implements BitStorage {
-
- // Paper start - block counting
- @Override
-- public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() {
-+ 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 = this.mask;
-+ final long mask = (1L << bits) - 1L;
- final int size = this.size;
-
-- // we may be backed by global palette, so limit bits for init capacity
-- final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(
-- 1 << Math.min(6, bits)
-- );
--
-- int index = 0;
--
-- for (long value : this.data) {
-- int li = 0;
-- do {
-- final int paletteIdx = (int)(value & mask);
-- value >>= bits;
-+ 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);
-+ }
-
-- ret.computeIfAbsent(paletteIdx, (final int key) -> {
-- return new it.unimi.dsi.fastutil.ints.IntArrayList();
-- }).add(index);
-+ 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);
-+ }
-
-- ++li;
-- ++index;
-- } while (li < valuesPerLong && index < size);
-+ return ret;
- }
--
-- return ret;
- }
- // Paper end - block counting
-
-diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java
-index 15c5164d0ef41a978c16ee317fa73e97f2480207..1f9c436a632e4f110be61cf76fcfc3b7eb80334e 100644
---- a/src/main/java/net/minecraft/util/ZeroBitStorage.java
-+++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java
-@@ -65,17 +65,17 @@ public class ZeroBitStorage implements BitStorage {
-
- // Paper start - block counting
- @Override
-- public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> moonrise$countEntries() {
-+ public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.shorts.ShortArrayList> moonrise$countEntries() {
- final int size = this.size;
-
-- final int[] raw = new int[size];
-+ final short[] raw = new short[size];
- for (int i = 0; i < size; ++i) {
-- raw[i] = i;
-+ raw[i] = (short)i;
- }
-
-- final it.unimi.dsi.fastutil.ints.IntArrayList coordinates = it.unimi.dsi.fastutil.ints.IntArrayList.wrap(raw, size);
-+ 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.ints.IntArrayList> ret = new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(1);
-+ 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;
- }
-diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
-index 5410a0380c44629f1c9b4f0a8e6017cfc5a31a89..a3b0363fbc207ed9edc8a4d6619b6fff9389a9c7 100644
---- a/src/main/java/net/minecraft/world/entity/Entity.java
-+++ b/src/main/java/net/minecraft/world/entity/Entity.java
-@@ -448,6 +448,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- // 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;
-@@ -468,6 +469,16 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- 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;
-@@ -516,6 +527,54 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- 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;
-
-@@ -1402,73 +1461,67 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- return movement;
- }
-
-- final Level world = this.level;
-- final AABB currBoundingBox = this.getBoundingBox();
--
-- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(currBoundingBox)) {
-- return movement;
-- }
-+ final AABB currentBox = this.getBoundingBox();
-
-- final List<AABB> potentialCollisionsBB = new ArrayList<>();
- final List<VoxelShape> potentialCollisionsVoxel = new ArrayList<>();
-- final double stepHeight = (double)this.maxUpStep();
-- final AABB collisionBox;
-- final boolean onGround = this.onGround;
-+ final List<AABB> potentialCollisionsBB = new ArrayList<>();
-
-+ final AABB initialCollisionBox;
- if (xZero & zZero) {
-- if (movement.y > 0.0) {
-- collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutUpwards(currBoundingBox, movement.y);
-- } else {
-- collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.cutDownwards(currBoundingBox, movement.y);
-- }
-+ // 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 {
-- // note: xZero == false or zZero == false
-- if (stepHeight > 0.0 && (onGround || (movement.y < 0.0))) {
-- // don't bother getting the collisions if we don't need them.
-- if (movement.y <= 0.0) {
-- collisionBox = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.expandUpwards(currBoundingBox.expandTowards(movement.x, movement.y, movement.z), stepHeight);
-- } else {
-- collisionBox = currBoundingBox.expandTowards(movement.x, Math.max(stepHeight, movement.y), movement.z);
-- }
-- } else {
-- collisionBox = currBoundingBox.expandTowards(movement.x, movement.y, movement.z);
-- }
-+ initialCollisionBox = currentBox.expandTowards(movement);
- }
-
-- ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getCollisions(
-- world, (Entity)(Object)this, collisionBox, potentialCollisionsVoxel, potentialCollisionsBB,
-- ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_FLAG_CHECK_BORDER,
-- null, null
-+ final List<AABB> entityAABBs = new ArrayList<>();
-+ ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.getEntityHardCollisions(
-+ this.level, (Entity)(Object)this, initialCollisionBox, entityAABBs, 0, null
- );
-
-- if (potentialCollisionsVoxel.isEmpty() && potentialCollisionsBB.isEmpty()) {
-- return movement;
-- }
-+ 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);
-
-- final Vec3 limitedMoveVector = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(movement, currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB);
-+ final boolean collidedX = collided.x != movement.x;
-+ final boolean collidedY = collided.y != movement.y;
-+ final boolean collidedZ = collided.z != movement.z;
-
-- if (stepHeight > 0.0
-- && (onGround || (limitedMoveVector.y != movement.y && movement.y < 0.0))
-- && (limitedMoveVector.x != movement.x || limitedMoveVector.z != movement.z)) {
-- Vec3 vec3d2 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, stepHeight, movement.z), currBoundingBox, potentialCollisionsVoxel, potentialCollisionsBB);
-- final Vec3 vec3d3 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(0.0, stepHeight, 0.0), currBoundingBox.expandTowards(movement.x, 0.0, movement.z), potentialCollisionsVoxel, potentialCollisionsBB);
-+ final boolean collidedDownwards = collidedY && movement.y < 0.0;
-
-- if (vec3d3.y < stepHeight) {
-- final Vec3 vec3d4 = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(movement.x, 0.0D, movement.z), currBoundingBox.move(vec3d3), potentialCollisionsVoxel, potentialCollisionsBB).add(vec3d3);
-+ final double stepHeight;
-
-- if (vec3d4.horizontalDistanceSqr() > vec3d2.horizontalDistanceSqr()) {
-- vec3d2 = vec3d4;
-- }
-- }
-+ if ((!collidedDownwards && !this.onGround) || (!collidedX && !collidedZ) || (stepHeight = (double)this.maxUpStep()) <= 0.0) {
-+ return collided;
-+ }
-
-- if (vec3d2.horizontalDistanceSqr() > limitedMoveVector.horizontalDistanceSqr()) {
-- return vec3d2.add(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.performCollisions(new Vec3(0.0D, -vec3d2.y + movement.y, 0.0D), currBoundingBox.move(vec3d2), potentialCollisionsVoxel, potentialCollisionsBB));
-- }
-+ 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);
-+ }
-
-- return limitedMoveVector;
-- } else {
-- return limitedMoveVector;
-+ 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 collided;
- // Paper end - optimise collisions
- }
-
-@@ -2836,64 +2889,99 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
- return false;
- }
-
-- final float reducedWith = this.dimensions.width() * 0.8F;
-- final AABB box = AABB.ofSize(this.getEyePosition(), reducedWith, 1.0E-6D, reducedWith);
-+ 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;
-
-- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(box)) {
-+ if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) {
- return false;
- }
-
-- final BlockPos.MutableBlockPos tempPos = new BlockPos.MutableBlockPos();
-+ 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 minX = Mth.floor(box.minX);
-- final int minY = Mth.floor(box.minY);
-- final int minZ = Mth.floor(box.minZ);
-- final int maxX = Mth.floor(box.maxX);
-- final int maxY = Mth.floor(box.maxY);
-- final int maxZ = Mth.floor(box.maxZ);
-+ final int minChunkX = minBlockX >> 4;
-+ final int minChunkY = minBlockY >> 4;
-+ final int minChunkZ = minBlockZ >> 4;
-
-- final net.minecraft.world.level.chunk.ChunkSource chunkProvider = this.level.getChunkSource();
-+ final int maxChunkX = maxBlockX >> 4;
-+ final int maxChunkY = maxBlockY >> 4;
-+ final int maxChunkZ = maxBlockZ >> 4;
-
-- long lastChunkKey = ChunkPos.INVALID_CHUNK_POS;
-- net.minecraft.world.level.chunk.LevelChunk lastChunk = null;
-- for (int fz = minZ; fz <= maxZ; ++fz) {
-- tempPos.setZ(fz);
-- for (int fx = minX; fx <= maxX; ++fx) {
-- final int newChunkX = fx >> 4;
-- final int newChunkZ = fz >> 4;
-- final net.minecraft.world.level.chunk.LevelChunk chunk = lastChunkKey == (lastChunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(newChunkX, newChunkZ)) ?
-- lastChunk : (lastChunk = (net.minecraft.world.level.chunk.LevelChunk)chunkProvider.getChunk(newChunkX, newChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, true));
-- tempPos.setX(fx);
-- for (int fy = minY; fy <= maxY; ++fy) {
-- tempPos.setY(fy);
-+ 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();
-
-- final BlockState state = chunk.getBlockState(tempPos);
-+ 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();
-
-- if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$emptyCollisionShape() || !state.isSuffocating(this.level, tempPos)) {
-+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
-+ final int sectionIdx = currChunkY - minSection;
-+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
- continue;
- }
--
-- // Yes, it does not use the Entity context stuff.
-- final VoxelShape collisionShape = state.getCollisionShape(this.level, tempPos);
--
-- if (collisionShape.isEmpty()) {
-+ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
-+ if (section.hasOnlyAir()) {
-+ // empty
- continue;
- }
-
-- final AABB toCollide = box.move(-(double)fx, -(double)fy, -(double)fz);
-+ 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;
-+ }
-
-- 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;
-- }
-+ // 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);
-
-- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) {
-- return true;
-+ 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;
-+ }
-+ }
- }
-- continue;
- }
- }
- }
-@@ -4508,82 +4596,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();
-
-- vec3d = vec3d.add(vec3d1);
-- ++k1;
-+ // 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;
-+
-+ 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;
-+ }
-+
-+ 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;
- }
-- // CraftBukkit start - store last lava contact location
-- if (tag == FluidTags.LAVA) {
-- this.lastLavaContact = blockposition_mutableblockposition.immutable();
-+
-+ ++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);
-diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-index 4cf6cb0abfeb7065c6d9381fb4194371c0cddc35..5930a430983061afddf20e3208ff2462ca1b78cd 100644
---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java
-@@ -73,8 +73,7 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
-
- 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);
-+ 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);
- }
-@@ -128,9 +127,13 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
- 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 section = this.levelHeightAccessor.getMinSection(); section < this.levelHeightAccessor.getMaxSection(); ++section) {
-- final long sectionPos = SectionPos.asLong(chunkX, section, chunkZ);
-+ for (int sectionY = minY; sectionY <= maxY; ++sectionY) {
-+ final long sectionPos = SectionPos.asLong(chunkX, sectionY, chunkZ);
- this.updateDistanceTracking(sectionPos);
- }
- }
-@@ -139,8 +142,12 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
- 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 = this.levelHeightAccessor.getMinSection(); sectionY < this.levelHeightAccessor.getMaxSection(); ++sectionY) {
-+ 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));
-@@ -316,8 +323,10 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> im
- }
-
- public int sectionsToVillage(SectionPos pos) {
-- this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
-- return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - rewrite chunk system
-+ // 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) {
-diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-index f6f0d7c21ee81ff33d4af350c4d39aadfbe140df..712cbfc100e8aaf612d1d651dae64f57f892a768 100644
---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiSection.java
-@@ -31,7 +31,7 @@ public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.
- private boolean isValid;
-
- // Paper start - rewrite chunk system
-- private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this);;
-+ private final Optional<PoiSection> noAllocOptional = Optional.of((PoiSection)(Object)this);
-
- @Override
- public final boolean moonrise$isEmpty() {
-diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
-index 332dc7e6bdfb5b3741764d4877185a2e86a982f8..40fe47c7c145587ac81f0f15c237ed72ea9c094d 100644
---- a/src/main/java/net/minecraft/world/level/Level.java
-+++ b/src/main/java/net/minecraft/world/level/Level.java
-@@ -264,26 +264,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- }
- // Paper end - rewrite chunk system
- // Paper start - optimise collisions
-- private final int minSection;
-- private final int maxSection;
--
-- @Override
-- public final int moonrise$getMinSection() {
-- return this.minSection;
-- }
--
-- @Override
-- public final int moonrise$getMaxSection() {
-- return this.maxSection;
-- }
--
- /**
- * Route to faster lookup.
- * See {@link EntityGetter#isUnobstructed(Entity, VoxelShape)} for expected behavior
- * @author Spottedleaf
- */
- @Override
-- public final boolean isUnobstructed(final Entity entity) {
-+ public boolean isUnobstructed(final Entity entity) {
- final AABB boundingBox = entity.getBoundingBox();
- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isEmpty(boundingBox)) {
- return false;
-@@ -313,7 +300,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- final Vec3 to = clipContext.getTo();
- final Vec3 from = clipContext.getFrom();
-
-- return net.minecraft.world.phys.BlockHitResult.miss(to, Direction.getNearest(from.x - to.x, from.y - to.y, from.z - to.z), BlockPos.containing(to.x, to.y, to.z));
-+ 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();
-@@ -367,7 +354,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- int lastChunkY = Integer.MIN_VALUE;
- int lastChunkZ = Integer.MIN_VALUE;
-
-- final int minSection = ((ca.spottedleaf.moonrise.patches.collisions.world.CollisionLevel)level).moonrise$getMinSection();
-+ final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(level);
-
- for (;;) {
- currPos.set(currX, currY, currZ);
-@@ -450,7 +437,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- * @author Spottedleaf
- */
- @Override
-- public final net.minecraft.world.phys.BlockHitResult clip(final ClipContext clipContext) {
-+ 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);
- }
-@@ -460,7 +447,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- * @author Spottedleaf
- */
- @Override
-- public final boolean collidesWithSuffocatingBlock(final Entity entity, final AABB box) {
-+ 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) -> {
-@@ -486,8 +473,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- * @author Spottedleaf
- */
- @Override
-- public final 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) {
-+ 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();
- }
-@@ -546,103 +533,139 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- * @author Spottedleaf
- */
- @Override
-- public final java.util.Optional<net.minecraft.core.BlockPos> findSupportingBlock(final Entity entity, final AABB aabb) {
-+ 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 = Mth.floor(aabb.minY - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) - 1;
-- final int maxBlockY = Mth.floor(aabb.maxY + 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;
-
-- ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext collisionContext = null;
--
-- final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
-+ 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();
-
-- LevelChunk lastChunk = null;
-- int lastChunkX = Integer.MIN_VALUE;
-- int lastChunkZ = Integer.MIN_VALUE;
-+ // special cases:
-+ if (minBlockY > maxBlockY) {
-+ // no point in checking
-+ return java.util.Optional.empty();
-+ }
-
-- final ChunkSource chunkSource = this.getChunkSource();
-+ final int minChunkX = minBlockX >> 4;
-+ final int maxChunkX = maxBlockX >> 4;
-
-- for (int currZ = minBlockZ; currZ <= maxBlockZ; ++currZ) {
-- pos.setZ(currZ);
-- for (int currX = minBlockX; currX <= maxBlockX; ++currX) {
-- pos.setX(currX);
-+ final int minChunkY = minBlockY >> 4;
-+ final int maxChunkY = maxBlockY >> 4;
-
-- final int newChunkX = currX >> 4;
-- final int newChunkZ = currZ >> 4;
-+ final int minChunkZ = minBlockZ >> 4;
-+ final int maxChunkZ = maxBlockZ >> 4;
-
-- if (((newChunkX ^ lastChunkX) | (newChunkZ ^ lastChunkZ)) != 0) {
-- lastChunkX = newChunkX;
-- lastChunkZ = newChunkZ;
-- lastChunk = (LevelChunk)chunkSource.getChunk(newChunkX, newChunkZ, ChunkStatus.FULL, false);
-- }
-+ final ChunkSource chunkSource = this.getChunkSource();
-
-- if (lastChunk == null) {
-+ 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;
- }
-- for (int currY = minBlockY; currY <= maxBlockY; ++currY) {
-- int edgeCount = ((currX == minBlockX || currX == maxBlockX) ? 1 : 0) +
-- ((currY == minBlockY || currY == maxBlockY) ? 1 : 0) +
-- ((currZ == minBlockZ || currZ == maxBlockZ) ? 1 : 0);
-- if (edgeCount == 3) {
-- continue;
-- }
-
-- pos.setY(currY);
-+ final net.minecraft.world.level.chunk.LevelChunkSection[] sections = chunk.getSections();
-
-- final double distance = pos.distToCenterSqr(entityPos);
-- if (distance > selectedDistance || (distance == selectedDistance && selected.compareTo(pos) >= 0)) {
-+ // bound y
-+ for (int currChunkY = minChunkY; currChunkY <= maxChunkY; ++currChunkY) {
-+ final int sectionIdx = currChunkY - minSection;
-+ if (sectionIdx < 0 || sectionIdx >= sections.length) {
- continue;
- }
--
-- final BlockState state = ((ca.spottedleaf.moonrise.patches.chunk_getblock.GetBlockChunk)lastChunk).moonrise$getBlock(currX, currY, currZ);
-- if (((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$emptyCollisionShape()) {
-+ final net.minecraft.world.level.chunk.LevelChunkSection section = sections[sectionIdx];
-+ if (section.hasOnlyAir()) {
-+ // empty
- continue;
- }
-
-- VoxelShape blockCollision = ((ca.spottedleaf.moonrise.patches.collisions.block.CollisionBlockState)state).moonrise$getConstantCollisionShape();
--
-- if ((edgeCount != 1 || state.hasLargeCollisionShape()) && (edgeCount != 2 || state.getBlock() == Blocks.MOVING_PISTON)) {
-- if (collisionContext == null) {
-- collisionContext = new ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.LazyEntityCollisionContext(entity);
-- }
--
-- if (blockCollision == null) {
-- blockCollision = state.getCollisionShape((Level)(Object)this, pos, collisionContext);
-- }
--
-- if (blockCollision.isEmpty()) {
-- continue;
-- }
--
-- // avoid VoxelShape#move by shifting the entity collision shape instead
-- final AABB shiftedAABB = aabb.move(-(double)currX, -(double)currY, -(double)currZ);
--
-- 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;
-+ 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;
-+ }
- }
--
-- selected = pos.immutable();
-- selectedDistance = distance;
-- continue;
- }
--
-- if (!ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(blockCollision, shiftedAABB)) {
-- continue;
-- }
--
-- selected = pos.immutable();
-- selectedDistance = distance;
-- continue;
- }
- }
- }
-@@ -651,6 +674,74 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- 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);
-@@ -669,6 +760,15 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- // 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) { // Paper - create paper world config
-+ // 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;
-@@ -1573,7 +1673,16 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
- if (slices == null) {
- return new org.bukkit.entity.Entity[0];
- }
-- return slices.getChunkEntities();
-+
-+ 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
-
-diff --git a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java b/src/main/java/net/minecraft/world/level/biome/BiomeManager.java
-index 01352cc83b25eb0e30b7e0ff521fc7c1b3d5155b..90f8360f547ce709fd13ee34f8e67d8bfa94b498 100644
---- a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-index a4b4fd83d201fff005c738c84fa5c1bc55d670bd..8631655a181735df53f8a02c9eb98f0cc13f55bb 100644
---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
-@@ -838,18 +838,12 @@ public abstract class BlockBehaviour implements FeatureElement {
- private int lightBlock;
-
- // Paper start - rewrite chunk system
-- private int opacityIfCached;
- private boolean isConditionallyFullOpaque;
-
- @Override
- public final boolean starlight$isConditionallyFullOpaque() {
- return this.isConditionallyFullOpaque;
- }
--
-- @Override
-- public final int starlight$getOpacityIfCached() {
-- return this.opacityIfCached;
-- }
- // Paper end - rewrite chunk system
- // Paper start - optimise collisions
- private static final int RANDOM_OFFSET = 704237939;
-@@ -859,16 +853,22 @@ public abstract class BlockBehaviour implements FeatureElement {
- 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 AABB constantAABBCollision;
-
-- private static void initCaches(final VoxelShape shape) {
-+ 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
-@@ -886,6 +886,11 @@ public abstract class BlockBehaviour implements FeatureElement {
- return this.emptyCollisionShape;
- }
-
-+ @Override
-+ public final boolean moonrise$emptyContextCollisionShape() {
-+ return this.emptyConstantCollisionShape;
-+ }
-+
- @Override
- public final int moonrise$uniqueId1() {
- return this.id1;
-@@ -897,14 +902,9 @@ public abstract class BlockBehaviour implements FeatureElement {
- }
-
- @Override
-- public final VoxelShape moonrise$getConstantCollisionShape() {
-+ public final VoxelShape moonrise$getConstantContextCollisionShape() {
- return this.constantCollisionShape;
- }
--
-- @Override
-- public final AABB moonrise$getConstantCollisionAABB() {
-- return this.constantAABBCollision;
-- }
- // Paper end - optimise collisions
-
- protected BlockStateBase(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) {
-@@ -993,39 +993,37 @@ public abstract class BlockBehaviour implements FeatureElement {
- this.lightBlock = ((Block) this.owner).getLightBlock(this.asState());
- // Paper start - rewrite chunk system
- this.isConditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion;
-- this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque ? -1 : this.cache.lightBlock;
- // Paper end - rewrite chunk system
- // Paper start - optimise collisions
- if (this.cache != null) {
- final VoxelShape collisionShape = this.cache.collisionShape;
- try {
- this.constantCollisionShape = this.getCollisionShape(null, null, null);
-- this.constantAABBCollision = this.constantCollisionShape == null ? null : ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)this.constantCollisionShape).moonrise$getSingleAABBRepresentation();
- } catch (final Throwable throwable) {
- this.constantCollisionShape = null;
-- this.constantAABBCollision = 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);
-- if (collisionShape != Shapes.empty() && collisionShape != Shapes.block()) {
-- for (final Direction direction : DIRECTIONS_CACHED) {
-- // initialise the directional face shape cache as well
-- final VoxelShape shape = Shapes.getFaceShape(collisionShape, direction);
-- initCaches(shape);
-- }
-- }
-- if (this.cache.occlusionShapes != null) {
-- for (final VoxelShape shape : this.cache.occlusionShapes) {
-- initCaches(shape);
-- }
-+ initCaches(collisionShape, true);
-+ if (this.constantCollisionShape != null) {
-+ initCaches(this.constantCollisionShape, true);
- }
- } else {
- this.occludesFullBlock = false;
- this.emptyCollisionShape = false;
-+ this.emptyConstantCollisionShape = false;
- this.constantCollisionShape = null;
-- this.constantAABBCollision = 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
- }
-diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java
-index 422b364764e0df16ca250b4939d7b226e69c0840..2df28ffc731bd77e0d7af3541cfd3741aa5af83b 100644
---- a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java
-index ea76aa490358e9e1d13350ba0ea246ec2c423894..98058505d36baf74008da08339afc196713b14a7 100644
---- a/src/main/java/net/minecraft/world/level/block/state/properties/BooleanProperty.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java
-index 85a197232be9377c0313ec00e8f935551e2c60e0..30b2fce9e47ffcc3de1542b1d0f073f5640127a7 100644
---- a/src/main/java/net/minecraft/world/level/block/state/properties/EnumProperty.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java b/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java
-index 55a87592a99105dbf57b26fb6ccba695295fce24..986365acc9983331a7982ea2e1eac2b0efe1506d 100644
---- a/src/main/java/net/minecraft/world/level/block/state/properties/IntegerProperty.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/block/state/properties/Property.java b/src/main/java/net/minecraft/world/level/block/state/properties/Property.java
-index fcf04c5c58ff35d38c5bf0df562ae2f8dc98a0ee..0b116160924300a9d62ad5948bfaf276f0386e4d 100644
---- a/src/main/java/net/minecraft/world/level/block/state/properties/Property.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java b/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java
-index 98dbeaf8bde15940e5b5d5d1f13fd4bb32f0a10d..7beea075b5a7ef738a4ac0558b99f4c5708f2c4a 100644
---- a/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-index a61294befc2f855fcecb2336a2d5444ce60e0a3a..a0e51681731dc7b487d5b14ae0d44a881bd5cb09 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -54,7 +54,7 @@ import net.minecraft.world.ticks.LevelChunkTicks;
- import net.minecraft.world.ticks.TickContainerAccess;
- import org.slf4j.Logger;
-
--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.chunk_getblock.GetBlockChunk { // Paper - rewrite chunk system // Paper - get block chunk optimisation
-+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() {
-@@ -688,7 +688,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
- */
- org.bukkit.Chunk bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
- server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkLoadEvent(bukkitChunk, this.needsDecoration));
-- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesLoadEvent(); // Paper - rewrite chunk system
-+ 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) {
- try (co.aikar.timings.Timing ignored = this.level.timings.chunkLoadPopulate.startTiming()) { // Paper
-@@ -719,7 +719,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
- public void unloadCallback() {
- if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper
- org.bukkit.Server server = this.level.getCraftServer();
-- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(this.locX, this.locZ).getEntityChunk().callEntitiesUnloadEvent(); // Paper - rewrite chunk system
-+ 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, true); // Paper - rewrite chunk system - force save to true so that mustNotSave is correctly set below
- server.getPluginManager().callEvent(unloadEvent);
-diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-index 161211124f3f8390530af7ab21f3a0f1025209c5..4167ed830382c6a76bb281e9d753919925c6bd00 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
-@@ -26,15 +26,17 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
- private PalettedContainer<Holder<Biome>> biomes; // CraftBukkit - read/write
-
- // Paper start - block counting
-- private static final it.unimi.dsi.fastutil.ints.IntArrayList FULL_LIST = new it.unimi.dsi.fastutil.ints.IntArrayList(16*16*16);
-+ private static final it.unimi.dsi.fastutil.shorts.ShortArrayList FULL_LIST = new it.unimi.dsi.fastutil.shorts.ShortArrayList(16*16*16);
- static {
-- for (int i = 0; i < (16*16*16); ++i) {
-+ for (short i = 0; i < (16*16*16); ++i) {
- FULL_LIST.add(i);
- }
- }
-
-- private int specialCollidingBlocks;
-- private final ca.spottedleaf.moonrise.common.list.IBlockDataList tickingBlocks = new ca.spottedleaf.moonrise.common.list.IBlockDataList();
-+ 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 int moonrise$getSpecialCollidingBlocks() {
-@@ -42,7 +44,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
- }
-
- @Override
-- public final ca.spottedleaf.moonrise.common.list.IBlockDataList moonrise$getTickingBlockList() {
-+ public final ca.spottedleaf.moonrise.common.list.ShortList moonrise$getTickingBlockList() {
- return this.tickingBlocks;
- }
- // Paper end - block counting
-@@ -86,6 +88,45 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
- 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;
-
-@@ -105,7 +146,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
- }
- }
-
-- if (!fluid.isEmpty()) {
-+ if (!!fluid.isRandomlyTicking()) { // Paper - block counting
- --this.tickingFluidCount;
- }
-
-@@ -116,25 +157,11 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
- }
- }
-
-- if (!fluid1.isEmpty()) {
-+ if (!!fluid1.isRandomlyTicking()) { // Paper - block counting
- ++this.tickingFluidCount;
- }
-
-- // Paper start - block counting
-- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(iblockdata1)) {
-- --this.specialCollidingBlocks;
-- }
-- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) {
-- ++this.specialCollidingBlocks;
-- }
--
-- if (iblockdata1.isRandomlyTicking()) {
-- this.tickingBlocks.remove(x, y, z);
-- }
-- if (state.isRandomlyTicking()) {
-- this.tickingBlocks.add(x, y, z, state);
-- }
-- // Paper end - block counting
-+ this.updateBlockCallback(x, y, z, state, iblockdata1); // Paper - block counting
-
- return iblockdata1;
- }
-@@ -170,7 +197,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
- final int paletteSize = palette.getSize();
- final net.minecraft.util.BitStorage storage = data.storage();
-
-- final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<it.unimi.dsi.fastutil.ints.IntArrayList> counts;
-+ 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);
-@@ -178,10 +205,10 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
- 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.ints.IntArrayList>> iterator = counts.int2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
-- final it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry<it.unimi.dsi.fastutil.ints.IntArrayList> entry = iterator.next();
-+ 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.ints.IntArrayList coordinates = entry.getValue();
-+ final it.unimi.dsi.fastutil.shorts.ShortArrayList coordinates = entry.getValue();
- final int paletteCount = coordinates.size();
-
- final BlockState state = palette.valueFor(paletteIdx);
-@@ -191,25 +218,30 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
- }
-
- if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isSpecialCollidingBlock(state)) {
-- this.specialCollidingBlocks += paletteCount;
-+ this.specialCollidingBlocks += (short)paletteCount;
- }
-- this.nonEmptyBlockCount += paletteCount;
-+ this.nonEmptyBlockCount += (short)paletteCount;
- if (state.isRandomlyTicking()) {
-- this.tickingBlockCount += paletteCount;
-- final int[] raw = coordinates.elements();
-+ this.tickingBlockCount += (short)paletteCount;
-+ final short[] raw = coordinates.elements();
-+ final int rawLen = raw.length;
-+
-+ final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks;
-+
-+ tickingBlocks.setMinCapacity(Math.min((rawLen + tickingBlocks.size()) * 3 / 2, 16*16*16));
-
- java.util.Objects.checkFromToIndex(0, paletteCount, raw.length);
- for (int i = 0; i < paletteCount; ++i) {
-- this.tickingBlocks.add(raw[i], state);
-+ tickingBlocks.add(raw[i]);
- }
- }
-
- final FluidState fluid = state.getFluidState();
-
- if (!fluid.isEmpty()) {
-- //this.nonEmptyBlockCount += count; // fix vanilla bug: make non empty block count correct
-+ //this.nonEmptyBlockCount += count; // fix vanilla bug: make non-empty block count correct
- if (fluid.isRandomlyTicking()) {
-- this.tickingFluidCount += paletteCount;
-+ this.tickingFluidCount += (short)paletteCount;
- }
- }
- }
-@@ -232,7 +264,11 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
-
- datapaletteblock.read(buf);
- this.biomes = datapaletteblock;
-- this.recalcBlockCounts(); // Paper - block counting
-+ // 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/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java b/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java
-index bc4d9452bbeb05a691fd285603e49491f41d3ad2..f8d9892970c9092f7cc84434d4fbf34354ce1195 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/Palette.java b/src/main/java/net/minecraft/world/level/chunk/Palette.java
-index b8922e4a13df535cdc5701e893a6e460b33ff90d..100807f8b8337f56f49cdb818ccc75be2f08ecd1 100644
---- a/src/main/java/net/minecraft/world/level/chunk/Palette.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-index b46c58c952e183bd74854c3eb70d64979af70f18..533167eaa8bd39006fb1c7e193c81359973da9af 100644
---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-@@ -71,6 +71,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
-+
- public PalettedContainer(
- IdMap<T> idList,
- PalettedContainer.Strategy paletteProvider,
-@@ -81,12 +108,14 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- this.registry = idList;
- this.strategy = paletteProvider;
- this.data = new PalettedContainer.Data<>(dataProvider, storage, dataProvider.factory().create(dataProvider.bits(), idList, this, paletteEntries));
-+ this.updateData(this.data); // Paper - optimise palette reads
- }
-
- private PalettedContainer(IdMap<T> idList, PalettedContainer.Strategy paletteProvider, PalettedContainer.Data<T> data) {
- this.registry = idList;
- this.strategy = paletteProvider;
- this.data = data;
-+ this.updateData(this.data); // Paper - optimise palette reads
- }
-
- private PalettedContainer(PalettedContainer<T> container) {
-@@ -100,6 +129,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) {
-@@ -115,6 +145,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- PalettedContainer.Data<T> data2 = this.createOrReuseData(data, newBits);
- data2.copyFrom(data.palette, data.storage);
- this.data = data2;
-+ this.updateData(this.data); // Paper - optimise palette reads
- return data2.palette.idFor(object);
- }
-
-@@ -136,9 +167,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) {
-@@ -162,8 +196,10 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- }
-
- public T get(int index) { // Paper - public
-- PalettedContainer.Data<T> data = this.data;
-- return data.palette.valueFor(data.storage.get(index));
-+ // 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
-@@ -183,6 +219,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- data.palette.read(buf);
- buf.readLongArray(data.storage.getRaw());
- this.data = data;
-+ this.updateData(this.data); // Paper - optimise palette reads
- } finally {
- this.release();
- }
-@@ -323,7 +360,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/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java b/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java
-index a45e6410600afc5464e5d29932c193786ce0a6fb..a1ba68c95c2cdebdc0d7782cce7895529918073c 100644
---- a/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
-index f1237f6fd6414900ffbad0caee31aa83310eeef4..8071ce70d66909bb4bda45792bf329a939d6f918 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java
-@@ -25,7 +25,7 @@ import net.minecraft.util.profiling.jfr.JvmProfiler;
- 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;
-@@ -49,6 +49,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.getSelected(), dsync);
- }
-@@ -220,6 +235,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])) {
-@@ -443,10 +468,29 @@ public class RegionFile implements AutoCloseable {
- }
-
- public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Paper - don't write garbage data to disk if writing serialization fails
-- 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);
-@@ -480,7 +524,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/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-index 18054304e08c8a6346c0135a0e6a68e77fe5c37c..9dbc9e2f9d5aab71720bb81803efe76e2f361f04 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
-@@ -28,8 +28,8 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
-
- // Paper start - rewrite chunk system
- private static final int REGION_SHIFT = 5;
-- private static final int MAX_NON_EXISTING_CACHE = 1024 * 64;
-- private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(MAX_NON_EXISTING_CACHE+1);
-+ 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";
- }
-@@ -104,6 +104,97 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
-
- 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
-@@ -112,6 +203,12 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
- this.info = storageKey;
- }
-
-+ // Paper start - rewrite chunk system
-+ public RegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException {
-+ return this.getRegionFile(chunkcoordintpair, false);
-+ }
-+ // Paper end - rewrite chunk system
-+
- public RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - public
- // Paper start - rewrite chunk system
- if (existingOnly) {
-diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
-index 261e5994d13f8bc30490b86691c80c0a21e7640a..f4fbcbb8ff6d2677af1a02a0801a323c06dce9b1 100644
---- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/level/material/FluidState.java b/src/main/java/net/minecraft/world/level/material/FluidState.java
-index 87adfe152abd1b8b4d547034576883c5d1cdf134..2d50d72bf026d0cf9c546a3c6fc1859379bfd805 100644
---- a/src/main/java/net/minecraft/world/level/material/FluidState.java
-+++ b/src/main/java/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/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-index 1d36f8dcffd22cf844448d3d8351fb8718cf5227..fbe0c4b0fdbb992b7002f6afe1e74d63cbb420f2 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
-@@ -57,7 +57,7 @@ public abstract class DiscreteVoxelShape implements ca.spottedleaf.moonrise.patc
- }
- }
-
-- final boolean hasSingleAABB = sizeX == 1 && sizeY == 1 && sizeZ == 1 && !isEmpty && discreteVoxelShape.isFull(0, 0, 0);
-+ 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);
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-index 672a2038c6d8b31090403766460c6149a75adf8b..513bed7f11aee667c87046db4cf912b80e8f3638 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
-@@ -180,13 +180,13 @@ public final class Shapes {
- final VoxelShape first = tmp[i];
- final VoxelShape second = tmp[next];
-
-- tmp[newSize++] = Shapes.or(first, second);
-+ tmp[newSize++] = Shapes.joinUnoptimized(first, second, BooleanOp.OR);
- }
- }
- size = newSize;
- }
-
-- return tmp[0];
-+ return tmp[0].optimize();
- // Paper end - optimise collisions
- }
-
-diff --git a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-index d850a7de74150a04622da71d9614320f3d5d69e8..3f8e7e29c3e52211a29e6f0a32890f6b53bfd9a8 100644
---- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-+++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
-@@ -162,13 +162,13 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
-
- if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) {
- if (DoubleMath.fuzzyEquals(this.max(axis), 1.0, ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) {
-- ret = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, this.shape.getSize(axis) - 1));
-+ 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 = tryForceBlock(new SliceShape((VoxelShape)(Object)this, axis, 0));
-+ ret = ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.sliceShape((VoxelShape)(Object)this, axis, 0);
- } else {
- ret = Shapes.empty();
- }
-@@ -179,23 +179,6 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
- return ret;
- }
-
-- private static VoxelShape tryForceBlock(final VoxelShape other) {
-- if (other == Shapes.block()) {
-- return other;
-- }
--
-- final AABB otherAABB = ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)other).moonrise$getSingleAABBRepresentation();
-- if (otherAABB == null) {
-- return other;
-- }
--
-- if (((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)Shapes.block()).moonrise$getSingleAABBRepresentation().equals(otherAABB)) {
-- return Shapes.block();
-- }
--
-- return other;
-- }
--
- private boolean computeOccludesFullBlock() {
- if (this.isEmpty) {
- this.occludesFullBlock = Boolean.FALSE;
-@@ -293,18 +276,21 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
- return result;
- }
-
-- private static DoubleList offsetList(final DoubleList src, final double by) {
-- if (src instanceof OffsetDoubleList offsetDoubleList) {
-- return new OffsetDoubleList(offsetDoubleList.delegate, by + offsetDoubleList.offset);
-+ 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(src, by);
-+ return new OffsetDoubleList(wrap, by);
- }
-
- private List<AABB> toAabbsUncached() {
-- final List<AABB> ret = new java.util.ArrayList<>();
-+ 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;
-@@ -426,6 +412,26 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
- 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, (positiveDir ? (1.0 - ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON) : (0.0 + ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON)) - offset,
-+ 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) {
-@@ -517,20 +523,32 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
- }
-
- 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);
-@@ -551,9 +569,9 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
-
- final ArrayVoxelShape ret = new ArrayVoxelShape(
- this.shape,
-- offsetList(this.getCoords(Direction.Axis.X), x),
-- offsetList(this.getCoords(Direction.Axis.Y), y),
-- offsetList(this.getCoords(Direction.Axis.Z), z)
-+ 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;
-@@ -578,6 +596,11 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
-
- 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);
-@@ -704,7 +727,32 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
- }
-
- 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, value - this.offsetX, 0, values.length - 1
-+ );
-+ }
-+ case Y: {
-+ final double[] values = this.rootCoordinatesY;
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor(
-+ values, value - this.offsetY, 0, values.length - 1
-+ );
-+ }
-+ case Z: {
-+ final double[] values = this.rootCoordinatesZ;
-+ return ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.findFloor(
-+ values, value - this.offsetZ, 0, values.length - 1
-+ );
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown axis: " + axis);
-+ }
-+ }
-+ // Paper end - optimise collisions
- }
-
- @Nullable
-@@ -727,13 +775,13 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
- final AABB singleAABB = this.singleAABBRepresentation;
- if (singleAABB != null) {
- if (singleAABB.contains(fromBehindOffsetX, fromBehindOffsetY, fromBehindOffsetZ)) {
-- return new BlockHitResult(fromBehind, Direction.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true);
-+ 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.getNearest(directionOpposite.x, directionOpposite.y, directionOpposite.z).getOpposite(), offset, true);
-+ 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);
-@@ -786,20 +834,24 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
- }
- }
-
-- 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() {