mirror of
https://github.com/Minestom/Minestom.git
synced 2025-02-18 21:32:56 +01:00
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:
parent
d0c7d7350e
commit
2a7df1ab55
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user