From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <spottedleaf@spottedleaf.dev>
Date: Wed, 28 Oct 2020 16:51:55 -0700
Subject: [PATCH] Rewrite the light engine

The standard vanilla light engine is plagued by
awful performance. Paper's changes to the light engine
help a bit, however they appear to cause some lighting
errors - most easily noticed in coral generation.

The vanilla light engine's is too abstract to be modified -
so an entirely new implementation is required to fix the
performance and lighting errors.

The new implementation is designed primarily to optimise
light level propagations (increase and decrease). Unlike
the vanilla light engine, this implementation tracks more
information per queued value when performing a
breadth first search. Vanilla just tracks coordinate, which
means every time they handle a queued value, they must
also determine the coordinate's target light level
from its neighbours - very wasteful, especially considering
these checks read neighbour block data.
The new light engine tracks both position and target level,
as well as whether the target block needs to be read at all
(for checking sided propagation). So, the work done per coordinate
is significantly reduced because no work is done for calculating
the target level.
In my testing, the block get calls were reduced by approximately
an order of magnitude. However, the light read checks were only
reduced by approximately 2x - but this is fine, light read checks
are extremely cheap compared to block gets.

Generation testing showed that the new light engine improved
total generation (not lighting itself, but the whole generation process)
by 2x. According to cpu time, the light engine itself spent 10x less time
lighting chunks for generation.

diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
index dcbee6ebfac34f793c18f21e07187ae7dcb37695..c891f7c247c81a865720237f316f352b2289c724 100644
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
@@ -186,6 +186,44 @@ public class PaperCommand extends Command {
         }
     }
 
