--- a/net/minecraft/server/level/ThreadedLevelLightEngine.java
+++ b/net/minecraft/server/level/ThreadedLevelLightEngine.java
@@ -23,21 +23,312 @@
 import net.minecraft.world.level.lighting.LevelLightEngine;
 import org.slf4j.Logger;
 
+// Paper start
+import ca.spottedleaf.starlight.common.light.StarLightEngine;
+import io.papermc.paper.util.CoordinateUtils;
+import java.util.function.Supplier;
+import net.minecraft.world.level.lighting.LayerLightEventListener;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import net.minecraft.world.level.chunk.ChunkStatus;
+// Paper end
+
 public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
     private static final Logger LOGGER = LogUtils.getLogger();
     private final ProcessorMailbox<Runnable> taskMailbox;
-    private final ObjectList<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> lightTasks = new ObjectArrayList<>();
-    private final ChunkMap chunkMap;
+    // Paper start
+    private static final int MAX_PRIORITIES = ChunkMap.MAX_CHUNK_DISTANCE + 2;
+
+    static class ChunkLightQueue {
+        public boolean shouldFastUpdate;
+        java.util.ArrayDeque<Runnable> pre = new java.util.ArrayDeque<Runnable>();
+        java.util.ArrayDeque<Runnable> post = new java.util.ArrayDeque<Runnable>();
+
+        ChunkLightQueue(long chunk) {}
+    }
+
+    static class PendingLightTask {
+        long chunkId;
+        IntSupplier priority;
+        Runnable pre;
+        Runnable post;
+        boolean fastUpdate;
+
+        public PendingLightTask(long chunkId, IntSupplier priority, Runnable pre, Runnable post, boolean fastUpdate) {
+            this.chunkId = chunkId;
+            this.priority = priority;
+            this.pre = pre;
+            this.post = post;
+            this.fastUpdate = fastUpdate;
+        }
+    }
+
+
+    // Retain the chunks priority level for queued light tasks
+    class LightQueue {
+        private int size = 0;
+        private final it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<ChunkLightQueue>[] buckets = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap[MAX_PRIORITIES];
+        private final java.util.concurrent.ConcurrentLinkedQueue<PendingLightTask> pendingTasks = new java.util.concurrent.ConcurrentLinkedQueue<>();
+        private final java.util.concurrent.ConcurrentLinkedQueue<Runnable> priorityChanges = new java.util.concurrent.ConcurrentLinkedQueue<>();
+
+        private LightQueue() {
+            for (int i = 0; i < buckets.length; i++) {
+                buckets[i] = new it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<>();
+            }
+        }
+
+        public void changePriority(long pair, int currentPriority, int priority) {
+            this.priorityChanges.add(() -> {
+                ChunkLightQueue remove = this.buckets[currentPriority].remove(pair);
+                if (remove != null) {
+                    ChunkLightQueue existing = this.buckets[Math.max(1, priority)].put(pair, remove);
+                    if (existing != null) {
+                        remove.pre.addAll(existing.pre);
+                        remove.post.addAll(existing.post);
+                    }
+                }
+            });
+        }
+
+        public final void addChunk(long chunkId, IntSupplier priority, Runnable pre, Runnable post) {
+            pendingTasks.add(new PendingLightTask(chunkId, priority, pre, post, true));
+            tryScheduleUpdate();
+        }
+
+        public final void add(long chunkId, IntSupplier priority, ThreadedLevelLightEngine.TaskType type, Runnable run) {
+            pendingTasks.add(new PendingLightTask(chunkId, priority, type == TaskType.PRE_UPDATE ? run : null, type == TaskType.POST_UPDATE ? run : null, false));
+        }
+        public final void add(PendingLightTask update) {
+            int priority = update.priority.getAsInt();
+            ChunkLightQueue lightQueue = this.buckets[priority].computeIfAbsent(update.chunkId, ChunkLightQueue::new);
+
+            if (update.pre != null) {
+                this.size++;
+                lightQueue.pre.add(update.pre);
+            }
+            if (update.post != null) {
+                this.size++;
+                lightQueue.post.add(update.post);
+            }
+            if (update.fastUpdate) {
+                lightQueue.shouldFastUpdate = true;
+            }
+        }
+
+        public final boolean isEmpty() {
+            return this.size == 0 && this.pendingTasks.isEmpty();
+        }
+
+        public final int size() {
+            return this.size;
+        }
+
+        public boolean poll(java.util.List<Runnable> pre, java.util.List<Runnable> post) {
+            PendingLightTask pending;
+            while ((pending = pendingTasks.poll()) != null) {
+                add(pending);
+            }
+            Runnable run;
+            while ((run = priorityChanges.poll()) != null) {
+                run.run();
+            }
+            boolean hasWork = false;
+            it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<ChunkLightQueue>[] buckets = this.buckets;
+            int priority = 0;
+            while (priority < MAX_PRIORITIES && !isEmpty()) {
+                it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap<ChunkLightQueue> bucket = buckets[priority];
+                if (bucket.isEmpty()) {
+                    priority++;
+                    if (hasWork) {
+                        return true;
+                    } else {
+                        continue;
+                    }
+                }
+                ChunkLightQueue queue = bucket.removeFirst();
+                this.size -= queue.pre.size() + queue.post.size();
+                pre.addAll(queue.pre);
+                post.addAll(queue.post);
+                queue.pre.clear();
+                queue.post.clear();
+                hasWork = true;
+                if (queue.shouldFastUpdate) {
+                    return true;
+                }
+            }
+            return hasWork;
+        }
+    }
+
+    final LightQueue queue = new LightQueue();
+    // Paper end
+    private final ChunkMap chunkMap; private final ChunkMap playerChunkMap; // Paper
     private final ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> sorterMailbox;
     private volatile int taskPerBatch = 5;
     private final AtomicBoolean scheduled = new AtomicBoolean();
 
