Paper/patches/server/0979-Starlight.patch
Jake Potrebic ac554ad46d
Updated Upstream (Bukkit/CraftBukkit) (#10691)
Updated Upstream (Bukkit/CraftBukkit)

Upstream has released updates that appear to apply and compile correctly.
This update has not been tested by PaperMC and as with ANY update, please do your own testing

Bukkit Changes:
fa99e752 PR-1007: Add ItemMeta#getAsComponentString()
94a91782 Fix copy-pasted BlockType.Typed documentation
9b34ac8c Largely restore deprecated PotionData API
51a6449b PR-1008: Deprecate ITEMS_TOOLS, removed in 1.20.5
702d15fe Fix Javadoc reference
42f6cdf4 PR-919: Add internal ItemType and BlockType, delegate Material methods to them
237bb37b SPIGOT-1166, SPIGOT-7647: Expose Damager BlockState in EntityDamageByBlockEvent
035ea146 SPIGOT-6993: Allow #setVelocity to change the speed of a fireball and add a note to #setDirection about it
8c7880fb PR-1004: Improve field rename handling and centralize conversion between bukkit and string more
87c90e93 SPIGOT-7650: Add DamageSource for EntityDeathEvent and PlayerDeathEvent

CraftBukkit Changes:
4af0f22e8 SPIGOT-7664: Item meta should prevail over block states
c2ccc46ec SPIGOT-7666: Fix access to llama and horse special slot
124ac66d7 SPIGOT-7665: Fix ThrownPotion#getEffects() implementation only bringing custom effects
66f1f439a Restore null page behaviour of signed books even though not strictly allowed by API
6118e5398 Fix regression listening to minecraft:brand custom payloads
c1a26b366 Fix unnecessary and potential not thread-safe chat visibility check
12360a7ec Remove unused imports
147b098b4 PR-1397: Add ItemMeta#getAsComponentString()
428aefe0e Largely restore deprecated PotionData API
afe5b5ee9 PR-1275: Add internal ItemType and BlockType, delegate Material methods to them
8afeafa7d SPIGOT-1166, SPIGOT-7647: Expose Damager BlockState in EntityDamageByBlockEvent
4e7d749d4 SPIGOT-6993: Allow #setVelocity to change the speed of a fireball and add a note to #setDirection about it
441880757 Support both entity_data and bucket_entity_data on axolotl/fish buckets
0e22fdd1e Fix custom direct BlockState being not correctly set in DamageSource
f2182ed47 SPIGOT-7659: TropicalFishBucketMeta should use BUCKET_ENTITY_DATA
2a6207fe1 PR-1393: Improve field rename handling and centralize conversion between bukkit and string more
c024a5039 SPIGOT-7650: Add DamageSource for EntityDeathEvent and PlayerDeathEvent
741b84480 PR-1390: Improve internal handling of damage sources
0364df4e1 SPIGOT-7657: Error when loading angry entities
2024-05-11 23:48:37 +02:00

5430 lines
247 KiB
Diff

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