From 3cbfe6be920cfd7a1fd681432658c5b3486bd51a Mon Sep 17 00:00:00 2001 From: Konstantin Shandurenko Date: Tue, 15 Mar 2022 20:51:11 +0300 Subject: [PATCH] Light engine --- .../java/net/minestom/demo/PlayerInit.java | 1 + gradle/libs.versions.toml | 4 +- .../minestom/server/instance/AnvilLoader.java | 17 +- .../net/minestom/server/instance/Chunk.java | 44 +- .../minestom/server/instance/ChunkStatus.java | 16 + .../server/instance/DynamicChunk.java | 62 +- .../minestom/server/instance/Instance.java | 37 +- .../server/instance/InstanceContainer.java | 30 +- .../net/minestom/server/instance/Section.java | 35 +- .../server/instance/SharedInstance.java | 11 +- .../instance/batch/ChunkGenerationBatch.java | 14 +- .../server/instance/light/ChunkLightData.java | 145 ++ .../server/instance/light/ChunkTasks.java | 20 + .../instance/light/InstanceLightManager.java | 175 ++ .../light/starlight/BlockStarLightEngine.java | 280 +++ .../instance/light/starlight/IntegerUtil.java | 21 + .../light/starlight/LightWorldUtil.java | 34 + .../light/starlight/SWMRNibbleArray.java | 436 +++++ .../light/starlight/SkyStarLightEngine.java | 717 ++++++++ .../light/starlight/StarLightEngine.java | 1494 +++++++++++++++++ .../minestom/server/registry/Registry.java | 25 + 21 files changed, 3540 insertions(+), 78 deletions(-) create mode 100644 src/main/java/net/minestom/server/instance/ChunkStatus.java create mode 100644 src/main/java/net/minestom/server/instance/light/ChunkLightData.java create mode 100644 src/main/java/net/minestom/server/instance/light/ChunkTasks.java create mode 100644 src/main/java/net/minestom/server/instance/light/InstanceLightManager.java create mode 100644 src/main/java/net/minestom/server/instance/light/starlight/BlockStarLightEngine.java create mode 100644 src/main/java/net/minestom/server/instance/light/starlight/IntegerUtil.java create mode 100644 src/main/java/net/minestom/server/instance/light/starlight/LightWorldUtil.java create mode 100644 src/main/java/net/minestom/server/instance/light/starlight/SWMRNibbleArray.java create mode 100644 src/main/java/net/minestom/server/instance/light/starlight/SkyStarLightEngine.java create mode 100644 src/main/java/net/minestom/server/instance/light/starlight/StarLightEngine.java diff --git a/demo/src/main/java/net/minestom/demo/PlayerInit.java b/demo/src/main/java/net/minestom/demo/PlayerInit.java index 0c6d818509b..43e903d5fcf 100644 --- a/demo/src/main/java/net/minestom/demo/PlayerInit.java +++ b/demo/src/main/java/net/minestom/demo/PlayerInit.java @@ -126,6 +126,7 @@ public class PlayerInit { NoiseTestGenerator noiseTestGenerator = new NoiseTestGenerator(); InstanceContainer instanceContainer = instanceManager.createInstanceContainer(DimensionType.OVERWORLD); + instanceContainer.setupLightManager(true, true); instanceContainer.setChunkGenerator(chunkGeneratorDemo); if (false) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6852175fa7a..60c721277d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ adventure = "4.9.3" kotlin = "1.6.10" hydrazine = "1.7.2" dependencyGetter = "v1.0.1" -minestomData = "895581d464" +minestomData = "cc58d6848671e8d48d240db7323a1d7ef8aefc4d" hephaistos = "2.4.2" jetbrainsAnnotations = "23.0.0" @@ -53,7 +53,7 @@ kotlin-stdlib-jdk8 = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk # Miscellaneous hydrazine = { group = "com.github.MadMartian", name = "hydrazine-path-finding", version.ref = "hydrazine" } dependencyGetter = { group = "com.github.Minestom", name = "DependencyGetter", version.ref = "dependencyGetter" } -minestomData = { group = "com.github.Minestom", name = "MinestomDataGenerator", version.ref = "minestomData" } +minestomData = { group = "com.github.RinesThaix", name = "MinestomDataGenerator", version.ref = "minestomData" } jetbrainsAnnotations = { group = "org.jetbrains", name = "annotations", version.ref = "jetbrainsAnnotations" } # Logging diff --git a/src/main/java/net/minestom/server/instance/AnvilLoader.java b/src/main/java/net/minestom/server/instance/AnvilLoader.java index 3b7f9ce07ec..27d2a6da361 100644 --- a/src/main/java/net/minestom/server/instance/AnvilLoader.java +++ b/src/main/java/net/minestom/server/instance/AnvilLoader.java @@ -3,6 +3,7 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.instance.light.ChunkLightData; import net.minestom.server.tag.Tag; import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.async.AsyncUtils; @@ -82,6 +83,7 @@ public void loadInstance(@NotNull Instance instance) { return CompletableFuture.completedFuture(null); Chunk chunk = new DynamicChunk(instance, chunkX, chunkZ); + chunk.setStatus(ChunkStatus.READING); if(fileChunk.getMinY() < instance.getDimensionType().getMinY()) { throw new AnvilException( String.format("Trying to load chunk with minY = %d, but instance dimension type (%s) has a minY of %d", @@ -125,14 +127,15 @@ public void loadInstance(@NotNull Instance instance) { loadBlocks(chunk, fileChunk); loadTileEntities(chunk, fileChunk); // Lights - for (int sectionY = chunk.getMinSection(); sectionY < chunk.getMaxSection(); sectionY++) { - var section = chunk.getSection(sectionY); - var chunkSection = fileChunk.getSection((byte) sectionY); - section.setSkyLight(chunkSection.getSkyLights()); - section.setBlockLight(chunkSection.getBlockLights()); + final ChunkLightData lightData = chunk.getLightData(); + for (int sectionY = chunk.getMinSection(); sectionY <= chunk.getMaxSection(); sectionY++) { + final ChunkSection chunkSection = fileChunk.getSection((byte) sectionY); + lightData.setSkyLight(sectionY, chunkSection.getSkyLights()); + lightData.setBlockLight(sectionY, chunkSection.getBlockLights()); } mcaFile.forget(fileChunk); - return CompletableFuture.completedFuture(chunk); + chunk.setStatus(ChunkStatus.LIGHTING); + return lightData.lightChunk(true); } private @Nullable RegionFile getMCAFile(Instance instance, int chunkX, int chunkZ) { @@ -277,7 +280,7 @@ private void loadTileEntities(Chunk loadedChunk, ChunkColumn fileChunk) { private void save(Chunk chunk, ChunkColumn chunkColumn) { chunkColumn.changeVersion(SupportedVersion.Companion.getLatest()); - chunkColumn.setYRange(chunk.getMinSection()*16, chunk.getMaxSection()*16-1); + chunkColumn.setYRange(chunk.getMinSection() << 4, chunk.getMaxSection() << 4); List tileEntities = new ArrayList<>(); chunkColumn.setGenerationStatus(ChunkColumn.GenerationStatus.Full); for (int x = 0; x < Chunk.CHUNK_SIZE_X; x++) { diff --git a/src/main/java/net/minestom/server/instance/Chunk.java b/src/main/java/net/minestom/server/instance/Chunk.java index be37c4904c0..36c3134eb39 100644 --- a/src/main/java/net/minestom/server/instance/Chunk.java +++ b/src/main/java/net/minestom/server/instance/Chunk.java @@ -7,13 +7,18 @@ import net.minestom.server.entity.Player; import net.minestom.server.entity.pathfinding.PFColumnarSpace; import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.light.ChunkLightData; +import net.minestom.server.instance.light.InstanceLightManager; import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.snapshot.Snapshotable; import net.minestom.server.tag.Tag; import net.minestom.server.tag.TagHandler; import net.minestom.server.utils.chunk.ChunkSupplier; import net.minestom.server.utils.chunk.ChunkUtils; +import net.minestom.server.utils.time.Cooldown; +import net.minestom.server.utils.validate.Check; import net.minestom.server.world.biomes.Biome; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; @@ -47,6 +52,8 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter, protected final int chunkX, chunkZ; protected final int minSection, maxSection; + private volatile ChunkStatus status = ChunkStatus.INITIALIZATION; + // Options private final boolean shouldGenerate; private boolean readOnly; @@ -60,6 +67,9 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter, // Data private final MutableNBTCompound nbt = new MutableNBTCompound(); + private volatile ChunkLightData lightData; + private long lastLightUpdate; + public Chunk(@NotNull Instance instance, int chunkX, int chunkZ, boolean shouldGenerate) { this.identifier = UUID.randomUUID(); this.instance = instance; @@ -69,6 +79,17 @@ public Chunk(@NotNull Instance instance, int chunkX, int chunkZ, boolean shouldG this.minSection = instance.getDimensionType().getMinY() / CHUNK_SECTION_SIZE; this.maxSection = (instance.getDimensionType().getMinY() + instance.getDimensionType().getHeight()) / CHUNK_SECTION_SIZE; this.viewers = new ChunkView(instance, toPosition()); + + instance.registerChunk(this); + } + + public @NotNull ChunkStatus getStatus() { + return this.status; + } + + public void setStatus(@NotNull ChunkStatus status) { + Check.argCondition(this.status.isOrAfter(status), "Can't switch chunk status from " + this.status + " to " + status); + this.status = status; } /** @@ -106,7 +127,16 @@ public Chunk(@NotNull Instance instance, int chunkX, int chunkZ, boolean shouldG * @param time the time of the update in milliseconds */ @Override - public abstract void tick(long time); + public void tick(long time) { + if (status.isOrAfter(ChunkStatus.LIGHTING)) { + final Instance instance = getInstance(); + final InstanceLightManager lightManager = instance.getLightManager(); + if (lightManager != null && !Cooldown.hasCooldown(time, lastLightUpdate, instance.getLightUpdate())) { + lastLightUpdate = time; + lightManager.runPendingTasks(this); + } + } + } /** * Gets the last time that this chunk changed. @@ -299,4 +329,16 @@ public void setTag(@NotNull Tag tag, @Nullable T value) { protected void unload() { this.loaded = false; } + + @ApiStatus.Internal + public void setLightData(@NotNull ChunkLightData lightData) { + this.lightData = lightData; + } + + public final @NotNull ChunkLightData getLightData() { + if (this.lightData == null) { + this.lightData = new ChunkLightData(this); + } + return this.lightData; + } } \ No newline at end of file diff --git a/src/main/java/net/minestom/server/instance/ChunkStatus.java b/src/main/java/net/minestom/server/instance/ChunkStatus.java new file mode 100644 index 00000000000..15013db9d38 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/ChunkStatus.java @@ -0,0 +1,16 @@ +package net.minestom.server.instance; + +import org.jetbrains.annotations.NotNull; + +public enum ChunkStatus { + INITIALIZATION, + READING, + GENERATION, + POPULATION, + LIGHTING, + COMPLETE; + + public boolean isOrAfter(@NotNull ChunkStatus status) { + return ordinal() >= status.ordinal(); + } +} diff --git a/src/main/java/net/minestom/server/instance/DynamicChunk.java b/src/main/java/net/minestom/server/instance/DynamicChunk.java index 6b65363ab57..39c421097ed 100644 --- a/src/main/java/net/minestom/server/instance/DynamicChunk.java +++ b/src/main/java/net/minestom/server/instance/DynamicChunk.java @@ -9,6 +9,8 @@ import net.minestom.server.entity.pathfinding.PFBlock; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.instance.light.ChunkLightData; +import net.minestom.server.instance.light.InstanceLightManager; import net.minestom.server.network.packet.server.CachedPacket; import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.network.packet.server.play.UpdateLightPacket; @@ -53,7 +55,7 @@ public class DynamicChunk extends Chunk { public DynamicChunk(@NotNull Instance instance, int chunkX, int chunkZ) { super(instance, chunkX, chunkZ, true); - var sectionsTemp = new Section[maxSection - minSection]; + var sectionsTemp = new Section[maxSection - minSection + 1]; Arrays.setAll(sectionsTemp, value -> new Section()); this.sections = List.of(sectionsTemp); } @@ -81,6 +83,11 @@ public void setBlock(int x, int y, int z, @NotNull Block block) { } else { this.entries.remove(index); } + // Light + final InstanceLightManager lightManager = getInstance().getLightManager(); + if (lightManager != null) { + lightManager.onBlockChange(this, x, y, z); + } // Block tick if (handler != null && handler.isTickable()) { this.tickableMap.put(index, block); @@ -111,15 +118,18 @@ public void setBiome(int x, int y, int z, @NotNull Biome biome) { @Override public void tick(long time) { - if (tickableMap.isEmpty()) return; - tickableMap.int2ObjectEntrySet().fastForEach(entry -> { - final int index = entry.getIntKey(); - final Block block = entry.getValue(); - final BlockHandler handler = block.handler(); - if (handler == null) return; - final Point blockPosition = ChunkUtils.getBlockPosition(index, chunkX, chunkZ); - handler.tick(new BlockHandler.Tick(block, instance, blockPosition)); - }); + super.tick(time); + if (getStatus() == ChunkStatus.COMPLETE) { + if (tickableMap.isEmpty()) return; + tickableMap.int2ObjectEntrySet().fastForEach(entry -> { + final int index = entry.getIntKey(); + final Block block = entry.getValue(); + final BlockHandler handler = block.handler(); + if (handler == null) return; + final Point blockPosition = ChunkUtils.getBlockPosition(index, chunkX, chunkZ); + handler.tick(new BlockHandler.Tick(block, instance, blockPosition)); + }); + } } @Override @@ -169,7 +179,8 @@ public void sendChunk() { @Override public @NotNull Chunk copy(@NotNull Instance instance, int chunkX, int chunkZ) { - DynamicChunk dynamicChunk = new DynamicChunk(instance, chunkX, chunkZ); + final DynamicChunk dynamicChunk = new DynamicChunk(instance, chunkX, chunkZ); + dynamicChunk.setLightData(getLightData().copy(dynamicChunk)); dynamicChunk.sections = sections.stream().map(Section::clone).toList(); dynamicChunk.entries.putAll(entries); return dynamicChunk; @@ -177,6 +188,7 @@ public void sendChunk() { @Override public void reset() { + getLightData().clear(); for (Section section : sections) section.clear(); this.entries.clear(); } @@ -220,22 +232,24 @@ private LightData createLightData() { List skyLights = new ArrayList<>(); List blockLights = new ArrayList<>(); - int index = 0; - for (Section section : sections) { - index++; - final byte[] skyLight = section.getSkyLight(); - final byte[] blockLight = section.getBlockLight(); - if (skyLight.length != 0) { - skyLights.add(skyLight); - skyMask.set(index); + final InstanceLightManager lightManager = getInstance().getLightManager(); + final boolean hasSkyLight = lightManager != null && lightManager.hasSkyLight(); + final boolean hasBlockLight = lightManager != null && lightManager.hasBlockLight(); + final ChunkLightData lightData = getLightData(); + for (int sectionY = getMinSection(), sectionIndex = 0; sectionY <= getMaxSection(); ++sectionY, ++sectionIndex) { + byte[] light = lightData.getSkyLight(sectionIndex); + if (hasSkyLight && light != null && light.length != 0) { + skyLights.add(light); + skyMask.set(sectionIndex); } else { - emptySkyMask.set(index); + emptySkyMask.set(sectionIndex); } - if (blockLight.length != 0) { - blockLights.add(blockLight); - blockMask.set(index); + light = lightData.getBlockLight(sectionIndex); + if (hasBlockLight && light != null && light.length != 0) { + blockLights.add(light); + blockMask.set(sectionIndex); } else { - emptyBlockMask.set(index); + emptyBlockMask.set(sectionIndex); } } return new LightData(true, diff --git a/src/main/java/net/minestom/server/instance/Instance.java b/src/main/java/net/minestom/server/instance/Instance.java index e8902d28b9d..aab150eea05 100644 --- a/src/main/java/net/minestom/server/instance/Instance.java +++ b/src/main/java/net/minestom/server/instance/Instance.java @@ -16,6 +16,7 @@ import net.minestom.server.event.instance.InstanceTickEvent; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.instance.light.InstanceLightManager; import net.minestom.server.network.packet.server.play.BlockActionPacket; import net.minestom.server.network.packet.server.play.TimeUpdatePacket; import net.minestom.server.snapshot.ChunkSnapshot; @@ -99,6 +100,9 @@ public abstract class Instance implements Block.Getter, Block.Setter, Tickable, // Adventure private final Pointers pointers; + private InstanceLightManager lightManager; + private Duration lightUpdate = TimeUnit.SERVER_TICK.getDuration(); + /** * Creates a new instance. * @@ -141,6 +145,13 @@ public void scheduleNextTick(@NotNull Consumer callback) { @ApiStatus.Internal public abstract boolean breakBlock(@NotNull Player player, @NotNull Point blockPosition); + protected abstract void cacheChunk(@NotNull Chunk chunk); + + protected final void registerChunk(@NotNull Chunk chunk) { + cacheChunk(chunk); + MinecraftServer.process().dispatcher().createPartition(chunk); + } + /** * Forces the generation of a {@link Chunk}, even if no file and {@link ChunkGenerator} are defined. * @@ -210,7 +221,11 @@ public void unloadChunk(int chunkX, int chunkZ) { * @param chunkZ the chunk Z * @return the chunk at the specified position, null if not loaded */ - public abstract @Nullable Chunk getChunk(int chunkX, int chunkZ); + public @Nullable Chunk getChunk(int chunkX, int chunkZ) { + return getChunk(chunkX, chunkZ, ChunkStatus.COMPLETE); + } + + public abstract @Nullable Chunk getChunk(int chunkX, int chunkZ, @NotNull ChunkStatus minStatus); /** * @param chunkX the chunk X @@ -692,4 +707,24 @@ public void setExplosionSupplier(@Nullable ExplosionSupplier supplier) { public @NotNull Pointers pointers() { return this.pointers; } + + public final void setupLightManager(final boolean enableSkyLight, final boolean enableBlockLight) { + this.lightManager = new InstanceLightManager(this, enableSkyLight, enableBlockLight); + } + + public final @Nullable InstanceLightManager getLightManager() { + return this.lightManager; + } + + public final void setLightUpdate(@NotNull Duration lightUpdate) { + this.lightUpdate = lightUpdate; + } + + final @NotNull Duration getLightUpdate() { + return this.lightUpdate; + } + + public void onLightUpdate(final int chunkX, final int chunkZ, final int sectionIndex, final boolean skyLight) { + + } } diff --git a/src/main/java/net/minestom/server/instance/InstanceContainer.java b/src/main/java/net/minestom/server/instance/InstanceContainer.java index b2fae949b20..9e6bc9b6557 100644 --- a/src/main/java/net/minestom/server/instance/InstanceContainer.java +++ b/src/main/java/net/minestom/server/instance/InstanceContainer.java @@ -13,6 +13,7 @@ import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; import net.minestom.server.instance.block.rule.BlockPlacementRule; +import net.minestom.server.instance.light.InstanceLightManager; import net.minestom.server.network.packet.server.play.BlockChangePacket; import net.minestom.server.network.packet.server.play.BlockEntityDataPacket; import net.minestom.server.network.packet.server.play.EffectPacket; @@ -200,6 +201,11 @@ public boolean breakBlock(@NotNull Player player, @NotNull Point blockPosition) return allowed; } + @Override + protected void cacheChunk(@NotNull Chunk chunk) { + this.chunks.put(getChunkIndex(chunk), chunk); + } + @Override public @NotNull CompletableFuture loadChunk(int chunkX, int chunkZ) { return loadOrRetrieve(chunkX, chunkZ, () -> retrieveChunk(chunkX, chunkZ)); @@ -224,6 +230,11 @@ public synchronized void unloadChunk(@NotNull Chunk chunk) { EventDispatcher.call(new InstanceChunkUnloadEvent(this, chunk)); // Remove all entities in chunk getEntityTracker().chunkEntities(chunkX, chunkZ, EntityTracker.Target.ENTITIES).forEach(Entity::remove); + // Stop light engine tasks + final InstanceLightManager lightManager = getLightManager(); + if (lightManager != null) { + lightManager.removeChunkTasks(chunk); + } // Clear cache this.chunks.remove(getChunkIndex(chunkX, chunkZ)); chunk.unload(); @@ -232,8 +243,12 @@ public synchronized void unloadChunk(@NotNull Chunk chunk) { } @Override - public Chunk getChunk(int chunkX, int chunkZ) { - return chunks.get(getChunkIndex(chunkX, chunkZ)); + public @Nullable Chunk getChunk(int chunkX, int chunkZ, @NotNull ChunkStatus status) { + final Chunk chunk = chunks.get(getChunkIndex(chunkX, chunkZ)); + if (chunk == null || !chunk.getStatus().isOrAfter(status)) { + return null; + } + return chunk; } @Override @@ -269,6 +284,7 @@ public Chunk getChunk(int chunkX, int chunkZ) { }) // cache the retrieved chunk .thenAccept(chunk -> { + chunk.setStatus(ChunkStatus.COMPLETE); // TODO run in the instance thread? cacheChunk(chunk); EventDispatcher.call(new InstanceChunkLoadEvent(this, chunk)); @@ -293,8 +309,9 @@ public Chunk getChunk(int chunkX, int chunkZ) { final ChunkGenerationBatch chunkBatch = new ChunkGenerationBatch(this, chunk); return chunkBatch.generate(generator); } else { - // No chunk generator, execute the callback with the empty chunk - return CompletableFuture.completedFuture(chunk); + // No chunk generator, generate light and execute the callback with the empty chunk + chunk.setStatus(ChunkStatus.LIGHTING); + return chunk.getLightData().lightChunk(false); } } @@ -529,9 +546,4 @@ private CompletableFuture loadOrRetrieve(int chunkX, int chunkZ, Supplier return supplier.get(); } - private void cacheChunk(@NotNull Chunk chunk) { - this.chunks.put(getChunkIndex(chunk), chunk); - var dispatcher = MinecraftServer.process().dispatcher(); - dispatcher.createPartition(chunk); - } } \ No newline at end of file diff --git a/src/main/java/net/minestom/server/instance/Section.java b/src/main/java/net/minestom/server/instance/Section.java index edda1189dcc..ccf3cfd1059 100644 --- a/src/main/java/net/minestom/server/instance/Section.java +++ b/src/main/java/net/minestom/server/instance/Section.java @@ -8,20 +8,14 @@ public final class Section implements Writeable { private Palette blockPalette; private Palette biomePalette; - private byte[] skyLight; - private byte[] blockLight; - private Section(Palette blockPalette, Palette biomePalette, - byte[] skyLight, byte[] blockLight) { + private Section(Palette blockPalette, Palette biomePalette) { this.blockPalette = blockPalette; this.biomePalette = biomePalette; - this.skyLight = skyLight; - this.blockLight = blockLight; } public Section() { - this(Palette.blocks(), Palette.biomes(), - new byte[0], new byte[0]); + this(Palette.blocks(), Palette.biomes()); } public Palette blockPalette() { @@ -32,33 +26,14 @@ public Palette biomePalette() { return biomePalette; } - public byte[] getSkyLight() { - return skyLight; - } - - public void setSkyLight(byte[] skyLight) { - this.skyLight = skyLight; - } - - public byte[] getBlockLight() { - return blockLight; - } - - public void setBlockLight(byte[] blockLight) { - this.blockLight = blockLight; - } - public void clear() { this.blockPalette.fill(0); this.biomePalette.fill(0); - this.skyLight = new byte[0]; - this.blockLight = new byte[0]; } @Override public @NotNull Section clone() { - return new Section(blockPalette.clone(), biomePalette.clone(), - skyLight.clone(), blockLight.clone()); + return new Section(blockPalette.clone(), biomePalette.clone()); } @Override @@ -67,4 +42,8 @@ public void write(@NotNull BinaryWriter writer) { writer.write(blockPalette); writer.write(biomePalette); } + + public boolean hasOnlyAir() { + return blockPalette.count() == 0; + } } diff --git a/src/main/java/net/minestom/server/instance/SharedInstance.java b/src/main/java/net/minestom/server/instance/SharedInstance.java index a4ae6eaaf40..e3a804b599a 100644 --- a/src/main/java/net/minestom/server/instance/SharedInstance.java +++ b/src/main/java/net/minestom/server/instance/SharedInstance.java @@ -1,10 +1,12 @@ package net.minestom.server.instance; +import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Point; import net.minestom.server.entity.Player; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.UUID; @@ -37,6 +39,11 @@ public boolean breakBlock(@NotNull Player player, @NotNull Point blockPosition) return instanceContainer.breakBlock(player, blockPosition); } + @Override + protected void cacheChunk(@NotNull Chunk chunk) { + instanceContainer.cacheChunk(chunk); + } + @Override public @NotNull CompletableFuture loadChunk(int chunkX, int chunkZ) { return instanceContainer.loadChunk(chunkX, chunkZ); @@ -53,8 +60,8 @@ public void unloadChunk(@NotNull Chunk chunk) { } @Override - public Chunk getChunk(int chunkX, int chunkZ) { - return instanceContainer.getChunk(chunkX, chunkZ); + public @Nullable Chunk getChunk(int chunkX, int chunkZ, @NotNull ChunkStatus status) { + return instanceContainer.getChunk(chunkX, chunkZ, status); } @Override diff --git a/src/main/java/net/minestom/server/instance/batch/ChunkGenerationBatch.java b/src/main/java/net/minestom/server/instance/batch/ChunkGenerationBatch.java index 933011c99e5..186a05d3adc 100644 --- a/src/main/java/net/minestom/server/instance/batch/ChunkGenerationBatch.java +++ b/src/main/java/net/minestom/server/instance/batch/ChunkGenerationBatch.java @@ -1,5 +1,6 @@ package net.minestom.server.instance.batch; +import net.minestom.server.MinecraftServer; import net.minestom.server.instance.*; import net.minestom.server.instance.block.Block; import net.minestom.server.utils.chunk.ChunkCallback; @@ -31,18 +32,23 @@ public void setBlock(int x, int y, int z, @NotNull Block block) { final List populators = chunkGenerator.getPopulators(); final boolean hasPopulator = populators != null && !populators.isEmpty(); + chunk.setStatus(ChunkStatus.GENERATION); chunkGenerator.generateChunkData(this, chunk.getChunkX(), chunk.getChunkZ()); if (hasPopulator) { + chunk.setStatus(ChunkStatus.POPULATION); for (ChunkPopulator chunkPopulator : populators) { chunkPopulator.populateChunk(this, chunk); } } - // Update the chunk. - this.chunk.sendChunk(); - this.instance.refreshLastBlockChangeTime(); - completableFuture.complete(chunk); + chunk.setStatus(ChunkStatus.LIGHTING); + chunk.getLightData().lightChunk(false).thenRun(() -> { + // Update the chunk. + this.chunk.sendChunk(); + this.instance.refreshLastBlockChangeTime(); + completableFuture.complete(chunk); + }); } }); return completableFuture; diff --git a/src/main/java/net/minestom/server/instance/light/ChunkLightData.java b/src/main/java/net/minestom/server/instance/light/ChunkLightData.java new file mode 100644 index 00000000000..0222db0ec5e --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/ChunkLightData.java @@ -0,0 +1,145 @@ +package net.minestom.server.instance.light; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.light.starlight.SWMRNibbleArray; +import net.minestom.server.instance.light.starlight.StarLightEngine; +import net.minestom.server.utils.validate.Check; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; + +public final class ChunkLightData { + + private final @NotNull Chunk chunk; + + @Nullable ChunkTasks tasks = null; + private boolean lightCorrect; + + private SWMRNibbleArray[] skyNibbles; + private boolean[] skyEmptinessMap; + private SWMRNibbleArray[] blockNibbles; + private boolean[] blockEmptinessMap; + + public ChunkLightData(@NotNull Chunk chunk) { + this.chunk = chunk; + clear(); + } + + private ChunkLightData(@NotNull Chunk chunk, boolean lightCorrect, SWMRNibbleArray[] skyNibbles, boolean[] skyEmptinessMap, SWMRNibbleArray[] blockNibbles, boolean[] blockEmptinessMap) { + this.chunk = chunk; + this.lightCorrect = lightCorrect; + this.skyNibbles = skyNibbles; + this.skyEmptinessMap = skyEmptinessMap; + this.blockNibbles = blockNibbles; + this.blockEmptinessMap = blockEmptinessMap; + } + + public byte[] getSkyLight(final int sectionIndex) { + return this.skyNibbles[sectionIndex].toVanillaNibble(); + } + + public void setSkyLight(final int sectionIndex, final byte[] light) { + this.skyNibbles[sectionIndex] = SWMRNibbleArray.fromVanilla(light); + } + + public byte[] getBlockLight(final int sectionIndex) { + return this.blockNibbles[sectionIndex].toVanillaNibble(); + } + + public void setBlockLight(final int sectionIndex, final byte[] light) { + this.blockNibbles[sectionIndex] = SWMRNibbleArray.fromVanilla(light); + } + + public void clear() { + lightCorrect = false; + skyNibbles = chunk.getInstance().getLightManager().hasSkyLight() ? StarLightEngine.getFilledEmptyLight(chunk.getInstance()) : null; + blockNibbles = chunk.getInstance().getLightManager().hasBlockLight() ? StarLightEngine.getFilledEmptyLight(chunk.getInstance()) : null; + } + + @ApiStatus.Internal + public boolean isLightCorrect() { + return this.lightCorrect; + } + + @ApiStatus.Internal + public void setSkyNibbles(@NotNull SWMRNibbleArray[] nibbles) { +// System.out.println("setting sky nibbles"); + skyNibbles = nibbles; + } + + @ApiStatus.Internal + public @NotNull SWMRNibbleArray[] getSkyNibbles() { + return skyNibbles; + } + + @ApiStatus.Internal + public boolean[] getSkyEmptinessMap() { + return skyEmptinessMap; + } + + @ApiStatus.Internal + public void setSkyEmptinessMap(boolean[] skyEmptinessMap) { + this.skyEmptinessMap = skyEmptinessMap; + } + + @ApiStatus.Internal + public void setBlockNibbles(@NotNull SWMRNibbleArray[] nibbles) { +// System.out.println("setting block nibbles"); + blockNibbles = nibbles; + } + + @ApiStatus.Internal + public @NotNull SWMRNibbleArray[] getBlockNibbles() { + return blockNibbles; + } + + @ApiStatus.Internal + public boolean[] getBlockEmptinessMap() { + return blockEmptinessMap; + } + + @ApiStatus.Internal + public void setBlockEmptinessMap(boolean[] blockEmptinessMap) { + this.blockEmptinessMap = blockEmptinessMap; + } + + @ApiStatus.Internal + public CompletableFuture lightChunk(final boolean lit) { + final InstanceLightManager lightManager = chunk.getInstance().getLightManager(); + Check.notNull(lightManager, "Light manager can't be null during chunk lighting"); + return CompletableFuture.supplyAsync( + () -> { + final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); + if (!lit) { + lightCorrect = false; + lightManager.lightChunk(chunk, emptySections); + } else { + lightManager.forceLoadInChunk(chunk, emptySections); + lightManager.checkChunkEdges(chunk); + } + lightCorrect = true; + return chunk; + }, + (runnable) -> lightManager.scheduleChunkLight(this, runnable) + ).whenComplete((final Chunk c, final Throwable throwable) -> { + if (throwable != null) { + MinecraftServer.LOGGER.error("Failed to light chunk " + chunk, throwable); + } + }); + } + + public @NotNull ChunkLightData copy(@NotNull Chunk newChunk) { + return new ChunkLightData( + newChunk, + lightCorrect, + skyNibbles.clone(), + skyEmptinessMap.clone(), + blockNibbles.clone(), + blockEmptinessMap.clone() + ); + } + +} diff --git a/src/main/java/net/minestom/server/instance/light/ChunkTasks.java b/src/main/java/net/minestom/server/instance/light/ChunkTasks.java new file mode 100644 index 00000000000..34fa2bb53cc --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/ChunkTasks.java @@ -0,0 +1,20 @@ +package net.minestom.server.instance.light; + +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +class ChunkTasks { + + public final IntSet changesPositions = new IntOpenHashSet(); + public Boolean[] changedSectionSet; + public ShortOpenHashSet queuedEdgeChecksSky; + public ShortOpenHashSet queuedEdgeChecksBlock; + public List lightTasks; + + public final CompletableFuture onComplete = new CompletableFuture<>(); + +} diff --git a/src/main/java/net/minestom/server/instance/light/InstanceLightManager.java b/src/main/java/net/minestom/server/instance/light/InstanceLightManager.java new file mode 100644 index 00000000000..2e4fd35d87b --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/InstanceLightManager.java @@ -0,0 +1,175 @@ +package net.minestom.server.instance.light; + +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.light.starlight.BlockStarLightEngine; +import net.minestom.server.instance.light.starlight.LightWorldUtil; +import net.minestom.server.instance.light.starlight.SkyStarLightEngine; +import net.minestom.server.utils.cache.LocalCache; +import net.minestom.server.utils.chunk.ChunkUtils; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; + +@ApiStatus.Internal +public class InstanceLightManager { + + private final Instance instance; + private final LocalCache cachedSkyPropagators; + private final LocalCache cachedBlockPropagators; + + public InstanceLightManager( + final @NotNull Instance instance, + final boolean hasSkyLight, + final boolean hasBlockLight + ) { + this.instance = instance; + this.cachedSkyPropagators = hasSkyLight ? LocalCache.of(() -> new SkyStarLightEngine(instance)) : null; + this.cachedBlockPropagators = hasBlockLight ? LocalCache.of(() -> new BlockStarLightEngine(instance)) : null; + } + + public @NotNull Instance getInstance() { + return instance; + } + + public boolean hasSkyLight() { + return cachedSkyPropagators != null; + } + + public boolean hasBlockLight() { + return cachedBlockPropagators != null; + } + + public @NotNull CompletableFuture onBlockChange(final @NotNull Chunk chunk, final int x, final int y, final int z) { + final ChunkLightData lightData = chunk.getLightData(); + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (lightData) { + if (lightData.tasks == null) { + lightData.tasks = new ChunkTasks(); + } + lightData.tasks.changesPositions.add(ChunkUtils.getBlockIndex(x, y, z)); + return lightData.tasks.onComplete; + } + } + + public @NotNull CompletableFuture onSectionChange(final @NotNull Chunk chunk, final int sectionIndex, final boolean newEmptyValue) { + final ChunkLightData lightData = chunk.getLightData(); + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (lightData) { + if (lightData.tasks == null) { + lightData.tasks = new ChunkTasks(); + } + if (lightData.tasks.changedSectionSet == null) { + lightData.tasks.changedSectionSet = new Boolean[LightWorldUtil.getTotalSections(instance)]; + } + lightData.tasks.changedSectionSet[sectionIndex - LightWorldUtil.getMinSection(instance)] = newEmptyValue; + return lightData.tasks.onComplete; + } + } + + public void scheduleChunkLight(final @NotNull ChunkLightData lightData, final Runnable runnable) { + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (lightData) { + if (lightData.tasks == null) { + lightData.tasks = new ChunkTasks(); + } + if (lightData.tasks.lightTasks == null) { + lightData.tasks.lightTasks = new ArrayList<>(); + } + lightData.tasks.lightTasks.add(runnable); + } + } + + public void lightChunk(final @NotNull Chunk chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = cachedSkyPropagators.get(); + final BlockStarLightEngine blockEngine = cachedBlockPropagators.get(); + if (skyEngine != null) { + skyEngine.light(instance, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.light(instance, chunk, emptySections); + } + } + + public void forceLoadInChunk(final @NotNull Chunk chunk, final Boolean[] emptySections) { + final SkyStarLightEngine skyEngine = cachedSkyPropagators.get(); + final BlockStarLightEngine blockEngine = cachedBlockPropagators.get(); + if (skyEngine != null) { + skyEngine.forceHandleEmptySectionChanges(instance, chunk, emptySections); + } + if (blockEngine != null) { + blockEngine.forceHandleEmptySectionChanges(instance, chunk, emptySections); + } + } + + public void checkChunkEdges(final @NotNull Chunk chunk) { + final SkyStarLightEngine skyEngine = cachedSkyPropagators.get(); + final BlockStarLightEngine blockEngine = cachedBlockPropagators.get(); + if (skyEngine != null) { + skyEngine.checkChunkEdges(instance, chunk.getChunkX(), chunk.getChunkZ()); + } + if (blockEngine != null) { + blockEngine.checkChunkEdges(instance, chunk.getChunkX(), chunk.getChunkZ()); + } + } + + public void removeChunkTasks(final @NotNull Chunk chunk) { + final ChunkLightData lightData = chunk.getLightData(); + final ChunkTasks tasks; + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (lightData) { + if (lightData.tasks == null) { + return; + } + tasks = lightData.tasks; + lightData.tasks = null; + } + tasks.onComplete.complete(null); + } + + public void runPendingTasks(@NotNull Chunk chunk) { + final ChunkLightData lightData = chunk.getLightData(); + final ChunkTasks tasks; + + // TODO: sync here is probably slow. Consider switching lightdata#tasks to atomic ref with internal sync? + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (lightData) { + if (lightData.tasks == null) { + return; + } + tasks = lightData.tasks; + lightData.tasks = null; + } + + final SkyStarLightEngine skyEngine = cachedSkyPropagators.get(); + final BlockStarLightEngine blockEngine = cachedBlockPropagators.get(); + if (tasks.lightTasks != null) { + for (final Runnable run : tasks.lightTasks) { + run.run(); + } + } + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + final IntSet blocks = tasks.changesPositions; + final Boolean[] sections = tasks.changedSectionSet; + if (!blocks.isEmpty() || sections != null) { + if (skyEngine != null) { + skyEngine.blocksChangedInChunk(instance, chunkX, chunkZ, blocks, sections); + } + if (blockEngine != null) { + blockEngine.blocksChangedInChunk(instance, chunkX, chunkZ, blocks, sections); + } + } + if (skyEngine != null && tasks.queuedEdgeChecksSky != null) { + skyEngine.checkChunkEdges(instance, chunkX, chunkZ, tasks.queuedEdgeChecksSky); + } + if (blockEngine != null && tasks.queuedEdgeChecksBlock != null) { + blockEngine.checkChunkEdges(instance, chunkX, chunkZ, tasks.queuedEdgeChecksBlock); + } + + tasks.onComplete.complete(null); + } +} diff --git a/src/main/java/net/minestom/server/instance/light/starlight/BlockStarLightEngine.java b/src/main/java/net/minestom/server/instance/light/starlight/BlockStarLightEngine.java new file mode 100644 index 00000000000..8cd2ef83908 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/starlight/BlockStarLightEngine.java @@ -0,0 +1,280 @@ +package net.minestom.server.instance.light.starlight; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntIterator; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.ChunkStatus; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.Section; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.palette.Palette; +import net.minestom.server.utils.chunk.ChunkUtils; + +import java.util.*; + +public final class BlockStarLightEngine extends StarLightEngine { + + public BlockStarLightEngine(final Instance world) { + super(false, world); + } + + @Override + protected boolean[] getEmptinessMap(final Chunk chunk) { + return chunk.getLightData().getBlockEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final Chunk chunk, final boolean[] to) { + chunk.getLightData().setBlockEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final Chunk chunk) { + return chunk.getLightData().getBlockNibbles(); + } + + @Override + protected void setNibbles(final Chunk chunk, final SWMRNibbleArray[] to) { + chunk.getLightData().setBlockNibbles(to); + } + + @Override + protected boolean canUseChunk(final Chunk chunk) { + return chunk.getStatus().isOrAfter(ChunkStatus.LIGHTING) && chunk.getLightData().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 Instance 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 Block blockState = this.getBlockState(worldX, worldY, worldZ); + final int emittedLevel = blockState.registry().lightEmission() & 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 + ((long) worldZ << 6) + ((long) worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLevel & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.registry().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 + ((long) worldZ << 6) + ((long) 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 Instance lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + final Block centerState = this.getBlockState(worldX, worldY, worldZ); + int level = centerState.registry().lightEmission() & 0xF; + + if (level >= (15 - 1) || level > expect) { + return level; + } + + final int sectionOffset = this.chunkSectionIndexOffset; +// final Block conditionallyOpaqueState; + int opacity = centerState.registry().opacity(); + + if (opacity == -1) { +// this.recalcCenterPos.set(worldX, worldY, worldZ); +// opacity = centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos); +// if (((ExtendedAbstractBlockState)centerState).isConditionallyFullOpaque()) { +// conditionallyOpaqueState = centerState; +// } else { +// conditionallyOpaqueState = null; +// } + return level; + } 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 Block neighbourState = this.getBlockState(offX, offY, offZ); + if (neighbourState.registry().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; +// } + 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 Instance lightAccess, final Chunk atChunk, final IntSet positions) { + final int startX = atChunk.getChunkX() << 4; + final int startZ = atChunk.getChunkZ() << 4; + for (final int pos : positions) { + this.checkBlock( + lightAccess, + startX + ChunkUtils.blockIndexToChunkPositionX(pos), + ChunkUtils.blockIndexToChunkPositionY(pos), + startZ + ChunkUtils.blockIndexToChunkPositionZ(pos) + ); + } + + this.performLightDecrease(lightAccess); + } + + private IntIterator getSources(final Chunk chunk) { + final IntList sources = new IntArrayList(); + + final List
sections = chunk.getSections(); + for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { + final Section section = sections.get(sectionY - this.minSection); + if (section == null || section.hasOnlyAir()) { + // no sources in empty sections + continue; + } + final Palette palette = section.blockPalette(); + final int offY = sectionY << 4; + + for (int x = 0; x < Chunk.CHUNK_SIZE_X; ++x) { + for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; ++y) { + for (int z = 0; z < Chunk.CHUNK_SIZE_Z; ++z) { + final short blockStateId = (short) palette.get(x, y, z); + final Block state = Objects.requireNonNullElse(Block.fromStateId(blockStateId), StarLightEngine.AIR_BLOCK_STATE); + if (state.registry().lightEmission() <= 0) { + continue; + } + sources.add(ChunkUtils.getBlockIndex(x, offY | y, z)); + } + } + } + } + + return sources.iterator(); + } + + @Override + public void lightChunk(final Instance lightAccess, final Chunk chunk, final boolean needsEdgeChecks) { + // setup sources + final int emittedMask = this.emittedLightMask; + for (final IntIterator positions = this.getSources(chunk); positions.hasNext();) { + final int pos = positions.nextInt(); + final int x = (chunk.getChunkX() << 4) | ChunkUtils.blockIndexToChunkPositionX(pos); + final int y = ChunkUtils.blockIndexToChunkPositionY(pos); + final int z = (chunk.getChunkZ() << 4) | ChunkUtils.blockIndexToChunkPositionZ(pos); + final Block blockState = this.getBlockState(x, y, z); + final int emittedLight = blockState.registry().lightEmission() & emittedMask; + + if (emittedLight <= this.getLightLevel(x, y, z)) { + // some other source is brighter + continue; + } + + this.appendToIncreaseQueue( + ((x + ((long) z << 6) + ((long) y << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) + | (emittedLight & 0xFL) << (6 + 6 + 16) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.registry().isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) + ); + + + // propagation wont set this for us + this.setLightLevel(x, y, z, 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/net/minestom/server/instance/light/starlight/IntegerUtil.java b/src/main/java/net/minestom/server/instance/light/starlight/IntegerUtil.java new file mode 100644 index 00000000000..90f2d2a205a --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/starlight/IntegerUtil.java @@ -0,0 +1,21 @@ +package net.minestom.server.instance.light.starlight; + +class IntegerUtil { + + public static int getTrailingBit(final int n) { + return -n & n; + } + + public static int trailingZeros(final int n) { + return Integer.numberOfTrailingZeros(n); + } + + 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 + } + + private IntegerUtil() {} + +} diff --git a/src/main/java/net/minestom/server/instance/light/starlight/LightWorldUtil.java b/src/main/java/net/minestom/server/instance/light/starlight/LightWorldUtil.java new file mode 100644 index 00000000000..22b5785ff02 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/starlight/LightWorldUtil.java @@ -0,0 +1,34 @@ +package net.minestom.server.instance.light.starlight; + +import net.minestom.server.instance.Instance; +import net.minestom.server.utils.chunk.ChunkUtils; + +public class LightWorldUtil { + + public static int getMinSection(Instance world) { + return ChunkUtils.getChunkCoordinate(world.getDimensionType().getMinY()); + } + + public static int getMaxSection(Instance world) { + return ChunkUtils.getChunkCoordinate(world.getDimensionType().getMaxY()); + } + + public static int getMinLightSection(Instance world) { + return getMinSection(world) - 1; + } + + public static int getMaxLightSection(Instance world) { + return getMaxSection(world) + 1; + } + + public static int getTotalSections(Instance world) { + return getMaxSection(world) - getMinSection(world) + 1; + } + + public static int getTotalLightSections(Instance world) { + return getMaxLightSection(world) - getMinLightSection(world) + 1; + } + + private LightWorldUtil() {} + +} diff --git a/src/main/java/net/minestom/server/instance/light/starlight/SWMRNibbleArray.java b/src/main/java/net/minestom/server/instance/light/starlight/SWMRNibbleArray.java new file mode 100644 index 00000000000..b4b0c6fa173 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/starlight/SWMRNibbleArray.java @@ -0,0 +1,436 @@ +package net.minestom.server.instance.light.starlight; + +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(byte[] data) { + if (data == null) { + return new SWMRNibbleArray(null, true); + } else if (data.length == 0) { + return new SWMRNibbleArray(); + } else { + return new SWMRNibbleArray(data.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.storageUpdating != null) { + into = this.storageUpdating; + } else { + this.storageUpdating = into = allocateBytes(); + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + + final int start = 0; + final int end = (15 | (15 << 4)) >>> 1; + + /* x | (z << 4) | (y << 8) */ + for (int y = 0; y <= 15; ++y) { + System.arraycopy(src, start, into, y << (8 - 1), end - start + 1); + } + } + + // operation type: updating + public void setFull() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1); + this.updatingDirty = true; + } + + // operation type: updating + public void setZero() { + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0); + this.updatingDirty = true; + } + + // operation type: updating + public void setNonNull() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + return; + } + if (this.stateUpdating != INIT_STATE_NULL) { + return; + } + this.stateUpdating = INIT_STATE_UNINIT; + } + + // operation type: updating + public void setNull() { + this.stateUpdating = INIT_STATE_NULL; + if (this.updatingDirty && this.storageUpdating != null) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setUninitialised() { + this.stateUpdating = INIT_STATE_UNINIT; + if (this.storageUpdating != null && this.updatingDirty) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = null; + this.updatingDirty = false; + } + + // operation type: updating + public void setHidden() { + if (this.stateUpdating == INIT_STATE_HIDDEN) { + return; + } + if (this.stateUpdating != INIT_STATE_INIT) { + this.setNull(); + } else { + this.stateUpdating = INIT_STATE_HIDDEN; + } + } + + // operation type: updating + public boolean isDirty() { + return this.stateUpdating != this.stateVisible || this.updatingDirty; + } + + // operation type: updating + public boolean isNullNibbleUpdating() { + return this.stateUpdating == INIT_STATE_NULL; + } + + // operation type: visible + public boolean isNullNibbleVisible() { + return this.stateVisible == INIT_STATE_NULL; + } + + // opeartion type: updating + public boolean isUninitialisedUpdating() { + return this.stateUpdating == INIT_STATE_UNINIT; + } + + // operation type: visible + public boolean isUninitialisedVisible() { + return this.stateVisible == INIT_STATE_UNINIT; + } + + // operation type: updating + public boolean isInitialisedUpdating() { + return this.stateUpdating == INIT_STATE_INIT; + } + + // operation type: visible + public boolean isInitialisedVisible() { + return this.stateVisible == INIT_STATE_INIT; + } + + // operation type: updating + public boolean isHiddenUpdating() { + return this.stateUpdating == INIT_STATE_HIDDEN; + } + + // operation type: updating + public boolean isHiddenVisible() { + return this.stateVisible == INIT_STATE_HIDDEN; + } + + // operation type: updating + protected void swapUpdatingAndMarkDirty() { + if (this.updatingDirty) { + return; + } + + if (this.storageUpdating == null) { + this.storageUpdating = allocateBytes(); + Arrays.fill(this.storageUpdating, (byte)0); + } else { + System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE); + } + + if (this.stateUpdating != INIT_STATE_HIDDEN) { + this.stateUpdating = INIT_STATE_INIT; + } + this.updatingDirty = true; + } + + // operation type: updating + public boolean updateVisible() { + if (!this.isDirty()) { + return false; + } + + synchronized (this) { + if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) { + this.storageVisible = null; + } else { + if (this.storageVisible == null) { + this.storageVisible = this.storageUpdating.clone(); + } else { + if (this.storageUpdating != this.storageVisible) { + System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); + } + } + + if (this.storageUpdating != this.storageVisible) { + freeBytes(this.storageUpdating); + } + this.storageUpdating = this.storageVisible; + } + this.updatingDirty = false; + this.stateVisible = this.stateUpdating; + } + + return true; + } + + // operation type: visible + public byte[] toVanillaNibble() { + synchronized (this) { + switch (this.stateVisible) { + case INIT_STATE_HIDDEN: + case INIT_STATE_NULL: + return null; + case INIT_STATE_UNINIT: + return new byte[0]; + case INIT_STATE_INIT: +// return this.storageVisible.clone(); + return this.storageVisible; + 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/net/minestom/server/instance/light/starlight/SkyStarLightEngine.java b/src/main/java/net/minestom/server/instance/light/starlight/SkyStarLightEngine.java new file mode 100644 index 00000000000..20780ec2ee7 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/starlight/SkyStarLightEngine.java @@ -0,0 +1,717 @@ +package net.minestom.server.instance.light.starlight; + +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.ChunkStatus; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.Section; +import net.minestom.server.instance.block.Block; +import net.minestom.server.utils.chunk.ChunkUtils; + +import java.util.Arrays; +import java.util.List; + +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 Instance world) { + super(true, world); + this.nullPropagationCheckCache = new boolean[LightWorldUtil.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 Section 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 void rewriteNibbleCacheForSkylight(final Chunk 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 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); + } + } + } + + return needInitNeighbours; + } + + protected 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 Chunk chunk) { + return chunk.getLightData().getSkyEmptinessMap(); + } + + @Override + protected void setEmptinessMap(final Chunk chunk, final boolean[] to) { + chunk.getLightData().setSkyEmptinessMap(to); + } + + @Override + protected SWMRNibbleArray[] getNibblesOnChunk(final Chunk chunk) { + return chunk.getLightData().getSkyNibbles(); + } + + @Override + protected void setNibbles(final Chunk chunk, final SWMRNibbleArray[] to) { + chunk.getLightData().setSkyNibbles(to); + } + + @Override + protected boolean canUseChunk(final Chunk chunk) { + // can only use chunks for sky stuff if their sections have been init'd + return chunk.getStatus().isOrAfter(ChunkStatus.LIGHTING) && chunk.getLightData().isLightCorrect(); + } + + @Override + protected void checkChunkEdges(final Instance lightAccess, final Chunk chunk, final int fromSection, + final int toSection) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + for (int y = toSection; y >= fromSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, fromSection, toSection); + } + + @Override + protected void checkChunkEdges(final Instance lightAccess, final Chunk chunk, final ShortCollection sections) { + Arrays.fill(this.nullPropagationCheckCache, false); + this.rewriteNibbleCacheForSkylight(chunk); + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + for (final ShortIterator iterator = sections.iterator(); iterator.hasNext();) { + final int y = iterator.nextShort(); + this.checkNullSection(chunkX, y, chunkZ, true); + } + + super.checkChunkEdges(lightAccess, chunk, sections); + } + + @Override + protected void checkBlock(final Instance 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 + ((long) worldZ << 6) + ((long) 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 + ((long) worldZ << 6) + ((long) 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 Instance lightAccess, final int worldX, final int worldY, final int worldZ, + final int expect) { + if (expect == 15) { + return expect; + } + + final int sectionOffset = this.chunkSectionIndexOffset; + final Block centerState = this.getBlockState(worldX, worldY, worldZ); + int opacity = centerState.registry().opacity(); + + final Block conditionallyOpaqueState; + if (opacity < 0) { +// this.recalcCenterPos.set(worldX, worldY, worldZ); +// opacity = Math.max(1, centerState.getLightBlock(lightAccess.getLevel(), this.recalcCenterPos)); +// if (((ExtendedAbstractBlockState)centerState).isConditionallyFullOpaque()) { +// conditionallyOpaqueState = centerState; +// } else { +// conditionallyOpaqueState = null; +// } + conditionallyOpaqueState = null; + opacity = 15; + } 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 Block neighbourState = this.getBlockState(offX, offY, offZ); + + if (neighbourState.registry().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; +// } + continue; + } + + final int calculated = neighbourLevel - opacity; + level = Math.max(calculated, level); + if (level > expect) { + return level; + } + } + + return level; + } + + @Override + protected void propagateBlockChanges(final Instance world, final Chunk atChunk, final IntSet positions) { + this.rewriteNibbleCacheForSkylight(atChunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final int chunkX = atChunk.getChunkX(); + final int chunkZ = atChunk.getChunkZ(); + + // setup heightmap for changes + for (final int pos : positions) { + final int x = ChunkUtils.blockIndexToChunkPositionX(pos); + final int y = ChunkUtils.blockIndexToChunkPositionY(pos); + final int z = ChunkUtils.blockIndexToChunkPositionZ(pos); + final int index = x + (z << 4); + final int curr = this.heightMapBlockChange[index]; + if (y > curr) { + this.heightMapBlockChange[index] = y; + } + } + + // 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 + ((long) columnZ << 6) + ((long) 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(); + + final int startX = atChunk.getChunkX() << 4; + final int startZ = atChunk.getChunkZ() << 4; + for (final int pos : positions) { + this.checkBlock( + world, + startX + ChunkUtils.blockIndexToChunkPositionX(pos), + ChunkUtils.blockIndexToChunkPositionY(pos), + startZ + ChunkUtils.blockIndexToChunkPositionZ(pos) + ); + } + + this.performLightDecrease(world); + } + + protected final int[] heightMapGen = new int[32 * 32]; + + @Override + protected void lightChunk(final Instance world, final Chunk chunk, final boolean needsEdgeChecks) { + this.rewriteNibbleCacheForSkylight(chunk); + Arrays.fill(this.nullPropagationCheckCache, false); + + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + + final List
sections = chunk.getSections(); + + int highestNonEmptySection = this.maxSection; + while (highestNonEmptySection == (this.minSection - 1) || + sections.get(highestNonEmptySection - this.minSection) == null || sections.get(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 + ((long) currZ << 6) + ((long) 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 = chunkX << 4; + final int maxX = chunkX << 4 | 15; + final int minZ = chunkZ << 4; + final int maxZ = chunkZ << 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(world); + + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + // no need to rewrite the nibble cache again + super.checkChunkEdges(world, chunk, this.minLightSection, highestNonEmptySection); + } else { + for (int y = highestNonEmptySection; y >= this.minLightSection; --y) { + this.checkNullSection(chunkX, y, chunkZ, false); + } + this.propagateNeighbourLevels(world, chunk, this.minLightSection, highestNonEmptySection); + + this.performLightIncrease(world); + } + } + + protected 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 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 int tryPropagateSkylight(final Instance 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); + + Block 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 Block current = this.getBlockState(worldX, startY, worldZ); + +// final VoxelShape fromShape; + //noinspection StatementWithEmptyBody + if (above.registry().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; +// } + break; + } else { +// fromShape = Shapes.empty(); + } + + final int opacityIfCached = current.registry().opacity(); + // 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 + ((long) worldZ << 6) + ((long) 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 (((ExtendedAbstractBlockState)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 +// ); + break; + } + + 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/net/minestom/server/instance/light/starlight/StarLightEngine.java b/src/main/java/net/minestom/server/instance/light/starlight/StarLightEngine.java new file mode 100644 index 00000000000..6f9d932abc7 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/starlight/StarLightEngine.java @@ -0,0 +1,1494 @@ +package net.minestom.server.instance.light.starlight; + +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.shorts.ShortCollection; +import it.unimi.dsi.fastutil.shorts.ShortIterator; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.ChunkStatus; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.Section; +import net.minestom.server.instance.block.Block; +import net.minestom.server.utils.chunk.ChunkUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public abstract class StarLightEngine { + + protected static final Block AIR_BLOCK_STATE = Block.AIR; + + 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 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; + } + + public AxisDirection opposite; + + public final int x; + public final int y; + public final int z; + 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.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 Section[] 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; + + // always initialsed during start of lighting. + // index = x + (z * 5) + protected final Chunk[] chunkCache = new Chunk[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 Instance world; + protected final int minLightSection; + protected final int maxLightSection; + protected final int minSection; + protected final int maxSection; + + protected StarLightEngine(final boolean skylightPropagator, final Instance world) { + this.skylightPropagator = skylightPropagator; + this.emittedLightMask = skylightPropagator ? 0 : 0xF; + this.world = world; + this.minLightSection = LightWorldUtil.getMinLightSection(world); + this.maxLightSection = LightWorldUtil.getMaxLightSection(world); + this.minSection = LightWorldUtil.getMinSection(world); + this.maxSection = LightWorldUtil.getMaxSection(world); + + this.sectionCache = new Section[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 + } + + protected final void setupEncodeOffset(final int centerX, 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 Instance chunkProvider, final int centerX, final int centerZ, + final boolean relaxed, final boolean tryToLoadChunksFor2Radius) { + final int centerChunkX = centerX >> 4; + final int centerChunkZ = centerZ >> 4; + + this.setupEncodeOffset(centerChunkX * 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 Chunk chunk = chunkProvider.getChunk(cx, cz, ChunkStatus.LIGHTING); + + 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 Chunk 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 Chunk chunk) { + this.chunkCache[chunkX + 5*chunkZ + this.chunkIndexOffset] = chunk; + } + + protected final Section 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 Section section) { + this.sectionCache[chunkX + 5*chunkZ + 5*5*chunkY + this.chunkSectionIndexOffset] = section; + } + + protected final void setBlocksForChunkInCache(final int chunkX, final int chunkZ, final List
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.get(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 Instance lightAccess) { + for (int index = 0, max = this.nibbleCache.length; index < max; ++index) { + final SWMRNibbleArray nibble = this.nibbleCache[index]; + if (nibble == null || !nibble.isDirty() || !nibble.updateVisible()) { + 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; + lightAccess.onLightUpdate(chunkX, chunkZ, chunkY, this.skylightPropagator); + } + } + + protected final void destroyCaches() { + Arrays.fill(this.sectionCache, null); + Arrays.fill(this.nibbleCache, null); + Arrays.fill(this.chunkCache, null); + Arrays.fill(this.emptinessMapCache, null); + } + + protected final Block getBlockState(final int worldX, final int worldY, final int worldZ) { + final Section section = this.sectionCache[(worldX >> 4) + 5 * (worldZ >> 4) + (5 * 5) * (worldY >> 4) + this.chunkSectionIndexOffset]; + + if (section != null && !section.hasOnlyAir()) { + final short blockStateId = (short) section.blockPalette().get(worldX & 15, worldY & 15, worldZ & 15); + return Objects.requireNonNullElse(Block.fromStateId(blockStateId), AIR_BLOCK_STATE); + } + + return AIR_BLOCK_STATE; + } + + protected final Block getBlockState(final int sectionIndex, final int localIndex) { + final Section section = this.sectionCache[sectionIndex]; + if (section != null && !section.hasOnlyAir()) { + final int x = localIndex & 15; + final int z = (localIndex >> 4) & 15; + final int y = (localIndex >> 8) & 15; + final short blockStateId = (short) section.blockPalette().get(x, y, z); + return Objects.requireNonNullElse(Block.fromStateId(blockStateId), AIR_BLOCK_STATE); + } + + 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); + } + } + + protected final void postLightUpdate(final int worldX, final int worldY, final int worldZ) { + + } + + 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); + } + } + + 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 Instance world) { + return getFilledEmptyLight(LightWorldUtil.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 Chunk chunk); + + protected abstract void setEmptinessMap(final Chunk chunk, final boolean[] to); + + protected abstract SWMRNibbleArray[] getNibblesOnChunk(final Chunk chunk); + + protected abstract void setNibbles(final Chunk chunk, final SWMRNibbleArray[] to); + + protected abstract boolean canUseChunk(final Chunk chunk); + + public final void blocksChangedInChunk(final Instance lightAccess, final int chunkX, final int chunkZ, + final IntSet positions, final Boolean[] changedSections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, chunkZ * 16 + 7, true, true); + try { + final Chunk 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 Instance lightAccess, final Chunk atChunk, final IntSet positions); + + protected abstract void checkBlock(final Instance 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 Instance 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 Instance lightAccess, final Chunk 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 Instance lightAccess, final Chunk chunk, final ShortCollection sections) { + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + + 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 Instance lightAccess, final Chunk chunk, final int fromSection, final int toSection) { + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + + 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 Instance lightAccess, final Chunk chunk, final int fromSection, final int toSection) { + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + + 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 Chunk chunk) { + final List
sections = chunk.getSections(); + final Boolean[] ret = new Boolean[sections.size()]; + + for (int i = 0; i < sections.size(); ++i) { + final Section section = sections.get(i); + if (section == null || section.hasOnlyAir()) { + ret[i] = Boolean.TRUE; + } else { + ret[i] = Boolean.FALSE; + } + } + + return ret; + } + + public final void forceHandleEmptySectionChanges(final Instance lightAccess, final Chunk chunk, final Boolean[] emptinessChanges) { + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + this.setupCaches(lightAccess, chunkX * 16 + 7, 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 Instance lightAccess, final int chunkX, final int chunkZ, + final Boolean[] emptinessChanges) { + this.setupCaches(lightAccess, chunkX * 16 + 7, chunkZ * 16 + 7, true, true); + try { + final Chunk 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 Instance lightAccess, final Chunk chunk, + final Boolean[] emptinessChanges, final boolean unlit) { + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + + 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[LightWorldUtil.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 Section section = this.getChunkSection(chunkX, sectionIndex + this.minSection, chunkZ); + emptinessChanges[sectionIndex] = valueBoxed = section == null || section.hasOnlyAir() ? Boolean.TRUE : Boolean.FALSE; + } + chunkEmptinessMap[sectionIndex] = valueBoxed; + } + + // 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; + + 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) { + final int y = sectionY + dy2; + if (y < this.minSection || y > this.maxSection) { + // empty + continue; + } + for (int dz2 = -1; dz2 <= 1; ++dz2) { + for (int dx2 = -1; dx2 <= 1; ++dx2) { + 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 Section 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 Instance lightAccess, final int chunkX, final int chunkZ) { + this.setupCaches(lightAccess, chunkX * 16 + 7, chunkZ * 16 + 7, true, false); + try { + final Chunk 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 Instance lightAccess, final int chunkX, final int chunkZ, final ShortCollection sections) { + this.setupCaches(lightAccess, chunkX * 16 + 7, chunkZ * 16 + 7, true, false); + try { + final Chunk 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 Instance lightAccess, final Chunk chunk, final boolean needsEdgeChecks); + + public final void light(final Instance lightAccess, final Chunk chunk, final Boolean[] emptySections) { + final int chunkX = chunk.getChunkX(); + final int chunkZ = chunk.getChunkZ(); + this.setupCaches(lightAccess, chunkX * 16 + 7, 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 Instance lightAccess, final LongSet 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 long chunkPos : chunks) { + final int chunkX = ChunkUtils.getChunkCoordX(chunkPos); + final int chunkZ = ChunkUtils.getChunkCoordZ(chunkPos); + final Chunk chunk = lightAccess.getChunk(chunkX, chunkZ, ChunkStatus.LIGHTING); + 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 Chunk neighbour = lightAccess.getChunk(neighbourX, neighbourZ, ChunkStatus.LIGHTING); + if (neighbour == null || !this.canUseChunk(neighbour)) { + continue; + } + + if (nibblesByChunk.get(ChunkUtils.getChunkIndex(neighbourX, neighbourZ)) != null) { + // lit already called for neighbour, no need to light it now + continue; + } + + // light neighbour chunk + this.setupEncodeOffset(neighbourX * 16 + 7, 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 = ChunkUtils.getChunkIndex(neighbourX2, neighbourZ2); + final Chunk neighbour2 = lightAccess.getChunk(neighbourX2, neighbourZ2, ChunkStatus.LIGHTING); + 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 = ChunkUtils.getChunkIndex(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(ChunkUtils.getChunkIndex(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(ChunkUtils.getChunkIndex(chunkX, chunkZ)); + for (final SWMRNibbleArray nibble : nibbles) { + nibble.updateVisible(); + } + + this.setNibbles(chunk, nibbles); + + for (int y = this.minLightSection; y <= this.maxLightSection; ++y) { + lightAccess.onLightUpdate(chunkX, chunkZ, y, this.skylightPropagator); + } + + // 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 Instance world) { + 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 Block blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.registry().opacity(); + //noinspection StatementWithEmptyBody + 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 + ((long) offZ << 6) + ((long) offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + } + } + } else { +// this.mutablePos1.set(offX, offY, offZ); +// long flags = 0; +// if (((ExtendedAbstractBlockState)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); +// } + } + } + } else { + // we actually need to worry about our state here +// final Block 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 = (((ExtendedAbstractBlockState)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 Block blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.registry().opacity(); + //noinspection StatementWithEmptyBody + 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 + ((long) offZ << 6) + ((long) offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | (propagate.everythingButTheOppositeDirection << (6 + 6 + 16 + 4)); + } + } + } else { +// this.mutablePos1.set(offX, offY, offZ); +// long flags = 0; +// if (((ExtendedAbstractBlockState)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); +// } + } + } + } + } + } + + protected final void performLightDecrease(final Instance world) { + 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 Block blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.registry().opacity(); + //noinspection StatementWithEmptyBody + 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 + ((long) offZ << 6) + ((long) 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.registry().lightEmission() & 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 + ((long) offZ << 6) + ((long) offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.registry().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 + ((long) offZ << 6) + ((long) offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + } + } else { +// this.mutablePos1.set(offX, offY, offZ); +// long flags = 0; +// if (((ExtendedAbstractBlockState)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; +// } + } + } + } else { + // we actually need to worry about our state here +// final Block 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 = (((ExtendedAbstractBlockState)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 Block blockState = this.getBlockState(sectionIndex, localIndex); + if (blockState == null) { + continue; + } + final int opacityCached = blockState.registry().opacity(); + //noinspection StatementWithEmptyBody + 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 + ((long) offZ << 6) + ((long) 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.registry().lightEmission() & 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 + ((long) offZ << 6) + ((long) offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((emittedLight & 0xFL) << (6 + 6 + 16)) + | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) + | (blockState.registry().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 + ((long) offZ << 6) + ((long) offY << 12) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) + | ((targetLevel & 0xFL) << (6 + 6 + 16)) + | ((propagate.everythingButTheOppositeDirection) << (6 + 6 + 16 + 4)); + } + } else { +// this.mutablePos1.set(offX, offY, offZ); +// long flags = 0; +// if (((ExtendedAbstractBlockState)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; +// } + } + } + } + } + + // propagate sources we clobbered + this.increaseQueueInitialLength = increaseQueueLength; + this.performLightIncrease(world); + } +} diff --git a/src/main/java/net/minestom/server/registry/Registry.java b/src/main/java/net/minestom/server/registry/Registry.java index a07d9161dc6..6138a421d77 100644 --- a/src/main/java/net/minestom/server/registry/Registry.java +++ b/src/main/java/net/minestom/server/registry/Registry.java @@ -161,6 +161,9 @@ public static final class BlockEntry implements Entry { private final double friction; private final double speedFactor; private final double jumpFactor; + private final boolean conditionallyFullOpaque; + private final int lightBlock; + private final int lightEmission; private final boolean air; private final boolean solid; private final boolean liquid; @@ -181,6 +184,9 @@ private BlockEntry(String namespace, Properties main, Properties custom) { this.friction = main.getDouble("friction"); this.speedFactor = main.getDouble("speedFactor", 1); this.jumpFactor = main.getDouble("jumpFactor", 1); + this.conditionallyFullOpaque = main.getBoolean("conditionallyFullOpaque", false); + this.lightBlock = main.getInt("lightBlock", 15); + this.lightEmission = main.getInt("lightEmission", 0); this.air = main.getBoolean("air", false); this.solid = main.getBoolean("solid"); this.liquid = main.getBoolean("liquid", false); @@ -240,6 +246,25 @@ public double jumpFactor() { return jumpFactor; } + public boolean isConditionallyFullOpaque() { + return conditionallyFullOpaque; + } + + public int lightBlock() { + return lightBlock; + } + + public int lightEmission() { + return lightEmission; + } + + public int opacity() { + if (conditionallyFullOpaque) { + return -1; + } + return lightBlock; + } + public boolean isAir() { return air; }