From 2a7df1ab550e536c143687ec43c1c4296211a7d9 Mon Sep 17 00:00:00 2001 From: Alexander <48819332+reosfire@users.noreply.github.com> Date: Tue, 30 Apr 2024 06:52:21 +0300 Subject: [PATCH] Lighting fix 35 (#2044) * optimize light compute * fix broken sections * encodeHeightmap without magic * separate heightmaps without empty sections optimization * empty sections skip optimization * working but not the best architecture * AbstractHeightmap * Anvil loading heightmaps. * refactor * Refactor * some refactoring after refactoring * test + cleanup * refactor * refactor * refactor * remove HeightMapContainer --------- Co-authored-by: iam4722202468 --- .../minestom/server/instance/AnvilLoader.java | 7 + .../net/minestom/server/instance/Chunk.java | 10 +- .../server/instance/DynamicChunk.java | 132 ++++++++-------- .../server/instance/LightingChunk.java | 59 +++----- .../server/instance/heightmap/Heightmap.java | 141 ++++++++++++++++++ .../heightmap/MotionBlockingHeightmap.java | 23 +++ .../heightmap/WorldSurfaceHeightmap.java | 21 +++ .../server/instance/light/LightCompute.java | 47 +++--- .../server/instance/light/SkyLight.java | 3 +- .../ChunkHeightmapIntegrationTest.java | 59 ++++++++ 10 files changed, 370 insertions(+), 132 deletions(-) create mode 100644 src/main/java/net/minestom/server/instance/heightmap/Heightmap.java create mode 100644 src/main/java/net/minestom/server/instance/heightmap/MotionBlockingHeightmap.java create mode 100644 src/main/java/net/minestom/server/instance/heightmap/WorldSurfaceHeightmap.java create mode 100644 src/test/java/net/minestom/server/instance/ChunkHeightmapIntegrationTest.java diff --git a/src/main/java/net/minestom/server/instance/AnvilLoader.java b/src/main/java/net/minestom/server/instance/AnvilLoader.java index 93048a71b..7aee1ba8b 100644 --- a/src/main/java/net/minestom/server/instance/AnvilLoader.java +++ b/src/main/java/net/minestom/server/instance/AnvilLoader.java @@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; import net.minestom.server.MinecraftServer; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.world.biomes.Biome; @@ -126,6 +127,8 @@ public class AnvilLoader implements IChunkLoader { // Block entities loadBlockEntities(chunk, chunkReader); + + chunk.loadHeightmapsFromNBT(chunkReader.getHeightmaps()); } synchronized (perRegionLoadedChunks) { int regionX = CoordinatesKt.chunkToRegion(chunkX); @@ -434,6 +437,10 @@ public class AnvilLoader implements IChunkLoader { chunkWriter.setSectionsData(NBT.List(NBTType.TAG_Compound, sectionData)); chunkWriter.setBlockEntityData(NBT.List(NBTType.TAG_Compound, blockEntities)); + + // Save heightmaps + chunkWriter.setMotionBlockingHeightMap(chunk.motionBlockingHeightmap().getNBT()); + chunkWriter.setWorldSurfaceHeightMap(chunk.worldSurfaceHeightmap().getNBT()); } /** diff --git a/src/main/java/net/minestom/server/instance/Chunk.java b/src/main/java/net/minestom/server/instance/Chunk.java index d85ee5751..dc9722e32 100644 --- a/src/main/java/net/minestom/server/instance/Chunk.java +++ b/src/main/java/net/minestom/server/instance/Chunk.java @@ -8,6 +8,7 @@ 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.block.BlockHandler; +import net.minestom.server.instance.heightmap.Heightmap; import net.minestom.server.network.packet.server.SendablePacket; import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.snapshot.Snapshotable; @@ -19,6 +20,7 @@ import net.minestom.server.world.biomes.Biome; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jglrxavpok.hephaistos.nbt.NBTCompound; import java.util.List; import java.util.Set; @@ -28,7 +30,7 @@ import java.util.UUID; /** * A chunk is a part of an {@link Instance}, limited by a size of 16x256x16 blocks and subdivided in 16 sections of 16 blocks height. - * Should contains all the blocks located at those positions and manage their tick updates. + * Should contain all the blocks located at those positions and manage their tick updates. * Be aware that implementations do not need to be thread-safe, all chunks are guarded by their own instance ('this'). *

* You can create your own implementation of this class by extending it @@ -101,6 +103,10 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter, public abstract @NotNull Section getSection(int section); + public abstract @NotNull Heightmap motionBlockingHeightmap(); + public abstract @NotNull Heightmap worldSurfaceHeightmap(); + public abstract void loadHeightmapsFromNBT(NBTCompound heightmaps); + public @NotNull Section getSectionAt(int blockY) { return getSection(ChunkUtils.getChunkCoordinate(blockY)); } @@ -122,7 +128,7 @@ public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter, *

* "Change" means here data used in {@link ChunkDataPacket}. * It is necessary to see if the cached version of this chunk can be used - * instead of re writing and compressing everything. + * instead of re-writing and compressing everything. * * @return the last change time in milliseconds */ diff --git a/src/main/java/net/minestom/server/instance/DynamicChunk.java b/src/main/java/net/minestom/server/instance/DynamicChunk.java index 87768d763..7dfe0c778 100644 --- a/src/main/java/net/minestom/server/instance/DynamicChunk.java +++ b/src/main/java/net/minestom/server/instance/DynamicChunk.java @@ -9,6 +9,7 @@ import net.minestom.server.entity.Entity; 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.heightmap.*; import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.packet.server.CachedPacket; import net.minestom.server.network.packet.server.SendablePacket; @@ -20,8 +21,6 @@ import net.minestom.server.snapshot.ChunkSnapshot; import net.minestom.server.snapshot.SnapshotImpl; import net.minestom.server.snapshot.SnapshotUpdater; import net.minestom.server.utils.ArrayUtils; -import net.minestom.server.utils.MathUtils; -import net.minestom.server.utils.ObjectPool; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.world.biomes.Biome; import net.minestom.server.world.biomes.BiomeManager; @@ -46,6 +45,11 @@ public class DynamicChunk extends Chunk { protected List

sections; + private boolean needsCompleteHeightmapRefresh = true; + + protected Heightmap motionBlocking = new MotionBlockingHeightmap(this); + protected Heightmap worldSurface = new WorldSurfaceHeightmap(this); + // Key = ChunkUtils#getBlockIndex protected final Int2ObjectOpenHashMap entries = new Int2ObjectOpenHashMap<>(0); protected final Int2ObjectOpenHashMap tickableMap = new Int2ObjectOpenHashMap<>(0); @@ -82,10 +86,14 @@ public class DynamicChunk extends Chunk { columnarOcclusionFieldList.onBlockChanged(x, y, z, blockDescription, 0); } Section section = getSectionAt(y); + + int sectionRelativeX = toSectionRelativeCoordinate(x); + int sectionRelativeZ = toSectionRelativeCoordinate(z); + section.blockPalette().set( - toSectionRelativeCoordinate(x), + sectionRelativeX, toSectionRelativeCoordinate(y), - toSectionRelativeCoordinate(z), + sectionRelativeZ, block.stateId() ); @@ -119,6 +127,10 @@ public class DynamicChunk extends Chunk { () -> new BlockHandler.Placement(finalBlock, instance, blockPosition))); } + // UpdateHeightMaps + if (needsCompleteHeightmapRefresh) calculateFullHeightmap(); + motionBlocking.refresh(sectionRelativeX, y, sectionRelativeZ, block); + worldSurface.refresh(sectionRelativeX, y, sectionRelativeZ, block); } @Override @@ -146,6 +158,27 @@ public class DynamicChunk extends Chunk { return sections.get(section - minSection); } + @Override + public @NotNull Heightmap motionBlockingHeightmap() { + return motionBlocking; + } + + @Override + public @NotNull Heightmap worldSurfaceHeightmap() { + return worldSurface; + } + + @Override + public void loadHeightmapsFromNBT(NBTCompound heightmapsNBT) { + if (heightmapsNBT.contains(motionBlockingHeightmap().NBTName())) { + motionBlockingHeightmap().loadFrom(heightmapsNBT.getLongArray(motionBlockingHeightmap().NBTName())); + } + + if (heightmapsNBT.contains(worldSurfaceHeightmap().NBTName())) { + worldSurfaceHeightmap().loadFrom(heightmapsNBT.getLongArray(worldSurfaceHeightmap().NBTName())); + } + } + @Override public void tick(long time) { if (tickableMap.isEmpty()) return; @@ -225,15 +258,14 @@ public class DynamicChunk extends Chunk { } private @NotNull ChunkDataPacket createChunkPacket() { - final NBTCompound heightmapsNBT = computeHeightmap(); - // Data - final byte[] data; + final NBTCompound heightmapsNBT; synchronized (this) { - data = ObjectPool.PACKET_POOL.use(buffer -> - NetworkBuffer.makeArray(networkBuffer -> { - for (Section section : sections) networkBuffer.write(section); - })); + heightmapsNBT = getHeightmapNBT(); + + data = NetworkBuffer.makeArray(networkBuffer -> { + for (Section section : sections) networkBuffer.write(section); + }); } return new ChunkDataPacket(chunkX, chunkZ, @@ -242,24 +274,6 @@ public class DynamicChunk extends Chunk { ); } - protected NBTCompound computeHeightmap() { - // TODO: don't hardcode heightmaps - // Heightmap - int dimensionHeight = getInstance().getDimensionType().getHeight(); - int[] motionBlocking = new int[16 * 16]; - int[] worldSurface = new int[16 * 16]; - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - motionBlocking[x + z * 16] = 0; - worldSurface[x + z * 16] = dimensionHeight - 1; - } - } - final int bitsForHeight = MathUtils.bitsToRepresent(dimensionHeight); - return NBT.Compound(Map.of( - "MOTION_BLOCKING", NBT.LongArray(encodeBlocks(motionBlocking, bitsForHeight)), - "WORLD_SURFACE", NBT.LongArray(encodeBlocks(worldSurface, bitsForHeight)))); - } - @NotNull UpdateLightPacket createLightPacket() { return new UpdateLightPacket(chunkX, chunkZ, createLightData()); } @@ -297,6 +311,23 @@ public class DynamicChunk extends Chunk { ); } + private NBTCompound getHeightmapNBT() { + if (needsCompleteHeightmapRefresh) calculateFullHeightmap(); + return NBT.Compound(Map.of( + motionBlocking.NBTName(), motionBlocking.getNBT(), + worldSurface.NBTName(), worldSurface.getNBT() + )); + } + + private void calculateFullHeightmap() { + int startY = Heightmap.getHighestBlockSection(this); + + motionBlocking.refresh(startY); + worldSurface.refresh(startY); + + needsCompleteHeightmapRefresh = false; + } + @Override public @NotNull ChunkSnapshot updateSnapshot(@NotNull SnapshotUpdater updater) { Section[] clonedSections = new Section[sections.size()]; @@ -312,47 +343,4 @@ public class DynamicChunk extends Chunk { private void assertLock() { assert Thread.holdsLock(this) : "Chunk must be locked before access"; } - - private static final int[] MAGIC = { - -1, -1, 0, Integer.MIN_VALUE, 0, 0, 1431655765, 1431655765, 0, Integer.MIN_VALUE, - 0, 1, 858993459, 858993459, 0, 715827882, 715827882, 0, 613566756, 613566756, - 0, Integer.MIN_VALUE, 0, 2, 477218588, 477218588, 0, 429496729, 429496729, 0, - 390451572, 390451572, 0, 357913941, 357913941, 0, 330382099, 330382099, 0, 306783378, - 306783378, 0, 286331153, 286331153, 0, Integer.MIN_VALUE, 0, 3, 252645135, 252645135, - 0, 238609294, 238609294, 0, 226050910, 226050910, 0, 214748364, 214748364, 0, - 204522252, 204522252, 0, 195225786, 195225786, 0, 186737708, 186737708, 0, 178956970, - 178956970, 0, 171798691, 171798691, 0, 165191049, 165191049, 0, 159072862, 159072862, - 0, 153391689, 153391689, 0, 148102320, 148102320, 0, 143165576, 143165576, 0, - 138547332, 138547332, 0, Integer.MIN_VALUE, 0, 4, 130150524, 130150524, 0, 126322567, - 126322567, 0, 122713351, 122713351, 0, 119304647, 119304647, 0, 116080197, 116080197, - 0, 113025455, 113025455, 0, 110127366, 110127366, 0, 107374182, 107374182, 0, - 104755299, 104755299, 0, 102261126, 102261126, 0, 99882960, 99882960, 0, 97612893, - 97612893, 0, 95443717, 95443717, 0, 93368854, 93368854, 0, 91382282, 91382282, - 0, 89478485, 89478485, 0, 87652393, 87652393, 0, 85899345, 85899345, 0, - 84215045, 84215045, 0, 82595524, 82595524, 0, 81037118, 81037118, 0, 79536431, - 79536431, 0, 78090314, 78090314, 0, 76695844, 76695844, 0, 75350303, 75350303, - 0, 74051160, 74051160, 0, 72796055, 72796055, 0, 71582788, 71582788, 0, - 70409299, 70409299, 0, 69273666, 69273666, 0, 68174084, 68174084, 0, Integer.MIN_VALUE, - 0, 5}; - - static long[] encodeBlocks(int[] blocks, int bitsPerEntry) { - final long maxEntryValue = (1L << bitsPerEntry) - 1; - final char valuesPerLong = (char) (64 / bitsPerEntry); - final int magicIndex = 3 * (valuesPerLong - 1); - final long divideMul = Integer.toUnsignedLong(MAGIC[magicIndex]); - final long divideAdd = Integer.toUnsignedLong(MAGIC[magicIndex + 1]); - final int divideShift = MAGIC[magicIndex + 2]; - final int size = (blocks.length + valuesPerLong - 1) / valuesPerLong; - - long[] data = new long[size]; - - for (int i = 0; i < blocks.length; i++) { - final long value = blocks[i]; - final int cellIndex = (int) (i * divideMul + divideAdd >> 32L >> divideShift); - final int bitIndex = (i - cellIndex * valuesPerLong) * bitsPerEntry; - data[cellIndex] = data[cellIndex] & ~(maxEntryValue << bitIndex) | (value & maxEntryValue) << bitIndex; - } - - return data; - } } diff --git a/src/main/java/net/minestom/server/instance/LightingChunk.java b/src/main/java/net/minestom/server/instance/LightingChunk.java index 4bbd33c83..8e4801a8c 100644 --- a/src/main/java/net/minestom/server/instance/LightingChunk.java +++ b/src/main/java/net/minestom/server/instance/LightingChunk.java @@ -8,16 +8,14 @@ import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.instance.heightmap.Heightmap; import net.minestom.server.instance.light.Light; import net.minestom.server.network.packet.server.CachedPacket; import net.minestom.server.network.packet.server.play.data.LightData; -import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.chunk.ChunkUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jglrxavpok.hephaistos.nbt.NBT; -import org.jglrxavpok.hephaistos.nbt.NBTCompound; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -39,7 +37,7 @@ public class LightingChunk extends DynamicChunk { private static final ExecutorService pool = Executors.newWorkStealingPool(); - private int[] heightmap; + private int[] occlusionMap; final CachedPacket lightCache = new CachedPacket(this::createLightPacket); private LightData lightData; @@ -133,7 +131,7 @@ public class LightingChunk extends DynamicChunk { @Nullable BlockHandler.Placement placement, @Nullable BlockHandler.Destroy destroy) { super.setBlock(x, y, z, block, placement, destroy); - this.heightmap = null; + this.occlusionMap = null; // Invalidate neighbor chunks, since they can be updated by this block change int coordinate = ChunkUtils.getChunkCoordinate(y); @@ -201,43 +199,33 @@ public class LightingChunk extends DynamicChunk { doneInit = true; } - @Override - protected NBTCompound computeHeightmap() { - // Heightmap - int[] heightmap = getHeightmap(); - int dimensionHeight = getInstance().getDimensionType().getHeight(); - final int bitsForHeight = MathUtils.bitsToRepresent(dimensionHeight); - return NBT.Compound(Map.of( - "MOTION_BLOCKING", NBT.LongArray(encodeBlocks(heightmap, bitsForHeight)), - "WORLD_SURFACE", NBT.LongArray(encodeBlocks(heightmap, bitsForHeight)))); - } - - // Lazy compute heightmap - public int[] getHeightmap() { - if (this.heightmap != null) return this.heightmap; - var heightmap = new int[CHUNK_SIZE_X * CHUNK_SIZE_Z]; + // Lazy compute occlusion map + public int[] getOcclusionMap() { + if (this.occlusionMap != null) return this.occlusionMap; + var occlusionMap = new int[CHUNK_SIZE_X * CHUNK_SIZE_Z]; int minY = instance.getDimensionType().getMinY(); - int maxY = instance.getDimensionType().getMinY() + instance.getDimensionType().getHeight(); - highestBlock = minY; + highestBlock = minY - 1; synchronized (this) { + int startY = Heightmap.getHighestBlockSection(this); + for (int x = 0; x < CHUNK_SIZE_X; x++) { for (int z = 0; z < CHUNK_SIZE_Z; z++) { - int height = maxY; - while (height > minY) { + int height = startY; + while (height >= minY) { Block block = getBlock(x, height, z, Condition.TYPE); if (block != Block.AIR) highestBlock = Math.max(highestBlock, height); if (checkSkyOcclusion(block)) break; height--; } - heightmap[z << 4 | x] = (height + 1); + occlusionMap[z << 4 | x] = (height + 1); } } } - this.heightmap = heightmap; - return heightmap; + this.occlusionMap = occlusionMap; + return occlusionMap; } @Override @@ -264,7 +252,7 @@ public class LightingChunk extends DynamicChunk { if (neighborChunk == null) continue; if (neighborChunk instanceof LightingChunk light) { - light.getHeightmap(); + light.getOcclusionMap(); highestNeighborBlock = Math.max(highestNeighborBlock, light.highestBlock); } } @@ -289,13 +277,12 @@ public class LightingChunk extends DynamicChunk { wasUpdatedSky = true; } + final int sectionMinY = index * 16 + chunkMin; index++; - final byte[] skyLight = section.skyLight().array(); - final byte[] blockLight = section.blockLight().array(); - final int sectionMaxY = index * 16 + chunkMin; + if ((wasUpdatedSky) && this.instance.getDimensionType().isSkylightEnabled() && sectionMinY <= (highestNeighborBlock + 16)) { + final byte[] skyLight = section.skyLight().array(); - if ((wasUpdatedSky) && this.instance.getDimensionType().isSkylightEnabled() && sectionMaxY <= (highestNeighborBlock + 16)) { if (skyLight.length != 0 && skyLight != emptyContent) { skyLights.add(skyLight); skyMask.set(index); @@ -305,6 +292,8 @@ public class LightingChunk extends DynamicChunk { } if (wasUpdatedBlock) { + final byte[] blockLight = section.blockLight().array(); + if (blockLight.length != 0 && blockLight != emptyContent) { blockLights.add(blockLight); blockMask.set(index); @@ -433,7 +422,7 @@ public class LightingChunk extends DynamicChunk { Set collected = new HashSet<>(); collected.add(point); - int highestRegionPoint = instance.getDimensionType().getMinY(); + int highestRegionPoint = instance.getDimensionType().getMinY() - 1; for (int x = point.blockX() - 1; x <= point.blockX() + 1; x++) { for (int z = point.blockZ() - 1; z <= point.blockZ() + 1; z++) { @@ -442,8 +431,8 @@ public class LightingChunk extends DynamicChunk { if (chunkCheck instanceof LightingChunk lighting) { // Ensure heightmap is calculated before taking values from it - lighting.getHeightmap(); - if (lighting.highestBlock > highestRegionPoint) highestRegionPoint = lighting.highestBlock; + lighting.getOcclusionMap(); + highestRegionPoint = Math.max(highestRegionPoint, lighting.highestBlock); } } } diff --git a/src/main/java/net/minestom/server/instance/heightmap/Heightmap.java b/src/main/java/net/minestom/server/instance/heightmap/Heightmap.java new file mode 100644 index 000000000..cc208765e --- /dev/null +++ b/src/main/java/net/minestom/server/instance/heightmap/Heightmap.java @@ -0,0 +1,141 @@ +package net.minestom.server.instance.heightmap; + +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.block.Block; +import net.minestom.server.utils.MathUtils; +import org.jetbrains.annotations.NotNull; +import org.jglrxavpok.hephaistos.collections.ImmutableLongArray; +import org.jglrxavpok.hephaistos.nbt.NBT; +import org.jglrxavpok.hephaistos.nbt.NBTLongArray; + +import static net.minestom.server.instance.Chunk.CHUNK_SIZE_X; +import static net.minestom.server.instance.Chunk.CHUNK_SIZE_Z; + +public abstract class Heightmap { + private final short[] heights = new short[CHUNK_SIZE_X * CHUNK_SIZE_Z]; + private final Chunk chunk; + private final int minHeight; + private boolean needsRefresh = true; + + public Heightmap(Chunk chunk) { + this.chunk = chunk; + minHeight = chunk.getInstance().getDimensionType().getMinY() - 1; + } + + protected abstract boolean checkBlock(@NotNull Block block); + public abstract String NBTName(); + + public void refresh(int x, int y, int z, Block block) { + if (checkBlock(block)) { + if (getHeight(x, z) < y) { + setHeightY(x, z, y); + } + } else if (y == getHeight(x, z)) { + refresh(x, z, y - 1); + } + } + + public void refresh(int startY) { + if (!needsRefresh) return; + + synchronized (chunk) { + for (int x = 0; x < CHUNK_SIZE_X; x++) { + for (int z = 0; z < CHUNK_SIZE_Z; z++) { + refresh(x, z, startY); + } + } + } + needsRefresh = false; + } + + public void refresh(int x, int z, int startY) { + int y = startY; + while (y > minHeight) { + Block block = chunk.getBlock(x, y, z, Block.Getter.Condition.TYPE); + if (block == null) continue; + if (checkBlock(block)) break; + y--; + } + setHeightY(x, z, y); + } + + public NBTLongArray getNBT() { + final int dimensionHeight = chunk.getInstance().getDimensionType().getHeight(); + final int bitsForHeight = MathUtils.bitsToRepresent(dimensionHeight); + return NBT.LongArray(encode(heights, bitsForHeight)); + } + + public void loadFrom(ImmutableLongArray data) { + final int dimensionHeight = chunk.getInstance().getDimensionType().getHeight(); + final int bitsPerEntry = MathUtils.bitsToRepresent(dimensionHeight); + + final int entriesPerLong = 64 / bitsPerEntry; + + final int maxPossibleIndexInContainer = entriesPerLong - 1; + final int entryMask = (1 << bitsPerEntry) - 1; + + int containerIndex = 0; + for (int i = 0; i < heights.length; i++) { + final int indexInContainer = i % entriesPerLong; + + heights[i] = (short) ((int)(data.get(containerIndex) >> (indexInContainer * bitsPerEntry)) & entryMask); + + if (indexInContainer == maxPossibleIndexInContainer) containerIndex++; + } + + needsRefresh = false; + } + + // highest breaking block in section + public int getHeight(int x, int z) { + if (needsRefresh) refresh(getHighestBlockSection(chunk)); + return heights[z << 4 | x] + minHeight; + } + + private void setHeightY(int x, int z, int height) { + heights[z << 4 | x] = (short) (height - minHeight); + } + + public static int getHighestBlockSection(Chunk chunk) { + int y = chunk.getInstance().getDimensionType().getMaxY(); + + final int sectionsCount = chunk.getMaxSection() - chunk.getMinSection(); + for (int i = 0; i < sectionsCount; i++) { + int sectionY = chunk.getMaxSection() - i - 1; + var blockPalette = chunk.getSection(sectionY).blockPalette(); + if (blockPalette.count() != 0) break; + y -= 16; + } + return y; + } + + /** + * Creates compressed longs array from uncompressed heights array. + * + * @param heights array of heights. Note that for this method it doesn't matter what size this array will be. + * But to get correct heights, array must be 256 elements long, and at index `i` must be height of (z=i/16, x=i%16). + * @param bitsPerEntry bits that each entry from height will take in `long` container. + * @return array of encoded heights. + */ + static long[] encode(short[] heights, int bitsPerEntry) { + final int entriesPerLong = 64 / bitsPerEntry; + // ceil(HeightsCount / entriesPerLong) + final int len = (heights.length + entriesPerLong - 1) / entriesPerLong; + + final int maxPossibleIndexInContainer = entriesPerLong - 1; + final int entryMask = (1 << bitsPerEntry) - 1; + + long[] data = new long[len]; + int containerIndex = 0; + for (int i = 0; i < heights.length; i++) { + final int indexInContainer = i % entriesPerLong; + final int entry = heights[i]; + + data[containerIndex] |= ((long) (entry & entryMask)) << (indexInContainer * bitsPerEntry); + + if (indexInContainer == maxPossibleIndexInContainer) containerIndex++; + } + + return data; + } +} diff --git a/src/main/java/net/minestom/server/instance/heightmap/MotionBlockingHeightmap.java b/src/main/java/net/minestom/server/instance/heightmap/MotionBlockingHeightmap.java new file mode 100644 index 000000000..8d07b9c76 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/heightmap/MotionBlockingHeightmap.java @@ -0,0 +1,23 @@ +package net.minestom.server.instance.heightmap; + +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +public class MotionBlockingHeightmap extends Heightmap { + public MotionBlockingHeightmap(Chunk attachedChunk) { + super(attachedChunk); + } + + @Override + protected boolean checkBlock(@NotNull Block block) { + return (block.isSolid() && !block.compare(Block.COBWEB) && !block.compare(Block.BAMBOO_SAPLING)) + || block.isLiquid() + || "true".equals(block.getProperty("waterlogged")); + } + + @Override + public String NBTName() { + return "MOTION_BLOCKING"; + } +} diff --git a/src/main/java/net/minestom/server/instance/heightmap/WorldSurfaceHeightmap.java b/src/main/java/net/minestom/server/instance/heightmap/WorldSurfaceHeightmap.java new file mode 100644 index 000000000..20f6f8c81 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/heightmap/WorldSurfaceHeightmap.java @@ -0,0 +1,21 @@ +package net.minestom.server.instance.heightmap; + +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +public class WorldSurfaceHeightmap extends Heightmap { + public WorldSurfaceHeightmap(Chunk attachedChunk) { + super(attachedChunk); + } + + @Override + protected boolean checkBlock(@NotNull Block block) { + return !block.isAir(); + } + + @Override + public String NBTName() { + return "WORLD_SURFACE"; + } +} diff --git a/src/main/java/net/minestom/server/instance/light/LightCompute.java b/src/main/java/net/minestom/server/instance/light/LightCompute.java index 2d48bb3cd..031feda46 100644 --- a/src/main/java/net/minestom/server/instance/light/LightCompute.java +++ b/src/main/java/net/minestom/server/instance/light/LightCompute.java @@ -7,13 +7,12 @@ import net.minestom.server.instance.palette.Palette; import net.minestom.server.utils.Direction; import org.jetbrains.annotations.NotNull; -import java.util.ArrayDeque; import java.util.Objects; import static net.minestom.server.instance.light.BlockLight.buildInternalQueue; public final class LightCompute { - static final BlockFace[] FACES = BlockFace.values(); + static final Direction[] DIRECTIONS = Direction.values(); static final int LIGHT_LENGTH = 16 * 16 * 16 / 2; static final int SECTION_SIZE = 16; @@ -23,45 +22,49 @@ public final class LightCompute { return LightCompute.compute(blockPalette, buildInternalQueue(blockPalette)); } + /** + * Computes light in one section + *

+ * Takes queue of lights positions and spreads light from this positions in 3d using Breadth-first search + * @param blockPalette blocks placed in section + * @param lightPre shorts queue in format: [4bit light level][4bit y][4bit z][4bit x] + * @return lighting wrapped in Result + */ static @NotNull Result compute(Palette blockPalette, ShortArrayFIFOQueue lightPre) { if (lightPre.isEmpty()) { return new Result(emptyContent); } - byte[] lightArray = new byte[LIGHT_LENGTH]; + final byte[] lightArray = new byte[LIGHT_LENGTH]; - var lightSources = new ArrayDeque(); + final ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); while (!lightPre.isEmpty()) { - int index = lightPre.dequeueShort(); + final int index = lightPre.dequeueShort(); - final int x = index & 15; - final int z = (index >> 4) & 15; - final int y = (index >> 8) & 15; final int newLightLevel = (index >> 12) & 15; - final int newIndex = x | (z << 4) | (y << 8); + final int newIndex = index & 0xFFF; final int oldLightLevel = getLight(lightArray, newIndex); if (oldLightLevel < newLightLevel) { placeLight(lightArray, newIndex, newLightLevel); - lightSources.add((short) index); + lightSources.enqueue((short) index); } } while (!lightSources.isEmpty()) { - final int index = lightSources.poll(); + final int index = lightSources.dequeueShort(); final int x = index & 15; final int z = (index >> 4) & 15; final int y = (index >> 8) & 15; final int lightLevel = (index >> 12) & 15; + final byte newLightLevel = (byte) (lightLevel - 1); - for (BlockFace face : FACES) { - Direction dir = face.toDirection(); - final int xO = x + dir.normalX(); - final int yO = y + dir.normalY(); - final int zO = z + dir.normalZ(); - final byte newLightLevel = (byte) (lightLevel - 1); + for (Direction direction : DIRECTIONS) { + final int xO = x + direction.normalX(); + final int yO = y + direction.normalY(); + final int zO = z + direction.normalZ(); // Handler border if (xO < 0 || xO >= SECTION_SIZE || yO < 0 || yO >= SECTION_SIZE || zO < 0 || zO >= SECTION_SIZE) { @@ -70,14 +73,16 @@ public final class LightCompute { // Section final int newIndex = xO | (zO << 4) | (yO << 8); - if (getLight(lightArray, newIndex) + 2 <= lightLevel) { + + if (getLight(lightArray, newIndex) < newLightLevel) { final Block currentBlock = Objects.requireNonNullElse(Block.fromStateId((short)blockPalette.get(x, y, z)), Block.AIR); final Block propagatedBlock = Objects.requireNonNullElse(Block.fromStateId((short)blockPalette.get(xO, yO, zO)), Block.AIR); - boolean airAir = currentBlock.isAir() && propagatedBlock.isAir(); - if (!airAir && currentBlock.registry().collisionShape().isOccluded(propagatedBlock.registry().collisionShape(), face)) continue; + final boolean airAir = currentBlock.isAir() && propagatedBlock.isAir(); + if (!airAir && currentBlock.registry().collisionShape().isOccluded(propagatedBlock.registry().collisionShape(), BlockFace.fromDirection(direction))) continue; + placeLight(lightArray, newIndex, newLightLevel); - lightSources.add((short) (newIndex | (newLightLevel << 12))); + lightSources.enqueue((short) (newIndex | (newLightLevel << 12))); } } } diff --git a/src/main/java/net/minestom/server/instance/light/SkyLight.java b/src/main/java/net/minestom/server/instance/light/SkyLight.java index 6bb15da17..4f9fd5e5a 100644 --- a/src/main/java/net/minestom/server/instance/light/SkyLight.java +++ b/src/main/java/net/minestom/server/instance/light/SkyLight.java @@ -10,7 +10,6 @@ import net.minestom.server.instance.Section; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.palette.Palette; -import org.jetbrains.annotations.NotNull; import java.util.Arrays; import java.util.HashSet; @@ -57,7 +56,7 @@ final class SkyLight implements Light { ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); if (c instanceof LightingChunk lc) { - int[] heightmap = lc.getHeightmap(); + int[] heightmap = lc.getOcclusionMap(); int maxY = c.getInstance().getDimensionType().getMinY() + c.getInstance().getDimensionType().getHeight(); int sectionMaxY = (sectionY + 1) * 16 - 1; int sectionMinY = sectionY * 16; diff --git a/src/test/java/net/minestom/server/instance/ChunkHeightmapIntegrationTest.java b/src/test/java/net/minestom/server/instance/ChunkHeightmapIntegrationTest.java new file mode 100644 index 000000000..269492302 --- /dev/null +++ b/src/test/java/net/minestom/server/instance/ChunkHeightmapIntegrationTest.java @@ -0,0 +1,59 @@ +package net.minestom.server.instance; + +import net.minestom.server.instance.block.Block; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@EnvTest +public class ChunkHeightmapIntegrationTest { + @Test + public void testChunkHeightmap(Env env) { + var instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + var chunk = instance.getChunk(0, 0); + + var heightmap = chunk.motionBlockingHeightmap().getHeight(0, 0); + assertEquals(heightmap, 39); + } + + @Test + public void heightMapPlaceTest(Env env) { + var instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + var chunk = instance.getChunk(0, 0); + + { + instance.setBlock(0, 40, 0, Block.STONE); + var heightmap = chunk.motionBlockingHeightmap().getHeight(0, 0); + assertEquals(heightmap, 40); + } + + { + instance.setBlock(0, 45, 0, Block.STONE); + var heightmap = chunk.motionBlockingHeightmap().getHeight(0, 0); + assertEquals(heightmap, 45); + } + } + + @Test + public void heightMapRemoveTest(Env env) { + var instance = env.createFlatInstance(); + instance.loadChunk(0, 0).join(); + var chunk = instance.getChunk(0, 0); + + { + instance.setBlock(0, 45, 0, Block.STONE); + var heightmap = chunk.motionBlockingHeightmap().getHeight(0, 0); + assertEquals(heightmap, 45); + } + + { + instance.setBlock(0, 45, 0, Block.AIR); + var heightmap = chunk.motionBlockingHeightmap().getHeight(0, 0); + assertEquals(heightmap, 39); + } + } +}