+    // Paper start - replace light engine impl
+    protected final ca.spottedleaf.starlight.common.light.StarLightInterface theLightEngine;
+    public final boolean hasBlockLight;
+    public final boolean hasSkyLight;
+    // Paper end - replace light engine impl
+
     public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox<Runnable> processor, ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> executor) {
-        super(chunkProvider, true, hasBlockLight);
-        this.chunkMap = chunkStorage;
+        super(chunkProvider, false, false); // Paper - destroy vanilla light engine state
+        this.chunkMap = chunkStorage; this.playerChunkMap = chunkMap; // Paper
         this.sorterMailbox = executor;
         this.taskMailbox = processor;
+        // Paper start - replace light engine impl
+        this.hasBlockLight = true;
+        this.hasSkyLight = hasBlockLight; // Nice variable name.
+        this.theLightEngine = new ca.spottedleaf.starlight.common.light.StarLightInterface(chunkProvider, this.hasSkyLight, this.hasBlockLight, this);
+        // Paper end - replace light engine impl
+    }
+
+// Paper start - replace light engine impl
+    protected final ChunkAccess getChunk(final int chunkX, final int chunkZ) {
+        return ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkX, chunkZ);
+    }
+
+    protected long relightCounter;
+
+    public int relight(java.util.Set<ChunkPos> chunks_param,
+                        java.util.function.Consumer<ChunkPos> chunkLightCallback,
+                        java.util.function.IntConsumer onComplete) {
+        if (!org.bukkit.Bukkit.isPrimaryThread()) {
+            throw new IllegalStateException("Must only be called on the main thread");
+        }
+
+        java.util.Set<ChunkPos> chunks = new java.util.LinkedHashSet<>(chunks_param);
+        // add tickets
+        java.util.Map<ChunkPos, Long> ticketIds = new java.util.HashMap<>();
+        int totalChunks = 0;
+        for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext();) {
+            final ChunkPos chunkPos = iterator.next();
+
+            final ChunkAccess chunk = (ChunkAccess)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z);
+            if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
+                // cannot relight this chunk
+                iterator.remove();
+                continue;
+            }
+
+            final Long id = Long.valueOf(this.relightCounter++);
+
+            ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id);
+            ticketIds.put(chunkPos, id);
+
+            ++totalChunks;
+        }
+
+        this.taskMailbox.tell(() -> {
+            this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> {
+                chunkLightCallback.accept(chunkPos);
+                ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> {
+                    ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null, true), false);
+                    ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos));
+                });
+            }, onComplete);
+        });
+        this.tryScheduleUpdate();
+
+        return totalChunks;
+    }
+
+    private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap();
+
+    private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, final Supplier<CompletableFuture<Void>> runnable) {
+        final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld();
+
+        final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ);
+        if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
+            // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing
+            // chunk scheduling, we could be lighting and generating a chunk at the same time
+            return;
+        }
+
+        if (center.getStatus() != ChunkStatus.FULL) {
+            // do not keep chunk loaded, we are probably in a gen thread
+            // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen)
+            runnable.get();
+            return;
+        }
+
+        if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) {
+            // ticket logic is not safe to run off-main, re-schedule
+            world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> {
+                this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable);
+            });
+            return;
+        }
+
+        final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
+
+        final CompletableFuture<Void> updateFuture = runnable.get();
+
+        if (updateFuture == null) {
+            // not scheduled
+            return;
+        }
+
+        final int references = this.chunksBeingWorkedOn.addTo(key, 1);
+        if (references == 0) {
+            final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
+            world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
+        }
+
+        // append future to this chunk and 1 radius neighbours chunk save futures
+        // this prevents us from saving the world without first waiting for the light engine
+
+        for (int dx = -1; dx <= 1; ++dx) {
+            for (int dz = -1; dz <= 1; ++dz) {
+                ChunkHolder neighbour = world.getChunkSource().chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ));
+                if (neighbour != null) {
+                    neighbour.chunkToSave = neighbour.chunkToSave.thenCombine(updateFuture, (final ChunkAccess curr, final Void ignore) -> {
+                        return curr;
+                    });
+                }
+            }
+        }
+
+        updateFuture.thenAcceptAsync((final Void ignore) -> {
+            final int newReferences = this.chunksBeingWorkedOn.get(key);
+            if (newReferences == 1) {
+                this.chunksBeingWorkedOn.remove(key);
+                final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
+                world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.common.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
+            } else {
+                this.chunksBeingWorkedOn.put(key, newReferences - 1);
+            }
+        }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> {
+            if (thr != null) {
+                LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr);
+            }
+        });
+    }
+
+    @Override
+    public boolean hasLightWork() {
+        // route to new light engine
+        return this.theLightEngine.hasUpdates() || !this.queue.isEmpty();
+    }
+
+    @Override
+    public LayerLightEventListener getLayerListener(final LightLayer lightType) {
+        return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader();
+    }
+
+    @Override
+    public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
+        // need to use new light hooks for this
+        final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness;
+        // Don't fetch the block light level if the skylight level is 15, since the value will never be higher.
+        if (sky == 15) return 15;
+        final int block = this.theLightEngine.getBlockReader().getLightValue(pos);
+        return Math.max(sky, block);
     }
