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 <aceparent@gmail.com>
This commit is contained in:
Alexander 2024-04-30 06:52:21 +03:00 committed by GitHub
parent d0c7d7350e
commit 2a7df1ab55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 370 additions and 132 deletions

View File

@ -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());
}
/**

View File

@ -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').
* <p>
* 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,
* <p>
* "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
*/

View File

@ -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<Section> sections;
private boolean needsCompleteHeightmapRefresh = true;
protected Heightmap motionBlocking = new MotionBlockingHeightmap(this);
protected Heightmap worldSurface = new WorldSurfaceHeightmap(this);
// Key = ChunkUtils#getBlockIndex
protected final Int2ObjectOpenHashMap<Block> entries = new Int2ObjectOpenHashMap<>(0);
protected final Int2ObjectOpenHashMap<Block> 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;
}
}

View File

@ -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<Point> 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -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";
}
}

View File

@ -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
* <p>
* 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<Short>();
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)));
}
}
}

View File

@ -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;

View File

@ -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);
}
}
}