+    private void starlightFixLight(EntityPlayer sender, WorldServer world, LightEngineThreaded lightengine, int radius) {
+        long start = System.nanoTime();
+        LinkedHashSet<ChunkCoordIntPair> chunks = new LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.getChunkCoordinates(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos
+
+        int[] pending = new int[1];
+        for (java.util.Iterator<ChunkCoordIntPair> iterator = chunks.iterator(); iterator.hasNext();) {
+            final ChunkCoordIntPair chunkPos = iterator.next();
+
+            final IChunkAccess chunk = world.getChunkProvider().getChunkAtImmediately(chunkPos.x, chunkPos.z);
+            if (chunk == null || !chunk.isLit() || !chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) {
+                // cannot relight this chunk
+                iterator.remove();
+                continue;
+            }
+
+            ++pending[0];
+        }
+
+        int[] relitChunks = new int[1];
+        lightengine.relight(chunks,
+                (ChunkCoordIntPair chunkPos) -> {
+                    ++relitChunks[0];
+                    sender.getBukkitEntity().sendMessage(
+                            ChatColor.BLUE + "Relit chunk " + ChatColor.DARK_AQUA + chunkPos + ChatColor.BLUE +
+                                    ", progress: " + ChatColor.DARK_AQUA + (int)(Math.round(100.0 * (double)(relitChunks[0])/(double)pending[0])) + "%"
+                    );
+                },
+                (int totalRelit) -> {
+                    final long end = System.nanoTime();
+                    final long diff = Math.round(1.0e-6*(end - start));
+                    sender.getBukkitEntity().sendMessage(
+                            ChatColor.BLUE + "Relit " + ChatColor.DARK_AQUA + totalRelit + ChatColor.BLUE + " chunks. Took " +
+                                    ChatColor.DARK_AQUA + diff + "ms"
+                    );
+                });
+        sender.getBukkitEntity().sendMessage(ChatColor.BLUE + "Relighting " + ChatColor.DARK_AQUA + pending[0] + ChatColor.BLUE + " chunks");
+    }
+
     private void doFixLight(CommandSender sender, String[] args) {
         if (!(sender instanceof Player)) {
             sender.sendMessage("Only players can use this command");
@@ -194,7 +232,7 @@ public class PaperCommand extends Command {
         int radius = 2;
         if (args.length > 1) {
             try {
-                radius = Math.min(5, Integer.parseInt(args[1]));
+                radius = Math.min(15, Integer.parseInt(args[1])); // Tuinity - MOOOOOORE
             } catch (Exception e) {
                 sender.sendMessage("Not a number");
                 return;
@@ -207,6 +245,13 @@ public class PaperCommand extends Command {
         net.minecraft.server.WorldServer world = (WorldServer) handle.world;
         LightEngineThreaded lightengine = world.getChunkProvider().getLightEngine();
 
+        // Tuinity start - rewrite light engine
+        if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) {
+            this.starlightFixLight(handle, world, lightengine, radius);
+            return;
+        }
+        // Tuinity end - rewrite light engine
+
         BlockPosition center = MCUtil.toBlockPosition(player.getLocation());
         Deque<ChunkCoordIntPair> queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius));
         updateLight(sender, world, lightengine, queue);
diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/BlockStarLightEngine.java b/src/main/java/com/tuinity/tuinity/chunk/light/BlockStarLightEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..43de95779a5472aaf04da11c9f4c5feb7253c6c3
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/BlockStarLightEngine.java
@@ -0,0 +1,277 @@
+package com.tuinity.tuinity.chunk.light;
+
+import net.minecraft.server.BlockPosition;
+import net.minecraft.server.Chunk;
+import net.minecraft.server.ChunkSection;
+import net.minecraft.server.ChunkStatus;
+import net.minecraft.server.DataPaletteBlock;
+import net.minecraft.server.IBlockData;
+import net.minecraft.server.IChunkAccess;
+import net.minecraft.server.ILightAccess;
+import net.minecraft.server.ProtoChunkExtension;
+import net.minecraft.server.VoxelShape;
+import net.minecraft.server.VoxelShapes;
+import net.minecraft.server.World;
+import net.minecraft.server.WorldServer;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+public final class BlockStarLightEngine extends StarLightEngine {
+
+    public BlockStarLightEngine(final World world) {
+        super(false, world);
+    }
+
+    @Override
+    protected boolean[] getEmptinessMap(final IChunkAccess chunk) {
+        return chunk.getBlockEmptinessMap();
+    }
+
+    @Override
+    protected void setEmptinessMap(final IChunkAccess chunk, final boolean[] to) {
+        chunk.setBlockEmptinessMap(to);
+    }
+
+    @Override
+    protected SWMRNibbleArray[] getNibblesOnChunk(final IChunkAccess chunk) {
+        return chunk.getBlockNibbles();
+    }
+
+    @Override
+    protected void setNibbles(final IChunkAccess chunk, final SWMRNibbleArray[] to) {
+        chunk.setBlockNibbles(to);
+    }
+
+    @Override
+    protected boolean canUseChunk(final IChunkAccess chunk) {
+        return chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLit());
+    }
+
+    @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, new SWMRNibbleArray());
+            }
+        } else {
+            nibble.setNonNull();
+        }
+    }
+
+    @Override
+    protected final void checkBlock(final ILightAccess 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 VariableBlockLightHandler customBlockHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers;
+        final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
+        final IBlockData blockState = this.getBlockState(worldX, worldY, worldZ);
+        final int emittedLevel = (customBlockHandler != null ? this.getCustomLightLevel(customBlockHandler, worldX, worldY, worldZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & 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 BlockPosition.MutableBlockPosition recalcCenterPos = new BlockPosition.MutableBlockPosition();
+    protected final BlockPosition.MutableBlockPosition recalcNeighbourPos = new BlockPosition.MutableBlockPosition();
+
+    @Override
+    protected int calculateLightValue(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ,
+                                      final int expect, final VariableBlockLightHandler customBlockLight) {
+        final IBlockData centerState = this.getBlockState(worldX, worldY, worldZ);
+        int level = centerState.getEmittedLight() & 0xFF;
+        if (customBlockLight != null) {
+            level = this.getCustomLightLevel(customBlockLight, worldX, worldY, worldZ, level);
+        }
+
+        if (level >= (15 - 1) || level > expect) {
+            return level;
+        }
+
+        final int sectionOffset = this.chunkSectionIndexOffset;
+        final IBlockData conditionallyOpaqueState;
+        int opacity = centerState.getOpacityIfCached();
+
+        if (opacity == -1) {
+            this.recalcCenterPos.setValues(worldX, worldY, worldZ);
+            opacity = centerState.getOpacity(lightAccess.getWorld(), 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 IBlockData 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.setValues(offX, offY, offZ);
+                final VoxelShape neighbourFace = neighbourState.getCullingFace(lightAccess.getWorld(), this.recalcNeighbourPos, direction.opposite.nms);
+                final VoxelShape thisFace = conditionallyOpaqueState == null ? VoxelShapes.empty() : conditionallyOpaqueState.getCullingFace(lightAccess.getWorld(), this.recalcCenterPos, direction.nms);
+                if (VoxelShapes.combinationOccludes(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 ILightAccess lightAccess, final IChunkAccess atChunk, final Set<BlockPosition> positions) {
+        for (final BlockPosition pos : positions) {
+            this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
+        }
+
+        this.performLightDecrease(lightAccess);
+    }
+
+    protected Iterator<BlockPosition> getSources(final ILightAccess lightAccess, final IChunkAccess chunk) {
+        if (chunk instanceof ProtoChunkExtension || chunk instanceof Chunk) {
+            // implementation on Chunk is pretty awful, so write our own here. The big optimisation is
+            // skipping empty sections, and the far more optimised reading of types.
+            List<BlockPosition> sources = new ArrayList<>();
+
+            int offX = chunk.getPos().x << 4;
+            int offZ = chunk.getPos().z << 4;
+
+            final ChunkSection[] sections = chunk.getSections();
+            for (int sectionY = 0; sectionY <= 15; ++sectionY) {
+                if (sections[sectionY] == null || sections[sectionY].isFullOfAir()) {
+                    // no sources in empty sections
+                    continue;
+                }
+                final DataPaletteBlock<IBlockData> section = sections[sectionY].blockIds;
+                final int offY = sectionY << 4;
+
+                for (int index = 0; index < (16 * 16 * 16); ++index) {
+                    final IBlockData state = section.rawGet(index);
+                    if (state.getEmittedLight() <= 0) {
+                        continue;
+                    }
+
+                    // index = x | (z << 4) | (y << 8)
+                    sources.add(new BlockPosition(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15)));
+                }
+            }
+
+            final VariableBlockLightHandler customBlockHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers;
+            if (customBlockHandler == null) {
+                return sources.iterator();
+            }
+
+            final Set<BlockPosition> ret = new HashSet<>(sources);
+            ret.addAll(customBlockHandler.getCustomLightPositions(chunk.getPos().x, chunk.getPos().z));
+
+            return ret.iterator();
+        } else {
+            return chunk.getLightSources().iterator();
+        }
+    }
+
+    @Override
+    public void lightChunk(final ILightAccess lightAccess, final IChunkAccess chunk, final boolean needsEdgeChecks) {
+        // setup sources
+        final int emittedMask = this.emittedLightMask;
+        final VariableBlockLightHandler customBlockHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers;
+        for (final Iterator<BlockPosition> positions = this.getSources(lightAccess, chunk); positions.hasNext();) {
+            final BlockPosition pos = positions.next();
+            final IBlockData blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ());
+            final int emittedLight = (customBlockHandler != null ? this.getCustomLightLevel(customBlockHandler, pos.getX(), pos.getY(), pos.getZ(), blockState.getEmittedLight()) : blockState.getEmittedLight()) & 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/com/tuinity/tuinity/chunk/light/SWMRNibbleArray.java b/src/main/java/com/tuinity/tuinity/chunk/light/SWMRNibbleArray.java
new file mode 100644
index 0000000000000000000000000000000000000000..051e2db5349b6f20887841efad7fbc183b190f68
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/SWMRNibbleArray.java
@@ -0,0 +1,325 @@
+package com.tuinity.tuinity.chunk.light;
+
+import net.minecraft.server.NibbleArray;
+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
+
+    public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block
+    protected static final byte[] FULL_LIT = new byte[ARRAY_SIZE];
+    static {
+        Arrays.fill(FULL_LIT, (byte)-1);
+    }
+    // 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);
+    }
+
+    protected int stateUpdating;
+    protected volatile int stateVisible;
+
+    protected byte[] storageUpdating;
+    protected boolean updatingDirty; // only returns whether storageUpdating is dirty
+    protected 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();
+        }
+        this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT;
+        this.storageUpdating = this.storageVisible = bytes;
+    }
+
+    // operation type: visible
+    public boolean isAllZero() {
+        final int state = this.stateVisible;
+
+        if (state == INIT_STATE_NULL) {
+            return false;
+        } else if (state == INIT_STATE_UNINIT) {
+            return true;
+        }
+
+        synchronized (this) {
+            final byte[] bytes = this.storageVisible;
+
+            if (bytes == null) {
+                return this.stateVisible == INIT_STATE_UNINIT;
+            }
+
+            for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) {
+                byte whole = bytes[i << 4];
+
+                for (int k = 1; k < (1 << 4); ++k) {
+                    whole |= bytes[(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.storageUpdating != null) {
+            into = this.storageUpdating;
+        } else {
+            this.storageUpdating = into = allocateBytes();
+            this.stateUpdating = INIT_STATE_INIT;
+        }
+        this.updatingDirty = true;
+
+        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() {
+        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() {
+        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_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 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
+    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);
+        }
+
+        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 {
+                    System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE);
+                }
+
+                freeBytes(this.storageUpdating);
+                this.storageUpdating = this.storageVisible;
+            }
+            this.updatingDirty = false;
+            this.stateVisible = this.stateUpdating;
+        }
+
+        return true;
+    }
+
+    // operation type: visible
+    public NibbleArray toVanillaNibble() {
+        synchronized (this) {
+            switch (this.stateVisible) {
+                case INIT_STATE_NULL:
+                    return null;
+                case INIT_STATE_UNINIT:
+                    return new NibbleArray();
+                case INIT_STATE_INIT:
+                    return new NibbleArray(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) {
+        synchronized (this) {
+            // 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));
+    }
+}
diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/SkyStarLightEngine.java b/src/main/java/com/tuinity/tuinity/chunk/light/SkyStarLightEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..26534e05ff5aac5a963906ba678411c843b132e3
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/SkyStarLightEngine.java
@@ -0,0 +1,706 @@
+package com.tuinity.tuinity.chunk.light;
+
+import com.tuinity.tuinity.util.WorldUtil;
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
+import it.unimi.dsi.fastutil.shorts.ShortIterator;
+import net.minecraft.server.BlockPosition;
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.ChunkSection;
+import net.minecraft.server.ChunkStatus;
+import net.minecraft.server.IBlockAccess;
+import net.minecraft.server.IBlockData;
+import net.minecraft.server.IChunkAccess;
+import net.minecraft.server.ILightAccess;
+import net.minecraft.server.VoxelShape;
+import net.minecraft.server.VoxelShapes;
+import net.minecraft.server.World;
+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 World 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);
+    }
+
+    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 ChunkSection current = this.getChunkSection(chunkX, currY, chunkZ);
+                if (current == null || current == EMPTY_CHUNK_SECTION) {
+                    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
+            this.getNibbleFromCache(chunkX, chunkY, chunkZ).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.extrudeLower(nibble);
+                    break;
+                }
+            }
+        } else {
+            currNibble.setNonNull();
+        }
+    }
+
+    protected final void rewriteNibbleCacheForSkylight(final IChunkAccess 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 IChunkAccess chunk) {
+        return chunk.getSkyEmptinessMap();
+    }
+
+    @Override
+    protected void setEmptinessMap(final IChunkAccess chunk, final boolean[] to) {
+        chunk.setSkyEmptinessMap(to);
+    }
+
+    @Override
+    protected SWMRNibbleArray[] getNibblesOnChunk(final IChunkAccess chunk) {
+        return chunk.getSkyNibbles();
+    }
+
+    @Override
+    protected void setNibbles(final IChunkAccess chunk, final SWMRNibbleArray[] to) {
+        chunk.setSkyNibbles(to);
+    }
+
+    @Override
+    protected boolean canUseChunk(final IChunkAccess chunk) {
+        // can only use chunks for sky stuff if their sections have been init'd
+        return chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT) && (this.isClientSide ? true : chunk.isLit());
+    }
+
+    @Override
+    protected void checkChunkEdges(final ILightAccess lightAccess, final IChunkAccess 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 ILightAccess lightAccess, final IChunkAccess 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);
+    }
+
+
+    protected final BlockPosition.MutableBlockPosition recalcCenterPos = new BlockPosition.MutableBlockPosition();
+    protected final BlockPosition.MutableBlockPosition recalcNeighbourPos = new BlockPosition.MutableBlockPosition();
+
+    @Override
+    protected int calculateLightValue(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ,
+                                      final int expect, final VariableBlockLightHandler customBlockLight) {
+        if (expect == 15) {
+            return expect;
+        }
+
+        final int sectionOffset = this.chunkSectionIndexOffset;
+        final IBlockData centerState = this.getBlockState(worldX, worldY, worldZ);
+        int opacity = centerState.getOpacityIfCached();
+
+
+        final IBlockData conditionallyOpaqueState;
+        if (opacity < 0) {
+            this.recalcCenterPos.setValues(worldX, worldY, worldZ);
+            opacity = Math.max(1, centerState.getOpacity(lightAccess.getWorld(), 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 IBlockData 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.setValues(offX, offY, offZ);
+                final VoxelShape neighbourFace = neighbourState.getCullingFace(lightAccess.getWorld(), this.recalcNeighbourPos, direction.opposite.nms);
+                final VoxelShape thisFace = conditionallyOpaqueState == null ? VoxelShapes.empty() : conditionallyOpaqueState.getCullingFace(lightAccess.getWorld(), this.recalcCenterPos, direction.nms);
+                if (VoxelShapes.combinationOccludes(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 checkBlock(final ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ) {
+        // blocks can change opacity
+        // blocks can change direction of propagation
+
+        // same logic applies from BlockStarLightEngine#checkBlock
+
+        final int encodeOffset = this.coordinateOffset;
+
+        final int currentLevel = this.getLightLevel(worldX, worldY, worldZ);
+
+        if (currentLevel == 15) {
+            // must re-propagate clobbered source
+            this.appendToIncreaseQueue(
+                    ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+                            | (currentLevel & 0xFL) << (6 + 6 + 16)
+                            | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+                            | FLAG_HAS_SIDED_TRANSPARENT_BLOCKS // don't know if the block is conditionally transparent
+            );
+        } else {
+            this.setLightLevel(worldX, worldY, worldZ, 0);
+        }
+
+        this.appendToDecreaseQueue(
+                ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1))
+                        | (currentLevel & 0xFL) << (6 + 6 + 16)
+                        | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4))
+        );
+    }
+
+    @Override
+    protected void propagateBlockChanges(final ILightAccess lightAccess, final IChunkAccess atChunk, final Set<BlockPosition> positions) {
+        this.rewriteNibbleCacheForSkylight(atChunk);
+        Arrays.fill(this.nullPropagationCheckCache, false);
+
+        final IBlockAccess world = lightAccess.getWorld();
+        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 BlockPosition 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 BlockPosition pos : positions) {
+            this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ());
+        }
+
+        this.performLightDecrease(lightAccess);
+    }
+
+    @Override
+    protected void lightChunk(final ILightAccess lightAccess, final IChunkAccess chunk, final boolean needsEdgeChecks) {
+        this.rewriteNibbleCacheForSkylight(chunk);
+        Arrays.fill(this.nullPropagationCheckCache, false);
+
+        final IBlockAccess world = lightAccess.getWorld();
+        final ChunkCoordIntPair chunkPos = chunk.getPos();
+        final int chunkX = chunkPos.x;
+        final int chunkZ = chunkPos.z;
+
+        final ChunkSection[] sections = chunk.getSections();
+
+        int highestNonEmptySection = this.maxSection;
+        while (highestNonEmptySection == (this.minSection - 1) ||
+                sections[highestNonEmptySection - this.minSection] == null || sections[highestNonEmptySection - this.minSection].isFullOfAir()) {
+            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 IBlockAccess world, final int worldX, int startY, final int worldZ,
+                                             final boolean extrudeInitialised, final boolean delayLightSet) {
+        final BlockPosition.MutableBlockPosition 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);
+
+        IBlockData above = this.getBlockState(worldX, startY + 1, worldZ);
+        if (above == null) {
+            above = AIR_BLOCK_STATE;
+        }
+
+        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);
+            }
+            IBlockData current = this.getBlockState(worldX, startY, worldZ);
+            if (current == null) {
+                current = AIR_BLOCK_STATE;
+            }
+
+            final VoxelShape fromShape;
+            if (above.isConditionallyFullOpaque()) {
+                this.mutablePos2.setValues(worldX, startY + 1, worldZ);
+                fromShape = above.getCullingFace(world, this.mutablePos2, AxisDirection.NEGATIVE_Y.nms);
+                if (VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), fromShape)) {
+                    // above wont let us propagate
+                    break;
+                }
+            } else {
+                fromShape = VoxelShapes.getEmptyShape();
+            }
+
+            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.setValues(worldX, startY, worldZ);
+                long flags = 0L;
+                if (current.isConditionallyFullOpaque()) {
+                    final VoxelShape cullingFace = current.getCullingFace(world, mutablePos, AxisDirection.POSITIVE_Y.nms);
+
+                    if (VoxelShapes.combinationOccludes(fromShape, cullingFace)) {
+                        // can't propagate here, we're done on this column.
+                        break;
+                    }
+                    flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+                }
+
+                final int opacity = current.getOpacity(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/com/tuinity/tuinity/chunk/light/StarLightEngine.java b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..433c27813bbcddea09b21f7629a0bbce7a658082
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightEngine.java
@@ -0,0 +1,1615 @@
+package com.tuinity.tuinity.chunk.light;
+
+import com.tuinity.tuinity.util.CoordinateUtils;
+import com.tuinity.tuinity.util.IntegerUtil;
+import com.tuinity.tuinity.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.server.BlockPosition;
+import net.minecraft.server.Blocks;
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.ChunkSection;
+import net.minecraft.server.EnumDirection;
+import net.minecraft.server.EnumSkyBlock;
+import net.minecraft.server.IBlockAccess;
+import net.minecraft.server.IBlockData;
+import net.minecraft.server.IChunkAccess;
+import net.minecraft.server.ILightAccess;
+import net.minecraft.server.SectionPosition;
+import net.minecraft.server.VoxelShape;
+import net.minecraft.server.VoxelShapes;
+import net.minecraft.server.World;
+import net.minecraft.server.WorldServer;
+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 IBlockData AIR_BLOCK_STATE = Blocks.AIR.getBlockData();
+
+    protected static final ChunkSection EMPTY_CHUNK_SECTION = new ChunkSection(0);
+
+    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 EnumDirection 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 = EnumDirection.from(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 ChunkSection[] 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. no index is null.
+    // index = x + (z * 5)
+    protected final IChunkAccess[] chunkCache = new IChunkAccess[5 * 5];
+
+    // index = x + (z * 5)
+    protected final boolean[][] emptinessMapCache = new boolean[5 * 5][];
+
+    protected final BlockPosition.MutableBlockPosition mutablePos1 = new BlockPosition.MutableBlockPosition();
+    protected final BlockPosition.MutableBlockPosition mutablePos2 = new BlockPosition.MutableBlockPosition();
+    protected final BlockPosition.MutableBlockPosition mutablePos3 = new BlockPosition.MutableBlockPosition();
+
+    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 static final boolean isClientSide = false;
+
+    protected final World world;
+    protected final int minLightSection;
+    protected final int maxLightSection;
+    protected final int minSection;
+    protected final int maxSection;
+
+    protected StarLightEngine(final boolean skylightPropagator, final World world) {
+        this.skylightPropagator = skylightPropagator;
+        this.emittedLightMask = skylightPropagator ? 0 : 0xF;
+        //this.isClientSide = 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 ChunkSection[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 ILightAccess 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 IChunkAccess chunk = (IChunkAccess)chunkProvider.getFeaturesReadyChunk(cx, cz); // mappings are awful here, this is the "get chunk at if at least features"
+
+                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 IChunkAccess 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 IChunkAccess chunk) {
+        this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk;
+    }
+
+    protected final ChunkSection 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 ChunkSection section) {
+        this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section;
+    }
+
+    protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final ChunkSection[] 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 || sections[cy - this.minSection].isFullOfAir() ? EMPTY_CHUNK_SECTION : sections[cy - this.minSection]) : EMPTY_CHUNK_SECTION));
+        }
+    }
+
+    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 ILightAccess 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 chunkY = ((index / (5*5)) % (16 + 2 + 2)) - this.chunkOffsetY;
+            if ((nibble != null && nibble.updateVisible()) || this.notifyUpdateCache[index]) {
+                lightAccess.markLightSectionDirty(this.skylightPropagator ? EnumSkyBlock.SKY : EnumSkyBlock.BLOCK, new SectionPosition(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 IBlockData getBlockState(final int worldX, final int worldY, final int worldZ) {
+        final ChunkSection section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
+
+        if (section != null) {
+            return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.getType(worldX & 15, worldY & 15, worldZ & 15);
+        }
+
+        return null;
+    }
+
+    protected final IBlockData getBlockState(final int sectionIndex, final int localIndex) {
+        final ChunkSection section = this.sectionCache[sectionIndex];
+
+        if (section != null) {
+            return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.blockIds.rawGet(localIndex);
+        }
+
+        return null;
+    }
+
+    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;
+    }
+
+    protected final int getCustomLightLevel(final VariableBlockLightHandler customBlockHandler, final int worldX, final int worldY,
+                                            final int worldZ, final int dfl) {
+        final int ret = customBlockHandler.getLightLevel(worldX, worldY, worldZ);
+        return ret == -1 ? dfl : ret;
+    }
+
+    // :(
+
+    protected final long getKnownTransparency(final int worldX, final int worldY, final int worldZ) {
+        throw new UnsupportedOperationException();
+    }
+
+    // warn: localIndex = y | (x << 4) | (z << 8)
+    protected final long getKnownTransparency(final int sectionIndex, final int localIndex) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * @deprecated To be removed in 1.17 due to variable section count
+     */
+    @Deprecated
+    public static SWMRNibbleArray[] getFilledEmptyLight() {
+        return getFilledEmptyLight(16 - (-1) + 1);
+    }
+
+    public static SWMRNibbleArray[] getFilledEmptyLight(final World 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 IChunkAccess chunk);
+
+    protected abstract void setEmptinessMap(final IChunkAccess chunk, final boolean[] to);
+
+    protected abstract SWMRNibbleArray[] getNibblesOnChunk(final IChunkAccess chunk);
+
+    protected abstract void setNibbles(final IChunkAccess chunk, final SWMRNibbleArray[] to);
+
+    protected abstract boolean canUseChunk(final IChunkAccess chunk);
+
+    // This reference is explicitly stored here so that a heap dump will show what blocks were being processed.
+    // This is to debug a rather nasty unbounded queue problem.
+    protected Set<BlockPosition> changedBlocksSet;
+    public final void blocksChangedInChunk(final ILightAccess lightAccess, final int chunkX, final int chunkZ,
+                                           final Set<BlockPosition> positions, final Boolean[] changedSections) {
+        this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+        try {
+            this.changedBlocksSet = positions;
+            final IChunkAccess chunk = this.getChunkInCache(chunkX, chunkZ);
+            if (this.isClientSide && 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.changedBlocksSet = null;
+            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 ILightAccess lightAccess, final IChunkAccess atChunk, final Set<BlockPosition> positions);
+
+    protected abstract void checkBlock(final ILightAccess 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 ILightAccess lightAccess, final int worldX, final int worldY, final int worldZ,
+                                               final int expect, final VariableBlockLightHandler customBlockLight);
+
+    protected final int[] chunkCheckDelayedUpdatesCenter = new int[16 * 16];
+    protected final int[] chunkCheckDelayedUpdatesNeighbour = new int[16 * 16];
+
+    protected void checkChunkEdge(final ILightAccess lightAccess, final IChunkAccess 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;
+            }
+
+            final VariableBlockLightHandler customLightHandler = ((WorldServer)lightAccess.getWorld()).customBlockLightHandlers;
+            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, customLightHandler) != currentLevel) {
+                        this.chunkCheckDelayedUpdatesCenter[centerDelayedChecks++] = currentIndex;
+                    }
+
+                    if (this.calculateLightValue(lightAccess, neighbourX, currY, neighbourZ, neighbourLevel, customLightHandler) != 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 ILightAccess lightAccess, final IChunkAccess chunk, final ShortCollection sections) {
+        final ChunkCoordIntPair 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 ILightAccess lightAccess, final IChunkAccess chunk, final int fromSection, final int toSection) {
+        final ChunkCoordIntPair 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 ILightAccess lightAccess, final IChunkAccess chunk, final int fromSection, final int toSection) {
+        final ChunkCoordIntPair 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 IChunkAccess chunk) {
+        final ChunkSection[] sections = chunk.getSections();
+        final Boolean[] ret = new Boolean[sections.length];
+
+        for (int i = 0; i < sections.length; ++i) {
+            if (sections[i] == null || sections[i].isFullOfAir()) {
+                ret[i] = Boolean.TRUE;
+            } else {
+                ret[i] = Boolean.FALSE;
+            }
+        }
+
+        return ret;
+    }
+
+    public final void forceHandleEmptySectionChanges(final ILightAccess lightAccess, final IChunkAccess 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 ILightAccess lightAccess, final int chunkX, final int chunkZ,
+                                                final Boolean[] emptinessChanges) {
+        this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, true);
+        try {
+            if (this.isClientSide) {
+                // force current chunk into cache
+                final IChunkAccess chunk =  (IChunkAccess)lightAccess.getFeaturesReadyChunk(chunkX, chunkZ);
+                if (chunk == null) {
+                    // unloaded this frame (or last), and we were still queued
+                    return;
+                }
+                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 IChunkAccess 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);
+
+    // 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 ILightAccess lightAccess, final IChunkAccess chunk,
+                                                        final Boolean[] emptinessChanges, final boolean unlit) {
+        final World world = (World)lightAccess.getWorld();
+        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) {
+            final Boolean valueBoxed = emptinessChanges[sectionIndex];
+            if (valueBoxed == null) {
+                if (needsInit) {
+                    throw new IllegalStateException("Current chunk has not initialised emptiness map yet supplied emptiness map isn't filled?");
+                }
+                continue;
+            }
+            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) {
+                    final SWMRNibbleArray nibble = this.getNibbleFromCache(dx + chunkX, sectionY, dz + chunkZ);
+
+                    // 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 ChunkSection section = this.getChunkSection(dx + dx2 + chunkX, y, dz + dz2 + chunkZ);
+                                    if (section != null && section != EMPTY_CHUNK_SECTION) {
+                                        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
+                        if (nibble != null) {
+                            nibble.setNull();
+                        }
+                    } 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 ILightAccess lightAccess, final int chunkX, final int chunkZ) {
+        this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
+        try {
+            final IChunkAccess 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 ILightAccess lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) {
+        this.setupCaches(lightAccess, chunkX * 16 + 7, 128, chunkZ * 16 + 7, true, false);
+        try {
+            final IChunkAccess 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 ILightAccess lightAccess, final IChunkAccess chunk, final boolean needsEdgeChecks);
+
+    public final void light(final ILightAccess lightAccess, final IChunkAccess 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); // TODO
+            this.setNibbles(chunk, nibbles);
+            this.updateVisible(lightAccess);
+        } finally {
+            this.destroyCaches();
+        }
+    }
+
+    public final void relightChunks(final ILightAccess lightAccess, final Set<ChunkCoordIntPair> chunks,
+                                    final Consumer<ChunkCoordIntPair> 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 ChunkCoordIntPair chunkPos : chunks) {
+            final int chunkX = chunkPos.x;
+            final int chunkZ = chunkPos.z;
+            final IChunkAccess chunk = (IChunkAccess) lightAccess.getFeaturesReadyChunk(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 IChunkAccess neighbour = (IChunkAccess) lightAccess.getFeaturesReadyChunk(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 IChunkAccess neighbour2 = (IChunkAccess)lightAccess.getFeaturesReadyChunk(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 ChunkCoordIntPair(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.markLightSectionDirty(this.skylightPropagator ? EnumSkyBlock.SKY : EnumSkyBlock.BLOCK, new SectionPosition(chunkX, y, chunkX));
+            }
+
+            // now do callback
+            if (chunkLightCallback != null) {
+                chunkLightCallback.accept(chunkPos);
+            }
+            ++lightCalls;
+        }
+
+        if (onComplete != null) {
+            onComplete.accept(lightCalls);
+        }
+    }
+
+    // old algorithm for propagating
+    // this is also the basic algorithm, the optimised algorithm is always going to be tested against this one
+    // and this one is always tested against vanilla
+    // 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 4 bits: state flags
+    // state flags:
+    // whether the propagation must set the current position's light value (0 if decrease, propagated light level if increase)
+    // 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 ILightAccess lightAccess) {
+        final IBlockAccess world = lightAccess.getWorld();
+        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;
+                }
+            }
+
+            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 IBlockData 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.setValues(offX, offY, offZ);
+                        long flags = 0;
+                        if (blockState.isConditionallyFullOpaque()) {
+                            final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms);
+
+                            if (VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), cullingFace)) {
+                                continue;
+                            }
+                            flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+                        }
+
+                        final int opacity = blockState.getOpacity(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 IBlockData fromBlock = this.getBlockState(posX, posY, posZ);
+                this.mutablePos2.setValues(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.getCullingFace(world, this.mutablePos2, propagate.nms) : VoxelShapes.getEmptyShape();
+
+                    if (fromShape != VoxelShapes.getEmptyShape() && VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), 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 IBlockData 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.setValues(offX, offY, offZ);
+                        long flags = 0;
+                        if (blockState.isConditionallyFullOpaque()) {
+                            final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms);
+
+                            if (VoxelShapes.combinationOccludes(fromShape, cullingFace)) {
+                                continue;
+                            }
+                            flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+                        }
+
+                        final int opacity = blockState.getOpacity(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 ILightAccess lightAccess) {
+        final IBlockAccess world = lightAccess.getWorld();
+        long[] queue = this.decreaseQueue;
+        long[] increaseQueue = this.increaseQueue;
+        int queueReadIndex = 0;
+        int queueLength = this.decreaseQueueInitialLength;
+        this.decreaseQueueInitialLength = 0;
+        int increaseQueueLength = this.increaseQueueInitialLength;
+        final int decodeOffsetX = -this.encodeOffsetX;
+        final int decodeOffsetY = -this.encodeOffsetY;
+        final int decodeOffsetZ = -this.encodeOffsetZ;
+        final int encodeOffset = this.coordinateOffset;
+        final int sectionOffset = this.chunkSectionIndexOffset;
+        final int emittedMask = this.emittedLightMask;
+        final VariableBlockLightHandler customLightHandler = this.skylightPropagator ? null : ((WorldServer)world).customBlockLightHandlers;
+
+        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 IBlockData 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 = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask;
+                        if (emittedLight != 0) {
+                            // re-propagate source
+                            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_HAS_SIDED_TRANSPARENT_BLOCKS : 0L);
+                        }
+
+                        currentNibble.set(localIndex, emittedLight);
+                        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.setValues(offX, offY, offZ);
+                        long flags = 0;
+                        if (blockState.isConditionallyFullOpaque()) {
+                            final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms);
+
+                            if (VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), cullingFace)) {
+                                continue;
+                            }
+                            flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+                        }
+
+                        final int opacity = blockState.getOpacity(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 = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask;
+                        if (emittedLight != 0) {
+                            // re-propagate source
+                            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;
+                        }
+
+                        currentNibble.set(localIndex, emittedLight);
+                        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 IBlockData fromBlock = this.getBlockState(posX, posY, posZ);
+                this.mutablePos2.setValues(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.getCullingFace(world, this.mutablePos2, propagate.nms) : VoxelShapes.getEmptyShape();
+
+                    if (fromShape != VoxelShapes.getEmptyShape() && VoxelShapes.combinationOccludes(VoxelShapes.getEmptyShape(), 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 IBlockData 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 = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask;
+                        if (emittedLight != 0) {
+                            // re-propagate source
+                            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_HAS_SIDED_TRANSPARENT_BLOCKS : 0L);
+                        }
+
+                        currentNibble.set(localIndex, emittedLight);
+                        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.setValues(offX, offY, offZ);
+                        long flags = 0;
+                        if (blockState.isConditionallyFullOpaque()) {
+                            final VoxelShape cullingFace = blockState.getCullingFace(world, this.mutablePos1, propagate.getOpposite().nms);
+
+                            if (VoxelShapes.combinationOccludes(fromShape, cullingFace)) {
+                                continue;
+                            }
+                            flags |= FLAG_HAS_SIDED_TRANSPARENT_BLOCKS;
+                        }
+
+                        final int opacity = blockState.getOpacity(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 = (customLightHandler != null ? this.getCustomLightLevel(customLightHandler, offX, offY, offZ, blockState.getEmittedLight()) : blockState.getEmittedLight()) & emittedMask;
+                        if (emittedLight != 0) {
+                            // re-propagate source
+                            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;
+                        }
+
+                        currentNibble.set(localIndex, emittedLight);
+                        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/com/tuinity/tuinity/chunk/light/StarLightInterface.java b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightInterface.java
new file mode 100644
index 0000000000000000000000000000000000000000..0fddb331bfcee762da38efea3a36dc6394718519
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/StarLightInterface.java
@@ -0,0 +1,490 @@
+package com.tuinity.tuinity.chunk.light;
+
+import com.tuinity.tuinity.util.CoordinateUtils;
+import com.tuinity.tuinity.util.WorldUtil;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
+import net.minecraft.server.BlockPosition;
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.ChunkStatus;
+import net.minecraft.server.IChunkAccess;
+import net.minecraft.server.ILightAccess;
+import net.minecraft.server.LightEngineLayerEventListener;
+import net.minecraft.server.NibbleArray;
+import net.minecraft.server.SectionPosition;
+import net.minecraft.server.WorldServer;
+import java.util.ArrayDeque;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+
+public final class StarLightInterface {
+
+
+    /**
+     * Can be {@code null}, indicating the light is all empty.
+     */
+    protected final WorldServer world;
+    protected final ILightAccess lightAccess;
+
+    protected final ArrayDeque<SkyStarLightEngine> cachedSkyPropagators;
+    protected final ArrayDeque<BlockStarLightEngine> cachedBlockPropagators;
+
+    protected final Long2ObjectOpenHashMap<ChunkChanges> changedBlocks = new Long2ObjectOpenHashMap<>();
+
+    protected final LightEngineLayerEventListener skyReader;
+    protected final LightEngineLayerEventListener blockReader;
+    protected static final boolean isClientSide = false;
+
+    protected final int minSection;
+    protected final int maxSection;
+    protected final int minLightSection;
+    protected final int maxLightSection;
+
+    public StarLightInterface(final ILightAccess lightAccess, final boolean hasSkyLight, final boolean hasBlockLight) {
+        this.lightAccess = lightAccess;
+        this.world = lightAccess == null ? null : (WorldServer)lightAccess.getWorld();
+        this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null;
+        this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null;
+        if (this.world == null) {
+            this.minSection = 0;
+            this.maxSection = 15;
+            this.minLightSection = -1;
+            this.maxLightSection = 16;
+        } 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.skyReader = !hasSkyLight ? LightEngineLayerEventListener.Void.INSTANCE : new LightEngineLayerEventListener() {
+            @Override
+            public NibbleArray a(final SectionPosition pos) {
+                final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
+                if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLit()) || !chunk.getChunkStatus().isAtLeastStatus(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 b(final BlockPosition blockPos) {
+                final int x = blockPos.getX();
+                int y = blockPos.getY();
+                final int z = blockPos.getZ();
+
+                final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(x >> 4, z >> 4);
+                if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLit()) || !chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) {
+                    return 15;
+                }
+
+                int sectionY = y >> 4;
+
+                if (sectionY > StarLightInterface.this.maxLightSection) {
+                    return 15;
+                }
+
+                if (sectionY < StarLightInterface.this.minLightSection) {
+                    sectionY = StarLightInterface.this.minLightSection;
+                    y = sectionY << 4;
+                }
+
+                final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles();
+                final SWMRNibbleArray immediate = nibbles[sectionY - StarLightInterface.this.minLightSection];
+
+                if (StarLightInterface.this.isClientSide) {
+                    if (!immediate.isNullNibbleUpdating()) {
+                        return immediate.getUpdating(x, y, z);
+                    }
+                } else {
+                    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 = StarLightInterface.this.minLightSection - 1;
+                for (int currY = StarLightInterface.this.maxSection; currY >= StarLightInterface.this.minSection; --currY) {
+                    if (emptinessMap[currY - StarLightInterface.this.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 <= StarLightInterface.this.maxLightSection; ++currY) {
+                    final SWMRNibbleArray nibble = nibbles[currY - StarLightInterface.this.minLightSection];
+                    if (StarLightInterface.this.isClientSide) {
+                        if (!nibble.isNullNibbleUpdating()) {
+                            return nibble.getUpdating(x, 0, z);
+                        }
+                    } else {
+                        if (!nibble.isNullNibbleVisible()) {
+                            return nibble.getVisible(x, 0, z);
+                        }
+                    }
+                }
+
+                // should never reach here
+                return 15;
+            }
+
+            @Override
+            public void a(final SectionPosition pos, final boolean notReady) {
+                StarLightInterface.this.sectionChange(pos, notReady);
+            }
+        };
+        this.blockReader = !hasBlockLight ? LightEngineLayerEventListener.Void.INSTANCE : new LightEngineLayerEventListener() {
+            @Override
+            public NibbleArray a(final SectionPosition pos) {
+                final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ());
+
+                if (pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) {
+                    return null;
+                }
+
+                return chunk != null ? chunk.getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble() : null;
+            }
+
+            @Override
+            public int b(final BlockPosition blockPos) {
+                final int cx = blockPos.getX() >> 4;
+                final int cy = blockPos.getY() >> 4;
+                final int cz = blockPos.getZ() >> 4;
+
+                if (cy < StarLightInterface.this.minLightSection || cy > StarLightInterface.this.maxLightSection) {
+                    return 0;
+                }
+
+                final IChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(cx, cz);
+
+                if (chunk == null) {
+                    return 0;
+                }
+
+                final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - StarLightInterface.this.minLightSection];
+                if (StarLightInterface.this.isClientSide) {
+                    return nibble.getUpdating(blockPos.getX(), blockPos.getY(), blockPos.getZ());
+                } else {
+                    return nibble.getVisible(blockPos.getX(), blockPos.getY(), blockPos.getZ());
+                }
+            }
+
+            @Override
+            public void a(final SectionPosition pos, final boolean notReady) {
+                return; // block engine doesn't care
+            }
+        };
+    }
+
+    public LightEngineLayerEventListener getSkyReader() {
+        return this.skyReader;
+    }
+
+    public LightEngineLayerEventListener getBlockReader() {
+        return this.blockReader;
+    }
+
+    public boolean isClientSide() {
+        return this.isClientSide;
+    }
+
+    public IChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) {
+        if (this.world == null) {
+            // empty world
+            return null;
+        }
+        return this.world.getChunkProvider().getChunkAtImmediately(chunkX, chunkZ);
+    }
+
+    public boolean hasUpdates() {
+        synchronized (this) {
+            return !this.changedBlocks.isEmpty();
+        }
+    }
+
+    public WorldServer getWorld() {
+        return this.world;
+    }
+
+    public ILightAccess 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 void blockChange(BlockPosition pos) {
+        if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world
+            return;
+        }
+
+        pos = pos.immutableCopy();
+        synchronized (this.changedBlocks) {
+            this.changedBlocks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> {
+                return new ChunkChanges();
+            }).changedPositions.add(pos);
+        }
+    }
+
+    public void sectionChange(final SectionPosition pos, final boolean newEmptyValue) {
+        if (this.world == null) { // empty world
+            return;
+        }
+
+        synchronized (this.changedBlocks) {
+            final ChunkChanges changes = this.changedBlocks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> {
+                return new ChunkChanges();
+            });
+            if (changes.changedSectionSet == null) {
+                changes.changedSectionSet = new Boolean[this.maxSection - this.minSection + 1];
+            }
+            changes.changedSectionSet[pos.getY() - this.minSection] = Boolean.valueOf(newEmptyValue);
+        }
+    }
+
+    public void forceLoadInChunk(final IChunkAccess 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 IChunkAccess 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<ChunkCoordIntPair> chunks, final Consumer<ChunkCoordIntPair> 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 propagateChanges() {
+        synchronized (this.changedBlocks) {
+            if (this.changedBlocks.isEmpty()) {
+                return;
+            }
+        }
+        final SkyStarLightEngine skyEngine = this.getSkyLightEngine();
+        final BlockStarLightEngine blockEngine = this.getBlockLightEngine();
+
+        try {
+            // TODO be smarter about this in the future
+            final Long2ObjectOpenHashMap<ChunkChanges> changedBlocks;
+            synchronized (this.changedBlocks) {
+                changedBlocks = this.changedBlocks.clone();
+                this.changedBlocks.clear();
+            }
+
+            for (final Iterator<Long2ObjectMap.Entry<ChunkChanges>> iterator = changedBlocks.long2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+                final Long2ObjectMap.Entry<ChunkChanges> entry = iterator.next();
+                final long coordinate = entry.getLongKey();
+                final ChunkChanges changes = entry.getValue();
+                final Set<BlockPosition> positions = changes.changedPositions;
+                final Boolean[] sectionChanges = changes.changedSectionSet;
+
+                final int chunkX = CoordinateUtils.getChunkX(coordinate);
+                final int chunkZ = CoordinateUtils.getChunkZ(coordinate);
+
+                if (skyEngine != null) {
+                    skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
+                }
+                if (blockEngine != null) {
+                    blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges);
+                }
+            }
+        } finally {
+            this.releaseSkyLightEngine(skyEngine);
+            this.releaseBlockLightEngine(blockEngine);
+        }
+    }
+
+    protected static final class ChunkChanges {
+
+        // note: on the main thread, empty section changes are queued before block changes. This means we don't need
+        // to worry about cases where a block change is called inside an empty chunk section, according to the "emptiness" map per chunk,
+        // for example.
+        public final Set<BlockPosition> changedPositions = new HashSet<>();
+
+        public Boolean[] changedSectionSet;
+
+    }
+}
diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandler.java b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..b8a6c59ee3c919e47e4be76fc4e1737d81a5810b
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandler.java
@@ -0,0 +1,30 @@
+package com.tuinity.tuinity.chunk.light;
+
+import net.minecraft.server.BlockPosition;
+import java.util.Collection;
+
+/**
+ * Recommended implementation is {@link VariableBlockLightHandlerImpl}, but you can implement this interface yourself
+ * if you want.
+ */
+public interface VariableBlockLightHandler {
+
+    /**
+     * Returns the custom light level for the specified position. Must return {@code -1} if there is custom level.
+     * @param x Block x world coordinate
+     * @param y Block y world coordinate
+     * @param z Block z world coordinate
+     * @return Custom light level for the specified position
+     */
+    public int getLightLevel(final int x, final int y, final int z);
+
+    /**
+     * Returns a collection of all the custom light positions inside the specified chunk. This must be fast,
+     * as it is used during chunk lighting.
+     * @param chunkX Chunk's x coordinate.
+     * @param chunkZ Chunk's z coordinate.
+     * @return Collection of all the custom light positions in the specified chunk.
+     */
+    public Collection<BlockPosition> getCustomLightPositions(final int chunkX, final int chunkZ);
+
+}
diff --git a/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandlerImpl.java b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandlerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..125f59826a9b0174039139ed0715a4ed3df3724b
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/chunk/light/VariableBlockLightHandlerImpl.java
@@ -0,0 +1,112 @@
+package com.tuinity.tuinity.chunk.light;
+
+import com.tuinity.tuinity.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import net.minecraft.server.BlockPosition;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.locks.StampedLock;
+
+public class VariableBlockLightHandlerImpl implements VariableBlockLightHandler {
+
+    protected final Long2ObjectOpenHashMap<Set<BlockPosition>> positionsByChunk = new Long2ObjectOpenHashMap<>();
+    protected final Long2IntOpenHashMap lightValuesByPosition = new Long2IntOpenHashMap();
+    protected final StampedLock seqlock = new StampedLock();
+    {
+        this.lightValuesByPosition.defaultReturnValue(-1);
+        this.positionsByChunk.defaultReturnValue(Collections.emptySet());
+    }
+
+    @Override
+    public int getLightLevel(final int x, final int y, final int z) {
+        final long key = CoordinateUtils.getBlockKey(x, y, z);
+        try {
+            final long attempt = this.seqlock.tryOptimisticRead();
+            if (attempt != 0L) {
+                final int ret = this.lightValuesByPosition.get(key);
+
+                if (this.seqlock.validate(attempt)) {
+                    return ret;
+                }
+            }
+        } catch (final Error error) {
+            throw error;
+        } catch (final Throwable thr) {
+            // ignore
+        }
+
+        this.seqlock.readLock();
+        try {
+            return this.lightValuesByPosition.get(key);
+        } finally {
+            this.seqlock.tryUnlockRead();
+        }
+    }
+
+    @Override
+    public Collection<BlockPosition> getCustomLightPositions(final int chunkX, final int chunkZ) {
+        final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+        try {
+            final long attempt = this.seqlock.tryOptimisticRead();
+            if (attempt != 0L) {
+                final Set<BlockPosition> ret = new HashSet<>(this.positionsByChunk.get(key));
+
+                if (this.seqlock.validate(attempt)) {
+                    return ret;
+                }
+            }
+        } catch (final Error error) {
+            throw error;
+        } catch (final Throwable thr) {
+            // ignore
+        }
+
+        this.seqlock.readLock();
+        try {
+            return new HashSet<>(this.positionsByChunk.get(key));
+        } finally {
+            this.seqlock.tryUnlockRead();
+        }
+    }
+
+    public void setSource(final int x, final int y, final int z, final int to) {
+        if (to < 0 || to > 15) {
+            throw new IllegalArgumentException();
+        }
+        this.seqlock.writeLock();
+        try {
+            if (this.lightValuesByPosition.put(CoordinateUtils.getBlockKey(x, y, z), to) == -1) {
+                this.positionsByChunk.computeIfAbsent(CoordinateUtils.getChunkKey(x >> 4, z >> 4), (final long keyInMap) -> {
+                    return new HashSet<>();
+                }).add(new BlockPosition(x, y, z));
+            }
+        } finally {
+            this.seqlock.tryUnlockWrite();
+        }
+    }
+
+    public int removeSource(final int x, final int y, final int z) {
+        this.seqlock.writeLock();
+        try {
+            final int ret = this.lightValuesByPosition.remove(CoordinateUtils.getBlockKey(x, y, z));
+
+            if (ret != -1) {
+                final long chunkKey = CoordinateUtils.getChunkKey(x >> 4, z >> 4);
+
+                final Set<BlockPosition> positions = this.positionsByChunk.get(chunkKey);
+                positions.remove(new BlockPosition(x, y, z));
+
+                if (positions.isEmpty()) {
+                    this.positionsByChunk.remove(chunkKey);
+                }
+            }
+
+            return ret;
+        } finally {
+            this.seqlock.tryUnlockWrite();
+        }
+    }
+}
diff --git a/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
index 03a59aabc2a35daf7eee899967b569a8ac3b632e..ceeb515f4528551659598d9999917f8596e3eded 100644
--- a/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
+++ b/src/main/java/com/tuinity/tuinity/config/TuinityConfig.java
@@ -223,6 +223,12 @@ public final class TuinityConfig {
         }
     }
 
