aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0981-Starlight.patch
diff options
context:
space:
mode:
authorJake Potrebic <[email protected]>2024-04-27 15:50:33 -0700
committerJake Potrebic <[email protected]>2024-04-27 15:50:38 -0700
commitd02bb811de6f9d7e42001137b93d6a6f88359ab6 (patch)
treea396cbb1f0ada45b98703f8125f06f17334dc080 /patches/server/0981-Starlight.patch
parentdd571d89f2202bda719a40fd15f92cc754b6ade1 (diff)
downloadPaper-d02bb811de6f9d7e42001137b93d6a6f88359ab6.tar.gz
Paper-d02bb811de6f9d7e42001137b93d6a6f88359ab6.zip
proper migration to gamerules for keep spawn loaded distance
Diffstat (limited to 'patches/server/0981-Starlight.patch')
-rw-r--r--patches/server/0981-Starlight.patch5429
1 files changed, 0 insertions, 5429 deletions
diff --git a/patches/server/0981-Starlight.patch b/patches/server/0981-Starlight.patch
deleted file mode 100644
index 72b56b3d53..0000000000
--- a/patches/server/0981-Starlight.patch
+++ /dev/null
@@ -1,5429 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <[email protected]>
-Date: Wed, 28 Oct 2020 16:51:55 -0700
-Subject: [PATCH] Starlight
-
-See https://github.com/PaperMC/Starlight
-
-== AT ==
-public net.minecraft.server.level.ChunkHolder broadcast(Lnet/minecraft/network/protocol/Packet;Z)V
-
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..3732a940d9603cf502983afbc4663113d1400be8
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java
-@@ -0,0 +1,275 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.level.chunk.PalettedContainer;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.Set;
-+
-+public final class BlockStarLightEngine extends StarLightEngine {
-+
-+ public BlockStarLightEngine(final Level world) {
-+ super(false, world);
-+ }
-+
-+ @Override
-+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
-+ return chunk.getBlockEmptinessMap();
-+ }
-+
-+ @Override
-+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
-+ chunk.setBlockEmptinessMap(to);
-+ }
-+
-+ @Override
-+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
-+ return chunk.getBlockNibbles();
-+ }
-+
-+ @Override
-+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
-+ chunk.setBlockNibbles(to);
-+ }
-+
-+ @Override
-+ protected boolean canUseChunk(final ChunkAccess chunk) {
-+ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
-+ }
-+
-+ @Override
-+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically
-+ // because a block was removed - which can decrease light. with sky data, block breaking can only result
-+ // in increases, and thus the existing sky block check will actually correctly propagate light through
-+ // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove
-+ // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running
-+ // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence
-+ // of vanilla data management we "hide" them.
-+ nibble.setHidden();
-+ }
-+ }
-+
-+ @Override
-+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
-+ return;
-+ }
-+
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble == null) {
-+ if (!initRemovedNibbles) {
-+ throw new IllegalStateException();
-+ } else {
-+ this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray());
-+ }
-+ } else {
-+ nibble.setNonNull();
-+ }
-+ }
-+
-+ @Override
-+ protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
-+ // blocks can change opacity
-+ // blocks can change emitted light
-+ // blocks can change direction of propagation
-+
-+ final int encodeOffset = this.coordinateOffset;
-+ final int emittedMask = this.emittedLightMask;
-+
-+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
-+ final BlockState blockState = this.getBlockState(worldX, worldY, worldZ);
-+ final int emittedLevel = blockState.getLightEmission() & emittedMask;
-+
-+ this.setLightLevel(worldX, worldY, worldZ, emittedLevel);
-+ // this accounts for change in emitted light that would cause an increase
-+ if (emittedLevel != 0) {
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (emittedLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
-+ );
-+ }
-+ // this also accounts for a change in emitted light that would cause a decrease
-+ // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa)
-+ // as it checks all neighbours (even if current level is 0)
-+ this.appendToDecreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ // always keep sided transparent false here, new block might be conditionally transparent which would
-+ // prevent us from decreasing sources in the directions where the new block is opaque
-+ // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always
-+ // catch that and fix it.
-+ );
-+ // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block
-+ }
-+
-+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
-+
-+ @Override
-+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect) {
-+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+ int level = centerState.getLightEmission() & 0xF;
-+
-+ if (level >= (15 - 1) || level > expect) {
-+ return level;
-+ }
-+
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final BlockState conditionallyOpaqueState;
-+ int opacity = centerState.getOpacityIfCached();
-+
-+ if (opacity == -1) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+ opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos);
-+ if (centerState.isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ } else if (opacity >= 15) {
-+ return level;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ opacity = Math.max(1, opacity);
-+
-+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
-+ final int offX = worldX + direction.x;
-+ final int offY = worldY + direction.y;
-+ final int offZ = worldZ + direction.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+
-+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
-+
-+ if ((neighbourLevel - 1) <= level) {
-+ // don't need to test transparency, we know it wont affect the result.
-+ continue;
-+ }
-+
-+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
-+ if (neighbourState.isConditionallyFullOpaque()) {
-+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
-+ // we don't read the blockstate because most of the time this is false, so using the faster
-+ // known transparency lookup results in a net win
-+ this.recalcNeighbourPos.set(offX, offY, offZ);
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
-+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
-+ // not allowed to propagate
-+ continue;
-+ }
-+ }
-+
-+ // passed transparency,
-+
-+ final int calculated = neighbourLevel - opacity;
-+ level = Math.max(calculated, level);
-+ if (level > expect) {
-+ return level;
-+ }
-+ }
-+
-+ return level;
-+ }
-+
-+ @Override
-+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
-+ for (final BlockPos pos : positions) {
-+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ protected List<BlockPos> getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) {
-+ final List<BlockPos> sources = new ArrayList<>();
-+
-+ final int offX = chunk.getPos().x << 4;
-+ final int offZ = chunk.getPos().z << 4;
-+
-+ final 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()) {
-+ // no sources in empty sections
-+ continue;
-+ }
-+ if (!section.maybeHas((final BlockState state) -> {
-+ return state.getLightEmission() > 0;
-+ })) {
-+ // no light sources in palette
-+ continue;
-+ }
-+ final PalettedContainer<BlockState> states = section.states;
-+ final int offY = sectionY << 4;
-+
-+ for (int index = 0; index < (16 * 16 * 16); ++index) {
-+ final BlockState state = states.get(index);
-+ if (state.getLightEmission() <= 0) {
-+ continue;
-+ }
-+
-+ // index = x | (z << 4) | (y << 8)
-+ sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
-+ }
-+ }
-+
-+ return sources;
-+ }
-+
-+ @Override
-+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
-+ // setup sources
-+ 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;
-+
-+ if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) {
-+ // some other source is brighter
-+ continue;
-+ }
-+
-+ this.appendToIncreaseQueue(
-+ ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (emittedLight & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (blockState.isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0)
-+ );
-+
-+
-+ // propagation wont set this for us
-+ this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight);
-+ }
-+
-+ if (needsEdgeChecks) {
-+ // not required to propagate here, but this will reduce the hit of the edge checks
-+ this.performLightIncrease(lightAccess);
-+
-+ // verify neighbour edges
-+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+ } else {
-+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+
-+ this.performLightIncrease(lightAccess);
-+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4ffb4ffe01c4628d52742c5c0bbd35220eea6294
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java
-@@ -0,0 +1,440 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import net.minecraft.world.level.chunk.DataLayer;
-+import java.util.ArrayDeque;
-+import java.util.Arrays;
-+
-+// SWMR -> Single Writer Multi Reader Nibble Array
-+public final class SWMRNibbleArray {
-+
-+ /*
-+ * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null
-+ * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised
-+ * nibbles can be written to.
-+ *
-+ * Uninitialised nibble - They are all 0, but the backing array isn't initialised.
-+ *
-+ * Initialised nibble - Has light data.
-+ */
-+
-+ protected static final int INIT_STATE_NULL = 0; // null
-+ protected static final int INIT_STATE_UNINIT = 1; // uninitialised
-+ protected static final int INIT_STATE_INIT = 2; // initialised
-+ protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL
-+
-+ public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block
-+ // this allows us to maintain only 1 byte array when we're not updating
-+ static final ThreadLocal<ArrayDeque<byte[]>> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new);
-+
-+ private static byte[] allocateBytes() {
-+ final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst();
-+ if (inPool != null) {
-+ return inPool;
-+ }
-+
-+ return new byte[ARRAY_SIZE];
-+ }
-+
-+ private static void freeBytes(final byte[] bytes) {
-+ WORKING_BYTES_POOL.get().addFirst(bytes);
-+ }
-+
-+ public static SWMRNibbleArray fromVanilla(final DataLayer nibble) {
-+ if (nibble == null) {
-+ return new SWMRNibbleArray(null, true);
-+ } else if (nibble.isEmpty()) {
-+ return new SWMRNibbleArray();
-+ } else {
-+ return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later
-+ }
-+ }
-+
-+ protected int stateUpdating;
-+ protected volatile int stateVisible;
-+
-+ protected byte[] storageUpdating;
-+ protected boolean updatingDirty; // only returns whether storageUpdating is dirty
-+ protected volatile byte[] storageVisible;
-+
-+ public SWMRNibbleArray() {
-+ this(null, false); // lazy init
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes) {
-+ this(bytes, false);
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) {
-+ if (bytes != null && bytes.length != ARRAY_SIZE) {
-+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
-+ }
-+ this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT;
-+ this.storageUpdating = this.storageVisible = bytes;
-+ }
-+
-+ public SWMRNibbleArray(final byte[] bytes, final int state) {
-+ if (bytes != null && bytes.length != ARRAY_SIZE) {
-+ throw new IllegalArgumentException("Data of wrong length: " + bytes.length);
-+ }
-+ if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) {
-+ throw new IllegalArgumentException("Data cannot be null and have state be initialised");
-+ }
-+ this.stateUpdating = this.stateVisible = state;
-+ this.storageUpdating = this.storageVisible = bytes;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ StringBuilder stringBuilder = new StringBuilder();
-+ stringBuilder.append("State: ");
-+ switch (this.stateVisible) {
-+ case INIT_STATE_NULL:
-+ stringBuilder.append("null");
-+ break;
-+ case INIT_STATE_UNINIT:
-+ stringBuilder.append("uninitialised");
-+ break;
-+ case INIT_STATE_INIT:
-+ stringBuilder.append("initialised");
-+ break;
-+ case INIT_STATE_HIDDEN:
-+ stringBuilder.append("hidden");
-+ break;
-+ default:
-+ stringBuilder.append("unknown");
-+ break;
-+ }
-+ stringBuilder.append("\nData:\n");
-+
-+ final byte[] data = this.storageVisible;
-+ if (data != null) {
-+ for (int i = 0; i < 4096; ++i) {
-+ // Copied from NibbleArray#toString
-+ final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF);
-+
-+ stringBuilder.append(Integer.toHexString(level));
-+ if ((i & 15) == 15) {
-+ stringBuilder.append("\n");
-+ }
-+
-+ if ((i & 255) == 255) {
-+ stringBuilder.append("\n");
-+ }
-+ }
-+ } else {
-+ stringBuilder.append("null");
-+ }
-+
-+ return stringBuilder.toString();
-+ }
-+
-+ public SaveState getSaveState() {
-+ synchronized (this) {
-+ final int state = this.stateVisible;
-+ final byte[] data = this.storageVisible;
-+ if (state == INIT_STATE_NULL) {
-+ return null;
-+ }
-+ if (state == INIT_STATE_UNINIT) {
-+ return new SaveState(null, state);
-+ }
-+ final boolean zero = isAllZero(data);
-+ if (zero) {
-+ return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null;
-+ } else {
-+ return new SaveState(data.clone(), state);
-+ }
-+ }
-+ }
-+
-+ protected static boolean isAllZero(final byte[] data) {
-+ for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) {
-+ byte whole = data[i << 4];
-+
-+ for (int k = 1; k < (1 << 4); ++k) {
-+ whole |= data[(i << 4) | k];
-+ }
-+
-+ if (whole != 0) {
-+ return false;
-+ }
-+ }
-+
-+ return true;
-+ }
-+
-+ // operation type: updating on src, updating on other
-+ public void extrudeLower(final SWMRNibbleArray other) {
-+ if (other.stateUpdating == INIT_STATE_NULL) {
-+ throw new IllegalArgumentException();
-+ }
-+
-+ if (other.storageUpdating == null) {
-+ this.setUninitialised();
-+ return;
-+ }
-+
-+ final byte[] src = other.storageUpdating;
-+ final byte[] into;
-+
-+ if (!this.updatingDirty) {
-+ if (this.storageUpdating != null) {
-+ into = this.storageUpdating = allocateBytes();
-+ } else {
-+ this.storageUpdating = into = allocateBytes();
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ this.updatingDirty = true;
-+ } else {
-+ into = this.storageUpdating;
-+ }
-+
-+ final int start = 0;
-+ final int end = (15 | (15 << 4)) >>> 1;
-+
-+ /* x | (z << 4) | (y << 8) */
-+ for (int y = 0; y <= 15; ++y) {
-+ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1);
-+ }
-+ }
-+
-+ // operation type: updating
-+ public void setFull() {
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1);
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public void setZero() {
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0);
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public void setNonNull() {
-+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ return;
-+ }
-+ if (this.stateUpdating != INIT_STATE_NULL) {
-+ return;
-+ }
-+ this.stateUpdating = INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: updating
-+ public void setNull() {
-+ this.stateUpdating = INIT_STATE_NULL;
-+ if (this.updatingDirty && this.storageUpdating != null) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = null;
-+ this.updatingDirty = false;
-+ }
-+
-+ // operation type: updating
-+ public void setUninitialised() {
-+ this.stateUpdating = INIT_STATE_UNINIT;
-+ if (this.storageUpdating != null && this.updatingDirty) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = null;
-+ this.updatingDirty = false;
-+ }
-+
-+ // operation type: updating
-+ public void setHidden() {
-+ if (this.stateUpdating == INIT_STATE_HIDDEN) {
-+ return;
-+ }
-+ if (this.stateUpdating != INIT_STATE_INIT) {
-+ this.setNull();
-+ } else {
-+ this.stateUpdating = INIT_STATE_HIDDEN;
-+ }
-+ }
-+
-+ // operation type: updating
-+ public boolean isDirty() {
-+ return this.stateUpdating != this.stateVisible || this.updatingDirty;
-+ }
-+
-+ // operation type: updating
-+ public boolean isNullNibbleUpdating() {
-+ return this.stateUpdating == INIT_STATE_NULL;
-+ }
-+
-+ // operation type: visible
-+ public boolean isNullNibbleVisible() {
-+ return this.stateVisible == INIT_STATE_NULL;
-+ }
-+
-+ // opeartion type: updating
-+ public boolean isUninitialisedUpdating() {
-+ return this.stateUpdating == INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: visible
-+ public boolean isUninitialisedVisible() {
-+ return this.stateVisible == INIT_STATE_UNINIT;
-+ }
-+
-+ // operation type: updating
-+ public boolean isInitialisedUpdating() {
-+ return this.stateUpdating == INIT_STATE_INIT;
-+ }
-+
-+ // operation type: visible
-+ public boolean isInitialisedVisible() {
-+ return this.stateVisible == INIT_STATE_INIT;
-+ }
-+
-+ // operation type: updating
-+ public boolean isHiddenUpdating() {
-+ return this.stateUpdating == INIT_STATE_HIDDEN;
-+ }
-+
-+ // operation type: updating
-+ public boolean isHiddenVisible() {
-+ return this.stateVisible == INIT_STATE_HIDDEN;
-+ }
-+
-+ // operation type: updating
-+ protected void swapUpdatingAndMarkDirty() {
-+ if (this.updatingDirty) {
-+ return;
-+ }
-+
-+ if (this.storageUpdating == null) {
-+ this.storageUpdating = allocateBytes();
-+ Arrays.fill(this.storageUpdating, (byte)0);
-+ } else {
-+ System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE);
-+ }
-+
-+ if (this.stateUpdating != INIT_STATE_HIDDEN) {
-+ this.stateUpdating = INIT_STATE_INIT;
-+ }
-+ this.updatingDirty = true;
-+ }
-+
-+ // operation type: updating
-+ public boolean updateVisible() {
-+ if (!this.isDirty()) {
-+ return false;
-+ }
-+
-+ synchronized (this) {
-+ if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) {
-+ this.storageVisible = null;
-+ } else {
-+ if (this.storageVisible == null) {
-+ this.storageVisible = this.storageUpdating.clone();
-+ } else {
-+ if (this.storageUpdating != this.storageVisible) {
-+ System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE);
-+ }
-+ }
-+
-+ if (this.storageUpdating != this.storageVisible) {
-+ freeBytes(this.storageUpdating);
-+ }
-+ this.storageUpdating = this.storageVisible;
-+ }
-+ this.updatingDirty = false;
-+ this.stateVisible = this.stateUpdating;
-+ }
-+
-+ return true;
-+ }
-+
-+ // operation type: visible
-+ public DataLayer toVanillaNibble() {
-+ synchronized (this) {
-+ switch (this.stateVisible) {
-+ case INIT_STATE_HIDDEN:
-+ case INIT_STATE_NULL:
-+ return null;
-+ case INIT_STATE_UNINIT:
-+ return new DataLayer();
-+ case INIT_STATE_INIT:
-+ return new DataLayer(this.storageVisible.clone());
-+ default:
-+ throw new IllegalStateException();
-+ }
-+ }
-+ }
-+
-+ /* x | (z << 4) | (y << 8) */
-+
-+ // operation type: updating
-+ public int getUpdating(final int x, final int y, final int z) {
-+ return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
-+ }
-+
-+ // operation type: updating
-+ public int getUpdating(final int index) {
-+ // indices range from 0 -> 4096
-+ final byte[] bytes = this.storageUpdating;
-+ if (bytes == null) {
-+ return 0;
-+ }
-+ final byte value = bytes[index >>> 1];
-+
-+ // if we are an even index, we want lower 4 bits
-+ // if we are an odd index, we want upper 4 bits
-+ return ((value >>> ((index & 1) << 2)) & 0xF);
-+ }
-+
-+ // operation type: visible
-+ public int getVisible(final int x, final int y, final int z) {
-+ return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8));
-+ }
-+
-+ // operation type: visible
-+ public int getVisible(final int index) {
-+ // indices range from 0 -> 4096
-+ final byte[] visibleBytes = this.storageVisible;
-+ if (visibleBytes == null) {
-+ return 0;
-+ }
-+ final byte value = visibleBytes[index >>> 1];
-+
-+ // if we are an even index, we want lower 4 bits
-+ // if we are an odd index, we want upper 4 bits
-+ return ((value >>> ((index & 1) << 2)) & 0xF);
-+ }
-+
-+ // operation type: updating
-+ public void set(final int x, final int y, final int z, final int value) {
-+ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value);
-+ }
-+
-+ // operation type: updating
-+ public void set(final int index, final int value) {
-+ if (!this.updatingDirty) {
-+ this.swapUpdatingAndMarkDirty();
-+ }
-+ final int shift = (index & 1) << 2;
-+ final int i = index >>> 1;
-+
-+ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift));
-+ }
-+
-+ public static final class SaveState {
-+
-+ public final byte[] data;
-+ public final int state;
-+
-+ public SaveState(final byte[] data, final int state) {
-+ this.data = data;
-+ this.state = state;
-+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..43a2cce467d29f81ba57d77c03608e57857dd579
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/SkyStarLightEngine.java
-@@ -0,0 +1,709 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import ca.spottedleaf.starlight.common.util.WorldUtil;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortIterator;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.world.level.BlockGetter;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.Arrays;
-+import java.util.Set;
-+
-+public final class SkyStarLightEngine extends StarLightEngine {
-+
-+ /*
-+ Specification for managing the initialisation and de-initialisation of skylight nibble arrays:
-+
-+ Skylight nibble initialisation requires that non-empty chunk sections have 1 radius nibbles non-null.
-+
-+ This presents some problems, as vanilla is only guaranteed to have 0 radius neighbours loaded when editing blocks.
-+ However starlight fixes this so that it has 1 radius loaded. Still, we don't actually have guarantees
-+ that we have the necessary chunks loaded to de-initialise neighbour sections (but we do have enough to de-initialise
-+ our own) - we need a radius of 2 to de-initialise neighbour nibbles.
-+ How do we solve this?
-+
-+ Each chunk will store the last known "emptiness" of sections for each of their 1 radius neighbour chunk sections.
-+ If the chunk does not have full data, then its nibbles are NOT de-initialised. This is because obviously the
-+ chunk did not go through the light stage yet - or its neighbours are not lit. In either case, once the last
-+ known "emptiness" of neighbouring sections is filled with data, the chunk will run a full check of the data
-+ to see if any of its nibbles need to be de-initialised.
-+
-+ The emptiness map allows us to de-initialise neighbour nibbles if the neighbour has it filled with data,
-+ and if it doesn't have data then we know it will correctly de-initialise once it fills up.
-+
-+ Unlike vanilla, we store whether nibbles are uninitialised on disk - so we don't need any dumb hacking
-+ around those.
-+ */
-+
-+ protected final int[] heightMapBlockChange = new int[16 * 16];
-+ {
-+ Arrays.fill(this.heightMapBlockChange, Integer.MIN_VALUE); // clear heightmap
-+ }
-+
-+ protected final boolean[] nullPropagationCheckCache;
-+
-+ public SkyStarLightEngine(final Level world) {
-+ super(true, world);
-+ this.nullPropagationCheckCache = new boolean[WorldUtil.getTotalLightSections(world)];
-+ }
-+
-+ @Override
-+ protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) {
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) {
-+ return;
-+ }
-+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble == null) {
-+ if (!initRemovedNibbles) {
-+ throw new IllegalStateException();
-+ } else {
-+ this.setNibbleInCache(chunkX, chunkY, chunkZ, nibble = new SWMRNibbleArray(null, true));
-+ }
-+ }
-+ this.initNibble(nibble, chunkX, chunkY, chunkZ, extrude);
-+ }
-+
-+ @Override
-+ protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ nibble.setNull();
-+ }
-+ }
-+
-+ protected final void initNibble(final SWMRNibbleArray currNibble, final int chunkX, final int chunkY, final int chunkZ, final boolean extrude) {
-+ if (!currNibble.isNullNibbleUpdating()) {
-+ // already initialised
-+ return;
-+ }
-+
-+ final boolean[] emptinessMap = this.getEmptinessMap(chunkX, chunkZ);
-+
-+ // are we above this chunk's lowest empty section?
-+ int lowestY = this.minLightSection - 1;
-+ for (int currY = this.maxSection; currY >= this.minSection; --currY) {
-+ if (emptinessMap == null) {
-+ // cannot delay nibble init for lit chunks, as we need to init to propagate into them.
-+ final LevelChunkSection current = this.getChunkSection(chunkX, currY, chunkZ);
-+ if (current == null || current.hasOnlyAir()) {
-+ continue;
-+ }
-+ } else {
-+ if (emptinessMap[currY - this.minSection]) {
-+ continue;
-+ }
-+ }
-+
-+ // should always be full lit here
-+ lowestY = currY;
-+ break;
-+ }
-+
-+ if (chunkY > lowestY) {
-+ // we need to set this one to full
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ nibble.setNonNull();
-+ nibble.setFull();
-+ return;
-+ }
-+
-+ if (extrude) {
-+ // this nibble is going to depend solely on the skylight data above it
-+ // find first non-null data above (there does exist one, as we just found it above)
-+ for (int currY = chunkY + 1; currY <= this.maxLightSection; ++currY) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, currY, chunkZ);
-+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
-+ currNibble.setNonNull();
-+ currNibble.extrudeLower(nibble);
-+ break;
-+ }
-+ }
-+ } else {
-+ currNibble.setNonNull();
-+ }
-+ }
-+
-+ protected final void rewriteNibbleCacheForSkylight(final ChunkAccess chunk) {
-+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[index];
-+ if (nibble != null && nibble.isNullNibbleUpdating()) {
-+ // stop propagation in these areas
-+ this.nibbleCache[index] = null;
-+ nibble.updateVisible();
-+ }
-+ }
-+ }
-+
-+ // rets whether neighbours were init'd
-+
-+ protected final boolean checkNullSection(final int chunkX, final int chunkY, final int chunkZ,
-+ final boolean extrudeInitialised) {
-+ // null chunk sections may have nibble neighbours in the horizontal 1 radius that are
-+ // non-null. Propagation to these neighbours is necessary.
-+ // What makes this easy is we know none of these neighbours are non-empty (otherwise
-+ // this nibble would be initialised). So, we don't have to initialise
-+ // the neighbours in the full 1 radius, because there's no worry that any "paths"
-+ // to the neighbours on this horizontal plane are blocked.
-+ if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.nullPropagationCheckCache[chunkY - this.minLightSection]) {
-+ return false;
-+ }
-+ this.nullPropagationCheckCache[chunkY - this.minLightSection] = true;
-+
-+ // check horizontal neighbours
-+ boolean needInitNeighbours = false;
-+ neighbour_search:
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, chunkY, dz + chunkZ);
-+ if (nibble != null && !nibble.isNullNibbleUpdating()) {
-+ needInitNeighbours = true;
-+ break neighbour_search;
-+ }
-+ }
-+ }
-+
-+ if (needInitNeighbours) {
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ this.initNibble(dx + chunkX, chunkY, dz + chunkZ, (dx | dz) == 0 ? extrudeInitialised : true, true);
-+ }
-+ }
-+ }
-+
-+ return needInitNeighbours;
-+ }
-+
-+ protected final int getLightLevelExtruded(final int worldX, final int worldY, final int worldZ) {
-+ final int chunkX = worldX >> 4;
-+ int chunkY = worldY >> 4;
-+ final int chunkZ = worldZ >> 4;
-+
-+ SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (nibble != null) {
-+ return nibble.getUpdating(worldX, worldY, worldZ);
-+ }
-+
-+ for (;;) {
-+ if (++chunkY > this.maxLightSection) {
-+ return 15;
-+ }
-+
-+ nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+
-+ if (nibble != null) {
-+ return nibble.getUpdating(worldX, 0, worldZ);
-+ }
-+ }
-+ }
-+
-+ @Override
-+ protected boolean[] getEmptinessMap(final ChunkAccess chunk) {
-+ return chunk.getSkyEmptinessMap();
-+ }
-+
-+ @Override
-+ protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) {
-+ chunk.setSkyEmptinessMap(to);
-+ }
-+
-+ @Override
-+ protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) {
-+ return chunk.getSkyNibbles();
-+ }
-+
-+ @Override
-+ protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) {
-+ chunk.setSkyNibbles(to);
-+ }
-+
-+ @Override
-+ protected boolean canUseChunk(final ChunkAccess chunk) {
-+ // can only use chunks for sky stuff if their sections have been init'd
-+ return chunk.getStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect());
-+ }
-+
-+ @Override
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection,
-+ final int toSection) {
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ for (int y = toSection; y >= fromSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, true);
-+ }
-+
-+ super.checkChunkEdges(lightAccess, chunk, fromSection, toSection);
-+ }
-+
-+ @Override
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
-+ final int y = (int)iterator.nextShort();
-+ this.checkNullSection(chunkX, y, chunkZ, true);
-+ }
-+
-+ super.checkChunkEdges(lightAccess, chunk, sections);
-+ }
-+
-+ @Override
-+ protected void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) {
-+ // blocks can change opacity
-+ // blocks can change direction of propagation
-+
-+ // same logic applies from BlockStarLightEngine#checkBlock
-+
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
-+
-+ if (currentLevel == 15) {
-+ // must re-propagate clobbered source
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent
-+ );
-+ } else {
-+ this.setLightLevel(worldX, worldY, worldZ, 0);
-+ }
-+
-+ this.appendToDecreaseQueue(
-+ ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (currentLevel & 0xFL) << (6 + 6 + 16)
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ );
-+ }
-+
-+ protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos();
-+
-+ @Override
-+ protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect) {
-+ if (expect == 15) {
-+ return expect;
-+ }
-+
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final BlockState centerState = this.getBlockState(worldX, worldY, worldZ);
-+ int opacity = centerState.getOpacityIfCached();
-+
-+ final BlockState conditionallyOpaqueState;
-+ if (opacity < 0) {
-+ this.recalcCenterPos.set(worldX, worldY, worldZ);
-+ opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos));
-+ if (centerState.isConditionallyFullOpaque()) {
-+ conditionallyOpaqueState = centerState;
-+ } else {
-+ conditionallyOpaqueState = null;
-+ }
-+ } else {
-+ conditionallyOpaqueState = null;
-+ opacity = Math.max(1, opacity);
-+ }
-+
-+ int level = 0;
-+
-+ for (final AxisDirection direction : AXIS_DIRECTIONS) {
-+ final int offX = worldX + direction.x;
-+ final int offY = worldY + direction.y;
-+ final int offZ = worldZ + direction.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+
-+ final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8));
-+
-+ if ((neighbourLevel - 1) <= level) {
-+ // don't need to test transparency, we know it wont affect the result.
-+ continue;
-+ }
-+
-+ final BlockState neighbourState = this.getBlockState(offX, offY, offZ);
-+
-+ if (neighbourState.isConditionallyFullOpaque()) {
-+ // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that
-+ // we don't read the blockstate because most of the time this is false, so using the faster
-+ // known transparency lookup results in a net win
-+ this.recalcNeighbourPos.set(offX, offY, offZ);
-+ final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcNeighbourPos, direction.opposite.nms);
-+ final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(lightAccess.getLevel(), this.recalcCenterPos, direction.nms);
-+ if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) {
-+ // not allowed to propagate
-+ continue;
-+ }
-+ }
-+
-+ final int calculated = neighbourLevel - opacity;
-+ level = Math.max(calculated, level);
-+ if (level > expect) {
-+ return level;
-+ }
-+ }
-+
-+ return level;
-+ }
-+
-+ @Override
-+ protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions) {
-+ this.rewriteNibbleCacheForSkylight(atChunk);
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+
-+ final BlockGetter world = lightAccess.getLevel();
-+ final int chunkX = atChunk.getPos().x;
-+ final int chunkZ = atChunk.getPos().z;
-+ final int heightMapOffset = chunkX * -16 + (chunkZ * (-16 * 16));
-+
-+ // setup heightmap for changes
-+ for (final BlockPos pos : positions) {
-+ final int index = pos.getX() + (pos.getZ() << 4) + heightMapOffset;
-+ final int curr = this.heightMapBlockChange[index];
-+ if (pos.getY() > curr) {
-+ this.heightMapBlockChange[index] = pos.getY();
-+ }
-+ }
-+
-+ // note: light sets are delayed while processing skylight source changes due to how
-+ // nibbles are initialised, as we want to avoid clobbering nibble values so what when
-+ // below nibbles are initialised they aren't reading from partially modified nibbles
-+
-+ // now we can recalculate the sources for the changed columns
-+ for (int index = 0; index < (16 * 16); ++index) {
-+ final int maxY = this.heightMapBlockChange[index];
-+ if (maxY == Integer.MIN_VALUE) {
-+ // not changed
-+ continue;
-+ }
-+ this.heightMapBlockChange[index] = Integer.MIN_VALUE; // restore default for next caller
-+
-+ final int columnX = (index & 15) | (chunkX << 4);
-+ final int columnZ = (index >>> 4) | (chunkZ << 4);
-+
-+ // try and propagate from the above y
-+ // delay light set until after processing all sources to setup
-+ final int maxPropagationY = this.tryPropagateSkylight(world, columnX, maxY, columnZ, true, true);
-+
-+ // maxPropagationY is now the highest block that could not be propagated to
-+
-+ // remove all sources below that are 15
-+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection;
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ if (this.getLightLevelExtruded(columnX, maxPropagationY, columnZ) == 15) {
-+ // ensure section is checked
-+ this.checkNullSection(columnX >> 4, maxPropagationY >> 4, columnZ >> 4, true);
-+
-+ for (int currY = maxPropagationY; currY >= (this.minLightSection << 4); --currY) {
-+ if ((currY & 15) == 15) {
-+ // ensure section is checked
-+ this.checkNullSection(columnX >> 4, (currY >> 4), columnZ >> 4, true);
-+ }
-+
-+ // ensure section below is always checked
-+ final SWMRNibbleArray nibble = this.getNibbleFromCache(columnX >> 4, currY >> 4, columnZ >> 4);
-+ if (nibble == null) {
-+ // advance currY to the the top of the section below
-+ currY = (currY) & (~15);
-+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
-+ // end up there
-+ continue;
-+ }
-+
-+ if (nibble.getUpdating(columnX, currY, columnZ) != 15) {
-+ break;
-+ }
-+
-+ // delay light set until after processing all sources to setup
-+ this.appendToDecreaseQueue(
-+ ((columnX + (columnZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16))
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ // do not set transparent blocks for the same reason we don't in the checkBlock method
-+ );
-+ }
-+ }
-+ }
-+
-+ // delayed light sets are processed here, and must be processed before checkBlock as checkBlock reads
-+ // immediate light value
-+ this.processDelayedIncreases();
-+ this.processDelayedDecreases();
-+
-+ for (final BlockPos pos : positions) {
-+ this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ protected final int[] heightMapGen = new int[32 * 32];
-+
-+ @Override
-+ protected void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
-+ this.rewriteNibbleCacheForSkylight(chunk);
-+ Arrays.fill(this.nullPropagationCheckCache, false);
-+
-+ final BlockGetter world = lightAccess.getLevel();
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ final LevelChunkSection[] sections = chunk.getSections();
-+
-+ int highestNonEmptySection = this.maxSection;
-+ while (highestNonEmptySection == (this.minSection - 1) ||
-+ sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].hasOnlyAir()) {
-+ this.checkNullSection(chunkX, highestNonEmptySection, chunkZ, false);
-+ // try propagate FULL to neighbours
-+
-+ // check neighbours to see if we need to propagate into them
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourX = chunkX + direction.x;
-+ final int neighbourZ = chunkZ + direction.z;
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(neighbourX, highestNonEmptySection, neighbourZ);
-+ if (neighbourNibble == null) {
-+ // unloaded neighbour
-+ // most of the time we fall here
-+ continue;
-+ }
-+
-+ // it looks like we need to propagate into the neighbour
-+
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (direction.x != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = chunkX << 4;
-+ } else {
-+ startX = chunkX << 4 | 15;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (direction.z < 0) {
-+ // negative
-+ startZ = chunkZ << 4;
-+ } else {
-+ startZ = chunkZ << 4 | 15;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ final int encodeOffset = this.coordinateOffset;
-+ final long propagateDirection = 1L << direction.ordinal(); // we only want to check in this direction
-+
-+ for (int currY = highestNonEmptySection << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ this.appendToIncreaseQueue(
-+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ // no transparent flag, we know for a fact there are no blocks here that could be directionally transparent (as the section is EMPTY)
-+ );
-+ }
-+ }
-+ }
-+
-+ if (highestNonEmptySection-- == (this.minSection - 1)) {
-+ break;
-+ }
-+ }
-+
-+ if (highestNonEmptySection >= this.minSection) {
-+ // fill out our other sources
-+ final int minX = chunkPos.x << 4;
-+ final int maxX = chunkPos.x << 4 | 15;
-+ final int minZ = chunkPos.z << 4;
-+ final int maxZ = chunkPos.z << 4 | 15;
-+ final int startY = highestNonEmptySection << 4 | 15;
-+ for (int currZ = minZ; currZ <= maxZ; ++currZ) {
-+ for (int currX = minX; currX <= maxX; ++currX) {
-+ this.tryPropagateSkylight(world, currX, startY + 1, currZ, false, false);
-+ }
-+ }
-+ } // else: apparently the chunk is empty
-+
-+ if (needsEdgeChecks) {
-+ // not required to propagate here, but this will reduce the hit of the edge checks
-+ this.performLightIncrease(lightAccess);
-+
-+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, false);
-+ }
-+ // no need to rewrite the nibble cache again
-+ super.checkChunkEdges(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
-+ } else {
-+ for (int y = highestNonEmptySection; y >= this.minLightSection; --y) {
-+ this.checkNullSection(chunkX, y, chunkZ, false);
-+ }
-+ this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, highestNonEmptySection);
-+
-+ this.performLightIncrease(lightAccess);
-+ }
-+ }
-+
-+ protected final void processDelayedIncreases() {
-+ // copied from performLightIncrease
-+ final long[] queue = this.increaseQueue;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+
-+ for (int i = 0, len = this.increaseQueueInitialLength; i < len; ++i) {
-+ final long queueValue = queue[i];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
-+
-+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
-+ }
-+ }
-+
-+ protected final void processDelayedDecreases() {
-+ // copied from performLightDecrease
-+ final long[] queue = this.decreaseQueue;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+
-+ for (int i = 0, len = this.decreaseQueueInitialLength; i < len; ++i) {
-+ final long queueValue = queue[i];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+
-+ this.setLightLevel(posX, posY, posZ, 0);
-+ }
-+ }
-+
-+ // delaying the light set is useful for block changes since they need to worry about initialising nibblearrays
-+ // while also queueing light at the same time (initialising nibblearrays might depend on nibbles above, so
-+ // clobbering the light values will result in broken propagation)
-+ protected final int tryPropagateSkylight(final BlockGetter world, final int worldX, int startY, final int worldZ,
-+ final boolean extrudeInitialised, final boolean delayLightSet) {
-+ final BlockPos.MutableBlockPos mutablePos = this.mutablePos3;
-+ final int encodeOffset = this.coordinateOffset;
-+ final long propagateDirection = AxisDirection.POSITIVE_Y.everythingButThisDirection; // just don't check upwards.
-+
-+ if (this.getLightLevelExtruded(worldX, startY + 1, worldZ) != 15) {
-+ return startY;
-+ }
-+
-+ // ensure this section is always checked
-+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
-+
-+ BlockState above = this.getBlockState(worldX, startY + 1, worldZ);
-+
-+ for (;startY >= (this.minLightSection << 4); --startY) {
-+ if ((startY & 15) == 15) {
-+ // ensure this section is always checked
-+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
-+ }
-+ final BlockState current = this.getBlockState(worldX, startY, worldZ);
-+
-+ final VoxelShape fromShape;
-+ if (above.isConditionallyFullOpaque()) {
-+ this.mutablePos2.set(worldX, startY + 1, worldZ);
-+ fromShape = above.getFaceOcclusionShape(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms);
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ // above wont let us propagate
-+ break;
-+ }
-+ } else {
-+ fromShape = Shapes.empty();
-+ }
-+
-+ final int opacityIfCached = current.getOpacityIfCached();
-+ // does light propagate from the top down?
-+ if (opacityIfCached != -1) {
-+ if (opacityIfCached != 0) {
-+ // we cannot propagate 15 through this
-+ break;
-+ }
-+ // most of the time it falls here.
-+ // add to propagate
-+ // light set delayed until we determine if this nibble section is null
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ );
-+ } else {
-+ mutablePos.set(worldX, startY, worldZ);
-+ long flags = 0L;
-+ if (current.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = current.getFaceOcclusionShape(world, mutablePos, AxisDirection.POSITIVE_Y.nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ // can't propagate here, we're done on this column.
-+ break;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = current.getLightBlock(world, mutablePos);
-+ if (opacity > 0) {
-+ // let the queued value (if any) handle it from here.
-+ break;
-+ }
-+
-+ // light set delayed until we determine if this nibble section is null
-+ this.appendToIncreaseQueue(
-+ ((worldX + (worldZ << 6) + (startY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | (15L << (6 + 6 + 16)) // we know we're at full lit here
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ | flags
-+ );
-+ }
-+
-+ above = current;
-+
-+ if (this.getNibbleFromCache(worldX >> 4, startY >> 4, worldZ >> 4) == null) {
-+ // we skip empty sections here, as this is just an easy way of making sure the above block
-+ // can propagate through air.
-+
-+ // nothing can propagate in null sections, remove the queue entry for it
-+ --this.increaseQueueInitialLength;
-+
-+ // advance currY to the the top of the section below
-+ startY = (startY) & (~15);
-+ // note: this value ^ is actually 1 above the top, but the loop decrements by 1 so we actually
-+ // end up there
-+
-+ // make sure this is marked as AIR
-+ above = AIR_BLOCK_STATE;
-+ } else if (!delayLightSet) {
-+ this.setLightLevel(worldX, startY, worldZ, 15);
-+ }
-+ }
-+
-+ return startY;
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..ad1eeebe6de219143492b94da309cb54ae9e0a5b
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightEngine.java
-@@ -0,0 +1,1572 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import ca.spottedleaf.starlight.common.util.CoordinateUtils;
-+import ca.spottedleaf.starlight.common.util.IntegerUtil;
-+import ca.spottedleaf.starlight.common.util.WorldUtil;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortIterator;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.Direction;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.world.level.BlockGetter;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.LevelHeightAccessor;
-+import net.minecraft.world.level.LightLayer;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.state.BlockState;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.LevelChunkSection;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.phys.shapes.Shapes;
-+import net.minecraft.world.phys.shapes.VoxelShape;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.function.Consumer;
-+import java.util.function.IntConsumer;
-+
-+public abstract class StarLightEngine {
-+
-+ protected static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState();
-+
-+ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values();
-+ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS;
-+ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] {
-+ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X,
-+ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z
-+ };
-+
-+ protected static enum AxisDirection {
-+
-+ // Declaration order is important and relied upon. Do not change without modifying propagation code.
-+ POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0),
-+ POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1),
-+ POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0);
-+
-+ static {
-+ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X;
-+ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z;
-+ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y;
-+ }
-+
-+ protected AxisDirection opposite;
-+
-+ public final int x;
-+ public final int y;
-+ public final int z;
-+ public final Direction nms;
-+ public final long everythingButThisDirection;
-+ public final long everythingButTheOppositeDirection;
-+
-+ AxisDirection(final int x, final int y, final int z) {
-+ this.x = x;
-+ this.y = y;
-+ this.z = z;
-+ this.nms = Direction.fromDelta(x, y, z);
-+ this.everythingButThisDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << this.ordinal()));
-+ // positive is always even, negative is always odd. Flip the 1 bit to get the negative direction.
-+ this.everythingButTheOppositeDirection = (long)(ALL_DIRECTIONS_BITSET ^ (1 << (this.ordinal() ^ 1)));
-+ }
-+
-+ public AxisDirection getOpposite() {
-+ return this.opposite;
-+ }
-+ }
-+
-+ // I'd like to thank https://www.seedofandromeda.com/blogs/29-fast-flood-fill-lighting-in-a-blocky-voxel-game-pt-1
-+ // for explaining how light propagates via breadth-first search
-+
-+ // While the above is a good start to understanding the general idea of what the general principles are, it's not
-+ // exactly how the vanilla light engine should behave for minecraft.
-+
-+ // similar to the above, except the chunk section indices vary from [-1, 1], or [0, 2]
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ // null index indicates the chunk section doesn't exist (empty or out of bounds)
-+ protected final LevelChunkSection[] sectionCache;
-+
-+ // the exact same as above, except for storing fast access to SWMRNibbleArray
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ protected final SWMRNibbleArray[] nibbleCache;
-+
-+ // the exact same as above, except for storing fast access to nibbles to call change callbacks for
-+ // for the y chunk section it's from [minLightSection, maxLightSection] or [0, maxLightSection - minLightSection]
-+ // index = x + (z * 5) + (y * 25)
-+ protected final boolean[] notifyUpdateCache;
-+
-+ // always initialsed during start of lighting.
-+ // index = x + (z * 5)
-+ protected final ChunkAccess[] chunkCache = new ChunkAccess[5 * 5];
-+
-+ // index = x + (z * 5)
-+ protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
-+
-+ protected final BlockPos.MutableBlockPos mutablePos1 = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos mutablePos2 = new BlockPos.MutableBlockPos();
-+ protected final BlockPos.MutableBlockPos mutablePos3 = new BlockPos.MutableBlockPos();
-+
-+ protected int encodeOffsetX;
-+ protected int encodeOffsetY;
-+ protected int encodeOffsetZ;
-+
-+ protected int coordinateOffset;
-+
-+ protected int chunkOffsetX;
-+ protected int chunkOffsetY;
-+ protected int chunkOffsetZ;
-+
-+ protected int chunkIndexOffset;
-+ protected int chunkSectionIndexOffset;
-+
-+ protected final boolean skylightPropagator;
-+ protected final int emittedLightMask;
-+ protected final boolean isClientSide;
-+
-+ protected final Level world;
-+ protected final int minLightSection;
-+ protected final int maxLightSection;
-+ protected final int minSection;
-+ protected final int maxSection;
-+
-+ protected StarLightEngine(final boolean skylightPropagator, final Level world) {
-+ this.skylightPropagator = skylightPropagator;
-+ this.emittedLightMask = skylightPropagator ? 0 : 0xF;
-+ this.isClientSide = world.isClientSide;
-+ this.world = world;
-+ this.minLightSection = WorldUtil.getMinLightSection(world);
-+ this.maxLightSection = WorldUtil.getMaxLightSection(world);
-+ this.minSection = WorldUtil.getMinSection(world);
-+ this.maxSection = WorldUtil.getMaxSection(world);
-+
-+ this.sectionCache = new LevelChunkSection[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ this.nibbleCache = new SWMRNibbleArray[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ this.notifyUpdateCache = new boolean[5 * 5 * ((this.maxLightSection - this.minLightSection + 1) + 2)]; // add two extra sections for buffer
-+ }
-+
-+ protected final void setupEncodeOffset(final int centerX, final int centerY, final int centerZ) {
-+ // 31 = center + encodeOffset
-+ this.encodeOffsetX = 31 - centerX;
-+ this.encodeOffsetY = (-(this.minLightSection - 1) << 4); // we want 0 to be the smallest encoded value
-+ this.encodeOffsetZ = 31 - centerZ;
-+
-+ // coordinateIndex = x | (z << 6) | (y << 12)
-+ this.coordinateOffset = this.encodeOffsetX + (this.encodeOffsetZ << 6) + (this.encodeOffsetY << 12);
-+
-+ // 2 = (centerX >> 4) + chunkOffset
-+ this.chunkOffsetX = 2 - (centerX >> 4);
-+ this.chunkOffsetY = -(this.minLightSection - 1); // lowest should be 0
-+ this.chunkOffsetZ = 2 - (centerZ >> 4);
-+
-+ // chunk index = x + (5 * z)
-+ this.chunkIndexOffset = this.chunkOffsetX + (5 * this.chunkOffsetZ);
-+
-+ // chunk section index = x + (5 * z) + ((5*5) * y)
-+ this.chunkSectionIndexOffset = this.chunkIndexOffset + ((5 * 5) * this.chunkOffsetY);
-+ }
-+
-+ protected final void setupCaches(final LightChunkGetter chunkProvider, final int centerX, final int centerY, final int centerZ,
-+ final boolean relaxed, final boolean tryToLoadChunksFor2Radius) {
-+ final int centerChunkX = centerX >> 4;
-+ final int centerChunkY = centerY >> 4;
-+ final int centerChunkZ = centerZ >> 4;
-+
-+ this.setupEncodeOffset(centerChunkX * 16 + 7, centerChunkY * 16 + 7, centerChunkZ * 16 + 7);
-+
-+ final int radius = tryToLoadChunksFor2Radius ? 2 : 1;
-+
-+ for (int dz = -radius; dz <= radius; ++dz) {
-+ for (int dx = -radius; dx <= radius; ++dx) {
-+ final int cx = centerChunkX + dx;
-+ final int cz = centerChunkZ + dz;
-+ final boolean isTwoRadius = Math.max(IntegerUtil.branchlessAbs(dx), IntegerUtil.branchlessAbs(dz)) == 2;
-+ final ChunkAccess chunk = (ChunkAccess)chunkProvider.getChunkForLighting(cx, cz);
-+
-+ if (chunk == null) {
-+ if (relaxed | isTwoRadius) {
-+ continue;
-+ }
-+ throw new IllegalArgumentException("Trying to propagate light update before 1 radius neighbours ready");
-+ }
-+
-+ if (!this.canUseChunk(chunk)) {
-+ continue;
-+ }
-+
-+ this.setChunkInCache(cx, cz, chunk);
-+ this.setEmptinessMapCache(cx, cz, this.getEmptinessMap(chunk));
-+ if (!isTwoRadius) {
-+ this.setBlocksForChunkInCache(cx, cz, chunk.getSections());
-+ this.setNibblesForChunkInCache(cx, cz, this.getNibblesOnChunk(chunk));
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final ChunkAccess getChunkInCache(final int chunkX, final int chunkZ) {
-+ return this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
-+ }
-+
-+ protected final void setChunkInCache(final int chunkX, final int chunkZ, final ChunkAccess chunk) {
-+ this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk;
-+ }
-+
-+ protected final LevelChunkSection getChunkSection(final int chunkX, final int chunkY, final int chunkZ) {
-+ return this.sectionCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
-+ }
-+
-+ protected final void setChunkSectionInCache(final int chunkX, final int chunkY, final int chunkZ, final LevelChunkSection section) {
-+ this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section;
-+ }
-+
-+ protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final LevelChunkSection[] sections) {
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ this.setChunkSectionInCache(chunkX, cy, chunkZ,
-+ sections == null ? null : (cy >= this.minSection && cy <= this.maxSection ? sections[cy - this.minSection] : null));
-+ }
-+ }
-+
-+ protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) {
-+ return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
-+ }
-+
-+ protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) {
-+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1];
-+
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset];
-+ }
-+
-+ return ret;
-+ }
-+
-+ protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) {
-+ this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble;
-+ }
-+
-+ protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) {
-+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
-+ this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]);
-+ }
-+ }
-+
-+ protected final void updateVisible(final LightChunkGetter lightAccess) {
-+ for (int index = 0, max = this.nibbleCache.length; index < max; ++index) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[index];
-+ if (!this.notifyUpdateCache[index] && (nibble == null || !nibble.isDirty())) {
-+ continue;
-+ }
-+
-+ final int chunkX = (index % 5) - this.chunkOffsetX;
-+ final int chunkZ = ((index / 5) % 5) - this.chunkOffsetZ;
-+ final int ySections = (this.maxSection - this.minSection) + 1;
-+ final int chunkY = ((index / (5*5)) % (ySections + 2 + 2)) - this.chunkOffsetY;
-+ if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) {
-+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, chunkY, chunkZ));
-+ }
-+ }
-+ }
-+
-+ protected final void destroyCaches() {
-+ Arrays.fill(this.sectionCache, null);
-+ Arrays.fill(this.nibbleCache, null);
-+ Arrays.fill(this.chunkCache, null);
-+ Arrays.fill(this.emptinessMapCache, null);
-+ if (this.isClientSide) {
-+ Arrays.fill(this.notifyUpdateCache, false);
-+ }
-+ }
-+
-+ protected final BlockState getBlockState(final int worldX, final int worldY, final int worldZ) {
-+ final LevelChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
-+
-+ if (section != null) {
-+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15);
-+ }
-+
-+ return AIR_BLOCK_STATE;
-+ }
-+
-+ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) {
-+ final LevelChunkSection section = this.sectionCache[sectionIndex];
-+
-+ if (section != null) {
-+ return section.hasOnlyAir() ? AIR_BLOCK_STATE : section.states.get(localIndex);
-+ }
-+
-+ return AIR_BLOCK_STATE;
-+ }
-+
-+ protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
-+
-+ return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8));
-+ }
-+
-+ protected final int getLightLevel(final int sectionIndex, final int localIndex) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ return nibble == null ? 0 : nibble.getUpdating(localIndex);
-+ }
-+
-+ protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) {
-+ final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset;
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ if (nibble != null) {
-+ nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level);
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) {
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) {
-+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
-+
-+ if (nibble != null) {
-+ nibble.set(localIndex, level);
-+ if (this.isClientSide) {
-+ int cx1 = (worldX - 1) >> 4;
-+ int cx2 = (worldX + 1) >> 4;
-+ int cy1 = (worldY - 1) >> 4;
-+ int cy2 = (worldY + 1) >> 4;
-+ int cz1 = (worldZ - 1) >> 4;
-+ int cz2 = (worldZ + 1) >> 4;
-+ for (int x = cx1; x <= cx2; ++x) {
-+ for (int y = cy1; y <= cy2; ++y) {
-+ for (int z = cz1; z <= cz2; ++z) {
-+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
-+ }
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) {
-+ return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
-+ }
-+
-+ protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) {
-+ this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap;
-+ }
-+
-+ public static SWMRNibbleArray[] getFilledEmptyLight(final LevelHeightAccessor world) {
-+ return getFilledEmptyLight(WorldUtil.getTotalLightSections(world));
-+ }
-+
-+ private static SWMRNibbleArray[] getFilledEmptyLight(final int totalLightSections) {
-+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[totalLightSections];
-+
-+ for (int i = 0, len = ret.length; i < len; ++i) {
-+ ret[i] = new SWMRNibbleArray(null, true);
-+ }
-+
-+ return ret;
-+ }
-+
-+ protected abstract boolean[] getEmptinessMap(final ChunkAccess chunk);
-+
-+ protected abstract void setEmptinessMap(final ChunkAccess chunk, final boolean[] to);
-+
-+ protected abstract SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk);
-+
-+ protected abstract void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to);
-+
-+ protected abstract boolean canUseChunk(final ChunkAccess chunk);
-+
-+ public final void blocksChangedInChunk(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
-+ final Set<BlockPos> positions, final Boolean[] changedSections) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ if (changedSections != null) {
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, changedSections, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ }
-+ if (!positions.isEmpty()) {
-+ this.propagateBlockChanges(lightAccess, chunk, positions);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ protected abstract void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set<BlockPos> positions);
-+
-+ protected abstract void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ);
-+
-+ // if ret > expect, then the real value is at least ret (early returns if ret > expect, rather than calculating actual)
-+ // if ret == expect, then expect is the correct light value for pos
-+ // if ret < expect, then ret is the real light value
-+ protected abstract int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ,
-+ final int expect);
-+
-+ protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16];
-+ protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16];
-+
-+ protected void checkChunkEdge(final LightChunkGetter lightAccess, final ChunkAccess chunk,
-+ final int chunkX, final int chunkY, final int chunkZ) {
-+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ);
-+ if (currNibble == null) {
-+ return;
-+ }
-+
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourOffX = direction.x;
-+ final int neighbourOffZ = direction.z;
-+
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
-+ chunkY, chunkZ + neighbourOffZ);
-+
-+ if (neighbourNibble == null) {
-+ continue;
-+ }
-+
-+ if (!currNibble.isInitialisedUpdating() && !neighbourNibble.isInitialisedUpdating()) {
-+ // both are zero, nothing to check.
-+ continue;
-+ }
-+
-+ // this chunk
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (neighbourOffX != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = chunkX << 4;
-+ } else {
-+ startX = chunkX << 4 | 15;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (neighbourOffZ < 0) {
-+ // negative
-+ startZ = chunkZ << 4;
-+ } else {
-+ startZ = chunkZ << 4 | 15;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ int centerDelayedChecks = 0;
-+ int neighbourDelayedChecks = 0;
-+ for (int currY = chunkY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ final int neighbourX = currX + neighbourOffX;
-+ final int neighbourZ = currZ + neighbourOffZ;
-+
-+ final int currentIndex = (currX & 15) |
-+ ((currZ & 15)) << 4 |
-+ ((currY & 15) << 8);
-+ final int currentLevel = currNibble.getUpdating(currentIndex);
-+
-+ final int neighbourIndex =
-+ (neighbourX & 15) |
-+ ((neighbourZ & 15)) << 4 |
-+ ((currY & 15) << 8);
-+ final int neighbourLevel = neighbourNibble.getUpdating(neighbourIndex);
-+
-+ // the checks are delayed because the checkBlock method clobbers light values - which then
-+ // affect later calculate light value operations. While they don't affect it in a behaviourly significant
-+ // way, they do have a negative performance impact due to simply queueing more values
-+
-+ if (this.calculateLightValue(lightAccess, currX, currY, currZ, currentLevel) != currentLevel) {
-+ this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex;
-+ }
-+
-+ if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel) != neighbourLevel) {
-+ this.chunkCheckDelayedUpdatesNeighbour[neighbourDelayedChecks++] = neighbourIndex;
-+ }
-+ }
-+ }
-+
-+ final int currentChunkOffX = chunkX << 4;
-+ final int currentChunkOffZ = chunkZ << 4;
-+ final int neighbourChunkOffX = (chunkX + direction.x) << 4;
-+ final int neighbourChunkOffZ = (chunkZ + direction.z) << 4;
-+ final int chunkOffY = chunkY << 4;
-+ for (int i = 0, len = Math.max(centerDelayedChecks, neighbourDelayedChecks); i < len; ++i) {
-+ // try to queue neighbouring data together
-+ // index = x | (z << 4) | (y << 8)
-+ if (i < centerDelayedChecks) {
-+ final int value = this.chunkCheckDelayedUpdatesCenter[i];
-+ this.checkBlock(lightAccess, currentChunkOffX | (value & 15),
-+ chunkOffY | (value >>> 8),
-+ currentChunkOffZ | ((value >>> 4) & 0xF));
-+ }
-+ if (i < neighbourDelayedChecks) {
-+ final int value = this.chunkCheckDelayedUpdatesNeighbour[i];
-+ this.checkBlock(lightAccess, neighbourChunkOffX | (value & 15),
-+ chunkOffY | (value >>> 8),
-+ neighbourChunkOffZ | ((value >>> 4) & 0xF));
-+ }
-+ }
-+ }
-+ }
-+
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final ShortCollection sections) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) {
-+ this.checkChunkEdge(lightAccess, chunk, chunkX, iterator.nextShort(), chunkZ);
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // verifies that light levels on this chunks edges are consistent with this chunk's neighbours
-+ // edges. if they are not, they are decreased (effectively performing the logic in checkBlock).
-+ // This does not resolve skylight source problems.
-+ protected void checkChunkEdges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
-+ this.checkChunkEdge(lightAccess, chunk, chunkX, currSectionY, chunkZ);
-+ }
-+
-+ this.performLightDecrease(lightAccess);
-+ }
-+
-+ // pulls light from neighbours, and adds them into the increase queue. does not actually propagate.
-+ protected final void propagateNeighbourLevels(final LightChunkGetter lightAccess, final ChunkAccess chunk, final int fromSection, final int toSection) {
-+ final ChunkPos chunkPos = chunk.getPos();
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+
-+ for (int currSectionY = toSection; currSectionY >= fromSection; --currSectionY) {
-+ final SWMRNibbleArray currNibble = this.getNibbleFromCache(chunkX, currSectionY, chunkZ);
-+ if (currNibble == null) {
-+ continue;
-+ }
-+ for (final AxisDirection direction : ONLY_HORIZONTAL_DIRECTIONS) {
-+ final int neighbourOffX = direction.x;
-+ final int neighbourOffZ = direction.z;
-+
-+ final SWMRNibbleArray neighbourNibble = this.getNibbleFromCache(chunkX + neighbourOffX,
-+ currSectionY, chunkZ + neighbourOffZ);
-+
-+ if (neighbourNibble == null || !neighbourNibble.isInitialisedUpdating()) {
-+ // can't pull from 0
-+ continue;
-+ }
-+
-+ // neighbour chunk
-+ final int incX;
-+ final int incZ;
-+ final int startX;
-+ final int startZ;
-+
-+ if (neighbourOffX != 0) {
-+ // x direction
-+ incX = 0;
-+ incZ = 1;
-+
-+ if (direction.x < 0) {
-+ // negative
-+ startX = (chunkX << 4) - 1;
-+ } else {
-+ startX = (chunkX << 4) + 16;
-+ }
-+ startZ = chunkZ << 4;
-+ } else {
-+ // z direction
-+ incX = 1;
-+ incZ = 0;
-+
-+ if (neighbourOffZ < 0) {
-+ // negative
-+ startZ = (chunkZ << 4) - 1;
-+ } else {
-+ startZ = (chunkZ << 4) + 16;
-+ }
-+ startX = chunkX << 4;
-+ }
-+
-+ final long propagateDirection = 1L << direction.getOpposite().ordinal(); // we only want to check in this direction towards this chunk
-+ final int encodeOffset = this.coordinateOffset;
-+
-+ for (int currY = currSectionY << 4, maxY = currY | 15; currY <= maxY; ++currY) {
-+ for (int i = 0, currX = startX, currZ = startZ; i < 16; ++i, currX += incX, currZ += incZ) {
-+ final int level = neighbourNibble.getUpdating(
-+ (currX & 15)
-+ | ((currZ & 15) << 4)
-+ | ((currY & 15) << 8)
-+ );
-+
-+ if (level <= 1) {
-+ // nothing to propagate
-+ continue;
-+ }
-+
-+ this.appendToIncreaseQueue(
-+ ((currX + (currZ << 6) + (currY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((level & 0xFL) << (6 + 6 + 16))
-+ | (propagateDirection << (6 + 6 + 16 + 4))
-+ | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the current block is transparent, must check.
-+ );
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ public static Boolean[] getEmptySectionsForChunk(final ChunkAccess chunk) {
-+ final LevelChunkSection[] sections = chunk.getSections();
-+ final Boolean[] ret = new Boolean[sections.length];
-+
-+ for (int i = 0; i < sections.length; ++i) {
-+ if (sections[i] == null || sections[i].hasOnlyAir()) {
-+ ret[i] = Boolean.TRUE;
-+ } else {
-+ ret[i] = Boolean.FALSE;
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public final void forceHandleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptinessChanges) {
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ // force current chunk into cache
-+ this.setChunkInCache(chunkX, chunkZ, chunk);
-+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
-+ this.setNibblesForChunkInCache(chunkX, chunkZ, this.getNibblesOnChunk(chunk));
-+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
-+
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void handleEmptySectionChanges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ,
-+ final Boolean[] emptinessChanges) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptinessChanges, false);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ protected abstract void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles);
-+
-+ protected abstract void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ);
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // subclasses are guaranteed that this is always called before a changed block set
-+ // newChunk specifies whether the changes describe a "first load" of a chunk or changes to existing, already loaded chunks
-+ // rets non-null when the emptiness map changed and needs to be updated
-+ protected final boolean[] handleEmptySectionChanges(final LightChunkGetter lightAccess, final ChunkAccess chunk,
-+ final Boolean[] emptinessChanges, final boolean unlit) {
-+ final Level world = (Level)lightAccess.getLevel();
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+
-+ boolean[] chunkEmptinessMap = this.getEmptinessMap(chunkX, chunkZ);
-+ boolean[] ret = null;
-+ final boolean needsInit = unlit || chunkEmptinessMap == null;
-+ if (needsInit) {
-+ this.setEmptinessMapCache(chunkX, chunkZ, ret = chunkEmptinessMap = new boolean[WorldUtil.getTotalSections(world)]);
-+ }
-+
-+ // update emptiness map
-+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
-+ Boolean valueBoxed = emptinessChanges[sectionIndex];
-+ if (valueBoxed == null) {
-+ if (!needsInit) {
-+ continue;
-+ }
-+ final LevelChunkSection section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ);
-+ emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE;
-+ }
-+ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue();
-+ }
-+
-+ // now init neighbour nibbles
-+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
-+ final Boolean valueBoxed = emptinessChanges[sectionIndex];
-+ final int sectionY = sectionIndex + this.minSection;
-+ if (valueBoxed == null) {
-+ continue;
-+ }
-+
-+ final boolean empty = valueBoxed.booleanValue();
-+
-+ if (empty) {
-+ continue;
-+ }
-+
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ // if we're not empty, we also need to initialise nibbles
-+ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up
-+ final boolean extrude = (dx | dz) != 0 || !unlit;
-+ for (int dy = 1; dy >= -1; --dy) {
-+ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false);
-+ }
-+ }
-+ }
-+ }
-+
-+ // check for de-init and lazy-init
-+ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running
-+ // init checks.
-+ for (int dz = -1; dz <= 1; ++dz) {
-+ for (int dx = -1; dx <= 1; ++dx) {
-+ // does this neighbour have 1 radius loaded?
-+ boolean neighboursLoaded = true;
-+ neighbour_loaded_search:
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) {
-+ neighboursLoaded = false;
-+ break neighbour_loaded_search;
-+ }
-+ }
-+ }
-+
-+ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) {
-+ // check neighbours to see if we need to de-init this one
-+ boolean allEmpty = true;
-+ neighbour_search:
-+ for (int dy2 = -1; dy2 <= 1; ++dy2) {
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ final int y = sectionY + dy2;
-+ if (y < this.minSection || y > this.maxSection) {
-+ // empty
-+ continue;
-+ }
-+ final boolean[] emptinessMap = this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ);
-+ if (emptinessMap != null) {
-+ if (!emptinessMap[y - this.minSection]) {
-+ allEmpty = false;
-+ break neighbour_search;
-+ }
-+ } else {
-+ final LevelChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ);
-+ if (section != null && !section.hasOnlyAir()) {
-+ allEmpty = false;
-+ break neighbour_search;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ if (allEmpty & neighboursLoaded) {
-+ // can only de-init when neighbours are loaded
-+ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting
-+ // to be correct
-+
-+ // all were empty, so de-init
-+ this.setNibbleNull(dx + chunkX, sectionY, dz + chunkZ);
-+ } else if (!allEmpty) {
-+ // must init
-+ final boolean extrude = (dx | dz) != 0 || !unlit;
-+ this.initNibble(dx + chunkX, sectionY, dz + chunkZ, extrude, false);
-+ }
-+ }
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void checkChunkEdges(final LightChunkGetter lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
-+ try {
-+ final ChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
-+ if (chunk == null) {
-+ return;
-+ }
-+ this.checkChunkEdges(lightAccess, chunk, sections);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // subclasses should not initialise caches, as this will always be done by the super call
-+ // subclasses should not invoke updateVisible, as this will always be done by the super call
-+ // needsEdgeChecks applies when possibly loading vanilla data, which means we need to validate the current
-+ // chunks light values with respect to neighbours
-+ // subclasses should note that the emptiness changes are propagated BEFORE this is called, so this function
-+ // does not need to detect empty chunks itself (and it should do no handling for them either!)
-+ protected abstract void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks);
-+
-+ public final void light(final LightChunkGetter lightAccess, final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final int chunkX = chunk.getPos().x;
-+ final int chunkZ = chunk.getPos().z;
-+ this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
-+
-+ try {
-+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.maxLightSection - this.minLightSection + 1);
-+ // force current chunk into cache
-+ this.setChunkInCache(chunkX, chunkZ, chunk);
-+ this.setBlocksForChunkInCache(chunkX, chunkZ, chunk.getSections());
-+ this.setNibblesForChunkInCache(chunkX, chunkZ, nibbles);
-+ this.setEmptinessMapCache(chunkX, chunkZ, this.getEmptinessMap(chunk));
-+
-+ final boolean[] ret = this.handleEmptySectionChanges(lightAccess, chunk, emptySections, true);
-+ if (ret != null) {
-+ this.setEmptinessMap(chunk, ret);
-+ }
-+ this.lightChunk(lightAccess, chunk, true);
-+ this.setNibbles(chunk, nibbles);
-+ this.updateVisible(lightAccess);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ public final void relightChunks(final LightChunkGetter lightAccess, final Set<ChunkPos> chunks,
-+ final Consumer<ChunkPos> chunkLightCallback, final IntConsumer onComplete) {
-+ // it's recommended for maximum performance that the set is ordered according to a BFS from the center of
-+ // the region of chunks to relight
-+ // it's required that tickets are added for each chunk to keep them loaded
-+ final Long2ObjectOpenHashMap<SWMRNibbleArray[]> nibblesByChunk = new Long2ObjectOpenHashMap<>();
-+ final Long2ObjectOpenHashMap<boolean[]> emptinessMapByChunk = new Long2ObjectOpenHashMap<>();
-+
-+ final int[] neighbourLightOrder = new int[] {
-+ // d = 0
-+ 0, 0,
-+ // d = 1
-+ -1, 0,
-+ 0, -1,
-+ 1, 0,
-+ 0, 1,
-+ // d = 2
-+ -1, 1,
-+ 1, 1,
-+ -1, -1,
-+ 1, -1,
-+ };
-+
-+ int lightCalls = 0;
-+
-+ for (final ChunkPos chunkPos : chunks) {
-+ final int chunkX = chunkPos.x;
-+ final int chunkZ = chunkPos.z;
-+ final ChunkAccess chunk = (ChunkAccess)lightAccess.getChunkForLighting(chunkX, chunkZ);
-+ if (chunk == null || !this.canUseChunk(chunk)) {
-+ throw new IllegalStateException();
-+ }
-+
-+ for (int i = 0, len = neighbourLightOrder.length; i < len; i += 2) {
-+ final int dx = neighbourLightOrder[i];
-+ final int dz = neighbourLightOrder[i + 1];
-+ final int neighbourX = dx + chunkX;
-+ final int neighbourZ = dz + chunkZ;
-+
-+ final ChunkAccess neighbour = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX, neighbourZ);
-+ if (neighbour == null || !this.canUseChunk(neighbour)) {
-+ continue;
-+ }
-+
-+ if (nibblesByChunk.get(CoordinateUtils.getChunkKey(neighbourX, neighbourZ)) != null) {
-+ // lit already called for neighbour, no need to light it now
-+ continue;
-+ }
-+
-+ // light neighbour chunk
-+ this.setupEncodeOffset(neighbourX * 16 + 7, 128, neighbourZ * 16 + 7);
-+ try {
-+ // insert all neighbouring chunks for this neighbour that we have data for
-+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
-+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
-+ final int neighbourX2 = neighbourX + dx2;
-+ final int neighbourZ2 = neighbourZ + dz2;
-+ final long key = CoordinateUtils.getChunkKey(neighbourX2, neighbourZ2);
-+ final ChunkAccess neighbour2 = (ChunkAccess)lightAccess.getChunkForLighting(neighbourX2, neighbourZ2);
-+ if (neighbour2 == null || !this.canUseChunk(neighbour2)) {
-+ continue;
-+ }
-+
-+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(key);
-+ if (nibbles == null) {
-+ // we haven't lit this chunk
-+ continue;
-+ }
-+
-+ this.setChunkInCache(neighbourX2, neighbourZ2, neighbour2);
-+ this.setBlocksForChunkInCache(neighbourX2, neighbourZ2, neighbour2.getSections());
-+ this.setNibblesForChunkInCache(neighbourX2, neighbourZ2, nibbles);
-+ this.setEmptinessMapCache(neighbourX2, neighbourZ2, emptinessMapByChunk.get(key));
-+ }
-+ }
-+
-+ final long key = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
-+
-+ // now insert the neighbour chunk and light it
-+ final SWMRNibbleArray[] nibbles = getFilledEmptyLight(this.world);
-+ nibblesByChunk.put(key, nibbles);
-+
-+ this.setChunkInCache(neighbourX, neighbourZ, neighbour);
-+ this.setBlocksForChunkInCache(neighbourX, neighbourZ, neighbour.getSections());
-+ this.setNibblesForChunkInCache(neighbourX, neighbourZ, nibbles);
-+
-+ final boolean[] neighbourEmptiness = this.handleEmptySectionChanges(lightAccess, neighbour, getEmptySectionsForChunk(neighbour), true);
-+ emptinessMapByChunk.put(key, neighbourEmptiness);
-+ if (chunks.contains(new ChunkPos(neighbourX, neighbourZ))) {
-+ this.setEmptinessMap(neighbour, neighbourEmptiness);
-+ }
-+
-+ this.lightChunk(lightAccess, neighbour, false);
-+ } finally {
-+ this.destroyCaches();
-+ }
-+ }
-+
-+ // done lighting all neighbours, so the chunk is now fully lit
-+
-+ // make sure nibbles are fully updated before calling back
-+ final SWMRNibbleArray[] nibbles = nibblesByChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+ for (final SWMRNibbleArray nibble : nibbles) {
-+ nibble.updateVisible();
-+ }
-+
-+ this.setNibbles(chunk, nibbles);
-+
-+ for (int y = this.minLightSection; y <= this.maxLightSection; ++y) {
-+ lightAccess.onLightUpdate(this.skylightPropagator ? LightLayer.SKY : LightLayer.BLOCK, SectionPos.of(chunkX, y, chunkX));
-+ }
-+
-+ // now do callback
-+ if (chunkLightCallback != null) {
-+ chunkLightCallback.accept(chunkPos);
-+ }
-+ ++lightCalls;
-+ }
-+
-+ if (onComplete != null) {
-+ onComplete.accept(lightCalls);
-+ }
-+ }
-+
-+ // contains:
-+ // lower (6 + 6 + 16) = 28 bits: encoded coordinate position (x | (z << 6) | (y << (6 + 6))))
-+ // next 4 bits: propagated light level (0, 15]
-+ // next 6 bits: propagation direction bitset
-+ // next 24 bits: unused
-+ // last 3 bits: state flags
-+ // state flags:
-+ // whether the increase propagator needs to write the propagated level to the position, used to avoid cascading light
-+ // updates for block sources
-+ protected static final long FLAG_WRITE_LEVEL = Long.MIN_VALUE >>> 2;
-+ // whether the propagation needs to check if its current level is equal to the expected level
-+ // used only in increase propagation
-+ protected static final long FLAG_RECHECK_LEVEL = Long.MIN_VALUE >>> 1;
-+ // whether the propagation needs to consider if its block is conditionally transparent
-+ protected static final long FLAG_HAS_SIDED_TRANSPARENT_BLOCKS = Long.MIN_VALUE;
-+
-+ protected long[] increaseQueue = new long[16 * 16 * 16];
-+ protected int increaseQueueInitialLength;
-+ protected long[] decreaseQueue = new long[16 * 16 * 16];
-+ protected int decreaseQueueInitialLength;
-+
-+ protected final long[] resizeIncreaseQueue() {
-+ return this.increaseQueue = Arrays.copyOf(this.increaseQueue, this.increaseQueue.length * 2);
-+ }
-+
-+ protected final long[] resizeDecreaseQueue() {
-+ return this.decreaseQueue = Arrays.copyOf(this.decreaseQueue, this.decreaseQueue.length * 2);
-+ }
-+
-+ protected final void appendToIncreaseQueue(final long value) {
-+ final int idx = this.increaseQueueInitialLength++;
-+ long[] queue = this.increaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ queue[idx] = value;
-+ } else {
-+ queue[idx] = value;
-+ }
-+ }
-+
-+ protected final void appendToDecreaseQueue(final long value) {
-+ final int idx = this.decreaseQueueInitialLength++;
-+ long[] queue = this.decreaseQueue;
-+ if (idx >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ queue[idx] = value;
-+ } else {
-+ queue[idx] = value;
-+ }
-+ }
-+
-+ protected static final AxisDirection[][] OLD_CHECK_DIRECTIONS = new AxisDirection[1 << 6][];
-+ protected static final int ALL_DIRECTIONS_BITSET = (1 << 6) - 1;
-+ static {
-+ for (int i = 0; i < OLD_CHECK_DIRECTIONS.length; ++i) {
-+ final List<AxisDirection> directions = new ArrayList<>();
-+ for (int bitset = i, len = Integer.bitCount(i), index = 0; index < len; ++index, bitset ^= IntegerUtil.getTrailingBit(bitset)) {
-+ directions.add(AXIS_DIRECTIONS[IntegerUtil.trailingZeros(bitset)]);
-+ }
-+ OLD_CHECK_DIRECTIONS[i] = directions.toArray(new AxisDirection[0]);
-+ }
-+ }
-+
-+ protected final void performLightIncrease(final LightChunkGetter lightAccess) {
-+ final BlockGetter world = lightAccess.getLevel();
-+ long[] queue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.increaseQueueInitialLength;
-+ this.increaseQueueInitialLength = 0;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xFL);
-+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63L)];
-+
-+ if ((queueValue & FLAG_RECHECK_LEVEL) != 0L) {
-+ if (this.getLightLevel(posX, posY, posZ) != propagatedLightLevel) {
-+ // not at the level we expect, so something changed.
-+ continue;
-+ }
-+ } else if ((queueValue & FLAG_WRITE_LEVEL) != 0L) {
-+ // these are used to restore block sources after a propagation decrease
-+ this.setLightLevel(posX, posY, posZ, propagatedLightLevel);
-+ }
-+
-+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
-+ // we don't need to worry about our state here.
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int currentLevel;
-+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
-+ continue; // already at the level we want or unloaded
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-+ if (targetLevel > currentLevel) {
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
-+ | (flags);
-+ }
-+ continue;
-+ }
-+ }
-+ } else {
-+ // we actually need to worry about our state here
-+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ this.mutablePos2.set(posX, posY, posZ);
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final VoxelShape fromShape = fromBlock.isConditionallyFullOpaque() ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
-+
-+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ continue;
-+ }
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int currentLevel;
-+
-+ if (currentNibble == null || (currentLevel = currentNibble.getUpdating(localIndex)) >= (propagatedLightLevel - 1)) {
-+ continue; // already at the level we want
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacityCached);
-+ if (targetLevel > currentLevel) {
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = propagatedLightLevel - Math.max(1, opacity);
-+ if (targetLevel <= currentLevel) {
-+ continue;
-+ }
-+
-+ currentNibble.set(localIndex, targetLevel);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 1) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeIncreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4))
-+ | (flags);
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+ }
-+
-+ protected final void performLightDecrease(final LightChunkGetter lightAccess) {
-+ final BlockGetter world = lightAccess.getLevel();
-+ long[] queue = this.decreaseQueue;
-+ long[] increaseQueue = this.increaseQueue;
-+ int queueReadIndex = 0;
-+ int queueLength = this.decreaseQueueInitialLength;
-+ this.decreaseQueueInitialLength = 0;
-+ int increaseQueueLength = this.increaseQueueInitialLength;
-+ final int decodeOffsetX = -this.encodeOffsetX;
-+ final int decodeOffsetY = -this.encodeOffsetY;
-+ final int decodeOffsetZ = -this.encodeOffsetZ;
-+ final int encodeOffset = this.coordinateOffset;
-+ final int sectionOffset = this.chunkSectionIndexOffset;
-+ final int emittedMask = this.emittedLightMask;
-+
-+ while (queueReadIndex < queueLength) {
-+ final long queueValue = queue[queueReadIndex++];
-+
-+ final int posX = ((int)queueValue & 63) + decodeOffsetX;
-+ final int posZ = (((int)queueValue >>> 6) & 63) + decodeOffsetZ;
-+ final int posY = (((int)queueValue >>> 12) & ((1 << 16) - 1)) + decodeOffsetY;
-+ final int propagatedLightLevel = (int)((queueValue >>> (6 + 6 + 16)) & 0xF);
-+ final AxisDirection[] checkDirections = OLD_CHECK_DIRECTIONS[(int)((queueValue >>> (6 + 6 + 16 + 4)) & 63)];
-+
-+ if ((queueValue & FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) == 0L) {
-+ // we don't need to worry about our state here.
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int lightLevel;
-+
-+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
-+ // already at lowest (or unloaded), nothing we can do
-+ continue;
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_RECHECK_LEVEL;
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // 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))
-+ | (blockState.isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(Shapes.empty(), cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // note: do not set recheck level, or else the propagation will fail
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (flags | FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) {
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+ continue;
-+ }
-+ }
-+ } else {
-+ // we actually need to worry about our state here
-+ final BlockState fromBlock = this.getBlockState(posX, posY, posZ);
-+ this.mutablePos2.set(posX, posY, posZ);
-+ for (final AxisDirection propagate : checkDirections) {
-+ final int offX = posX + propagate.x;
-+ final int offY = posY + propagate.y;
-+ final int offZ = posZ + propagate.z;
-+
-+ final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset;
-+ final int localIndex = (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8);
-+
-+ final VoxelShape fromShape = (fromBlock.isConditionallyFullOpaque()) ? fromBlock.getFaceOcclusionShape(world, this.mutablePos2, propagate.nms) : Shapes.empty();
-+
-+ if (fromShape != Shapes.empty() && Shapes.faceShapeOccludes(Shapes.empty(), fromShape)) {
-+ continue;
-+ }
-+
-+ final SWMRNibbleArray currentNibble = this.nibbleCache[sectionIndex];
-+ final int lightLevel;
-+
-+ if (currentNibble == null || (lightLevel = currentNibble.getUpdating(localIndex)) == 0) {
-+ // already at lowest (or unloaded), nothing we can do
-+ continue;
-+ }
-+
-+ final BlockState blockState = this.getBlockState(sectionIndex, localIndex);
-+ if (blockState == null) {
-+ continue;
-+ }
-+ final int opacityCached = blockState.getOpacityIfCached();
-+ if (opacityCached != -1) {
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacityCached));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | FLAG_RECHECK_LEVEL;
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // 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))
-+ | (blockState.isConditionallyFullOpaque() ? (FLAG_WRITE_LEVEL | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS) : FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4));
-+ continue;
-+ }
-+ continue;
-+ } else {
-+ this.mutablePos1.set(offX, offY, offZ);
-+ long flags = 0;
-+ if (blockState.isConditionallyFullOpaque()) {
-+ final VoxelShape cullingFace = blockState.getFaceOcclusionShape(world, this.mutablePos1, propagate.getOpposite().nms);
-+
-+ if (Shapes.faceShapeOccludes(fromShape, cullingFace)) {
-+ continue;
-+ }
-+ flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
-+ }
-+
-+ final int opacity = blockState.getLightBlock(world, this.mutablePos1);
-+ final int targetLevel = Math.max(0, propagatedLightLevel - Math.max(1, opacity));
-+ if (lightLevel > targetLevel) {
-+ // it looks like another source propagated here, so re-propagate it
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((lightLevel & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (FLAG_RECHECK_LEVEL | flags);
-+ continue;
-+ }
-+ final int emittedLight = blockState.getLightEmission() & emittedMask;
-+ if (emittedLight != 0) {
-+ // re-propagate source
-+ // note: do not set recheck level, or else the propagation will fail
-+ if (increaseQueueLength >= increaseQueue.length) {
-+ increaseQueue = this.resizeIncreaseQueue();
-+ }
-+ increaseQueue[increaseQueueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((emittedLight & 0xFL) << (6 + 6 + 16))
-+ | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
-+ | (flags | FLAG_WRITE_LEVEL);
-+ }
-+
-+ currentNibble.set(localIndex, 0);
-+ this.postLightUpdate(offX, offY, offZ);
-+
-+ if (targetLevel > 0) { // we actually need to propagate 0 just in case we find a neighbour...
-+ if (queueLength >= queue.length) {
-+ queue = this.resizeDecreaseQueue();
-+ }
-+ queue[queueLength++] =
-+ ((offX + (offZ << 6) + (offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
-+ | ((targetLevel & 0xFL) << (6 + 6 + 16))
-+ | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4))
-+ | flags;
-+ }
-+ continue;
-+ }
-+ }
-+ }
-+ }
-+
-+ // propagate sources we clobbered
-+ this.increaseQueueInitialLength = increaseQueueLength;
-+ this.performLightIncrease(lightAccess);
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..e0338db4d6fa359029ed5edeacc3646aa98701f5
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java
-@@ -0,0 +1,674 @@
-+package ca.spottedleaf.starlight.common.light;
-+
-+import ca.spottedleaf.starlight.common.util.CoordinateUtils;
-+import ca.spottedleaf.starlight.common.util.WorldUtil;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
-+import it.unimi.dsi.fastutil.shorts.ShortCollection;
-+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
-+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import net.minecraft.world.level.chunk.DataLayer;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.chunk.LightChunkGetter;
-+import net.minecraft.world.level.lighting.LayerLightEventListener;
-+import net.minecraft.world.level.lighting.LevelLightEngine;
-+import java.util.ArrayDeque;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.concurrent.CompletableFuture;
-+import java.util.function.Consumer;
-+import java.util.function.IntConsumer;
-+
-+public final class StarLightInterface {
-+
-+ public static final TicketType<ChunkPos> CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong()));
-+
-+ /**
-+ * Can be {@code null}, indicating the light is all empty.
-+ */
-+ protected final Level world;
-+ protected final LightChunkGetter lightAccess;
-+
-+ protected final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators;
-+ protected final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators;
-+
-+ protected final LightQueue lightQueue = new LightQueue(this);
-+
-+ protected final LayerLightEventListener skyReader;
-+ protected final LayerLightEventListener blockReader;
-+ protected final boolean isClientSide;
-+
-+ protected final int minSection;
-+ protected final int maxSection;
-+ protected final int minLightSection;
-+ protected final int maxLightSection;
-+
-+ public final LevelLightEngine lightEngine;
-+
-+ private final boolean hasBlockLight;
-+ private final boolean hasSkyLight;
-+
-+ public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) {
-+ this.lightAccess = lightAccess;
-+ this.world = lightAccess == null ? null : (Level)lightAccess.getLevel();
-+ this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null;
-+ this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null;
-+ this.isClientSide = !(this.world instanceof ServerLevel);
-+ if (this.world == null) {
-+ this.minSection = -4;
-+ this.maxSection = 19;
-+ this.minLightSection = -5;
-+ this.maxLightSection = 20;
-+ } else {
-+ this.minSection = WorldUtil.getMinSection(this.world);
-+ this.maxSection = WorldUtil.getMaxSection(this.world);
-+ this.minLightSection = WorldUtil.getMinLightSection(this.world);
-+ this.maxLightSection = WorldUtil.getMaxLightSection(this.world);
-+ }
-+ this.lightEngine = lightEngine;
-+ this.hasBlockLight = hasBlockLight;
-+ this.hasSkyLight = hasSkyLight;
-+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
-+ @Override
-+ public void checkBlock(final BlockPos blockPos) {
-+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
-+ }
-+
-+ @Override
-+ public void propagateLightSources(final ChunkPos chunkPos) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // not really correct...
-+ return StarLightInterface.this.hasUpdates();
-+ }
-+
-+ @Override
-+ public int runLightUpdates() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public DataLayer getDataLayerData(final SectionPos pos) {
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
-+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ return null;
-+ }
-+
-+ final int sectionY = pos.getY();
-+
-+ if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) {
-+ return null;
-+ }
-+
-+ if (chunk.getSkyEmptinessMap() == null) {
-+ return null;
-+ }
-+
-+ return chunk.getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble();
-+ }
-+
-+ @Override
-+ public int getLightValue(final BlockPos blockPos) {
-+ return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
-+ }
-+
-+ @Override
-+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
-+ StarLightInterface.this.sectionChange(pos, notReady);
-+ }
-+ };
-+ this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
-+ @Override
-+ public void checkBlock(final BlockPos blockPos) {
-+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
-+ }
-+
-+ @Override
-+ public void propagateLightSources(final ChunkPos chunkPos) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // not really correct...
-+ return StarLightInterface.this.hasUpdates();
-+ }
-+
-+ @Override
-+ public int runLightUpdates() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public DataLayer getDataLayerData(final SectionPos pos) {
-+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
-+
-+ if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) {
-+ return null;
-+ }
-+
-+ return chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble();
-+ }
-+
-+ @Override
-+ public int getLightValue(final BlockPos blockPos) {
-+ return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4));
-+ }
-+
-+ @Override
-+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
-+ StarLightInterface.this.sectionChange(pos, notReady);
-+ }
-+ };
-+ }
-+
-+ public boolean hasSkyLight() {
-+ return this.hasSkyLight;
-+ }
-+
-+ public boolean hasBlockLight() {
-+ return this.hasBlockLight;
-+ }
-+
-+ public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
-+ if (!this.hasSkyLight) {
-+ return 0;
-+ }
-+ final int x = blockPos.getX();
-+ int y = blockPos.getY();
-+ final int z = blockPos.getZ();
-+
-+ final int minSection = this.minSection;
-+ final int maxSection = this.maxSection;
-+ final int minLightSection = this.minLightSection;
-+ final int maxLightSection = this.maxLightSection;
-+
-+ if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ return 15;
-+ }
-+
-+ int sectionY = y >> 4;
-+
-+ if (sectionY > maxLightSection) {
-+ return 15;
-+ }
-+
-+ if (sectionY < minLightSection) {
-+ sectionY = minLightSection;
-+ y = sectionY << 4;
-+ }
-+
-+ final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles();
-+ final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection];
-+
-+ if (!immediate.isNullNibbleVisible()) {
-+ return immediate.getVisible(x, y, z);
-+ }
-+
-+ final boolean[] emptinessMap = chunk.getSkyEmptinessMap();
-+
-+ if (emptinessMap == null) {
-+ return 15;
-+ }
-+
-+ // are we above this chunk's lowest empty section?
-+ int lowestY = minLightSection - 1;
-+ for (int currY = maxSection; currY >= minSection; --currY) {
-+ if (emptinessMap[currY - minSection]) {
-+ continue;
-+ }
-+
-+ // should always be full lit here
-+ lowestY = currY;
-+ break;
-+ }
-+
-+ if (sectionY > lowestY) {
-+ return 15;
-+ }
-+
-+ // this nibble is going to depend solely on the skylight data above it
-+ // find first non-null data above (there does exist one, as we just found it above)
-+ for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) {
-+ final SWMRNibbleArray nibble = nibbles[currY - minLightSection];
-+ if (!nibble.isNullNibbleVisible()) {
-+ return nibble.getVisible(x, 0, z);
-+ }
-+ }
-+
-+ // should never reach here
-+ return 15;
-+ }
-+
-+ public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) {
-+ if (!this.hasBlockLight) {
-+ return 0;
-+ }
-+ final int y = blockPos.getY();
-+ final int cy = y >> 4;
-+
-+ final int minLightSection = this.minLightSection;
-+ final int maxLightSection = this.maxLightSection;
-+
-+ if (cy < minLightSection || cy > maxLightSection) {
-+ return 0;
-+ }
-+
-+ if (chunk == null) {
-+ return 0;
-+ }
-+
-+ final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - minLightSection];
-+ return nibble.getVisible(blockPos.getX(), y, blockPos.getZ());
-+ }
-+
-+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
-+ final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4);
-+
-+ final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness;
-+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher.
-+ if (sky == 15) return 15;
-+ final int block = this.getBlockLightValue(pos, chunk);
-+ return Math.max(sky, block);
-+ }
-+
-+ public LayerLightEventListener getSkyReader() {
-+ return this.skyReader;
-+ }
-+
-+ public LayerLightEventListener getBlockReader() {
-+ return this.blockReader;
-+ }
-+
-+ public boolean isClientSide() {
-+ return this.isClientSide;
-+ }
-+
-+ public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) {
-+ if (this.world == null) {
-+ // empty world
-+ return null;
-+ }
-+
-+ final ServerChunkCache chunkProvider = ((ServerLevel)this.world).getChunkSource();
-+ final LevelChunk fullLoaded = chunkProvider.getChunkAtIfLoadedImmediately(chunkX, chunkZ);
-+ if (fullLoaded != null) {
-+ return fullLoaded;
-+ }
-+
-+ return chunkProvider.getChunkAtImmediately(chunkX, chunkZ);
-+ }
-+
-+ public boolean hasUpdates() {
-+ return !this.lightQueue.isEmpty();
-+ }
-+
-+ public Level getWorld() {
-+ return this.world;
-+ }
-+
-+ public LightChunkGetter getLightAccess() {
-+ return this.lightAccess;
-+ }
-+
-+ protected final SkyStarLightEngine getSkyLightEngine() {
-+ if (this.cachedSkyPropagators == null) {
-+ return null;
-+ }
-+ final SkyStarLightEngine ret;
-+ synchronized (this.cachedSkyPropagators) {
-+ ret = this.cachedSkyPropagators.pollFirst();
-+ }
-+
-+ if (ret == null) {
-+ return new SkyStarLightEngine(this.world);
-+ }
-+ return ret;
-+ }
-+
-+ protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) {
-+ if (this.cachedSkyPropagators == null) {
-+ return;
-+ }
-+ synchronized (this.cachedSkyPropagators) {
-+ this.cachedSkyPropagators.addFirst(engine);
-+ }
-+ }
-+
-+ protected final BlockStarLightEngine getBlockLightEngine() {
-+ if (this.cachedBlockPropagators == null) {
-+ return null;
-+ }
-+ final BlockStarLightEngine ret;
-+ synchronized (this.cachedBlockPropagators) {
-+ ret = this.cachedBlockPropagators.pollFirst();
-+ }
-+
-+ if (ret == null) {
-+ return new BlockStarLightEngine(this.world);
-+ }
-+ return ret;
-+ }
-+
-+ protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) {
-+ if (this.cachedBlockPropagators == null) {
-+ return;
-+ }
-+ synchronized (this.cachedBlockPropagators) {
-+ this.cachedBlockPropagators.addFirst(engine);
-+ }
-+ }
-+
-+ public LightQueue.ChunkTasks blockChange(final BlockPos pos) {
-+ if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
-+ return null;
-+ }
-+
-+ return this.lightQueue.queueBlockChange(pos);
-+ }
-+
-+ public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ if (this.world == null) { // empty world
-+ return null;
-+ }
-+
-+ return this.lightQueue.queueSectionChange(pos, newEmptyValue);
-+ }
-+
-+ public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.light(this.lightAccess, chunk, emptySections);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.light(this.lightAccess, chunk, emptySections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void relightChunks(final Set<ChunkPos> chunks, final Consumer<ChunkPos> chunkLightCallback,
-+ final IntConsumer onComplete) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null,
-+ blockEngine == null ? onComplete : null);
-+ }
-+ if (blockEngine != null) {
-+ blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void checkChunkEdges(final int chunkX, final int chunkZ) {
-+ this.checkSkyEdges(chunkX, chunkZ);
-+ this.checkBlockEdges(chunkX, chunkZ);
-+ }
-+
-+ public void checkSkyEdges(final int chunkX, final int chunkZ) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ }
-+ }
-+
-+ public void checkBlockEdges(final int chunkX, final int chunkZ) {
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+ try {
-+ if (blockEngine != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ);
-+ }
-+ } finally {
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+
-+ try {
-+ if (skyEngine != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ }
-+ }
-+
-+ public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) {
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+ try {
-+ if (blockEngine != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections);
-+ }
-+ } finally {
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public void scheduleChunkLight(final ChunkPos pos, final Runnable run) {
-+ this.lightQueue.queueChunkLighting(pos, run);
-+ }
-+
-+ public void removeChunkTasks(final ChunkPos pos) {
-+ this.lightQueue.removeChunk(pos);
-+ }
-+
-+ public void propagateChanges() {
-+ if (this.lightQueue.isEmpty()) {
-+ return;
-+ }
-+
-+ final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
-+ final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
-+
-+ try {
-+ LightQueue.ChunkTasks task;
-+ while ((task = this.lightQueue.removeFirstTask()) != null) {
-+ if (task.lightTasks != null) {
-+ for (final Runnable run : task.lightTasks) {
-+ run.run();
-+ }
-+ }
-+
-+ final long coordinate = task.chunkCoordinate;
-+ final int chunkX = CoordinateUtils.getChunkX(coordinate);
-+ final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
-+
-+ final Set<BlockPos> positions = task.changedPositions;
-+ final Boolean[] sectionChanges = task.changedSectionSet;
-+
-+ if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
-+ }
-+ if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) {
-+ blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
-+ }
-+
-+ if (skyEngine != null && task.queuedEdgeChecksSky != null) {
-+ skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky);
-+ }
-+ if (blockEngine != null && task.queuedEdgeChecksBlock != null) {
-+ blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock);
-+ }
-+
-+ task.onComplete.complete(null);
-+ }
-+ } finally {
-+ this.releaseSkyLightEngine(skyEngine);
-+ this.releaseBlockLightEngine(blockEngine);
-+ }
-+ }
-+
-+ public static final class LightQueue {
-+
-+ protected final Long2ObjectLinkedOpenHashMap<ChunkTasks> chunkTasks = new Long2ObjectLinkedOpenHashMap<>();
-+ protected final StarLightInterface manager;
-+
-+ public LightQueue(final StarLightInterface manager) {
-+ this.manager = manager;
-+ }
-+
-+ public synchronized boolean isEmpty() {
-+ return this.chunkTasks.isEmpty();
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueBlockChange(final BlockPos pos) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+ tasks.changedPositions.add(pos.immutable());
-+ return tasks;
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+
-+ if (tasks.changedSectionSet == null) {
-+ tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1];
-+ }
-+ tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue);
-+
-+ return tasks;
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueChunkLighting(final ChunkPos pos, final Runnable lightTask) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+ if (tasks.lightTasks == null) {
-+ tasks.lightTasks = new ArrayList<>();
-+ }
-+ tasks.lightTasks.add(lightTask);
-+
-+ return tasks;
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+
-+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky;
-+ if (queuedEdges == null) {
-+ queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet();
-+ }
-+ queuedEdges.addAll(sections);
-+
-+ return tasks;
-+ }
-+
-+ public synchronized LightQueue.ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) {
-+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
-+
-+ ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock;
-+ if (queuedEdges == null) {
-+ queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet();
-+ }
-+ queuedEdges.addAll(sections);
-+
-+ return tasks;
-+ }
-+
-+ public void removeChunk(final ChunkPos pos) {
-+ final ChunkTasks tasks;
-+ synchronized (this) {
-+ tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos));
-+ }
-+ if (tasks != null) {
-+ tasks.onComplete.complete(null);
-+ }
-+ }
-+
-+ public synchronized ChunkTasks removeFirstTask() {
-+ if (this.chunkTasks.isEmpty()) {
-+ return null;
-+ }
-+ return this.chunkTasks.removeFirst();
-+ }
-+
-+ public static final class ChunkTasks {
-+
-+ public final Set<BlockPos> changedPositions = new ObjectOpenHashSet<>();
-+ public Boolean[] changedSectionSet;
-+ public ShortOpenHashSet queuedEdgeChecksSky;
-+ public ShortOpenHashSet queuedEdgeChecksBlock;
-+ public List<Runnable> lightTasks;
-+
-+ public boolean isTicketAdded = false;
-+ public final CompletableFuture<Void> onComplete = new CompletableFuture<>();
-+
-+ public final long chunkCoordinate;
-+
-+ public ChunkTasks(final long chunkCoordinate) {
-+ this.chunkCoordinate = chunkCoordinate;
-+ }
-+ }
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java b/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..16a4a14e7ccf9e4d7fdf1166674fe8f529c06d39
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java
-@@ -0,0 +1,128 @@
-+package ca.spottedleaf.starlight.common.util;
-+
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.core.SectionPos;
-+import net.minecraft.util.Mth;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.ChunkPos;
-+
-+public final class CoordinateUtils {
-+
-+ // dx, dz are relative to the target chunk
-+ // dx, dz in [-radius, radius]
-+ public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) {
-+ return (dx + radius) + (2 * radius + 1)*(dz + radius);
-+ }
-+
-+ // the chunk keys are compatible with vanilla
-+
-+ public static long getChunkKey(final BlockPos pos) {
-+ return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
-+ }
-+
-+ public static long getChunkKey(final Entity entity) {
-+ return ((long)(Mth.floor(entity.getZ()) >> 4) << 32) | ((Mth.floor(entity.getX()) >> 4) & 0xFFFFFFFFL);
-+ }
-+
-+ public static long getChunkKey(final ChunkPos pos) {
-+ return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
-+ }
-+
-+ public static long getChunkKey(final SectionPos pos) {
-+ return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL);
-+ }
-+
-+ public static long getChunkKey(final int x, final int z) {
-+ return ((long)z << 32) | (x & 0xFFFFFFFFL);
-+ }
-+
-+ public static int getChunkX(final long chunkKey) {
-+ return (int)chunkKey;
-+ }
-+
-+ public static int getChunkZ(final long chunkKey) {
-+ return (int)(chunkKey >>> 32);
-+ }
-+
-+ public static int getChunkCoordinate(final double blockCoordinate) {
-+ return Mth.floor(blockCoordinate) >> 4;
-+ }
-+
-+ // the section keys are compatible with vanilla's
-+
-+ static final int SECTION_X_BITS = 22;
-+ static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1;
-+ static final int SECTION_Y_BITS = 20;
-+ static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1;
-+ static final int SECTION_Z_BITS = 22;
-+ static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1;
-+ // format is y,z,x (in order of LSB to MSB)
-+ static final int SECTION_Y_SHIFT = 0;
-+ static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS;
-+ static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS;
-+ static final int SECTION_TO_BLOCK_SHIFT = 4;
-+
-+ public static long getChunkSectionKey(final int x, final int y, final int z) {
-+ return ((x & SECTION_X_MASK) << SECTION_X_SHIFT)
-+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
-+ | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
-+ }
-+
-+ public static long getChunkSectionKey(final SectionPos pos) {
-+ return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT)
-+ | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT)
-+ | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT);
-+ }
-+
-+ public static long getChunkSectionKey(final ChunkPos pos, final int y) {
-+ return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT)
-+ | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT)
-+ | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT);
-+ }
-+
-+ public static long getChunkSectionKey(final BlockPos pos) {
-+ return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
-+ ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
-+ (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
-+ }
-+
-+ public static long getChunkSectionKey(final Entity entity) {
-+ return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
-+ ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
-+ ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT));
-+ }
-+
-+ public static int getChunkSectionX(final long key) {
-+ return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS));
-+ }
-+
-+ public static int getChunkSectionY(final long key) {
-+ return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS));
-+ }
-+
-+ public static int getChunkSectionZ(final long key) {
-+ return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS));
-+ }
-+
-+ // the block coordinates are not necessarily compatible with vanilla's
-+
-+ public static int getBlockCoordinate(final double blockCoordinate) {
-+ return Mth.floor(blockCoordinate);
-+ }
-+
-+ public static long getBlockKey(final int x, final int y, final int z) {
-+ return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54);
-+ }
-+
-+ public static long getBlockKey(final BlockPos pos) {
-+ return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54);
-+ }
-+
-+ public static long getBlockKey(final Entity entity) {
-+ return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54);
-+ }
-+
-+ private CoordinateUtils() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..fabf1e97c019c7365212f40018dcd08d3b828113
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java
-@@ -0,0 +1,242 @@
-+package ca.spottedleaf.starlight.common.util;
-+
-+public final class IntegerUtil {
-+
-+ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
-+ public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
-+
-+ public static int ceilLog2(final int value) {
-+ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static long ceilLog2(final long value) {
-+ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static int floorLog2(final int value) {
-+ // xor is optimized subtract for 2^n -1
-+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static int floorLog2(final long value) {
-+ // xor is optimized subtract for 2^n -1
-+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
-+ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
-+ }
-+
-+ public static int roundCeilLog2(final int value) {
-+ // optimized variant of 1 << (32 - leading(val - 1))
-+ // given
-+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
-+ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
-+ // HIGH_BIT_32 >>> (-1 + leading(val - 1))
-+ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
-+ }
-+
-+ public static long roundCeilLog2(final long value) {
-+ // see logic documented above
-+ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
-+ }
-+
-+ public static int roundFloorLog2(final int value) {
-+ // optimized variant of 1 << (31 - leading(val))
-+ // given
-+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
-+ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+ // HIGH_BIT_32 >> (31 - (31 - leading(val)))
-+ // HIGH_BIT_32 >> (31 - 31 + leading(val))
-+ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
-+ }
-+
-+ public static long roundFloorLog2(final long value) {
-+ // see logic documented above
-+ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
-+ }
-+
-+ public static boolean isPowerOfTwo(final int n) {
-+ // 2^n has one bit
-+ // note: this rets true for 0 still
-+ return IntegerUtil.getTrailingBit(n) == n;
-+ }
-+
-+ public static boolean isPowerOfTwo(final long n) {
-+ // 2^n has one bit
-+ // note: this rets true for 0 still
-+ return IntegerUtil.getTrailingBit(n) == n;
-+ }
-+
-+ public static int getTrailingBit(final int n) {
-+ return -n & n;
-+ }
-+
-+ public static long getTrailingBit(final long n) {
-+ return -n & n;
-+ }
-+
-+ public static int trailingZeros(final int n) {
-+ return Integer.numberOfTrailingZeros(n);
-+ }
-+
-+ public static int trailingZeros(final long n) {
-+ return Long.numberOfTrailingZeros(n);
-+ }
-+
-+ // from hacker's delight (signed division magic value)
-+ public static int getDivisorMultiple(final long numbers) {
-+ return (int)(numbers >>> 32);
-+ }
-+
-+ // from hacker's delight (signed division magic value)
-+ public static int getDivisorShift(final long numbers) {
-+ return (int)numbers;
-+ }
-+
-+ // copied from hacker's delight (signed division magic value)
-+ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
-+ public static long getDivisorNumbers(final int d) {
-+ final int ad = branchlessAbs(d);
-+
-+ if (ad < 2) {
-+ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
-+ }
-+
-+ final int two31 = 0x80000000;
-+ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
-+
-+ /*
-+ Signed usage:
-+ int number;
-+ long magic = getDivisorNumbers(div);
-+ long mul = magic >>> 32;
-+ int sign = number >> 31;
-+ int result = (int)(((long)number * mul) >>> magic) - sign;
-+ */
-+ /*
-+ Unsigned usage:
-+ int number;
-+ long magic = getDivisorNumbers(div);
-+ long mul = magic >>> 32;
-+ int result = (int)(((long)number * mul) >>> magic);
-+ */
-+
-+ int p = 31;
-+
-+ // all these variables are UNSIGNED!
-+ int t = two31 + (d >>> 31);
-+ int anc = t - 1 - (int)((t & mask)%ad);
-+ int q1 = (int)((two31 & mask)/(anc & mask));
-+ int r1 = two31 - q1*anc;
-+ int q2 = (int)((two31 & mask)/(ad & mask));
-+ int r2 = two31 - q2*ad;
-+ int delta;
-+
-+ do {
-+ p = p + 1;
-+ q1 = 2*q1; // Update q1 = 2**p/|nc|.
-+ r1 = 2*r1; // Update r1 = rem(2**p, |nc|).
-+ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
-+ q1 = q1 + 1;
-+ r1 = r1 - anc;
-+ }
-+ q2 = 2*q2; // Update q2 = 2**p/|d|.
-+ r2 = 2*r2; // Update r2 = rem(2**p, |d|).
-+ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
-+ q2 = q2 + 1;
-+ r2 = r2 - ad;
-+ }
-+ delta = ad - r2;
-+ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
-+
-+ int magicNum = q2 + 1;
-+ if (d < 0) {
-+ magicNum = -magicNum;
-+ }
-+ int shift = p;
-+ return ((long)magicNum << 32) | shift;
-+ }
-+
-+ public static int branchlessAbs(final int val) {
-+ // -n = -1 ^ n + 1
-+ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
-+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
-+ }
-+
-+ public static long branchlessAbs(final long val) {
-+ // -n = -1 ^ n + 1
-+ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
-+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
-+ }
-+
-+ //https://github.com/skeeto/hash-prospector for hash functions
-+
-+ //score = ~590.47984224483832
-+ public static int hash0(int x) {
-+ x *= 0x36935555;
-+ x ^= x >>> 16;
-+ return x;
-+ }
-+
-+ //score = ~310.01596637036749
-+ public static int hash1(int x) {
-+ x ^= x >>> 15;
-+ x *= 0x356aaaad;
-+ x ^= x >>> 17;
-+ return x;
-+ }
-+
-+ public static int hash2(int x) {
-+ x ^= x >>> 16;
-+ x *= 0x7feb352d;
-+ x ^= x >>> 15;
-+ x *= 0x846ca68b;
-+ x ^= x >>> 16;
-+ return x;
-+ }
-+
-+ public static int hash3(int x) {
-+ x ^= x >>> 17;
-+ x *= 0xed5ad4bb;
-+ x ^= x >>> 11;
-+ x *= 0xac4c1b51;
-+ x ^= x >>> 15;
-+ x *= 0x31848bab;
-+ x ^= x >>> 14;
-+ return x;
-+ }
-+
-+ //score = ~365.79959673201887
-+ public static long hash1(long x) {
-+ x ^= x >>> 27;
-+ x *= 0xb24924b71d2d354bL;
-+ x ^= x >>> 28;
-+ return x;
-+ }
-+
-+ //h2 hash
-+ public static long hash2(long x) {
-+ x ^= x >>> 32;
-+ x *= 0xd6e8feb86659fd93L;
-+ x ^= x >>> 32;
-+ x *= 0xd6e8feb86659fd93L;
-+ x ^= x >>> 32;
-+ return x;
-+ }
-+
-+ public static long hash3(long x) {
-+ x ^= x >>> 45;
-+ x *= 0xc161abe5704b6c79L;
-+ x ^= x >>> 41;
-+ x *= 0xe3e5389aedbc90f7L;
-+ x ^= x >>> 56;
-+ x *= 0x1f9aba75a52db073L;
-+ x ^= x >>> 53;
-+ return x;
-+ }
-+
-+ private IntegerUtil() {
-+ throw new RuntimeException();
-+ }
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..c2903150c8fc6955f4f4f71acc932b6c2ac83484
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java
-@@ -0,0 +1,192 @@
-+package ca.spottedleaf.starlight.common.util;
-+
-+import ca.spottedleaf.starlight.common.light.SWMRNibbleArray;
-+import ca.spottedleaf.starlight.common.light.StarLightEngine;
-+import com.mojang.logging.LogUtils;
-+import net.minecraft.nbt.CompoundTag;
-+import net.minecraft.nbt.ListTag;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+import org.slf4j.Logger;
-+
-+public final class SaveUtil {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+
-+ private 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 void saveLightHook(final Level world, final ChunkAccess chunk, final CompoundTag nbt) {
-+ try {
-+ saveLightHookReal(world, chunk, nbt);
-+ } catch (final Throwable ex) {
-+ // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false
-+ // for Vanilla to relight on load and it will not set our lit tag so we will relight on load
-+ if (ex instanceof ThreadDeath) {
-+ throw (ThreadDeath)ex;
-+ }
-+ LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex);
-+ }
-+ }
-+
-+ private static void saveLightHookReal(final Level world, final ChunkAccess chunk, final CompoundTag tag) {
-+ if (tag == null) {
-+ return;
-+ }
-+
-+ final int minSection = WorldUtil.getMinLightSection(world);
-+ final int maxSection = WorldUtil.getMaxLightSection(world);
-+
-+ SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles();
-+ SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles();
-+
-+ boolean lit = chunk.isLightCorrect() || !(world instanceof ServerLevel);
-+ // diff start - store our tag for whether light data is init'd
-+ if (lit) {
-+ tag.putBoolean("isLightOn", false);
-+ }
-+ // diff end - store our tag for whether light data is init'd
-+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
-+
-+ CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1];
-+
-+ ListTag sectionsStored = tag.getList("sections", 10);
-+
-+ for (int i = 0; i < sectionsStored.size(); ++i) {
-+ CompoundTag sectionStored = sectionsStored.getCompound(i);
-+ int k = sectionStored.getByte("Y");
-+
-+ // strip light data
-+ sectionStored.remove("BlockLight");
-+ sectionStored.remove("SkyLight");
-+
-+ if (!sectionStored.isEmpty()) {
-+ sections[k - minSection] = sectionStored;
-+ }
-+ }
-+
-+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) {
-+ for (int i = minSection; i <= maxSection; ++i) {
-+ SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState();
-+ SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState();
-+ if (blockNibble != null || skyNibble != null) {
-+ CompoundTag section = sections[i - minSection];
-+ if (section == null) {
-+ section = new CompoundTag();
-+ section.putByte("Y", (byte)i);
-+ sections[i - minSection] = section;
-+ }
-+
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+
-+ if (blockNibble != null) {
-+ if (blockNibble.data != null) {
-+ section.putByteArray("BlockLight", blockNibble.data);
-+ }
-+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state);
-+ }
-+
-+ if (skyNibble != null) {
-+ if (skyNibble.data != null) {
-+ section.putByteArray("SkyLight", skyNibble.data);
-+ }
-+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state);
-+ }
-+ }
-+ }
-+ }
-+
-+ // rewrite section list
-+ sectionsStored.clear();
-+ for (CompoundTag section : sections) {
-+ if (section != null) {
-+ sectionsStored.add(section);
-+ }
-+ }
-+ tag.put("sections", sectionsStored);
-+ if (lit) {
-+ tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data
-+ }
-+ }
-+
-+ public static void loadLightHook(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
-+ try {
-+ loadLightHookReal(world, pos, tag, into);
-+ } catch (final Throwable ex) {
-+ // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct
-+ // lighting in both cases.
-+ if (ex instanceof ThreadDeath) {
-+ throw (ThreadDeath)ex;
-+ }
-+ LOGGER.warn("Failed to load light for chunk " + pos + ", light will be recalculated", ex);
-+ }
-+ }
-+
-+ private static void loadLightHookReal(final Level world, final ChunkPos pos, final CompoundTag tag, final ChunkAccess into) {
-+ if (into == null) {
-+ return;
-+ }
-+ final int minSection = WorldUtil.getMinLightSection(world);
-+ final int maxSection = WorldUtil.getMaxLightSection(world);
-+
-+ into.setLightCorrect(false); // mark as unlit in case we fail parsing
-+
-+ SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world);
-+ SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world);
-+
-+
-+ // start copy from the original method
-+ boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION;
-+ boolean canReadSky = world.dimensionType().hasSkyLight();
-+ ChunkStatus status = ChunkStatus.byName(tag.getString("Status"));
-+ if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { // diff - we add the status check here
-+ ListTag sections = tag.getList("sections", 10);
-+
-+ for (int i = 0; i < sections.size(); ++i) {
-+ CompoundTag sectionData = sections.getCompound(i);
-+ int y = sectionData.getByte("Y");
-+
-+ if (sectionData.contains("BlockLight", 7)) {
-+ // this is where our diff is
-+ blockNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety
-+ } else {
-+ blockNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG));
-+ }
-+
-+ if (canReadSky) {
-+ if (sectionData.contains("SkyLight", 7)) {
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+ skyNibbles[y - minSection] = new SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety
-+ } else {
-+ skyNibbles[y - minSection] = new SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG));
-+ }
-+ }
-+ }
-+ }
-+ // end copy from vanilla
-+
-+ into.setBlockNibbles(blockNibbles);
-+ into.setSkyNibbles(skyNibbles);
-+ into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data
-+ }
-+
-+ private SaveUtil() {}
-+}
-diff --git a/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java b/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..dd995e25ae620ae36cd5eecb2fe10ad034ba50d2
---- /dev/null
-+++ b/src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java
-@@ -0,0 +1,47 @@
-+package ca.spottedleaf.starlight.common.util;
-+
-+import net.minecraft.world.level.LevelHeightAccessor;
-+
-+public final class WorldUtil {
-+
-+ // min, max are inclusive
-+
-+ public static int getMaxSection(final LevelHeightAccessor world) {
-+ return world.getMaxSection() - 1; // getMaxSection() is exclusive
-+ }
-+
-+ public static int getMinSection(final LevelHeightAccessor world) {
-+ return world.getMinSection();
-+ }
-+
-+ public static int getMaxLightSection(final LevelHeightAccessor world) {
-+ return getMaxSection(world) + 1;
-+ }
-+
-+ public static int getMinLightSection(final LevelHeightAccessor world) {
-+ return getMinSection(world) - 1;
-+ }
-+
-+
-+
-+ public static int getTotalSections(final LevelHeightAccessor world) {
-+ return getMaxSection(world) - getMinSection(world) + 1;
-+ }
-+
-+ public static int getTotalLightSections(final LevelHeightAccessor world) {
-+ return getMaxLightSection(world) - getMinLightSection(world) + 1;
-+ }
-+
-+ public static int getMinBlockY(final LevelHeightAccessor world) {
-+ return getMinSection(world) << 4;
-+ }
-+
-+ public static int getMaxBlockY(final LevelHeightAccessor world) {
-+ return (getMaxSection(world) << 4) | 15;
-+ }
-+
-+ private WorldUtil() {
-+ throw new RuntimeException();
-+ }
-+
-+}
-diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
-index 534d9c380f26d6cce3c99fa88ad2e15410535094..e47fb2aa5e885162cae5cbfc9f33ff7864bf538e 100644
---- a/src/main/java/io/papermc/paper/command/PaperCommand.java
-+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
-@@ -42,6 +42,7 @@ public final class PaperCommand extends Command {
- commands.put(Set.of("dumpitem"), new DumpItemCommand());
- commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand());
- commands.put(Set.of("dumplisteners"), new DumpListenersCommand());
-+ commands.put(Set.of("fixlight"), new FixLightCommand());
-
- return commands.entrySet().stream()
- .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
-diff --git a/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..56524cbe4303901007e1e7fb3703a19efbf79ae7
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/command/subcommands/FixLightCommand.java
-@@ -0,0 +1,109 @@
-+package io.papermc.paper.command.subcommands;
-+
-+import io.papermc.paper.command.PaperSubcommand;
-+import io.papermc.paper.util.MCUtil;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.level.ThreadedLevelLightEngine;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.chunk.ChunkAccess;
-+import org.bukkit.command.CommandSender;
-+import org.bukkit.craftbukkit.entity.CraftPlayer;
-+import org.bukkit.entity.Player;
-+import org.checkerframework.checker.nullness.qual.NonNull;
-+import org.checkerframework.checker.nullness.qual.Nullable;
-+import org.checkerframework.framework.qual.DefaultQualifier;
-+
-+import static net.kyori.adventure.text.Component.text;
-+import static net.kyori.adventure.text.format.NamedTextColor.BLUE;
-+import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA;
-+import static net.kyori.adventure.text.format.NamedTextColor.RED;
-+
-+@DefaultQualifier(NonNull.class)
-+public final class FixLightCommand implements PaperSubcommand {
-+ @Override
-+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
-+ this.doFixLight(sender, args);
-+ return true;
-+ }
-+
-+ private void doFixLight(final CommandSender sender, final String[] args) {
-+ if (!(sender instanceof Player)) {
-+ sender.sendMessage(text("Only players can use this command", RED));
-+ return;
-+ }
-+ @Nullable Runnable post = null;
-+ int radius = 2;
-+ if (args.length > 0) {
-+ try {
-+ final int parsed = Integer.parseInt(args[0]);
-+ if (parsed < 0) {
-+ sender.sendMessage(text("Radius cannot be negative!", RED));
-+ return;
-+ }
-+ final int maxRadius = 32;
-+ radius = Math.min(maxRadius, parsed);
-+ if (radius != parsed) {
-+ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED));
-+ }
-+ } catch (final Exception e) {
-+ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED));
-+ return;
-+ }
-+ }
-+
-+ CraftPlayer player = (CraftPlayer) sender;
-+ ServerPlayer handle = player.getHandle();
-+ ServerLevel world = (ServerLevel) handle.level();
-+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine();
-+ this.starlightFixLight(handle, world, lightengine, radius, post);
-+ }
-+
-+ private void starlightFixLight(
-+ final ServerPlayer sender,
-+ final ServerLevel world,
-+ final ThreadedLevelLightEngine lightengine,
-+ final int radius,
-+ final @Nullable Runnable done
-+ ) {
-+ final long start = System.nanoTime();
-+ final java.util.LinkedHashSet<ChunkPos> chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos
-+
-+ final int[] pending = new int[1];
-+ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext(); ) {
-+ final ChunkPos chunkPos = iterator.next();
-+
-+ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z);
-+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) {
-+ // cannot relight this chunk
-+ iterator.remove();
-+ continue;
-+ }
-+
-+ ++pending[0];
-+ }
-+
-+ final int[] relitChunks = new int[1];
-+ lightengine.relight(chunks,
-+ (final ChunkPos chunkPos) -> {
-+ ++relitChunks[0];
-+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
-+ text("Relit chunk ", BLUE), text(chunkPos.toString()),
-+ text(", progress: ", BLUE), text((int) (Math.round(100.0 * (double) (relitChunks[0]) / (double) pending[0])) + "%")
-+ ));
-+ },
-+ (final int totalRelit) -> {
-+ final long end = System.nanoTime();
-+ final long diff = Math.round(1.0e-6 * (end - start));
-+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append(
-+ text("Relit ", BLUE), text(totalRelit),
-+ text(" chunks. Took ", BLUE), text(diff + "ms")
-+ ));
-+ if (done != null) {
-+ done.run();
-+ }
-+ }
-+ );
-+ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks")));
-+ }
-+}
-diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-index b12921579cb9ab3cbf5607841cc84f2f843624ea..88729d92878f98729eb5669cce5ae5b1418865a1 100644
---- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
-@@ -51,7 +51,7 @@ public class ChunkHolder {
- private volatile CompletableFuture<ChunkResult<LevelChunk>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
- private volatile CompletableFuture<ChunkResult<LevelChunk>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
- private volatile CompletableFuture<ChunkResult<LevelChunk>> entityTickingChunkFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
-- private CompletableFuture<ChunkAccess> chunkToSave;
-+ public CompletableFuture<ChunkAccess> chunkToSave; // Paper - public
- @Nullable
- private final DebugBuffer<ChunkHolder.ChunkSaveDebug> chunkToSaveHistory;
- public int oldTicketLevel;
-@@ -261,6 +261,12 @@ public class ChunkHolder {
- }
- }
-
-+ // Paper start - starlight
-+ public void broadcast(Packet<?> packet, boolean onChunkViewEdge) {
-+ this.broadcast(this.playerProvider.getPlayers(this.pos, onChunkViewEdge), packet);
-+ }
-+ // Paper end - starlight
-+
- public void broadcastChanges(LevelChunk chunk) {
- if (this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
- Level world = chunk.getLevel();
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 35f627c58e93c03ee58b44877398432bba57dc2d..d3f63185edd1db9fab3887ea3f08982435b3a23c 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -128,7 +128,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- private final LongSet entitiesInLevel;
- public final ServerLevel level;
- private final ThreadedLevelLightEngine lightEngine;
-- private final BlockableEventLoop<Runnable> mainThreadExecutor;
-+ public final BlockableEventLoop<Runnable> mainThreadExecutor; // Paper - public
- public ChunkGenerator generator;
- private final RandomState randomState;
- private final ChunkGeneratorStructureState chunkGeneratorState;
-diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
-index c473cb1888e9ab0e91ba44f1439b81742758304e..7a48ae2ba962ff56d0abff581b51f28b48bd9aae 100644
---- a/src/main/java/net/minecraft/server/level/DistanceManager.java
-+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
-@@ -379,7 +379,7 @@ public abstract class DistanceManager {
- }
-
- public void removeTicketsOnClosing() {
-- ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT); // Paper - add additional tickets to preserve
-+ ImmutableSet<TicketType<?>> immutableset = ImmutableSet.of(TicketType.UNKNOWN, TicketType.POST_TELEPORT, TicketType.LIGHT, TicketType.FUTURE_AWAIT, TicketType.CHUNK_RELIGHT, ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET); // Paper - add additional tickets to preserve
- ObjectIterator<Entry<SortedArraySet<Ticket<?>>>> objectiterator = this.tickets.long2ObjectEntrySet().fastIterator();
-
- while (objectiterator.hasNext()) {
-diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-index 1dfae40ec19c4df0a97359941cf2c948cd1c9cb2..f206df06a7d8895175db31d4a840d7467ffe826f 100644
---- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
-@@ -23,6 +23,17 @@ import net.minecraft.world.level.chunk.LightChunkGetter;
- import net.minecraft.world.level.lighting.LevelLightEngine;
- import org.slf4j.Logger;
-
-+// Paper start
-+import ca.spottedleaf.starlight.common.light.StarLightEngine;
-+import io.papermc.paper.util.CoordinateUtils;
-+import java.util.function.Supplier;
-+import net.minecraft.world.level.lighting.LayerLightEventListener;
-+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongArrayList;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import net.minecraft.world.level.chunk.status.ChunkStatus;
-+// Paper end
-+
- public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
- public static final int DEFAULT_BATCH_SIZE = 1000;
- private static final Logger LOGGER = LogUtils.getLogger();
-@@ -33,6 +44,12 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- private final int taskPerBatch = 1000;
- private final AtomicBoolean scheduled = new AtomicBoolean();
-
-+ // Paper start - replace light engine impl
-+ protected final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine;
-+ public final boolean hasBlockLight;
-+ public final boolean hasSkyLight;
-+ // Paper end - replace light engine impl
-+
- public ThreadedLevelLightEngine(
- LightChunkGetter chunkProvider,
- ChunkMap chunkStorage,
-@@ -40,11 +57,153 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- ProcessorMailbox<Runnable> processor,
- ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> executor
- ) {
-- super(chunkProvider, true, hasBlockLight);
-+ super(chunkProvider, false, false); // Paper - destroy vanilla light engine state
- this.chunkMap = chunkStorage;
- this.sorterMailbox = executor;
- this.taskMailbox = processor;
-+ // Paper start - replace light engine impl
-+ this.hasBlockLight = true;
-+ this.hasSkyLight = hasBlockLight; // Nice variable name.
-+ this.theLightEngine = new ca.spottedleaf.starlight.common.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this);
-+ // Paper end - replace light engine impl
-+ }
-+
-+ // Paper start - replace light engine impl
-+ protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) {
-+ return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkX, chunkZ);
-+ }
-+
-+ protected long relightCounter;
-+
-+ public int relight(java.util.Set<ChunkPos> chunks_param,
-+ java.util.function.Consumer<ChunkPos> chunkLightCallback,
-+ java.util.function.IntConsumer onComplete) {
-+ if (!org.bukkit.Bukkit.isPrimaryThread()) {
-+ throw new IllegalStateException("Must only be called on the main thread");
-+ }
-+
-+ java.util.Set<ChunkPos> chunks = new java.util.LinkedHashSet<>(chunks_param);
-+ // add tickets
-+ java.util.Map<ChunkPos, Long> ticketIds = new java.util.HashMap<>();
-+ int totalChunks = 0;
-+ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext();) {
-+ final ChunkPos chunkPos = iterator.next();
-+
-+ final ChunkAccess chunk = (ChunkAccess)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z);
-+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ // cannot relight this chunk
-+ iterator.remove();
-+ continue;
-+ }
-+
-+ final Long id = Long.valueOf(this.relightCounter++);
-+
-+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id);
-+ ticketIds.put(chunkPos, id);
-+
-+ ++totalChunks;
-+ }
-+
-+ this.taskMailbox.tell(() -> {
-+ this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> {
-+ chunkLightCallback.accept(chunkPos);
-+ ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> {
-+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null), false);
-+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, io.papermc.paper.util.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos));
-+ });
-+ }, onComplete);
-+ });
-+ this.tryScheduleUpdate();
-+
-+ return totalChunks;
-+ }
-+
-+ private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap();
-+
-+ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ,
-+ final Supplier<ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks> runnable) {
-+ final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld();
-+
-+ final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ);
-+ if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
-+ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing
-+ // chunk scheduling, we could be lighting and generating a chunk at the same time
-+ return;
-+ }
-+
-+ if (center.getStatus() != ChunkStatus.FULL) {
-+ // do not keep chunk loaded, we are probably in a gen thread
-+ // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen)
-+ runnable.get();
-+ return;
-+ }
-+
-+ if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) {
-+ // ticket logic is not safe to run off-main, re-schedule
-+ world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> {
-+ this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable);
-+ });
-+ return;
-+ }
-+
-+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
-+
-+ final ca.spottedleaf.starlight.common.light.StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get();
-+
-+ if (updateFuture == null) {
-+ // not scheduled
-+ return;
-+ }
-+
-+ if (updateFuture.isTicketAdded) {
-+ // ticket already added
-+ return;
-+ }
-+ updateFuture.isTicketAdded = true;
-+
-+ final int references = this.chunksBeingWorkedOn.addTo(key, 1);
-+ if (references == 0) {
-+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
-+ world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
-+ }
-+
-+ updateFuture.onComplete.thenAcceptAsync((final Void ignore) -> {
-+ final int newReferences = this.chunksBeingWorkedOn.get(key);
-+ if (newReferences == 1) {
-+ this.chunksBeingWorkedOn.remove(key);
-+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
-+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
-+ } else {
-+ this.chunksBeingWorkedOn.put(key, newReferences - 1);
-+ }
-+ }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> {
-+ if (thr != null) {
-+ LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr);
-+ }
-+ });
-+ }
-+
-+ @Override
-+ public boolean hasLightWork() {
-+ // route to new light engine
-+ return this.theLightEngine.hasUpdates();
-+ }
-+
-+ @Override
-+ public LayerLightEventListener getLayerListener(final LightLayer lightType) {
-+ return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader();
-+ }
-+
-+ @Override
-+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
-+ // need to use new light hooks for this
-+ final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness;
-+ // Don't fetch the block light level if the skylight level is 15, since the value will never be higher.
-+ if (sky == 15) return 15;
-+ final int block = this.theLightEngine.getBlockReader().getLightValue(pos);
-+ return Math.max(sky, block);
- }
-+ // Paper end - replace light engine imp
-
- @Override
- public void close() {
-@@ -57,16 +216,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void checkBlock(BlockPos pos) {
-- BlockPos blockPos = pos.immutable();
-- this.addTask(
-- SectionPos.blockToSectionCoord(pos.getX()),
-- SectionPos.blockToSectionCoord(pos.getZ()),
-- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
-- Util.name(() -> super.checkBlock(blockPos), () -> "checkBlock " + blockPos)
-- );
-+ // Paper start - replace light engine impl
-+ final BlockPos posCopy = pos.immutable();
-+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> {
-+ return this.theLightEngine.blockChange(posCopy);
-+ });
-+ // Paper end - replace light engine impl
- }
-
- protected void updateChunkStatus(ChunkPos pos) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- super.retainData(pos, false);
- super.setLightEnabled(pos, false);
-@@ -84,17 +243,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void updateSectionStatus(SectionPos pos, boolean notReady) {
-- this.addTask(
-- pos.x(),
-- pos.z(),
-- () -> 0,
-- ThreadedLevelLightEngine.TaskType.PRE_UPDATE,
-- Util.name(() -> super.updateSectionStatus(pos, notReady), () -> "updateSectionStatus " + pos + " " + notReady)
-- );
-+ // Paper start - replace light engine impl
-+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
-+ return this.theLightEngine.sectionChange(pos, notReady);
-+ });
-+ // Paper end - replace light engine impl
- }
-
- @Override
- public void propagateLightSources(ChunkPos chunkPos) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(
- chunkPos.x,
- chunkPos.z,
-@@ -105,6 +263,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void setLightEnabled(ChunkPos pos, boolean retainData) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(
- pos.x,
- pos.z,
-@@ -115,6 +274,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(
- pos.x(),
- pos.z(),
-@@ -139,12 +299,14 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
-
- @Override
- public void retainData(ChunkPos pos, boolean retainData) {
-+ if (true) return; // Paper - replace light engine impl
- this.addTask(
- pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> super.retainData(pos, retainData), () -> "retainData " + pos)
- );
- }
-
- public CompletableFuture<ChunkAccess> initializeLight(ChunkAccess chunk, boolean bl) {
-+ if (true) return CompletableFuture.completedFuture(chunk); // Paper - replace light engine impl
- ChunkPos chunkPos = chunk.getPos();
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
- LevelChunkSection[] levelChunkSections = chunk.getSections();
-@@ -165,6 +327,37 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- }
-
- public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
-+ // Paper start - replace light engine impl
-+ if (true) {
-+ boolean lit = excludeBlocks;
-+ final ChunkPos chunkPos = chunk.getPos();
-+
-+ return CompletableFuture.supplyAsync(() -> {
-+ final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk);
-+ if (!lit) {
-+ chunk.setLightCorrect(false);
-+ this.theLightEngine.lightChunk(chunk, emptySections);
-+ chunk.setLightCorrect(true);
-+ } else {
-+ this.theLightEngine.forceLoadInChunk(chunk, emptySections);
-+ // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have
-+ // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should
-+ // catch what we miss here.
-+ this.theLightEngine.checkChunkEdges(chunkPos.x, chunkPos.z);
-+ }
-+
-+ this.chunkMap.releaseLightTicket(chunkPos);
-+ return chunk;
-+ }, (runnable) -> {
-+ this.theLightEngine.scheduleChunkLight(chunkPos, runnable);
-+ this.tryScheduleUpdate();
-+ }).whenComplete((final ChunkAccess c, final Throwable throwable) -> {
-+ if (throwable != null) {
-+ LOGGER.error("Failed to light chunk " + chunkPos, throwable);
-+ }
-+ });
-+ }
-+ // Paper end - replace light engine impl
- ChunkPos chunkPos = chunk.getPos();
- chunk.setLightCorrect(false);
- this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
-@@ -180,7 +373,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- }
-
- public void tryScheduleUpdate() {
-- if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
-+ if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine
- this.taskMailbox.tell(() -> {
- this.runUpdate();
- this.scheduled.set(false);
-@@ -201,7 +394,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
- }
-
- objectListIterator.back(j);
-- super.runLightUpdates();
-+ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine
-
- for (int var5 = 0; objectListIterator.hasNext() && var5 < i; var5++) {
- Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
-diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
-index 0d536d72ac918fbd403397ff369d10143ee9c204..6051e5f272838ef23276a90e21c2fc821ca155d1 100644
---- a/src/main/java/net/minecraft/server/level/TicketType.java
-+++ b/src/main/java/net/minecraft/server/level/TicketType.java
-@@ -26,6 +26,7 @@ public class TicketType<T> {
- public static final TicketType<ChunkPos> UNKNOWN = TicketType.create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1);
- public static final TicketType<Unit> PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit
- public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
-+ public static final TicketType<Long> CHUNK_RELIGHT = create("light_update", Long::compareTo); // Paper - ensure chunks stay loaded for lighting
-
- public static <T> TicketType<T> create(String name, Comparator<T> argumentComparator) {
- return new TicketType<>(name, argumentComparator, 0L);
-diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-index 333a02e08cccf5cb0efa2076582cbd69e95ff0c0..ca4c8e256047a4af45811c3e772b5a959e2ae941 100644
---- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-+++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java
-@@ -109,6 +109,27 @@ public class WorldGenRegion implements WorldGenLevel {
- }
- }
-
-+ // Paper start - starlight
-+ @Override
-+ public int getBrightness(final net.minecraft.world.level.LightLayer lightLayer, final BlockPos blockPos) {
-+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
-+ if (!chunk.isLightCorrect()) {
-+ return 0;
-+ }
-+ return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos);
-+ }
-+
-+
-+ @Override
-+ public int getRawBrightness(final BlockPos blockPos, final int subtract) {
-+ final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4);
-+ if (!chunk.isLightCorrect()) {
-+ return 0;
-+ }
-+ return this.getLightEngine().getRawBrightness(blockPos, subtract);
-+ }
-+ // Paper end - starlight
-+
- public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) {
- return this.level.getChunkSource().chunkMap.isOldChunkAround(chunkPos, checkRadius);
- }
-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 f87d9cb38caf3bf92fd32f2118f76799ede418db..c7da359c525522b55763e594a1db0c26a026b73f 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
-@@ -812,6 +812,7 @@ public abstract class BlockBehaviour implements FeatureElement {
- this.spawnTerrainParticles = blockbase_info.spawnTerrainParticles;
- this.instrument = blockbase_info.instrument;
- this.replaceable = blockbase_info.replaceable;
-+ this.conditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; // Paper
- }
- // Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time
- private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData;
-@@ -848,6 +849,18 @@ public abstract class BlockBehaviour implements FeatureElement {
- return this.shapeExceedsCube;
- }
- // Paper end
-+ // Paper start - starlight
-+ protected int opacityIfCached = -1;
-+ // ret -1 if opacity is dynamic, or -1 if the block is conditionally full opaque, else return opacity in [0, 15]
-+ public final int getOpacityIfCached() {
-+ return this.opacityIfCached;
-+ }
-+
-+ protected final boolean conditionallyFullOpaque;
-+ public final boolean isConditionallyFullOpaque() {
-+ return this.conditionallyFullOpaque;
-+ }
-+ // Paper end - starlight
-
- public void initCache() {
- this.fluidState = ((Block) this.owner).getFluidState(this.asState());
-@@ -856,6 +869,7 @@ public abstract class BlockBehaviour implements FeatureElement {
- this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState());
- }
- this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
-+ this.opacityIfCached = this.cache == null || this.isConditionallyFullOpaque() ? -1 : this.cache.lightBlock; // Paper - starlight - cache opacity for light
-
- this.legacySolid = this.calculateSolid();
- }
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-index f4e3bd2ae4f63e6d3d25463a3635b8f89fecc068..1f8c72b6c7d8683d67880fa175843c73b3d39b78 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
-@@ -77,7 +77,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
- @Nullable
- protected BlendingData blendingData;
- public final Map<Heightmap.Types, Heightmap> heightmaps = Maps.newEnumMap(Heightmap.Types.class);
-- protected ChunkSkyLightSources skyLightSources;
-+ // Paper - starlight - remove skyLightSources
- private final Map<Structure, StructureStart> structureStarts = Maps.newHashMap();
- private final Map<Structure, LongSet> structuresRefences = Maps.newHashMap();
- protected final Map<BlockPos, CompoundTag> pendingBlockEntities = Maps.newHashMap();
-@@ -89,8 +89,55 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
- private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
- public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY);
- // CraftBukkit end
-+ // Paper start - rewrite light engine
-+ private volatile ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles;
-+
-+ private volatile ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles;
-+
-+ private volatile boolean[] skyEmptinessMap;
-+
-+ private volatile boolean[] blockEmptinessMap;
-+
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() {
-+ return this.blockNibbles;
-+ }
-+
-+ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {
-+ this.blockNibbles = nibbles;
-+ }
-+
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() {
-+ return this.skyNibbles;
-+ }
-+
-+ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {
-+ this.skyNibbles = nibbles;
-+ }
-+
-+ public boolean[] getSkyEmptinessMap() {
-+ return this.skyEmptinessMap;
-+ }
-+
-+ public void setSkyEmptinessMap(final boolean[] emptinessMap) {
-+ this.skyEmptinessMap = emptinessMap;
-+ }
-+
-+ public boolean[] getBlockEmptinessMap() {
-+ return this.blockEmptinessMap;
-+ }
-+
-+ public void setBlockEmptinessMap(final boolean[] emptinessMap) {
-+ this.blockEmptinessMap = emptinessMap;
-+ }
-+ // Paper end - rewrite light engine
-
- public ChunkAccess(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry<Biome> biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) {
-+ // Paper start - rewrite light engine
-+ if (!(this instanceof ImposterProtoChunk)) {
-+ this.setBlockNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
-+ this.setSkyNibbles(ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(heightLimitView));
-+ }
-+ // Paper end - rewrite light engine
- this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups
- this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key
- this.upgradeData = upgradeData;
-@@ -99,7 +146,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
- this.inhabitedTime = inhabitedTime;
- this.postProcessing = new ShortList[heightLimitView.getSectionsCount()];
- this.blendingData = blendingData;
-- this.skyLightSources = new ChunkSkyLightSources(heightLimitView);
-+ // Paper - starlight - remove skyLightSources
- if (sectionArray != null) {
- if (this.sections.length == sectionArray.length) {
- System.arraycopy(sectionArray, 0, this.sections, 0, this.sections.length);
-@@ -510,12 +557,12 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom
- }
-
- public void initializeLightSources() {
-- this.skyLightSources.fillFrom(this);
-+ // Paper - starlight - remove skyLightSources
- }
-
- @Override
- public ChunkSkyLightSources getSkyLightSources() {
-- return this.skyLightSources;
-+ return null; // Paper - starlight - remove skyLightSources
- }
-
- public static record TicksToSave(SerializableTickContainer<Block> blocks, SerializableTickContainer<Fluid> fluids) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-index 2ee1658532cb00d7bcd1d11e03f19d21ca7f2a9e..ac754827172a4de600d0a57a7d11853481a2dbf2 100644
---- a/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/EmptyLevelChunk.java
-@@ -21,6 +21,40 @@ public class EmptyLevelChunk extends LevelChunk {
- this.biome = biomeEntry;
- }
-
-+ // Paper start - starlight
-+ @Override
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() {
-+ return ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
-+ }
-+
-+ @Override
-+ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {}
-+
-+ @Override
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() {
-+ return ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(this.getLevel());
-+ }
-+
-+ @Override
-+ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {}
-+
-+ @Override
-+ public boolean[] getSkyEmptinessMap() {
-+ return null;
-+ }
-+
-+ @Override
-+ public void setSkyEmptinessMap(final boolean[] emptinessMap) {}
-+
-+ @Override
-+ public boolean[] getBlockEmptinessMap() {
-+ return null;
-+ }
-+
-+ @Override
-+ public void setBlockEmptinessMap(final boolean[] emptinessMap) {}
-+ // Paper end - starlight
-+
- @Override
- public BlockState getBlockState(BlockPos pos) {
- return Blocks.VOID_AIR.defaultBlockState();
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-index 2953e93965aa688be8fc1620580701ba0c9d907e..aa5dee839d4c0dbc3c2abee9b501ec250c575cb3 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
-@@ -47,6 +47,48 @@ public class ImposterProtoChunk extends ProtoChunk {
- this.allowWrites = propagateToWrapped;
- }
-
-+ // Paper start - rewrite light engine
-+ @Override
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getBlockNibbles() {
-+ return this.wrapped.getBlockNibbles();
-+ }
-+
-+ @Override
-+ public void setBlockNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {
-+ this.wrapped.setBlockNibbles(nibbles);
-+ }
-+
-+ @Override
-+ public ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] getSkyNibbles() {
-+ return this.wrapped.getSkyNibbles();
-+ }
-+
-+ @Override
-+ public void setSkyNibbles(final ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] nibbles) {
-+ this.wrapped.setSkyNibbles(nibbles);
-+ }
-+
-+ @Override
-+ public boolean[] getSkyEmptinessMap() {
-+ return this.wrapped.getSkyEmptinessMap();
-+ }
-+
-+ @Override
-+ public void setSkyEmptinessMap(final boolean[] emptinessMap) {
-+ this.wrapped.setSkyEmptinessMap(emptinessMap);
-+ }
-+
-+ @Override
-+ public boolean[] getBlockEmptinessMap() {
-+ return this.wrapped.getBlockEmptinessMap();
-+ }
-+
-+ @Override
-+ public void setBlockEmptinessMap(final boolean[] emptinessMap) {
-+ this.wrapped.setBlockEmptinessMap(emptinessMap);
-+ }
-+ // Paper end - rewrite light engine
-+
- @Nullable
- @Override
- public BlockEntity getBlockEntity(BlockPos pos) {
-diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-index 8de6ad8b131061b2dae440dff71e2e6e7af2de39..bac191f92ea3735df19c68d5568c2c7962c8680f 100644
---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
-@@ -222,6 +222,12 @@ public class LevelChunk extends ChunkAccess {
-
- public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
- this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());
-+ // Paper start - rewrite light engine
-+ this.setBlockNibbles(protoChunk.getBlockNibbles());
-+ this.setSkyNibbles(protoChunk.getSkyNibbles());
-+ this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap());
-+ this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap());
-+ // Paper end - rewrite light engine
- Iterator iterator = protoChunk.getBlockEntities().values().iterator();
-
- while (iterator.hasNext()) {
-@@ -248,7 +254,7 @@ public class LevelChunk extends ChunkAccess {
- }
- }
-
-- this.skyLightSources = protoChunk.skyLightSources;
-+ // Paper - starlight - remove skyLightSources
- this.setLightCorrect(protoChunk.isLightCorrect());
- this.unsaved = true;
- this.needsDecoration = true; // CraftBukkit
-@@ -437,7 +443,7 @@ public class LevelChunk extends ChunkAccess {
- ProfilerFiller gameprofilerfiller = this.level.getProfiler();
-
- gameprofilerfiller.push("updateSkyLightSources");
-- this.skyLightSources.update(this, j, i, l);
-+ // Paper - starlight - remove skyLightSources
- gameprofilerfiller.popPush("queueCheckLight");
- this.level.getChunkSource().getLightEngine().checkBlock(blockposition);
- gameprofilerfiller.pop();
-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 2fa0097a9374a89177e4f1068d1bfed30b8ff122..fa9df6ebcd90d4e9e5836a37212b1f60665783b1 100644
---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
-@@ -155,7 +155,7 @@ public class PalettedContainer<T> implements PaletteResize<T>, PalettedContainer
- return this.get(this.strategy.getIndex(x, y, z));
- }
-
-- protected T get(int index) {
-+ public T get(int index) { // Paper - public
- PalettedContainer.Data<T> data = this.data;
- return data.palette.valueFor(data.storage.get(index));
- }
-diff --git a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-index bcc70883d23d38c408130ffe778205e371ff4e8a..576ae0cb138b265c8a3995de7b5ebc827d50949d 100644
---- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
-@@ -143,7 +143,7 @@ public class ProtoChunk extends ChunkAccess {
- }
-
- if (LightEngine.hasDifferentLightProperties(this, pos, blockState, state)) {
-- this.skyLightSources.update(this, m, j, o);
-+ // Paper - starlight - remove skyLightSources
- this.lightEngine.checkBlock(pos);
- }
- }
-diff --git a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-index ae992aeb8b836e8c2e5bab338ae46cc31c317245..95318092f8281d98132d1d3ceb4a5c36cf32eb05 100644
---- a/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/status/ChunkStatus.java
-@@ -117,6 +117,18 @@ public class ChunkStatus {
- private final ChunkType chunkType;
- private final EnumSet<Heightmap.Types> heightmapsAfter;
-
-+ // Paper start - starlight
-+ public static ChunkStatus getStatus(String name) {
-+ try {
-+ // We need this otherwise we return EMPTY for invalid names
-+ ResourceLocation key = new ResourceLocation(name);
-+ return BuiltInRegistries.CHUNK_STATUS.getOptional(key).orElse(null);
-+ } catch (Exception ex) {
-+ return null; // invalid name
-+ }
-+ }
-+ // Paper end - starlight
-+
- private static ChunkStatus register(
- String id,
- @Nullable ChunkStatus previous,
-diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-index 88f0aca2da0e14ed5ec0513944fa0ba28b73b5d1..01d6b8683a9fa30d05b03ebfef8ee2dca4e83a56 100644
---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java
-@@ -90,6 +90,14 @@ public class ChunkSerializer {
- private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion();
- private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion");
- // Paper end - Do not let the server load chunks from newer versions
-+ // Paper start - replace light engine impl
-+ private static final int STARLIGHT_LIGHT_VERSION = 9;
-+
-+ 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";
-+ // Paper end - replace light engine impl
-+
- public ChunkSerializer() {}
-
- // Paper start - guard against serializing mismatching coordinates
-@@ -121,19 +129,26 @@ public class ChunkSerializer {
- }
-
- UpgradeData chunkconverter = nbt.contains("UpgradeData", 10) ? new UpgradeData(nbt.getCompound("UpgradeData"), world) : UpgradeData.EMPTY;
-- boolean flag = nbt.getBoolean("isLightOn");
-+ boolean flag = getStatus(nbt) != null && getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbt.get("isLightOn") != null && nbt.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Paper
- ListTag nbttaglist = nbt.getList("sections", 10);
- int i = world.getSectionsCount();
- LevelChunkSection[] achunksection = new LevelChunkSection[i];
- boolean flag1 = world.dimensionType().hasSkyLight();
- ServerChunkCache chunkproviderserver = world.getChunkSource();
- LevelLightEngine levellightengine = chunkproviderserver.getLightEngine();
-+ // Paper start
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world);
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.common.light.StarLightEngine.getFilledEmptyLight(world);
-+ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world);
-+ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world);
-+ boolean canReadSky = world.dimensionType().hasSkyLight();
-+ // Paper end
- Registry<Biome> iregistry = world.registryAccess().registryOrThrow(Registries.BIOME);
- Codec<PalettedContainer<Holder<Biome>>> codec = ChunkSerializer.makeBiomeCodecRW(iregistry); // CraftBukkit - read/write
- boolean flag2 = false;
-
- for (int j = 0; j < nbttaglist.size(); ++j) {
-- CompoundTag nbttagcompound1 = nbttaglist.getCompound(j);
-+ CompoundTag nbttagcompound1 = nbttaglist.getCompound(j); CompoundTag sectionData = nbttagcompound1; // Paper
- byte b0 = nbttagcompound1.getByte("Y");
- int k = world.getSectionIndexFromSectionY(b0);
-
-@@ -169,19 +184,39 @@ public class ChunkSerializer {
- boolean flag3 = nbttagcompound1.contains("BlockLight", 7);
- boolean flag4 = flag1 && nbttagcompound1.contains("SkyLight", 7);
-
-- if (flag3 || flag4) {
-- if (!flag2) {
-- levellightengine.retainData(chunkPos, true);
-- flag2 = true;
-- }
--
-+ // Paper start - rewrite the light engine
-+ if (flag) {
-+ try {
-+ int y = sectionData.getByte("Y");
-+ // Paper end - rewrite the light engine
- if (flag3) {
-- levellightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("BlockLight")));
-+ // Paper start - rewrite the light engine
-+ // this is where our diff is
-+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety
-+ } else {
-+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG));
-+ // Paper end - rewrite the light engine
- }
-
- if (flag4) {
-- levellightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkPos, b0), new DataLayer(nbttagcompound1.getByteArray("SkyLight")));
-+ // Paper start - rewrite the light engine
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety
-+ } else if (flag1) {
-+ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.common.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG));
-+ // Paper end - rewrite the light engine
-+ }
-+
-+ // Paper start - rewrite the light engine
-+ } catch (Exception ex) {
-+ LOGGER.warn("Failed to load light data for chunk " + chunkPos + " in world '" + world.getWorld().getName() + "', light will be regenerated", ex);
-+ flag = false;
- }
-+ // Paper end - rewrite light engine
- }
- }
-
-@@ -211,6 +246,8 @@ public class ChunkSerializer {
- }, chunkPos);
-
- object1 = new LevelChunk(world.getLevel(), chunkPos, chunkconverter, levelchunkticks, levelchunkticks1, l, achunksection, ChunkSerializer.postLoadChunk(world, nbt), blendingdata);
-+ ((LevelChunk)object1).setBlockNibbles(blockNibbles); // Paper - replace light impl
-+ ((LevelChunk)object1).setSkyNibbles(skyNibbles); // Paper - replace light impl
- } else {
- ProtoChunkTicks<Block> protochunkticklist = ProtoChunkTicks.load(nbt.getList("block_ticks", 10), (s) -> {
- return BuiltInRegistries.BLOCK.getOptional(ResourceLocation.tryParse(s));
-@@ -219,6 +256,8 @@ public class ChunkSerializer {
- return BuiltInRegistries.FLUID.getOptional(ResourceLocation.tryParse(s));
- }, chunkPos);
- ProtoChunk protochunk = new ProtoChunk(chunkPos, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, world, iregistry, blendingdata);
-+ protochunk.setBlockNibbles(blockNibbles); // Paper - replace light impl
-+ protochunk.setSkyNibbles(skyNibbles); // Paper - replace light impl
-
- object1 = protochunk;
- protochunk.setInhabitedTime(l);
-@@ -340,6 +379,12 @@ public class ChunkSerializer {
- // CraftBukkit end
-
- public static CompoundTag write(ServerLevel world, ChunkAccess chunk) {
-+ // Paper start - rewrite light impl
-+ final int minSection = io.papermc.paper.util.WorldUtil.getMinLightSection(world);
-+ final int maxSection = io.papermc.paper.util.WorldUtil.getMaxLightSection(world);
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles();
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles();
-+ // Paper end - rewrite light impl
- ChunkPos chunkcoordintpair = chunk.getPos();
- CompoundTag nbttagcompound = NbtUtils.addCurrentDataVersion(new CompoundTag());
-
-@@ -389,11 +434,14 @@ public class ChunkSerializer {
- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) {
- int j = chunk.getSectionIndexFromSectionY(i);
- boolean flag1 = j >= 0 && j < achunksection.length;
-- DataLayer nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
-- DataLayer nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i));
-+ // Paper - replace light engine
-
-- if (flag1 || nibblearray != null || nibblearray1 != null) {
-- CompoundTag nbttagcompound1 = new CompoundTag();
-+ // Paper start - replace light engine
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState();
-+ ca.spottedleaf.starlight.common.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState();
-+ if (flag1 || blockNibble != null || skyNibble != null) {
-+ // Paper end - replace light engine
-+ CompoundTag nbttagcompound1 = new CompoundTag(); CompoundTag section = nbttagcompound1; // Paper
-
- if (flag1) {
- LevelChunkSection chunksection = achunksection[j];
-@@ -402,13 +450,27 @@ public class ChunkSerializer {
- nbttagcompound1.put("biomes", (Tag) codec.encodeStart(NbtOps.INSTANCE, chunksection.getBiomes()).getOrThrow());
- }
-
-- if (nibblearray != null && !nibblearray.isEmpty()) {
-- nbttagcompound1.putByteArray("BlockLight", nibblearray.getData());
-+ // Paper start
-+ // we store under the same key so mod programs editing nbt
-+ // can still read the data, hopefully.
-+ // however, for compatibility we store chunks as unlit so vanilla
-+ // is forced to re-light them if it encounters our data. It's too much of a burden
-+ // to try and maintain compatibility with a broken and inferior skylight management system.
-+
-+ if (blockNibble != null) {
-+ if (blockNibble.data != null) {
-+ section.putByteArray("BlockLight", blockNibble.data);
-+ }
-+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state);
- }
-
-- if (nibblearray1 != null && !nibblearray1.isEmpty()) {
-- nbttagcompound1.putByteArray("SkyLight", nibblearray1.getData());
-+ if (skyNibble != null) {
-+ if (skyNibble.data != null) {
-+ section.putByteArray("SkyLight", skyNibble.data);
-+ }
-+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state);
- }
-+ // Paper end
-
- if (!nbttagcompound1.isEmpty()) {
- nbttagcompound1.putByte("Y", (byte) i);
-@@ -419,7 +481,8 @@ public class ChunkSerializer {
-
- nbttagcompound.put("sections", nbttaglist);
- if (flag) {
-- nbttagcompound.putBoolean("isLightOn", true);
-+ nbttagcompound.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Paper
-+ nbttagcompound.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_)
- }
-
- ListTag nbttaglist1 = new ListTag();
-@@ -493,6 +556,17 @@ public class ChunkSerializer {
- }));
- }
-
-+ // Paper start
-+ public static @Nullable ChunkStatus getStatus(@Nullable CompoundTag compound) {
-+ if (compound == null) {
-+ return null;
-+ }
-+
-+ // Note: Copied from below
-+ return ChunkStatus.getStatus(compound.getString("Status"));
-+ }
-+ // Paper end
-+
- public static ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) {
- return nbt != null ? ChunkStatus.byName(nbt.getString("Status")).getChunkType() : ChunkType.PROTOCHUNK;
- }
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-index 34f03dd227181a03fa90845067424a26382bab9b..4b6a04e47f5d4c071607516519098fab317dcf12 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-@@ -507,12 +507,7 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- }
- }
-
-- for (final ChunkPos pos : chunksToRelight) {
-- final ChunkAccess chunk = serverChunkCache.getChunk(pos.x, pos.z, false);
-- if (chunk != null) {
-- serverChunkCache.getLightEngine().lightChunk(chunk, false);
-- }
-- }
-+ serverChunkCache.getLightEngine().relight(chunksToRelight, pos -> {}, relit -> {}); // Paper - Starlight
-
- return true;
- // Paper end - implement regenerate chunk method