4783 lines
223 KiB
Diff
4783 lines
223 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Spottedleaf <spottedleaf@spottedleaf.dev>
|
|
Date: Wed, 28 Oct 2020 16:51:55 -0700
|
|
Subject: [PATCH] Rewrite the light engine
|
|
|
|
The standard vanilla light engine is plagued by
|
|
awful performance. Paper's changes to the light engine
|
|
help a bit, however they appear to cause some lighting
|
|
errors - most easily noticed in coral generation.
|
|
|
|
The vanilla light engine's is too abstract to be modified -
|
|
so an entirely new implementation is required to fix the
|
|
performance and lighting errors.
|
|
|
|
The new implementation is designed primarily to optimise
|
|
light level propagations (increase and decrease). Unlike
|
|
the vanilla light engine, this implementation tracks more
|
|
information per queued value when performing a
|
|
breadth first search. Vanilla just tracks coordinate, which
|
|
means every time they handle a queued value, they must
|
|
also determine the coordinate's target light level
|
|
from its neighbours - very wasteful, especially considering
|
|
these checks read neighbour block data.
|
|
The new light engine tracks both position and target level,
|
|
as well as whether the target block needs to be read at all
|
|
(for checking sided propagation). So, the work done per coordinate
|
|
is significantly reduced because no work is done for calculating
|
|
the target level.
|
|
In my testing, the block get calls were reduced by approximately
|
|
an order of magnitude. However, the light read checks were only
|
|
reduced by approximately 2x - but this is fine, light read checks
|
|
are extremely cheap compared to block gets.
|
|
|
|
Generation testing showed that the new light engine improved
|
|
total generation (not lighting itself, but the whole generation process)
|
|
by 2x. According to cpu time, the light engine itself spent 10x less time
|
|
lighting chunks for generation.
|
|
|
|
diff --git a/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..9efbdba758aebcad3454a9a52c8a7eae4b7fc7eb
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/starlight/light/BlockStarLightEngine.java
|
|
@@ -0,0 +1,283 @@
|
|
+package ca.spottedleaf.starlight.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.*;
|
|
+import net.minecraft.world.phys.shapes.Shapes;
|
|
+import net.minecraft.world.phys.shapes.VoxelShape;
|
|
+
|
|
+import java.util.ArrayList;
|
|
+import java.util.Iterator;
|
|
+import java.util.List;
|
|
+import java.util.Set;
|
|
+import java.util.stream.Collectors;
|
|
+
|
|
+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 Iterator<BlockPos> getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) {
|
|
+ if (chunk instanceof ImposterProtoChunk || chunk instanceof LevelChunk) {
|
|
+ // implementation on Chunk is pretty awful, so write our own here. The big optimisation is
|
|
+ // skipping empty sections, and the far more optimised reading of types.
|
|
+ List<BlockPos> sources = new ArrayList<>();
|
|
+
|
|
+ int offX = chunk.getPos().x << 4;
|
|
+ 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.isEmpty()) {
|
|
+ // no sources in empty sections
|
|
+ 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.iterator();
|
|
+ } else {
|
|
+ // world gen and lighting run in parallel, and if lighting keeps up it can be lighting chunks that are
|
|
+ // being generated. In the nether, lava will add a lot of sources. This resulted in quite a few CME crashes.
|
|
+ // So all we do spinloop until we can collect a list of sources, and even if it is out of date we will pick up
|
|
+ // the missing sources from checkBlock.
|
|
+ for (;;) {
|
|
+ try {
|
|
+ return chunk.getLights().collect(Collectors.toList()).iterator();
|
|
+ } catch (final Exception cme) {
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) {
|
|
+ // setup sources
|
|
+ final int emittedMask = this.emittedLightMask;
|
|
+ for (final Iterator<BlockPos> positions = this.getSources(lightAccess, chunk); positions.hasNext();) {
|
|
+ final BlockPos pos = positions.next();
|
|
+ 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/light/SWMRNibbleArray.java b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..174dc7ffa66258da0b867fba5c54880e81daa6ce
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/starlight/light/SWMRNibbleArray.java
|
|
@@ -0,0 +1,439 @@
|
|
+package ca.spottedleaf.starlight.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 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.storageUpdating != null) {
|
|
+ into = this.storageUpdating;
|
|
+ } else {
|
|
+ this.storageUpdating = into = allocateBytes();
|
|
+ this.stateUpdating = INIT_STATE_INIT;
|
|
+ }
|
|
+ this.updatingDirty = true;
|
|
+
|
|
+ final int start = 0;
|
|
+ final int end = (15 | (15 << 4)) >>> 1;
|
|
+
|
|
+ /* x | (z << 4) | (y << 8) */
|
|
+ for (int y = 0; y <= 15; ++y) {
|
|
+ System.arraycopy(src, start, into, y << (8 - 1), end - start + 1);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // operation type: updating
|
|
+ public void setFull() {
|
|
+ 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) {
|
|
+ synchronized (this) {
|
|
+ // indices range from 0 -> 4096
|
|
+ final byte[] visibleBytes = this.storageVisible;
|
|
+ if (visibleBytes == null) {
|
|
+ return 0;
|
|
+ }
|
|
+ final byte value = visibleBytes[index >>> 1];
|
|
+
|
|
+ // if we are an even index, we want lower 4 bits
|
|
+ // if we are an odd index, we want upper 4 bits
|
|
+ return ((value >>> ((index & 1) << 2)) & 0xF);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // operation type: updating
|
|
+ public void set(final int x, final int y, final int z, final int value) {
|
|
+ this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value);
|
|
+ }
|
|
+
|
|
+ // operation type: updating
|
|
+ public void set(final int index, final int value) {
|
|
+ if (!this.updatingDirty) {
|
|
+ this.swapUpdatingAndMarkDirty();
|
|
+ }
|
|
+ final int shift = (index & 1) << 2;
|
|
+ final int i = index >>> 1;
|
|
+
|
|
+ this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift));
|
|
+ }
|
|
+
|
|
+ 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/light/SkyStarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..e843ceb27bce134f7785e8c45fac25d5ec747233
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/starlight/light/SkyStarLightEngine.java
|
|
@@ -0,0 +1,715 @@
|
|
+package ca.spottedleaf.starlight.light;
|
|
+
|
|
+import io.papermc.paper.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.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 == EMPTY_CHUNK_SECTION) {
|
|
+ continue;
|
|
+ }
|
|
+ } else {
|
|
+ if (emptinessMap[currY - this.minSection]) {
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // should always be full lit here
|
|
+ lowestY = currY;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ if (chunkY > lowestY) {
|
|
+ // we need to set this one to full
|
|
+ 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();
|
|
+
|
|
+ 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].isEmpty()) {
|
|
+ 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);
|
|
+ if (above == null) {
|
|
+ above = AIR_BLOCK_STATE;
|
|
+ }
|
|
+
|
|
+ for (;startY >= (this.minLightSection << 4); --startY) {
|
|
+ if ((startY & 15) == 15) {
|
|
+ // ensure this section is always checked
|
|
+ this.checkNullSection(worldX >> 4, startY >> 4, worldZ >> 4, extrudeInitialised);
|
|
+ }
|
|
+ BlockState current = this.getBlockState(worldX, startY, worldZ);
|
|
+ if (current == null) {
|
|
+ current = AIR_BLOCK_STATE;
|
|
+ }
|
|
+
|
|
+ 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/light/StarLightEngine.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..319e5c674f027e2e06322bb75b38acd46b51cc7a
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightEngine.java
|
|
@@ -0,0 +1,1577 @@
|
|
+package ca.spottedleaf.starlight.light;
|
|
+
|
|
+import io.papermc.paper.util.CoordinateUtils;
|
|
+import io.papermc.paper.util.IntegerUtil;
|
|
+import io.papermc.paper.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.*;
|
|
+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 LevelChunkSection EMPTY_CHUNK_SECTION = new LevelChunkSection(0);
|
|
+
|
|
+ protected static final AxisDirection[] DIRECTIONS = AxisDirection.values();
|
|
+ protected static final AxisDirection[] AXIS_DIRECTIONS = DIRECTIONS;
|
|
+ protected static final AxisDirection[] ONLY_HORIZONTAL_DIRECTIONS = new AxisDirection[] {
|
|
+ AxisDirection.POSITIVE_X, AxisDirection.NEGATIVE_X,
|
|
+ AxisDirection.POSITIVE_Z, AxisDirection.NEGATIVE_Z
|
|
+ };
|
|
+
|
|
+ protected static enum AxisDirection {
|
|
+
|
|
+ // Declaration order is important and relied upon. Do not change without modifying propagation code.
|
|
+ POSITIVE_X(1, 0, 0), NEGATIVE_X(-1, 0, 0),
|
|
+ POSITIVE_Z(0, 0, 1), NEGATIVE_Z(0, 0, -1),
|
|
+ POSITIVE_Y(0, 1, 0), NEGATIVE_Y(0, -1, 0);
|
|
+
|
|
+ static {
|
|
+ POSITIVE_X.opposite = NEGATIVE_X; NEGATIVE_X.opposite = POSITIVE_X;
|
|
+ POSITIVE_Z.opposite = NEGATIVE_Z; NEGATIVE_Z.opposite = POSITIVE_Z;
|
|
+ POSITIVE_Y.opposite = NEGATIVE_Y; NEGATIVE_Y.opposite = POSITIVE_Y;
|
|
+ }
|
|
+
|
|
+ protected AxisDirection opposite;
|
|
+
|
|
+ public final int x;
|
|
+ public final int y;
|
|
+ public final int z;
|
|
+ public final 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.fromNormal(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 || sections[cy - this.minSection].isEmpty() ? EMPTY_CHUNK_SECTION : sections[cy - this.minSection]) : EMPTY_CHUNK_SECTION));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected final SWMRNibbleArray getNibbleFromCache(final int chunkX, final int chunkY, final int chunkZ) {
|
|
+ return this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset];
|
|
+ }
|
|
+
|
|
+ protected final SWMRNibbleArray[] getNibblesForChunkFromCache(final int chunkX, final int chunkZ) {
|
|
+ final SWMRNibbleArray[] ret = new SWMRNibbleArray[this.maxLightSection - this.minLightSection + 1];
|
|
+
|
|
+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
|
|
+ ret[cy - this.minLightSection] = this.nibbleCache[chunkX + 5*chunkZ + (cy * (5 * 5)) + this.chunkSectionIndexOffset];
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ protected final void setNibbleInCache(final int chunkX, final int chunkY, final int chunkZ, final SWMRNibbleArray nibble) {
|
|
+ this.nibbleCache[chunkX + 5*chunkZ + (5 * 5) * chunkY + this.chunkSectionIndexOffset] = nibble;
|
|
+ }
|
|
+
|
|
+ protected final void setNibblesForChunkInCache(final int chunkX, final int chunkZ, final SWMRNibbleArray[] nibbles) {
|
|
+ for (int cy = this.minLightSection; cy <= this.maxLightSection; ++cy) {
|
|
+ this.setNibbleInCache(chunkX, cy, chunkZ, nibbles == null ? null : nibbles[cy - this.minLightSection]);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected final void updateVisible(final 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 chunkY = ((index / (5*5)) % (16 + 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 == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.getBlockState(worldX & 15, worldY & 15, worldZ & 15);
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ protected final BlockState getBlockState(final int sectionIndex, final int localIndex) {
|
|
+ final LevelChunkSection section = this.sectionCache[sectionIndex];
|
|
+
|
|
+ if (section != null) {
|
|
+ return section == EMPTY_CHUNK_SECTION ? AIR_BLOCK_STATE : section.states.get(localIndex);
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ protected final int getLightLevel(final int worldX, final int worldY, final int worldZ) {
|
|
+ final SWMRNibbleArray nibble = this.nibbleCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset];
|
|
+
|
|
+ return nibble == null ? 0 : nibble.getUpdating((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8));
|
|
+ }
|
|
+
|
|
+ protected final int getLightLevel(final int sectionIndex, final int localIndex) {
|
|
+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
|
|
+
|
|
+ return nibble == null ? 0 : nibble.getUpdating(localIndex);
|
|
+ }
|
|
+
|
|
+ protected final void setLightLevel(final int worldX, final int worldY, final int worldZ, final int level) {
|
|
+ final int sectionIndex = (worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset;
|
|
+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
|
|
+
|
|
+ if (nibble != null) {
|
|
+ nibble.set((worldX & 15) | ((worldZ & 15) << 4) | ((worldY & 15) << 8), level);
|
|
+ if (this.isClientSide) {
|
|
+ int cx1 = (worldX - 1) >> 4;
|
|
+ int cx2 = (worldX + 1) >> 4;
|
|
+ int cy1 = (worldY - 1) >> 4;
|
|
+ int cy2 = (worldY + 1) >> 4;
|
|
+ int cz1 = (worldZ - 1) >> 4;
|
|
+ int cz2 = (worldZ + 1) >> 4;
|
|
+ for (int x = cx1; x <= cx2; ++x) {
|
|
+ for (int y = cy1; y <= cy2; ++y) {
|
|
+ for (int z = cz1; z <= cz2; ++z) {
|
|
+ this.notifyUpdateCache[x + 5 * z + (5 * 5) * y + this.chunkSectionIndexOffset] = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) {
|
|
+ if (this.isClientSide) {
|
|
+ int cx1 = (worldX - 1) >> 4;
|
|
+ int cx2 = (worldX + 1) >> 4;
|
|
+ int cy1 = (worldY - 1) >> 4;
|
|
+ int cy2 = (worldY + 1) >> 4;
|
|
+ int cz1 = (worldZ - 1) >> 4;
|
|
+ int cz2 = (worldZ + 1) >> 4;
|
|
+ for (int x = cx1; x <= cx2; ++x) {
|
|
+ for (int y = cy1; y <= cy2; ++y) {
|
|
+ for (int z = cz1; z <= cz2; ++z) {
|
|
+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected final void setLightLevel(final int sectionIndex, final int localIndex, final int worldX, final int worldY, final int worldZ, final int level) {
|
|
+ final SWMRNibbleArray nibble = this.nibbleCache[sectionIndex];
|
|
+
|
|
+ if (nibble != null) {
|
|
+ nibble.set(localIndex, level);
|
|
+ if (this.isClientSide) {
|
|
+ int cx1 = (worldX - 1) >> 4;
|
|
+ int cx2 = (worldX + 1) >> 4;
|
|
+ int cy1 = (worldY - 1) >> 4;
|
|
+ int cy2 = (worldY + 1) >> 4;
|
|
+ int cz1 = (worldZ - 1) >> 4;
|
|
+ int cz2 = (worldZ + 1) >> 4;
|
|
+ for (int x = cx1; x <= cx2; ++x) {
|
|
+ for (int y = cy1; y <= cy2; ++y) {
|
|
+ for (int z = cz1; z <= cz2; ++z) {
|
|
+ this.notifyUpdateCache[x + (5 * z) + (5 * 5 * y) + this.chunkSectionIndexOffset] = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected final boolean[] getEmptinessMap(final int chunkX, final int chunkZ) {
|
|
+ return this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset];
|
|
+ }
|
|
+
|
|
+ protected final void setEmptinessMapCache(final int chunkX, final int chunkZ, final boolean[] emptinessMap) {
|
|
+ this.emptinessMapCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = emptinessMap;
|
|
+ }
|
|
+
|
|
+ protected final long getKnownTransparency(final int worldX, final int worldY, final int worldZ) {
|
|
+ throw new UnsupportedOperationException(); // :(
|
|
+ }
|
|
+
|
|
+ // warn: localIndex = y | (x << 4) | (z << 8)
|
|
+ protected final long getKnownTransparency(final int sectionIndex, final int localIndex) {
|
|
+ throw new UnsupportedOperationException(); // :(
|
|
+ }
|
|
+
|
|
+ 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].isEmpty()) {
|
|
+ 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) {
|
|
+ final Boolean valueBoxed = emptinessChanges[sectionIndex];
|
|
+ if (valueBoxed == null) {
|
|
+ if (needsInit) {
|
|
+ throw new IllegalStateException("Current chunk has not initialised emptiness map yet supplied emptiness map isn't filled?");
|
|
+ }
|
|
+ continue;
|
|
+ }
|
|
+ chunkEmptinessMap[sectionIndex] = valueBoxed.booleanValue();
|
|
+ }
|
|
+
|
|
+ // now init neighbour nibbles
|
|
+ for (int sectionIndex = (emptinessChanges.length - 1); sectionIndex >= 0; --sectionIndex) {
|
|
+ final Boolean valueBoxed = emptinessChanges[sectionIndex];
|
|
+ final int sectionY = sectionIndex + this.minSection;
|
|
+ if (valueBoxed == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final boolean empty = valueBoxed.booleanValue();
|
|
+
|
|
+ if (empty) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ for (int dz = -1; dz <= 1; ++dz) {
|
|
+ for (int dx = -1; dx <= 1; ++dx) {
|
|
+ // if we're not empty, we also need to initialise nibbles
|
|
+ // note: if we're unlit, we absolutely do not want to extrude, as light data isn't set up
|
|
+ final boolean extrude = (dx | dz) != 0 || !unlit;
|
|
+ for (int dy = 1; dy >= -1; --dy) {
|
|
+ this.initNibble(dx + chunkX, dy + sectionY, dz + chunkZ, extrude, false);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // check for de-init and lazy-init
|
|
+ // lazy init is when chunks are being lit, so at the time they weren't loaded when their neighbours were running
|
|
+ // init checks.
|
|
+ for (int dz = -1; dz <= 1; ++dz) {
|
|
+ for (int dx = -1; dx <= 1; ++dx) {
|
|
+ // does this neighbour have 1 radius loaded?
|
|
+ boolean neighboursLoaded = true;
|
|
+ neighbour_loaded_search:
|
|
+ for (int dz2 = -1; dz2 <= 1; ++dz2) {
|
|
+ for (int dx2 = -1; dx2 <= 1; ++dx2) {
|
|
+ if (this.getEmptinessMap(dx + dx2 + chunkX, dz + dz2 + chunkZ) == null) {
|
|
+ neighboursLoaded = false;
|
|
+ break neighbour_loaded_search;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (int sectionY = this.maxLightSection; sectionY >= this.minLightSection; --sectionY) {
|
|
+ // 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 != EMPTY_CHUNK_SECTION) {
|
|
+ allEmpty = false;
|
|
+ break neighbour_search;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (allEmpty & neighboursLoaded) {
|
|
+ // can only de-init when neighbours are loaded
|
|
+ // de-init is fine to delay, as de-init is just an optimisation - it's not required for lighting
|
|
+ // to be correct
|
|
+
|
|
+ // all were empty, so de-init
|
|
+ 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/light/StarLightInterface.java b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..9b8412f50d161471166cdf5c9effc2d58915faa1
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/starlight/light/StarLightInterface.java
|
|
@@ -0,0 +1,635 @@
|
|
+package ca.spottedleaf.starlight.light;
|
|
+
|
|
+import io.papermc.paper.util.CoordinateUtils;
|
|
+import io.papermc.paper.util.WorldUtil;
|
|
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.shorts.ShortCollection;
|
|
+import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
|
|
+import net.minecraft.core.BlockPos;
|
|
+import net.minecraft.core.SectionPos;
|
|
+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.ChunkStatus;
|
|
+import net.minecraft.world.level.chunk.DataLayer;
|
|
+import net.minecraft.world.level.chunk.LightChunkGetter;
|
|
+import net.minecraft.world.level.lighting.LayerLightEventListener;
|
|
+import net.minecraft.world.level.lighting.LevelLightEngine;
|
|
+import java.util.*;
|
|
+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;
|
|
+
|
|
+ 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 = 0;
|
|
+ this.maxSection = 15;
|
|
+ this.minLightSection = -1;
|
|
+ this.maxLightSection = 16;
|
|
+ } else {
|
|
+ this.minSection = WorldUtil.getMinSection(this.world);
|
|
+ this.maxSection = WorldUtil.getMaxSection(this.world);
|
|
+ this.minLightSection = WorldUtil.getMinLightSection(this.world);
|
|
+ this.maxLightSection = WorldUtil.getMaxLightSection(this.world);
|
|
+ }
|
|
+ this.lightEngine = lightEngine;
|
|
+ this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() {
|
|
+ @Override
|
|
+ public void checkBlock(final BlockPos blockPos) {
|
|
+ StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable());
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void onBlockEmissionIncrease(final BlockPos blockPos, final int i) {
|
|
+ // skylight doesn't care
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean hasLightWork() {
|
|
+ // not really correct...
|
|
+ return StarLightInterface.this.hasUpdates();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int runUpdates(final int i, final boolean bl, final boolean bl2) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void enableLightSources(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) {
|
|
+ final int x = blockPos.getX();
|
|
+ int y = blockPos.getY();
|
|
+ final int z = blockPos.getZ();
|
|
+
|
|
+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(x >> 4, z >> 4);
|
|
+ if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect()) || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
|
|
+ return 15;
|
|
+ }
|
|
+
|
|
+ int sectionY = y >> 4;
|
|
+
|
|
+ if (sectionY > StarLightInterface.this.maxLightSection) {
|
|
+ return 15;
|
|
+ }
|
|
+
|
|
+ if (sectionY < StarLightInterface.this.minLightSection) {
|
|
+ sectionY = StarLightInterface.this.minLightSection;
|
|
+ y = sectionY << 4;
|
|
+ }
|
|
+
|
|
+ final SWMRNibbleArray[] nibbles = chunk.getSkyNibbles();
|
|
+ final SWMRNibbleArray immediate = nibbles[sectionY - StarLightInterface.this.minLightSection];
|
|
+
|
|
+ if (StarLightInterface.this.isClientSide) {
|
|
+ if (!immediate.isNullNibbleUpdating()) {
|
|
+ return immediate.getUpdating(x, y, z);
|
|
+ }
|
|
+ } else {
|
|
+ if (!immediate.isNullNibbleVisible()) {
|
|
+ return immediate.getVisible(x, y, z);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final boolean[] emptinessMap = chunk.getSkyEmptinessMap();
|
|
+
|
|
+ if (emptinessMap == null) {
|
|
+ return 15;
|
|
+ }
|
|
+
|
|
+ // are we above this chunk's lowest empty section?
|
|
+ int lowestY = StarLightInterface.this.minLightSection - 1;
|
|
+ for (int currY = StarLightInterface.this.maxSection; currY >= StarLightInterface.this.minSection; --currY) {
|
|
+ if (emptinessMap[currY - StarLightInterface.this.minSection]) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ // should always be full lit here
|
|
+ lowestY = currY;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ if (sectionY > lowestY) {
|
|
+ return 15;
|
|
+ }
|
|
+
|
|
+ // this nibble is going to depend solely on the skylight data above it
|
|
+ // find first non-null data above (there does exist one, as we just found it above)
|
|
+ for (int currY = sectionY + 1; currY <= StarLightInterface.this.maxLightSection; ++currY) {
|
|
+ final SWMRNibbleArray nibble = nibbles[currY - StarLightInterface.this.minLightSection];
|
|
+ if (StarLightInterface.this.isClientSide) {
|
|
+ if (!nibble.isNullNibbleUpdating()) {
|
|
+ return nibble.getUpdating(x, 0, z);
|
|
+ }
|
|
+ } else {
|
|
+ if (!nibble.isNullNibbleVisible()) {
|
|
+ return nibble.getVisible(x, 0, z);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // should never reach here
|
|
+ return 15;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void 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 onBlockEmissionIncrease(final BlockPos blockPos, final int i) {
|
|
+ this.checkBlock(blockPos);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean hasLightWork() {
|
|
+ // not really correct...
|
|
+ return StarLightInterface.this.hasUpdates();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int runUpdates(final int i, final boolean bl, final boolean bl2) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void enableLightSources(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) {
|
|
+ final int cx = blockPos.getX() >> 4;
|
|
+ final int cy = blockPos.getY() >> 4;
|
|
+ final int cz = blockPos.getZ() >> 4;
|
|
+
|
|
+ if (cy < StarLightInterface.this.minLightSection || cy > StarLightInterface.this.maxLightSection) {
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(cx, cz);
|
|
+
|
|
+ if (chunk == null) {
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ final SWMRNibbleArray nibble = chunk.getBlockNibbles()[cy - StarLightInterface.this.minLightSection];
|
|
+ if (StarLightInterface.this.isClientSide) {
|
|
+ return nibble.getUpdating(blockPos.getX(), blockPos.getY(), blockPos.getZ());
|
|
+ } else {
|
|
+ return nibble.getVisible(blockPos.getX(), blockPos.getY(), blockPos.getZ());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void updateSectionStatus(final SectionPos pos, final boolean notReady) {
|
|
+ StarLightInterface.this.sectionChange(pos, notReady);
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+
|
|
+ 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;
|
|
+ }
|
|
+ return ((ServerLevel)this.world).getChunkSource().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 CompletableFuture<Void> 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 CompletableFuture<Void> 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);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected 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 CompletableFuture<Void> queueBlockChange(final BlockPos pos) {
|
|
+ final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new);
|
|
+ tasks.changedPositions.add(pos.immutable());
|
|
+ return tasks.onComplete;
|
|
+ }
|
|
+
|
|
+ public synchronized CompletableFuture<Void> 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.onComplete;
|
|
+ }
|
|
+
|
|
+ public synchronized CompletableFuture<Void> 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.onComplete;
|
|
+ }
|
|
+
|
|
+ public synchronized CompletableFuture<Void> 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.onComplete;
|
|
+ }
|
|
+
|
|
+ public synchronized CompletableFuture<Void> 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.onComplete;
|
|
+ }
|
|
+
|
|
+ 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();
|
|
+ }
|
|
+
|
|
+ protected static final class ChunkTasks {
|
|
+
|
|
+ public final Set<BlockPos> changedPositions = new HashSet<>();
|
|
+ public Boolean[] changedSectionSet;
|
|
+ public ShortOpenHashSet queuedEdgeChecksSky;
|
|
+ public ShortOpenHashSet queuedEdgeChecksBlock;
|
|
+ public List<Runnable> lightTasks;
|
|
+
|
|
+ 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/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java
|
|
index f436ab35798c9b6e6cb2eb60d2c02cbf9b742e69..807bbe54f6516f794bdcb735bb7b8d6812e3ef01 100644
|
|
--- a/src/main/java/com/destroystokyo/paper/PaperCommand.java
|
|
+++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java
|
|
@@ -501,6 +501,46 @@ public class PaperCommand extends Command {
|
|
}
|
|
}
|
|
|
|
+ // Paper start - rewrite light engine
|
|
+ private void starlightFixLight(ServerPlayer sender, ServerLevel world, ThreadedLevelLightEngine lightengine, int radius) {
|
|
+ long start = System.nanoTime();
|
|
+ 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
|
|
+
|
|
+ int[] pending = new int[1];
|
|
+ for (java.util.Iterator<ChunkPos> iterator = chunks.iterator(); iterator.hasNext();) {
|
|
+ final ChunkPos chunkPos = iterator.next();
|
|
+
|
|
+ final net.minecraft.world.level.chunk.ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z);
|
|
+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(net.minecraft.world.level.chunk.ChunkStatus.LIGHT)) {
|
|
+ // cannot relight this chunk
|
|
+ iterator.remove();
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ ++pending[0];
|
|
+ }
|
|
+
|
|
+ int[] relitChunks = new int[1];
|
|
+ lightengine.relight(chunks,
|
|
+ (ChunkPos chunkPos) -> {
|
|
+ ++relitChunks[0];
|
|
+ sender.getBukkitEntity().sendMessage(
|
|
+ ChatColor.BLUE + "Relit chunk " + ChatColor.DARK_AQUA + chunkPos + ChatColor.BLUE +
|
|
+ ", progress: " + ChatColor.DARK_AQUA + (int)(Math.round(100.0 * (double)(relitChunks[0])/(double)pending[0])) + "%"
|
|
+ );
|
|
+ },
|
|
+ (int totalRelit) -> {
|
|
+ final long end = System.nanoTime();
|
|
+ final long diff = Math.round(1.0e-6*(end - start));
|
|
+ sender.getBukkitEntity().sendMessage(
|
|
+ ChatColor.BLUE + "Relit " + ChatColor.DARK_AQUA + totalRelit + ChatColor.BLUE + " chunks. Took " +
|
|
+ ChatColor.DARK_AQUA + diff + "ms"
|
|
+ );
|
|
+ });
|
|
+ sender.getBukkitEntity().sendMessage(ChatColor.BLUE + "Relighting " + ChatColor.DARK_AQUA + pending[0] + ChatColor.BLUE + " chunks");
|
|
+ }
|
|
+ // Paper end - rewrite light engine
|
|
+
|
|
private void doFixLight(CommandSender sender, String[] args) {
|
|
if (!(sender instanceof Player)) {
|
|
sender.sendMessage("Only players can use this command");
|
|
@@ -509,7 +549,7 @@ public class PaperCommand extends Command {
|
|
int radius = 2;
|
|
if (args.length > 1) {
|
|
try {
|
|
- radius = Math.min(5, Integer.parseInt(args[1]));
|
|
+ radius = Math.min(32, Integer.parseInt(args[1])); // Paper - MOOOOOORE
|
|
} catch (Exception e) {
|
|
sender.sendMessage("Not a number");
|
|
return;
|
|
@@ -522,6 +562,13 @@ public class PaperCommand extends Command {
|
|
ServerLevel world = (ServerLevel) handle.level;
|
|
ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine();
|
|
|
|
+ // Paper start - rewrite light engine
|
|
+ if (true) {
|
|
+ this.starlightFixLight(handle, world, lightengine, radius);
|
|
+ return;
|
|
+ }
|
|
+ // Paper end - rewrite light engine
|
|
+
|
|
net.minecraft.core.BlockPos center = MCUtil.toBlockPosition(player.getLocation());
|
|
Deque<ChunkPos> queue = new ArrayDeque<>(MCUtil.getSpiralOutChunks(center, radius));
|
|
updateLight(sender, world, lightengine, queue);
|
|
diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java
|
|
index d8be2ad889f46491e50404916fb4ae0de5f42098..29ba097a7b230ef67d4d1b5f4ebe20cd5228d214 100644
|
|
--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java
|
|
+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundLightUpdatePacket.java
|
|
@@ -32,25 +32,17 @@ public class ClientboundLightUpdatePacket implements Packet<ClientGamePacketList
|
|
|
|
@Override
|
|
public void onPacketDispatch(ServerPlayer player) {
|
|
- remainingSends.incrementAndGet();
|
|
+ // Paper - rewrite light engine
|
|
}
|
|
|
|
@Override
|
|
public void onPacketDispatchFinish(ServerPlayer player, ChannelFuture future) {
|
|
- if (remainingSends.decrementAndGet() <= 0) {
|
|
- // incase of any race conditions, schedule this delayed
|
|
- MCUtil.scheduleTask(5, () -> {
|
|
- if (remainingSends.get() == 0) {
|
|
- cleaner1.run();
|
|
- cleaner2.run();
|
|
- }
|
|
- }, "Light Packet Release");
|
|
- }
|
|
+ // Paper - rewrite light engine
|
|
}
|
|
|
|
@Override
|
|
public boolean hasFinishListener() {
|
|
- return true;
|
|
+ return false; // Paper - rewrite light engine
|
|
}
|
|
|
|
// Paper end
|
|
@@ -63,8 +55,8 @@ public class ClientboundLightUpdatePacket implements Packet<ClientGamePacketList
|
|
this.blockYMask = new BitSet();
|
|
this.emptySkyYMask = new BitSet();
|
|
this.emptyBlockYMask = new BitSet();
|
|
- this.skyUpdates = Lists.newArrayList();this.cleaner1 = MCUtil.registerListCleaner(this, this.skyUpdates, DataLayer::releaseBytes); // Paper
|
|
- this.blockUpdates = Lists.newArrayList();this.cleaner2 = MCUtil.registerListCleaner(this, this.blockUpdates, DataLayer::releaseBytes); // Paper
|
|
+ this.skyUpdates = Lists.newArrayList();// Paper - rewrite light engine
|
|
+ this.blockUpdates = Lists.newArrayList();// Paper - rewrite light engine
|
|
|
|
for(int i = 0; i < lightProvider.getLightSectionCount(); ++i) {
|
|
if (bitSet == null || bitSet.get(i)) {
|
|
@@ -85,7 +77,7 @@ public class ClientboundLightUpdatePacket implements Packet<ClientGamePacketList
|
|
bitSet2.set(i);
|
|
} else {
|
|
bitSet.set(i);
|
|
- list.add((byte[])dataLayer.getCloneIfSet()); // Paper
|
|
+ list.add((byte[])dataLayer.getDataRaw()); // Paper // Paper - rewrite light engine - data is already cloned, don't do it again
|
|
}
|
|
}
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
index b0a6eb7846580489e0476e69565676e77fd224cd..54822e418e319db551bfea3218d00faf0e043f43 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
|
|
@@ -55,7 +55,7 @@ public class ChunkHolder {
|
|
private volatile CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
|
|
private volatile CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> tickingChunkFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
|
|
private volatile CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> 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;
|
|
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
index 410d3d840e4858dd2a4f0ac9c61ff9bb98d57c1c..6cf01d98d5d7a41f743527381d496c0d01421928 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
|
|
@@ -121,7 +121,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
|
|
public final LongSet entitiesInLevel;
|
|
public final ServerLevel level;
|
|
private final ThreadedLevelLightEngine lightEngine;
|
|
- private final BlockableEventLoop<Runnable> mainThreadExecutor;
|
|
+ public final BlockableEventLoop<Runnable> mainThreadExecutor; // Paper - public
|
|
final java.util.concurrent.Executor mainInvokingExecutor; // Paper
|
|
public final ChunkGenerator generator;
|
|
public final Supplier<DimensionDataStorage> overworldDataStorage;
|
|
diff --git a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
index 833b6b2193cf08e123aabb344f2283730aed1bcd..136ca55ac38b709e086f8e2e68dc1d0db2670ae4 100644
|
|
--- a/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
+++ b/src/main/java/net/minecraft/server/level/ThreadedLevelLightEngine.java
|
|
@@ -25,6 +25,17 @@ import net.minecraft.world.level.lighting.LevelLightEngine;
|
|
import org.apache.logging.log4j.LogManager;
|
|
import org.apache.logging.log4j.Logger;
|
|
|
|
+// Paper start
|
|
+import ca.spottedleaf.starlight.light.StarLightEngine;
|
|
+import io.papermc.paper.util.CoordinateUtils;
|
|
+import java.util.function.Supplier;
|
|
+import net.minecraft.world.level.lighting.LayerLightEventListener;
|
|
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
|
+import it.unimi.dsi.fastutil.longs.LongArrayList;
|
|
+import it.unimi.dsi.fastutil.longs.LongIterator;
|
|
+import net.minecraft.world.level.chunk.ChunkStatus;
|
|
+// Paper end
|
|
+
|
|
public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCloseable {
|
|
private static final Logger LOGGER = LogManager.getLogger();
|
|
private final ProcessorMailbox<Runnable> taskMailbox;
|
|
@@ -159,13 +170,166 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
private volatile int taskPerBatch = 5;
|
|
private final AtomicBoolean scheduled = new AtomicBoolean();
|
|
|
|
+ // Paper start - replace light engine impl
|
|
+ protected final ca.spottedleaf.starlight.light.StarLightInterface theLightEngine;
|
|
+ public final boolean hasBlockLight;
|
|
+ public final boolean hasSkyLight;
|
|
+ // Paper end - replace light engine impl
|
|
+
|
|
public ThreadedLevelLightEngine(LightChunkGetter chunkProvider, ChunkMap chunkStorage, boolean hasBlockLight, ProcessorMailbox<Runnable> processor, ProcessorHandle<ChunkTaskPriorityQueueSorter.Message<Runnable>> executor) {
|
|
- super(chunkProvider, true, hasBlockLight);
|
|
+ super(chunkProvider, false, false); // Paper - destroy vanilla light engine state
|
|
this.chunkMap = chunkStorage; this.playerChunkMap = chunkMap; // Paper
|
|
this.sorterMailbox = executor;
|
|
this.taskMailbox = processor;
|
|
+ // Paper start - replace light engine impl
|
|
+ this.hasBlockLight = true;
|
|
+ this.hasSkyLight = hasBlockLight; // Nice variable name.
|
|
+ this.theLightEngine = new ca.spottedleaf.starlight.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 = ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().getChunkAtImmediately(chunkPos.x, chunkPos.z);
|
|
+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
|
|
+ // cannot relight this chunk
|
|
+ iterator.remove();
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final Long id = Long.valueOf(this.relightCounter++);
|
|
+
|
|
+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().addTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), id);
|
|
+ ticketIds.put(chunkPos, id);
|
|
+
|
|
+ ++totalChunks;
|
|
+ }
|
|
+
|
|
+ this.taskMailbox.tell(() -> {
|
|
+ this.theLightEngine.relightChunks(chunks, (ChunkPos chunkPos) -> {
|
|
+ chunkLightCallback.accept(chunkPos);
|
|
+ ((java.util.concurrent.Executor)((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().mainThreadProcessor).execute(() -> {
|
|
+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong()).broadcast(new net.minecraft.network.protocol.game.ClientboundLightUpdatePacket(chunkPos, ThreadedLevelLightEngine.this, null, null, true), false);
|
|
+ ((ServerLevel)this.theLightEngine.getWorld()).getChunkSource().removeTicketAtLevel(TicketType.CHUNK_RELIGHT, chunkPos, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.LIGHT), ticketIds.get(chunkPos));
|
|
+ });
|
|
+ }, onComplete);
|
|
+ });
|
|
+ this.tryScheduleUpdate();
|
|
+
|
|
+ return totalChunks;
|
|
+ }
|
|
+
|
|
+ private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap();
|
|
+
|
|
+ private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, final Supplier<CompletableFuture<Void>> runnable) {
|
|
+ final ServerLevel world = (ServerLevel)this.theLightEngine.getWorld();
|
|
+
|
|
+ final ChunkAccess center = this.theLightEngine.getAnyChunkNow(chunkX, chunkZ);
|
|
+ if (center == null || !center.getStatus().isOrAfter(ChunkStatus.LIGHT)) {
|
|
+ // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing
|
|
+ // chunk scheduling, we could be lighting and generating a chunk at the same time
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (center.getStatus() != ChunkStatus.FULL) {
|
|
+ // do not keep chunk loaded, we are probably in a gen thread
|
|
+ // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen)
|
|
+ runnable.get();
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) {
|
|
+ // ticket logic is not safe to run off-main, re-schedule
|
|
+ world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> {
|
|
+ this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable);
|
|
+ });
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ);
|
|
+
|
|
+ final CompletableFuture<Void> updateFuture = runnable.get();
|
|
+
|
|
+ if (updateFuture == null) {
|
|
+ // not scheduled
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final int references = this.chunksBeingWorkedOn.addTo(key, 1);
|
|
+ if (references == 0) {
|
|
+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
|
|
+ world.getChunkSource().addRegionTicket(ca.spottedleaf.starlight.light.StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos);
|
|
+ }
|
|
+
|
|
+ // append future to this chunk and 1 radius neighbours chunk save futures
|
|
+ // this prevents us from saving the world without first waiting for the light engine
|
|
+
|
|
+ for (int dx = -1; dx <= 1; ++dx) {
|
|
+ for (int dz = -1; dz <= 1; ++dz) {
|
|
+ ChunkHolder neighbour = world.getChunkSource().chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(dx + chunkX, dz + chunkZ));
|
|
+ if (neighbour != null) {
|
|
+ neighbour.chunkToSave = neighbour.chunkToSave.thenCombine(updateFuture, (final ChunkAccess curr, final Void ignore) -> {
|
|
+ return curr;
|
|
+ });
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ updateFuture.thenAcceptAsync((final Void ignore) -> {
|
|
+ final int newReferences = this.chunksBeingWorkedOn.get(key);
|
|
+ if (newReferences == 1) {
|
|
+ this.chunksBeingWorkedOn.remove(key);
|
|
+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
|
|
+ world.getChunkSource().removeRegionTicket(ca.spottedleaf.starlight.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.fatal("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr);
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean hasLightWork() {
|
|
+ // route to new light engine
|
|
+ return this.theLightEngine.hasUpdates() || !this.queue.isEmpty();
|
|
}
|
|
|
|
+ @Override
|
|
+ public LayerLightEventListener getLayerListener(final LightLayer lightType) {
|
|
+ return lightType == LightLayer.BLOCK ? this.theLightEngine.getBlockReader() : this.theLightEngine.getSkyReader();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int getRawBrightness(final BlockPos pos, final int ambientDarkness) {
|
|
+ // need to use new light hooks for this
|
|
+ final int sky = this.theLightEngine.getSkyReader().getLightValue(pos) - ambientDarkness;
|
|
+ final int block = this.theLightEngine.getBlockReader().getLightValue(pos);
|
|
+ return Math.max(sky, block);
|
|
+ }
|
|
+ // Paper end - replace light engine impl
|
|
+
|
|
@Override
|
|
public void close() {
|
|
}
|
|
@@ -182,15 +346,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.POST_UPDATE, Util.name(() -> {
|
|
- super.checkBlock(blockPos);
|
|
- }, () -> {
|
|
- return "checkBlock " + blockPos;
|
|
- }));
|
|
+ // Paper start - replace light engine impl
|
|
+ final BlockPos posCopy = pos.immutable();
|
|
+ this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> {
|
|
+ return this.theLightEngine.blockChange(posCopy);
|
|
+ });
|
|
+ // Paper end - replace light engine impl
|
|
}
|
|
|
|
protected void updateChunkStatus(ChunkPos pos) {
|
|
+ if (true) return; // Paper - replace light engine impl
|
|
this.addTask(pos.x, pos.z, () -> {
|
|
return 0;
|
|
}, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
@@ -213,17 +378,16 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
|
|
@Override
|
|
public void updateSectionStatus(SectionPos pos, boolean notReady) {
|
|
- this.addTask(pos.x(), pos.z(), () -> {
|
|
- return 0;
|
|
- }, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
- super.updateSectionStatus(pos, notReady);
|
|
- }, () -> {
|
|
- return "updateSectionStatus " + pos + " " + notReady;
|
|
- }));
|
|
+ // Paper start - replace light engine impl
|
|
+ this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> {
|
|
+ return this.theLightEngine.sectionChange(pos, notReady);
|
|
+ });
|
|
+ // Paper end - replace light engine impl
|
|
}
|
|
|
|
@Override
|
|
public void enableLightSources(ChunkPos chunkPos, boolean bl) {
|
|
+ if (true) return; // Paper - replace light engine impl
|
|
this.addTask(chunkPos.x, chunkPos.z, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
super.enableLightSources(chunkPos, bl);
|
|
}, () -> {
|
|
@@ -233,6 +397,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
|
|
@Override
|
|
public void queueSectionData(LightLayer lightType, SectionPos pos, @Nullable DataLayer nibbles, boolean bl) {
|
|
+ if (true) return; // Paper - replace light engine impl
|
|
this.addTask(pos.x(), pos.z(), () -> {
|
|
return 0;
|
|
}, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
@@ -254,6 +419,7 @@ 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, () -> {
|
|
return 0;
|
|
}, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> {
|
|
@@ -264,6 +430,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.fatal("Failed to light chunk " + chunkPos, throwable);
|
|
+ }
|
|
+ });
|
|
+ }
|
|
+ // Paper end - replace light engine impl
|
|
ChunkPos chunkPos = chunk.getPos();
|
|
// Paper start
|
|
//ichunkaccess.b(false); // Don't need to disable this
|
|
@@ -306,7 +503,7 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
}
|
|
|
|
public void tryScheduleUpdate() {
|
|
- if ((!this.queue.isEmpty() || super.hasLightWork()) && this.scheduled.compareAndSet(false, true)) { // Paper
|
|
+ if (this.hasLightWork() && this.scheduled.compareAndSet(false, true)) { // Paper // Paper - rewrite light engine
|
|
this.taskMailbox.tell(() -> {
|
|
this.runUpdate();
|
|
this.scheduled.set(false);
|
|
@@ -323,12 +520,12 @@ public class ThreadedLevelLightEngine extends LevelLightEngine implements AutoCl
|
|
if (queue.poll(pre, post)) {
|
|
pre.forEach(Runnable::run);
|
|
pre.clear();
|
|
- super.runUpdates(Integer.MAX_VALUE, true, true);
|
|
+ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine
|
|
post.forEach(Runnable::run);
|
|
post.clear();
|
|
} else {
|
|
// might have level updates to go still
|
|
- super.runUpdates(Integer.MAX_VALUE, true, true);
|
|
+ this.theLightEngine.propagateChanges(); // Paper - rewrite light engine
|
|
}
|
|
// Paper end
|
|
}
|
|
diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java
|
|
index 41ddcf6775f99c56cf4b13b284420061e5dd6bdc..ae46429264e6a7e5c88b6b6a41a6df4db7b3e70d 100644
|
|
--- a/src/main/java/net/minecraft/server/level/TicketType.java
|
|
+++ b/src/main/java/net/minecraft/server/level/TicketType.java
|
|
@@ -32,6 +32,7 @@ public class TicketType<T> {
|
|
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> DELAY_UNLOAD = create("delay_unload", Long::compareTo, 300); // Paper
|
|
public static final TicketType<Long> REQUIRED_LOAD = create("required_load", Long::compareTo); // Paper - make sure getChunkAt does not fail
|
|
+ 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/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java
|
|
index b534fd9e5d2a17926282cf40c9d66a2143a37bfe..4a7fdea6a5f966db444dc41f7215faa99e3820b3 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
|
|
@@ -643,6 +643,7 @@ public abstract class BlockBehaviour {
|
|
this.isViewBlocking = blockbase_info.isViewBlocking;
|
|
this.hasPostProcess = blockbase_info.hasPostProcess;
|
|
this.emissiveRendering = blockbase_info.emissiveRendering;
|
|
+ this.conditionallyFullOpaque = this.isOpaque() & this.isTransparentOnSomeFaces(); // Paper
|
|
}
|
|
// Paper start - impl cached craft block data, lazy load to fix issue with loading at the wrong time
|
|
private org.bukkit.craftbukkit.block.data.CraftBlockData cachedCraftBlockData;
|
|
@@ -663,6 +664,18 @@ public abstract class BlockBehaviour {
|
|
protected boolean isTicking;
|
|
protected FluidState fluid;
|
|
// Paper end
|
|
+ // Paper start
|
|
+ protected int opacityIfCached = -1;
|
|
+ // ret -1 if opacity is dynamic, or -1 if the block is conditionally full opaque, else return opacity in [0, 15]
|
|
+ public final int getOpacityIfCached() {
|
|
+ return this.opacityIfCached;
|
|
+ }
|
|
+
|
|
+ protected final boolean conditionallyFullOpaque;
|
|
+ public final boolean isConditionallyFullOpaque() {
|
|
+ return this.conditionallyFullOpaque;
|
|
+ }
|
|
+ // Paper end
|
|
|
|
public void initCache() {
|
|
this.fluid = this.getBlock().getFluidState(this.asState()); // Paper - moved from getFluid()
|
|
@@ -671,6 +684,7 @@ public abstract class BlockBehaviour {
|
|
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 - cache opacity for light
|
|
|
|
}
|
|
|
|
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 8393950a0b38ec7897d7643803d5accdb1f983f3..ae2050da03ea2ed82b5b0dadbe4e9d37162e8fdb 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java
|
|
@@ -41,6 +41,36 @@ public interface ChunkAccess extends BlockGetter, FeatureAccess {
|
|
net.minecraft.world.level.Level getLevel();
|
|
// Paper end
|
|
|
|
+ // Paper start
|
|
+ default ca.spottedleaf.starlight.light.SWMRNibbleArray[] getBlockNibbles() {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ default void setBlockNibbles(ca.spottedleaf.starlight.light.SWMRNibbleArray[] nibbles) {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+
|
|
+ default ca.spottedleaf.starlight.light.SWMRNibbleArray[] getSkyNibbles() {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ default void setSkyNibbles(ca.spottedleaf.starlight.light.SWMRNibbleArray[] nibbles) {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ public default boolean[] getSkyEmptinessMap() {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ public default void setSkyEmptinessMap(final boolean[] emptinessMap) {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+
|
|
+ public default boolean[] getBlockEmptinessMap() {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+
|
|
+ public default void setBlockEmptinessMap(final boolean[] emptinessMap) {
|
|
+ throw new UnsupportedOperationException(this.getClass().getName());
|
|
+ }
|
|
+ // Paper end
|
|
+
|
|
BlockState getType(final int x, final int y, final int z); // Paper
|
|
@Nullable
|
|
BlockState setBlockState(BlockPos pos, BlockState state, boolean moved);
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java
|
|
index c561d69b4b903cd3625468b239cb1ace3e317700..08c1be9c9735132f383b9be9fda6f8af4a502107 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/DataLayer.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/DataLayer.java
|
|
@@ -12,7 +12,7 @@ public class DataLayer {
|
|
public static final int SIZE = 2048;
|
|
private static final int NIBBLE_SIZE = 4;
|
|
@Nullable
|
|
- protected byte[] data;
|
|
+ protected byte[] data; public final byte[] getDataRaw() { return this.data; } // Paper - provide accessor
|
|
// Paper start
|
|
public static byte[] EMPTY_NIBBLE = new byte[2048];
|
|
private static final int nibbleBucketSizeMultiplier = Integer.getInteger("Paper.nibbleBucketSize", 3072);
|
|
@@ -55,6 +55,7 @@ public class DataLayer {
|
|
boolean poolSafe = false;
|
|
public java.lang.Runnable cleaner;
|
|
private void registerCleaner() {
|
|
+ if (true) return; // Paper - purge cleaner usage
|
|
if (!poolSafe) {
|
|
cleaner = net.minecraft.server.MCUtil.registerCleaner(this, this.data, DataLayer::releaseBytes);
|
|
} else {
|
|
@@ -69,7 +70,7 @@ public class DataLayer {
|
|
}
|
|
public DataLayer(byte[] bytes, boolean isSafe) {
|
|
this.data = bytes;
|
|
- if (!isSafe) this.data = getCloneIfSet(); // Paper - clone for safety
|
|
+ // Paper - purge cleaner usage
|
|
registerCleaner();
|
|
// Paper end
|
|
if (bytes.length != 2048) {
|
|
@@ -155,7 +156,7 @@ public class DataLayer {
|
|
}
|
|
// Paper end
|
|
public DataLayer copy() {
|
|
- return this.data == null ? new DataLayer() : new DataLayer(this.data); // Paper - clone in ctor
|
|
+ return this.data == null ? new DataLayer() : new DataLayer(this.data.clone()); // Paper - clone in ctor // Paper - no longer clone in constructor
|
|
}
|
|
|
|
public String toString() {
|
|
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 8245c5834ec69beb8e3b95fb3900601009a9273f..5f6b611dc99cc04cd553b9e01dba19ec6d8b250e 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/ImposterProtoChunk.java
|
|
@@ -1,5 +1,6 @@
|
|
package net.minecraft.world.level.chunk;
|
|
|
|
+import ca.spottedleaf.starlight.light.SWMRNibbleArray;
|
|
import it.unimi.dsi.fastutil.longs.LongSet;
|
|
import java.util.BitSet;
|
|
import java.util.Map;
|
|
@@ -29,6 +30,48 @@ public class ImposterProtoChunk extends ProtoChunk {
|
|
this.wrapped = wrapped;
|
|
}
|
|
|
|
+ // Paper start - rewrite light engine
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getBlockNibbles() {
|
|
+ return this.getWrapped().getBlockNibbles();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.getWrapped().setBlockNibbles(nibbles);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getSkyNibbles() {
|
|
+ return this.getWrapped().getSkyNibbles();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.getWrapped().setSkyNibbles(nibbles);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getSkyEmptinessMap() {
|
|
+ return this.getWrapped().getSkyEmptinessMap();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.getWrapped().setSkyEmptinessMap(emptinessMap);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getBlockEmptinessMap() {
|
|
+ return this.getWrapped().getBlockEmptinessMap();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.getWrapped().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 d580f0375cce5e995c024f1b0cd4843b5718121c..f0c43f9f636a5dd1f0dfbae604dfa1f4ff9ebd4e 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
|
|
@@ -1,5 +1,7 @@
|
|
package net.minecraft.world.level.chunk;
|
|
|
|
+import ca.spottedleaf.starlight.light.SWMRNibbleArray;
|
|
+import ca.spottedleaf.starlight.light.StarLightEngine;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.destroystokyo.paper.exception.ServerInternalException;
|
|
import com.google.common.collect.Maps;
|
|
@@ -17,7 +19,6 @@ import java.util.Collections;
|
|
import java.util.Iterator;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
-import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Supplier;
|
|
@@ -28,7 +29,6 @@ import net.minecraft.CrashReport;
|
|
import net.minecraft.CrashReportCategory;
|
|
import net.minecraft.ReportedException;
|
|
import net.minecraft.core.BlockPos;
|
|
-import net.minecraft.core.DefaultedRegistry;
|
|
import net.minecraft.core.Registry;
|
|
import net.minecraft.core.SectionPos;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
@@ -125,11 +125,62 @@ public class LevelChunk implements ChunkAccess {
|
|
private volatile boolean isLightCorrect;
|
|
private final Int2ObjectMap<GameEventDispatcher> gameEventDispatcherSections;
|
|
|
|
+ // Paper start - rewrite light engine
|
|
+ protected volatile SWMRNibbleArray[] blockNibbles;
|
|
+ protected volatile SWMRNibbleArray[] skyNibbles;
|
|
+ protected volatile boolean[] skyEmptinessMap;
|
|
+ protected volatile boolean[] blockEmptinessMap;
|
|
+
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getBlockNibbles() {
|
|
+ return this.blockNibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.blockNibbles = nibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getSkyNibbles() {
|
|
+ return this.skyNibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.skyNibbles = nibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getSkyEmptinessMap() {
|
|
+ return this.skyEmptinessMap;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.skyEmptinessMap = emptinessMap;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getBlockEmptinessMap() {
|
|
+ return this.blockEmptinessMap;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.blockEmptinessMap = emptinessMap;
|
|
+ }
|
|
+ // Paper end - rewrite light engine
|
|
+
|
|
public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes) {
|
|
this(world, pos, biomes, UpgradeData.EMPTY, EmptyTickList.empty(), EmptyTickList.empty(), 0L, (LevelChunkSection[]) null, (Consumer) null);
|
|
}
|
|
|
|
public LevelChunk(Level world, ChunkPos pos, ChunkBiomeContainer biomes, UpgradeData upgradeData, TickList<Block> blockTickScheduler, TickList<Fluid> fluidTickScheduler, long inhabitedTime, @Nullable LevelChunkSection[] sections, @Nullable Consumer<LevelChunk> loadToWorldConsumer) {
|
|
+ // Paper start
|
|
+ this.blockNibbles = StarLightEngine.getFilledEmptyLight(world);
|
|
+ this.skyNibbles = StarLightEngine.getFilledEmptyLight(world);
|
|
+ // Paper end
|
|
this.pendingBlockEntities = Maps.newHashMap();
|
|
this.tickersInLevel = Maps.newHashMap();
|
|
this.heightmaps = Maps.newEnumMap(Heightmap.Types.class);
|
|
@@ -333,6 +384,12 @@ public class LevelChunk implements ChunkAccess {
|
|
|
|
public LevelChunk(ServerLevel worldserver, ProtoChunk protoChunk, @Nullable Consumer<LevelChunk> consumer) {
|
|
this(worldserver, protoChunk.getPos(), protoChunk.getBiomes(), protoChunk.getUpgradeData(), protoChunk.getBlockTicks(), protoChunk.getLiquidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), consumer);
|
|
+ // Paper start - copy over protochunk light
|
|
+ this.setBlockNibbles(protoChunk.getBlockNibbles());
|
|
+ this.setSkyNibbles(protoChunk.getSkyNibbles());
|
|
+ this.setSkyEmptinessMap(protoChunk.getSkyEmptinessMap());
|
|
+ this.setBlockEmptinessMap(protoChunk.getBlockEmptinessMap());
|
|
+ // Paper end - copy over protochunk light
|
|
Iterator iterator = protoChunk.getBlockEntities().values().iterator();
|
|
|
|
while (iterator.hasNext()) {
|
|
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
|
|
index cdac1f7b30e4c043dcb12ac9e29af926df8170bd..c1c95ac9deb134a0cf5c7763090ac5f3cddf24cc 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
|
|
@@ -18,7 +18,7 @@ public class LevelChunkSection {
|
|
short nonEmptyBlockCount; // Paper - package-private
|
|
private short tickingBlockCount;
|
|
private short tickingFluidCount;
|
|
- final PalettedContainer<BlockState> states; // Paper - package-private
|
|
+ public final PalettedContainer<BlockState> states; // Paper - package-private // Paper - public
|
|
|
|
// Paper start - Anti-Xray - Add parameters
|
|
@Deprecated public LevelChunkSection(int yOffset) { this(yOffset, null, null, true); } // Notice for updates: Please make sure this constructor isn't used anywhere
|
|
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 554474d4b2e57d8a005b3c3b9b23f32a62243058..79fd7a6e8a6eb1f699d03801910d97066677311c 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
|
|
@@ -174,7 +174,7 @@ public class PalettedContainer<T> implements PaletteResize<T> {
|
|
return this.get(y << 8 | z << 4 | x); // Paper - inline
|
|
}
|
|
|
|
- protected T get(int index) {
|
|
+ public T get(int index) { // Paper - public
|
|
T object = this.palette.valueFor(this.storage.get(index));
|
|
return (T)(object == null ? this.defaultValue : object);
|
|
}
|
|
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 7dc3d806a680150c6a2fffa1436fd63bbdc31eb3..e1b32b644bc976ff66258ed706f4d1e8de99420d 100644
|
|
--- a/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
|
|
+++ b/src/main/java/net/minecraft/world/level/chunk/ProtoChunk.java
|
|
@@ -1,5 +1,7 @@
|
|
package net.minecraft.world.level.chunk;
|
|
|
|
+import ca.spottedleaf.starlight.light.SWMRNibbleArray;
|
|
+import ca.spottedleaf.starlight.light.StarLightEngine;
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Maps;
|
|
import com.google.common.collect.Sets;
|
|
@@ -72,6 +74,53 @@ public class ProtoChunk implements ChunkAccess {
|
|
// Paper end
|
|
private static boolean PRINTED_OUTDATED_CTOR_MSG = false; // Paper - Add level
|
|
|
|
+ // Paper start - rewrite light engine
|
|
+ protected volatile SWMRNibbleArray[] blockNibbles;
|
|
+ protected volatile SWMRNibbleArray[] skyNibbles;
|
|
+ protected volatile boolean[] skyEmptinessMap;
|
|
+ protected volatile boolean[] blockEmptinessMap;
|
|
+
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getBlockNibbles() {
|
|
+ return this.blockNibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.blockNibbles = nibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public SWMRNibbleArray[] getSkyNibbles() {
|
|
+ return this.skyNibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyNibbles(SWMRNibbleArray[] nibbles) {
|
|
+ this.skyNibbles = nibbles;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getSkyEmptinessMap() {
|
|
+ return this.skyEmptinessMap;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setSkyEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.skyEmptinessMap = emptinessMap;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean[] getBlockEmptinessMap() {
|
|
+ return this.blockEmptinessMap;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void setBlockEmptinessMap(boolean[] emptinessMap) {
|
|
+ this.blockEmptinessMap = emptinessMap;
|
|
+ }
|
|
+ // Paper end - rewrite light engine
|
|
+
|
|
@Deprecated // Paper start - add level
|
|
public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor world) {
|
|
// Paper start
|
|
@@ -100,6 +149,10 @@ public class ProtoChunk implements ChunkAccess {
|
|
}
|
|
}
|
|
public ProtoChunk(ChunkPos pos, UpgradeData upgradeData, @Nullable LevelChunkSection[] levelChunkSections, ProtoTickList<Block> blockTickScheduler, ProtoTickList<Fluid> fluidTickScheduler, LevelHeightAccessor world, net.minecraft.server.level.ServerLevel level) {
|
|
+ // Paper start
|
|
+ this.blockNibbles = StarLightEngine.getFilledEmptyLight(world);
|
|
+ this.skyNibbles = StarLightEngine.getFilledEmptyLight(world);
|
|
+ // Paper end
|
|
this.level = level;
|
|
// Paper end
|
|
this.chunkPos = pos;
|
|
@@ -197,7 +250,7 @@ public class ProtoChunk implements ChunkAccess {
|
|
|
|
LevelChunkSection levelChunkSection = this.getOrCreateSection(l);
|
|
BlockState blockState = levelChunkSection.setBlockState(i & 15, j & 15, k & 15, state);
|
|
- if (this.status.isOrAfter(ChunkStatus.FEATURES) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) {
|
|
+ if (this.status.isOrAfter(ChunkStatus.LIGHT) && state != blockState && (state.getLightBlock(this, pos) != blockState.getLightBlock(this, pos) || state.getLightEmission() != blockState.getLightEmission() || state.useShapeForLightOcclusion() || blockState.useShapeForLightOcclusion())) { // Paper - move block updates to only happen after lighting occurs (or during, thanks chunk system)
|
|
this.lightEngine.checkBlock(pos);
|
|
}
|
|
|
|
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 80b9f3547bc30cb470d272132e96fcce188efd91..c81392f5b4a6dcef9c1864c1b2c268914b904561 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
|
|
@@ -63,6 +63,14 @@ import org.apache.logging.log4j.Logger;
|
|
|
|
public class ChunkSerializer {
|
|
|
|
+ // Paper start - replace light engine impl
|
|
+ private static final int STARLIGHT_LIGHT_VERSION = 5;
|
|
+
|
|
+ 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
|
|
+
|
|
private static final Logger LOGGER = LogManager.getLogger();
|
|
public static final String TAG_UPGRADE_DATA = "UpgradeData";
|
|
|
|
@@ -131,13 +139,20 @@ public class ChunkSerializer {
|
|
ProtoTickList<Fluid> protochunkticklist1 = new ProtoTickList<>((fluidtype) -> {
|
|
return fluidtype == null || fluidtype == Fluids.EMPTY;
|
|
}, pos, nbttagcompound1.getList("LiquidsToBeTicked", 9), world);
|
|
- boolean flag = nbttagcompound1.getBoolean("isLightOn");
|
|
+ boolean flag = getStatus(nbt).isOrAfter(ChunkStatus.LIGHT) && nbttagcompound1.get("isLightOn") != null && nbttagcompound1.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; // Paper
|
|
ListTag nbttaglist = nbttagcompound1.getList("Sections", 10);
|
|
int i = world.getSectionsCount();
|
|
LevelChunkSection[] achunksection = new LevelChunkSection[i];
|
|
boolean flag1 = world.dimensionType().hasSkyLight();
|
|
ServerChunkCache chunkproviderserver = world.getChunkSource();
|
|
LevelLightEngine lightengine = chunkproviderserver.getLightEngine();
|
|
+ // Paper start
|
|
+ ca.spottedleaf.starlight.light.SWMRNibbleArray[] blockNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Paper - replace light impl
|
|
+ ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = ca.spottedleaf.starlight.light.StarLightEngine.getFilledEmptyLight(world); // Paper - replace light impl
|
|
+ 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
|
|
|
|
if (flag) {
|
|
tasksToExecuteOnMain.add(() -> { // Paper - delay this task since we're executing off-main
|
|
@@ -146,7 +161,7 @@ public class ChunkSerializer {
|
|
}
|
|
|
|
for (int j = 0; j < nbttaglist.size(); ++j) {
|
|
- CompoundTag nbttagcompound2 = nbttaglist.getCompound(j);
|
|
+ CompoundTag nbttagcompound2 = nbttaglist.getCompound(j); CompoundTag sectionData = nbttagcompound2; // Paper
|
|
byte b0 = nbttagcompound2.getByte("Y");
|
|
|
|
if (nbttagcompound2.contains("Palette", 9) && nbttagcompound2.contains("BlockStates", 12)) {
|
|
@@ -164,23 +179,29 @@ public class ChunkSerializer {
|
|
}
|
|
|
|
if (flag) {
|
|
- if (nbttagcompound2.contains("BlockLight", 7)) {
|
|
- // Paper start - delay this task since we're executing off-main
|
|
- DataLayer blockLight = new DataLayer(nbttagcompound2.getByteArray("BlockLight"));
|
|
- tasksToExecuteOnMain.add(() -> {
|
|
- lightengine.queueSectionData(LightLayer.BLOCK, SectionPos.of(chunkcoordintpair1, b0), blockLight, true);
|
|
- });
|
|
- // Paper end - delay this task since we're executing off-main
|
|
+ // Paper start - rewrite light engine
|
|
+ int y = sectionData.getByte("Y");
|
|
+
|
|
+ if (sectionData.contains("BlockLight", 7)) {
|
|
+ // this is where our diff is
|
|
+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety
|
|
+ } else {
|
|
+ blockNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG));
|
|
}
|
|
|
|
- if (flag1 && nbttagcompound2.contains("SkyLight", 7)) {
|
|
- // Paper start - delay this task since we're executing off-main
|
|
- DataLayer skyLight = new DataLayer(nbttagcompound2.getByteArray("SkyLight"));
|
|
- tasksToExecuteOnMain.add(() -> {
|
|
- lightengine.queueSectionData(LightLayer.SKY, SectionPos.of(chunkcoordintpair1, b0), skyLight, true);
|
|
- });
|
|
- // Paper end - delay this task since we're executing off-main
|
|
+ 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 ca.spottedleaf.starlight.light.SWMRNibbleArray(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety
|
|
+ } else {
|
|
+ skyNibbles[y - minSection] = new ca.spottedleaf.starlight.light.SWMRNibbleArray(null, sectionData.getInt(SKYLIGHT_STATE_TAG));
|
|
+ }
|
|
}
|
|
+ // Paper end - rewrite light engine
|
|
}
|
|
}
|
|
|
|
@@ -224,8 +245,12 @@ public class ChunkSerializer {
|
|
object = new LevelChunk(world.getLevel(), pos, biomestorage, chunkconverter, (TickList) object1, (TickList) object2, k, achunksection, // Paper start - fix massive nbt memory leak due to lambda. move lambda into a container method to not leak scope. Only clone needed NBT keys.
|
|
createLoadEntitiesConsumer(new SafeNBTCopy(nbttagcompound1, "TileEntities", "Entities", "ChunkBukkitValues")) // Paper - move CB Chunk PDC into here
|
|
);// Paper end
|
|
+ ((LevelChunk)object).setBlockNibbles(blockNibbles); // Paper - replace light impl
|
|
+ ((LevelChunk)object).setSkyNibbles(skyNibbles); // Paper - replace light impl
|
|
} else {
|
|
ProtoChunk protochunk = new ProtoChunk(pos, chunkconverter, achunksection, protochunkticklist, protochunkticklist1, world, world); // Paper - add level
|
|
+ protochunk.setBlockNibbles(blockNibbles); // Paper - replace light impl
|
|
+ protochunk.setSkyNibbles(skyNibbles); // Paper - replace light impl
|
|
|
|
protochunk.setBiomes(biomestorage);
|
|
object = protochunk;
|
|
@@ -406,7 +431,7 @@ public class ChunkSerializer {
|
|
DataLayer[] blockLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()];
|
|
DataLayer[] skyLight = new DataLayer[lightenginethreaded.getMaxLightSection() - lightenginethreaded.getMinLightSection()];
|
|
|
|
- for (int i = lightenginethreaded.getMinLightSection(); i < lightenginethreaded.getMaxLightSection(); ++i) {
|
|
+ for (int i = lightenginethreaded.getMinLightSection(); false && i < lightenginethreaded.getMaxLightSection(); ++i) { // Paper - don't run loop, we don't need to - light data is per chunk now
|
|
DataLayer blockArray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkPos, i));
|
|
DataLayer skyArray = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkPos, i));
|
|
|
|
@@ -455,6 +480,12 @@ public class ChunkSerializer {
|
|
return saveChunk(world, chunk, null);
|
|
}
|
|
public static CompoundTag saveChunk(ServerLevel world, ChunkAccess chunk, AsyncSaveData asyncsavedata) {
|
|
+ // 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.light.SWMRNibbleArray[] blockNibbles = chunk.getBlockNibbles();
|
|
+ ca.spottedleaf.starlight.light.SWMRNibbleArray[] skyNibbles = chunk.getSkyNibbles();
|
|
+ // Paper end - rewrite light impl
|
|
// Paper end
|
|
ChunkPos chunkcoordintpair = chunk.getPos();
|
|
CompoundTag nbttagcompound = new CompoundTag();
|
|
@@ -483,32 +514,33 @@ public class ChunkSerializer {
|
|
LevelChunkSection chunksection = (LevelChunkSection) Arrays.stream(achunksection).filter((chunksection1) -> {
|
|
return chunksection1 != null && SectionPos.blockToSectionCoord(chunksection1.bottomBlockY()) == finalI; // CraftBukkit - decompile errors
|
|
}).findFirst().orElse(LevelChunk.EMPTY_SECTION);
|
|
- // Paper start - async chunk save for unload
|
|
- DataLayer nibblearray; // block light
|
|
- DataLayer nibblearray1; // sky light
|
|
- if (asyncsavedata == null) {
|
|
- nibblearray = lightenginethreaded.getLayerListener(LightLayer.BLOCK).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); /// Paper - diff on method change (see getAsyncSaveData)
|
|
- nibblearray1 = lightenginethreaded.getLayerListener(LightLayer.SKY).getDataLayerData(SectionPos.of(chunkcoordintpair, i)); // Paper - diff on method change (see getAsyncSaveData)
|
|
- } else {
|
|
- nibblearray = asyncsavedata.blockLight[i - lightenginethreaded.getMinLightSection()];
|
|
- nibblearray1 = asyncsavedata.skyLight[i - lightenginethreaded.getMinLightSection()];
|
|
- }
|
|
- // Paper end
|
|
- if (chunksection != LevelChunk.EMPTY_SECTION || nibblearray != null || nibblearray1 != null) {
|
|
- CompoundTag nbttagcompound2 = new CompoundTag();
|
|
+ // Paper start - replace light engine
|
|
+ ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection].getSaveState();
|
|
+ ca.spottedleaf.starlight.light.SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection].getSaveState();
|
|
+ if (chunksection != LevelChunk.EMPTY_SECTION || blockNibble != null || skyNibble != null) {
|
|
+ // Paper end - replace light engine
|
|
+ CompoundTag nbttagcompound2 = new CompoundTag(); CompoundTag section = nbttagcompound2; // Paper
|
|
|
|
nbttagcompound2.putByte("Y", (byte) (i & 255));
|
|
if (chunksection != LevelChunk.EMPTY_SECTION) {
|
|
chunksection.getStates().write(nbttagcompound2, "Palette", "BlockStates");
|
|
}
|
|
|
|
- if (nibblearray != null && !nibblearray.isEmpty()) {
|
|
- nbttagcompound2.putByteArray("BlockLight", nibblearray.asBytesPoolSafe().clone()); // Paper
|
|
+ // Paper start - replace light engine
|
|
+ if (blockNibble != null) {
|
|
+ if (blockNibble.data != null) {
|
|
+ section.putByteArray("BlockLight", blockNibble.data);
|
|
+ }
|
|
+ section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state);
|
|
}
|
|
|
|
- if (nibblearray1 != null && !nibblearray1.isEmpty()) {
|
|
- nbttagcompound2.putByteArray("SkyLight", nibblearray1.asBytesPoolSafe().clone()); // Paper
|
|
+ if (skyNibble != null) {
|
|
+ if (skyNibble.data != null) {
|
|
+ section.putByteArray("SkyLight", skyNibble.data);
|
|
+ }
|
|
+ section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state);
|
|
}
|
|
+ // Paper end - replace light engine
|
|
|
|
nbttaglist.add(nbttagcompound2);
|
|
}
|
|
@@ -516,7 +548,8 @@ public class ChunkSerializer {
|
|
|
|
nbttagcompound1.put("Sections", nbttaglist);
|
|
if (flag) {
|
|
- nbttagcompound1.putBoolean("isLightOn", true);
|
|
+ nbttagcompound1.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // Paper
|
|
+ nbttagcompound1.putBoolean("isLightOn", false); // Paper - set to false but still store, this allows us to detect --eraseCache (as eraseCache _removes_)
|
|
}
|
|
|
|
ChunkBiomeContainer biomestorage = chunk.getBiomes();
|