+    public static boolean useNewLightEngine;
+
+    private static void useNewLightEngine() {
+        useNewLightEngine = TuinityConfig.getBoolean("use-new-light-engine", true);
+    }
+
     public static final class WorldConfig {
 
         public final String worldName;
diff --git a/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..81fe01c122529f1716a264263957500015476f5f
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/CoordinateUtils.java
@@ -0,0 +1,128 @@
+package com.tuinity.tuinity.util;
+
+import net.minecraft.server.BlockPosition;
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.Entity;
+import net.minecraft.server.MathHelper;
+import net.minecraft.server.SectionPosition;
+
+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 BlockPosition pos) {
+        return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL);
+    }
+
+    public static long getChunkKey(final Entity entity) {
+        return ((long)(MathHelper.floor(entity.locZ()) >> 4) << 32) | ((MathHelper.floor(entity.locX()) >> 4) & 0xFFFFFFFFL);
+    }
+
+    public static long getChunkKey(final ChunkCoordIntPair pos) {
+        return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL);
+    }
+
+    public static long getChunkKey(final SectionPosition 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 MathHelper.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 SectionPosition 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 ChunkCoordIntPair 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 BlockPosition 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 ((MathHelper.floorLong(entity.locX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) |
+                ((MathHelper.floorLong(entity.locY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) |
+                ((MathHelper.floorLong(entity.locZ()) << (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 MathHelper.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 BlockPosition 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.locX() & 0x7FFFFFF) | (((long)entity.locZ() & 0x7FFFFFF) << 27) | ((long)entity.locY() << 54);
+    }
+
+    private CoordinateUtils() {
+        throw new RuntimeException();
+    }
+}
diff --git a/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..695444a510e616180734f5fd284f1a00a2d73ea6
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/IntegerUtil.java
@@ -0,0 +1,226 @@
+package com.tuinity.tuinity.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 = IntegerUtil.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
+
+        int p = 31;
+
+        // all these variables are UNSIGNED!
+        int t = two31 + (d >>> 31);
+        int anc = t - 1 - t%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 - 32;
+        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/com/tuinity/tuinity/util/WorldUtil.java b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..141748fe4915eb46671f1d532951f14d7080818d
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/util/WorldUtil.java
@@ -0,0 +1,48 @@
+package com.tuinity.tuinity.util;
+
+import net.minecraft.server.World;
+
+public final class WorldUtil {
+
+    // min, max are inclusive
+    // TODO update these for 1.17
+
+    public static int getMaxSection(final World world) {
+        return 15;
+    }
+
+    public static int getMinSection(final World world) {
+        return 0;
+    }
+
+    public static int getMaxLightSection(final World world) {
+        return getMaxSection(world) + 1;
+    }
+
+    public static int getMinLightSection(final World world) {
+        return getMinSection(world) - 1;
+    }
+
+
+
+    public static int getTotalSections(final World world) {
+        return getMaxSection(world) - getMinSection(world) + 1;
+    }
+
+    public static int getTotalLightSections(final World world) {
+        return getMaxLightSection(world) - getMinLightSection(world) + 1;
+    }
+
+    public static int getMinBlockY(final World world) {
+        return getMinSection(world) << 4;
+    }
+
+    public static int getMaxBlockY(final World world) {
+        return (getMaxSection(world) << 4) | 15;
+    }
+
+    private WorldUtil() {
+        throw new RuntimeException();
+    }
+
+}
diff --git a/src/main/java/net/minecraft/server/BlockBase.java b/src/main/java/net/minecraft/server/BlockBase.java
index 2760b377d1f68ac5f66e7274317379e2dda8288a..829d4a7508e1656dbdc912096b7eafcf30cbb5b2 100644
--- a/src/main/java/net/minecraft/server/BlockBase.java
+++ b/src/main/java/net/minecraft/server/BlockBase.java
@@ -330,6 +330,7 @@ public abstract class BlockBase {
             this.n = blockbase_info.s;
             this.o = blockbase_info.t;
             this.p = blockbase_info.u;
+            this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // Tuinity
         }
         // Paper start - impl cached craft block data, lazy load to fix issue with loading at the wrong time
         private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData;
@@ -352,6 +353,19 @@ public abstract class BlockBase {
         }
         // Tuinity end
 
+        // Tuinity start
+        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;
+        }
+        // Tuinity end
+
         public void a() {
             this.fluid = this.getBlock().d(this.p()); // Paper - moved from getFluid()
             this.isTicking = this.getBlock().isTicking(this.p()); // Paper - moved from isTicking()
@@ -361,6 +375,33 @@ public abstract class BlockBase {
             this.shapeExceedsCube = this.a == null || this.a.c; // Tuinity - moved from actual method to here
             this.staticPathType = null; // Tuinity - cache static path type
             this.neighbourOverridePathType = null; // Tuinity - cache static path types
+            this.opacityIfCached = this.a == null || this.isConditionallyFullOpaque() ? -1 : this.a.getOpacity(); // Tuinity - cache opacity for light
+            // Tuinity start - optimise culling shape cache for light
+            if (this.a != null && this.a.getCullingShapeCache() != null) {
+                for (int i = 0, len = this.a.getCullingShapeCache().length; i < len; ++i) {
+                    VoxelShape face = this.a.getCullingShapeCache()[i].simplify();
+                    if (face.isEmpty()) {
+                        this.a.getCullingShapeCache()[i] = VoxelShapes.getEmptyShape();
+                        continue;
+                    }
+                    List<AxisAlignedBB> boxes = face.getBoundingBoxesRepresentation();
+                    if (boxes.size() == 1) {
+                        AxisAlignedBB boundingBox = boxes.get(0);
+                        if (boundingBox.equals(VoxelShapes.optimisedFullCube.aabb)) {
+                            this.a.getCullingShapeCache()[i] = VoxelShapes.fullCube();
+                        } else {
+                            this.a.getCullingShapeCache()[i] = VoxelShapes.of(boundingBox);
+                            if (!(this.a.getCullingShapeCache()[i] instanceof com.tuinity.tuinity.voxel.AABBVoxelShape) &&
+                                this.a.getCullingShapeCache()[i].getBoundingBoxesRepresentation().size() == 1) {
+                                this.a.getCullingShapeCache()[i] = new com.tuinity.tuinity.voxel.AABBVoxelShape(boundingBox);
+                            }
+                        }
+                        continue;
+                    }
+                    this.a.getCullingShapeCache()[i] = face;
+                }
+            }
+            // Tuinity end - optimise culling shape cache for light
 
         }
 
@@ -689,9 +730,9 @@ public abstract class BlockBase {
             private static final int f = EnumBlockSupport.values().length;
             protected final boolean a;
             private final boolean g;
-            private final int h;
+            private final int h; private final int getOpacity() { return this.h; } // Tuinity - OBFHELPER
             @Nullable
-            private final VoxelShape[] i;
+            private final VoxelShape[] i; private final VoxelShape[] getCullingShapeCache () { return this.i; } // Tuinity - OBFHELPER
             protected final VoxelShape b;
             protected final boolean c;
             private final boolean[] j;
diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java
index 936c392c9faa178b5645ea79b0130e0d3e3e1368..8fda4702764e80dae93ef9c0eb53abc198642ab1 100644
--- a/src/main/java/net/minecraft/server/Chunk.java
+++ b/src/main/java/net/minecraft/server/Chunk.java
@@ -98,6 +98,52 @@ public class Chunk implements IChunkAccess {
         this.entitySlicesManager.getHardCollidingEntities(entity, axisalignedbb, into, predicate); // Tuinity
     }
     // Tuinity end - optimise hard collision handling
+    // Tuinity start - rewrite light engine
+    protected volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] blockNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight();
+    protected volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] skyNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight();
+    protected volatile boolean[] skyEmptinessMap;
+    protected volatile boolean[] blockEmptinessMap;
+
+    @Override
+    public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() {
+        return this.blockNibbles;
+    }
+
+    @Override
+    public void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+        this.blockNibbles = nibbles;
+    }
+
+    @Override
+    public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() {
+        return this.skyNibbles;
+    }
+
+    @Override
+    public void setSkyNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+        this.skyNibbles = nibbles;
+    }
+
+    @Override
+    public boolean[] getSkyEmptinessMap() {
+        return this.skyEmptinessMap;
+    }
+
+    @Override
+    public void setSkyEmptinessMap(boolean[] emptinessMap) {
+        this.skyEmptinessMap = emptinessMap;
+    }
+
+    @Override
+    public boolean[] getBlockEmptinessMap() {
+        return this.blockEmptinessMap;
+    }
+
+    @Override
+    public void setBlockEmptinessMap(boolean[] emptinessMap) {
+        this.blockEmptinessMap = emptinessMap;
+    }
+    // Tuinity end - rewrite light engine
 
     // Tuinity start - optimised entity slices
     protected final com.tuinity.tuinity.world.ChunkEntitySlices entitySlicesManager;
@@ -405,6 +451,12 @@ public class Chunk implements IChunkAccess {
 
     public Chunk(World world, ProtoChunk protochunk) {
         this(world, protochunk.getPos(), protochunk.getBiomeIndex(), protochunk.p(), protochunk.n(), protochunk.o(), protochunk.getInhabitedTime(), protochunk.getSections(), (Consumer) null);
+        // Tuinity start - copy over protochunk light
+        this.setBlockNibbles(protochunk.getBlockNibbles());
+        this.setSkyNibbles(protochunk.getSkyNibbles());
+        this.setSkyEmptinessMap(protochunk.getSkyEmptinessMap());
+        this.setBlockEmptinessMap(protochunk.getBlockEmptinessMap());
+        // Tuinity end - copy over protochunk light
         Iterator iterator = protochunk.y().iterator();
 
         while (iterator.hasNext()) {
diff --git a/src/main/java/net/minecraft/server/ChunkRegionLoader.java b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
index 17a5b5b5bab4412242a18224b366674c25cdf286..076d6c1e1cc049dd312ecb30518e7b25fc2d7371 100644
--- a/src/main/java/net/minecraft/server/ChunkRegionLoader.java
+++ b/src/main/java/net/minecraft/server/ChunkRegionLoader.java
@@ -64,6 +64,13 @@ public class ChunkRegionLoader {
     private static final boolean JUST_CORRUPT_IT = Boolean.getBoolean("Paper.ignoreWorldDataVersion");
     // Paper end
 
+    // Tuinity start - rewrite light engine
+    private static final int STARLIGHT_LIGHT_VERSION = 4;
+
+    private static final String UNINITIALISED_SKYLIGHT_TAG = "starlight.skylight_uninit";
+    private static final String STARLIGHT_VERSION_TAG = "starlight.light_version";
+    // Tuinity end - rewrite light engine
+
     public static InProgressChunkHolder loadChunk(WorldServer worldserver, DefinedStructureManager definedstructuremanager, VillagePlace villageplace, ChunkCoordIntPair chunkcoordintpair, NBTTagCompound nbttagcompound, boolean distinguish) {
         ArrayDeque<Runnable> tasksToExecuteOnMain = new ArrayDeque<>();
         // Paper end
@@ -93,13 +100,17 @@ public class ChunkRegionLoader {
         ProtoChunkTickList<FluidType> protochunkticklist1 = new ProtoChunkTickList<>((fluidtype) -> {
             return fluidtype == null || fluidtype == FluidTypes.EMPTY;
         }, chunkcoordintpair, nbttagcompound1.getList("LiquidsToBeTicked", 9));
-        boolean flag = nbttagcompound1.getBoolean("isLightOn");
+        boolean flag = (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nbttagcompound1.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION : nbttagcompound1.getBoolean("isLightOn")); boolean canUseSkyLight = flag && getStatus(nbttagcompound).isAtLeastStatus(ChunkStatus.LIGHT); boolean canUseBlockLight = canUseSkyLight; // Tuinity
         NBTTagList nbttaglist = nbttagcompound1.getList("Sections", 10);
         boolean flag1 = true;
         ChunkSection[] achunksection = new ChunkSection[16];
         boolean flag2 = worldserver.getDimensionManager().hasSkyLight();
         ChunkProviderServer chunkproviderserver = worldserver.getChunkProvider();
         LightEngine lightengine = chunkproviderserver.getLightEngine();
+        com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] blockNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(worldserver); // Tuinity - replace light impl
+        com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] skyNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight(worldserver); // Tuinity - replace light impl
+        final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(worldserver);
+        final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(worldserver);
 
         if (flag) {
             tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main
@@ -127,6 +138,7 @@ public class ChunkRegionLoader {
 
             if (flag) {
                 if (nbttagcompound2.hasKeyOfType("BlockLight", 7)) {
+                    if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && canUseBlockLight) blockNibbles[b0 - minSection] = new com.tuinity.tuinity.chunk.light.SWMRNibbleArray(nbttagcompound2.getByteArray("BlockLight").clone()); // Tuinity - replace light impl
                     // Paper start - delay this task since we're executing off-main
                     NibbleArray blockLight = new NibbleArray(nbttagcompound2.getByteArray("BlockLight"));
                     tasksToExecuteOnMain.add(() -> {
@@ -136,13 +148,14 @@ public class ChunkRegionLoader {
                 }
 
                 if (flag2 && nbttagcompound2.hasKeyOfType("SkyLight", 7)) {
+                    if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && canUseSkyLight) skyNibbles[b0 - minSection] = new com.tuinity.tuinity.chunk.light.SWMRNibbleArray(nbttagcompound2.getByteArray("SkyLight").clone()); // Tuinity - replace light impl
                     // Paper start - delay this task since we're executing off-main
                     NibbleArray skyLight = new NibbleArray(nbttagcompound2.getByteArray("SkyLight"));
                     tasksToExecuteOnMain.add(() -> {
                         lightengine.a(EnumSkyBlock.SKY, SectionPosition.a(chunkcoordintpair, b0), skyLight, true);
                     });
                     // Paper end - delay this task since we're executing off-main
-                }
+                } else if (flag2 && nbttagcompound2.getBoolean(UNINITIALISED_SKYLIGHT_TAG)) skyNibbles[b0 - minSection] = new com.tuinity.tuinity.chunk.light.SWMRNibbleArray(); // Tuinity - replace light impl
             }
         }
 
@@ -181,8 +194,12 @@ public class ChunkRegionLoader {
             object = new Chunk(worldserver.getMinecraftWorld(), chunkcoordintpair, biomestorage, chunkconverter, (TickList) object1, (TickList) object2, j, achunksection, // Paper start - fix massive nbt memory leak due to lambda. move lambda into a container method to not leak scope. Only clone needed NBT keys.
                 createLoadEntitiesConsumer(new SafeNBTCopy(nbttagcompound1, "TileEntities", "Entities", "ChunkBukkitValues")) // Paper - move CB Chunk PDC into here
             );// Paper end
+            ((Chunk)object).setBlockNibbles(blockNibbles); // Tuinity - replace light impl
+            ((Chunk)object).setSkyNibbles(skyNibbles); // Tuinity - replace light impl
         } else {
             ProtoChunk protochunk = new ProtoChunk(chunkcoordintpair, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, worldserver); // Paper - Anti-Xray - Add parameter
+            protochunk.setBlockNibbles(blockNibbles); // Tuinity - replace light impl
+            protochunk.setSkyNibbles(skyNibbles); // Tuinity - replace light impl
 
             protochunk.a(biomestorage);
             object = protochunk;
@@ -361,15 +378,20 @@ public class ChunkRegionLoader {
         NibbleArray[] blockLight = new NibbleArray[17 - (-1)];
         NibbleArray[] skyLight = new NibbleArray[17 - (-1)];
 
+        // Tuinity start - rewrite light impl
+        final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(world);
+        final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(world);
+        // Tuinity end - rewrite light impl
+
         for (int i = -1; i < 17; ++i) {
-            NibbleArray blockArray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkPos, i));
-            NibbleArray skyArray = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkPos, i));
+            NibbleArray blockArray = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasBlockLight ? null : (chunk.getBlockNibbles()[i - minSection].isAllZero() ? new NibbleArray() : chunk.getBlockNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkPos, i)); // Tuinity - chunk might not be loaded
+            NibbleArray skyArray = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasSkyLight ? null : (chunk.getSkyNibbles()[i - minSection].isAllZero() ? new NibbleArray() : chunk.getSkyNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkPos, i)); // Tuinity - chunk might not be loaded
 
             // copy data for safety
-            if (blockArray != null) {
+            if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && blockArray != null) { // Tuinity - data already copied
                 blockArray = blockArray.copy();
             }
-            if (skyArray != null) {
+            if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && skyArray != null) { // Tuinity - data already copied
                 skyArray = skyArray.copy();
             }
 
@@ -404,6 +426,10 @@ public class ChunkRegionLoader {
     }
     public static NBTTagCompound saveChunk(WorldServer worldserver, IChunkAccess ichunkaccess, AsyncSaveData asyncsavedata) {
         // Paper end
+        // Tuinity start - rewrite light impl
+        final int minSection = com.tuinity.tuinity.util.WorldUtil.getMinLightSection(worldserver);
+        final int maxSection = com.tuinity.tuinity.util.WorldUtil.getMaxLightSection(worldserver);
+        // Tuinity end - rewrite light impl
         ChunkCoordIntPair chunkcoordintpair = ichunkaccess.getPos();
         NBTTagCompound nbttagcompound = new NBTTagCompound();
         NBTTagCompound nbttagcompound1 = new NBTTagCompound();
@@ -437,8 +463,8 @@ public class ChunkRegionLoader {
             NibbleArray nibblearray; // block light
             NibbleArray nibblearray1; // sky light
             if (asyncsavedata == null) {
-                nibblearray = lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData)
-                nibblearray1 = lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData)
+                nibblearray = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasBlockLight ? null : (ichunkaccess.getBlockNibbles()[i - minSection].isAllZero() ? new NibbleArray() : ichunkaccess.getBlockNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, i)); // Tuinity - chunk might not be loaded
+                nibblearray1 = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (!lightenginethreaded.hasSkyLight ? null : (ichunkaccess.getSkyNibbles()[i - minSection].isAllZero() ? new NibbleArray() : ichunkaccess.getSkyNibbles()[i - minSection].toVanillaNibble())) : lightenginethreaded.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, i)); // Tuinity - chunk might not be loaded
             } else {
                 nibblearray = asyncsavedata.blockLight[i + 1]; // +1 to offset the -1 starting index
                 nibblearray1 = asyncsavedata.skyLight[i + 1]; // +1 to offset the -1 starting index
@@ -452,12 +478,12 @@ public class ChunkRegionLoader {
                 }
 
                 if (nibblearray != null && !nibblearray.c()) {
-                    nbttagcompound2.setByteArray("BlockLight", nibblearray.asBytesPoolSafe().clone()); // Paper
+                    nbttagcompound2.setByteArray("BlockLight", com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.asBytesPoolSafe().clone()); // Paper // Tuinity - data is already cloned
                 }
 
                 if (nibblearray1 != null && !nibblearray1.c()) {
-                    nbttagcompound2.setByteArray("SkyLight", nibblearray1.asBytesPoolSafe().clone()); // Paper
-                }
+                    nbttagcompound2.setByteArray("SkyLight", com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray1.asBytes() : nibblearray1.asBytesPoolSafe().clone()); // Paper // Tuinity - data is already cloned
+                } else if (nibblearray1 != null && com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) nbttagcompound2.setBoolean(UNINITIALISED_SKYLIGHT_TAG, true); // Tuinity - store uninitialised tags
 
                 nbttaglist.add(nbttagcompound2);
             }
