From 249fdac7ee745e8073298ee44592e79fe13615ea Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Mon, 21 Oct 2024 21:01:00 -0700
Subject: [PATCH] Start Moonrise update

This is based on Moonrise's 1.21.2 branch, but this on
1.21.1 so some diffs cannot be applied (and this doesn't
compile).

See moonrise_update_1_21_2.txt for progress
---
 moonrise_update_1_21_2.txt                    |    43 +
 patches/server/1070-fixup-MC-Utils.patch      |    87 +
 patches/server/Improve-ServerGUI.patch        |   647 +-
 ...ble-implementation-for-blockstate-st.patch |   345 +
 patches/server/fixup-ConcurrentUtil.patch     |  6177 +++++++
 .../fixup-Moonrise-optimisation-patches.patch | 14304 ++++++++++++++++
 ...ptimize-BlockPosition-helper-methods.patch |    64 +
 7 files changed, 21344 insertions(+), 323 deletions(-)
 create mode 100644 moonrise_update_1_21_2.txt
 create mode 100644 patches/server/1070-fixup-MC-Utils.patch
 create mode 100644 patches/server/Revert-Custom-table-implementation-for-blockstate-st.patch
 create mode 100644 patches/server/fixup-ConcurrentUtil.patch
 create mode 100644 patches/server/fixup-Moonrise-optimisation-patches.patch
 create mode 100644 patches/server/fixup-Optimize-BlockPosition-helper-methods.patch

diff --git a/moonrise_update_1_21_2.txt b/moonrise_update_1_21_2.txt
new file mode 100644
index 0000000000..87e5b1546a
--- /dev/null
+++ b/moonrise_update_1_21_2.txt
@@ -0,0 +1,43 @@
+reference comparison:
+https://github.com/Tuinity/Moonrise/compare/6a2c6d27df11d417c1fefa749109d8e87599e8c2...03784b8c69c299db4af4f9984565e5752617d9dc
+need to compare the diffs
+
+
+add notes to moonrise patch:
+ - implemented fast palette patch
+ - implemented better bitstorage magic patch
+ - implemented blockstate property patch (replaced old paper one)
+ - implemented fluid patch
+
+
+todo:
+- double check that the misc changes commit on dev/1.21.2 moonrise is applied
+- implement platformhooks
+- move common diff from moonrise patch to mcutil patch
+- delete old block state table patch
+- in StateHolder, implement getNullableValue from blockstate_propertyaccess
+- ChunkEntitySlices getChunkEntities(), callEntitiesLoadEvent(), callEntitiesUnloadEvent()
+- in ChunkEntitySlices, implement modifySavedEntities() by copying from old
+- in ChunkEntitySlices, implement unload() Entity.setRemoved()
+- change PersistentEntitySectionManager addEntity chunk system call to have event=true 
+- implement PlayerChunkUnloadEvent in PlatformHooks#onChunkUnWatch
+- make sure chunk pos is passed in PlatformHooks#postLoadProtoChunk
+- implement chunk_system.ChunkMapMixin diff from reference 
+- implement chunk_system.ChunkStorageMixin diff from reference
+- implement chunk_system.DistanceManagerMixin diff from reference
+- implement chunk_system.GenerationChunkHolderMixin diff from reference
+- implement chunk_system.LevelChunkMixin diff from reference
+- implement chunk_system.LevelMixin diff from reference
+- implement chunk_system.SectionStorageMixin diff from reference
+- implement chunk_system.SerializableChunkDataMixin diff from reference
+- implement chunk_system.ServerLevelMixin diff from reference
+- implement chunk_tick_iteration
+- implement collisions.ServerExplosionMixin diff from reference
+- implement modifyEntityTrackingRange with org.spigotmc.TrackingRange.getEntityTrackingRange
+- implement random_ticking.BiomeMixin diff from reference
+- implement starlight.LevelLightEngineMixin diff from reference
+- implement starlight.ThreadedLevelLightEngineMixin diff from reference
+- implement starlight.ChunkSerializerMixin diff from reference
+- implement starlight.SerializableChunkData$SectionData diff from reference
+- implement starlight.SerializableChunkDataMixin diff from reference
+
diff --git a/patches/server/1070-fixup-MC-Utils.patch b/patches/server/1070-fixup-MC-Utils.patch
new file mode 100644
index 0000000000..ac97458423
--- /dev/null
+++ b/patches/server/1070-fixup-MC-Utils.patch
@@ -0,0 +1,87 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <Spottedleaf@users.noreply.github.com>
+Date: Mon, 21 Oct 2024 12:21:54 -0700
+Subject: [PATCH] fixup! MC Utils
+
+
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+@@ -0,0 +0,0 @@
+ package ca.spottedleaf.moonrise.common.util;
+ 
+-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.util.Priority;
+ 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;
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+     private static final Logger LOGGER = LogUtils.getLogger();
+ 
+     public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
+-        scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
++        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) {
+         ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkTask(chunkX, chunkZ, run, priority);
+     }
+ 
+     public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
+-                                         final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority,
++                                         final ChunkStatus toStatus, final boolean addTicket, final Priority priority,
+                                          final Consumer<ChunkAccess> onComplete) {
+         ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, gen, toStatus, addTicket, priority, onComplete);
+     }
+ 
+     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) {
+         ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleChunkLoad(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+     }
+ 
+     public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
+                                             final FullChunkStatus toStatus, final boolean addTicket,
+-                                            final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
++                                            final Priority priority, final Consumer<LevelChunk> onComplete) {
+         ((ChunkSystemServerLevel)level).moonrise$getChunkTaskScheduler().scheduleTickingState(chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
+     }
+ 
+@@ -0,0 +0,0 @@ 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) {
+         return true;
+     }
+ 
+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
++++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+@@ -0,0 +0,0 @@ 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/patches/server/Improve-ServerGUI.patch b/patches/server/Improve-ServerGUI.patch
index 35de6d4971..77f9c8782b 100644
--- a/patches/server/Improve-ServerGUI.patch
+++ b/patches/server/Improve-ServerGUI.patch
@@ -102,329 +102,330 @@ new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
 GIT binary patch
 literal 16900
-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
-LFIp>X81%mY^Bg|U
+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
+XXyugpZ1#qMfq}u()z4*}Q$iB}^Bg|U
 
 literal 0
 HcmV?d00001