+    // Paper end - replace light engine imp
 
     @Override
     public void close() {
@@ -55,15 +346,16 @@
 
     @Override
     public void checkBlock(BlockPos pos) {
-        BlockPos blockPos = pos.immutable();
-        this.addTask(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ThreadedLevelLightEngine.TaskType.POST_UPDATE, Util.name(() -> {
-            super.checkBlock(blockPos);
-        }, () -> {
-            return "checkBlock " + blockPos;
-        }));
+        // Paper start - replace light engine impl
+        final BlockPos posCopy = pos.immutable();
+        this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> {
+            return this.theLightEngine.blockChange(posCopy);
+        });
+        // Paper end - replace light engine impl
     }
 
     protected void updateChunkStatus(ChunkPos pos) {
+        if (true) return; // Paper - replace light engine impl
         this.addTask(pos.x, pos.z, () -> {
             return 0;
         }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
@@ -86,17 +378,16 @@
 
     @Override
     public void updateSectionStatus(SectionPos pos, boolean notReady) {
-        this.addTask(pos.x(), pos.z(), () -> {
-            return 0;
-        }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
-            super.updateSectionStatus(pos, notReady);
-        }, () -> {
-            return "updateSectionStatus " + pos + " " + notReady;
-        }));
+        // Paper start - replace light engine impl
+        this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
+            return this.theLightEngine.sectionChange(pos, notReady);
+        });
+        // Paper end - replace light engine impl
     }
 
     @Override
     public void enableLightSources(ChunkPos pos, boolean retainData) {
+        if (true) return; // Paper - replace light engine impl
         this.addTask(pos.x, pos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
             super.enableLightSources(pos, retainData);
         }, () -> {
@@ -106,6 +397,7 @@
 
     @Override
     public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles, boolean nonEdge) {
+        if (true) return; // Paper - replace light engine impl
         this.addTask(pos.x(), pos.z(), () -> {
             return 0;
         }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
@@ -120,17 +412,14 @@
     }
 
     private void addTask(int x, int z, IntSupplier completedLevelSupplier, ThreadedLevelLightEngine.TaskType stage, Runnable task) {
-        this.sorterMailbox.tell(ChunkTaskPriorityQueueSorter.message(() -> {
-            this.lightTasks.add(Pair.of(stage, task));
-            if (this.lightTasks.size() >= this.taskPerBatch) {
-                this.runUpdate();
-            }
-
-        }, ChunkPos.asLong(x, z), completedLevelSupplier));
+        // Paper start - replace method
+        this.queue.add(ChunkPos.asLong(x, z), completedLevelSupplier, stage, task);
+        // Paper end
     }
 
     @Override
     public void retainData(ChunkPos pos, boolean retainData) {
+        if (true) return; // Paper - replace light engine impl
         this.addTask(pos.x, pos.z, () -> {
             return 0;
         }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
@@ -153,9 +442,46 @@
     }
 
     public CompletableFuture<ChunkAccess> lightChunk(ChunkAccess chunk, boolean excludeBlocks) {
+        // Paper start - replace light engine impl
+        if (true) {
+            boolean lit = excludeBlocks;
+            final ChunkPos chunkPos = chunk.getPos();
+
+            return CompletableFuture.supplyAsync(() -> {
+                final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk);
+                if (!lit) {
+                    chunk.setLightCorrect(false);
+                    this.theLightEngine.lightChunk(chunk, emptySections);
+                    chunk.setLightCorrect(true);
+                } else {
+                    this.theLightEngine.forceLoadInChunk(chunk, emptySections);
+                    // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have
+                    // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should
+                    // catch what we miss here.
+                    this.theLightEngine.checkChunkEdges(chunkPos.x, chunkPos.z);
+                }
+
+                this.chunkMap.releaseLightTicket(chunkPos);
+                return chunk;
+            }, (runnable) -> {
+                this.theLightEngine.scheduleChunkLight(chunkPos, runnable);
+                this.tryScheduleUpdate();
+            }).whenComplete((final ChunkAccess c, final Throwable throwable) -> {
+                if (throwable != null) {
+                    LOGGER.error("Failed to light chunk " + chunkPos, throwable);
+                }
+            });
+        }
+        // Paper end - replace light engine impl
         ChunkPos chunkPos = chunk.getPos();
-        chunk.setLightCorrect(false);
-        this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
+        // Paper start
+        //ichunkaccess.b(false); // Don't need to disable this
+        long pair = chunkPos.toLong();
+        CompletableFuture<ChunkAccess> future = new CompletableFuture<>();
+        IntSupplier prioritySupplier = playerChunkMap.getChunkQueueLevel(pair);
+        boolean[] skippedPre = {false};
+        this.queue.addChunk(pair, prioritySupplier, Util.name(() -> {
+            // Paper end
             LevelChunkSection[] levelChunkSections = chunk.getSections();
 
             for(int i = 0; i < chunk.getSectionsCount(); ++i) {
@@ -175,51 +501,45 @@
 
         }, () -> {
             return "lightChunk " + chunkPos + " " + excludeBlocks;
-        }));
-        return CompletableFuture.supplyAsync(() -> {
+            // Paper start  - merge the 2 together
+        }), () -> {
+            this.chunkMap.releaseLightTicket(chunkPos); // Paper - moved from below, we want to call this even when returning early
+            if (skippedPre[0]) return; // Paper - future's already complete
             chunk.setLightCorrect(true);
             super.retainData(chunkPos, false);
-            this.chunkMap.releaseLightTicket(chunkPos);
-            return chunk;
-        }, (runnable) -> {
-            this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.POST_UPDATE, runnable);
+            //this.chunkMap.releaseLightTicket(chunkPos); // Paper - moved up
+            future.complete(chunk);
         });
+        return future;
+        // Paper end
     }
 
     public void tryScheduleUpdate() {
-        if ((!this.lightTasks.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) {
+        if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper  // Paper - rewrite light engine
             this.taskMailbox.tell(() -> {
                 this.runUpdate();
                 this.scheduled.set(false);
+                tryScheduleUpdate(); // Paper - if we still have work to do, do it!
             });
         }
 
     }
 
+    // Paper start - replace impl
+    private final java.util.List<Runnable> pre = new java.util.ArrayList<>();
+    private final java.util.List<Runnable> post = new java.util.ArrayList<>();
     private void runUpdate() {
-        int i = Math.min(this.lightTasks.size(), this.taskPerBatch);
-        ObjectListIterator<Pair<ThreadedLevelLightEngine.TaskType, Runnable>> objectListIterator = this.lightTasks.iterator();
-
-        int j;
-        for(j = 0; objectListIterator.hasNext() && j < i; ++j) {
-            Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair = objectListIterator.next();
-            if (pair.getFirst() == ThreadedLevelLightEngine.TaskType.PRE_UPDATE) {
-                pair.getSecond().run();
-            }
-        }
-
-        objectListIterator.back(j);
-        super.runUpdates(Integer.MAX_VALUE, true, true);
-
-        for(int var5 = 0; objectListIterator.hasNext() && var5 < i; ++var5) {
-            Pair<ThreadedLevelLightEngine.TaskType, Runnable> pair2 = objectListIterator.next();
-            if (pair2.getFirst() == ThreadedLevelLightEngine.TaskType.POST_UPDATE) {
-                pair2.getSecond().run();
-            }
-
-            objectListIterator.remove();
+        if (queue.poll(pre, post)) {
+            pre.forEach(Runnable::run);
+            pre.clear();
+            this.theLightEngine.propagateChanges(); // Paper - rewrite light engine
+            post.forEach(Runnable::run);
+            post.clear();
+        } else {
+            // might have level updates to go still
+            this.theLightEngine.propagateChanges(); // Paper - rewrite light engine
         }
-
+        // Paper end
     }
 
     public void setTaskPerBatch(int taskBatchSize) {