@@ -465,7 +491,7 @@ public class ChunkRegionLoader {
 
         nbttagcompound1.set("Sections", nbttaglist);
         if (flag) {
-            nbttagcompound1.setBoolean("isLightOn", true);
+            nbttagcompound1.setBoolean("isLightOn", com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? false : true); if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) nbttagcompound1.setInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Tuinity
         }
 
         BiomeStorage biomestorage = ichunkaccess.getBiomeIndex();
diff --git a/src/main/java/net/minecraft/server/ChunkSection.java b/src/main/java/net/minecraft/server/ChunkSection.java
index cebd808e273dbdb88feb16920dd7a2f60390b34f..33b8f4e0f09fdc41c8ea48b6ed77af199136ab92 100644
--- a/src/main/java/net/minecraft/server/ChunkSection.java
+++ b/src/main/java/net/minecraft/server/ChunkSection.java
@@ -11,7 +11,7 @@ public class ChunkSection {
     short nonEmptyBlockCount; // Paper - package-private
     short tickingBlockCount; // Paper - private -> package-private
     private short e;
-    final DataPaletteBlock<IBlockData> blockIds; // Paper - package-private
+    public final DataPaletteBlock<IBlockData> blockIds; // Paper - package-private // Tuinity - public
 
     final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper
 
diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
index d51925612bc3d37164a8e821833ef1721b6e6976..cf14b83ec3c2b9a2812f50f3bca991e029bf7c67 100644
--- a/src/main/java/net/minecraft/server/EntityPlayer.java
+++ b/src/main/java/net/minecraft/server/EntityPlayer.java
@@ -531,6 +531,185 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
                 }
             }
 