diff --git a/patches/server/Revert-Custom-table-implementation-for-blockstate-st.patch b/patches/server/Revert-Custom-table-implementation-for-blockstate-st.patch
new file mode 100644
index 0000000000..bab62f109f
--- /dev/null
+++ b/patches/server/Revert-Custom-table-implementation-for-blockstate-st.patch
@@ -0,0 +1,345 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <Spottedleaf@users.noreply.github.com>
+Date: Mon, 21 Oct 2024 12:52:44 -0700
+Subject: [PATCH] Revert "Custom table implementation for blockstate state
+ lookups"
+
+This reverts commit 14a7e6521ba0ce6dc8ebf98a1ccce59a5ec6a194.
+
+TODO Replace via deleting the patch
+
+diff --git a/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java b/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/io/papermc/paper/util/table/ZeroCollidingReferenceStateTable.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package io.papermc.paper.util.table;
+-
+-import com.google.common.collect.Table;
+-import net.minecraft.world.level.block.state.StateHolder;
+-import net.minecraft.world.level.block.state.properties.Property;
+-import java.util.Collection;
+-import java.util.HashSet;
+-import java.util.Map;
+-import java.util.Set;
+-
+-public final class ZeroCollidingReferenceStateTable {
+-
+-    // upper 32 bits: starting index
+-    // lower 32 bits: bitset for contained ids
+-    protected final long[] this_index_table;
+-    protected final Comparable<?>[] this_table;
+-    protected final StateHolder<?, ?> this_state;
+-
+-    protected long[] index_table;
+-    protected StateHolder<?, ?>[][] value_table;
+-
+-    public ZeroCollidingReferenceStateTable(final StateHolder<?, ?> state, final Map<Property<?>, Comparable<?>> this_map) {
+-        this.this_state = state;
+-        this.this_index_table = this.create_table(this_map.keySet());
+-
+-        int max_id = -1;
+-        for (final Property<?> property : this_map.keySet()) {
+-            final int id = lookup_vindex(property, this.this_index_table);
+-            if (id > max_id) {
+-                max_id = id;
+-            }
+-        }
+-
+-        this.this_table = new Comparable[max_id + 1];
+-        for (final Map.Entry<Property<?>, Comparable<?>> entry : this_map.entrySet()) {
+-            this.this_table[lookup_vindex(entry.getKey(), this.this_index_table)] = entry.getValue();
+-        }
+-    }
+-
+-    public void loadInTable(final Table<Property<?>, Comparable<?>, StateHolder<?, ?>> table,
+-                            final Map<Property<?>, Comparable<?>> this_map) {
+-        final Set<Property<?>> combined = new HashSet<>(table.rowKeySet());
+-        combined.addAll(this_map.keySet());
+-
+-        this.index_table = this.create_table(combined);
+-
+-        int max_id = -1;
+-        for (final Property<?> property : combined) {
+-            final int id = lookup_vindex(property, this.index_table);
+-            if (id > max_id) {
+-                max_id = id;
+-            }
+-        }
+-
+-        this.value_table = new StateHolder[max_id + 1][];
+-
+-        final Map<Property<?>, Map<Comparable<?>, StateHolder<?, ?>>> map = table.rowMap();
+-        for (final Property<?> property : map.keySet()) {
+-            final Map<Comparable<?>, StateHolder<?, ?>> propertyMap = map.get(property);
+-
+-            final int id = lookup_vindex(property, this.index_table);
+-            final StateHolder<?, ?>[] states = this.value_table[id] = new StateHolder[property.getPossibleValues().size()];
+-
+-            for (final Map.Entry<Comparable<?>, StateHolder<?, ?>> entry : propertyMap.entrySet()) {
+-                if (entry.getValue() == null) {
+-                    // TODO what
+-                    continue;
+-                }
+-
+-                states[((Property)property).getIdFor(entry.getKey())] = entry.getValue();
+-            }
+-        }
+-
+-
+-        for (final Map.Entry<Property<?>, Comparable<?>> entry : this_map.entrySet()) {
+-            final Property<?> property = entry.getKey();
+-            final int index = lookup_vindex(property, this.index_table);
+-
+-            if (this.value_table[index] == null) {
+-                this.value_table[index] = new StateHolder[property.getPossibleValues().size()];
+-            }
+-
+-            this.value_table[index][((Property)property).getIdFor(entry.getValue())] = this.this_state;
+-        }
+-    }
+-
+-
+-    protected long[] create_table(final Collection<Property<?>> collection) {
+-        int max_id = -1;
+-        for (final Property<?> property : collection) {
+-            final int id = property.getId();
+-            if (id > max_id) {
+-                max_id = id;
+-            }
+-        }
+-
+-        final long[] ret = new long[((max_id + 1) + 31) >>> 5]; // ceil((max_id + 1) / 32)
+-
+-        for (final Property<?> property : collection) {
+-            final int id = property.getId();
+-
+-            ret[id >>> 5] |= (1L << (id & 31));
+-        }
+-
+-        int total = 0;
+-        for (int i = 1, len = ret.length; i < len; ++i) {
+-            ret[i] |= (long)(total += Long.bitCount(ret[i - 1] & 0xFFFFFFFFL)) << 32;
+-        }
+-
+-        return ret;
+-    }
+-
+-    public Comparable<?> get(final Property<?> state) {
+-        final Comparable<?>[] table = this.this_table;
+-        final int index = lookup_vindex(state, this.this_index_table);
+-
+-        if (index < 0 || index >= table.length) {
+-            return null;
+-        }
+-        return table[index];
+-    }
+-
+-    public StateHolder<?, ?> get(final Property<?> property, final Comparable<?> with) {
+-        final int withId = ((Property)property).getIdFor(with);
+-        if (withId < 0) {
+-            return null;
+-        }
+-
+-        final int index = lookup_vindex(property, this.index_table);
+-        final StateHolder<?, ?>[][] table = this.value_table;
+-        if (index < 0 || index >= table.length) {
+-            return null;
+-        }
+-
+-        final StateHolder<?, ?>[] values = table[index];
+-
+-        if (withId >= values.length) {
+-            return null;
+-        }
+-
+-        return values[withId];
+-    }
+-
+-    protected static int lookup_vindex(final Property<?> property, final long[] index_table) {
+-        final int id = property.getId();
+-        final long bitset_mask = (1L << (id & 31));
+-        final long lower_mask = bitset_mask - 1;
+-        final int index = id >>> 5;
+-        if (index >= index_table.length) {
+-            return -1;
+-        }
+-        final long index_value = index_table[index];
+-        final long contains_check = ((index_value & bitset_mask) - 1) >> (Long.SIZE - 1); // -1L if doesn't contain
+-
+-        // index = total bits set in lower table values (upper 32 bits of index_value) plus total bits set in lower indices below id
+-        // contains_check is 0 if the bitset had id set, else it's -1: so index is unaffected if contains_check == 0,
+-        // otherwise it comes out as -1.
+-        return (int)(((index_value >>> 32) + Long.bitCount(index_value & lower_mask)) | contains_check);
+-    }
+-}
+diff --git a/src/main/java/net/minecraft/world/level/block/state/StateHolder.java b/src/main/java/net/minecraft/world/level/block/state/StateHolder.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+     private final Reference2ObjectArrayMap<Property<?>, Comparable<?>> values;
+     private Table<Property<?>, Comparable<?>, S> neighbours;
+     protected final MapCodec<S> propertiesCodec;
+-    protected final io.papermc.paper.util.table.ZeroCollidingReferenceStateTable optimisedTable; // Paper - optimise state lookup
+ 
+     protected StateHolder(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<S> codec) {
+         this.owner = owner;
+         this.values = propertyMap;
+         this.propertiesCodec = codec;
+-        this.optimisedTable = new io.papermc.paper.util.table.ZeroCollidingReferenceStateTable(this, propertyMap); // Paper - optimise state lookup
+     }
+ 
+     public <T extends Comparable<T>> S cycle(Property<T> property) {
+@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+     }
+ 
+     public <T extends Comparable<T>> boolean hasProperty(Property<T> property) {
+-        return this.optimisedTable.get(property) != null; // Paper - optimise state lookup
++        return this.values.containsKey(property);
+     }
+ 
+     public <T extends Comparable<T>> T getValue(Property<T> property) {
+-        Comparable<?> comparable = this.optimisedTable.get(property); // Paper - optimise state lookup
++        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 {
+@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+     }
+ 
+     public <T extends Comparable<T>> Optional<T> getOptionalValue(Property<T> property) {
+-        Comparable<?> comparable = this.optimisedTable.get(property); // Paper - optimise state lookup
++        Comparable<?> comparable = this.values.get(property);
+         return comparable == null ? Optional.empty() : Optional.of(property.getValueClass().cast(comparable));
+     }
+ 
+     public <T extends Comparable<T>, V extends T> S setValue(Property<T> property, V value) {
+-        // Paper start - optimise state lookup
+-        final S ret = (S)this.optimisedTable.get(property, value);
+-        if (ret == null) {
+-            throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed 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 if (comparable.equals(value)) {
++            return (S)this;
++        } else {
++            S object = this.neighbours.get(property, value);
++            if (object == null) {
++                throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value");
++            } else {
++                return object;
++            }
+         }
+-        return ret;
+-        // Paper end - optimise state lookup
+     }
+ 
+     public <T extends Comparable<T>, V extends T> S trySetValue(Property<T> property, V value) {
+@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+                 }
+             }
+ 
+-            this.neighbours = (Table<Property<?>, Comparable<?>, S>)(table.isEmpty() ? table : ArrayTable.create(table)); this.optimisedTable.loadInTable((Table)this.neighbours, this.values); // Paper - optimise state lookup
++            this.neighbours = (Table<Property<?>, Comparable<?>, S>)(table.isEmpty() ? table : ArrayTable.create(table));
+         }
+     }
+ 
+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ import java.util.Optional;
+ public class BooleanProperty extends Property<Boolean> {
+     private final ImmutableSet<Boolean> values = ImmutableSet.of(true, false);
+ 
+-    // Paper start - optimise iblockdata state lookup
+-    @Override
+-    public final int getIdFor(final Boolean value) {
+-        return value.booleanValue() ? 1 : 0;
+-    }
+-    // Paper end - optimise iblockdata state lookup
+-
+     protected BooleanProperty(String name) {
+         super(name, Boolean.class);
+     }
+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ public class EnumProperty<T extends Enum<T> & StringRepresentable> extends Prope
+     private final ImmutableSet<T> values;
+     private final Map<String, T> names = Maps.newHashMap();
+ 
+-    // Paper start - optimise iblockdata state lookup
+-    private int[] idLookupTable;
+-
+-    @Override
+-    public final int getIdFor(final T value) {
+-        return this.idLookupTable[value.ordinal()];
+-    }
+-    // Paper end - optimise iblockdata state lookup
+-
+     protected EnumProperty(String name, Class<T> type, Collection<T> values) {
+         super(name, type);
+         this.values = ImmutableSet.copyOf(values);
+@@ -0,0 +0,0 @@ public class EnumProperty<T extends Enum<T> & StringRepresentable> extends Prope
+ 
+             this.names.put(string, enum_);
+         }
+-        // Paper start - optimise BlockState lookup
+-        int id = 0;
+-        this.idLookupTable = new int[type.getEnumConstants().length];
+-        java.util.Arrays.fill(this.idLookupTable, -1);
+-        for (final T value : this.getPossibleValues()) {
+-            this.idLookupTable[value.ordinal()] = id++;
+-        }
+-        // Paper end - optimise BlockState lookup
+     }
+ 
+     @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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ public class IntegerProperty extends Property<Integer> {
+     public final int min;
+     public final int max;
+ 
+-    // Paper start - optimise iblockdata state lookup
+-    @Override
+-    public final int getIdFor(final Integer value) {
+-        final int val = value.intValue();
+-        final int ret = val - this.min;
+-
+-        return ret | ((this.max - ret) >> 31);
+-    }
+-    // Paper end - optimise iblockdata state lookup
+-
+     protected IntegerProperty(String name, int min, int max) {
+         super(name, Integer.class);
+         if (min < 0) {
+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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 iblockdata state lookup
+-    private static final java.util.concurrent.atomic.AtomicInteger ID_GENERATOR = new java.util.concurrent.atomic.AtomicInteger();
+-    private final int id = ID_GENERATOR.getAndIncrement();
+-
+-    public final int getId() {
+-        return this.id;
+-    }
+-
+-    public abstract int getIdFor(final T value);
+-    // Paper end - optimise state lookup
+-
+     protected Property(String name, Class<T> type) {
+         this.clazz = type;
+         this.name = name;
diff --git a/patches/server/fixup-ConcurrentUtil.patch b/patches/server/fixup-ConcurrentUtil.patch
new file mode 100644
index 0000000000..640fe960cb
--- /dev/null
+++ b/patches/server/fixup-ConcurrentUtil.patch
@@ -0,0 +1,6177 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <Spottedleaf@users.noreply.github.com>
+Date: Mon, 21 Oct 2024 11:47:34 -0700
+Subject: [PATCH] fixup! ConcurrentUtil
+
+
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package ca.spottedleaf.concurrentutil.collection;
+-
+-import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+-import ca.spottedleaf.concurrentutil.util.Validate;
+-
+-import java.lang.invoke.VarHandle;
+-import java.util.ConcurrentModificationException;
+-
+-/**
+- * Single reader thread single writer thread queue. The reader side of the queue is ordered by acquire semantics,
+- * and the writer side of the queue is ordered by release semantics.
+- */
+-// TODO test
+-public class SRSWLinkedQueue<E> {
+-
+-    // always non-null
+-    protected LinkedNode<E> head;
+-
+-    // always non-null
+-    protected LinkedNode<E> tail;
+-
+-    /* IMPL NOTE: Leave hashCode and equals to their defaults */
+-
+-    public SRSWLinkedQueue() {
+-        final LinkedNode<E> dummy = new LinkedNode<>(null, null);
+-        this.head = this.tail = dummy;
+-    }
+-
+-    /**
+-     * Must be the reader thread.
+-     *
+-     * <p>
+-     * Returns, without removing, the first element of this queue.
+-     * </p>
+-     * @return Returns, without removing, the first element of this queue.
+-     */
+-    public E peekFirst() {
+-        LinkedNode<E> head = this.head;
+-        E ret = head.getElementPlain();
+-        if (ret == null) {
+-            head = head.getNextAcquire();
+-            if (head == null) {
+-                // empty
+-                return null;
+-            }
+-            // update head reference for next poll() call
+-            this.head = head;
+-            // guaranteed to be non-null
+-            ret = head.getElementPlain();
+-            if (ret == null) {
+-                throw new ConcurrentModificationException("Multiple reader threads");
+-            }
+-        }
+-
+-        return ret;
+-    }
+-
+-    /**
+-     * Must be the reader thread.
+-     *
+-     * <p>
+-     * Returns and removes the first element of this queue.
+-     * </p>
+-     * @return Returns and removes the first element of this queue.
+-     */
+-    public E poll() {
+-        LinkedNode<E> head = this.head;
+-        E ret = head.getElementPlain();
+-        if (ret == null) {
+-            head = head.getNextAcquire();
+-            if (head == null) {
+-                // empty
+-                return null;
+-            }
+-            // guaranteed to be non-null
+-            ret = head.getElementPlain();
+-            if (ret == null) {
+-                throw new ConcurrentModificationException("Multiple reader threads");
+-            }
+-        }
+-
+-        head.setElementPlain(null);
+-        LinkedNode<E> next = head.getNextAcquire();
+-        this.head = next == null ? head : next;
+-
+-        return ret;
+-    }
+-
+-    /**
+-     * Must be the writer thread.
+-     *
+-     * <p>
+-     * Adds the element to the end of the queue.
+-     * </p>
+-     *
+-     * @throws NullPointerException If the provided element is null
+-     */
+-    public void addLast(final E element) {
+-        Validate.notNull(element, "Provided element cannot be null");
+-        final LinkedNode<E> append = new LinkedNode<>(element, null);
+-
+-        this.tail.setNextRelease(append);
+-        this.tail = append;
+-    }
+-
+-    protected static final class LinkedNode<E> {
+-
+-        protected volatile Object element;
+-        protected volatile LinkedNode<E> next;
+-
+-        protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class);
+-        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class);
+-
+-        protected LinkedNode(final Object element, final LinkedNode<E> next) {
+-            ELEMENT_HANDLE.set(this, element);
+-            NEXT_HANDLE.set(this, next);
+-        }
+-
+-        /* element */
+-
+-        @SuppressWarnings("unchecked")
+-        protected final E getElementPlain() {
+-            return (E)ELEMENT_HANDLE.get(this);
+-        }
+-
+-        protected final void setElementPlain(final E update) {
+-            ELEMENT_HANDLE.set(this, (Object)update);
+-        }
+-        /* next */
+-
+-        @SuppressWarnings("unchecked")
+-        protected final LinkedNode<E> getNextPlain() {
+-            return (LinkedNode<E>)NEXT_HANDLE.get(this);
+-        }
+-
+-        @SuppressWarnings("unchecked")
+-        protected final LinkedNode<E> getNextAcquire() {
+-            return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this);
+-        }
+-
+-        protected final void setNextPlain(final LinkedNode<E> next) {
+-            NEXT_HANDLE.set(this, next);
+-        }
+-
+-        protected final void setNextRelease(final LinkedNode<E> next) {
+-            NEXT_HANDLE.setRelease(this, next);
+-        }
+-    }
+-}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/CallbackCompletable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/CallbackCompletable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/CallbackCompletable.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.completable;
++
++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
++import ca.spottedleaf.concurrentutil.executor.Cancellable;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.util.function.BiConsumer;
++
++public final class CallbackCompletable<T> {
++
++    private static final Logger LOGGER = LoggerFactory.getLogger(CallbackCompletable.class);
++
++    private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>();
++    private T result;
++    private Throwable throwable;
++    private volatile boolean completed;
++
++    public boolean isCompleted() {
++        return this.completed;
++    }
++
++    /**
++     * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
++     * synchronisation
++     */
++    public T getResult() {
++        return this.result;
++    }
++
++    /**
++     * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
++     * synchronisation
++     */
++    public Throwable getThrowable() {
++        return this.throwable;
++    }
++
++    /**
++     * Adds a waiter that should only be completed asynchronously by the complete() calls. If complete()
++     * has already been called, returns {@code null} and does not invoke the specified consumer.
++     * @param consumer Consumer to be executed on completion
++     * @throws NullPointerException If consumer is null
++     * @return A cancellable which will control the execution of the specified consumer
++     */
++    public Cancellable addAsynchronousWaiter(final BiConsumer<T, Throwable> consumer) {
++        if (this.waiters.add(consumer)) {
++            return new CancellableImpl(consumer);
++        }
++        return null;
++    }
++
++    private void completeAllWaiters(final T result, final Throwable throwable) {
++        this.completed = true;
++        BiConsumer<T, Throwable> waiter;
++        while ((waiter = this.waiters.pollOrBlockAdds()) != null) {
++            this.completeWaiter(waiter, result, throwable);
++        }
++    }
++
++    private void completeWaiter(final BiConsumer<T, Throwable> consumer, final T result, final Throwable throwable) {
++        try {
++            consumer.accept(result, throwable);
++        } catch (final Throwable throwable2) {
++            LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2);
++        }
++    }
++
++    /**
++     * Adds a waiter that will be completed asynchronously by the complete() calls. If complete()
++     * has already been called, then invokes the consumer synchronously with the completed result.
++     * @param consumer Consumer to be executed on completion
++     * @throws NullPointerException If consumer is null
++     * @return A cancellable which will control the execution of the specified consumer
++     */
++    public Cancellable addWaiter(final BiConsumer<T, Throwable> consumer) {
++        if (this.waiters.add(consumer)) {
++            return new CancellableImpl(consumer);
++        }
++        this.completeWaiter(consumer, this.result, this.throwable);
++        return new CancellableImpl(consumer);
++    }
++
++    public void complete(final T result) {
++        this.result = result;
++        this.completeAllWaiters(result, null);
++    }
++
++    public void completeWithThrowable(final Throwable throwable) {
++        if (throwable == null) {
++            throw new NullPointerException("Throwable cannot be null");
++        }
++        this.throwable = throwable;
++        this.completeAllWaiters(null, throwable);
++    }
++
++    private final class CancellableImpl implements Cancellable {
++
++        private final BiConsumer<T, Throwable> waiter;
++
++        private CancellableImpl(final BiConsumer<T, Throwable> waiter) {
++            this.waiter = waiter;
++        }
++
++        @Override
++        public boolean cancel() {
++            return CallbackCompletable.this.waiters.remove(this.waiter);
++        }
++    }
++}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
++++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
+@@ -0,0 +0,0 @@
+ package ca.spottedleaf.concurrentutil.completable;
+ 
+-import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+-import ca.spottedleaf.concurrentutil.executor.Cancellable;
+ import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Validate;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
++import java.lang.invoke.VarHandle;
++import java.util.concurrent.CompletableFuture;
++import java.util.concurrent.CompletionException;
++import java.util.concurrent.CompletionStage;
++import java.util.concurrent.Executor;
++import java.util.concurrent.ForkJoinPool;
++import java.util.concurrent.locks.LockSupport;
+ import java.util.function.BiConsumer;
++import java.util.function.BiFunction;
++import java.util.function.Consumer;
++import java.util.function.Function;
++import java.util.function.Supplier;
+ 
+ public final class Completable<T> {
+ 
+     private static final Logger LOGGER = LoggerFactory.getLogger(Completable.class);
++    private static final Function<? super Throwable, ? extends Throwable> DEFAULT_EXCEPTION_HANDLER = (final Throwable thr) -> {
++        LOGGER.error("Unhandled exception during Completable operation", thr);
++        return thr;
++    };
+ 
+-    private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>();
+-    private T result;
+-    private Throwable throwable;
+-    private volatile boolean completed;
++    public static Executor getDefaultExecutor() {
++        return ForkJoinPool.commonPool();
++    }
++
++    private static final Transform<?, ?> COMPLETED_STACK = new Transform<>(null, null, null, null) {
++        @Override
++        public void run() {}
++    };
++    private volatile Transform<?, T> completeStack;
++    private static final VarHandle COMPLETE_STACK_HANDLE = ConcurrentUtil.getVarHandle(Completable.class, "completeStack", Transform.class);
++
++    private static final Object NULL_MASK = new Object();
++    private volatile Object result;
++    private static final VarHandle RESULT_HANDLE = ConcurrentUtil.getVarHandle(Completable.class, "result", Object.class);
+ 
+-    public boolean isCompleted() {
+-        return this.completed;
++    private Object getResultPlain() {
++        return (Object)RESULT_HANDLE.get(this);
+     }
+ 
+-    /**
+-     * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
+-     * synchronisation
+-     */
+-    public T getResult() {
+-        return this.result;
++    private Object getResultVolatile() {
++        return (Object)RESULT_HANDLE.getVolatile(this);
+     }
+ 
+-    /**
+-     * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
+-     * synchronisation
+-     */
+-    public Throwable getThrowable() {
+-        return this.throwable;
++    private void pushStackOrRun(final Transform<?, T> push) {
++        int failures = 0;
++        for (Transform<?, T> curr = (Transform<?, T>)COMPLETE_STACK_HANDLE.getVolatile(this);;) {
++            if (curr == COMPLETED_STACK) {
++                push.execute();
++                return;
++            }
++
++            push.next = curr;
++
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = (Transform<?, T>)COMPLETE_STACK_HANDLE.compareAndExchange(this, curr, push))) {
++                return;
++            }
++            push.next = null;
++            ++failures;
++        }
+     }
+ 
+-    /**
+-     * Adds a waiter that should only be completed asynchronously by the complete() calls. If complete()
+-     * has already been called, returns {@code null} and does not invoke the specified consumer.
+-     * @param consumer Consumer to be executed on completion
+-     * @throws NullPointerException If consumer is null
+-     * @return A cancellable which will control the execution of the specified consumer
+-     */
+-    public Cancellable addAsynchronousWaiter(final BiConsumer<T, Throwable> consumer) {
+-        if (this.waiters.add(consumer)) {
+-            return new CancellableImpl(consumer);
++    private void propagateStack() {
++        Transform<?, T> topStack = (Transform<?, T>)COMPLETE_STACK_HANDLE.getAndSet(this, COMPLETED_STACK);
++        while (topStack != null) {
++            topStack.execute();
++            topStack = topStack.next;
+         }
+-        return null;
+     }
+ 
+-    private void completeAllWaiters(final T result, final Throwable throwable) {
+-        this.completed = true;
+-        BiConsumer<T, Throwable> waiter;
+-        while ((waiter = this.waiters.pollOrBlockAdds()) != null) {
+-            this.completeWaiter(waiter, result, throwable);
++    private static Object maskNull(final Object res) {
++        return res == null ? NULL_MASK : res;
++    }
++
++    private static Object unmaskNull(final Object res) {
++        return res == NULL_MASK ? null : res;
++    }
++
++    private static Executor checkExecutor(final Executor executor) {
++        return Validate.notNull(executor, "Executor may not be null");
++    }
++
++    public Completable() {}
++
++    private Completable(final Object complete) {
++        COMPLETE_STACK_HANDLE.set(this, COMPLETED_STACK);
++        RESULT_HANDLE.setRelease(this, complete);
++    }
++
++    public static <T> Completable<T> completed(final T value) {
++        return new Completable<>(maskNull(value));
++    }
++
++    public static <T> Completable<T> failed(final Throwable ex) {
++        Validate.notNull(ex, "Exception may not be null");
++
++        return new Completable<>(new ExceptionResult(ex));
++    }
++
++    public static <T> Completable<T> supplied(final Supplier<T> supplier) {
++        return supplied(supplier, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public static <T> Completable<T> supplied(final Supplier<T> supplier, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        try {
++            return completed(supplier.get());
++        } catch (final Throwable throwable) {
++            Throwable complete;
++            try {
++                complete = exceptionHandler.apply(throwable);
++            } catch (final Throwable thr2) {
++                throwable.addSuppressed(thr2);
++                complete = throwable;
++            }
++            return failed(complete);
+         }
+     }
+ 
+-    private void completeWaiter(final BiConsumer<T, Throwable> consumer, final T result, final Throwable throwable) {
++    public static <T> Completable<T> suppliedAsync(final Supplier<T> supplier, final Executor executor) {
++        return suppliedAsync(supplier, executor, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public static <T> Completable<T> suppliedAsync(final Supplier<T> supplier, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        final Completable<T> ret = new Completable<>();
++
++        class AsyncSuppliedCompletable implements Runnable, CompletableFuture.AsynchronousCompletionTask {
++            @Override
++            public void run() {
++                try {
++                    ret.complete(supplier.get());
++                } catch (final Throwable throwable) {
++                    Throwable complete;
++                    try {
++                        complete = exceptionHandler.apply(throwable);
++                    } catch (final Throwable thr2) {
++                        throwable.addSuppressed(thr2);
++                        complete = throwable;
++                    }
++                    ret.completeExceptionally(complete);
++                }
++            }
++        }
++
+         try {
+-            consumer.accept(result, throwable);
+-        } catch (final ThreadDeath death) {
+-            throw death;
+-        } catch (final Throwable throwable2) {
+-            LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2);
++            executor.execute(new AsyncSuppliedCompletable());
++        } catch (final Throwable throwable) {
++            Throwable complete;
++            try {
++                complete = exceptionHandler.apply(throwable);
++            } catch (final Throwable thr2) {
++                throwable.addSuppressed(thr2);
++                complete = throwable;
++            }
++            ret.completeExceptionally(complete);
++        }
++
++        return ret;
++    }
++
++    private boolean completeRaw(final Object value) {
++        if ((Object)RESULT_HANDLE.getVolatile(this) != null || !(boolean)RESULT_HANDLE.compareAndSet(this, (Object)null, value)) {
++            return false;
++        }
++
++        this.propagateStack();
++        return true;
++    }
++
++    public boolean complete(final T result) {
++        return this.completeRaw(maskNull(result));
++    }
++
++    public boolean completeExceptionally(final Throwable exception) {
++        Validate.notNull(exception, "Exception may not be null");
++
++        return this.completeRaw(new ExceptionResult(exception));
++    }
++
++    public boolean isDone() {
++        return this.getResultVolatile() != null;
++    }
++
++    public boolean isNormallyComplete() {
++        return this.getResultVolatile() != null && !(this.getResultVolatile() instanceof ExceptionResult);
++    }
++
++    public boolean isExceptionallyComplete() {
++        return this.getResultVolatile() instanceof ExceptionResult;
++    }
++
++    public Throwable getException() {
++        final Object res = this.getResultVolatile();
++        if (res == null) {
++            return null;
++        }
++
++        if (!(res instanceof ExceptionResult exRes)) {
++            throw new IllegalStateException("Not completed exceptionally");
++        }
++
++        return exRes.ex;
++    }
++
++    public T getNow(final T dfl) throws CompletionException {
++        final Object res = this.getResultVolatile();
++        if (res == null) {
++            return dfl;
++        }
++
++        if (res instanceof ExceptionResult exRes) {
++            throw new CompletionException(exRes.ex);
++        }
++
++        return (T)unmaskNull(res);
++    }
++
++    public T join() throws CompletionException {
++        if (this.isDone()) {
++            return this.getNow(null);
++        }
++
++        final UnparkTransform<T> unparkTransform = new UnparkTransform<>(this, Thread.currentThread());
++
++        this.pushStackOrRun(unparkTransform);
++
++        boolean interuptted = false;
++        while (!unparkTransform.isReleasable()) {
++            try {
++                ForkJoinPool.managedBlock(unparkTransform);
++            } catch (final InterruptedException ex) {
++                interuptted = true;
++            }
++        }
++
++        if (interuptted) {
++            Thread.currentThread().interrupt();
++        }
++
++        return this.getNow(null);
++    }
++
++    public CompletableFuture<T> toFuture() {
++        final Object rawResult = this.getResultVolatile();
++        if (rawResult != null) {
++            if (rawResult instanceof ExceptionResult exRes) {
++                return CompletableFuture.failedFuture(exRes.ex);
++            } else {
++                return CompletableFuture.completedFuture((T)unmaskNull(rawResult));
++            }
++        }
++
++        final CompletableFuture<T> ret = new CompletableFuture<>();
++
++        class ToFuture implements BiConsumer<T, Throwable> {
++
++            @Override
++            public void accept(final T res, final Throwable ex) {
++                if (ex != null) {
++                    ret.completeExceptionally(ex);
++                } else {
++                    ret.complete(res);
++                }
++            }
++        }
++
++        this.whenComplete(new ToFuture());
++
++        return ret;
++    }
++
++    public static <T> Completable<T> fromFuture(final CompletionStage<T> stage) {
++        final Completable<T> ret = new Completable<>();
++
++        class FromFuture implements BiConsumer<T, Throwable> {
++            @Override
++            public void accept(final T res, final Throwable ex) {
++                if (ex != null) {
++                    ret.completeExceptionally(ex);
++                } else {
++                    ret.complete(res);
++                }
++            }
++        }
++
++        stage.whenComplete(new FromFuture());
++
++        return ret;
++    }
++
++
++    public <U> Completable<U> thenApply(final Function<? super T, ? extends U> function) {
++        return this.thenApply(function, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public <U> Completable<U> thenApply(final Function<? super T, ? extends U> function, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(function, "Function may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<U> ret = new Completable<>();
++        this.pushStackOrRun(new ApplyTransform<>(null, this, ret, exceptionHandler, function));
++        return ret;
++    }
++
++    public <U> Completable<U> thenApplyAsync(final Function<? super T, ? extends U> function) {
++        return this.thenApplyAsync(function, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public <U> Completable<U> thenApplyAsync(final Function<? super T, ? extends U> function, final Executor executor) {
++        return this.thenApplyAsync(function, executor, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public <U> Completable<U> thenApplyAsync(final Function<? super T, ? extends U> function, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(function, "Function may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<U> ret = new Completable<>();
++        this.pushStackOrRun(new ApplyTransform<>(checkExecutor(executor), this, ret, exceptionHandler, function));
++        return ret;
++    }
++
++
++    public Completable<Void> thenAccept(final Consumer<? super T> consumer) {
++        return this.thenAccept(consumer, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<Void> thenAccept(final Consumer<? super T> consumer, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(consumer, "Consumer may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<Void> ret = new Completable<>();
++        this.pushStackOrRun(new AcceptTransform<>(null, this, ret, exceptionHandler, consumer));
++        return ret;
++    }
++
++    public Completable<Void> thenAcceptAsync(final Consumer<? super T> consumer) {
++        return this.thenAcceptAsync(consumer, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<Void> thenAcceptAsync(final Consumer<? super T> consumer, final Executor executor) {
++        return this.thenAcceptAsync(consumer, executor, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<Void> thenAcceptAsync(final Consumer<? super T> consumer, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(consumer, "Consumer may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<Void> ret = new Completable<>();
++        this.pushStackOrRun(new AcceptTransform<>(checkExecutor(executor), this, ret, exceptionHandler, consumer));
++        return ret;
++    }
++
++
++    public Completable<Void> thenRun(final Runnable run) {
++        return this.thenRun(run, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<Void> thenRun(final Runnable run, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(run, "Run may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<Void> ret = new Completable<>();
++        this.pushStackOrRun(new RunTransform<>(null, this, ret, exceptionHandler, run));
++        return ret;
++    }
++
++    public Completable<Void> thenRunAsync(final Runnable run) {
++        return this.thenRunAsync(run, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<Void> thenRunAsync(final Runnable run, final Executor executor) {
++        return this.thenRunAsync(run, executor, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<Void> thenRunAsync(final Runnable run, final Executor executor, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(run, "Run may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<Void> ret = new Completable<>();
++        this.pushStackOrRun(new RunTransform<>(checkExecutor(executor), this, ret, exceptionHandler, run));
++        return ret;
++    }
++
++
++    public <U> Completable<U> handle(final BiFunction<? super T, ? super Throwable, ? extends U> function) {
++        return this.handle(function, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public <U> Completable<U> handle(final BiFunction<? super T, ? super Throwable, ? extends U> function,
++                                     final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(function, "Function may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<U> ret = new Completable<>();
++        this.pushStackOrRun(new HandleTransform<>(null, this, ret, exceptionHandler, function));
++        return ret;
++    }
++
++    public <U> Completable<U> handleAsync(final BiFunction<? super T, ? super Throwable, ? extends U> function) {
++        return this.handleAsync(function, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public <U> Completable<U> handleAsync(final BiFunction<? super T, ? super Throwable, ? extends U> function,
++                                          final Executor executor) {
++        return this.handleAsync(function, executor, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public <U> Completable<U> handleAsync(final BiFunction<? super T, ? super Throwable, ? extends U> function,
++                                          final Executor executor,
++                                          final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(function, "Function may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<U> ret = new Completable<>();
++        this.pushStackOrRun(new HandleTransform<>(checkExecutor(executor), this, ret, exceptionHandler, function));
++        return ret;
++    }
++
++
++    public Completable<T> whenComplete(final BiConsumer<? super T, ? super Throwable> consumer) {
++        return this.whenComplete(consumer, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<T> whenComplete(final BiConsumer<? super T, ? super Throwable> consumer, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(consumer, "Consumer may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<T> ret = new Completable<>();
++        this.pushStackOrRun(new WhenTransform<>(null, this, ret, exceptionHandler, consumer));
++        return ret;
++    }
++
++    public Completable<T> whenCompleteAsync(final BiConsumer<? super T, ? super Throwable> consumer) {
++        return this.whenCompleteAsync(consumer, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<T> whenCompleteAsync(final BiConsumer<? super T, ? super Throwable> consumer, final Executor executor) {
++        return this.whenCompleteAsync(consumer, executor, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<T> whenCompleteAsync(final BiConsumer<? super T, ? super Throwable> consumer, final Executor executor,
++                                            final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(consumer, "Consumer may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<T> ret = new Completable<>();
++        this.pushStackOrRun(new WhenTransform<>(checkExecutor(executor), this, ret, exceptionHandler, consumer));
++        return ret;
++    }
++
++
++    public Completable<T> exceptionally(final Function<Throwable, ? extends T> function) {
++        return this.exceptionally(function, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<T> exceptionally(final Function<Throwable, ? extends T> function, final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(function, "Function may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<T> ret = new Completable<>();
++        this.pushStackOrRun(new ExceptionallyTransform<>(null, this, ret, exceptionHandler, function));
++        return ret;
++    }
++
++    public Completable<T> exceptionallyAsync(final Function<Throwable, ? extends T> function) {
++        return this.exceptionallyAsync(function, getDefaultExecutor(), DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<T> exceptionallyAsync(final Function<Throwable, ? extends T> function, final Executor executor) {
++        return this.exceptionallyAsync(function, executor, DEFAULT_EXCEPTION_HANDLER);
++    }
++
++    public Completable<T> exceptionallyAsync(final Function<Throwable, ? extends T> function, final Executor executor,
++                                             final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++        Validate.notNull(function, "Function may not be null");
++        Validate.notNull(exceptionHandler, "Exception handler may not be null");
++
++        final Completable<T> ret = new Completable<>();
++        this.pushStackOrRun(new ExceptionallyTransform<>(checkExecutor(executor), this, ret, exceptionHandler, function));
++        return ret;
++    }
++
++    private static final class ExceptionResult {
++        public final Throwable ex;
++
++        public ExceptionResult(final Throwable ex) {
++            this.ex = ex;
+         }
+     }
+ 
+-    /**
+-     * Adds a waiter that will be completed asynchronously by the complete() calls. If complete()
+-     * has already been called, then invokes the consumer synchronously with the completed result.
+-     * @param consumer Consumer to be executed on completion
+-     * @throws NullPointerException If consumer is null
+-     * @return A cancellable which will control the execution of the specified consumer
+-     */
+-    public Cancellable addWaiter(final BiConsumer<T, Throwable> consumer) {
+-        if (this.waiters.add(consumer)) {
+-            return new CancellableImpl(consumer);
++    private static abstract class Transform<U, T> implements Runnable, CompletableFuture.AsynchronousCompletionTask {
++
++        private Transform<?, T> next;
++
++        private final Executor executor;
++        protected final Completable<T> from;
++        protected final Completable<U> to;
++        protected final Function<? super Throwable, ? extends Throwable> exceptionHandler;
++
++        protected Transform(final Executor executor, final Completable<T> from, final Completable<U> to,
++                            final Function<? super Throwable, ? extends Throwable> exceptionHandler) {
++            this.executor = executor;
++            this.from = from;
++            this.to = to;
++            this.exceptionHandler = exceptionHandler;
++        }
++
++        // force interface call to become virtual call
++        @Override
++        public abstract void run();
++
++        protected void failed(final Throwable throwable) {
++            Throwable complete;
++            try {
++                complete = this.exceptionHandler.apply(throwable);
++            } catch (final Throwable thr2) {
++                throwable.addSuppressed(thr2);
++                complete = throwable;
++            }
++            this.to.completeExceptionally(complete);
++        }
++
++        public void execute() {
++            if (this.executor == null) {
++                this.run();
++                return;
++            }
++
++            try {
++                this.executor.execute(this);
++            } catch (final Throwable throwable) {
++                this.failed(throwable);
++            }
++        }
++    }
++
++    private static final class ApplyTransform<U, T> extends Transform<U, T> {
++
++        private final Function<? super T, ? extends U> function;
++
++        public ApplyTransform(final Executor executor, final Completable<T> from, final Completable<U> to,
++                              final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++                              final Function<? super T, ? extends U> function) {
++            super(executor, from, to, exceptionHandler);
++            this.function = function;
++        }
++
++        @Override
++        public void run() {
++            final Object result = this.from.getResultPlain();
++            try {
++                if (result instanceof ExceptionResult exRes) {
++                    this.to.completeExceptionally(exRes.ex);
++                } else {
++                    this.to.complete(this.function.apply((T)unmaskNull(result)));
++                }
++            } catch (final Throwable throwable) {
++                this.failed(throwable);
++            }
+         }
+-        this.completeWaiter(consumer, this.result, this.throwable);
+-        return new CancellableImpl(consumer);
+     }
+ 
+-    public void complete(final T result) {
+-        this.result = result;
+-        this.completeAllWaiters(result, null);
++    private static final class AcceptTransform<T> extends Transform<Void, T> {
++        private final Consumer<? super T> consumer;
++
++        public AcceptTransform(final Executor executor, final Completable<T> from, final Completable<Void> to,
++                               final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++                               final Consumer<? super T> consumer) {
++            super(executor, from, to, exceptionHandler);
++            this.consumer = consumer;
++        }
++
++        @Override
++        public void run() {
++            final Object result = this.from.getResultPlain();
++            try {
++                if (result instanceof ExceptionResult exRes) {
++                    this.to.completeExceptionally(exRes.ex);
++                } else {
++                    this.consumer.accept((T)unmaskNull(result));
++                    this.to.complete(null);
++                }
++            } catch (final Throwable throwable) {
++                this.failed(throwable);
++            }
++        }
+     }
+ 
+-    public void completeWithThrowable(final Throwable throwable) {
+-        if (throwable == null) {
+-            throw new NullPointerException("Throwable cannot be null");
++    private static final class RunTransform<T> extends Transform<Void, T> {
++        private final Runnable run;
++
++        public RunTransform(final Executor executor, final Completable<T> from, final Completable<Void> to,
++                            final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++                            final Runnable run) {
++            super(executor, from, to, exceptionHandler);
++            this.run = run;
++        }
++
++        @Override
++        public void run() {
++            final Object result = this.from.getResultPlain();
++            try {
++                if (result instanceof ExceptionResult exRes) {
++                    this.to.completeExceptionally(exRes.ex);
++                } else {
++                    this.run.run();
++                    this.to.complete(null);
++                }
++            } catch (final Throwable throwable) {
++                this.failed(throwable);
++            }
+         }
+-        this.throwable = throwable;
+-        this.completeAllWaiters(null, throwable);
+     }
+ 
+-    private final class CancellableImpl implements Cancellable {
++    private static final class HandleTransform<U, T> extends Transform<U, T> {
++
++        private final BiFunction<? super T, ? super Throwable, ? extends U> function;
++
++        public HandleTransform(final Executor executor, final Completable<T> from, final Completable<U> to,
++                               final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++                               final BiFunction<? super T, ? super Throwable, ? extends U> function) {
++            super(executor, from, to, exceptionHandler);
++            this.function = function;
++        }
+ 
+-        private final BiConsumer<T, Throwable> waiter;
++        @Override
++        public void run() {
++            final Object result = this.from.getResultPlain();
++            try {
++                if (result instanceof ExceptionResult exRes) {
++                    this.to.complete(this.function.apply(null, exRes.ex));
++                } else {
++                    this.to.complete(this.function.apply((T)unmaskNull(result), null));
++                }
++            } catch (final Throwable throwable) {
++                this.failed(throwable);
++            }
++        }
++    }
++
++    private static final class WhenTransform<T> extends Transform<T, T> {
++
++        private final BiConsumer<? super T, ? super Throwable> consumer;
++
++        public WhenTransform(final Executor executor, final Completable<T> from, final Completable<T> to,
++                              final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++                              final BiConsumer<? super T, ? super Throwable> consumer) {
++            super(executor, from, to, exceptionHandler);
++            this.consumer = consumer;
++        }
++
++        @Override
++        public void run() {
++            final Object result = this.from.getResultPlain();
++            try {
++                if (result instanceof ExceptionResult exRes) {
++                    this.consumer.accept(null, exRes.ex);
++                    this.to.completeExceptionally(exRes.ex);
++                } else {
++                    final T unmasked = (T)unmaskNull(result);
++                    this.consumer.accept(unmasked, null);
++                    this.to.complete(unmasked);
++                }
++            } catch (final Throwable throwable) {
++                this.failed(throwable);
++            }
++        }
++    }
++
++    private static final class ExceptionallyTransform<T> extends Transform<T, T> {
++        private final Function<Throwable, ? extends T> function;
++
++        public ExceptionallyTransform(final Executor executor, final Completable<T> from, final Completable<T> to,
++                              final Function<? super Throwable, ? extends Throwable> exceptionHandler,
++                              final Function<Throwable, ? extends T> function) {
++            super(executor, from, to, exceptionHandler);
++            this.function = function;
++        }
++
++        @Override
++        public void run() {
++            final Object result = this.from.getResultPlain();
++            try {
++                if (result instanceof ExceptionResult exRes) {
++                    this.to.complete(this.function.apply(exRes.ex));
++                } else {
++                    this.to.complete((T)unmaskNull(result));
++                }
++            } catch (final Throwable throwable) {
++                this.failed(throwable);
++            }
++        }
++    }
++
++    private static final class UnparkTransform<T> extends Transform<Void, T> implements ForkJoinPool.ManagedBlocker {
++
++        private volatile Thread thread;
++
++        public UnparkTransform(final Completable<T> from, final Thread target) {
++            super(null, from, null, null);
++            this.thread = target;
++        }
++
++        @Override
++        public void run() {
++            final Thread t = this.thread;
++            this.thread = null;
++            LockSupport.unpark(t);
++        }
++
++        @Override
++        public boolean block() throws InterruptedException {
++            while (!this.isReleasable()) {
++                if (Thread.interrupted()) {
++                    throw new InterruptedException();
++                }
++                LockSupport.park(this);
++            }
+ 
+-        private CancellableImpl(final BiConsumer<T, Throwable> waiter) {
+-            this.waiter = waiter;
++            return true;
+         }
+ 
+         @Override
+-        public boolean cancel() {
+-            return Completable.this.waiters.remove(this.waiter);
++        public boolean isReleasable() {
++            return this.thread == null;
+         }
+     }
+ }
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package ca.spottedleaf.concurrentutil.executor;
+-
+-import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+-import java.util.function.BooleanSupplier;
+-
+-/**
+- * Base implementation for an abstract queue of tasks which are executed either synchronously or asynchronously.
+- *
+- * <p>
+- * The implementation supports tracking task executions using {@link #getTotalTasksScheduled()} and
+- * {@link #getTotalTasksExecuted()}, and optionally shutting down the executor using {@link #shutdown()}
+- * </p>
+- *
+- * <p>
+- * The base implementation does not provide a method to queue a task for execution, rather that is specified in
+- * the specific implementation. However, it is required that a specific implementation provides a method to
+- * <i>queue</i> a task or <i>create</i> a task. A <i>queued</i> task is one which will eventually be executed,
+- * and a <i>created</i> task must be queued to execute via {@link BaseTask#queue()} or be executed manually via
+- * {@link BaseTask#execute()}. This choice of delaying the queueing of a task may be useful to provide a task handle
+- * which may be cancelled or adjusted before the actual real task logic is ready to be executed.
+- * </p>
+- */
+-public interface BaseExecutor {
+-
+-    /**
+-     * Returns whether every task scheduled to this queue has been removed and executed or cancelled. If no tasks have been queued,
+-     * returns {@code true}.
+-     *
+-     * @return {@code true} if all tasks that have been queued have finished executing or no tasks have been queued, {@code false} otherwise.
+-     */
+-    public default boolean haveAllTasksExecuted() {
+-        // order is important
+-        // if new tasks are scheduled between the reading of these variables, scheduled is guaranteed to be higher -
+-        // so our check fails, and we try again
+-        final long completed = this.getTotalTasksExecuted();
+-        final long scheduled = this.getTotalTasksScheduled();
+-
+-        return completed == scheduled;
+-    }
+-
+-    /**
+-     * Returns the number of tasks that have been scheduled or execute or are pending to be scheduled.
+-     */
+-    public long getTotalTasksScheduled();
+-
+-    /**
+-     * Returns the number of tasks that have fully been executed.
+-     */
+-    public long getTotalTasksExecuted();
+-
+-    /**
+-     * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()}
+-     * <p>
+-     *     This call is most effective after a {@link #shutdown()} call, as the shutdown call guarantees no tasks can
+-     *     be executed and the waitUntilAllExecuted call makes sure the queue is empty. Effectively, using shutdown then using
+-     *     waitUntilAllExecuted ensures this queue is empty - and most importantly, will remain empty.
+-     * </p>
+-     * <p>
+-     *     This method is not guaranteed to be immediately responsive to queue state, so calls may take significantly more
+-     *     time than expected. Effectively, do not rely on this call being fast - even if there are few tasks scheduled.
+-     * </p>
+-     * <p>
+-     *     Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this call.
+-     * </p>
+-     *
+-     * @throws IllegalStateException If the current thread is not allowed to wait
+-     */
+-    public default void waitUntilAllExecuted() throws IllegalStateException {
+-        long failures = 1L; // start at 0.25ms
+-
+-        while (!this.haveAllTasksExecuted()) {
+-            Thread.yield();
+-            failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms
+-        }
+-    }
+-
+-    /**
+-     * Executes the next available task.
+-     *
+-     * @return {@code true} if a task was executed, {@code false} otherwise
+-     * @throws IllegalStateException If the current thread is not allowed to execute a task
+-     */
+-    public boolean executeTask() throws IllegalStateException;
+-
+-    /**
+-     * Executes all queued tasks.
+-     *
+-     * @return {@code true} if a task was executed, {@code false} otherwise
+-     * @throws IllegalStateException If the current thread is not allowed to execute a task
+-     */
+-    public default boolean executeAll() {
+-        if (!this.executeTask()) {
+-            return false;
+-        }
+-
+-        while (this.executeTask());
+-
+-        return true;
+-    }
+-
+-    /**
+-     * Waits and executes tasks until the condition returns {@code true}.
+-     * <p>
+-     *     WARNING: This function is <i>not</i> suitable for waiting until a deadline!
+-     *     Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead.
+-     * </p>
+-     */
+-    public default void executeConditionally(final BooleanSupplier condition) {
+-        long failures = 0;
+-        while (!condition.getAsBoolean()) {
+-            if (this.executeTask()) {
+-                failures = failures >>> 2;
+-            } else {
+-                failures = ConcurrentUtil.linearLongBackoff(failures, 100_000L, 10_000_000L); // 100us, 10ms
+-            }
+-        }
+-    }
+-
+-    /**
+-     * Waits and executes tasks until the condition returns {@code true} or {@code System.nanoTime() - deadline >= 0}.
+-     */
+-    public default void executeConditionally(final BooleanSupplier condition, final long deadline) {
+-        long failures = 0;
+-        // double check deadline; we don't know how expensive the condition is
+-        while ((System.nanoTime() - deadline < 0L) && !condition.getAsBoolean() && (System.nanoTime() - deadline < 0L)) {
+-            if (this.executeTask()) {
+-                failures = failures >>> 2;
+-            } else {
+-                failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms
+-            }
+-        }
+-    }
+-
+-    /**
+-     * Waits and executes tasks until {@code System.nanoTime() - deadline >= 0}.
+-     */
+-    public default void executeUntil(final long deadline) {
+-        long failures = 0;
+-        while (System.nanoTime() - deadline < 0L) {
+-            if (this.executeTask()) {
+-                failures = failures >>> 2;
+-            } else {
+-                failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms
+-            }
+-        }
+-    }
+-
+-    /**
+-     * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will
+-     * result in {@link IllegalStateException} being thrown.
+-     * <p>
+-     *     This operation is atomic with respect to other shutdown calls
+-     * </p>
+-     * <p>
+-     *     After this call has completed, regardless of return value, this queue will be shutdown.
+-     * </p>
+-     *
+-     * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already
+-     * @throws UnsupportedOperationException If this queue does not support shutdown
+-     * @see #isShutdown()
+-     */
+-    public default boolean shutdown() throws UnsupportedOperationException {
+-        throw new UnsupportedOperationException();
+-    }
+-
+-    /**
+-     * Returns whether this queue has shut down. Effectively, whether new tasks will be rejected - this method
+-     * does not indicate whether all the tasks scheduled have been executed.
+-     * @return Returns whether this queue has shut down.
+-     * @see #waitUntilAllExecuted()
+-     */
+-    public default boolean isShutdown() {
+-        return false;
+-    }
+-
+-    /**
+-     * Task object returned for any {@link BaseExecutor} scheduled task.
+-     * @see BaseExecutor
+-     */
+-    public static interface BaseTask extends Cancellable {
+-
+-        /**
+-         * Causes a lazily queued task to become queued or executed
+-         *
+-         * @throws IllegalStateException If the backing queue has shutdown
+-         * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed
+-         */
+-        public boolean queue();
+-
+-        /**
+-         * Forces this task to be marked as completed.
+-         *
+-         * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed.
+-         */
+-        @Override
+-        public boolean cancel();
+-
+-        /**
+-         * Executes this task. This will also mark the task as completing.
+-         * <p>
+-         *     Exceptions thrown from the runnable will be rethrown.
+-         * </p>
+-         *
+-         * @return {@code true} if this task was executed, {@code false} if it was already marked as completed.
+-         */
+-        public boolean execute();
+-    }
+-}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/PrioritisedExecutor.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/PrioritisedExecutor.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.executor;
++
++import ca.spottedleaf.concurrentutil.util.Priority;
++
++public interface PrioritisedExecutor {
++
++    /**
++     * Returns the number of tasks that have been scheduled are pending to be scheduled.
++     */
++    public long getTotalTasksScheduled();
++
++    /**
++     * Returns the number of tasks that have been executed.
++     */
++    public long getTotalTasksExecuted();
++
++    /**
++     * Generates the next suborder id.
++     * @return The next suborder id.
++     */
++    public long generateNextSubOrder();
++
++    /**
++     * Executes the next available task.
++     * <p>
++     *     If there is a task with priority {@link Priority#BLOCKING} available, then that such task is executed.
++     * </p>
++     * <p>
++     *     If there is a task with priority {@link Priority#IDLE} available then that task is only executed
++     *     when there are no other tasks available with a higher priority.
++     * </p>
++     * <p>
++     *     If there are no tasks that have priority {@link Priority#BLOCKING} or {@link Priority#IDLE}, then
++     *     this function will be biased to execute tasks that have higher priorities.
++     * </p>
++     *
++     * @return {@code true} if a task was executed, {@code false} otherwise
++     * @throws IllegalStateException If the current thread is not allowed to execute a task
++     */
++    public boolean executeTask() throws IllegalStateException;
++
++    /**
++     * Prevent further additions to this executor. Attempts to add after this call has completed (potentially during) will
++     * result in {@link IllegalStateException} being thrown.
++     * <p>
++     *     This operation is atomic with respect to other shutdown calls
++     * </p>
++     * <p>
++     *     After this call has completed, regardless of return value, this executor will be shutdown.
++     * </p>
++     *
++     * @return {@code true} if the executor was shutdown, {@code false} if it has shut down already
++     * @see #isShutdown()
++     */
++    public boolean shutdown();
++
++    /**
++     * Returns whether this executor has shut down. Effectively, returns whether new tasks will be rejected.
++     * This method does not indicate whether all the tasks scheduled have been executed.
++     * @return Returns whether this executor has shut down.
++     */
++    public boolean isShutdown();
++
++    /**
++     * Queues or executes a task at {@link Priority#NORMAL} priority.
++     * @param task The task to run.
++     *
++     * @throws IllegalStateException If this executor has shutdown.
++     * @throws NullPointerException If the task is null
++     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++     *         associated with the parameter
++     */
++    public PrioritisedTask queueTask(final Runnable task);
++
++    /**
++     * Queues or executes a task.
++     *
++     * @param task The task to run.
++     * @param priority The priority for the task.
++     *
++     * @throws IllegalStateException If this executor has shutdown.
++     * @throws NullPointerException If the task is null
++     * @throws IllegalArgumentException If the priority is invalid.
++     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++     *         associated with the parameter
++     */
++    public PrioritisedTask queueTask(final Runnable task, final Priority priority);
++
++    /**
++     * Queues or executes a task.
++     *
++     * @param task The task to run.
++     * @param priority The priority for the task.
++     * @param subOrder The task's suborder.
++     *
++     * @throws IllegalStateException If this executor has shutdown.
++     * @throws NullPointerException If the task is null
++     * @throws IllegalArgumentException If the priority is invalid.
++     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++     *         associated with the parameter
++     */
++    public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder);
++
++    /**
++     * Creates, but does not queue or execute, a task at {@link Priority#NORMAL} priority.
++     * @param task The task to run.
++     *
++     * @throws NullPointerException If the task is null
++     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++     *         associated with the parameter
++     */
++    public PrioritisedTask createTask(final Runnable task);
++
++    /**
++     * Creates, but does not queue or execute, a task at {@link Priority#NORMAL} priority.
++     *
++     * @param task The task to run.
++     * @param priority The priority for the task.
++     *
++     * @throws NullPointerException If the task is null
++     * @throws IllegalArgumentException If the priority is invalid.
++     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++     *         associated with the parameter
++     */
++    public PrioritisedTask createTask(final Runnable task, final Priority priority);
++
++    /**
++     * Creates, but does not queue or execute, a task at {@link Priority#NORMAL} priority.
++     *
++     * @param task The task to run.
++     * @param priority The priority for the task.
++     * @param subOrder The task's suborder.
++     *
++     * @throws NullPointerException If the task is null
++     * @throws IllegalArgumentException If the priority is invalid.
++     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++     *         associated with the parameter
++     */
++    public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder);
++
++    public static interface PrioritisedTask extends Cancellable {
++
++        /**
++         * Returns the executor associated with this task.
++         * @return The executor associated with this task.
++         */
++        public PrioritisedExecutor getExecutor();
++
++        /**
++         * Causes a lazily queued task to become queued or executed
++         *
++         * @throws IllegalStateException If the backing executor has shutdown
++         * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed
++         */
++        public boolean queue();
++
++        /**
++         * Returns whether this task has been queued and is not completing.
++         * @return {@code true} If the task has been queued, {@code false} if the task has not been queued or is marked
++         *         as completing.
++         */
++        public boolean isQueued();
++
++        /**
++         * Forces this task to be marked as completed.
++         *
++         * @return {@code true} if the task was cancelled, {@code false} if the task has already completed
++         *         or is being completed.
++         */
++        @Override
++        public boolean cancel();
++
++        /**
++         * Executes this task. This will also mark the task as completing.
++         * <p>
++         *     Exceptions thrown from the runnable will be rethrown.
++         * </p>
++         *
++         * @return {@code true} if this task was executed, {@code false} if it was already marked as completed.
++         */
++        public boolean execute();
++
++        /**
++         * Returns the current priority. Note that {@link Priority#COMPLETING} will be returned
++         * if this task is completing or has completed.
++         */
++        public Priority getPriority();
++
++        /**
++         * Attempts to set this task's priority level to the level specified.
++         *
++         * @param priority Specified priority level.
++         *
++         * @throws IllegalArgumentException If the priority is invalid
++         * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
++         *         this task was scheduled on was shutdown, or if the priority was already at the specified level.
++         */
++        public boolean setPriority(final Priority priority);
++
++        /**
++         * Attempts to raise the priority to the priority level specified.
++         *
++         * @param priority Priority specified
++         *
++         * @throws IllegalArgumentException If the priority is invalid
++         * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the
++         *          specified level or was already at the specified level or higher.
++         */
++        public boolean raisePriority(final Priority priority);
++
++        /**
++         * Attempts to lower the priority to the priority level specified.
++         *
++         * @param priority Priority specified
++         *
++         * @throws IllegalArgumentException If the priority is invalid
++         * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the
++         *          specified level or was already at the specified level or lower.
++         */
++        public boolean lowerPriority(final Priority priority);
++
++        /**
++         * Returns the suborder id associated with this task.
++         * @return The suborder id associated with this task.
++         */
++        public long getSubOrder();
++
++        /**
++         * Sets the suborder id associated with this task. Ths function has no effect when this task
++         * is completing or is completed.
++         *
++         * @param subOrder Specified new sub order.
++         *
++         * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
++         *         this task was scheduled on was shutdown, or if the current suborder is the same as the new sub order.
++         */
++        public boolean setSubOrder(final long subOrder);
++
++        /**
++         * Attempts to raise the suborder to the suborder specified.
++         *
++         * @param subOrder Specified new sub order.
++         *
++         * @return {@code false} if the current task is completing, {@code true} if the suborder was raised to the
++         *          specified suborder or was already at the specified suborder or higher.
++         */
++        public boolean raiseSubOrder(final long subOrder);
++
++        /**
++         * Attempts to lower the suborder to the suborder specified.
++         *
++         * @param subOrder Specified new sub order.
++         *
++         * @return {@code false} if the current task is completing, {@code true} if the suborder was lowered to the
++         *          specified suborder or was already at the specified suborder or lower.
++         */
++        public boolean lowerSubOrder(final long subOrder);
++
++        /**
++         * Sets the priority and suborder id associated with this task. Ths function has no effect when this task
++         * is completing or is completed.
++         *
++         * @param priority Priority specified
++         * @param subOrder Specified new sub order.
++         * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
++         *         this task was scheduled on was shutdown, or if the current priority and suborder are the same as
++         *         the parameters.
++         */
++        public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder);
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/queue/PrioritisedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/queue/PrioritisedTaskQueue.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/queue/PrioritisedTaskQueue.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.executor.queue;
++
++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Priority;
++import java.lang.invoke.VarHandle;
++import java.util.Comparator;
++import java.util.Map;
++import java.util.concurrent.ConcurrentSkipListMap;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.concurrent.atomic.AtomicLong;
++
++public final class PrioritisedTaskQueue implements PrioritisedExecutor {
++
++    /**
++     * Required for tie-breaking in the queue
++     */
++    private final AtomicLong taskIdGenerator = new AtomicLong();
++    private final AtomicLong scheduledTasks = new AtomicLong();
++    private final AtomicLong executedTasks = new AtomicLong();
++    private final AtomicLong subOrderGenerator = new AtomicLong();
++    private final AtomicBoolean shutdown = new AtomicBoolean();
++    private final ConcurrentSkipListMap<PrioritisedQueuedTask.Holder, Boolean> tasks = new ConcurrentSkipListMap<>(PrioritisedQueuedTask.COMPARATOR);
++
++    @Override
++    public long getTotalTasksScheduled() {
++        return this.scheduledTasks.get();
++    }
++
++    @Override
++    public long getTotalTasksExecuted() {
++        return this.executedTasks.get();
++    }
++
++    @Override
++    public long generateNextSubOrder() {
++        return this.subOrderGenerator.getAndIncrement();
++    }
++
++    @Override
++    public boolean shutdown() {
++        return !this.shutdown.getAndSet(true);
++    }
++
++    @Override
++    public boolean isShutdown() {
++        return this.shutdown.get();
++    }
++
++    public PrioritisedTask peekFirst() {
++        final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.firstEntry();
++        return firstEntry == null ? null : firstEntry.getKey().task;
++    }
++
++    public Priority getHighestPriority() {
++        final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.firstEntry();
++        return firstEntry == null ? null : Priority.getPriority(firstEntry.getKey().priority);
++    }
++
++    public boolean hasNoScheduledTasks() {
++        final long executedTasks = this.executedTasks.get();
++        final long scheduledTasks = this.scheduledTasks.get();
++
++        return executedTasks == scheduledTasks;
++    }
++
++    public PrioritySubOrderPair getHighestPrioritySubOrder() {
++        final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.firstEntry();
++        if (firstEntry == null) {
++            return null;
++        }
++
++        final PrioritisedQueuedTask.Holder holder = firstEntry.getKey();
++
++        return new PrioritySubOrderPair(Priority.getPriority(holder.priority), holder.subOrder);
++    }
++
++    public Runnable pollTask() {
++        for (;;) {
++            final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.pollFirstEntry();
++            if (firstEntry != null) {
++                final PrioritisedQueuedTask.Holder task = firstEntry.getKey();
++                task.markRemoved();
++                if (!task.task.cancel()) {
++                    continue;
++                }
++                return task.task.execute;
++            }
++
++            return null;
++        }
++    }
++
++    @Override
++    public boolean executeTask() {
++        for (;;) {
++            final Map.Entry<PrioritisedQueuedTask.Holder, Boolean> firstEntry = this.tasks.pollFirstEntry();
++            if (firstEntry != null) {
++                final PrioritisedQueuedTask.Holder task = firstEntry.getKey();
++                task.markRemoved();
++                if (!task.task.execute()) {
++                    continue;
++                }
++                return true;
++            }
++
++            return false;
++        }
++    }
++
++    @Override
++    public PrioritisedTask createTask(final Runnable task) {
++        return this.createTask(task, Priority.NORMAL, this.generateNextSubOrder());
++    }
++
++    @Override
++    public PrioritisedTask createTask(final Runnable task, final Priority priority) {
++        return this.createTask(task, priority, this.generateNextSubOrder());
++    }
++
++    @Override
++    public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder) {
++        return new PrioritisedQueuedTask(task, priority, subOrder);
++    }
++
++    @Override
++    public PrioritisedTask queueTask(final Runnable task) {
++        return this.queueTask(task, Priority.NORMAL, this.generateNextSubOrder());
++    }
++
++    @Override
++    public PrioritisedTask queueTask(final Runnable task, final Priority priority) {
++        return this.queueTask(task, priority, this.generateNextSubOrder());
++    }
++
++    @Override
++    public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder) {
++        final PrioritisedQueuedTask ret = new PrioritisedQueuedTask(task, priority, subOrder);
++
++        ret.queue();
++
++        return ret;
++    }
++
++    private final class PrioritisedQueuedTask implements PrioritisedExecutor.PrioritisedTask {
++        public static final Comparator<PrioritisedQueuedTask.Holder> COMPARATOR = (final PrioritisedQueuedTask.Holder t1, final PrioritisedQueuedTask.Holder t2) -> {
++            final int priorityCompare = t1.priority - t2.priority;
++            if (priorityCompare != 0) {
++                return priorityCompare;
++            }
++
++            final int subOrderCompare = Long.compare(t1.subOrder, t2.subOrder);
++            if (subOrderCompare != 0) {
++                return subOrderCompare;
++            }
++
++            return Long.compare(t1.id, t2.id);
++        };
++
++        private static final class Holder {
++            private final PrioritisedQueuedTask task;
++            private final int priority;
++            private final long subOrder;
++            private final long id;
++
++            private volatile boolean removed;
++            private static final VarHandle REMOVED_HANDLE = ConcurrentUtil.getVarHandle(Holder.class, "removed", boolean.class);
++
++            private Holder(final PrioritisedQueuedTask task, final int priority, final long subOrder,
++                           final long id) {
++                this.task = task;
++                this.priority = priority;
++                this.subOrder = subOrder;
++                this.id = id;
++            }
++
++            /**
++             * Returns true if marked as removed
++             */
++            public boolean markRemoved() {
++                return !(boolean)REMOVED_HANDLE.getAndSet((Holder)this, (boolean)true);
++            }
++        }
++
++        private final long id;
++        private final Runnable execute;
++
++        private Priority priority;
++        private long subOrder;
++        private Holder holder;
++
++        public PrioritisedQueuedTask(final Runnable execute, final Priority priority, final long subOrder) {
++            if (!Priority.isValidPriority(priority)) {
++                throw new IllegalArgumentException("Invalid priority " + priority);
++            }
++
++            this.execute = execute;
++            this.priority = priority;
++            this.subOrder = subOrder;
++            this.id = PrioritisedTaskQueue.this.taskIdGenerator.getAndIncrement();
++        }
++
++        @Override
++        public PrioritisedExecutor getExecutor() {
++            return PrioritisedTaskQueue.this;
++        }
++
++        @Override
++        public boolean queue() {
++            synchronized (this) {
++                if (this.holder != null || this.priority == Priority.COMPLETING) {
++                    return false;
++                }
++
++                if (PrioritisedTaskQueue.this.isShutdown()) {
++                    throw new IllegalStateException("Queue is shutdown");
++                }
++
++                final Holder holder = new Holder(this, this.priority.priority, this.subOrder, this.id);
++                this.holder = holder;
++
++                PrioritisedTaskQueue.this.scheduledTasks.getAndIncrement();
++                PrioritisedTaskQueue.this.tasks.put(holder, Boolean.TRUE);
++            }
++
++            if (PrioritisedTaskQueue.this.isShutdown()) {
++                this.cancel();
++                throw new IllegalStateException("Queue is shutdown");
++            }
++
++
++            return true;
++        }
++
++        @Override
++        public boolean isQueued() {
++            synchronized (this) {
++                return this.holder != null && this.priority != Priority.COMPLETING;
++            }
++        }
++
++        @Override
++        public boolean cancel() {
++            synchronized (this) {
++                if (this.priority == Priority.COMPLETING) {
++                    return false;
++                }
++
++                this.priority = Priority.COMPLETING;
++
++                if (this.holder != null) {
++                    if (this.holder.markRemoved()) {
++                        PrioritisedTaskQueue.this.tasks.remove(this.holder);
++                    }
++                    PrioritisedTaskQueue.this.executedTasks.getAndIncrement();
++                }
++
++                return true;
++            }
++        }
++
++        @Override
++        public boolean execute() {
++            final boolean increaseExecuted;
++
++            synchronized (this) {
++                if (this.priority == Priority.COMPLETING) {
++                    return false;
++                }
++
++                this.priority = Priority.COMPLETING;
++
++                if (increaseExecuted = (this.holder != null)) {
++                    if (this.holder.markRemoved()) {
++                        PrioritisedTaskQueue.this.tasks.remove(this.holder);
++                    }
++                }
++            }
++
++            try {
++                this.execute.run();
++                return true;
++            } finally {
++                if (increaseExecuted) {
++                    PrioritisedTaskQueue.this.executedTasks.getAndIncrement();
++                }
++            }
++        }
++
++        @Override
++        public Priority getPriority() {
++            synchronized (this) {
++                return this.priority;
++            }
++        }
++
++        @Override
++        public boolean setPriority(final Priority priority) {
++            synchronized (this) {
++                if (this.priority == Priority.COMPLETING || this.priority == priority) {
++                    return false;
++                }
++
++                this.priority = priority;
++
++                if (this.holder != null) {
++                    if (this.holder.markRemoved()) {
++                        PrioritisedTaskQueue.this.tasks.remove(this.holder);
++                    }
++                    this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++                    PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++                }
++
++                return true;
++            }
++        }
++
++        @Override
++        public boolean raisePriority(final Priority priority) {
++            synchronized (this) {
++                if (this.priority == Priority.COMPLETING || this.priority.isHigherOrEqualPriority(priority)) {
++                    return false;
++                }
++
++                this.priority = priority;
++
++                if (this.holder != null) {
++                    if (this.holder.markRemoved()) {
++                        PrioritisedTaskQueue.this.tasks.remove(this.holder);
++                    }
++                    this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++                    PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++                }
++
++                return true;
++            }
++        }
++
++        @Override
++        public boolean lowerPriority(Priority priority) {
++            synchronized (this) {
++                if (this.priority == Priority.COMPLETING || this.priority.isLowerOrEqualPriority(priority)) {
++                    return false;
++                }
++
++                this.priority = priority;
++
++                if (this.holder != null) {
++                    if (this.holder.markRemoved()) {
++                        PrioritisedTaskQueue.this.tasks.remove(this.holder);
++                    }
++                    this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++                    PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++                }
++
++                return true;
++            }
++        }
++
++        @Override
++        public long getSubOrder() {
++            synchronized (this) {
++                return this.subOrder;
++            }
++        }
++
++        @Override
++        public boolean setSubOrder(final long subOrder) {
++            synchronized (this) {
++                if (this.priority == Priority.COMPLETING || this.subOrder == subOrder) {
++                    return false;
++                }
++
++                this.subOrder = subOrder;
++
++                if (this.holder != null) {
++                    if (this.holder.markRemoved()) {
++                        PrioritisedTaskQueue.this.tasks.remove(this.holder);
++                    }
++                    this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++                    PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++                }
++
++                return true;
++            }
++        }
++
++        @Override
++        public boolean raiseSubOrder(long subOrder) {
++            synchronized (this) {
++                if (this.priority == Priority.COMPLETING || this.subOrder >= subOrder) {
++                    return false;
++                }
++
++                this.subOrder = subOrder;
++
++                if (this.holder != null) {
++                    if (this.holder.markRemoved()) {
++                        PrioritisedTaskQueue.this.tasks.remove(this.holder);
++                    }
++                    this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++                    PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++                }
++
++                return true;
++            }
++        }
++
++        @Override
++        public boolean lowerSubOrder(final long subOrder) {
++            synchronized (this) {
++                if (this.priority == Priority.COMPLETING || this.subOrder <= subOrder) {
++                    return false;
++                }
++
++                this.subOrder = subOrder;
++
++                if (this.holder != null) {
++                    if (this.holder.markRemoved()) {
++                        PrioritisedTaskQueue.this.tasks.remove(this.holder);
++                    }
++                    this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++                    PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++                }
++
++                return true;
++            }
++        }
++
++        @Override
++        public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
++            synchronized (this) {
++                if (this.priority == Priority.COMPLETING || (this.priority == priority && this.subOrder == subOrder)) {
++                    return false;
++                }
++
++                this.priority = priority;
++                this.subOrder = subOrder;
++
++                if (this.holder != null) {
++                    if (this.holder.markRemoved()) {
++                        PrioritisedTaskQueue.this.tasks.remove(this.holder);
++                    }
++                    this.holder = new Holder(this, priority.priority, this.subOrder, this.id);
++                    PrioritisedTaskQueue.this.tasks.put(this.holder, Boolean.TRUE);
++                }
++
++                return true;
++            }
++        }
++    }
++
++    public static record PrioritySubOrderPair(Priority priority, long subOrder) {}
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package ca.spottedleaf.concurrentutil.executor.standard;
+-
+-import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+-import java.lang.invoke.VarHandle;
+-
+-public class DelayedPrioritisedTask {
+-
+-    protected volatile int priority;
+-    protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "priority", int.class);
+-
+-    protected static final int PRIORITY_SET = Integer.MIN_VALUE >>> 0;
+-
+-    protected final int getPriorityVolatile() {
+-        return (int)PRIORITY_HANDLE.getVolatile((DelayedPrioritisedTask)this);
+-    }
+-
+-    protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
+-        return (int)PRIORITY_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (int)expect, (int)update);
+-    }
+-
+-    protected final int getAndOrPriorityVolatile(final int val) {
+-        return (int)PRIORITY_HANDLE.getAndBitwiseOr((DelayedPrioritisedTask)this, (int)val);
+-    }
+-
+-    protected final void setPriorityPlain(final int val) {
+-        PRIORITY_HANDLE.set((DelayedPrioritisedTask)this, (int)val);
+-    }
+-
+-    protected volatile PrioritisedExecutor.PrioritisedTask task;
+-    protected static final VarHandle TASK_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "task", PrioritisedExecutor.PrioritisedTask.class);
+-
+-    protected PrioritisedExecutor.PrioritisedTask getTaskPlain() {
+-        return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.get((DelayedPrioritisedTask)this);
+-    }
+-
+-    protected PrioritisedExecutor.PrioritisedTask getTaskVolatile() {
+-        return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.getVolatile((DelayedPrioritisedTask)this);
+-    }
+-
+-    protected final PrioritisedExecutor.PrioritisedTask compareAndExchangeTaskVolatile(final PrioritisedExecutor.PrioritisedTask expect, final PrioritisedExecutor.PrioritisedTask update) {
+-        return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (PrioritisedExecutor.PrioritisedTask)expect, (PrioritisedExecutor.PrioritisedTask)update);
+-    }
+-
+-    public DelayedPrioritisedTask(final PrioritisedExecutor.Priority priority) {
+-        this.setPriorityPlain(priority.priority);
+-    }
+-
+-    // only public for debugging
+-    public int getPriorityInternal() {
+-        return this.getPriorityVolatile();
+-    }
+-
+-    public PrioritisedExecutor.PrioritisedTask getTask() {
+-        return this.getTaskVolatile();
+-    }
+-
+-    public void setTask(final PrioritisedExecutor.PrioritisedTask task) {
+-        int priority = this.getPriorityVolatile();
+-
+-        if (this.compareAndExchangeTaskVolatile(null, task) != null) {
+-            throw new IllegalStateException("setTask() called twice");
+-        }
+-
+-        int failures = 0;
+-        for (;;) {
+-            task.setPriority(PrioritisedExecutor.Priority.getPriority(priority));
+-
+-            if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SET))) {
+-                return;
+-            }
+-
+-            ++failures;
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-        }
+-    }
+-
+-    public PrioritisedExecutor.Priority getPriority() {
+-        final int priority = this.getPriorityVolatile();
+-        if ((priority & PRIORITY_SET) != 0) {
+-            return this.task.getPriority();
+-        }
+-
+-        return PrioritisedExecutor.Priority.getPriority(priority);
+-    }
+-
+-    public void raisePriority(final PrioritisedExecutor.Priority priority) {
+-        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+-            throw new IllegalArgumentException("Invalid priority " + priority);
+-        }
+-
+-        int failures = 0;
+-        for (int curr = this.getPriorityVolatile();;) {
+-            if ((curr & PRIORITY_SET) != 0) {
+-                this.getTaskPlain().raisePriority(priority);
+-                return;
+-            }
+-
+-            if (!priority.isLowerPriority(curr)) {
+-                return;
+-            }
+-
+-            if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+-                return;
+-            }
+-
+-            // failed, retry
+-
+-            ++failures;
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-        }
+-    }
+-
+-    public void setPriority(final PrioritisedExecutor.Priority priority) {
+-        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+-            throw new IllegalArgumentException("Invalid priority " + priority);
+-        }
+-
+-        int failures = 0;
+-        for (int curr = this.getPriorityVolatile();;) {
+-            if ((curr & PRIORITY_SET) != 0) {
+-                this.getTaskPlain().setPriority(priority);
+-                return;
+-            }
+-
+-            if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+-                return;
+-            }
+-
+-            // failed, retry
+-
+-            ++failures;
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-        }
+-    }
+-
+-    public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+-        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+-            throw new IllegalArgumentException("Invalid priority " + priority);
+-        }
+-
+-        int failures = 0;
+-        for (int curr = this.getPriorityVolatile();;) {
+-            if ((curr & PRIORITY_SET) != 0) {
+-                this.getTaskPlain().lowerPriority(priority);
+-                return;
+-            }
+-
+-            if (!priority.isHigherPriority(curr)) {
+-                return;
+-            }
+-
+-            if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+-                return;
+-            }
+-
+-            // failed, retry
+-
+-            ++failures;
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-        }
+-    }
+-}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package ca.spottedleaf.concurrentutil.executor.standard;
+-
+-import ca.spottedleaf.concurrentutil.executor.BaseExecutor;
+-
+-/**
+- * Implementation of {@link BaseExecutor} which schedules tasks to be executed by a given priority.
+- * @see BaseExecutor
+- */
+-public interface PrioritisedExecutor extends BaseExecutor {
+-
+-    public static enum Priority {
+-
+-        /**
+-         * Priority value indicating the task has completed or is being completed.
+-         * This priority cannot be used to schedule tasks.
+-         */
+-        COMPLETING(-1),
+-
+-        /**
+-         * Absolute highest priority, should only be used for when a task is blocking a time-critical thread.
+-         */
+-        BLOCKING(),
+-
+-        /**
+-         * Should only be used for urgent but not time-critical tasks.
+-         */
+-        HIGHEST(),
+-
+-        /**
+-         * Two priorities above normal.
+-         */
+-        HIGHER(),
+-
+-        /**
+-         * One priority above normal.
+-         */
+-        HIGH(),
+-
+-        /**
+-         * Default priority.
+-         */
+-        NORMAL(),
+-
+-        /**
+-         * One priority below normal.
+-         */
+-        LOW(),
+-
+-        /**
+-         * Two priorities below normal.
+-         */
+-        LOWER(),
+-
+-        /**
+-         * Use for tasks that should eventually execute, but are not needed to.
+-         */
+-        LOWEST(),
+-
+-        /**
+-         * Use for tasks that can be delayed indefinitely.
+-         */
+-        IDLE();
+-
+-        // returns whether the priority can be scheduled
+-        public static boolean isValidPriority(final Priority priority) {
+-            return priority != null && priority != Priority.COMPLETING;
+-        }
+-
+-        // returns the higher priority of the two
+-        public static Priority max(final Priority p1, final Priority p2) {
+-            return p1.isHigherOrEqualPriority(p2) ? p1 : p2;
+-        }
+-
+-        // returns the lower priroity of the two
+-        public static Priority min(final Priority p1, final Priority p2) {
+-            return p1.isLowerOrEqualPriority(p2) ? p1 : p2;
+-        }
+-
+-        public boolean isHigherOrEqualPriority(final Priority than) {
+-            return this.priority <= than.priority;
+-        }
+-
+-        public boolean isHigherPriority(final Priority than) {
+-            return this.priority < than.priority;
+-        }
+-
+-        public boolean isLowerOrEqualPriority(final Priority than) {
+-            return this.priority >= than.priority;
+-        }
+-
+-        public boolean isLowerPriority(final Priority than) {
+-            return this.priority > than.priority;
+-        }
+-
+-        public boolean isHigherOrEqualPriority(final int than) {
+-            return this.priority <= than;
+-        }
+-
+-        public boolean isHigherPriority(final int than) {
+-            return this.priority < than;
+-        }
+-
+-        public boolean isLowerOrEqualPriority(final int than) {
+-            return this.priority >= than;
+-        }
+-
+-        public boolean isLowerPriority(final int than) {
+-            return this.priority > than;
+-        }
+-
+-        public static boolean isHigherOrEqualPriority(final int priority, final int than) {
+-            return priority <= than;
+-        }
+-
+-        public static boolean isHigherPriority(final int priority, final int than) {
+-            return priority < than;
+-        }
+-
+-        public static boolean isLowerOrEqualPriority(final int priority, final int than) {
+-            return priority >= than;
+-        }
+-
+-        public static boolean isLowerPriority(final int priority, final int than) {
+-            return priority > than;
+-        }
+-
+-        static final Priority[] PRIORITIES = Priority.values();
+-
+-        /** includes special priorities */
+-        public static final int TOTAL_PRIORITIES = PRIORITIES.length;
+-
+-        public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1;
+-
+-        public static Priority getPriority(final int priority) {
+-            return PRIORITIES[priority + 1];
+-        }
+-
+-        private static int priorityCounter;
+-
+-        private static int nextCounter() {
+-            return priorityCounter++;
+-        }
+-
+-        public final int priority;
+-
+-        Priority() {
+-            this(nextCounter());
+-        }
+-
+-        Priority(final int priority) {
+-            this.priority = priority;
+-        }
+-    }
+-
+-    /**
+-     * Executes the next available task.
+-     * <p>
+-     *     If there is a task with priority {@link PrioritisedExecutor.Priority#BLOCKING} available, then that such task is executed.
+-     * </p>
+-     * <p>
+-     *     If there is a task with priority {@link PrioritisedExecutor.Priority#IDLE} available then that task is only executed
+-     *     when there are no other tasks available with a higher priority.
+-     * </p>
+-     * <p>
+-     *     If there are no tasks that have priority {@link PrioritisedExecutor.Priority#BLOCKING} or {@link PrioritisedExecutor.Priority#IDLE}, then
+-     *     this function will be biased to execute tasks that have higher priorities.
+-     * </p>
+-     *
+-     * @return {@code true} if a task was executed, {@code false} otherwise
+-     * @throws IllegalStateException If the current thread is not allowed to execute a task
+-     */
+-    @Override
+-    public boolean executeTask() throws IllegalStateException;
+-
+-    /**
+-     * Queues or executes a task at {@link Priority#NORMAL} priority.
+-     * @param task The task to run.
+-     *
+-     * @throws IllegalStateException If this queue has shutdown.
+-     * @throws NullPointerException If the task is null
+-     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
+-     * associated with the parameter
+-     */
+-    public default PrioritisedTask queueRunnable(final Runnable task) {
+-        return this.queueRunnable(task, Priority.NORMAL);
+-    }
+-
+-    /**
+-     * Queues or executes a task.
+-     *
+-     * @param task The task to run.
+-     * @param priority The priority for the task.
+-     *
+-     * @throws IllegalStateException If this queue has shutdown.
+-     * @throws NullPointerException If the task is null
+-     * @throws IllegalArgumentException If the priority is invalid.
+-     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
+-     * associated with the parameter
+-     */
+-    public PrioritisedTask queueRunnable(final Runnable task, final Priority priority);
+-
+-    /**
+-     * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseTask#queue()}.
+-     *
+-     * @param task The task to run.
+-     *
+-     * @throws IllegalStateException If this queue has shutdown.
+-     * @throws NullPointerException If the task is null
+-     * @throws IllegalArgumentException If the priority is invalid.
+-     * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
+-     * @return The prioritised task associated with the parameters
+-     */
+-    public default PrioritisedTask createTask(final Runnable task) {
+-        return this.createTask(task, Priority.NORMAL);
+-    }
+-
+-    /**
+-     * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseTask#queue()}.
+-     *
+-     * @param task The task to run.
+-     * @param priority The priority for the task.
+-     *
+-     * @throws IllegalStateException If this queue has shutdown.
+-     * @throws NullPointerException If the task is null
+-     * @throws IllegalArgumentException If the priority is invalid.
+-     * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
+-     * @return The prioritised task associated with the parameters
+-     */
+-    public PrioritisedTask createTask(final Runnable task, final Priority priority);
+-
+-    /**
+-     * Extension of {@link ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask} which adds functions
+-     * to retrieve and modify the task's associated priority.
+-     *
+-     * @see ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask
+-     */
+-    public static interface PrioritisedTask extends BaseTask {
+-
+-        /**
+-         * Returns the current priority. Note that {@link Priority#COMPLETING} will be returned
+-         * if this task is completing or has completed.
+-         */
+-        public Priority getPriority();
+-
+-        /**
+-         * Attempts to set this task's priority level to the level specified.
+-         *
+-         * @param priority Specified priority level.
+-         *
+-         * @throws IllegalArgumentException If the priority is invalid
+-         * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
+-         * this task was scheduled on was shutdown, or if the priority was already at the specified level.
+-         */
+-        public boolean setPriority(final Priority priority);
+-
+-        /**
+-         * Attempts to raise the priority to the priority level specified.
+-         *
+-         * @param priority Priority specified
+-         *
+-         * @throws IllegalArgumentException If the priority is invalid
+-         * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher.
+-         */
+-        public boolean raisePriority(final Priority priority);
+-
+-        /**
+-         * Attempts to lower the priority to the priority level specified.
+-         *
+-         * @param priority Priority specified
+-         *
+-         * @throws IllegalArgumentException If the priority is invalid
+-         * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower.
+-         */
+-        public boolean lowerPriority(final Priority priority);
+-    }
+-}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package ca.spottedleaf.concurrentutil.executor.standard;
+-
+-import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+-import org.slf4j.Logger;
+-import org.slf4j.LoggerFactory;
+-import java.util.ArrayList;
+-import java.util.Arrays;
+-import java.util.Comparator;
+-import java.util.TreeSet;
+-import java.util.concurrent.atomic.AtomicBoolean;
+-import java.util.function.BiConsumer;
+-
+-public final class PrioritisedThreadPool {
+-
+-    private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedThreadPool.class);
+-
+-    private final PrioritisedThread[] threads;
+-    private final TreeSet<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator());
+-    private final String name;
+-    private final long queueMaxHoldTime;
+-
+-    private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>();
+-    private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> activeQueues = new ReferenceOpenHashSet<>();
+-
+-    private boolean shutdown;
+-
+-    private long schedulingIdGenerator;
+-
+-    private static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6);
+-
+-    /**
+-     * @param name Specified debug name of this thread pool
+-     * @param threads The number of threads to use
+-     */
+-    public PrioritisedThreadPool(final String name, final int threads) {
+-        this(name, threads, null);
+-    }
+-
+-    /**
+-     * @param name Specified debug name of this thread pool
+-     * @param threads The number of threads to use
+-     * @param threadModifier Invoked for each created thread with its incremental id before starting them
+-     */
+-    public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier) {
+-        this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms
+-    }
+-
+-    /**
+-     * @param name Specified debug name of this thread pool
+-     * @param threads The number of threads to use
+-     * @param threadModifier Invoked for each created thread with its incremental id before starting them
+-     * @param queueHoldTime The maximum amount of time to spend executing tasks in a specific queue before attempting
+-     *                      to switch to another queue, per thread
+-     */
+-    public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier,
+-                                 final long queueHoldTime) { // in ns
+-        if (threads <= 0) {
+-            throw new IllegalArgumentException("Thread count must be > 0, not " + threads);
+-        }
+-        if (name == null) {
+-            throw new IllegalArgumentException("Name cannot be null");
+-        }
+-        this.name = name;
+-        this.queueMaxHoldTime = queueHoldTime;
+-
+-        this.threads = new PrioritisedThread[threads];
+-        for (int i = 0; i < threads; ++i) {
+-            this.threads[i] = new PrioritisedThread(this);
+-
+-            // set default attributes
+-            this.threads[i].setName("Prioritised thread for pool '" + name + "' #" + i);
+-            this.threads[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
+-                LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
+-            });
+-
+-            // let thread modifier override defaults
+-            if (threadModifier != null) {
+-                threadModifier.accept(this.threads[i], Integer.valueOf(i));
+-            }
+-
+-            // now the thread can start
+-            this.threads[i].start();
+-        }
+-    }
+-
+-    /**
+-     * Returns an array representing the threads backing this thread pool.
+-     */
+-    public Thread[] getThreads() {
+-        return Arrays.copyOf(this.threads, this.threads.length, Thread[].class);
+-    }
+-
+-    /**
+-     * Creates and returns a {@link PrioritisedPoolExecutor} to schedule tasks onto. The returned executor will execute
+-     * tasks on this thread pool only.
+-     * @param name The debug name of the executor.
+-     * @param minParallelism The minimum number of threads to be executing tasks from the returned executor
+-     *                       before threads may be allocated to other queues in this thread pool.
+-     * @param parallelism The maximum number of threads which may be executing tasks from the returned executor.
+-     * @throws IllegalStateException If this thread pool is shut down
+-     */
+-    public PrioritisedPoolExecutor createExecutor(final String name, final int minParallelism, final int parallelism) {
+-        synchronized (this.nonShutdownQueues) {
+-            if (this.shutdown) {
+-                throw new IllegalStateException("Queue is shutdown: " + this.toString());
+-            }
+-            final PrioritisedPoolExecutorImpl ret = new PrioritisedPoolExecutorImpl(
+-                    this, name,
+-                    Math.min(Math.max(1, parallelism), this.threads.length),
+-                    Math.min(Math.max(0, minParallelism), this.threads.length)
+-            );
+-
+-            this.nonShutdownQueues.add(ret);
+-
+-            synchronized (this.activeQueues) {
+-                this.activeQueues.add(ret);
+-            }
+-
+-            return ret;
+-        }
+-    }
+-
+-    /**
+-     * Prevents creation of new queues, shutdowns all non-shutdown queues if specified
+-     */
+-    public void halt(final boolean shutdownQueues) {
+-        synchronized (this.nonShutdownQueues) {
+-            this.shutdown = true;
+-        }
+-        if (shutdownQueues) {
+-            final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown;
+-            synchronized (this.nonShutdownQueues) {
+-                this.shutdown = true;
+-                queuesToShutdown = new ArrayList<>(this.nonShutdownQueues);
+-            }
+-
+-            for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) {
+-                queue.shutdown();
+-            }
+-        }
+-
+-
+-        for (final PrioritisedThread thread : this.threads) {
+-            // can't kill queue, queue is null
+-            thread.halt(false);
+-        }
+-    }
+-
+-    /**
+-     * Waits until all threads in this pool have shutdown, or until the specified time has passed.
+-     * @param msToWait Maximum time to wait.
+-     * @return {@code false} if the maximum time passed, {@code true} otherwise.
+-     */
+-    public boolean join(final long msToWait) {
+-        try {
+-            return this.join(msToWait, false);
+-        } catch (final InterruptedException ex) {
+-            throw new IllegalStateException(ex);
+-        }
+-    }
+-
+-    /**
+-     * Waits until all threads in this pool have shutdown, or until the specified time has passed.
+-     * @param msToWait Maximum time to wait.
+-     * @return {@code false} if the maximum time passed, {@code true} otherwise.
+-     * @throws InterruptedException If this thread is interrupted.
+-     */
+-    public boolean joinInterruptable(final long msToWait) throws InterruptedException {
+-        return this.join(msToWait, true);
+-    }
+-
+-    protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException {
+-        final long nsToWait = msToWait * (1000 * 1000);
+-        final long start = System.nanoTime();
+-        final long deadline = start + nsToWait;
+-        boolean interrupted = false;
+-        try {
+-            for (final PrioritisedThread thread : this.threads) {
+-                for (;;) {
+-                    if (!thread.isAlive()) {
+-                        break;
+-                    }
+-                    final long current = System.nanoTime();
+-                    if (current >= deadline) {
+-                        return false;
+-                    }
+-
+-                    try {
+-                        thread.join(Math.max(1L, (deadline - current) / (1000 * 1000)));
+-                    } catch (final InterruptedException ex) {
+-                        if (interruptable) {
+-                            throw ex;
+-                        }
+-                        interrupted = true;
+-                    }
+-                }
+-            }
+-
+-            return true;
+-        } finally {
+-            if (interrupted) {
+-                Thread.currentThread().interrupt();
+-            }
+-        }
+-    }
+-
+-    /**
+-     * Shuts down this thread pool, optionally waiting for all tasks to be executed.
+-     * This function will invoke {@link PrioritisedPoolExecutor#shutdown()} on all created executors on this
+-     * thread pool.
+-     * @param wait Whether to wait for tasks to be executed
+-     */
+-    public void shutdown(final boolean wait) {
+-        final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown;
+-        synchronized (this.nonShutdownQueues) {
+-            this.shutdown = true;
+-            queuesToShutdown = new ArrayList<>(this.nonShutdownQueues);
+-        }
+-
+-        for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) {
+-            queue.shutdown();
+-        }
+-
+-        for (final PrioritisedThread thread : this.threads) {
+-            // none of these can be true or else NPE
+-            thread.close(false, false);
+-        }
+-
+-        if (wait) {
+-            final ArrayList<PrioritisedPoolExecutorImpl> queues;
+-            synchronized (this.activeQueues) {
+-                queues = new ArrayList<>(this.activeQueues);
+-            }
+-            for (final PrioritisedPoolExecutorImpl queue : queues) {
+-                queue.waitUntilAllExecuted();
+-            }
+-        }
+-    }
+-
+-    protected static final class PrioritisedThread extends PrioritisedQueueExecutorThread {
+-
+-        protected final PrioritisedThreadPool pool;
+-        protected final AtomicBoolean alertedHighPriority = new AtomicBoolean();
+-
+-        public PrioritisedThread(final PrioritisedThreadPool pool) {
+-            super(null);
+-            this.pool = pool;
+-        }
+-
+-        public boolean alertHighPriorityExecutor() {
+-            if (!this.notifyTasks()) {
+-                if (!this.alertedHighPriority.get()) {
+-                    this.alertedHighPriority.set(true);
+-                }
+-                return false;
+-            }
+-
+-            return true;
+-        }
+-
+-        private boolean isAlertedHighPriority() {
+-            return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false);
+-        }
+-
+-        @Override
+-        protected boolean pollTasks() {
+-            final PrioritisedThreadPool pool = this.pool;
+-            final TreeSet<PrioritisedPoolExecutorImpl> queues = this.pool.queues;
+-
+-            boolean ret = false;
+-            for (;;) {
+-                if (this.halted) {
+-                    break;
+-                }
+-                // try to find a queue
+-                // note that if and ONLY IF the queues set is empty, this means there are no tasks for us to execute.
+-                // so we can only break when it's empty
+-                final PrioritisedPoolExecutorImpl queue;
+-                // select queue
+-                synchronized (queues) {
+-                    queue = queues.pollFirst();
+-                    if (queue == null) {
+-                        // no tasks to execute
+-                        break;
+-                    }
+-
+-                    queue.schedulingId = ++pool.schedulingIdGenerator;
+-                    // we own this queue now, so increment the executor count
+-                    // do we also need to push this queue up for grabs for another executor?
+-                    if (++queue.concurrentExecutors < queue.maximumExecutors) {
+-                        // re-add to queues
+-                        // it's very important this is done in the same synchronised block for polling, as this prevents
+-                        // us from possibly later adding a queue that should not exist in the set
+-                        queues.add(queue);
+-                        queue.isQueued = true;
+-                    } else {
+-                        queue.isQueued = false;
+-                    }
+-                    // note: we cannot drain entries from the queue while holding this lock, as it will cause deadlock
+-                    // the queue addition holds the per-queue lock first then acquires the lock we have now, but if we
+-                    // try to poll now we don't hold the per queue lock but we do hold the global lock...
+-                }
+-
+-                // parse tasks as long as we are allowed
+-                final long start = System.nanoTime();
+-                final long deadline = start + pool.queueMaxHoldTime;
+-                do {
+-                    try {
+-                        if (this.halted) {
+-                            break;
+-                        }
+-                        if (!queue.executeTask()) {
+-                            // no more tasks, try next queue
+-                            break;
+-                        }
+-                        ret = true;
+-                    } catch (final ThreadDeath death) {
+-                        throw death; // goodbye world...
+-                    } catch (final Throwable throwable) {
+-                        LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + queue.toString() + "'", throwable);
+-                    }
+-                } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline);
+-
+-                synchronized (queues) {
+-                    // decrement executors, we are no longer executing
+-                    if (queue.isQueued) {
+-                        queues.remove(queue);
+-                        queue.isQueued = false;
+-                    }
+-                    if (--queue.concurrentExecutors == 0 && queue.scheduledPriority == null) {
+-                        // reset scheduling id once the queue is empty again
+-                        // this will ensure empty queues are not prioritised suddenly over active queues once tasks are
+-                        // queued
+-                        queue.schedulingId = 0L;
+-                    }
+-
+-                    // ensure the executor is queued for execution again
+-                    if (!queue.isHalted && queue.scheduledPriority != null) { // make sure it actually has tasks
+-                        queues.add(queue);
+-                        queue.isQueued = true;
+-                    }
+-                }
+-            }
+-
+-            return ret;
+-        }
+-    }
+-
+-    public interface PrioritisedPoolExecutor extends PrioritisedExecutor {
+-
+-        /**
+-         * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed
+-         */
+-        public void halt();
+-
+-        /**
+-         * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether
+-         * this queue is not halted and not shutdown.
+-         */
+-        public boolean isActive();
+-    }
+-
+-    protected static final class PrioritisedPoolExecutorImpl extends PrioritisedThreadedTaskQueue implements PrioritisedPoolExecutor {
+-
+-        protected final PrioritisedThreadPool pool;
+-        protected final long[] priorityCounts = new long[Priority.TOTAL_SCHEDULABLE_PRIORITIES];
+-        protected long schedulingId;
+-        protected int concurrentExecutors;
+-        protected Priority scheduledPriority;
+-
+-        protected final String name;
+-        protected final int maximumExecutors;
+-        protected final int minimumExecutors;
+-        protected boolean isQueued;
+-
+-        public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors, final int minimumExecutors) {
+-            this.pool = pool;
+-            this.name = name;
+-            this.maximumExecutors = maximumExecutors;
+-            this.minimumExecutors = minimumExecutors;
+-        }
+-
+-        public static Comparator<PrioritisedPoolExecutorImpl> comparator() {
+-            return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> {
+-                if (p1 == p2) {
+-                    return 0;
+-                }
+-
+-                final int belowMin1 = p1.minimumExecutors - p1.concurrentExecutors;
+-                final int belowMin2 = p2.minimumExecutors - p2.concurrentExecutors;
+-
+-                // test minimum executors
+-                if (belowMin1 > 0 || belowMin2 > 0) {
+-                    // want the largest belowMin to be first
+-                    final int minCompare = Integer.compare(belowMin2, belowMin1);
+-
+-                    if (minCompare != 0) {
+-                        return minCompare;
+-                    }
+-                }
+-
+-                // prefer higher priority
+-                final int priorityCompare = p1.scheduledPriority.ordinal() - p2.scheduledPriority.ordinal();
+-                if (priorityCompare != 0) {
+-                    return priorityCompare;
+-                }
+-
+-                // try to spread out the executors so that each can have threads executing
+-                final int executorCompare = p1.concurrentExecutors - p2.concurrentExecutors;
+-                if (executorCompare != 0) {
+-                    return executorCompare;
+-                }
+-
+-                // if all else fails here we just choose whichever executor was queued first
+-                return Long.compare(p1.schedulingId, p2.schedulingId);
+-            };
+-        }
+-
+-        private boolean isHalted;
+-
+-        @Override
+-        public void halt() {
+-            final PrioritisedThreadPool pool = this.pool;
+-            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
+-            synchronized (queues) {
+-                if (this.isHalted) {
+-                    return;
+-                }
+-                this.isHalted = true;
+-                if (this.isQueued) {
+-                    queues.remove(this);
+-                    this.isQueued = false;
+-                }
+-            }
+-            synchronized (pool.nonShutdownQueues) {
+-                pool.nonShutdownQueues.remove(this);
+-            }
+-            synchronized (pool.activeQueues) {
+-                pool.activeQueues.remove(this);
+-            }
+-        }
+-
+-        @Override
+-        public boolean isActive() {
+-            final PrioritisedThreadPool pool = this.pool;
+-            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
+-
+-            synchronized (queues) {
+-                if (this.concurrentExecutors != 0) {
+-                    return true;
+-                }
+-                synchronized (pool.activeQueues) {
+-                    if (pool.activeQueues.contains(this)) {
+-                        return true;
+-                    }
+-                }
+-            }
+-
+-            return false;
+-        }
+-
+-        private long totalQueuedTasks = 0L;
+-
+-        @Override
+-        protected void priorityChange(final PrioritisedThreadedTaskQueue.PrioritisedTask task, final Priority from, final Priority to) {
+-            // Note: The superclass' queue lock is ALWAYS held when inside this method. So we do NOT need to do any additional synchronisation
+-            // for accessing this queue's state.
+-            final long[] priorityCounts = this.priorityCounts;
+-            final boolean shutdown = this.isShutdown();
+-
+-            if (from == null && to == Priority.COMPLETING) {
+-                throw new IllegalStateException("Cannot complete task without queueing it first");
+-            }
+-
+-            // we should only notify for queueing of tasks, not changing priorities
+-            final boolean shouldNotifyTasks = from == null;
+-
+-            final Priority scheduledPriority = this.scheduledPriority;
+-            if (from != null) {
+-                --priorityCounts[from.priority];
+-            }
+-            if (to != Priority.COMPLETING) {
+-                ++priorityCounts[to.priority];
+-            }
+-            final long totalQueuedTasks;
+-            if (to == Priority.COMPLETING) {
+-                totalQueuedTasks = --this.totalQueuedTasks;
+-            } else if (from == null) {
+-                totalQueuedTasks = ++this.totalQueuedTasks;
+-            } else {
+-                totalQueuedTasks = this.totalQueuedTasks;
+-            }
+-
+-            // find new highest priority
+-            int highest = Math.min(to == Priority.COMPLETING ? Priority.IDLE.priority : to.priority, scheduledPriority == null ? Priority.IDLE.priority : scheduledPriority.priority);
+-            int lowestPriority = priorityCounts.length; // exclusive
+-            for (;highest < lowestPriority; ++highest) {
+-                final long count = priorityCounts[highest];
+-                if (count < 0) {
+-                    throw new IllegalStateException("Priority " + highest + " has " + count + " scheduled tasks");
+-                }
+-
+-                if (count != 0) {
+-                    break;
+-                }
+-            }
+-
+-            final Priority newPriority;
+-            if (highest == lowestPriority) {
+-                // no tasks left
+-                newPriority = null;
+-            } else if (shutdown) {
+-                // whichever is lower, the actual greatest priority or simply HIGHEST
+-                // this is so shutdown automatically gets priority
+-                newPriority = Priority.getPriority(Math.min(highest, Priority.HIGHEST.priority));
+-            } else {
+-                newPriority = Priority.getPriority(highest);
+-            }
+-
+-            final int executorsWanted;
+-            boolean shouldNotifyHighPriority = false;
+-
+-            final PrioritisedThreadPool pool = this.pool;
+-            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
+-
+-            synchronized (queues) {
+-                if (!this.isQueued) {
+-                    // see if we need to be queued
+-                    if (newPriority != null) {
+-                        if (this.schedulingId == 0L) {
+-                            this.schedulingId = ++pool.schedulingIdGenerator;
+-                        }
+-                        this.scheduledPriority = newPriority; // must be updated before queue add
+-                        if (!this.isHalted && this.concurrentExecutors < this.maximumExecutors) {
+-                            shouldNotifyHighPriority = newPriority.isHigherOrEqualPriority(Priority.HIGH);
+-                            queues.add(this);
+-                            this.isQueued = true;
+-                        }
+-                    } else {
+-                        // do not queue
+-                        this.scheduledPriority = null;
+-                    }
+-                } else {
+-                    // see if we need to NOT be queued
+-                    if (newPriority == null) {
+-                        queues.remove(this);
+-                        this.scheduledPriority = null;
+-                        this.isQueued = false;
+-                    } else if (scheduledPriority != newPriority) {
+-                        // if our priority changed, we need to update it - which means removing and re-adding into the queue
+-                        queues.remove(this);
+-                        // only now can we update scheduledPriority, since we are no longer in queue
+-                        this.scheduledPriority = newPriority;
+-                        queues.add(this);
+-                        shouldNotifyHighPriority = (scheduledPriority == null || scheduledPriority.isLowerPriority(Priority.HIGH)) && newPriority.isHigherOrEqualPriority(Priority.HIGH);
+-                    }
+-                }
+-
+-                if (this.isQueued) {
+-                    executorsWanted = Math.min(this.maximumExecutors - this.concurrentExecutors, (int)totalQueuedTasks);
+-                } else {
+-                    executorsWanted = 0;
+-                }
+-            }
+-
+-            if (newPriority == null && shutdown) {
+-                synchronized (pool.activeQueues) {
+-                    pool.activeQueues.remove(this);
+-                }
+-            }
+-
+-            // Wake up the number of executors we want
+-            if (executorsWanted > 0 || (shouldNotifyTasks | shouldNotifyHighPriority)) {
+-                int notified = 0;
+-                for (final PrioritisedThread thread : pool.threads) {
+-                    if ((shouldNotifyHighPriority ? thread.alertHighPriorityExecutor() : thread.notifyTasks())
+-                            && (++notified >= executorsWanted)) {
+-                        break;
+-                    }
+-                }
+-            }
+-        }
+-
+-        @Override
+-        public boolean shutdown() {
+-            final boolean ret = super.shutdown();
+-            if (!ret) {
+-                return ret;
+-            }
+-
+-            final PrioritisedThreadPool pool = this.pool;
+-
+-            // remove from active queues
+-            synchronized (pool.nonShutdownQueues) {
+-                pool.nonShutdownQueues.remove(this);
+-            }
+-
+-            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
+-
+-            // try and shift around our priority
+-            synchronized (queues) {
+-                if (this.scheduledPriority == null) {
+-                    // no tasks are queued, ensure we aren't in activeQueues
+-                    synchronized (pool.activeQueues) {
+-                        pool.activeQueues.remove(this);
+-                    }
+-
+-                    return ret;
+-                }
+-
+-                // try to set scheduled priority to HIGHEST so it drains faster
+-
+-                if (this.scheduledPriority.isHigherOrEqualPriority(Priority.HIGHEST)) {
+-                    // already at target priority (highest or above)
+-                    return ret;
+-                }
+-
+-                // shift priority to HIGHEST
+-
+-                if (this.isQueued) {
+-                    queues.remove(this);
+-                    this.scheduledPriority = Priority.HIGHEST;
+-                    queues.add(this);
+-                } else {
+-                    this.scheduledPriority = Priority.HIGHEST;
+-                }
+-            }
+-
+-            return ret;
+-        }
+-    }
+-}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package ca.spottedleaf.concurrentutil.executor.standard;
+-
+-import java.util.ArrayDeque;
+-import java.util.concurrent.atomic.AtomicLong;
+-
+-public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor {
+-
+-    protected final ArrayDeque<PrioritisedTask>[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; {
+-        for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) {
+-            this.queues[i] = new ArrayDeque<>();
+-        }
+-    }
+-
+-    // Use AtomicLong to separate from the queue field, we don't want false sharing here.
+-    protected final AtomicLong totalScheduledTasks = new AtomicLong();
+-    protected final AtomicLong totalCompletedTasks = new AtomicLong();
+-
+-    // this is here to prevent failures to queue stalling flush() calls (as the schedule calls would increment totalScheduledTasks without this check)
+-    protected volatile boolean hasShutdown;
+-
+-    protected long taskIdGenerator = 0;
+-
+-    @Override
+-    public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final Priority priority) throws IllegalStateException, IllegalArgumentException {
+-        if (!Priority.isValidPriority(priority)) {
+-            throw new IllegalArgumentException("Priority " + priority + " is invalid");
+-        }
+-        if (task == null) {
+-            throw new NullPointerException("Task cannot be null");
+-        }
+-
+-        if (this.hasShutdown) {
+-            // prevent us from stalling flush() calls by incrementing scheduled tasks when we really didn't schedule something
+-            throw new IllegalStateException("Queue has shutdown");
+-        }
+-
+-        final PrioritisedTask ret;
+-
+-        synchronized (this.queues) {
+-            if (this.hasShutdown) {
+-                throw new IllegalStateException("Queue has shutdown");
+-            }
+-            this.getAndAddTotalScheduledTasksVolatile(1L);
+-
+-            ret = new PrioritisedTask(this.taskIdGenerator++, task, priority, this);
+-
+-            this.queues[ret.priority.priority].add(ret);
+-
+-            // call priority change callback (note: only after we successfully queue!)
+-            this.priorityChange(ret, null, priority);
+-        }
+-
+-        return ret;
+-    }
+-
+-    @Override
+-    public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) {
+-        if (!Priority.isValidPriority(priority)) {
+-            throw new IllegalArgumentException("Priority " + priority + " is invalid");
+-        }
+-        if (task == null) {
+-            throw new NullPointerException("Task cannot be null");
+-        }
+-
+-        return new PrioritisedTask(task, priority, this);
+-    }
+-
+-    @Override
+-    public long getTotalTasksScheduled() {
+-        return this.totalScheduledTasks.get();
+-    }
+-
+-    @Override
+-    public long getTotalTasksExecuted() {
+-        return this.totalCompletedTasks.get();
+-    }
+-
+-    // callback method for subclasses to override
+-    // from is null when a task is immediately created
+-    protected void priorityChange(final PrioritisedTask task, final Priority from, final Priority to) {}
+-
+-    /**
+-     * Polls the highest priority task currently available. {@code null} if none. This will mark the
+-     * returned task as completed.
+-     */
+-    protected PrioritisedTask poll() {
+-        return this.poll(Priority.IDLE);
+-    }
+-
+-    protected PrioritisedTask poll(final Priority minPriority) {
+-        final ArrayDeque<PrioritisedTask>[] queues = this.queues;
+-        synchronized (queues) {
+-            final int max = minPriority.priority;
+-            for (int i = 0; i <= max; ++i) {
+-                final ArrayDeque<PrioritisedTask> queue = queues[i];
+-                PrioritisedTask task;
+-                while ((task = queue.pollFirst()) != null) {
+-                    if (task.trySetCompleting(i)) {
+-                        return task;
+-                    }
+-                }
+-            }
+-        }
+-
+-        return null;
+-    }
+-
+-    /**
+-     * Polls and executes the highest priority task currently available. Exceptions thrown during task execution will
+-     * be rethrown.
+-     * @return {@code true} if a task was executed, {@code false} otherwise.
+-     */
+-    @Override
+-    public boolean executeTask() {
+-        final PrioritisedTask task = this.poll();
+-
+-        if (task != null) {
+-            task.executeInternal();
+-            return true;
+-        }
+-
+-        return false;
+-    }
+-
+-    @Override
+-    public boolean shutdown() {
+-        synchronized (this.queues) {
+-            if (this.hasShutdown) {
+-                return false;
+-            }
+-            this.hasShutdown = true;
+-        }
+-        return true;
+-    }
+-
+-    @Override
+-    public boolean isShutdown() {
+-        return this.hasShutdown;
+-    }
+-
+-    /* totalScheduledTasks */
+-
+-    protected final long getTotalScheduledTasksVolatile() {
+-        return this.totalScheduledTasks.get();
+-    }
+-
+-    protected final long getAndAddTotalScheduledTasksVolatile(final long value) {
+-        return this.totalScheduledTasks.getAndAdd(value);
+-    }
+-
+-    /* totalCompletedTasks */
+-
+-    protected final long getTotalCompletedTasksVolatile() {
+-        return this.totalCompletedTasks.get();
+-    }
+-
+-    protected final long getAndAddTotalCompletedTasksVolatile(final long value) {
+-        return this.totalCompletedTasks.getAndAdd(value);
+-    }
+-
+-    protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask {
+-        protected final PrioritisedThreadedTaskQueue queue;
+-        protected long id;
+-        protected static final long NOT_SCHEDULED_ID = -1L;
+-
+-        protected Runnable runnable;
+-        protected volatile Priority priority;
+-
+-        protected PrioritisedTask(final long id, final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) {
+-            if (!Priority.isValidPriority(priority)) {
+-                throw new IllegalArgumentException("Invalid priority " + priority);
+-            }
+-
+-            this.priority = priority;
+-            this.runnable = runnable;
+-            this.queue = queue;
+-            this.id = id;
+-        }
+-
+-        protected PrioritisedTask(final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) {
+-            if (!Priority.isValidPriority(priority)) {
+-                throw new IllegalArgumentException("Invalid priority " + priority);
+-            }
+-
+-            this.priority = priority;
+-            this.runnable = runnable;
+-            this.queue = queue;
+-            this.id = NOT_SCHEDULED_ID;
+-        }
+-
+-        @Override
+-        public boolean queue() {
+-            if (this.queue.hasShutdown) {
+-                throw new IllegalStateException("Queue has shutdown");
+-            }
+-
+-            synchronized (this.queue.queues) {
+-                if (this.queue.hasShutdown) {
+-                    throw new IllegalStateException("Queue has shutdown");
+-                }
+-
+-                final Priority priority = this.priority;
+-                if (priority == Priority.COMPLETING) {
+-                    return false;
+-                }
+-
+-                if (this.id != NOT_SCHEDULED_ID) {
+-                    return false;
+-                }
+-
+-                this.queue.getAndAddTotalScheduledTasksVolatile(1L);
+-                this.id = this.queue.taskIdGenerator++;
+-                this.queue.queues[priority.priority].add(this);
+-
+-                this.queue.priorityChange(this, null, priority);
+-
+-                return true;
+-            }
+-        }
+-
+-        protected boolean trySetCompleting(final int minPriority) {
+-            final Priority oldPriority = this.priority;
+-            if (oldPriority != Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) {
+-                this.priority = Priority.COMPLETING;
+-                if (this.id != NOT_SCHEDULED_ID) {
+-                    this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
+-                }
+-                return true;
+-            }
+-
+-            return false;
+-        }
+-
+-        @Override
+-        public Priority getPriority() {
+-            return this.priority;
+-        }
+-
+-        @Override
+-        public boolean setPriority(final Priority priority) {
+-            if (!Priority.isValidPriority(priority)) {
+-                throw new IllegalArgumentException("Invalid priority " + priority);
+-            }
+-            synchronized (this.queue.queues) {
+-                final Priority curr = this.priority;
+-
+-                if (curr == Priority.COMPLETING) {
+-                    return false;
+-                }
+-
+-                if (curr == priority) {
+-                    return true;
+-                }
+-
+-                this.priority = priority;
+-                if (this.id != NOT_SCHEDULED_ID) {
+-                    this.queue.queues[priority.priority].add(this);
+-
+-                    // call priority change callback
+-                    this.queue.priorityChange(this, curr, priority);
+-                }
+-            }
+-
+-            return true;
+-        }
+-
+-        @Override
+-        public boolean raisePriority(final Priority priority) {
+-            if (!Priority.isValidPriority(priority)) {
+-                throw new IllegalArgumentException("Invalid priority " + priority);
+-            }
+-
+-            synchronized (this.queue.queues) {
+-                final Priority curr = this.priority;
+-
+-                if (curr == Priority.COMPLETING) {
+-                    return false;
+-                }
+-
+-                if (curr.isHigherOrEqualPriority(priority)) {
+-                    return true;
+-                }
+-
+-                this.priority = priority;
+-                if (this.id != NOT_SCHEDULED_ID) {
+-                    this.queue.queues[priority.priority].add(this);
+-
+-                    // call priority change callback
+-                    this.queue.priorityChange(this, curr, priority);
+-                }
+-            }
+-
+-            return true;
+-        }
+-
+-        @Override
+-        public boolean lowerPriority(final Priority priority) {
+-            if (!Priority.isValidPriority(priority)) {
+-                throw new IllegalArgumentException("Invalid priority " + priority);
+-            }
+-
+-            synchronized (this.queue.queues) {
+-                final Priority curr = this.priority;
+-
+-                if (curr == Priority.COMPLETING) {
+-                    return false;
+-                }
+-
+-                if (curr.isLowerOrEqualPriority(priority)) {
+-                    return true;
+-                }
+-
+-                this.priority = priority;
+-                if (this.id != NOT_SCHEDULED_ID) {
+-                    this.queue.queues[priority.priority].add(this);
+-
+-                    // call priority change callback
+-                    this.queue.priorityChange(this, curr, priority);
+-                }
+-            }
+-
+-            return true;
+-        }
+-
+-        @Override
+-        public boolean cancel() {
+-            final long id;
+-            synchronized (this.queue.queues) {
+-                final Priority oldPriority = this.priority;
+-                if (oldPriority == Priority.COMPLETING) {
+-                    return false;
+-                }
+-
+-                this.priority = Priority.COMPLETING;
+-                // call priority change callback
+-                if ((id = this.id) != NOT_SCHEDULED_ID) {
+-                    this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
+-                }
+-            }
+-            this.runnable = null;
+-            if (id != NOT_SCHEDULED_ID) {
+-                this.queue.getAndAddTotalCompletedTasksVolatile(1L);
+-            }
+-            return true;
+-        }
+-
+-        protected void executeInternal() {
+-            try {
+-                final Runnable execute = this.runnable;
+-                this.runnable = null;
+-                execute.run();
+-            } finally {
+-                if (this.id != NOT_SCHEDULED_ID) {
+-                    this.queue.getAndAddTotalCompletedTasksVolatile(1L);
+-                }
+-            }
+-        }
+-
+-        @Override
+-        public boolean execute() {
+-            synchronized (this.queue.queues) {
+-                final Priority oldPriority = this.priority;
+-                if (oldPriority == Priority.COMPLETING) {
+-                    return false;
+-                }
+-
+-                this.priority = Priority.COMPLETING;
+-                // call priority change callback
+-                if (this.id != NOT_SCHEDULED_ID) {
+-                    this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
+-                }
+-            }
+-
+-            this.executeInternal();
+-            return true;
+-        }
+-    }
+-}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedQueueExecutorThread.java
+similarity index 60%
+rename from src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java
+rename to src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedQueueExecutorThread.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedQueueExecutorThread.java
+@@ -0,0 +0,0 @@
+-package ca.spottedleaf.concurrentutil.executor.standard;
++package ca.spottedleaf.concurrentutil.executor.thread;
+ 
++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
+ import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Priority;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+ import java.lang.invoke.VarHandle;
+@@ -0,0 +0,0 @@ import java.util.concurrent.locks.LockSupport;
+  * <p>
+  *     Note: When using this thread, queue additions to the underlying {@link #queue} are not sufficient to get this thread
+  *     to execute the task. The function {@link #notifyTasks()} must be used after scheduling a task. For expected behaviour
+- *     of task scheduling (thread wakes up after tasks are scheduled), use the methods provided on {@link PrioritisedExecutor}
+- *     methods.
++ *     of task scheduling, use the methods provided on this class to schedule tasks.
+  * </p>
+  */
+ public class PrioritisedQueueExecutorThread extends Thread implements PrioritisedExecutor {
+@@ -0,0 +0,0 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise
+ 
+     protected final long spinWaitTime;
+ 
+-    static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms
++    protected static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms
+ 
+     public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue) {
+         this(queue, DEFAULT_SPINWAIT_TIME); // 0.1ms
+@@ -0,0 +0,0 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise
+     }
+ 
+     @Override
+-    public void run() {
++    public final void run() {
++        try {
++            this.begin();
++            this.doRun();
++        } finally {
++            this.die();
++        }
++    }
++
++    public final void doRun() {
+         final long spinWaitTime = this.spinWaitTime;
+ 
+         main_loop:
+@@ -0,0 +0,0 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise
+             this.setThreadParkedVolatile(true);
+ 
+             // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true
+-            // (i.e it will not notify us)
++            // (i.e. it will not notify us)
+             if (this.pollTasks()) {
+                 this.setThreadParkedVolatile(false);
+                 continue;
+@@ -0,0 +0,0 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise
+         }
+     }
+ 
++    protected void begin() {}
++
++    protected void die() {}
++
+     /**
+      * Attempts to poll as many tasks as possible, returning when finished.
+      * @return Whether any tasks were executed.
+@@ -0,0 +0,0 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise
+                     break;
+                 }
+                 ret = true;
+-            } catch (final ThreadDeath death) {
+-                throw death; // goodbye world...
+             } catch (final Throwable throwable) {
+                 LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "'", throwable);
+             }
+@@ -0,0 +0,0 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise
+     }
+ 
+     @Override
+-    public PrioritisedTask createTask(final Runnable task, final Priority priority) {
+-        final PrioritisedTask queueTask = this.queue.createTask(task, priority);
+-
+-        // need to override queue() to notify us of tasks
+-        return new PrioritisedTask() {
+-            @Override
+-            public Priority getPriority() {
+-                return queueTask.getPriority();
+-            }
+-
+-            @Override
+-            public boolean setPriority(final Priority priority) {
+-                return queueTask.setPriority(priority);
+-            }
++    public long getTotalTasksExecuted() {
++        return this.queue.getTotalTasksExecuted();
++    }
+ 
+-            @Override
+-            public boolean raisePriority(final Priority priority) {
+-                return queueTask.raisePriority(priority);
+-            }
++    @Override
++    public long getTotalTasksScheduled() {
++        return this.queue.getTotalTasksScheduled();
++    }
+ 
+-            @Override
+-            public boolean lowerPriority(final Priority priority) {
+-                return queueTask.lowerPriority(priority);
+-            }
++    @Override
++    public long generateNextSubOrder() {
++        return this.queue.generateNextSubOrder();
++    }
+ 
+-            @Override
+-            public boolean queue() {
+-                final boolean ret = queueTask.queue();
+-                if (ret) {
+-                    PrioritisedQueueExecutorThread.this.notifyTasks();
+-                }
+-                return ret;
+-            }
++    @Override
++    public boolean shutdown() {
++        throw new UnsupportedOperationException();
++    }
+ 
+-            @Override
+-            public boolean cancel() {
+-                return queueTask.cancel();
+-            }
++    @Override
++    public boolean isShutdown() {
++        return false;
++    }
+ 
+-            @Override
+-            public boolean execute() {
+-                return queueTask.execute();
+-            }
+-        };
++    /**
++     * {@inheritDoc}
++     * @throws IllegalStateException Always
++     */
++    @Override
++    public boolean executeTask() throws IllegalStateException {
++        throw new IllegalStateException();
+     }
+ 
+     @Override
+-    public PrioritisedTask queueRunnable(final Runnable task, final Priority priority) {
+-        final PrioritisedTask ret = this.queue.queueRunnable(task, priority);
++    public PrioritisedTask queueTask(final Runnable task) {
++        final PrioritisedTask ret = this.createTask(task);
+ 
+-        this.notifyTasks();
++        ret.queue();
+ 
+         return ret;
+     }
+ 
+     @Override
+-    public boolean haveAllTasksExecuted() {
+-        return this.queue.haveAllTasksExecuted();
++    public PrioritisedTask queueTask(final Runnable task, final Priority priority) {
++        final PrioritisedTask ret = this.createTask(task, priority);
++
++        ret.queue();
++
++        return ret;
+     }
+ 
+     @Override
+-    public long getTotalTasksExecuted() {
+-        return this.queue.getTotalTasksExecuted();
++    public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder) {
++        final PrioritisedTask ret = this.createTask(task, priority, subOrder);
++
++        ret.queue();
++
++        return ret;
+     }
+ 
++
+     @Override
+-    public long getTotalTasksScheduled() {
+-        return this.queue.getTotalTasksScheduled();
++    public PrioritisedTask createTask(Runnable task) {
++        final PrioritisedTask queueTask = this.queue.createTask(task);
++
++        return new WrappedTask(queueTask);
+     }
+ 
+-    /**
+-     * {@inheritDoc}
+-     * @throws IllegalStateException If the current thread is {@code this} thread, or the underlying queue throws this exception.
+-     */
+     @Override
+-    public void waitUntilAllExecuted() throws IllegalStateException {
+-        if (Thread.currentThread() == this) {
+-            throw new IllegalStateException("Cannot block on our own queue");
+-        }
+-        this.queue.waitUntilAllExecuted();
++    public PrioritisedTask createTask(final Runnable task, final Priority priority) {
++        final PrioritisedTask queueTask = this.queue.createTask(task, priority);
++
++        return new WrappedTask(queueTask);
+     }
+ 
+-    /**
+-     * {@inheritDoc}
+-     * @throws IllegalStateException Always
+-     */
+     @Override
+-    public boolean executeTask() throws IllegalStateException {
+-        throw new IllegalStateException();
++    public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder) {
++        final PrioritisedTask queueTask = this.queue.createTask(task, priority, subOrder);
++
++        return new WrappedTask(queueTask);
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise
+      * <p>
+      *     This function is MT-Safe.
+      * </p>
+-     * @param wait If this call is to wait until the queue is empty and there are no tasks executing in the queue.
++     * @param wait If this call is to wait until this thread shuts down.
+      * @param killQueue Whether to shutdown this thread's queue
+      * @return whether this thread shut down the queue
+      * @see #halt(boolean)
+@@ -0,0 +0,0 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise
+         LockSupport.unpark(this);
+ 
+         if (wait) {
+-            this.waitUntilAllExecuted();
++            boolean interrupted = false;
++            for (;;) {
++                if (this.isAlive()) {
++                    if (interrupted) {
++                        Thread.currentThread().interrupt();
++                    }
++                    break;
++                }
++                try {
++                    this.join();
++                } catch (final InterruptedException ex) {
++                    interrupted = true;
++                }
++            }
+         }
+ 
+         return ret;
+@@ -0,0 +0,0 @@ public class PrioritisedQueueExecutorThread extends Thread implements Prioritise
+     protected final void setThreadParkedVolatile(final boolean value) {
+         THREAD_PARKED_HANDLE.setVolatile(this, value);
+     }
++
++    /**
++     * Required so that queue() can notify (unpark) this thread
++     */
++    private final class WrappedTask implements PrioritisedTask {
++        private final PrioritisedTask queueTask;
++
++        public WrappedTask(final PrioritisedTask queueTask) {
++            this.queueTask = queueTask;
++        }
++
++        @Override
++        public PrioritisedExecutor getExecutor() {
++            return PrioritisedQueueExecutorThread.this;
++        }
++
++        @Override
++        public boolean queue() {
++            final boolean ret = this.queueTask.queue();
++            if (ret) {
++                PrioritisedQueueExecutorThread.this.notifyTasks();
++            }
++            return ret;
++        }
++
++        @Override
++        public boolean isQueued() {
++            return this.queueTask.isQueued();
++        }
++
++        @Override
++        public boolean cancel() {
++            return this.queueTask.cancel();
++        }
++
++        @Override
++        public boolean execute() {
++            return this.queueTask.execute();
++        }
++
++        @Override
++        public Priority getPriority() {
++            return this.queueTask.getPriority();
++        }
++
++        @Override
++        public boolean setPriority(final Priority priority) {
++            return this.queueTask.setPriority(priority);
++        }
++
++        @Override
++        public boolean raisePriority(final Priority priority) {
++            return this.queueTask.raisePriority(priority);
++        }
++
++        @Override
++        public boolean lowerPriority(final Priority priority) {
++            return this.queueTask.lowerPriority(priority);
++        }
++
++        @Override
++        public long getSubOrder() {
++            return this.queueTask.getSubOrder();
++        }
++
++        @Override
++        public boolean setSubOrder(final long subOrder) {
++            return this.queueTask.setSubOrder(subOrder);
++        }
++
++        @Override
++        public boolean raiseSubOrder(final long subOrder) {
++            return this.queueTask.raiseSubOrder(subOrder);
++        }
++
++        @Override
++        public boolean lowerSubOrder(final long subOrder) {
++            return this.queueTask.lowerSubOrder(subOrder);
++        }
++
++        @Override
++        public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
++            return this.queueTask.setPriorityAndSubOrder(priority, subOrder);
++        }
++    }
+ }
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedThreadPool.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/thread/PrioritisedThreadPool.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.executor.thread;
++
++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.executor.queue.PrioritisedTaskQueue;
++import ca.spottedleaf.concurrentutil.util.Priority;
++import ca.spottedleaf.concurrentutil.util.TimeUtil;
++import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
++import java.lang.reflect.Array;
++import java.util.Arrays;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.concurrent.atomic.AtomicLong;
++import java.util.function.Consumer;
++
++public final class PrioritisedThreadPool {
++
++    private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedThreadPool.class);
++
++    private final Consumer<Thread> threadModifier;
++    private final COWArrayList<ExecutorGroup> executors = new COWArrayList<>(ExecutorGroup.class);
++    private final COWArrayList<PrioritisedThread> threads = new COWArrayList<>(PrioritisedThread.class);
++    private final COWArrayList<PrioritisedThread> aliveThreads = new COWArrayList<>(PrioritisedThread.class);
++
++    private static final Priority HIGH_PRIORITY_NOTIFY_THRESHOLD = Priority.HIGH;
++    private static final Priority QUEUE_SHUTDOWN_PRIORITY = Priority.HIGH;
++
++    private boolean shutdown;
++
++    public PrioritisedThreadPool(final Consumer<Thread> threadModifier) {
++        this.threadModifier = threadModifier;
++
++        if (threadModifier == null) {
++            throw new NullPointerException("Thread factory may not be null");
++        }
++    }
++
++    public Thread[] getAliveThreads() {
++        final PrioritisedThread[] threads = this.aliveThreads.getArray();
++
++        return Arrays.copyOf(threads, threads.length, Thread[].class);
++    }
++
++    public Thread[] getCoreThreads() {
++        final PrioritisedThread[] threads = this.threads.getArray();
++
++        return Arrays.copyOf(threads, threads.length, Thread[].class);
++    }
++
++    /**
++     * Prevents creation of new queues, shutdowns all non-shutdown queues if specified
++     */
++    public void halt(final boolean shutdownQueues) {
++        synchronized (this) {
++            this.shutdown = true;
++        }
++
++        if (shutdownQueues) {
++            for (final ExecutorGroup group : this.executors.getArray()) {
++                for (final ExecutorGroup.ThreadPoolExecutor executor : group.executors.getArray()) {
++                    executor.shutdown();
++                }
++            }
++        }
++
++        for (final PrioritisedThread thread : this.threads.getArray()) {
++            thread.halt(false);
++        }
++    }
++
++    /**
++     * Waits until all threads in this pool have shutdown, or until the specified time has passed.
++     * @param msToWait Maximum time to wait.
++     * @return {@code false} if the maximum time passed, {@code true} otherwise.
++     */
++    public boolean join(final long msToWait) {
++        try {
++            return this.join(msToWait, false);
++        } catch (final InterruptedException ex) {
++            throw new IllegalStateException(ex);
++        }
++    }
++
++    /**
++     * Waits until all threads in this pool have shutdown, or until the specified time has passed.
++     * @param msToWait Maximum time to wait.
++     * @return {@code false} if the maximum time passed, {@code true} otherwise.
++     * @throws InterruptedException If this thread is interrupted.
++     */
++    public boolean joinInterruptable(final long msToWait) throws InterruptedException {
++        return this.join(msToWait, true);
++    }
++
++    protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException {
++        final long nsToWait = msToWait * (1000 * 1000);
++        final long start = System.nanoTime();
++        final long deadline = start + nsToWait;
++        boolean interrupted = false;
++        try {
++            for (final PrioritisedThread thread : this.aliveThreads.getArray()) {
++                for (;;) {
++                    if (!thread.isAlive()) {
++                        break;
++                    }
++                    final long current = System.nanoTime();
++                    if (current >= deadline && msToWait > 0L) {
++                        return false;
++                    }
++
++                    try {
++                        thread.join(msToWait <= 0L ? 0L : Math.max(1L, (deadline - current) / (1000 * 1000)));
++                    } catch (final InterruptedException ex) {
++                        if (interruptable) {
++                            throw ex;
++                        }
++                        interrupted = true;
++                    }
++                }
++            }
++
++            return true;
++        } finally {
++            if (interrupted) {
++                Thread.currentThread().interrupt();
++            }
++        }
++    }
++
++    /**
++     * Shuts down this thread pool, optionally waiting for all tasks to be executed.
++     * This function will invoke {@link PrioritisedExecutor#shutdown()} on all created executors on this
++     * thread pool.
++     * @param wait Whether to wait for tasks to be executed
++     */
++    public void shutdown(final boolean wait) {
++        synchronized (this) {
++            this.shutdown = true;
++        }
++
++        for (final ExecutorGroup group : this.executors.getArray()) {
++            for (final ExecutorGroup.ThreadPoolExecutor executor : group.executors.getArray()) {
++                executor.shutdown();
++            }
++        }
++
++
++        for (final PrioritisedThread thread : this.threads.getArray()) {
++            // none of these can be true or else NPE
++            thread.close(false, false);
++        }
++
++        if (wait) {
++            this.join(0L);
++        }
++    }
++
++    private void die(final PrioritisedThread thread) {
++        this.aliveThreads.remove(thread);
++    }
++
++    public void adjustThreadCount(final int threads) {
++        synchronized (this) {
++            if (this.shutdown) {
++                return;
++            }
++
++            final PrioritisedThread[] currentThreads = this.threads.getArray();
++            if (threads == currentThreads.length) {
++                // no adjustment needed
++                return;
++            }
++
++            if (threads < currentThreads.length) {
++                // we need to trim threads
++                for (int i = 0, difference = currentThreads.length - threads; i < difference; ++i) {
++                    final PrioritisedThread remove = currentThreads[currentThreads.length - i - 1];
++
++                    remove.halt(false);
++                    this.threads.remove(remove);
++                }
++            } else {
++                // we need to add threads
++                for (int i = 0, difference = threads - currentThreads.length; i < difference; ++i) {
++                    final PrioritisedThread thread = new PrioritisedThread();
++
++                    this.threadModifier.accept(thread);
++                    this.aliveThreads.add(thread);
++                    this.threads.add(thread);
++
++                    thread.start();
++                }
++            }
++        }
++    }
++
++    private static int compareInsideGroup(final ExecutorGroup.ThreadPoolExecutor src, final Priority srcPriority,
++                                          final ExecutorGroup.ThreadPoolExecutor dst, final Priority dstPriority) {
++        final int priorityCompare = srcPriority.ordinal() - dstPriority.ordinal();
++        if (priorityCompare != 0) {
++            return priorityCompare;
++        }
++
++        final int parallelismCompare = src.currentParallelism - dst.currentParallelism;
++        if (parallelismCompare != 0) {
++            return parallelismCompare;
++        }
++
++        return TimeUtil.compareTimes(src.lastRetrieved, dst.lastRetrieved);
++    }
++
++    private static int compareOutsideGroup(final ExecutorGroup.ThreadPoolExecutor src, final Priority srcPriority,
++                                           final ExecutorGroup.ThreadPoolExecutor dst, final Priority dstPriority) {
++        if (src.getGroup().division == dst.getGroup().division) {
++            // can only compare priorities inside the same division
++            final int priorityCompare = srcPriority.ordinal() - dstPriority.ordinal();
++            if (priorityCompare != 0) {
++                return priorityCompare;
++            }
++        }
++
++        final int parallelismCompare = src.getGroup().currentParallelism - dst.getGroup().currentParallelism;
++        if (parallelismCompare != 0) {
++            return parallelismCompare;
++        }
++
++        return TimeUtil.compareTimes(src.lastRetrieved, dst.lastRetrieved);
++    }
++
++    private ExecutorGroup.ThreadPoolExecutor obtainQueue() {
++        final long time = System.nanoTime();
++        synchronized (this) {
++            ExecutorGroup.ThreadPoolExecutor ret = null;
++            Priority retPriority = null;
++
++            for (final ExecutorGroup executorGroup : this.executors.getArray()) {
++                ExecutorGroup.ThreadPoolExecutor highest = null;
++                Priority highestPriority = null;
++                for (final ExecutorGroup.ThreadPoolExecutor executor : executorGroup.executors.getArray()) {
++                    final int maxParallelism = executor.maxParallelism;
++                    if (maxParallelism > 0 && executor.currentParallelism >= maxParallelism) {
++                        continue;
++                    }
++
++                    final Priority priority = executor.getTargetPriority();
++
++                    if (priority == null) {
++                        continue;
++                    }
++
++                    if (highestPriority == null || compareInsideGroup(highest, highestPriority, executor, priority) > 0) {
++                        highest = executor;
++                        highestPriority = priority;
++                    }
++                }
++
++                if (highest == null) {
++                    continue;
++                }
++
++                if (ret == null || compareOutsideGroup(ret, retPriority, highest, highestPriority) > 0) {
++                    ret = highest;
++                    retPriority = highestPriority;
++                }
++            }
++
++            if (ret != null) {
++                ret.lastRetrieved = time;
++                ++ret.currentParallelism;
++                ++ret.getGroup().currentParallelism;
++                return ret;
++            }
++
++            return ret;
++        }
++    }
++
++    private void returnQueue(final ExecutorGroup.ThreadPoolExecutor executor) {
++        synchronized (this) {
++            --executor.currentParallelism;
++            --executor.getGroup().currentParallelism;
++        }
++
++        if (executor.isShutdown() && executor.queue.hasNoScheduledTasks()) {
++            executor.getGroup().executors.remove(executor);
++        }
++    }
++
++    private void notifyAllThreads() {
++        for (final PrioritisedThread thread : this.threads.getArray()) {
++            thread.notifyTasks();
++        }
++    }
++
++    public ExecutorGroup createExecutorGroup(final int division, final int flags) {
++        synchronized (this) {
++            if (this.shutdown) {
++                throw new IllegalStateException("Queue is shutdown: " + this.toString());
++            }
++
++            final ExecutorGroup ret = new ExecutorGroup(division, flags);
++
++            this.executors.add(ret);
++
++            return ret;
++        }
++    }
++
++    private final class PrioritisedThread extends PrioritisedQueueExecutorThread {
++
++        private final AtomicBoolean alertedHighPriority = new AtomicBoolean();
++
++        public PrioritisedThread() {
++            super(null);
++        }
++
++        public boolean alertHighPriorityExecutor() {
++            if (!this.notifyTasks()) {
++                if (!this.alertedHighPriority.get()) {
++                    this.alertedHighPriority.set(true);
++                }
++                return false;
++            }
++
++            return true;
++        }
++
++        private boolean isAlertedHighPriority() {
++            return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false);
++        }
++
++        @Override
++        protected void die() {
++            PrioritisedThreadPool.this.die(this);
++        }
++
++        @Override
++        protected boolean pollTasks() {
++            boolean ret = false;
++
++            for (;;) {
++                if (this.halted) {
++                    break;
++                }
++
++                final ExecutorGroup.ThreadPoolExecutor executor = PrioritisedThreadPool.this.obtainQueue();
++                if (executor == null) {
++                    break;
++                }
++                final long deadline = System.nanoTime() + executor.queueMaxHoldTime;
++                do {
++                    try {
++                        if (this.halted || executor.halt) {
++                            break;
++                        }
++                        if (!executor.executeTask()) {
++                            // no more tasks, try next queue
++                            break;
++                        }
++                        ret = true;
++                    } catch (final Throwable throwable) {
++                        LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + executor.toString() + "'", throwable);
++                    }
++                } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline);
++
++                PrioritisedThreadPool.this.returnQueue(executor);
++            }
++
++
++            return ret;
++        }
++    }
++
++    public final class ExecutorGroup {
++
++        private final AtomicLong subOrderGenerator = new AtomicLong();
++        private final COWArrayList<ThreadPoolExecutor> executors = new COWArrayList<>(ThreadPoolExecutor.class);
++
++        private final int division;
++        private int currentParallelism;
++
++        private ExecutorGroup(final int division, final int flags) {
++            this.division = division;
++        }
++
++        public ThreadPoolExecutor[] getAllExecutors() {
++            return this.executors.getArray().clone();
++        }
++
++        private PrioritisedThreadPool getThreadPool() {
++            return PrioritisedThreadPool.this;
++        }
++
++        public ThreadPoolExecutor createExecutor(final int maxParallelism, final long queueMaxHoldTime, final int flags) {
++            synchronized (PrioritisedThreadPool.this) {
++                if (PrioritisedThreadPool.this.shutdown) {
++                    throw new IllegalStateException("Queue is shutdown: " + PrioritisedThreadPool.this.toString());
++                }
++
++                final ThreadPoolExecutor ret = new ThreadPoolExecutor(maxParallelism, queueMaxHoldTime, flags);
++
++                this.executors.add(ret);
++
++                return ret;
++            }
++        }
++
++        public final class ThreadPoolExecutor implements PrioritisedExecutor {
++
++            private final PrioritisedTaskQueue queue = new PrioritisedTaskQueue();
++
++            private volatile int maxParallelism;
++            private final long queueMaxHoldTime;
++            private volatile int currentParallelism;
++            private volatile boolean halt;
++            private long lastRetrieved = System.nanoTime();
++
++            private ThreadPoolExecutor(final int maxParallelism, final long queueMaxHoldTime, final int flags) {
++                this.maxParallelism = maxParallelism;
++                this.queueMaxHoldTime = queueMaxHoldTime;
++            }
++
++            private ExecutorGroup getGroup() {
++                return ExecutorGroup.this;
++            }
++
++            private boolean canNotify() {
++                if (this.halt) {
++                    return false;
++                }
++
++                final int max = this.maxParallelism;
++                return max < 0 || this.currentParallelism < max;
++            }
++
++            private void notifyHighPriority() {
++                if (!this.canNotify()) {
++                    return;
++                }
++                for (final PrioritisedThread thread : this.getGroup().getThreadPool().threads.getArray()) {
++                    if (thread.alertHighPriorityExecutor()) {
++                        return;
++                    }
++                }
++            }
++
++            private void notifyScheduled() {
++                if (!this.canNotify()) {
++                    return;
++                }
++                for (final PrioritisedThread thread : this.getGroup().getThreadPool().threads.getArray()) {
++                    if (thread.notifyTasks()) {
++                        return;
++                    }
++                }
++            }
++
++            /**
++             * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed
++             */
++            public void halt() {
++                this.halt = true;
++
++                ExecutorGroup.this.executors.remove(this);
++            }
++
++            /**
++             * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether
++             * this queue is not halted and not shutdown.
++             */
++            public boolean isActive() {
++                if (this.halt) {
++                    return this.currentParallelism > 0;
++                } else {
++                    if (!this.isShutdown()) {
++                        return true;
++                    }
++
++                    return !this.queue.hasNoScheduledTasks();
++                }
++            }
++
++            @Override
++            public boolean shutdown() {
++                if (!this.queue.shutdown()) {
++                    return false;
++                }
++
++                if (this.queue.hasNoScheduledTasks()) {
++                    ExecutorGroup.this.executors.remove(this);
++                }
++
++                return true;
++            }
++
++            @Override
++            public boolean isShutdown() {
++                return this.queue.isShutdown();
++            }
++
++            public void setMaxParallelism(final int maxParallelism) {
++                this.maxParallelism = maxParallelism;
++                // assume that we could have increased the parallelism
++                if (this.getTargetPriority() != null) {
++                    ExecutorGroup.this.getThreadPool().notifyAllThreads();
++                }
++            }
++
++            Priority getTargetPriority() {
++                final Priority ret = this.queue.getHighestPriority();
++                if (!this.isShutdown()) {
++                    return ret;
++                }
++
++                return ret == null ? QUEUE_SHUTDOWN_PRIORITY : Priority.max(ret, QUEUE_SHUTDOWN_PRIORITY);
++            }
++
++            @Override
++            public long getTotalTasksScheduled() {
++                return this.queue.getTotalTasksScheduled();
++            }
++
++            @Override
++            public long getTotalTasksExecuted() {
++                return this.queue.getTotalTasksExecuted();
++            }
++
++            @Override
++            public long generateNextSubOrder() {
++                return ExecutorGroup.this.subOrderGenerator.getAndIncrement();
++            }
++
++            @Override
++            public boolean executeTask() {
++                return this.queue.executeTask();
++            }
++
++            @Override
++            public PrioritisedTask queueTask(final Runnable task) {
++                final PrioritisedTask ret = this.createTask(task);
++
++                ret.queue();
++
++                return ret;
++            }
++
++            @Override
++            public PrioritisedTask queueTask(final Runnable task, final Priority priority) {
++                final PrioritisedTask ret = this.createTask(task, priority);
++
++                ret.queue();
++
++                return ret;
++            }
++
++            @Override
++            public PrioritisedTask queueTask(final Runnable task, final Priority priority, final long subOrder) {
++                final PrioritisedTask ret = this.createTask(task, priority, subOrder);
++
++                ret.queue();
++
++                return ret;
++            }
++
++            @Override
++            public PrioritisedTask createTask(final Runnable task) {
++                return this.createTask(task, Priority.NORMAL);
++            }
++
++            @Override
++            public PrioritisedTask createTask(final Runnable task, final Priority priority) {
++                return this.createTask(task, priority, this.generateNextSubOrder());
++            }
++
++            @Override
++            public PrioritisedTask createTask(final Runnable task, final Priority priority, final long subOrder) {
++                return new WrappedTask(this.queue.createTask(task, priority, subOrder));
++            }
++
++            private final class WrappedTask implements PrioritisedTask {
++
++                private final PrioritisedTask wrapped;
++
++                private WrappedTask(final PrioritisedTask wrapped) {
++                    this.wrapped = wrapped;
++                }
++
++                @Override
++                public PrioritisedExecutor getExecutor() {
++                    return ThreadPoolExecutor.this;
++                }
++
++                @Override
++                public boolean queue() {
++                    if (this.wrapped.queue()) {
++                        final Priority priority = this.getPriority();
++                        if (priority != Priority.COMPLETING) {
++                            if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) {
++                                ThreadPoolExecutor.this.notifyHighPriority();
++                            } else {
++                                ThreadPoolExecutor.this.notifyScheduled();
++                            }
++                        }
++                        return true;
++                    }
++
++                    return false;
++                }
++
++                @Override
++                public boolean isQueued() {
++                    return this.wrapped.isQueued();
++                }
++
++                @Override
++                public boolean cancel() {
++                    return this.wrapped.cancel();
++                }
++
++                @Override
++                public boolean execute() {
++                    return this.wrapped.execute();
++                }
++
++                @Override
++                public Priority getPriority() {
++                    return this.wrapped.getPriority();
++                }
++
++                @Override
++                public boolean setPriority(final Priority priority) {
++                    if (this.wrapped.setPriority(priority)) {
++                        if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) {
++                            ThreadPoolExecutor.this.notifyHighPriority();
++                        }
++                        return true;
++                    }
++
++                    return false;
++                }
++
++                @Override
++                public boolean raisePriority(final Priority priority) {
++                    if (this.wrapped.raisePriority(priority)) {
++                        if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) {
++                            ThreadPoolExecutor.this.notifyHighPriority();
++                        }
++                        return true;
++                    }
++
++                    return false;
++                }
++
++                @Override
++                public boolean lowerPriority(final Priority priority) {
++                    return this.wrapped.lowerPriority(priority);
++                }
++
++                @Override
++                public long getSubOrder() {
++                    return this.wrapped.getSubOrder();
++                }
++
++                @Override
++                public boolean setSubOrder(final long subOrder) {
++                    return this.wrapped.setSubOrder(subOrder);
++                }
++
++                @Override
++                public boolean raiseSubOrder(final long subOrder) {
++                    return this.wrapped.raiseSubOrder(subOrder);
++                }
++
++                @Override
++                public boolean lowerSubOrder(final long subOrder) {
++                    return this.wrapped.lowerSubOrder(subOrder);
++                }
++
++                @Override
++                public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
++                    if (this.wrapped.setPriorityAndSubOrder(priority, subOrder)) {
++                        if (priority.isHigherOrEqualPriority(HIGH_PRIORITY_NOTIFY_THRESHOLD)) {
++                            ThreadPoolExecutor.this.notifyHighPriority();
++                        }
++                        return true;
++                    }
++
++                    return false;
++                }
++            }
++        }
++    }
++
++    private static final class COWArrayList<E> {
++
++        private volatile E[] array;
++
++        public COWArrayList(final Class<E> clazz) {
++            this.array = (E[])Array.newInstance(clazz, 0);
++        }
++
++        public E[] getArray() {
++            return this.array;
++        }
++
++        public void add(final E element) {
++            synchronized (this) {
++                final E[] array = this.array;
++
++                final E[] copy = Arrays.copyOf(array, array.length + 1);
++                copy[array.length] = element;
++
++                this.array = copy;
++            }
++        }
++
++        public boolean remove(final E element) {
++            synchronized (this) {
++                final E[] array = this.array;
++                int index = -1;
++                for (int i = 0, len = array.length; i < len; ++i) {
++                    if (array[i] == element) {
++                        index = i;
++                        break;
++                    }
++                }
++
++                if (index == -1) {
++                    return false;
++                }
++
++                final E[] copy = (E[])Array.newInstance(array.getClass().getComponentType(), array.length - 1);
++
++                System.arraycopy(array, 0, copy, 0, index);
++                System.arraycopy(array, index + 1, copy, index, (array.length - 1) - index);
++
++                this.array = copy;
++            }
++
++            return true;
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
+@@ -0,0 +0,0 @@ import java.util.function.Predicate;
+  * @param <V>
+  * @see java.util.concurrent.ConcurrentMap
+  */
+-public class ConcurrentLong2ReferenceChainedHashTable<V> {
++public class ConcurrentLong2ReferenceChainedHashTable<V> implements Iterable<ConcurrentLong2ReferenceChainedHashTable.TableEntry<V>> {
+ 
+     protected static final int DEFAULT_CAPACITY = 16;
+     protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+             }
+ 
+             if (node.resize) {
++                // noinspection unchecked
+                 table = (TableEntry<V>[])node.getValuePlain();
+                 continue;
+             }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+     public int size() {
+         final long ret = this.size.sum();
+ 
+-        if (ret <= 0L) {
++        if (ret < 0L) {
+             return 0;
+         }
+-        if (ret >= (long)Integer.MAX_VALUE) {
++        if (ret > (long)Integer.MAX_VALUE) {
+             return Integer.MAX_VALUE;
+         }
+ 
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+ 
+         // create new table data
+ 
++        // noinspection unchecked
+         final TableEntry<V>[] newTable = new TableEntry[capacity];
+         // noinspection unchecked
+         final TableEntry<V> resizeNode = new TableEntry<>(0L, (V)newTable, true);
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+             throw new IllegalStateException("Resizing to same size");
+         }
+ 
++        // noinspection unchecked
+         final TableEntry<V>[] work = new TableEntry[1 << capDiffShift]; // typically, capDiffShift = 1
+ 
+         for (int i = 0, len = oldTable.length; i < len; ++i) {
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+                 }
+ 
+                 if (node.resize) {
++                    // noinspection unchecked
+                     table = (TableEntry<V>[])node.getValuePlain();
+                     continue table_loop;
+                 }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+         return new EntryIterator<>(this);
+     }
+ 
++    @Override
++    public final Iterator<TableEntry<V>> iterator() {
++        return this.entryIterator();
++    }
++
+     /**
+      * Returns an iterator over the keys in this map. The iterator is only guaranteed to see keys that were
+      * added before the beginning of this call, but it may see keys added during.
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+ 
+     protected static final class EntryIterator<V> extends BaseIteratorImpl<V, TableEntry<V>> {
+ 
+-        protected EntryIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
++        public EntryIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
+             super(map);
+         }
+ 
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+ 
+     protected static final class KeyIterator<V> extends BaseIteratorImpl<V, Long> implements PrimitiveIterator.OfLong {
+ 
+-        protected KeyIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
++        public KeyIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
+             super(map);
+         }
+ 
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+ 
+     protected static final class ValueIterator<V> extends BaseIteratorImpl<V, V> {
+ 
+-        protected ValueIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
++        public ValueIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
+             super(map);
+         }
+ 
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+ 
+         @Override
+         public final void remove() {
+-            final TableEntry<V> lastReturned = this.nextToReturn;
++            final TableEntry<V> lastReturned = this.lastReturned;
+             if (lastReturned == null) {
+                 throw new NoSuchElementException();
+             }
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+             final ResizeChain<V> chain = this.resizeChain;
+ 
+             if (chain == null) {
++                // noinspection unchecked
+                 final TableEntry<V>[] nextTable = (TableEntry<V>[])entry.getValuePlain();
+ 
+                 final ResizeChain<V> oldChain = new ResizeChain<>(table, null, null);
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+             } else {
+                 ResizeChain<V> currChain = chain.next;
+                 if (currChain == null) {
++                    // noinspection unchecked
+                     final TableEntry<V>[] ret = (TableEntry<V>[])entry.getValuePlain();
+                     currChain = new ResizeChain<>(ret, chain, null);
+                     chain.next = currChain;
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+ 
+         protected static final class ResizeChain<V> {
+ 
+-            protected final TableEntry<V>[] table;
+-            protected final ResizeChain<V> prev;
+-            protected ResizeChain<V> next;
++            public final TableEntry<V>[] table;
++            public final ResizeChain<V> prev;
++            public ResizeChain<V> next;
+ 
+-            protected ResizeChain(final TableEntry<V>[] table, final ResizeChain<V> prev, final ResizeChain<V> next) {
++            public ResizeChain(final TableEntry<V>[] table, final ResizeChain<V> prev, final ResizeChain<V> next) {
+                 this.table = table;
+                 this.prev = prev;
+                 this.next = next;
+@@ -0,0 +0,0 @@ public class ConcurrentLong2ReferenceChainedHashTable<V> {
+ 
+     public static final class TableEntry<V> {
+ 
+-        protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
++        private static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
+ 
+-        protected final boolean resize;
++        private final boolean resize;
+ 
+-        protected final long key;
++        private final long key;
+ 
+-        protected volatile V value;
+-        protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
++        private volatile V value;
++        private static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
+ 
+-        protected final V getValuePlain() {
++        private V getValuePlain() {
+             //noinspection unchecked
+             return (V)VALUE_HANDLE.get(this);
+         }
+ 
+-        protected final V getValueAcquire() {
++        private V getValueAcquire() {
+             //noinspection unchecked
+             return (V)VALUE_HANDLE.getAcquire(this);
+         }
+ 
+-        protected final V getValueVolatile() {
++        private V getValueVolatile() {
+             //noinspection unchecked
+             return (V)VALUE_HANDLE.getVolatile(this);
+         }
+ 
+-        protected final void setValuePlain(final V value) {
++        private void setValuePlain(final V value) {
+             VALUE_HANDLE.set(this, (Object)value);
+         }
+ 
+-        protected final void setValueRelease(final V value) {
++        private void setValueRelease(final V value) {
+             VALUE_HANDLE.setRelease(this, (Object)value);
+         }
+ 
+-        protected final void setValueVolatile(final V value) {
++        private void setValueVolatile(final V value) {
+             VALUE_HANDLE.setVolatile(this, (Object)value);
+         }
+ 
+-        protected volatile TableEntry<V> next;
+-        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
++        private volatile TableEntry<V> next;
++        private static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
+ 
+-        protected final TableEntry<V> getNextPlain() {
++        private TableEntry<V> getNextPlain() {
+             //noinspection unchecked
+             return (TableEntry<V>)NEXT_HANDLE.get(this);
+         }
+ 
+-        protected final TableEntry<V> getNextVolatile() {
++        private TableEntry<V> getNextVolatile() {
+             //noinspection unchecked
+             return (TableEntry<V>)NEXT_HANDLE.getVolatile(this);
+         }
+ 
+-        protected final void setNextPlain(final TableEntry<V> next) {
++        private void setNextPlain(final TableEntry<V> next) {
+             NEXT_HANDLE.set(this, next);
+         }
+ 
+-        protected final void setNextRelease(final TableEntry<V> next) {
++        private void setNextRelease(final TableEntry<V> next) {
+             NEXT_HANDLE.setRelease(this, next);
+         }
+ 
+-        protected final void setNextVolatile(final TableEntry<V> next) {
++        private void setNextVolatile(final TableEntry<V> next) {
+             NEXT_HANDLE.setVolatile(this, next);
+         }
+ 
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
++++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
+@@ -0,0 +0,0 @@ import java.util.concurrent.atomic.AtomicLong;
+ import java.util.concurrent.locks.LockSupport;
+ import java.util.function.BooleanSupplier;
+ 
++/**
++ * @deprecated To be replaced
++ */
++@Deprecated
+ public class SchedulerThreadPool {
+ 
+     public static final long DEADLINE_NOT_SET = Long.MIN_VALUE;
+@@ -0,0 +0,0 @@ public class SchedulerThreadPool {
+      * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to
+      * parse intermediate tasks.
+      * </p>
++     * @deprecated To be replaced
+      */
++    @Deprecated
+     public static abstract class SchedulableTick {
+         private static final AtomicLong ID_GENERATOR = new AtomicLong();
+         public final long id = ID_GENERATOR.getAndIncrement();
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
++++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
+@@ -0,0 +0,0 @@ public final class LinkedSortedSet<E> implements Iterable<E> {
+ 
+     public final Comparator<? super E> comparator;
+ 
+-    protected Link<E> head;
+-    protected Link<E> tail;
++    private Link<E> head;
++    private Link<E> tail;
+ 
+     public LinkedSortedSet() {
+         this((Comparator)Comparator.naturalOrder());
+@@ -0,0 +0,0 @@ public final class LinkedSortedSet<E> implements Iterable<E> {
+         private Link<E> prev;
+         private Link<E> next;
+ 
+-        private Link() {}
+-
+         private Link(final E element) {
+             this.element = element;
+         }
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedUnsortedList.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedUnsortedList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedUnsortedList.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.set;
++
++import java.util.Iterator;
++import java.util.NoSuchElementException;
++import java.util.Objects;
++
++public final class LinkedUnsortedList<E> implements Iterable<E> {
++
++    private Link<E> head;
++    private Link<E> tail;
++
++    public LinkedUnsortedList() {}
++
++    public void clear() {
++        this.head = this.tail = null;
++    }
++
++    public boolean isEmpty() {
++        return this.head == null;
++    }
++
++    public E first() {
++        final Link<E> head = this.head;
++        return head == null ? null : head.element;
++    }
++
++    public E last() {
++        final Link<E> tail = this.tail;
++        return tail == null ? null : tail.element;
++    }
++
++    public boolean containsFirst(final E element) {
++        for (Link<E> curr = this.head; curr != null; curr = curr.next) {
++            if (Objects.equals(element, curr.element)) {
++                return true;
++            }
++        }
++        return false;
++    }
++
++    public boolean containsLast(final E element) {
++        for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
++            if (Objects.equals(element, curr.element)) {
++                return true;
++            }
++        }
++        return false;
++    }
++
++    private void removeNode(final Link<E> node) {
++        final Link<E> prev = node.prev;
++        final Link<E> next = node.next;
++
++        // help GC
++        node.element = null;
++        node.prev = null;
++        node.next = null;
++
++        if (prev == null) {
++            this.head = next;
++        } else {
++            prev.next = next;
++        }
++
++        if (next == null) {
++            this.tail = prev;
++        } else {
++            next.prev = prev;
++        }
++    }
++
++    public boolean remove(final Link<E> link) {
++        if (link.element == null) {
++            return false;
++        }
++
++        this.removeNode(link);
++        return true;
++    }
++
++    public boolean removeFirst(final E element) {
++        for (Link<E> curr = this.head; curr != null; curr = curr.next) {
++            if (Objects.equals(element, curr.element)) {
++                this.removeNode(curr);
++                return true;
++            }
++        }
++        return false;
++    }
++
++    public boolean removeLast(final E element) {
++        for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
++            if (Objects.equals(element, curr.element)) {
++                this.removeNode(curr);
++                return true;
++            }
++        }
++        return false;
++    }
++
++    @Override
++    public Iterator<E> iterator() {
++        return new Iterator<>() {
++            private Link<E> next = LinkedUnsortedList.this.head;
++
++            @Override
++            public boolean hasNext() {
++                return this.next != null;
++            }
++
++            @Override
++            public E next() {
++                final Link<E> next = this.next;
++                if (next == null) {
++                    throw new NoSuchElementException();
++                }
++                this.next = next.next;
++                return next.element;
++            }
++        };
++    }
++
++    public E pollFirst() {
++        final Link<E> head = this.head;
++        if (head == null) {
++            return null;
++        }
++
++        final E ret = head.element;
++        final Link<E> next = head.next;
++
++        // unlink head
++        this.head = next;
++        if (next == null) {
++            this.tail = null;
++        } else {
++            next.prev = null;
++        }
++
++        // help GC
++        head.element = null;
++        head.next = null;
++
++        return ret;
++    }
++
++    public E pollLast() {
++        final Link<E> tail = this.tail;
++        if (tail == null) {
++            return null;
++        }
++
++        final E ret = tail.element;
++        final Link<E> prev = tail.prev;
++
++        // unlink tail
++        this.tail = prev;
++        if (prev == null) {
++            this.head = null;
++        } else {
++            prev.next = null;
++        }
++
++        // help GC
++        tail.element = null;
++        tail.prev = null;
++
++        return ret;
++    }
++
++    public Link<E> addLast(final E element) {
++        final Link<E> curr = this.tail;
++        if (curr != null) {
++            return this.tail = new Link<>(element, curr, null);
++        } else {
++            return this.head = this.tail = new Link<>(element);
++        }
++    }
++
++    public Link<E> addFirst(final E element) {
++        final Link<E> curr = this.head;
++        if (curr != null) {
++            return this.head = new Link<>(element, null, curr);
++        } else {
++            return this.head = this.tail = new Link<>(element);
++        }
++    }
++
++    public static final class Link<E> {
++        private E element;
++        private Link<E> prev;
++        private Link<E> next;
++
++        private Link(final E element) {
++            this.element = element;
++        }
++
++        private Link(final E element, final Link<E> prev, final Link<E> next) {
++            this.element = element;
++            this.prev = prev;
++            this.next = next;
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package ca.spottedleaf.concurrentutil.util;
+-
+-import java.lang.invoke.VarHandle;
+-
+-public final class ArrayUtil {
+-
+-    public static final VarHandle BOOLEAN_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(boolean[].class);
+-
+-    public static final VarHandle BYTE_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(byte[].class);
+-
+-    public static final VarHandle SHORT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(short[].class);
+-
+-    public static final VarHandle INT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(int[].class);
+-
+-    public static final VarHandle LONG_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(long[].class);
+-
+-    public static final VarHandle OBJECT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(Object[].class);
+-
+-    private ArrayUtil() {
+-        throw new RuntimeException();
+-    }
+-
+-    /* byte array */
+-
+-    public static byte getPlain(final byte[] array, final int index) {
+-        return (byte)BYTE_ARRAY_HANDLE.get(array, index);
+-    }
+-
+-    public static byte getOpaque(final byte[] array, final int index) {
+-        return (byte)BYTE_ARRAY_HANDLE.getOpaque(array, index);
+-    }
+-
+-    public static byte getAcquire(final byte[] array, final int index) {
+-        return (byte)BYTE_ARRAY_HANDLE.getAcquire(array, index);
+-    }
+-
+-    public static byte getVolatile(final byte[] array, final int index) {
+-        return (byte)BYTE_ARRAY_HANDLE.getVolatile(array, index);
+-    }
+-
+-    public static void setPlain(final byte[] array, final int index, final byte value) {
+-        BYTE_ARRAY_HANDLE.set(array, index, value);
+-    }
+-
+-    public static void setOpaque(final byte[] array, final int index, final byte value) {
+-        BYTE_ARRAY_HANDLE.setOpaque(array, index, value);
+-    }
+-
+-    public static void setRelease(final byte[] array, final int index, final byte value) {
+-        BYTE_ARRAY_HANDLE.setRelease(array, index, value);
+-    }
+-
+-    public static void setVolatile(final byte[] array, final int index, final byte value) {
+-        BYTE_ARRAY_HANDLE.setVolatile(array, index, value);
+-    }
+-
+-    public static void setVolatileContended(final byte[] array, final int index, final byte param) {
+-        int failures = 0;
+-
+-        for (byte curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return;
+-            }
+-        }
+-    }
+-
+-    public static byte compareAndExchangeVolatile(final byte[] array, final int index, final byte expect, final byte update) {
+-        return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static byte getAndAddVolatile(final byte[] array, final int index, final byte param) {
+-        return (byte)BYTE_ARRAY_HANDLE.getAndAdd(array, index, param);
+-    }
+-
+-    public static byte getAndAndVolatile(final byte[] array, final int index, final byte param) {
+-        return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
+-    }
+-
+-    public static byte getAndOrVolatile(final byte[] array, final int index, final byte param) {
+-        return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+-    }
+-
+-    public static byte getAndXorVolatile(final byte[] array, final int index, final byte param) {
+-        return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+-    }
+-
+-    public static byte getAndSetVolatile(final byte[] array, final int index, final byte param) {
+-        return (byte)BYTE_ARRAY_HANDLE.getAndSet(array, index, param);
+-    }
+-
+-    public static byte compareAndExchangeVolatileContended(final byte[] array, final int index, final byte expect, final byte update) {
+-        return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static byte getAndAddVolatileContended(final byte[] array, final int index, final byte param) {
+-        int failures = 0;
+-
+-        for (byte curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr + param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static byte getAndAndVolatileContended(final byte[] array, final int index, final byte param) {
+-        int failures = 0;
+-
+-        for (byte curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr & param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static byte getAndOrVolatileContended(final byte[] array, final int index, final byte param) {
+-        int failures = 0;
+-
+-        for (byte curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr | param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static byte getAndXorVolatileContended(final byte[] array, final int index, final byte param) {
+-        int failures = 0;
+-
+-        for (byte curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr ^ param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static byte getAndSetVolatileContended(final byte[] array, final int index, final byte param) {
+-        int failures = 0;
+-
+-        for (byte curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    /* short array */
+-
+-    public static short getPlain(final short[] array, final int index) {
+-        return (short)SHORT_ARRAY_HANDLE.get(array, index);
+-    }
+-
+-    public static short getOpaque(final short[] array, final int index) {
+-        return (short)SHORT_ARRAY_HANDLE.getOpaque(array, index);
+-    }
+-
+-    public static short getAcquire(final short[] array, final int index) {
+-        return (short)SHORT_ARRAY_HANDLE.getAcquire(array, index);
+-    }
+-
+-    public static short getVolatile(final short[] array, final int index) {
+-        return (short)SHORT_ARRAY_HANDLE.getVolatile(array, index);
+-    }
+-
+-    public static void setPlain(final short[] array, final int index, final short value) {
+-        SHORT_ARRAY_HANDLE.set(array, index, value);
+-    }
+-
+-    public static void setOpaque(final short[] array, final int index, final short value) {
+-        SHORT_ARRAY_HANDLE.setOpaque(array, index, value);
+-    }
+-
+-    public static void setRelease(final short[] array, final int index, final short value) {
+-        SHORT_ARRAY_HANDLE.setRelease(array, index, value);
+-    }
+-
+-    public static void setVolatile(final short[] array, final int index, final short value) {
+-        SHORT_ARRAY_HANDLE.setVolatile(array, index, value);
+-    }
+-
+-    public static void setVolatileContended(final short[] array, final int index, final short param) {
+-        int failures = 0;
+-
+-        for (short curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return;
+-            }
+-        }
+-    }
+-
+-    public static short compareAndExchangeVolatile(final short[] array, final int index, final short expect, final short update) {
+-        return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static short getAndAddVolatile(final short[] array, final int index, final short param) {
+-        return (short)SHORT_ARRAY_HANDLE.getAndAdd(array, index, param);
+-    }
+-
+-    public static short getAndAndVolatile(final short[] array, final int index, final short param) {
+-        return (short)SHORT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
+-    }
+-
+-    public static short getAndOrVolatile(final short[] array, final int index, final short param) {
+-        return (short)SHORT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+-    }
+-
+-    public static short getAndXorVolatile(final short[] array, final int index, final short param) {
+-        return (short)SHORT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+-    }
+-
+-    public static short getAndSetVolatile(final short[] array, final int index, final short param) {
+-        return (short)SHORT_ARRAY_HANDLE.getAndSet(array, index, param);
+-    }
+-
+-    public static short compareAndExchangeVolatileContended(final short[] array, final int index, final short expect, final short update) {
+-        return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static short getAndAddVolatileContended(final short[] array, final int index, final short param) {
+-        int failures = 0;
+-
+-        for (short curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr + param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static short getAndAndVolatileContended(final short[] array, final int index, final short param) {
+-        int failures = 0;
+-
+-        for (short curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr & param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static short getAndOrVolatileContended(final short[] array, final int index, final short param) {
+-        int failures = 0;
+-
+-        for (short curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr | param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static short getAndXorVolatileContended(final short[] array, final int index, final short param) {
+-        int failures = 0;
+-
+-        for (short curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr ^ param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static short getAndSetVolatileContended(final short[] array, final int index, final short param) {
+-        int failures = 0;
+-
+-        for (short curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    /* int array */
+-
+-    public static int getPlain(final int[] array, final int index) {
+-        return (int)INT_ARRAY_HANDLE.get(array, index);
+-    }
+-
+-    public static int getOpaque(final int[] array, final int index) {
+-        return (int)INT_ARRAY_HANDLE.getOpaque(array, index);
+-    }
+-
+-    public static int getAcquire(final int[] array, final int index) {
+-        return (int)INT_ARRAY_HANDLE.getAcquire(array, index);
+-    }
+-
+-    public static int getVolatile(final int[] array, final int index) {
+-        return (int)INT_ARRAY_HANDLE.getVolatile(array, index);
+-    }
+-
+-    public static void setPlain(final int[] array, final int index, final int value) {
+-        INT_ARRAY_HANDLE.set(array, index, value);
+-    }
+-
+-    public static void setOpaque(final int[] array, final int index, final int value) {
+-        INT_ARRAY_HANDLE.setOpaque(array, index, value);
+-    }
+-
+-    public static void setRelease(final int[] array, final int index, final int value) {
+-        INT_ARRAY_HANDLE.setRelease(array, index, value);
+-    }
+-
+-    public static void setVolatile(final int[] array, final int index, final int value) {
+-        INT_ARRAY_HANDLE.setVolatile(array, index, value);
+-    }
+-
+-    public static void setVolatileContended(final int[] array, final int index, final int param) {
+-        int failures = 0;
+-
+-        for (int curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return;
+-            }
+-        }
+-    }
+-
+-    public static int compareAndExchangeVolatile(final int[] array, final int index, final int expect, final int update) {
+-        return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static int getAndAddVolatile(final int[] array, final int index, final int param) {
+-        return (int)INT_ARRAY_HANDLE.getAndAdd(array, index, param);
+-    }
+-
+-    public static int getAndAndVolatile(final int[] array, final int index, final int param) {
+-        return (int)INT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
+-    }
+-
+-    public static int getAndOrVolatile(final int[] array, final int index, final int param) {
+-        return (int)INT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+-    }
+-
+-    public static int getAndXorVolatile(final int[] array, final int index, final int param) {
+-        return (int)INT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+-    }
+-
+-    public static int getAndSetVolatile(final int[] array, final int index, final int param) {
+-        return (int)INT_ARRAY_HANDLE.getAndSet(array, index, param);
+-    }
+-
+-    public static int compareAndExchangeVolatileContended(final int[] array, final int index, final int expect, final int update) {
+-        return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static int getAndAddVolatileContended(final int[] array, final int index, final int param) {
+-        int failures = 0;
+-
+-        for (int curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr + param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static int getAndAndVolatileContended(final int[] array, final int index, final int param) {
+-        int failures = 0;
+-
+-        for (int curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr & param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static int getAndOrVolatileContended(final int[] array, final int index, final int param) {
+-        int failures = 0;
+-
+-        for (int curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr | param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static int getAndXorVolatileContended(final int[] array, final int index, final int param) {
+-        int failures = 0;
+-
+-        for (int curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr ^ param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static int getAndSetVolatileContended(final int[] array, final int index, final int param) {
+-        int failures = 0;
+-
+-        for (int curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    /* long array */
+-
+-    public static long getPlain(final long[] array, final int index) {
+-        return (long)LONG_ARRAY_HANDLE.get(array, index);
+-    }
+-
+-    public static long getOpaque(final long[] array, final int index) {
+-        return (long)LONG_ARRAY_HANDLE.getOpaque(array, index);
+-    }
+-
+-    public static long getAcquire(final long[] array, final int index) {
+-        return (long)LONG_ARRAY_HANDLE.getAcquire(array, index);
+-    }
+-
+-    public static long getVolatile(final long[] array, final int index) {
+-        return (long)LONG_ARRAY_HANDLE.getVolatile(array, index);
+-    }
+-
+-    public static void setPlain(final long[] array, final int index, final long value) {
+-        LONG_ARRAY_HANDLE.set(array, index, value);
+-    }
+-
+-    public static void setOpaque(final long[] array, final int index, final long value) {
+-        LONG_ARRAY_HANDLE.setOpaque(array, index, value);
+-    }
+-
+-    public static void setRelease(final long[] array, final int index, final long value) {
+-        LONG_ARRAY_HANDLE.setRelease(array, index, value);
+-    }
+-
+-    public static void setVolatile(final long[] array, final int index, final long value) {
+-        LONG_ARRAY_HANDLE.setVolatile(array, index, value);
+-    }
+-
+-    public static void setVolatileContended(final long[] array, final int index, final long param) {
+-        int failures = 0;
+-
+-        for (long curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return;
+-            }
+-        }
+-    }
+-
+-    public static long compareAndExchangeVolatile(final long[] array, final int index, final long expect, final long update) {
+-        return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static long getAndAddVolatile(final long[] array, final int index, final long param) {
+-        return (long)LONG_ARRAY_HANDLE.getAndAdd(array, index, param);
+-    }
+-
+-    public static long getAndAndVolatile(final long[] array, final int index, final long param) {
+-        return (long)LONG_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
+-    }
+-
+-    public static long getAndOrVolatile(final long[] array, final int index, final long param) {
+-        return (long)LONG_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+-    }
+-
+-    public static long getAndXorVolatile(final long[] array, final int index, final long param) {
+-        return (long)LONG_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+-    }
+-
+-    public static long getAndSetVolatile(final long[] array, final int index, final long param) {
+-        return (long)LONG_ARRAY_HANDLE.getAndSet(array, index, param);
+-    }
+-
+-    public static long compareAndExchangeVolatileContended(final long[] array, final int index, final long expect, final long update) {
+-        return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static long getAndAddVolatileContended(final long[] array, final int index, final long param) {
+-        int failures = 0;
+-
+-        for (long curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr + param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static long getAndAndVolatileContended(final long[] array, final int index, final long param) {
+-        int failures = 0;
+-
+-        for (long curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr & param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static long getAndOrVolatileContended(final long[] array, final int index, final long param) {
+-        int failures = 0;
+-
+-        for (long curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr | param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static long getAndXorVolatileContended(final long[] array, final int index, final long param) {
+-        int failures = 0;
+-
+-        for (long curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr ^ param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static long getAndSetVolatileContended(final long[] array, final int index, final long param) {
+-        int failures = 0;
+-
+-        for (long curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    /* boolean array */
+-
+-    public static boolean getPlain(final boolean[] array, final int index) {
+-        return (boolean)BOOLEAN_ARRAY_HANDLE.get(array, index);
+-    }
+-
+-    public static boolean getOpaque(final boolean[] array, final int index) {
+-        return (boolean)BOOLEAN_ARRAY_HANDLE.getOpaque(array, index);
+-    }
+-
+-    public static boolean getAcquire(final boolean[] array, final int index) {
+-        return (boolean)BOOLEAN_ARRAY_HANDLE.getAcquire(array, index);
+-    }
+-
+-    public static boolean getVolatile(final boolean[] array, final int index) {
+-        return (boolean)BOOLEAN_ARRAY_HANDLE.getVolatile(array, index);
+-    }
+-
+-    public static void setPlain(final boolean[] array, final int index, final boolean value) {
+-        BOOLEAN_ARRAY_HANDLE.set(array, index, value);
+-    }
+-
+-    public static void setOpaque(final boolean[] array, final int index, final boolean value) {
+-        BOOLEAN_ARRAY_HANDLE.setOpaque(array, index, value);
+-    }
+-
+-    public static void setRelease(final boolean[] array, final int index, final boolean value) {
+-        BOOLEAN_ARRAY_HANDLE.setRelease(array, index, value);
+-    }
+-
+-    public static void setVolatile(final boolean[] array, final int index, final boolean value) {
+-        BOOLEAN_ARRAY_HANDLE.setVolatile(array, index, value);
+-    }
+-
+-    public static void setVolatileContended(final boolean[] array, final int index, final boolean param) {
+-        int failures = 0;
+-
+-        for (boolean curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return;
+-            }
+-        }
+-    }
+-
+-    public static boolean compareAndExchangeVolatile(final boolean[] array, final int index, final boolean expect, final boolean update) {
+-        return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static boolean getAndOrVolatile(final boolean[] array, final int index, final boolean param) {
+-        return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+-    }
+-
+-    public static boolean getAndXorVolatile(final boolean[] array, final int index, final boolean param) {
+-        return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+-    }
+-
+-    public static boolean getAndSetVolatile(final boolean[] array, final int index, final boolean param) {
+-        return (boolean)BOOLEAN_ARRAY_HANDLE.getAndSet(array, index, param);
+-    }
+-
+-    public static boolean compareAndExchangeVolatileContended(final boolean[] array, final int index, final boolean expect, final boolean update) {
+-        return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+-    }
+-
+-    public static boolean getAndAndVolatileContended(final boolean[] array, final int index, final boolean param) {
+-        int failures = 0;
+-
+-        for (boolean curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr & param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static boolean getAndOrVolatileContended(final boolean[] array, final int index, final boolean param) {
+-        int failures = 0;
+-
+-        for (boolean curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr | param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static boolean getAndXorVolatileContended(final boolean[] array, final int index, final boolean param) {
+-        int failures = 0;
+-
+-        for (boolean curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr ^ param)))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    public static boolean getAndSetVolatileContended(final boolean[] array, final int index, final boolean param) {
+-        int failures = 0;
+-
+-        for (boolean curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return curr;
+-            }
+-        }
+-    }
+-
+-    @SuppressWarnings("unchecked")
+-    public static <T> T getPlain(final T[] array, final int index) {
+-        final Object ret = OBJECT_ARRAY_HANDLE.get((Object[])array, index);
+-        return (T)ret;
+-    }
+-
+-    @SuppressWarnings("unchecked")
+-    public static <T> T getOpaque(final T[] array, final int index) {
+-        final Object ret = OBJECT_ARRAY_HANDLE.getOpaque((Object[])array, index);
+-        return (T)ret;
+-    }
+-
+-    @SuppressWarnings("unchecked")
+-    public static <T> T getAcquire(final T[] array, final int index) {
+-        final Object ret = OBJECT_ARRAY_HANDLE.getAcquire((Object[])array, index);
+-        return (T)ret;
+-    }
+-
+-    @SuppressWarnings("unchecked")
+-    public static <T> T getVolatile(final T[] array, final int index) {
+-        final Object ret = OBJECT_ARRAY_HANDLE.getVolatile((Object[])array, index);
+-        return (T)ret;
+-    }
+-
+-    public static <T> void setPlain(final T[] array, final int index, final T value) {
+-        OBJECT_ARRAY_HANDLE.set((Object[])array, index, (Object)value);
+-    }
+-
+-    public static <T> void setOpaque(final T[] array, final int index, final T value) {
+-        OBJECT_ARRAY_HANDLE.setOpaque((Object[])array, index, (Object)value);
+-    }
+-
+-    public static <T> void setRelease(final T[] array, final int index, final T value) {
+-        OBJECT_ARRAY_HANDLE.setRelease((Object[])array, index, (Object)value);
+-    }
+-
+-    public static <T> void setVolatile(final T[] array, final int index, final T value) {
+-        OBJECT_ARRAY_HANDLE.setVolatile((Object[])array, index, (Object)value);
+-    }
+-
+-    public static <T> void setVolatileContended(final T[] array, final int index, final T param) {
+-        int failures = 0;
+-
+-        for (T curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return;
+-            }
+-        }
+-    }
+-
+-    @SuppressWarnings("unchecked")
+-    public static <T> T compareAndExchangeVolatile(final T[] array, final int index, final T expect, final T update) {
+-        final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update);
+-        return (T)ret;
+-    }
+-
+-    @SuppressWarnings("unchecked")
+-    public static <T> T getAndSetVolatile(final T[] array, final int index, final T param) {
+-        final Object ret = BYTE_ARRAY_HANDLE.getAndSet((Object[])array, index, (Object)param);
+-        return (T)ret;
+-    }
+-
+-    @SuppressWarnings("unchecked")
+-    public static <T> T compareAndExchangeVolatileContended(final T[] array, final int index, final T expect, final T update) {
+-        final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update);
+-        return (T)ret;
+-    }
+-
+-    public static <T> T getAndSetVolatileContended(final T[] array, final int index, final T param) {
+-        int failures = 0;
+-
+-        for (T curr = getVolatile(array, index);;++failures) {
+-            for (int i = 0; i < failures; ++i) {
+-                ConcurrentUtil.backoff();
+-            }
+-
+-            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+-                return curr;
+-            }
+-        }
+-    }
+-}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
+@@ -0,0 +0,0 @@ public final class IntegerUtil {
+         return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
+     }
+ 
++    // https://lemire.me/blog/2019/02/08/faster-remainders-when-the-divisor-is-a-constant-beating-compilers-and-libdivide
++    /**
++     *
++     * Usage:
++     * <pre>
++     * {@code
++     *     static final long mult = getSimpleMultiplier(divisor, bits);
++     *     long x = ...;
++     *     long magic = x * mult;
++     *     long divQ = magic >>> bits;
++     *     long divR = ((magic & ((1 << bits) - 1)) * divisor) >>> bits;
++     * }
++     * </pre>
++     *
++     * @param bits The number of bits of precision for the returned result
++     */
++    public static long getUnsignedDivisorMagic(final long divisor, final int bits) {
++        return (((1L << bits) - 1L) / divisor) + 1;
++    }
++
+     private IntegerUtil() {
+         throw new RuntimeException();
+     }
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Priority.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Priority.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Priority.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.util;
++
++public enum Priority {
++
++    /**
++     * Priority value indicating the task has completed or is being completed.
++     * This priority cannot be used to schedule tasks.
++     */
++    COMPLETING(-1),
++
++    /**
++     * Absolute highest priority, should only be used for when a task is blocking a time-critical thread.
++     */
++    BLOCKING(),
++
++    /**
++     * Should only be used for urgent but not time-critical tasks.
++     */
++    HIGHEST(),
++
++    /**
++     * Two priorities above normal.
++     */
++    HIGHER(),
++
++    /**
++     * One priority above normal.
++     */
++    HIGH(),
++
++    /**
++     * Default priority.
++     */
++    NORMAL(),
++
++    /**
++     * One priority below normal.
++     */
++    LOW(),
++
++    /**
++     * Two priorities below normal.
++     */
++    LOWER(),
++
++    /**
++     * Use for tasks that should eventually execute, but are not needed to.
++     */
++    LOWEST(),
++
++    /**
++     * Use for tasks that can be delayed indefinitely.
++     */
++    IDLE();
++
++    // returns whether the priority can be scheduled
++    public static boolean isValidPriority(final Priority priority) {
++        return priority != null && priority != priority.COMPLETING;
++    }
++
++    // returns the higher priority of the two
++    public static Priority max(final Priority p1, final Priority p2) {
++        return p1.isHigherOrEqualPriority(p2) ? p1 : p2;
++    }
++
++    // returns the lower priroity of the two
++    public static Priority min(final Priority p1, final Priority p2) {
++        return p1.isLowerOrEqualPriority(p2) ? p1 : p2;
++    }
++
++    public boolean isHigherOrEqualPriority(final Priority than) {
++        return this.priority <= than.priority;
++    }
++
++    public boolean isHigherPriority(final Priority than) {
++        return this.priority < than.priority;
++    }
++
++    public boolean isLowerOrEqualPriority(final Priority than) {
++        return this.priority >= than.priority;
++    }
++
++    public boolean isLowerPriority(final Priority than) {
++        return this.priority > than.priority;
++    }
++
++    public boolean isHigherOrEqualPriority(final int than) {
++        return this.priority <= than;
++    }
++
++    public boolean isHigherPriority(final int than) {
++        return this.priority < than;
++    }
++
++    public boolean isLowerOrEqualPriority(final int than) {
++        return this.priority >= than;
++    }
++
++    public boolean isLowerPriority(final int than) {
++        return this.priority > than;
++    }
++
++    public static boolean isHigherOrEqualPriority(final int priority, final int than) {
++        return priority <= than;
++    }
++
++    public static boolean isHigherPriority(final int priority, final int than) {
++        return priority < than;
++    }
++
++    public static boolean isLowerOrEqualPriority(final int priority, final int than) {
++        return priority >= than;
++    }
++
++    public static boolean isLowerPriority(final int priority, final int than) {
++        return priority > than;
++    }
++
++    static final Priority[] PRIORITIES = Priority.values();
++
++    /** includes special priorities */
++    public static final int TOTAL_PRIORITIES = PRIORITIES.length;
++
++    public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1;
++
++    public static Priority getPriority(final int priority) {
++        return PRIORITIES[priority + 1];
++    }
++
++    private static int priorityCounter;
++
++    private static int nextCounter() {
++        return priorityCounter++;
++    }
++
++    public final int priority;
++
++    private Priority() {
++        this(nextCounter());
++    }
++
++    private Priority(final int priority) {
++        this.priority = priority;
++    }
++}
+\ No newline at end of file
diff --git a/patches/server/fixup-Moonrise-optimisation-patches.patch b/patches/server/fixup-Moonrise-optimisation-patches.patch
new file mode 100644
index 0000000000..719a8c0253
--- /dev/null
+++ b/patches/server/fixup-Moonrise-optimisation-patches.patch
@@ -0,0 +1,14304 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <Spottedleaf@users.noreply.github.com>
+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/PlatformHooks.java b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/PlatformHooks.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.moonrise.common;
++
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder;
++import com.mojang.datafixers.DataFixer;
++import net.minecraft.core.BlockPos;
++import net.minecraft.nbt.CompoundTag;
++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 NewChunkHolder 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/EntityList.java
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/moonrise/common/list/IBlockDataList.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package ca.spottedleaf.moonrise.common.list;
+-
+-import it.unimi.dsi.fastutil.longs.LongIterator;
+-import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap;
+-import java.util.Arrays;
+-import net.minecraft.world.level.block.Block;
+-import net.minecraft.world.level.block.state.BlockState;
+-import net.minecraft.world.level.chunk.GlobalPalette;
+-
+-public final class IBlockDataList {
+-
+-    private static final GlobalPalette<BlockState> GLOBAL_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY);
+-
+-    // map of location -> (index | (location << 16) | (palette id << 32))
+-    private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f);
+-    {
+-        this.map.defaultReturnValue(Long.MAX_VALUE);
+-    }
+-
+-    private static final long[] EMPTY_LIST = new long[0];
+-
+-    private long[] byIndex = EMPTY_LIST;
+-    private int size;
+-
+-    public static int getLocationKey(final int x, final int y, final int z) {
+-        return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4));
+-    }
+-
+-    public static BlockState getBlockDataFromRaw(final long raw) {
+-        return GLOBAL_PALETTE.valueFor((int)(raw >>> 32));
+-    }
+-
+-    public static int getIndexFromRaw(final long raw) {
+-        return (int)(raw & 0xFFFF);
+-    }
+-
+-    public static int getLocationFromRaw(final long raw) {
+-        return (int)((raw >>> 16) & 0xFFFF);
+-    }
+-
+-    public static long getRawFromValues(final int index, final int location, final BlockState data) {
+-        return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.idFor(data)) << 32);
+-    }
+-
+-    public static long setIndexRawValues(final long value, final int index) {
+-        return value & ~(0xFFFF) | (index);
+-    }
+-
+-    public long add(final int x, final int y, final int z, final BlockState data) {
+-        return this.add(getLocationKey(x, y, z), data);
+-    }
+-
+-    public long add(final int location, final BlockState data) {
+-        final long curr = this.map.get((short)location);
+-
+-        if (curr == Long.MAX_VALUE) {
+-            final int index = this.size++;
+-            final long raw = getRawFromValues(index, location, data);
+-            this.map.put((short)location, raw);
+-
+-            if (index >= this.byIndex.length) {
+-                this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L));
+-            }
+-
+-            this.byIndex[index] = raw;
+-            return raw;
+-        } else {
+-            final int index = getIndexFromRaw(curr);
+-            final long raw = this.byIndex[index] = getRawFromValues(index, location, data);
+-
+-            this.map.put((short)location, raw);
+-
+-            return raw;
+-        }
+-    }
+-
+-    public long remove(final int x, final int y, final int z) {
+-        return this.remove(getLocationKey(x, y, z));
+-    }
+-
+-    public long remove(final int location) {
+-        final long ret = this.map.remove((short)location);
+-        final int index = getIndexFromRaw(ret);
+-        if (ret == Long.MAX_VALUE) {
+-            return ret;
+-        }
+-
+-        // move the entry at the end to this index
+-        final int endIndex = --this.size;
+-        final long end = this.byIndex[endIndex];
+-        if (index != endIndex) {
+-            // not empty after this call
+-            this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index));
+-        }
+-        this.byIndex[index] = end;
+-        this.byIndex[endIndex] = 0L;
+-
+-        return ret;
+-    }
+-
+-    public int size() {
+-        return this.size;
+-    }
+-
+-    public long getRaw(final int index) {
+-        return this.byIndex[index];
+-    }
+-
+-    public int getLocation(final int index) {
+-        return getLocationFromRaw(this.getRaw(index));
+-    }
+-
+-    public BlockState getData(final int index) {
+-        return getBlockDataFromRaw(this.getRaw(index));
+-    }
+-
+-    public void clear() {
+-        this.size = 0;
+-        this.map.clear();
+-    }
+-
+-    public LongIterator getRawIterator() {
+-        return this.map.values().iterator();
+-    }
+-}
+\ No newline at end of file
+diff --git a/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/IntList.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/list/ShortList.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/LazyRunnable.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+@@ -0,0 +0,0 @@ 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 {
+ 
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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;
+         }
+@@ -0,0 +0,0 @@ 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;
+             }
+ 
+@@ -0,0 +0,0 @@ public final class NearbyPlayers {
+ 
+             if (list.size() == 0) {
+                 this.players[idx] = null;
++                this.nearbyPlayers.directByChunk[idx].remove(this.chunkKey);
+                 --this.nonEmptyLists;
+             }
+         }
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java
+@@ -0,0 +0,0 @@
+ package ca.spottedleaf.moonrise.common.util;
+ 
+ import ca.spottedleaf.concurrentutil.util.Priority;
++import ca.spottedleaf.moonrise.common.PlatformHooks;
+ import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
+ import ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk;
+ import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
+ import ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache;
++import ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel;
+ import com.mojang.logging.LogUtils;
+ import net.minecraft.server.level.ChunkHolder;
+ import net.minecraft.server.level.FullChunkStatus;
+ import net.minecraft.server.level.ServerLevel;
+ import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.server.level.progress.ChunkProgressListener;
+ import net.minecraft.world.entity.Entity;
+ import net.minecraft.world.level.chunk.ChunkAccess;
+ import net.minecraft.world.level.chunk.LevelChunk;
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+     }
+ 
+     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;
+     }
+ 
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java
+@@ -0,0 +0,0 @@ package ca.spottedleaf.moonrise.common.util;
+ public final class MixinWorkarounds {
+ 
+     // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs
++    // 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java
+@@ -0,0 +0,0 @@
+ package ca.spottedleaf.moonrise.common.util;
+ 
+-import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedThreadPool;
++import 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseConstants.java
+@@ -0,0 +0,0 @@
+ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/SimpleRandom.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java
+@@ -0,0 +0,0 @@ public class TickThread extends Thread {
+     }
+ 
+     public 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/WorldUtil.java
+@@ -0,0 +0,0 @@ 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/patches/block_counting/BlockCountingBitStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingBitStorage.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccess.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/PropertyAccessStateHolder.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/blockstate_propertyaccess/util/ZeroCollidingReferenceStateTable.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ public final class ChunkSystemConverters {
+     public static CompoundTag convertPoiCompoundTag(final CompoundTag data, final ServerLevel world) {
+         final int dataVersion = getDataVersion(data, DEFAULT_POI_DATA_VERSION);
+ 
+-        // Paper start - dataconverter
+-        return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(
+-            ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.POI_CHUNK, data, dataVersion, getCurrentVersion()
+-        );
+-        // Paper end - dataconverter
++        return PlatformHooks.get().convertNBT(DataFixTypes.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);
+ 
+-        // Paper start - dataconverter
+-        return ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(
+-            ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.ENTITY_CHUNK, data, dataVersion, getCurrentVersion()
+-        );
+-        // Paper end - dataconverter
++        return PlatformHooks.get().convertNBT(DataFixTypes.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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/ChunkSystemFeatures.java
++++ /dev/null
+@@ -0,0 +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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/async_save/AsyncChunkSaveData.java
++++ /dev/null
+@@ -0,0 +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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+ 
+@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/RegionFileIOThread.java
++++ /dev/null
+@@ -0,0 +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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+     }
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+     }
+ 
+@@ -0,0 +0,0 @@ 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 {
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+     }
+ 
+@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/ChunkSystemChunkMap.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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 {
+ 
+@@ -0,0 +0,0 @@ public final class ChunkEntitySlices {
+     private final EntityList entities = new EntityList();
+ 
+     public FullChunkStatus status;
++    public final ChunkData chunkData;
+ 
+     private boolean isTransient;
+ 
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ public final class ChunkEntitySlices {
+         }
+ 
+         final ListTag entitiesTag = new ListTag();
+-        final java.util.Map<net.minecraft.world.entity.EntityType<?>, Integer> savedEntityCounts = new java.util.HashMap<>(); // Paper - Entity load/save limit per chunk
+-        for (final Entity entity : entities) {
+-            // Paper start - Entity load/save limit per chunk
+-            final EntityType<?> entityType = entity.getType();
+-            final int saveLimit = world.paperConfig().chunks.entityPerChunkSaveLimit.getOrDefault(entityType, -1);
+-            if (saveLimit > -1) {
+-                if (savedEntityCounts.getOrDefault(entityType, 0) >= saveLimit) {
+-                    continue;
+-                }
+-                savedEntityCounts.merge(entityType, 1, Integer::sum);
+-            }
+-            // Paper end - Entity load/save limit per chunk
++        for (final Entity entity : PlatformHooks.get().modifySavedEntities(world, chunkPos.x, chunkPos.z, entities)) {
+             CompoundTag compoundTag = new CompoundTag();
+             if (entity.save(compoundTag)) {
+                 entitiesTag.add(compoundTag);
+@@ -0,0 +0,0 @@ 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);
+                     }
+                 }
+             }
+@@ -0,0 +0,0 @@ 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() {
+         final int len = this.entities.size();
+         if (len == 0) {
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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<>();
+@@ -0,0 +0,0 @@ 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;
+     }
+ 
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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");
+ 
+@@ -0,0 +0,0 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
+             return false;
+         }
+ 
+-        if (!this.screenEntity(entity)) {
++        if (!this.screenEntity(entity, fromDisk, event)) {
+             return false;
+         }
+ 
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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);
+         }
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+ 
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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
+             );
+         }
+ 
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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));
+@@ -0,0 +0,0 @@ public final class RegionizedPlayerChunkLoader {
+             // now all tickets should be removed, which is all of our external state
+         }
+ 
+-        // For external checks
+-        public it.unimi.dsi.fastutil.longs.LongOpenHashSet getSentChunksRaw() {
++        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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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) + "'");
+             }
+@@ -0,0 +0,0 @@ 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);
+             }
+@@ -0,0 +0,0 @@ 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();
+ 
+@@ -0,0 +0,0 @@ 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"
++            );
+         }
+     }
+ 
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ public final class ChunkHolderManager {
+                         final ChunkLoadTask.EntityDataLoadTask entityLoad = current.getEntityDataLoadTask();
+ 
+                         if (entityLoad != null) {
+-                            entityLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++                            entityLoad.raisePriority(Priority.BLOCKING);
+                         }
+                     }
+                 }
+@@ -0,0 +0,0 @@ public final class ChunkHolderManager {
+                     final ChunkLoadTask.PoiDataLoadTask poiLoad = current.getPoiDataLoadTask();
+ 
+                     if (poiLoad != null) {
+-                        poiLoad.raisePriority(PrioritisedExecutor.Priority.BLOCKING);
++                        poiLoad.raisePriority(Priority.BLOCKING);
+                     }
+                 }
+             } finally {
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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;
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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;
+ 
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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);
+     }
+ 
+@@ -0,0 +0,0 @@ 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));
+     }
+@@ -0,0 +0,0 @@ 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
+ 
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+         }
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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");
+         }
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ public final class ChunkTaskScheduler {
+ 
+         final int neighbourReadRadius = Math.max(
+                 0,
+-                chunkPyramid.getStepTo(toStatus).getAccumulatedRadiusOf(ChunkStatus.EMPTY)
++                chunkStep.getAccumulatedRadiusOf(ChunkStatus.EMPTY)
+         );
+ 
+         boolean unGeneratedNeighbours = false;
+@@ -0,0 +0,0 @@ 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);
+ 
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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() &&
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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<>();
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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<>();
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ public final class NewChunkHolder {
+             return neighbour;
+         }
+ 
+-        return PrioritisedExecutor.Priority.max(us, neighbour);
++        return Priority.max(us, neighbour);
+     }
+ 
+     private void recalculateNeighbourRequestedPriority() {
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+         }
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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;
+         }
+@@ -0,0 +0,0 @@ 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() {
+@@ -0,0 +0,0 @@ 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() {
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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<>();
+@@ -0,0 +0,0 @@ public final class NewChunkHolder {
+                     LOGGER.error("Failed to process chunk status callback", thr);
+                 }
+             }
+-        }, PrioritisedExecutor.Priority.HIGHEST);
++        }, Priority.HIGHEST);
+     }
+ 
+     // note: must hold scheduling lock
+@@ -0,0 +0,0 @@ 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");
+ 
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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()));
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+                 }
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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 {
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ public abstract class PriorityHolder {
+             return;
+         }
+ 
+-        this.scheduleTask(PrioritisedExecutor.Priority.getPriority(priority));
++        this.scheduleTask(Priority.getPriority(priority));
+ 
+         int failures = 0;
+         for (;;) {
+@@ -0,0 +0,0 @@ public abstract class PriorityHolder {
+                 return;
+             }
+ 
+-            this.setPriorityScheduled(PrioritisedExecutor.Priority.getPriority(priority));
++            this.setPriorityScheduled(Priority.getPriority(priority));
+ 
+             ++failures;
+             for (int i = 0; i < failures; ++i) {
+@@ -0,0 +0,0 @@ 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);
+         }
+ 
+@@ -0,0 +0,0 @@ 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);
+         }
+ 
+@@ -0,0 +0,0 @@ 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);
+         }
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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() {
+@@ -0,0 +0,0 @@ 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];
+ 
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+         }
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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() {
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ public class RadiusAwarePrioritisedExecutor {
+             scheduleTasks(toSchedule);
+         }
+ 
++        @Override
++        public PrioritisedExecutor getExecutor() {
++            return this.scheduler.executor;
++        }
++
+         @Override
+         public void run() {
+             final Runnable run = this.run;
+@@ -0,0 +0,0 @@ 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;
+                 }
+ 
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ public class RadiusAwarePrioritisedExecutor {
+         }
+ 
+         @Override
+-        public PrioritisedExecutor.Priority getPriority() {
++        public Priority getPriority() {
+             final PrioritisedExecutor.PrioritisedTask task;
+             synchronized (this.scheduler) {
+                 if ((task = this.queuedTask) == null) {
+@@ -0,0 +0,0 @@ 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);
+             }
+ 
+@@ -0,0 +0,0 @@ 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;
+                     }
+ 
+@@ -0,0 +0,0 @@ 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);
+             }
+ 
+@@ -0,0 +0,0 @@ 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;
+                     }
+ 
+@@ -0,0 +0,0 @@ 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);
+             }
+ 
+@@ -0,0 +0,0 @@ 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;
+                     }
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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 {
+@@ -0,0 +0,0 @@ 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
++                    ChunkStatusTasks.postLoadProtoChunk(world, protoChunk.getEntities());
+                 });
+                 this.chunkHolder.replaceProtoChunk(new ImposterProtoChunk(chunk, false));
+             }
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+     }
+ 
+@@ -0,0 +0,0 @@ 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;
+         }
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+         }
+ 
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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();
+         }
+ 
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+ 
+@@ -0,0 +0,0 @@ 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 {
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+             }
+ 
+@@ -0,0 +0,0 @@ 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;
+                 }
+ 
+@@ -0,0 +0,0 @@ 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);
+             }
+ 
+@@ -0,0 +0,0 @@ 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;
+                 }
+ 
+@@ -0,0 +0,0 @@ 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);
+             }
+ 
+@@ -0,0 +0,0 @@ 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;
+                 }
+ 
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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
+                 );
+             }
+ 
+@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemRegionFile.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/util/stream/ExternalChunkStreamMarker.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
+@@ -0,0 +0,0 @@
+ 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;
+     }
+ 
+@@ -0,0 +0,0 @@ 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));
+     }
+ 
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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];
+@@ -0,0 +0,0 @@ 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;
+         }
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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);
+         }
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+ 
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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;
+ 
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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;
+ 
+@@ -0,0 +0,0 @@ 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,
+@@ -0,0 +0,0 @@ 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];
+ 
+@@ -0,0 +0,0 @@ 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);
+             }
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ public final class CollisionUtil {
+             return true;
+         } else if (isEmpty1 ^ isEmpty2) {
+             return false;
+-        }
++        } // else: isEmpty1 = isEmpty2 = false
+ 
+         if (cachedShapeData1.hasSingleAABB() != cachedShapeData2.hasSingleAABB()) {
+             return false;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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 */
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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)) {
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+         }
+ 
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/ExplosionBlockCache.java
+@@ -0,0 +0,0 @@
+ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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())
+@@ -0,0 +0,0 @@ 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();
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+         }
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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();
+@@ -0,0 +0,0 @@ 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();
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/util/EmptyStreamForMoveCall.java
++++ /dev/null
+@@ -0,0 +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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/world/CollisionLevel.java
++++ /dev/null
+@@ -0,0 +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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPalette.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/fast_palette/FastPaletteData.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/fluid/FluidFluidState.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_getblock/GetBlockChunk.java
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/getblock/GetBlockChunk.java
+@@ -0,0 +0,0 @@
+-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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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());
+             }
+         }
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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.
+ 
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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)));
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+                 }
+             }
+         }
+@@ -0,0 +0,0 @@ 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++];
+ 
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@
+ 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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(
+@@ -0,0 +0,0 @@ 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),
+@@ -0,0 +0,0 @@ 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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/moonrise/patches/starlight/storage/StarlightSectionData.java
+@@ -0,0 +0,0 @@
++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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
++++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/core/MappedRegistry.java
++++ b/src/main/java/net/minecraft/core/MappedRegistry.java
+@@ -0,0 +0,0 @@ public class MappedRegistry<T> implements WritableRegistry<T> {
+     };
+     private final Object tagAdditionLock = new Object();
+ 
++    // 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);
+     }
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
+         // Spigot end
+ 
+         // Paper end - Improved watchdog support; move final shutdown items here
+-        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
+         // Paper start - move final shutdown items here
+         Util.shutdownExecutors();
+         try {
+diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+@@ -0,0 +0,0 @@ public class ChunkHolder extends GenerationChunkHolder implements ca.spottedleaf
+ 
+         if (ichunkaccess != null) {
+             ichunkaccess.setUnsaved(true);
+-            LevelChunk chunk = this.getChunkToSend(); // Paper - rewrite chunk system
++            LevelChunk chunk = this.playersSentChunkTo.size() == 0 ? null : this.getChunkToSend(); // Paper - rewrite chunk system
+ 
+             if (chunk != null) {
+                 int j = this.lightEngine.getMinLightSection();
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+     // 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+@@ -0,0 +0,0 @@ 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
+         );
+ 
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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() {
+@@ -0,0 +0,0 @@ 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
+             );
+ 
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
+     }
+ 
+     // 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;
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
+             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);
+                     }
+                 }
+             }
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
+     // 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();
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
+         ProfilerFiller gameprofilerfiller = this.getProfiler();
+ 
+         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)) {
+@@ -0,0 +0,0 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf.
+ 
+         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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/util/BitStorage.java
++++ b/src/main/java/net/minecraft/util/BitStorage.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
++++ b/src/main/java/net/minecraft/util/CrudeIncrementalIntIdentityHashBiMap.java
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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];
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/util/SimpleBitStorage.java
++++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/util/ZeroBitStorage.java
++++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/entity/Entity.java
++++ b/src/main/java/net/minecraft/world/entity/Entity.java
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+ 
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+@@ -0,0 +0,0 @@ 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;
++                                }
+ 
+-                    if (ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.voxelShapeIntersectNoEmpty(collisionShape, toCollide)) {
+-                        return true;
++                                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;
+                 }
+             }
+         }
+@@ -0,0 +0,0 @@ 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);
++        final AABB boundingBox = this.getBoundingBox().deflate(1.0E-3);
+ 
+-                        if (fluid.is(tag)) {
+-                            double d2 = (double) ((float) i2 + fluid.getHeight(this.level(), blockposition_mutableblockposition));
++        final Level world = this.level;
++        final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection(world);
+ 
+-                            if (d2 >= axisalignedbb.minY) {
+-                                flag1 = true;
+-                                d1 = Math.max(d2 - axisalignedbb.minY, d1);
+-                                if (flag) {
+-                                    Vec3 vec3d1 = fluid.getFlow(this.level(), blockposition_mutableblockposition);
++        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);
+ 
+-                                    if (d1 < 0.4D) {
+-                                        vec3d1 = vec3d1.scale(d1);
+-                                    }
++        // 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();
+ 
+-                                    vec3d = vec3d.add(vec3d1);
+-                                    ++k1;
++        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;
+                                 }
+-                                // CraftBukkit start - store last lava contact location
+-                                if (tag == FluidTags.LAVA) {
+-                                    this.lastLavaContact = blockposition_mutableblockposition.immutable();
++
++                                mutablePos.set(currX | (currChunkX << 4), currY | (currChunkY << 4), currZ | (currChunkZ << 4));
++
++                                final double height = (double)((float)mutablePos.getY() + fluidState.getHeight(world, mutablePos));
++                                final double diff = height - boundingBox.minY;
++
++                                if (diff < 0.0) {
++                                    continue;
++                                }
++
++                                inFluid = true;
++                                maxHeightDiff = Math.max(maxHeightDiff, diff);
++
++                                if (!isPushable) {
++                                    continue;
++                                }
++
++                                ++totalPushes;
++
++                                final Vec3 flow = fluidState.getFlow(world, mutablePos);
++
++                                if (diff < 0.4) {
++                                    pushVector = pushVector.add(flow.scale(diff));
++                                } else {
++                                    pushVector = pushVector.add(flow);
+                                 }
+-                                // CraftBukkit end
+                             }
+                         }
+                     }
+                 }
+             }
++        }
+ 
+-            if (vec3d.length() > 0.0D) {
+-                if (k1 > 0) {
+-                    vec3d = vec3d.scale(1.0D / (double) k1);
+-                }
++        this.fluidHeight.put(fluid, maxHeightDiff);
+ 
+-                if (!(this instanceof Player)) {
+-                    vec3d = vec3d.normalize();
+-                }
++        if (pushVector.lengthSqr() == 0.0) {
++            return inFluid;
++        }
+ 
+-                Vec3 vec3d2 = this.getDeltaMovement();
++        // note: totalPushes != 0 as pushVector != 0
++        pushVector = pushVector.scale(1.0 / totalPushes);
++        final Vec3 currMovement = this.getDeltaMovement();
+ 
+-                vec3d = vec3d.scale(speed);
+-                double d3 = 0.003D;
++        if (!((Entity)(Object)this instanceof Player)) {
++            pushVector = pushVector.normalize();
++        }
+ 
+-                if (Math.abs(vec3d2.x) < 0.003D && Math.abs(vec3d2.z) < 0.003D && vec3d.length() < 0.0045000000000000005D) {
+-                    vec3d = vec3d.normalize().scale(0.0045000000000000005D);
+-                }
++        pushVector = pushVector.scale(flowScale);
++        if (Math.abs(currMovement.x) < 0.003 && Math.abs(currMovement.z) < 0.003 && pushVector.length() < 0.0045000000000000005) {
++            pushVector = pushVector.normalize().scale(0.0045000000000000005);
++        }
+ 
+-                this.setDeltaMovement(this.getDeltaMovement().add(vec3d));
+-            }
++        this.setDeltaMovement(currMovement.add(pushVector));
+ 
+-            this.fluidHeight.put(tag, d1);
+-            return flag1;
+-        }
++        // note: inFluid = true here as pushVector != 0
++        return true;
+     }
++    // Paper end - optimise collisions
+ 
+     public boolean touchingUnloadedChunk() {
+         AABB axisalignedbb = this.getBoundingBox().inflate(1.0D);
+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted
+ 
+         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);
+     }
+@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted
+     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);
+         }
+     }
+@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted
+     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));
+@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted
+ 
+     @Override
+     public final net.minecraft.nbt.CompoundTag moonrise$read(final int chunkX, final int chunkZ) throws java.io.IOException {
+-        if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
+-            return ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.loadData(
+-                this.world, chunkX, chunkZ, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.POI_DATA,
+-                ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.getIOBlockingPriorityForCurrentThread()
+-            );
+-        }
+-        return this.moonrise$getRegionStorage().read(new ChunkPos(chunkX, chunkZ));
++        return ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.loadData(
++            this.world, chunkX, chunkZ, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.POI_DATA,
++            ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.getIOBlockingPriorityForCurrentThread()
++        );
+     }
+ 
+     @Override
+     public final void moonrise$write(final int chunkX, final int chunkZ, final net.minecraft.nbt.CompoundTag data) throws java.io.IOException {
+-        if (!ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.isRegionFileThread()) {
+-            ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.scheduleSave(this.world, chunkX, chunkZ, data, ca.spottedleaf.moonrise.patches.chunk_system.io.RegionFileIOThread.RegionFileType.POI_DATA);
+-            return;
+-        }
+-        this.moonrise$getRegionStorage().write(new ChunkPos(chunkX, chunkZ), data);
++        ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.scheduleSave(this.world, chunkX, chunkZ, data, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionFileType.POI_DATA);
+     }
+     // Paper end - rewrite chunk system
+ 
+@@ -0,0 +0,0 @@ public class PoiManager extends SectionStorage<PoiSection> implements ca.spotted
+     }
+ 
+     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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ public class PoiSection implements ca.spottedleaf.moonrise.patches.chunk_system.
+     }
+ 
+     // 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/Level.java
++++ b/src/main/java/net/minecraft/world/level/Level.java
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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();
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+     }
+@@ -0,0 +0,0 @@ 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) -> {
+@@ -0,0 +0,0 @@ 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();
+         }
+@@ -0,0 +0,0 @@ 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();
++
++        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 (lastChunk == null) {
++                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;
+                     }
+                 }
+             }
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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, Supplier<ProfilerFiller> supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function<org.spigotmc.SpigotWorldConfig, io.papermc.paper.configuration.WorldConfiguration> paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config & Anti-Xray
++        // Paper start - getblock optimisations - cache world height/sections
++        final DimensionType dimType = holder.value();
++        this.minY = dimType.minY();
++        this.height = dimType.height();
++        this.maxY = this.minY + this.height - 1;
++        this.minSectionY = this.minY >> 4;
++        this.maxSectionY = this.maxY >> 4;
++        this.sectionsCount = this.maxSectionY - this.minSectionY + 1;
++        // Paper end - getblock optimisations - cache world height/sections
+         this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot
+         this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
+         this.generator = gen;
+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/biome/BiomeManager.java
++++ b/src/main/java/net/minecraft/world/level/biome/BiomeManager.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ public abstract class BlockBehaviour implements FeatureElement {
+         private boolean isRandomlyTicking;
+ 
+         // 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;
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ public abstract class BlockBehaviour implements FeatureElement {
+             this.legacySolid = this.calculateSolid();
+             // 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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>() {
+@@ -0,0 +0,0 @@ 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 Table<Property<?>, Comparable<?>, 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) {
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ public abstract class StateHolder<O, S> {
+     }
+ 
+     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 if (comparable.equals(value)) {
+-            return (S)this;
+-        } else {
+-            S object = this.neighbours.get(property, value);
+-            if (object == null) {
+-                throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value");
+-            } else {
+-                return object;
+-            }
++        // 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);
+-        if (comparable != null && !comparable.equals(value)) {
+-            S object = this.neighbours.get(property, value);
+-            if (object == null) {
+-                throw new IllegalArgumentException("Cannot set property " + property + " to " + value + " on " + this.owner + ", it is not an allowed value");
+-            } else {
+-                return object;
+-            }
+-        } else {
+-            return (S)this;
++        // 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
+     }
+ 
+     public void populateNeighbours(Map<Map<Property<?>, Comparable<?>>, S> states) {
++        // Paper start - optimise blockstate property access
++        final Map<Map<Property<?>, Comparable<?>>, S> map = states;
++        if (true) {
++            if (this.optimisedTable.isLoaded()) {
++                return;
++            }
++            this.optimisedTable.loadInTable(map);
++
++            // 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;
++            }
++
++            // 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
+         if (this.neighbours != null) {
+             throw new IllegalStateException();
+         } else {
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ import com.google.common.collect.ImmutableSet;
+ import java.util.Collection;
+ import java.util.Optional;
+ 
+-public class BooleanProperty extends Property<Boolean> {
++public class BooleanProperty extends Property<Boolean> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Boolean> { // Paper - optimise blockstate property access
+     private final ImmutableSet<Boolean> values = ImmutableSet.of(true, false);
+ 
++    // 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
++
+     protected 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ import java.util.function.Predicate;
+ import java.util.stream.Collectors;
+ import net.minecraft.util.StringRepresentable;
+ 
+-public class EnumProperty<T extends Enum<T> & StringRepresentable> extends Property<T> {
++public 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 ImmutableSet<T> values;
+     private final Map<String, T> names = Maps.newHashMap();
+ 
++    // 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 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
++
+     protected EnumProperty(String name, Class<T> type, Collection<T> values) {
+         super(name, type);
+         this.values = ImmutableSet.copyOf(values);
+@@ -0,0 +0,0 @@ public class EnumProperty<T extends Enum<T> & StringRepresentable> extends Prope
+ 
+             this.names.put(string, enum_);
+         }
++        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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ import java.util.Collection;
+ import java.util.Optional;
+ import java.util.Set;
+ 
+-public class IntegerProperty extends Property<Integer> {
++public class IntegerProperty extends Property<Integer> implements ca.spottedleaf.moonrise.patches.blockstate_propertyaccess.PropertyAccess<Integer> { // Paper - optimise blockstate property access
+     private final ImmutableSet<Integer> 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
++
+     protected IntegerProperty(String name, int min, int max) {
+         super(name, Integer.class);
+         if (min < 0) {
+@@ -0,0 +0,0 @@ public class IntegerProperty extends Property<Integer> {
+ 
+             this.values = ImmutableSet.copyOf(set);
+         }
++        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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java
++++ b/src/main/java/net/minecraft/world/level/chunk/HashMapPalette.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+@@ -0,0 +0,0 @@ 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() {
+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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+@@ -0,0 +0,0 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
+     private PalettedContainer<Holder<Biome>> biomes;
+ 
+     // 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() {
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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;
+ 
+@@ -0,0 +0,0 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_
+             }
+         }
+ 
+-        if (!fluid.isEmpty()) {
++        if (!!fluid.isRandomlyTicking()) { // Paper - block counting
+             --this.tickingFluidCount;
+         }
+ 
+@@ -0,0 +0,0 @@ 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;
+     }
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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;
+                     }
+                 }
+             }
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java
++++ b/src/main/java/net/minecraft/world/level/chunk/LinearPalette.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/Palette.java
++++ b/src/main/java/net/minecraft/world/level/chunk/Palette.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
++++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+         // this.threadingDetector.checkAndUnlock(); // Paper - disable this
+     }
+ 
++    // Paper start - optimise palette reads
++    private void updateData(final PalettedContainer.Data<T> data) {
++        if (data != null) {
++            ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$setPalette(
++                ((ca.spottedleaf.moonrise.patches.fast_palette.FastPalette<T>)data.palette).moonrise$getRawPalette((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data)
++            );
++        }
++    }
++
++    private T readPaletteSlow(final PalettedContainer.Data<T> data, final int paletteIdx) {
++        return data.palette.valueFor(paletteIdx);
++    }
++
++    private T readPalette(final PalettedContainer.Data<T> data, final int paletteIdx) {
++        final T[] palette = ((ca.spottedleaf.moonrise.patches.fast_palette.FastPaletteData<T>)(Object)data).moonrise$getPalette();
++        if (palette == null) {
++            return this.readPaletteSlow(data, paletteIdx);
++        }
++
++        final T ret = palette[paletteIdx];
++        if (ret == null) {
++            throw new IllegalArgumentException("Palette index out of bounds");
++        }
++        return ret;
++    }
++    // Paper end - optimise palette reads
++
+     // Paper start - Anti-Xray - Add preset values
+     @Deprecated @io.papermc.paper.annotation.DoNotUse public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue) { return PalettedContainer.codecRW(idList, entryCodec, paletteProvider, defaultValue, null); }
+     public static <T> Codec<PalettedContainer<T>> codecRW(IdMap<T> idList, Codec<T> entryCodec, PalettedContainer.Strategy paletteProvider, T defaultValue, T @org.jetbrains.annotations.Nullable [] presetValues) {
+@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+             }
+         }
+         // Paper end
++        this.updateData(this.data); // Paper - optimise palette reads
+     }
+ 
+     // Paper start - Anti-Xray - Add preset values
+@@ -0,0 +0,0 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
+         this.registry = idList;
+         this.strategy = paletteProvider;
+         this.data = data;
++        this.updateData(this.data); // Paper - optimise palette reads
+     }
+ 
+     // Paper start - Anti-Xray - Add preset values
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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
+         this.addPresetValues();
+         return object == null ? -1 : data2.palette.idFor(object);
+         // Paper end
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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
+             this.addPresetValues(); // Paper - Anti-Xray - Add preset values (inefficient, but this isn't used by the server)
+         } finally {
+             this.release();
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java
++++ b/src/main/java/net/minecraft/world/level/chunk/SingleValuePalette.java
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+         }
+     }
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ import net.minecraft.nbt.NbtIo; // Paper
+ import net.minecraft.world.level.ChunkPos;
+ import org.slf4j.Logger;
+ 
+-public class RegionFile implements AutoCloseable {
++public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile { // Paper - rewrite chunk system
+ 
+     private static final Logger LOGGER = LogUtils.getLogger();
+     private static final int SECTOR_BYTES = 4096;
+@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
+     private final IntBuffer timestamps;
+     @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 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
+     // Paper start - Attempt to recalculate regionfile header if it is corrupt
+     private static long roundToSectors(long bytes) {
+         long sectors = bytes >>> 12; // 4096 = 2^12
+@@ -0,0 +0,0 @@ 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])) {
+@@ -0,0 +0,0 @@ public class RegionFile implements AutoCloseable {
+ 
+     }
+     // Paper end
+-    private class ChunkBuffer extends ByteArrayOutputStream {
++    private class ChunkBuffer extends ByteArrayOutputStream implements ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemChunkBuffer { // Paper - rewrite chunk system
+ 
+         private final ChunkPos pos;
+ 
++        // Paper start - rewrite chunk system
++        private boolean writeOnClose = true;
++
++        @Override
++        public final boolean moonrise$getWriteOnClose() {
++            return this.writeOnClose;
++        }
++
++        @Override
++        public final void moonrise$setWriteOnClose(final boolean value) {
++            this.writeOnClose = value;
++        }
++
++        @Override
++        public final void moonrise$write(final RegionFile regionFile) throws IOException {
++            regionFile.write(this.pos, ByteBuffer.wrap(this.buf, 0, this.count));
++        }
++        // Paper end - rewrite chunk system
++
+         public ChunkBuffer(final ChunkPos chunkcoordintpair) {
+             super(8096);
+             super.write(0);
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 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
+@@ -0,0 +0,0 @@ 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";
+     }
+@@ -0,0 +0,0 @@ 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
+     // Paper start - recalculate region file headers
+     private final boolean isChunkData;
+@@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
+         this.isChunkData = isChunkDataFolder(this.folder); // Paper - recalculate region file headers
+     }
+ 
++    // 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
++++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ public abstract class FlowingFluid extends Fluid {
+         }
+     }
+ 
+-    private boolean canPassThroughWall(Direction face, BlockGetter world, BlockPos pos, BlockState state, BlockPos fromPos, BlockState fromState) {
+-        Object2ByteLinkedOpenHashMap object2bytelinkedopenhashmap;
++    // 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 (!state.getBlock().hasDynamicShape() && !fromState.getBlock().hasDynamicShape()) {
+-            object2bytelinkedopenhashmap = (Object2ByteLinkedOpenHashMap) FlowingFluid.OCCLUSION_CACHE.get();
+-        } else {
+-            object2bytelinkedopenhashmap = null;
++        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;
+         }
+ 
+-        Block.BlockStatePairKey block_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) {
+-            block_a = new Block.BlockStatePairKey(state, fromState, face);
+-            byte b0 = object2bytelinkedopenhashmap.getAndMoveToFirst(block_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;
++        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();
+             }
+-        } else {
+-            block_a = null;
+         }
+ 
+-        VoxelShape voxelshape = state.getCollisionShape(world, pos);
+-        VoxelShape voxelshape1 = fromState.getCollisionShape(world, fromPos);
+-        boolean flag = !Shapes.mergedFaceOccludes(voxelshape, voxelshape1, face);
++        final VoxelShape shape1 = fromState.getCollisionShape(level, fromPos);
++        final VoxelShape shape2 = toState.getCollisionShape(level, toPos);
+ 
+-        if (object2bytelinkedopenhashmap != null) {
+-            if (object2bytelinkedopenhashmap.size() == 200) {
+-                object2bytelinkedopenhashmap.removeLastByte();
+-            }
++        final boolean result = !Shapes.mergedFaceOccludes(shape1, shape2, direction);
+ 
+-            object2bytelinkedopenhashmap.putAndMoveToFirst(block_a, (byte) (flag ? 1 : 0));
++        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 flag;
++        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(Level 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/material/FluidState.java
++++ b/src/main/java/net/minecraft/world/level/material/FluidState.java
+@@ -0,0 +0,0 @@ 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()
+@@ -0,0 +0,0 @@ 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() {
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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(Level world, BlockPos pos, RandomSource random) {
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
++++ b/src/main/java/net/minecraft/world/phys/shapes/DiscreteVoxelShape.java
+@@ -0,0 +0,0 @@ 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
++++ b/src/main/java/net/minecraft/world/phys/shapes/Shapes.java
+@@ -0,0 +0,0 @@ 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
+     }
+ 
+@@ -0,0 +0,0 @@ public final class Shapes {
+     }
+ 
+     public static VoxelShape getFaceShape(VoxelShape shape, Direction direction) {
+-        return ((ca.spottedleaf.moonrise.patches.collisions.shape.CollisionVoxelShape)shape).moonrise$getFaceShapeClamped(direction); // Paper - optimise collisions
++        if (shape == block()) {
++            return block();
++        } else {
++            Direction.Axis axis = direction.getAxis();
++            boolean bl;
++            int i;
++            if (direction.getAxisDirection() == Direction.AxisDirection.POSITIVE) {
++                bl = DoubleMath.fuzzyEquals(shape.max(axis), 1.0, 1.0E-7);
++                i = shape.shape.getSize(axis) - 1;
++            } else {
++                bl = DoubleMath.fuzzyEquals(shape.min(axis), 0.0, 1.0E-7);
++                i = 0;
++            }
++
++            return (VoxelShape)(!bl ? empty() : new SliceShape(shape, axis, i));
++        }
+     }
+ 
+     // Paper start - 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 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
++++ b/src/main/java/net/minecraft/world/phys/shapes/VoxelShape.java
+@@ -0,0 +0,0 @@ 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();
+             }
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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) {
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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;
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ 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
+@@ -0,0 +0,0 @@ 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);
+@@ -0,0 +0,0 @@ public abstract class VoxelShape implements ca.spottedleaf.moonrise.patches.coll
+     }
+ 
+     private VoxelShape calculateFace(Direction direction) {
+-        Direction.Axis axis = direction.getAxis();
+-        DoubleList doubleList = this.getCoords(axis);
+-        if (doubleList.size() == 2
+-            && DoubleMath.fuzzyEquals(doubleList.getDouble(0), 0.0, 1.0E-7)
+-            && DoubleMath.fuzzyEquals(doubleList.getDouble(1), 1.0, 1.0E-7)) {
+-            return this;
+-        } else {
+-            Direction.AxisDirection axisDirection = direction.getAxisDirection();
+-            int i = this.findIndex(axis, axisDirection == Direction.AxisDirection.POSITIVE ? 0.9999999 : 1.0E-7);
+-            return new SliceShape(this, axis, i);
++        // 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
+     }
+ 
+     // Paper start - optimise collisions
diff --git a/patches/server/fixup-Optimize-BlockPosition-helper-methods.patch b/patches/server/fixup-Optimize-BlockPosition-helper-methods.patch
new file mode 100644
index 0000000000..5718214103
--- /dev/null
+++ b/patches/server/fixup-Optimize-BlockPosition-helper-methods.patch
@@ -0,0 +1,64 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <Spottedleaf@users.noreply.github.com>
+Date: Mon, 21 Oct 2024 19:13:43 -0700
+Subject: [PATCH] fixup! Optimize BlockPosition helper methods
+
+
+diff --git a/src/main/java/net/minecraft/core/BlockPos.java b/src/main/java/net/minecraft/core/BlockPos.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/core/BlockPos.java
++++ b/src/main/java/net/minecraft/core/BlockPos.java
+@@ -0,0 +0,0 @@ public class BlockPos extends Vec3i {
+ 
+     @Override
+     public BlockPos above(int distance) {
+-        return distance == 0 ? this : new BlockPos(this.getX(), this.getY() + distance, this.getZ()); // Paper - Perf: Optimize BlockPosition
++        return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY() + distance, this.getZ()); // Paper - Perf: Optimize BlockPosition
+     }
+ 
+     @Override
+@@ -0,0 +0,0 @@ public class BlockPos extends Vec3i {
+ 
+     @Override
+     public BlockPos below(int i) {
+-        return i == 0 ? this : new BlockPos(this.getX(), this.getY() - i, this.getZ()); // Paper - Perf: Optimize BlockPosition
++        return i == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY() - i, this.getZ()); // Paper - Perf: Optimize BlockPosition
+     }
+ 
+     @Override
+@@ -0,0 +0,0 @@ public class BlockPos extends Vec3i {
+ 
+     @Override
+     public BlockPos north(int distance) {
+-        return distance == 0 ? this : new BlockPos(this.getX(), this.getY(), this.getZ() - distance); // Paper - Perf: Optimize BlockPosition
++        return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY(), this.getZ() - distance); // Paper - Perf: Optimize BlockPosition
+     }
+ 
+     @Override
+@@ -0,0 +0,0 @@ public class BlockPos extends Vec3i {
+ 
+     @Override
+     public BlockPos south(int distance) {
+-        return distance == 0 ? this : new BlockPos(this.getX(), this.getY(), this.getZ() + distance); // Paper - Perf: Optimize BlockPosition
++        return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY(), this.getZ() + distance); // Paper - Perf: Optimize BlockPosition
+     }
+ 
+     @Override
+@@ -0,0 +0,0 @@ public class BlockPos extends Vec3i {
+ 
+     @Override
+     public BlockPos west(int distance) {
+-        return distance == 0 ? this : new BlockPos(this.getX() - distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
++        return distance == 0 ? this.immutable() : new BlockPos(this.getX() - distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+     }
+ 
+     @Override
+@@ -0,0 +0,0 @@ public class BlockPos extends Vec3i {
+ 
+     @Override
+     public BlockPos east(int distance) {
+-        return distance == 0 ? this : new BlockPos(this.getX() + distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
++        return distance == 0 ? this.immutable() : new BlockPos(this.getX() + distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+     }
+ 
+     @Override