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