+            /* // TODO remove debug
+            this.networkManager.disableAutomaticFlush();
+
+            if (MinecraftServer.currentTick % 20 == 0) {
+                int centerX = MathHelper.floor(this.locX()) >> 4;
+                int centerZ = MathHelper.floor(this.locZ()) >> 4;
+                byte[] full = new byte[2048];
+                byte[] empty = new byte[2048];
+                java.util.Arrays.fill(full, (byte)-1);
+                for (int dz = -1; dz <= 1; ++dz) {
+                    for (int dx = -1; dx <= 1; ++dx) {
+                        int cx = centerX + dx;
+                        int cz = centerZ + dz;
+
+                        Chunk chunk = this.getWorldServer().getChunkProvider().getChunkAtIfLoadedImmediately(cx, cz);
+
+                        if (chunk == null) {
+                            continue;
+                        }
+
+                        for (int y = -1; y <= 16; ++y) {
+                            NibbleArray nibble = this.getWorldServer().getChunkProvider().getLightEngine().a(EnumSkyBlock.SKY)
+                                    .a(new SectionPosition(cx, y, cz));
+                            org.bukkit.Color color;
+                            org.bukkit.block.data.BlockData blockColor;
+                            if (nibble == null) {
+                                color = org.bukkit.Color.PURPLE;
+                                blockColor = org.bukkit.Material.PURPLE_WOOL.createBlockData();
+                                continue;
+                            } else {
+                                if (nibble.c()) { // is null
+                                    color = org.bukkit.Color.BLUE;
+                                    blockColor = org.bukkit.Material.BLUE_WOOL.createBlockData();
+                                } else if (java.util.Arrays.equals(nibble.justGiveMeTheFuckingByteArrayNoCleanerBullshitJesusFuckingChrist(), full)) {
+                                    color = org.bukkit.Color.RED;
+                                    blockColor = org.bukkit.Material.RED_WOOL.createBlockData();
+                                } else if (java.util.Arrays.equals(nibble.justGiveMeTheFuckingByteArrayNoCleanerBullshitJesusFuckingChrist(), empty)) {
+                                    color = org.bukkit.Color.BLACK;
+                                    blockColor = org.bukkit.Material.BLACK_WOOL.createBlockData();
+                                } else {
+                                    color = org.bukkit.Color.ORANGE;
+                                    blockColor = org.bukkit.Material.ORANGE_WOOL.createBlockData();
+                                }
+                            }
+                            if (false) {
+                                if (y < 0 || y > 15 || chunk.getSections()[y] == null || chunk.getSections()[y].isFullOfAir()) {
+                                    color = org.bukkit.Color.BLACK;
+                                    blockColor = org.bukkit.Material.BLACK_WOOL.createBlockData();
+                                } else {
+                                    color = org.bukkit.Color.WHITE;
+                                    blockColor = org.bukkit.Material.WHITE_WOOL.createBlockData();
+                                }
+                            }
+
+                            org.bukkit.Particle.DustOptions dustOptions = new org.bukkit.Particle.DustOptions(color, 1.7f);
+
+                            for (int i = 0; i <= 16; ++i) {
+                                // y axis
+
+                                double xVal = i == 0 ? 0.5 : (i == 16 ? 15.5 : i);
+
+                                // left side
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + 0.5,
+                                        y*16 + xVal,
+                                        cz * 16 + 0.5,
+                                        1,
+                                        dustOptions
+                                );
+
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + 0.5,
+                                        y*16 + xVal,
+                                        cz * 16 + 15.5,
+                                        1,
+                                        dustOptions
+                                );
+
+                                // right side
+
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + 15.5,
+                                        y*16 + xVal,
+                                        cz * 16 + 0.5,
+                                        1,
+                                        dustOptions
+                                );
+
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + 15.5,
+                                        y*16 + xVal,
+                                        cz * 16 + 15.5,
+                                        1,
+                                        dustOptions
+                                );
+
+
+                                // x axis
+
+                                // bottom
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + xVal,
+                                        y*16 + 0.5,
+                                        cz * 16 + 0.5,
+                                        1,
+                                        dustOptions
+                                );
+
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + xVal,
+                                        y*16 + 0.5,
+                                        cz * 16 + 15.5,
+                                        1,
+                                        dustOptions
+                                );
+
+                                // top
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + xVal,
+                                        y*16 + 15.5,
+                                        cz * 16 + 0.5,
+                                        1,
+                                        dustOptions
+                                );
+
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + xVal,
+                                        y*16 + 15.5,
+                                        cz * 16 + 15.5,
+                                        1,
+                                        dustOptions
+                                );
+
+                                // z axis
+                                // bottom
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + 0.5,
+                                        y*16 + 0.5,
+                                        cz * 16 + xVal,
+                                        1,
+                                        dustOptions
+                                );
+
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + 15.5,
+                                        y*16 + 0.5,
+                                        cz * 16 + xVal,
+                                        1,
+                                        dustOptions
+                                );
+
+                                //top
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + 0.5,
+                                        y*16 + 15.5,
+                                        cz * 16 + xVal,
+                                        1,
+                                        dustOptions
+                                );
+
+                                this.getBukkitEntity().spawnParticle(org.bukkit.Particle.REDSTONE,
+                                        cx * 16 + 15.5,
+                                        y*16 + 15.5,
+                                        cz * 16 + xVal,
+                                        1,
+                                        dustOptions
+                                );
+                            }
+                        }
+                    }
+                }
+            }
+
+            this.networkManager.enableAutomaticFlush();
+
+            //System.out.println("Block: " + this.getBukkitEntity().getLocation().getBlock().getLightFromBlocks());
+            //System.out.println("Sky: " + this.getBukkitEntity().getLocation().getBlock().getLightFromSky());
+            */  // TODO remove debug
+
             if (this.getHealth() != this.lastHealthSent || this.lastFoodSent != this.foodData.getFoodLevel() || this.foodData.getSaturationLevel() == 0.0F != this.lastSentSaturationZero) {
                 this.playerConnection.sendPacket(new PacketPlayOutUpdateHealth(this.getBukkitEntity().getScaledHealth(), this.foodData.getFoodLevel(), this.foodData.getSaturationLevel())); // CraftBukkit
                 this.lastHealthSent = this.getHealth();
diff --git a/src/main/java/net/minecraft/server/EnumDirection.java b/src/main/java/net/minecraft/server/EnumDirection.java
index 05b0090ae36cf61f67e26aad478df30c89f31941..30ba21ac1bced18a9d0946b7c3ed55971ada48bb 100644
--- a/src/main/java/net/minecraft/server/EnumDirection.java
+++ b/src/main/java/net/minecraft/server/EnumDirection.java
@@ -170,8 +170,8 @@ public enum EnumDirection implements INamable {
         return EnumDirection.q[MathHelper.a(i % EnumDirection.q.length)];
     }
 
-    @Nullable
-    public static EnumDirection a(int i, int j, int k) {
+    @Nullable public static EnumDirection from(int i, int j, int k) { return a(i, j, k); } // Tuinity - OBFHELPER
+    @Nullable public static EnumDirection a(int i, int j, int k) {
         return (EnumDirection) EnumDirection.r.get(BlockPosition.a(i, j, k));
     }
 
diff --git a/src/main/java/net/minecraft/server/IChunkAccess.java b/src/main/java/net/minecraft/server/IChunkAccess.java
index 180b6b58dc5663158db84b6f1257591439b48c31..eb0d794b7275af7f860e7c7b85a9e3b2aa4a863f 100644
--- a/src/main/java/net/minecraft/server/IChunkAccess.java
+++ b/src/main/java/net/minecraft/server/IChunkAccess.java
@@ -24,6 +24,36 @@ public interface IChunkAccess extends IBlockAccess, IStructureAccess {
     }
     // Paper end
 
+    // Tuinity start
+    default com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() {
+        throw new UnsupportedOperationException(this.getClass().getName());
+    }
+    default void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+        throw new UnsupportedOperationException(this.getClass().getName());
+    }
+
+    default com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() {
+        throw new UnsupportedOperationException(this.getClass().getName());
+    }
+    default void setSkyNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+        throw new UnsupportedOperationException(this.getClass().getName());
+    }
+    public default boolean[] getSkyEmptinessMap() {
+        throw new UnsupportedOperationException(this.getClass().getName());
+    }
+    public default void setSkyEmptinessMap(final boolean[] emptinessMap) {
+        throw new UnsupportedOperationException(this.getClass().getName());
+    }
+
+    public default boolean[] getBlockEmptinessMap() {
+        throw new UnsupportedOperationException(this.getClass().getName());
+    }
+
+    public default void setBlockEmptinessMap(final boolean[] emptinessMap) {
+        throw new UnsupportedOperationException(this.getClass().getName());
+    }
+    // Tuinity end
+
     IBlockData getType(final int x, final int y, final int z); // Paper
     @Nullable
     IBlockData setType(BlockPosition blockposition, IBlockData iblockdata, boolean flag);
@@ -122,6 +152,7 @@ public interface IChunkAccess extends IBlockAccess, IStructureAccess {
     @Nullable
     NBTTagCompound j(BlockPosition blockposition);
 
+    default Stream<BlockPosition> getLightSources() { return this.m(); } // Tuinity - OBFHELPER
     Stream<BlockPosition> m();
 
     TickList<Block> n();
@@ -142,7 +173,9 @@ public interface IChunkAccess extends IBlockAccess, IStructureAccess {
         return ashortlist[i];
     }
 
+    default boolean isLit() { return this.r(); } // Tuinity - OBFHELPER
     boolean r();
 
+    default void setLit(boolean lit) { this.b(lit); } // Tuinity - OBFHELPER
     void b(boolean flag);
 }
diff --git a/src/main/java/net/minecraft/server/ILightAccess.java b/src/main/java/net/minecraft/server/ILightAccess.java
index be5384ee41290b24b0c419c3e8f4553db34b2399..df28f7a6bf4c650a22ddf046eae4d5e8ca5879a9 100644
--- a/src/main/java/net/minecraft/server/ILightAccess.java
+++ b/src/main/java/net/minecraft/server/ILightAccess.java
@@ -4,9 +4,10 @@ import javax.annotation.Nullable;
 
 public interface ILightAccess {
 
-    @Nullable
-    IBlockAccess c(int i, int j);
+    default @Nullable IBlockAccess getFeaturesReadyChunk(int i, int j) { return this.c(i, j); } // Tuinity - OBFHELPER
+    @Nullable IBlockAccess c(int i, int j);
 
+    default void markLightSectionDirty(EnumSkyBlock enumskyblock, SectionPosition sectionposition) { this.a(enumskyblock, sectionposition); } // Tuinity - OBFHELPER
     default void a(EnumSkyBlock enumskyblock, SectionPosition sectionposition) {}
 
     IBlockAccess getWorld();
diff --git a/src/main/java/net/minecraft/server/LightEngineThreaded.java b/src/main/java/net/minecraft/server/LightEngineThreaded.java
index 2f9c97dd4e1d705a87772d18c7ab4883a876af08..b2f51a6786d4bb92c9cbbceeea812375b1a49bd6 100644
--- a/src/main/java/net/minecraft/server/LightEngineThreaded.java
+++ b/src/main/java/net/minecraft/server/LightEngineThreaded.java
@@ -2,6 +2,11 @@ package net.minecraft.server;
 
 import com.mojang.datafixers.util.Pair;
 import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; // Paper
+// Tuinity start
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+// Tuinity end
 import it.unimi.dsi.fastutil.objects.ObjectArrayList;
 import it.unimi.dsi.fastutil.objects.ObjectList;
 import it.unimi.dsi.fastutil.objects.ObjectListIterator;
@@ -15,11 +20,12 @@ import org.apache.logging.log4j.Logger;
 public class LightEngineThreaded extends LightEngine implements AutoCloseable {
 
     private static final Logger LOGGER = LogManager.getLogger();
-    private final ThreadedMailbox<Runnable> b;
+    private final ThreadedMailbox<Runnable> b; private ThreadedMailbox<Runnable> getExecutor() { return this.b; } // Tuinity - OBFHELPER
     // Paper start
     private static final int MAX_PRIORITIES = PlayerChunkMap.GOLDEN_TICKET + 2;
 
     private boolean isChunkLightStatus(long pair) {
+        if (true) return true; // Tuinity - viewing ticket levels async can result in the viewing of transient levels, and LIGHT ticket isn't guaranteed to exist for all loading chunks thanks to really dumb unloading behaviors with the chunk system
         PlayerChunk playerChunk = playerChunkMap.getVisibleChunk(pair);
         if (playerChunk == null) {
             return false;
@@ -156,13 +162,218 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
     private volatile int f = 5;
     private final AtomicBoolean g = new AtomicBoolean();
 
+    // Tuinity start - replace light engine impl
+    protected final com.tuinity.tuinity.chunk.light.StarLightInterface theLightEngine;
+    public final boolean hasBlockLight;
+    public final boolean hasSkyLight;
+    // Tuinity end - replace light engine impl
+
     public LightEngineThreaded(ILightAccess ilightaccess, PlayerChunkMap playerchunkmap, boolean flag, ThreadedMailbox<Runnable> threadedmailbox, Mailbox<ChunkTaskQueueSorter.a<Runnable>> mailbox) {
         super(ilightaccess, true, flag);
         this.d = playerchunkmap; this.playerChunkMap = d; // Paper
         this.e = mailbox;
         this.b = threadedmailbox;
+        // Tuinity start - replace light engine impl
+        this.hasBlockLight = true;
+        this.hasSkyLight = flag;
+        this.theLightEngine = com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? new com.tuinity.tuinity.chunk.light.StarLightInterface(ilightaccess, flag, true) : null;
+        // Tuinity end - replace light engine impl
+    }
+
+    // Tuinity start - replace light engine impl
+    protected final IChunkAccess getChunk(final int chunkX, final int chunkZ) {
+        final WorldServer world = (WorldServer)this.theLightEngine.getWorld();
+        return world.getChunkProvider().getChunkAtImmediately(chunkX, chunkZ);
+    }
+
+    protected long relightCounter;
+
+    public int relight(java.util.Set<ChunkCoordIntPair> chunks_param,
+                        java.util.function.Consumer<ChunkCoordIntPair> 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<ChunkCoordIntPair> chunks = new java.util.LinkedHashSet<>(chunks_param);
+        // add tickets
+        java.util.Map<ChunkCoordIntPair, Long> ticketIds = new java.util.HashMap<>();
+        int totalChunks = 0;
+        for (java.util.Iterator<ChunkCoordIntPair> iterator = chunks.iterator(); iterator.hasNext();) {
+            final ChunkCoordIntPair chunkPos = iterator.next();
+
+            final IChunkAccess chunk = this.theLightEngine.getWorld().getChunkProvider().getChunkAtImmediately(chunkPos.x, chunkPos.z);
+            if (chunk == null || !chunk.isLit() || !chunk.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) {
+                // cannot relight this chunk
+                iterator.remove();
+                continue;
+            }
+
+            final Long id = Long.valueOf(this.relightCounter++);
+
+            this.theLightEngine.getWorld().getChunkProvider().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id);
+            ticketIds.put(chunkPos, id);
+
+            ++totalChunks;
+        }
+
+        this.getExecutor().queue(() -> {
+            this.theLightEngine.relightChunks(chunks, (ChunkCoordIntPair chunkPos) -> {
+                chunkLightCallback.accept(chunkPos);
+                ((java.util.concurrent.Executor)this.theLightEngine.getWorld().getChunkProvider().serverThreadQueue).execute(() -> {
+                    this.theLightEngine.getWorld().getChunkProvider().playerChunkMap.getUpdatingChunk(chunkPos.pair()).sendPacketToTrackedPlayers(new PacketPlayOutLightUpdate(chunkPos, this, true), false);
+                    this.theLightEngine.getWorld().getChunkProvider().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos));
+                });
+            }, onComplete);
+        });
+        this.queueUpdate();
+
+        return totalChunks;
+    }
+
+    protected final Long2IntOpenHashMap holdingChunks = new Long2IntOpenHashMap();
+    protected final LongArrayList postWorkTicketRelease = new LongArrayList();
+
+    private void addLightWorkTicket(int chunkX, int chunkZ) {
+        final long coordinate = MCUtil.getCoordinateKey(chunkX, chunkZ);
+        final int current = this.holdingChunks.putIfAbsent(coordinate, 1);
+        if (current == 0) {
+            final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(coordinate);
+            this.theLightEngine.getWorld().getChunkProvider().addTicketAtLevel(TicketType.LIGHT_UPDATE, chunkPos,
+                    MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), chunkPos);
+            this.theLightEngine.getWorld().getChunkProvider().tickDistanceManager();
+        } else {
+            this.holdingChunks.put(coordinate, current + 1);
+        }
+    }
+
+    protected final void releaseLightWorkChunk(int chunkX, int chunkZ) {
+        final long coordinate = MCUtil.getCoordinateKey(chunkX, chunkZ);
+        final int current = this.holdingChunks.get(coordinate);
+        if (current == 1) {
+            this.holdingChunks.remove(coordinate);
+            final ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(coordinate);
+            this.theLightEngine.getWorld().getChunkProvider().removeTicketAtLevel(TicketType.LIGHT_UPDATE, chunkPos,
+                    MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), chunkPos);
+        } else {
+            this.holdingChunks.put(coordinate, current - 1);
+        }
+    }
+
+    protected final CompletableFuture<IChunkAccess> acquireLightWorkChunk(int chunkX, int chunkZ) {
+        ChunkProviderServer chunkProvider = this.theLightEngine.getWorld().getChunkProvider();
+        PlayerChunkMap chunkMap = chunkProvider.playerChunkMap;
+        int targetLevel = MCUtil.getTicketLevelFor(ChunkStatus.LIGHT);
+
+        this.addLightWorkTicket(chunkX, chunkZ);
+
+        // light doesn't always load one radius neighbours...
+        // i.e if they get unloaded
+        boolean neighboursAtFeatures = true;
+        int targetNeighbourLevel = MCUtil.getTicketLevelFor(ChunkStatus.LIGHT.getPreviousStatus());
+        for (int dx = -1; dx <= 1; ++dx) {
+            for (int dz = -1; dz <= 1; ++dz) {
+                PlayerChunk neighbour = chunkMap.getUpdatingChunk(MCUtil.getCoordinateKey(dx + chunkX, dz + chunkZ));
+                ChunkStatus status;
+                if (neighbour == null || neighbour.getTicketLevel() > targetNeighbourLevel ||
+                        (status = neighbour.getChunkHolderStatus()) == null ||
+                        !status.isAtLeastStatus(ChunkStatus.LIGHT.getPreviousStatus())) {
+                    neighboursAtFeatures = false;
+                    break;
+                }
+            }
+        }
+
+        PlayerChunk playerChunk = chunkMap.getUpdatingChunk(MCUtil.getCoordinateKey(chunkX, chunkZ));
+        ChunkStatus holderStatus;
+        if (!neighboursAtFeatures || playerChunk == null || playerChunk.getTicketLevel() > targetLevel ||
+                (holderStatus = playerChunk.getChunkHolderStatus()) == null ||
+                !holderStatus.isAtLeastStatus(ChunkStatus.LIGHT)) {
+            CompletableFuture<IChunkAccess> ret = new CompletableFuture<>();
+
+            int[] loads = new int[1];
+            int requiredLoads = 3 * 3;
+            java.util.function.Consumer<IChunkAccess> onLoad = (chunk) -> {
+                if (++loads[0] == requiredLoads) {
+                    ret.complete(this.getChunk(chunkX, chunkZ));
+                }
+            };
+
+            for (int dx = -1; dx <= 1; ++dx) {
+                for (int dz = -1; dz <= 1; ++dz) {
+                    chunkProvider.getChunkAtAsynchronously(chunkX + dx, chunkZ + dz,
+                            (dx | dz) == 0 ? ChunkStatus.LIGHT : ChunkStatus.LIGHT.getPreviousStatus(),
+                            true, false, onLoad);
+                }
+            }
+
+            return ret;
+        }
+
+        return CompletableFuture.completedFuture(playerChunk.getAvailableChunkNow());
+    }
+
+    // note: task is discarded if the chunk is not at light status or if the chunk is not lit
+    protected final void scheduleLightWorkTask(int chunkX, int chunkZ, LightEngineThreaded.Update type, Runnable task) {
+        if (!org.bukkit.Bukkit.isPrimaryThread()) {
+            this.playerChunkMap.mainInvokingExecutor.execute(() -> {
+                this.scheduleLightWorkTask(chunkX, chunkZ, type, task);
+            });
+            return;
+        }
+
+        IChunkAccess current = this.getChunk(chunkX, chunkZ);
+
+        if (current == null || !current.isLit() || !current.getChunkStatus().isAtLeastStatus(ChunkStatus.LIGHT)) {
+            return;
+        }
+
+        this.acquireLightWorkChunk(chunkX, chunkZ).whenCompleteAsync((chunk, throwable) -> {
+            if (throwable != null) {
+                MinecraftServer.LOGGER.fatal("Failed to load light chunk for light work", throwable);
+                this.releaseLightWorkChunk(chunkX, chunkZ);
+            } else {
+                this.scheduleTask(chunkX, chunkZ, type, () -> {
+                    try {
+                        task.run();
+                    } finally {
+                        this.postWorkTicketRelease.add(MCUtil.getCoordinateKey(chunkX, chunkZ));
+                    }
+                });
+            }
+        }, this.playerChunkMap.mainInvokingExecutor);
     }
 
+    // override things from superclass
+
+    @Override
+    public boolean a() {
+        return this.theLightEngine != null ? false : super.a();
+    }
+
+    @Override
+    public LightEngineLayerEventListener a(EnumSkyBlock var0) {
+        if (this.theLightEngine == null) {
+            return super.a(var0);
+        }
+        if (var0 == EnumSkyBlock.BLOCK) {
+            return this.theLightEngine.getBlockReader();
+        } else {
+            return this.theLightEngine.getSkyReader();
+        }
+    }
+
+    @Override
+    public int b(BlockPosition var0, int var1) {
+        if (this.theLightEngine == null) {
+            return super.b(var0, var1);
+        }
+        int var2 = this.theLightEngine.getSkyReader().b(var0) - var1;
+        int var3 = this.theLightEngine.getBlockReader().b(var0);
+        return Math.max(var3, var2);
+    }
+    // Tuinity end - replace light engine impl
+
     public void close() {}
 
     @Override
@@ -179,6 +390,15 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
     public void a(BlockPosition blockposition) {
         BlockPosition blockposition1 = blockposition.immutableCopy();
 
+        // Tuinity start - replace light engine impl
+        if (this.theLightEngine != null) {
+            this.scheduleLightWorkTask(blockposition1.getX() >> 4, blockposition1.getZ() >> 4, LightEngineThreaded.Update.POST_UPDATE, () -> {
+                this.theLightEngine.blockChange(blockposition1);
+            });
+            return;
+        }
+        // Tuinity start - replace light engine impl
+
         this.a(blockposition.getX() >> 4, blockposition.getZ() >> 4, LightEngineThreaded.Update.POST_UPDATE, SystemUtils.a(() -> {
             super.a(blockposition1);
         }, () -> {
@@ -187,6 +407,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
     }
 
     protected void a(ChunkCoordIntPair chunkcoordintpair) {
+        // Tuinity start - replace light impl
+        if (this.theLightEngine != null) {
+            return;
+        }
+        // Tuinity end - replace light impl
         this.a(chunkcoordintpair.x, chunkcoordintpair.z, () -> {
             return 0;
         }, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
@@ -211,6 +436,14 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
 
     @Override
     public void a(SectionPosition sectionposition, boolean flag) {
+        // Tuinity start - replace light engine impl
+        if (this.theLightEngine != null) {
+            this.scheduleLightWorkTask(sectionposition.getX(), sectionposition.getZ(), LightEngineThreaded.Update.POST_UPDATE, () -> {
+                this.theLightEngine.sectionChange(sectionposition, flag);
+            });
+            return;
+        }
+        // Tuinity start - replace light engine impl
         this.a(sectionposition.a(), sectionposition.c(), () -> {
             return 0;
         }, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
@@ -222,6 +455,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
 
     @Override
     public void a(ChunkCoordIntPair chunkcoordintpair, boolean flag) {
+        // Tuinity start - replace light impl
+        if (this.theLightEngine != null) {
+            return;
+        }
+        // Tuinity end - replace light impl
         this.a(chunkcoordintpair.x, chunkcoordintpair.z, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
             super.a(chunkcoordintpair, flag);
         }, () -> {
@@ -231,6 +469,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
 
     @Override
     public void a(EnumSkyBlock enumskyblock, SectionPosition sectionposition, @Nullable NibbleArray nibblearray, boolean flag) {
+        // Tuinity start - replace light impl
+        if (this.theLightEngine != null) {
+            return;
+        }
+        // Tuinity end - replace light impl
         this.a(sectionposition.a(), sectionposition.c(), () -> {
             return 0;
         }, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
@@ -240,6 +483,7 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
         }));
     }
 
+    private void scheduleTask(int x, int z, LightEngineThreaded.Update lightenginethreaded_update, Runnable runnable) { this.a(x, z, lightenginethreaded_update, runnable); } // Tuinity - OBFHELPER
     private void a(int i, int j, LightEngineThreaded.Update lightenginethreaded_update, Runnable runnable) {
         this.a(i, j, this.d.c(ChunkCoordIntPair.pair(i, j)), lightenginethreaded_update, runnable);
     }
@@ -252,6 +496,11 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
 
     @Override
     public void b(ChunkCoordIntPair chunkcoordintpair, boolean flag) {
+        // Tuinity start - replace light impl
+        if (this.theLightEngine != null) {
+            return;
+        }
+        // Tuinity end - replace light impl
         this.a(chunkcoordintpair.x, chunkcoordintpair.z, () -> {
             return 0;
         }, LightEngineThreaded.Update.PRE_UPDATE, SystemUtils.a(() -> {
@@ -264,6 +513,35 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
     public CompletableFuture<IChunkAccess> a(IChunkAccess ichunkaccess, boolean flag) {
         ChunkCoordIntPair chunkcoordintpair = ichunkaccess.getPos();
 
+        // Tuinity start - replace light engine
+        if (this.theLightEngine != null) {
+            // make the completion of this future only depend on pre-update execution
+            // post-update is used for block updates, so by using pre here we prioritise chunk lighting
+            return CompletableFuture.supplyAsync(() -> {
+                Boolean[] emptySections = com.tuinity.tuinity.chunk.light.StarLightEngine.getEmptySectionsForChunk(ichunkaccess);
+                if (!flag) {
+                    ichunkaccess.setLit(false);
+                    this.theLightEngine.lightChunk(ichunkaccess, emptySections);
+                    ichunkaccess.setLit(true);
+                } else {
+                    this.theLightEngine.forceLoadInChunk(ichunkaccess, emptySections);
+                    this.theLightEngine.checkChunkEdges(chunkcoordintpair.x, chunkcoordintpair.z);
+                }
+
+                // safe to move the release light to here, as all the work required is done now
+                this.playerChunkMap.removeLightTicket(chunkcoordintpair); // copied from below
+                return ichunkaccess;
+            }, (runnable) -> {
+                LightEngineThreaded.this.scheduleTask(chunkcoordintpair.x, chunkcoordintpair.z, LightEngineThreaded.Update.PRE_UPDATE, runnable);
+                LightEngineThreaded.this.queueUpdate();
+            }).whenComplete((IChunkAccess chunk, Throwable throwable) -> {
+                if (throwable != null && !(throwable instanceof ThreadDeath)) {
+                    MinecraftServer.LOGGER.fatal("Failed to light chunk " + ichunkaccess.getPos().toString() + " in world '" + this.theLightEngine.getWorld().getWorld().getName() + "'");
+                }
+            });
+        }
+        // Tuinity end - replace light engine
+
         // Paper start
         //ichunkaccess.b(false); // Don't need to disable this
         long pair = chunkcoordintpair.pair();
@@ -311,7 +589,7 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
     }
 
     public void queueUpdate() {
-        if ((!this.queue.isEmpty() || super.a()) && this.g.compareAndSet(false, true)) { // Paper
+        if ((!this.queue.isEmpty() || (this.theLightEngine == null && super.a())) && this.g.compareAndSet(false, true)) { // Paper // Tuinity - replace light impl
             this.b.a((() -> { // Paper - decompile error
                 this.b();
                 this.g.set(false);
@@ -325,17 +603,36 @@ public class LightEngineThreaded extends LightEngine implements AutoCloseable {
     private final java.util.List<Runnable> pre = new java.util.ArrayList<>();
     private final java.util.List<Runnable> post = new java.util.ArrayList<>();
     private void b() {
+        //final long start = System.nanoTime(); // TODO remove debug
         if (queue.poll(pre, post)) {
             pre.forEach(Runnable::run);
             pre.clear();
-            super.a(Integer.MAX_VALUE, true, true);
+            if (this.theLightEngine == null) super.a(Integer.MAX_VALUE, true, true); // Tuinity - replace light impl
             post.forEach(Runnable::run);
             post.clear();
         } else {
             // might have level updates to go still
-            super.a(Integer.MAX_VALUE, true, true);
+            if (this.theLightEngine == null) super.a(Integer.MAX_VALUE, true, true); // Tuinity - replace light impl
+        }
+        // Tuinity start - replace light impl
+        if (this.theLightEngine != null) {
+            this.theLightEngine.propagateChanges();
+            if (!this.postWorkTicketRelease.isEmpty()) {
+                LongArrayList copy = this.postWorkTicketRelease.clone();
+                this.postWorkTicketRelease.clear();
+                this.playerChunkMap.mainInvokingExecutor.execute(() -> {
+                    LongIterator iterator = copy.iterator();
+                    while (iterator.hasNext()) {
+                        long coordinate = iterator.nextLong();
+                        this.releaseLightWorkChunk(MCUtil.getCoordinateX(coordinate), MCUtil.getCoordinateZ(coordinate));
+                    }
+                });
+            }
         }
+        // Tuinity end - replace light impl
         // Paper end
+        //final long end = System.nanoTime(); // TODO remove debug
+        //System.out.println("Block updates took " + (end - start) * 1.0e-6 + "ms"); // TODO remove debug
     }
 
     public void a(int i) {
diff --git a/src/main/java/net/minecraft/server/NibbleArray.java b/src/main/java/net/minecraft/server/NibbleArray.java
index 4085426af03f032cf405bdfd1e40a8e5dc27c1d1..348d16ddec3b4da0b6b4e4f49916b966005b5259 100644
--- a/src/main/java/net/minecraft/server/NibbleArray.java
+++ b/src/main/java/net/minecraft/server/NibbleArray.java
@@ -56,6 +56,7 @@ public class NibbleArray {
     boolean poolSafe = false;
     public java.lang.Runnable cleaner;
     private void registerCleaner() {
+        if (com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) return; // Tuinity - purge cleaner usage
         if (!poolSafe) {
             cleaner = MCUtil.registerCleaner(this, this.a, NibbleArray::releaseBytes);
         } else {
@@ -63,7 +64,7 @@ public class NibbleArray {
         }
     }
     // Paper end
-    @Nullable protected byte[] a;
+    @Nullable protected byte[] a; public final byte[] justGiveMeTheFuckingByteArrayNoCleanerBullshitJesusFuckingChrist() { return this.a; }
 
 
     public NibbleArray() {}
@@ -74,7 +75,7 @@ public class NibbleArray {
     }
     public NibbleArray(byte[] abyte, boolean isSafe) {
         this.a = abyte;
-        if (!isSafe) this.a = getCloneIfSet(); // Paper - clone for safety
+        if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && !isSafe) this.a = getCloneIfSet(); // Paper - clone for safety // Tuinity - no need to clone
         registerCleaner();
         // Paper end
         if (abyte.length != 2048) {
@@ -162,7 +163,7 @@ public class NibbleArray {
 
     public NibbleArray copy() { return this.b(); } // Paper - OBFHELPER
     public NibbleArray b() {
-        return this.a == null ? new NibbleArray() : new NibbleArray(this.a); // Paper - clone in ctor
+        return this.a == null ? new NibbleArray() : new NibbleArray(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? this.a.clone() : this.a); // Paper - clone in ctor // Tuinity - no longer clone in constructor
     }
 
     public String toString() {
diff --git a/src/main/java/net/minecraft/server/PacketPlayOutLightUpdate.java b/src/main/java/net/minecraft/server/PacketPlayOutLightUpdate.java
index a22f0cccecc85b4e4fe4603bcfa213f15c23db69..6cc4a035c8b1312b59685b20039d5e82bb1e1a3e 100644
--- a/src/main/java/net/minecraft/server/PacketPlayOutLightUpdate.java
+++ b/src/main/java/net/minecraft/server/PacketPlayOutLightUpdate.java
@@ -26,12 +26,12 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
 
     @Override
     public void onPacketDispatch(EntityPlayer player) {
-        remainingSends.incrementAndGet();
+        if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) remainingSends.incrementAndGet();
     }
 
     @Override
     public void onPacketDispatchFinish(EntityPlayer player, ChannelFuture future) {
-        if (remainingSends.decrementAndGet() <= 0) {
+        if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine && remainingSends.decrementAndGet() <= 0) {
             // incase of any race conditions, schedule this delayed
             MCUtil.scheduleTask(5, () -> {
                 if (remainingSends.get() == 0) {
@@ -44,7 +44,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
 
     @Override
     public boolean hasFinishListener() {
-        return true;
+        return !com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine; // Tuinity - replace light impl
     }
 
     // Paper end
@@ -54,8 +54,8 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
         this.a = chunkcoordintpair.x;
         this.b = chunkcoordintpair.z;
         this.i = flag;
-        this.g = Lists.newArrayList();cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper
-        this.h = Lists.newArrayList();cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper
+        this.g = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage
+        this.h = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage
 
         for (int i = 0; i < 18; ++i) {
             NibbleArray nibblearray = lightengine.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, -1 + i));
@@ -66,7 +66,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
                     this.e |= 1 << i;
                 } else {
                     this.c |= 1 << i;
-                    this.g.add(nibblearray.getCloneIfSet()); // Paper
+                    this.g.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.getCloneIfSet()); // Paper // Tuinity - don't clone again
                 }
             }
 
@@ -75,7 +75,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
                     this.f |= 1 << i;
                 } else {
                     this.d |= 1 << i;
-                    this.h.add(nibblearray1.getCloneIfSet()); // Paper
+                    this.h.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray1.asBytes() : nibblearray1.getCloneIfSet()); // Paper // Tuinity - don't clone again
                 }
             }
         }
@@ -88,8 +88,8 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
         this.i = flag;
         this.c = i;
         this.d = j;
-        this.g = Lists.newArrayList();cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper
-        this.h = Lists.newArrayList();cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper
+        this.g = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner1 = MCUtil.registerListCleaner(this, this.g, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage
+        this.h = Lists.newArrayList();if (!com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine) cleaner2 = MCUtil.registerListCleaner(this, this.h, NibbleArray::releaseBytes); // Paper // Tuinity - purge cleaner usage
 
         for (int k = 0; k < 18; ++k) {
             NibbleArray nibblearray;
@@ -97,7 +97,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
             if ((this.c & 1 << k) != 0) {
                 nibblearray = lightengine.a(EnumSkyBlock.SKY).a(SectionPosition.a(chunkcoordintpair, -1 + k));
                 if (nibblearray != null && !nibblearray.c()) {
-                    this.g.add(nibblearray.getCloneIfSet()); // Paper
+                    this.g.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.getCloneIfSet()); // Paper // Tuinity - don't clone again
                 } else {
                     this.c &= ~(1 << k);
                     if (nibblearray != null) {
@@ -109,7 +109,7 @@ public class PacketPlayOutLightUpdate implements Packet<PacketListenerPlayOut> {
             if ((this.d & 1 << k) != 0) {
                 nibblearray = lightengine.a(EnumSkyBlock.BLOCK).a(SectionPosition.a(chunkcoordintpair, -1 + k));
                 if (nibblearray != null && !nibblearray.c()) {
-                    this.h.add(nibblearray.getCloneIfSet()); // Paper
+                    this.h.add(com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? nibblearray.asBytes() : nibblearray.getCloneIfSet()); // Paper // Tuinity - don't clone again
                 } else {
                     this.d &= ~(1 << k);
                     if (nibblearray != null) {
diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java
index a22021766b3bffa4f96d1d4ee546b12e96b5ca58..3127fc9dd87e82243e167862cae83ac87b7f4fa0 100644
--- a/src/main/java/net/minecraft/server/PlayerChunk.java
+++ b/src/main/java/net/minecraft/server/PlayerChunk.java
@@ -387,13 +387,14 @@ public class PlayerChunk {
 
     public void a(EnumSkyBlock enumskyblock, int i) {
         Chunk chunk = this.getSendingChunk(); // Paper - no-tick view distance
+        if (this.getAvailableChunkNow() != null) this.getAvailableChunkNow().setNeedsSaving(true); // Tuinity - always mark as needing saving
 
         if (chunk != null) {
             chunk.setNeedsSaving(true);
             if (enumskyblock == EnumSkyBlock.SKY) {
-                this.s |= 1 << i - -1;
+                this.s |= 1 << (i - -1); // Tuinity - fix mojang oopsie
             } else {
-                this.r |= 1 << i - -1;
+                this.r |= 1 << (i - -1); // Tuinity - fix mojang oopsie
             }
 
         }
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
index 8c4a350d59b20efb3f3c6c19e0ae0b676465de70..44c04a0a0c744402edb5f6054919a3c181818c27 100644
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
@@ -1322,6 +1322,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
         // Tuinity end - force competion on the main thread
     }
 
+    protected final void removeLightTicket(ChunkCoordIntPair chunkcoordintpair) { this.c(chunkcoordintpair); } // Tuinity - OBFHELPER
     protected void c(ChunkCoordIntPair chunkcoordintpair) {
         this.executor.a(SystemUtils.a(() -> {
             this.chunkDistanceManager.b(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.a(ChunkStatus.FEATURES), chunkcoordintpair);
diff --git a/src/main/java/net/minecraft/server/ProtoChunk.java b/src/main/java/net/minecraft/server/ProtoChunk.java
index a3ac883500eaebb353ad3108a17b5c740e384b03..d8b759874545293764690b2ba08a4bd7605c76ae 100644
--- a/src/main/java/net/minecraft/server/ProtoChunk.java
+++ b/src/main/java/net/minecraft/server/ProtoChunk.java
@@ -48,6 +48,54 @@ public class ProtoChunk implements IChunkAccess {
     private volatile boolean u;
     final World world; // Paper - Anti-Xray - Add world // Paper - private -> default
 
+    // Tuinity start - rewrite light engine
+    protected volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] blockNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight();
+    protected volatile com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] skyNibbles = com.tuinity.tuinity.chunk.light.StarLightEngine.getFilledEmptyLight();
+    protected volatile boolean[] skyEmptinessMap;
+    protected volatile boolean[] blockEmptinessMap;
+
+    @Override
+    public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() {
+        return this.blockNibbles;
+    }
+
+    @Override
+    public void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+        this.blockNibbles = nibbles;
+    }
+
+    @Override
+    public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() {
+        return this.skyNibbles;
+    }
+
+    @Override
+    public void setSkyNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+        this.skyNibbles = nibbles;
+    }
+
+    @Override
+    public boolean[] getSkyEmptinessMap() {
+        return this.skyEmptinessMap;
+    }
+
+    @Override
+    public void setSkyEmptinessMap(boolean[] emptinessMap) {
+        this.skyEmptinessMap = emptinessMap;
+    }
+
+    @Override
+    public boolean[] getBlockEmptinessMap() {
+        return this.blockEmptinessMap;
+    }
+
+    @Override
+    public void setBlockEmptinessMap(boolean[] emptinessMap) {
+        this.blockEmptinessMap = emptinessMap;
+    }
+
+    // Tuinity end - rewrite light engine
+
     // Paper start - Anti-Xray - Add world
     @Deprecated public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter) { this(chunkcoordintpair, chunkconverter, null); } // Notice for updates: Please make sure this constructor isn't used anywhere
     public ProtoChunk(ChunkCoordIntPair chunkcoordintpair, ChunkConverter chunkconverter, World world) {
@@ -173,7 +221,7 @@ public class ProtoChunk implements IChunkAccess {
                 ChunkSection chunksection = this.a(j >> 4);
                 IBlockData iblockdata1 = chunksection.setType(i & 15, j & 15, k & 15, iblockdata);
 
-                if (this.g.b(ChunkStatus.FEATURES) && iblockdata != iblockdata1 && (iblockdata.b((IBlockAccess) this, blockposition) != iblockdata1.b((IBlockAccess) this, blockposition) || iblockdata.f() != iblockdata1.f() || iblockdata.e() || iblockdata1.e())) {
+                if ((com.tuinity.tuinity.config.TuinityConfig.useNewLightEngine ? (this.g.b(ChunkStatus.LIGHT) && this.isLit()) : (this.g.b(ChunkStatus.FEATURES))) && iblockdata != iblockdata1 && (iblockdata.b((IBlockAccess) this, blockposition) != iblockdata1.b((IBlockAccess) this, blockposition) || iblockdata.f() != iblockdata1.f() || iblockdata.e() || iblockdata1.e())) { // Tuinity - move block updates to only happen after lighting occurs
                     LightEngine lightengine = this.e();
 
                     lightengine.a(blockposition);
diff --git a/src/main/java/net/minecraft/server/ProtoChunkExtension.java b/src/main/java/net/minecraft/server/ProtoChunkExtension.java
index 300cbb8b01d94e7eb0cded0c8e118103c416d4b6..2d7c86c9a323fb7294607ee5685cef8f9ef52794 100644
--- a/src/main/java/net/minecraft/server/ProtoChunkExtension.java
+++ b/src/main/java/net/minecraft/server/ProtoChunkExtension.java
@@ -8,7 +8,50 @@ import javax.annotation.Nullable;
 
 public class ProtoChunkExtension extends ProtoChunk {
 
-    private final Chunk a;
+    private final Chunk a; public final Chunk getWrappedChunk() { return this.a; } // Tuinity - OBFHELPER
+
+    // Tuinity start - rewrite light engine
+    @Override
+    public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getBlockNibbles() {
+        return this.getWrappedChunk().getBlockNibbles();
+    }
+
+    @Override
+    public void setBlockNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+        this.getWrappedChunk().setBlockNibbles(nibbles);
+    }
+
+    @Override
+    public com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] getSkyNibbles() {
+        return this.getWrappedChunk().getSkyNibbles();
+    }
+
+    @Override
+    public void setSkyNibbles(com.tuinity.tuinity.chunk.light.SWMRNibbleArray[] nibbles) {
+        this.getWrappedChunk().setSkyNibbles(nibbles);
+    }
+
+    @Override
+    public boolean[] getSkyEmptinessMap() {
+        return this.getWrappedChunk().getSkyEmptinessMap();
+    }
+
+    @Override
+    public void setSkyEmptinessMap(final boolean[] emptinessMap) {
+        this.getWrappedChunk().setSkyEmptinessMap(emptinessMap);
+    }
+
+    @Override
+    public boolean[] getBlockEmptinessMap() {
+        return this.getWrappedChunk().getBlockEmptinessMap();
+    }
+
+    @Override
+    public void setBlockEmptinessMap(boolean[] emptinessMap) {
+        this.getWrappedChunk().setBlockEmptinessMap(emptinessMap);
+    }
+
+    // Tuinity end - rewrite light engine
 
     public ProtoChunkExtension(Chunk chunk) {
         super(chunk.getPos(), ChunkConverter.a, chunk.world); // Paper - Anti-Xray - Add parameter
diff --git a/src/main/java/net/minecraft/server/SectionPosition.java b/src/main/java/net/minecraft/server/SectionPosition.java
index f95925f1c5d091f1a129d0437bb6e175c6ac080f..0bb3ad0bffc04eba38cd827eaf5c63e8bf2aee93 100644
--- a/src/main/java/net/minecraft/server/SectionPosition.java
+++ b/src/main/java/net/minecraft/server/SectionPosition.java
@@ -7,7 +7,7 @@ import java.util.stream.StreamSupport;
 
 public class SectionPosition extends BaseBlockPosition {
 
-    private SectionPosition(int i, int j, int k) {
+    public SectionPosition(int i, int j, int k) { // Tuinity - private -> public
         super(i, j, k);
     }
 
diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java
index 25cff70b45aa2c92a9ffb2cd968ffef5bb1a6c2f..3c964f26592fc84bb5fc11c60808d11c65d93b16 100644
--- a/src/main/java/net/minecraft/server/TicketType.java
+++ b/src/main/java/net/minecraft/server/TicketType.java
@@ -28,6 +28,8 @@ public class TicketType<T> {
     public static final TicketType<ChunkCoordIntPair> URGENT = a("urgent", Comparator.comparingLong(ChunkCoordIntPair::pair), 300); // Paper
     public static final TicketType<Long> DELAYED_UNLOAD = a("delayed_unload", Long::compareTo); // Tuinity - delay chunk unloads
     public static final TicketType<Long> REQUIRED_LOAD = a("required_load", Long::compareTo); // Tuinity - make sure getChunkAt does not fail
+    public static final TicketType<ChunkCoordIntPair> LIGHT_UPDATE = a("light_update", Comparator.comparingLong(ChunkCoordIntPair::pair)); // Tuinity - ensure chunks stay loaded for lighting
+    public static final TicketType<Long> CHUNK_RELIGHT = a("chunk_relight", Long::compareTo); // Tuinity - ensure chunk stays loaded for relighting
 
     // Tuinity start - delay chunk unloads
     boolean delayUnloadViable = true;
@@ -37,6 +39,7 @@ public class TicketType<T> {
         TicketType.PRIORITY.delayUnloadViable = false;
         TicketType.URGENT.delayUnloadViable = false;
         TicketType.DELAYED_UNLOAD.delayUnloadViable = false;
+        TicketType.LIGHT_UPDATE.delayUnloadViable = false; // Tuinity - ensure chunks stay loaded for lighting
     }
     // Tuinity end - delay chunk unloads
     public static <T> TicketType<T> a(String s, Comparator<T> comparator) {
diff --git a/src/main/java/net/minecraft/server/VoxelShape.java b/src/main/java/net/minecraft/server/VoxelShape.java
index 700660dd93b3090334bb3033d5f5fdd6ab684744..e3b72922e2dfad07f3452ec5ee2af379d968c52d 100644
--- a/src/main/java/net/minecraft/server/VoxelShape.java
+++ b/src/main/java/net/minecraft/server/VoxelShape.java
@@ -57,6 +57,7 @@ public abstract class VoxelShape {
     }
     // Tuinity end - optimise multi-aabb shapes
 
+    public final VoxelShape simplify() { return this.c(); } // Tuinity - OBFHELPER
     public VoxelShape c() {
         VoxelShape[] avoxelshape = new VoxelShape[]{VoxelShapes.a()};
 
diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
index 2814846acb8b855050a865770dcc12e00fb780d1..fda23d89da29e6a6065fc4bb57a4809f8bdb8b46 100644
--- a/src/main/java/net/minecraft/server/WorldServer.java
+++ b/src/main/java/net/minecraft/server/WorldServer.java
@@ -402,6 +402,13 @@ public class WorldServer extends World implements GeneratorAccessSeed {
     }
     // Tuinity end - optimise get nearest players for entity AI
 
+    // Tuinity start - rewrite light engine
+    /**
+     * Cannot be modified during light updates.
+     */
+    public com.tuinity.tuinity.chunk.light.VariableBlockLightHandler customBlockLightHandlers;
+    // Tuinity end - rewrite light engine
+
     // Add env and gen to constructor, WorldData -> WorldDataServer
     public WorldServer(MinecraftServer minecraftserver, Executor executor, Convertable.ConversionSession convertable_conversionsession, IWorldDataServer iworlddataserver, ResourceKey<World> resourcekey, DimensionManager dimensionmanager, WorldLoadListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List<MobSpawner> list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen) {
         super(iworlddataserver, resourcekey, dimensionmanager, minecraftserver::getMethodProfiler, false, flag, i, gen, env, executor); // Paper pass executor
