hollow-cube/lighting (#13)

* Lighting

* Remove invalidate

* Private

* Fix chunk loading

* Small fixes

* Fix loading light from anvil world

* Fix solid

* Temporary

* Fix tests, add seagrass and tall seagrass to diffusion list

* Make test faster, replace hephaistos

* Cleanup

* Assume failed test

* Fix chunk not getting invalidated

(cherry picked from commit f13a7b49fa)
This commit is contained in:
iam 2023-05-27 19:41:14 -04:00 committed by mworzala
parent 8a56d147f3
commit c86267c516
No known key found for this signature in database
GPG Key ID: B148F922E64797C7
24 changed files with 2851 additions and 63 deletions

View File

@ -20,6 +20,7 @@ import net.minestom.server.event.server.ServerTickMonitorEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.InstanceManager;
import net.minestom.server.instance.LightingChunk;
import net.minestom.server.instance.block.Block;
import net.minestom.server.inventory.Inventory;
import net.minestom.server.inventory.InventoryType;
@ -123,6 +124,7 @@ public class PlayerInit {
InstanceContainer instanceContainer = instanceManager.createInstanceContainer(DimensionType.OVERWORLD);
instanceContainer.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE));
instanceContainer.setChunkSupplier(LightingChunk::new);
if (false) {
System.out.println("start");

View File

@ -4,6 +4,7 @@ import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.instance.block.BlockFace;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -33,6 +34,11 @@ public final class BoundingBox implements Shape {
this(width, height, depth, new Vec(-width / 2, 0, -depth / 2));
}
@Override
public boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face) {
return false;
}
@Override
@ApiStatus.Experimental
public boolean intersectBox(@NotNull Point positionRelative, @NotNull BoundingBox boundingBox) {

View File

@ -113,7 +113,7 @@ public final class CollisionUtils {
};
}
public static Shape parseBlockShape(String str, Registry.BlockEntry blockEntry) {
return ShapeImpl.parseBlockFromRegistry(str, blockEntry);
public static Shape parseBlockShape(String collision, String occlusion, Registry.BlockEntry blockEntry) {
return ShapeImpl.parseBlockFromRegistry(collision, occlusion, blockEntry);
}
}

View File

@ -1,11 +1,14 @@
package net.minestom.server.collision;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.BlockFace;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@ApiStatus.Experimental
public interface Shape {
boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face);
/**
* Checks if two bounding boxes intersect.
*

View File

@ -5,28 +5,37 @@ import it.unimi.dsi.fastutil.doubles.DoubleList;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockFace;
import net.minestom.server.registry.Registry;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class ShapeImpl implements Shape {
private static final Pattern PATTERN = Pattern.compile("\\d.\\d{1,3}", Pattern.MULTILINE);
private final BoundingBox[] blockSections;
private final BoundingBox[] collisionBoundingBoxes;
private final Point relativeStart, relativeEnd;
private final BoundingBox[] occlusionBoundingBoxes;
private final byte blockOcclusion;
private final byte airOcclusion;
private final Registry.BlockEntry blockEntry;
private Block block;
private ShapeImpl(BoundingBox[] boundingBoxes, Registry.BlockEntry blockEntry) {
this.blockSections = boundingBoxes;
private ShapeImpl(BoundingBox[] boundingBoxes, BoundingBox[] occlusionBoundingBoxes, Registry.BlockEntry blockEntry) {
this.collisionBoundingBoxes = boundingBoxes;
this.occlusionBoundingBoxes = occlusionBoundingBoxes;
this.blockEntry = blockEntry;
// Find bounds
// Find bounds of collision
{
double minX = 1, minY = 1, minZ = 1;
double maxX = 0, maxY = 0, maxZ = 0;
for (BoundingBox blockSection : blockSections) {
for (BoundingBox blockSection : collisionBoundingBoxes) {
// Min
if (blockSection.minX() < minX) minX = blockSection.minX();
if (blockSection.minY() < minY) minY = blockSection.minY();
@ -39,16 +48,26 @@ final class ShapeImpl implements Shape {
this.relativeStart = new Vec(minX, minY, minZ);
this.relativeEnd = new Vec(maxX, maxY, maxZ);
}
byte airFaces = 0;
byte fullFaces = 0;
for (BlockFace f : BlockFace.values()) {
final byte res = isFaceCovered(computeOcclusionSet(f));
airFaces |= ((res == 0) ? 0b1 : 0b0) << (byte) f.ordinal();
fullFaces |= ((res == 2) ? 0b1 : 0b0) << (byte) f.ordinal();
}
this.airOcclusion = airFaces;
this.blockOcclusion = fullFaces;
}
static ShapeImpl parseBlockFromRegistry(String str, Registry.BlockEntry blockEntry) {
static private BoundingBox[] parseRegistryBoundingBoxString(String str) {
final Matcher matcher = PATTERN.matcher(str);
DoubleList vals = new DoubleArrayList();
while (matcher.find()) {
double newVal = Double.parseDouble(matcher.group());
vals.add(newVal);
}
final int count = vals.size() / 6;
BoundingBox[] boundingBoxes = new BoundingBox[count];
for (int i = 0; i < count; ++i) {
@ -66,7 +85,36 @@ final class ShapeImpl implements Shape {
assert bb.minZ() == minZ;
boundingBoxes[i] = bb;
}
return new ShapeImpl(boundingBoxes, blockEntry);
return boundingBoxes;
}
/**
* Computes the occlusion for a given face.
*
* @param covering The rectangle set to check for covering.
* @return 0 if face is not covered, 1 if face is covered partially, 2 if face is fully covered.
*/
private static byte isFaceCovered(List<Rectangle> covering) {
if (covering.isEmpty()) return 0;
Rectangle r = new Rectangle(0, 0, 1, 1);
List<Rectangle> toCover = new ArrayList<>();
toCover.add(r);
for (Rectangle rect : covering) {
List<Rectangle> nextCovering = new ArrayList<>();
for (Rectangle toCoverRect : toCover) {
List<Rectangle> remaining = getRemaining(rect, toCoverRect);
nextCovering.addAll(remaining);
}
toCover = nextCovering;
if (toCover.isEmpty()) return 2;
}
return 1;
}
static ShapeImpl parseBlockFromRegistry(String collision, String occlusion, Registry.BlockEntry blockEntry) {
BoundingBox[] collisionBoundingBoxes = parseRegistryBoundingBoxString(collision);
BoundingBox[] occlusionBoundingBoxes = blockEntry.occludes() ? parseRegistryBoundingBoxString(occlusion) : new BoundingBox[0];
return new ShapeImpl(collisionBoundingBoxes, occlusionBoundingBoxes, blockEntry);
}
@Override
@ -79,9 +127,32 @@ final class ShapeImpl implements Shape {
return relativeEnd;
}
@Override
public boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face) {
final ShapeImpl shapeImpl = ((ShapeImpl) shape);
final boolean hasBlockOcclusion = (((blockOcclusion >> face.ordinal()) & 1) == 1);
final boolean hasBlockOcclusionOther = ((shapeImpl.blockOcclusion >> face.getOppositeFace().ordinal()) & 1) == 1;
if (blockEntry.lightEmission() > 0) return hasBlockOcclusionOther;
// If either face is full, return true
if (hasBlockOcclusion || hasBlockOcclusionOther) return true;
final boolean hasAirOcclusion = (((airOcclusion >> face.ordinal()) & 1) == 1);
final boolean hasAirOcclusionOther = ((shapeImpl.airOcclusion >> face.getOppositeFace().ordinal()) & 1) == 1;
// If a single face is air, return false
if (hasAirOcclusion || hasAirOcclusionOther) return false;
// Comparing two partial faces. Computation needed
List<Rectangle> allRectangles = shapeImpl.computeOcclusionSet(face.getOppositeFace());
allRectangles.addAll(computeOcclusionSet(face));
return isFaceCovered(allRectangles) == 2;
}
@Override
public boolean intersectBox(@NotNull Point position, @NotNull BoundingBox boundingBox) {
for (BoundingBox blockSection : blockSections) {
for (BoundingBox blockSection : collisionBoundingBoxes) {
if (boundingBox.intersectBox(position, blockSection)) return true;
}
return false;
@ -91,7 +162,7 @@ final class ShapeImpl implements Shape {
public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection,
@NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) {
boolean hitBlock = false;
for (BoundingBox blockSection : blockSections) {
for (BoundingBox blockSection : collisionBoundingBoxes) {
// Update final result if the temp result collision is sooner than the current final result
if (RayUtils.BoundingBoxIntersectionCheck(moving, rayStart, rayDirection, blockSection, shapePos, finalResult)) {
finalResult.collidedShapePosition = shapePos;
@ -108,4 +179,75 @@ final class ShapeImpl implements Shape {
if (block == null) this.block = block = Block.fromStateId((short) blockEntry.stateId());
return block;
}
private List<Rectangle> computeOcclusionSet(BlockFace face) {
List<Rectangle> rSet = new ArrayList<>();
for (BoundingBox boundingBox : this.occlusionBoundingBoxes) {
switch (face) {
case NORTH -> // negative Z
{
if (boundingBox.minZ() == 0)
rSet.add(new Rectangle(boundingBox.minX(), boundingBox.minY(), boundingBox.maxX(), boundingBox.maxY()));
}
case SOUTH -> // positive Z
{
if (boundingBox.maxZ() == 1)
rSet.add(new Rectangle(boundingBox.minX(), boundingBox.minY(), boundingBox.maxX(), boundingBox.maxY()));
}
case WEST -> // negative X
{
if (boundingBox.minX() == 0)
rSet.add(new Rectangle(boundingBox.minY(), boundingBox.minZ(), boundingBox.maxY(), boundingBox.maxZ()));
}
case EAST -> // positive X
{
if (boundingBox.maxX() == 1)
rSet.add(new Rectangle(boundingBox.minY(), boundingBox.minZ(), boundingBox.maxY(), boundingBox.maxZ()));
}
case BOTTOM -> // negative Y
{
if (boundingBox.minY() == 0)
rSet.add(new Rectangle(boundingBox.minX(), boundingBox.minZ(), boundingBox.maxX(), boundingBox.maxZ()));
}
case TOP -> // positive Y
{
if (boundingBox.maxY() == 1)
rSet.add(new Rectangle(boundingBox.minX(), boundingBox.minZ(), boundingBox.maxX(), boundingBox.maxZ()));
}
}
}
return rSet;
}
private static List<Rectangle> getRemaining(Rectangle covering, Rectangle toCover) {
List<Rectangle> remaining = new ArrayList<>();
covering = clipRectangle(covering, toCover);
// Up
if (covering.y1() > toCover.y1()) {
remaining.add(new Rectangle(toCover.x1(), toCover.y1(), toCover.x2(), covering.y1()));
}
// Down
if (covering.y2() < toCover.y2()) {
remaining.add(new Rectangle(toCover.x1(), covering.y2(), toCover.x2(), toCover.y2()));
}
// Left
if (covering.x1() > toCover.x1()) {
remaining.add(new Rectangle(toCover.x1(), covering.y1(), covering.x1(), covering.y2()));
}
//Right
if (covering.x2() < toCover.x2()) {
remaining.add(new Rectangle(covering.x2(), covering.y1(), toCover.x2(), covering.y2()));
}
return remaining;
}
private static Rectangle clipRectangle(Rectangle covering, Rectangle toCover) {
final double x1 = Math.max(covering.x1(), toCover.x1());
final double y1 = Math.max(covering.y1(), toCover.y1());
final double x2 = Math.min(covering.x2(), toCover.x2());
final double y2 = Math.min(covering.y2(), toCover.y2());
return new Rectangle(x1, y1, x2, y2);
}
private record Rectangle(double x1, double y1, double x2, double y2) { }
}

View File

@ -101,7 +101,7 @@ public class AnvilLoader implements IChunkLoader {
final ChunkReader chunkReader = new ChunkReader(chunkData);
Chunk chunk = new DynamicChunk(instance, chunkX, chunkZ);
Chunk chunk = instance.getChunkSupplier().createChunk(instance, chunkX, chunkZ);
synchronized (chunk) {
var yRange = chunkReader.getYRange();
if (yRange.getStart() < instance.getDimensionType().getMinY()) {
@ -375,8 +375,8 @@ public class AnvilLoader implements IChunkLoader {
ChunkSectionWriter sectionWriter = new ChunkSectionWriter(SupportedVersion.Companion.getLatest(), (byte) sectionY);
Section section = chunk.getSection(sectionY);
sectionWriter.setSkyLights(section.getSkyLight());
sectionWriter.setBlockLights(section.getBlockLight());
sectionWriter.setSkyLights(section.skyLight().array());
sectionWriter.setBlockLights(section.blockLight().array());
BiomePalette biomePalette = new BiomePalette();
BlockPalette blockPalette = new BlockPalette();

View File

@ -39,7 +39,7 @@ import static net.minestom.server.utils.chunk.ChunkUtils.toSectionRelativeCoordi
*/
public class DynamicChunk extends Chunk {
private List<Section> sections;
protected List<Section> sections;
// Key = ChunkUtils#getBlockIndex
protected final Int2ObjectOpenHashMap<Block> entries = new Int2ObjectOpenHashMap<>(0);
@ -47,7 +47,6 @@ public class DynamicChunk extends Chunk {
private long lastChange;
final CachedPacket chunkCache = new CachedPacket(this::createChunkPacket);
final CachedPacket lightCache = new CachedPacket(this::createLightPacket);
public DynamicChunk(@NotNull Instance instance, int chunkX, int chunkZ) {
super(instance, chunkX, chunkZ, true);
@ -61,7 +60,7 @@ public class DynamicChunk extends Chunk {
assertLock();
this.lastChange = System.currentTimeMillis();
this.chunkCache.invalidate();
this.lightCache.invalidate();
// Update pathfinder
if (columnarSpace != null) {
final ColumnarOcclusionFieldList columnarOcclusionFieldList = columnarSpace.occlusionFields();
@ -183,7 +182,7 @@ public class DynamicChunk extends Chunk {
this.entries.clear();
}
private synchronized @NotNull ChunkDataPacket createChunkPacket() {
private @NotNull ChunkDataPacket createChunkPacket() {
final NBTCompound heightmapsNBT;
// TODO: don't hardcode heightmaps
// Heightmap
@ -203,20 +202,25 @@ public class DynamicChunk extends Chunk {
"WORLD_SURFACE", NBT.LongArray(encodeBlocks(worldSurface, bitsForHeight))));
}
// Data
final byte[] data = ObjectPool.PACKET_POOL.use(buffer ->
NetworkBuffer.makeArray(networkBuffer -> {
for (Section section : sections) networkBuffer.write(section);
}));
final byte[] data;
synchronized (this) {
data = ObjectPool.PACKET_POOL.use(buffer ->
NetworkBuffer.makeArray(networkBuffer -> {
for (Section section : sections) networkBuffer.write(section);
}));
}
return new ChunkDataPacket(chunkX, chunkZ,
new ChunkData(heightmapsNBT, data, entries),
createLightData());
createLightData(true));
}
private synchronized @NotNull UpdateLightPacket createLightPacket() {
return new UpdateLightPacket(chunkX, chunkZ, createLightData());
@NotNull UpdateLightPacket createLightPacket() {
return new UpdateLightPacket(chunkX, chunkZ, createLightData(false));
}
private LightData createLightData() {
protected LightData createLightData(boolean sendAll) {
BitSet skyMask = new BitSet();
BitSet blockMask = new BitSet();
BitSet emptySkyMask = new BitSet();
@ -227,8 +231,8 @@ public class DynamicChunk extends Chunk {
int index = 0;
for (Section section : sections) {
index++;
final byte[] skyLight = section.getSkyLight();
final byte[] blockLight = section.getBlockLight();
final byte[] skyLight = section.skyLight().array();
final byte[] blockLight = section.blockLight().array();
if (skyLight.length != 0) {
skyLights.add(skyLight);
skyMask.set(index);

View File

@ -34,6 +34,7 @@ import net.minestom.server.timer.Scheduler;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.chunk.ChunkCache;
import net.minestom.server.utils.chunk.ChunkSupplier;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.time.Cooldown;
import net.minestom.server.utils.time.TimeUnit;
@ -278,6 +279,14 @@ public abstract class Instance implements Block.Getter, Block.Setter,
setGenerator(chunkGenerator != null ? new ChunkGeneratorCompatibilityLayer(chunkGenerator) : null);
}
public abstract void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier);
/**
* Gets the chunk supplier of the instance.
* @return the chunk supplier of the instance
*/
public abstract ChunkSupplier getChunkSupplier();
/**
* Gets the generator associated with the instance
*

View File

@ -328,9 +328,11 @@ public class InstanceContainer extends Instance {
if (forkChunk != null) {
applyFork(forkChunk, sectionModifier);
// Update players
if (forkChunk instanceof DynamicChunk dynamicChunk) {
if (forkChunk instanceof LightingChunk lightingChunk) {
lightingChunk.chunkCache.invalidate();
lightingChunk.lightCache.invalidate();
} else if (forkChunk instanceof DynamicChunk dynamicChunk) {
dynamicChunk.chunkCache.invalidate();
dynamicChunk.lightCache.invalidate();
}
forkChunk.sendChunk();
} else {
@ -427,6 +429,7 @@ public class InstanceContainer extends Instance {
* @param chunkSupplier the new {@link ChunkSupplier} of this instance, chunks need to be non-null
* @throws NullPointerException if {@code chunkSupplier} is null
*/
@Override
public void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier) {
this.chunkSupplier = chunkSupplier;
}

View File

@ -0,0 +1,384 @@
package net.minestom.server.instance;
import net.minestom.server.MinecraftServer;
import net.minestom.server.collision.Shape;
import net.minestom.server.coordinate.Point;
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.light.Light;
import net.minestom.server.network.packet.server.CachedPacket;
import net.minestom.server.network.packet.server.play.data.LightData;
import net.minestom.server.timer.ExecutionType;
import net.minestom.server.timer.Task;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.NamespaceID;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class LightingChunk extends DynamicChunk {
private int[] heightmap;
final CachedPacket lightCache = new CachedPacket(this::createLightPacket);
boolean sendNeighbours = true;
enum LightType {
SKY,
BLOCK
}
private static final Set<NamespaceID> DIFFUSE_SKY_LIGHT = Set.of(
Block.COBWEB.namespace(),
Block.ICE.namespace(),
Block.HONEY_BLOCK.namespace(),
Block.SLIME_BLOCK.namespace(),
Block.WATER.namespace(),
Block.ACACIA_LEAVES.namespace(),
Block.AZALEA_LEAVES.namespace(),
Block.BIRCH_LEAVES.namespace(),
Block.DARK_OAK_LEAVES.namespace(),
Block.FLOWERING_AZALEA_LEAVES.namespace(),
Block.JUNGLE_LEAVES.namespace(),
Block.OAK_LEAVES.namespace(),
Block.SPRUCE_LEAVES.namespace(),
Block.SPAWNER.namespace(),
Block.BEACON.namespace(),
Block.END_GATEWAY.namespace(),
Block.CHORUS_PLANT.namespace(),
Block.CHORUS_FLOWER.namespace(),
Block.FROSTED_ICE.namespace(),
Block.SEAGRASS.namespace(),
Block.TALL_SEAGRASS.namespace(),
Block.LAVA.namespace()
);
public LightingChunk(@NotNull Instance instance, int chunkX, int chunkZ) {
super(instance, chunkX, chunkZ);
}
private boolean checkSkyOcclusion(Block block) {
if (block == Block.AIR) return false;
if (DIFFUSE_SKY_LIGHT.contains(block.namespace())) return true;
Shape shape = block.registry().collisionShape();
boolean occludesTop = Block.AIR.registry().collisionShape().isOccluded(shape, BlockFace.TOP);
boolean occludesBottom = Block.AIR.registry().collisionShape().isOccluded(shape, BlockFace.BOTTOM);
return occludesBottom || occludesTop;
}
private void invalidateSection(int coordinate) {
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
Chunk neighborChunk = instance.getChunk(chunkX + i, chunkZ + j);
if (neighborChunk == null) continue;
if (neighborChunk instanceof LightingChunk lightingChunk) {
lightingChunk.lightCache.invalidate();
lightingChunk.chunkCache.invalidate();
}
for (int k = -1; k <= 1; k++) {
if (k + coordinate < neighborChunk.getMinSection() || k + coordinate >= neighborChunk.getMaxSection()) continue;
neighborChunk.getSection(k + coordinate).blockLight().invalidate();
neighborChunk.getSection(k + coordinate).skyLight().invalidate();
}
}
}
}
@Override
public void setBlock(int x, int y, int z, @NotNull Block block) {
super.setBlock(x, y, z, block);
this.heightmap = null;
// Invalidate neighbor chunks, since they can be updated by this block change
int coordinate = ChunkUtils.getChunkCoordinate(y);
invalidateSection(coordinate);
this.lightCache.invalidate();
}
public void sendLighting() {
if (!isLoaded()) return;
sendPacketToViewers(lightCache);
}
public int[] calculateHeightMap() {
if (this.heightmap != null) return this.heightmap;
var heightmap = new int[CHUNK_SIZE_X * CHUNK_SIZE_Z];
int minY = instance.getDimensionType().getMinY();
int maxY = instance.getDimensionType().getMinY() + instance.getDimensionType().getHeight();
synchronized (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) {
Block block = getBlock(x, height, z, Condition.TYPE);
if (checkSkyOcclusion(block)) break;
height--;
}
heightmap[z << 4 | x] = (height + 1);
}
}
}
this.heightmap = heightmap;
return heightmap;
}
@Override
protected LightData createLightData(boolean sendAll) {
BitSet skyMask = new BitSet();
BitSet blockMask = new BitSet();
BitSet emptySkyMask = new BitSet();
BitSet emptyBlockMask = new BitSet();
List<byte[]> skyLights = new ArrayList<>();
List<byte[]> blockLights = new ArrayList<>();
int index = 0;
for (Section section : sections) {
boolean wasUpdatedBlock = false;
boolean wasUpdatedSky = false;
if (section.blockLight().requiresUpdate()) {
relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.BLOCK);
wasUpdatedBlock = true;
} else if (section.blockLight().requiresSend()) {
wasUpdatedBlock = true;
}
if (section.skyLight().requiresUpdate()) {
relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.SKY);
wasUpdatedSky = true;
} else if (section.skyLight().requiresSend()) {
wasUpdatedSky = true;
}
index++;
final byte[] skyLight = section.skyLight().array();
final byte[] blockLight = section.blockLight().array();
// System.out.println("Relit sky: " + wasUpdatedSky + " block: " + wasUpdatedBlock + " for section " + (index + minSection) + " in chunk " + chunkX + " " + chunkZ);
if ((wasUpdatedSky || sendAll) && this.instance.getDimensionType().isSkylightEnabled()) {
if (skyLight.length != 0) {
skyLights.add(skyLight);
skyMask.set(index);
} else {
emptySkyMask.set(index);
}
}
if (wasUpdatedBlock || sendAll) {
if (blockLight.length != 0) {
blockLights.add(blockLight);
blockMask.set(index);
} else {
emptyBlockMask.set(index);
}
}
}
if (sendNeighbours) {
updateAfterGeneration(this);
sendNeighbours = false;
}
return new LightData(true,
skyMask, blockMask,
emptySkyMask, emptyBlockMask,
skyLights, blockLights);
}
private static final Set<LightingChunk> sendQueue = ConcurrentHashMap.newKeySet();
private static Task sendingTask = null;
private static void updateAfterGeneration(LightingChunk chunk) {
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
Chunk neighborChunk = chunk.instance.getChunk(chunk.chunkX + i, chunk.chunkZ + j);
if (neighborChunk == null) continue;
if (neighborChunk instanceof LightingChunk lightingChunk) {
sendQueue.add(lightingChunk);
}
}
}
if (sendingTask == null) {
sendingTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> {
sendingTask = null;
for (LightingChunk f : sendQueue) {
if (f.isLoaded()) {
f.sections.forEach(s -> {
s.blockLight().invalidate();
s.skyLight().invalidate();
});
f.sendLighting();
sendQueue.remove(f);
}
f.chunkCache.invalidate();
}
}, TaskSchedule.tick(5), TaskSchedule.stop(), ExecutionType.ASYNC);
}
}
private static void flushQueue(Instance instance, Set<Point> queue, LightType type) {
var updateQueue =
queue.parallelStream()
.map(sectionLocation -> {
Chunk chunk = instance.getChunk(sectionLocation.blockX(), sectionLocation.blockZ());
if (chunk == null) return null;
if (type == LightType.BLOCK) {
return chunk.getSection(sectionLocation.blockY()).blockLight()
.calculateExternal(instance, chunk, sectionLocation.blockY());
} else {
return chunk.getSection(sectionLocation.blockY()).skyLight()
.calculateExternal(instance, chunk, sectionLocation.blockY());
}
})
.filter(Objects::nonNull)
.toList()
.parallelStream()
.flatMap(light -> light.flip().stream())
.collect(Collectors.toSet());
if (updateQueue.size() > 0) {
flushQueue(instance, updateQueue, type);
}
}
public static void relight(Instance instance, Collection<Chunk> chunks) {
Set<Point> toPropagate = chunks
.parallelStream()
.flatMap(chunk -> IntStream
.range(chunk.getMinSection(), chunk.getMaxSection())
.mapToObj(index -> Map.entry(index, chunk)))
.map(chunkIndex -> {
final Chunk chunk = chunkIndex.getValue();
final int section = chunkIndex.getKey();
chunk.getSection(section).blockLight().invalidate();
chunk.getSection(section).skyLight().invalidate();
return new Vec(chunk.getChunkX(), section, chunk.getChunkZ());
}).collect(Collectors.toSet());
synchronized (instance) {
relight(instance, toPropagate, LightType.BLOCK);
relight(instance, toPropagate, LightType.SKY);
}
}
private static Set<Point> getNearbyRequired(Instance instance, Point point) {
Set<Point> collected = new HashSet<>();
collected.add(point);
for (int x = point.blockX() - 1; x <= point.blockX() + 1; x++) {
for (int z = point.blockZ() - 1; z <= point.blockZ() + 1; z++) {
Chunk chunkCheck = instance.getChunk(x, z);
if (chunkCheck == null) continue;
for (int y = point.blockY() - 1; y <= point.blockY() + 1; y++) {
Point sectionPosition = new Vec(x, y, z);
if (sectionPosition.blockY() < chunkCheck.getMaxSection() && sectionPosition.blockY() >= chunkCheck.getMinSection()) {
Section s = chunkCheck.getSection(sectionPosition.blockY());
if (!s.blockLight().requiresUpdate() && !s.skyLight().requiresUpdate()) continue;
collected.add(sectionPosition);
}
}
}
}
return collected;
}
private static Set<Point> collectRequiredNearby(Instance instance, Point point) {
final Set<Point> found = new HashSet<>();
final ArrayDeque<Point> toCheck = new ArrayDeque<>();
toCheck.add(point);
found.add(point);
while (toCheck.size() > 0) {
final Point current = toCheck.poll();
final Set<Point> nearby = getNearbyRequired(instance, current);
nearby.forEach(p -> {
if (!found.contains(p)) {
found.add(p);
toCheck.add(p);
}
});
}
return found;
}
static void relightSection(Instance instance, int chunkX, int sectionY, int chunkZ) {
relightSection(instance, chunkX, sectionY, chunkZ, LightType.BLOCK);
relightSection(instance, chunkX, sectionY, chunkZ, LightType.SKY);
}
private static void relightSection(Instance instance, int chunkX, int sectionY, int chunkZ, LightType type) {
Chunk c = instance.getChunk(chunkX, chunkZ);
if (c == null) return;
Set<Point> collected = collectRequiredNearby(instance, new Vec(chunkX, sectionY, chunkZ));
// System.out.println("Calculating " + chunkX + " " + sectionY + " " + chunkZ + " | " + collected.size());
synchronized (instance) {
relight(instance, collected, type);
}
}
private static void relight(Instance instance, Set<Point> sections, LightType type) {
Set<Point> toPropagate = sections
.parallelStream()
// .stream()
.map(chunkIndex -> {
final Chunk chunk = instance.getChunk(chunkIndex.blockX(), chunkIndex.blockZ());
final int section = chunkIndex.blockY();
if (chunk == null) return null;
if (type == LightType.BLOCK) return chunk.getSection(section).blockLight().calculateInternal(chunk.getInstance(), chunk.getChunkX(), section, chunk.getChunkZ());
else return chunk.getSection(section).skyLight().calculateInternal(chunk.getInstance(), chunk.getChunkX(), section, chunk.getChunkZ());
}).filter(Objects::nonNull)
.flatMap(lightSet -> lightSet.flip().stream())
.collect(Collectors.toSet())
// .stream()
.parallelStream()
.flatMap(sectionLocation -> {
final Chunk chunk = instance.getChunk(sectionLocation.blockX(), sectionLocation.blockZ());
final int section = sectionLocation.blockY();
if (chunk == null) return Stream.empty();
final Light light = type == LightType.BLOCK ? chunk.getSection(section).blockLight() : chunk.getSection(section).skyLight();
light.calculateExternal(chunk.getInstance(), chunk, section);
return light.flip().stream();
}).collect(Collectors.toSet());
flushQueue(instance, toPropagate, type);
}
@Override
public @NotNull Chunk copy(@NotNull Instance instance, int chunkX, int chunkZ) {
LightingChunk lightingChunk = new LightingChunk(instance, chunkX, chunkZ);
lightingChunk.sections = sections.stream().map(Section::clone).toList();
lightingChunk.entries.putAll(entries);
return lightingChunk;
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.instance;
import net.minestom.server.instance.light.Light;
import net.minestom.server.instance.palette.Palette;
import net.minestom.server.network.NetworkBuffer;
import org.jetbrains.annotations.NotNull;
@ -7,22 +8,20 @@ import org.jetbrains.annotations.NotNull;
import static net.minestom.server.network.NetworkBuffer.SHORT;
public final class Section implements NetworkBuffer.Writer {
private Palette blockPalette;
private Palette biomePalette;
private byte[] skyLight;
private byte[] blockLight;
private final Palette blockPalette;
private final Palette biomePalette;
private final Light skyLight;
private final Light blockLight;
private Section(Palette blockPalette, Palette biomePalette,
byte[] skyLight, byte[] blockLight) {
private Section(Palette blockPalette, Palette biomePalette) {
this.blockPalette = blockPalette;
this.biomePalette = biomePalette;
this.skyLight = skyLight;
this.blockLight = blockLight;
this.skyLight = Light.sky(blockPalette);
this.blockLight = Light.block(blockPalette);
}
public Section() {
this(Palette.blocks(), Palette.biomes(),
new byte[0], new byte[0]);
this(Palette.blocks(), Palette.biomes());
}
public Palette blockPalette() {
@ -33,33 +32,14 @@ public final class Section implements NetworkBuffer.Writer {
return biomePalette;
}
public byte[] getSkyLight() {
return skyLight;
}
public void setSkyLight(byte[] skyLight) {
this.skyLight = skyLight;
}
public byte[] getBlockLight() {
return blockLight;
}
public void setBlockLight(byte[] blockLight) {
this.blockLight = blockLight;
}
public void clear() {
this.blockPalette.fill(0);
this.biomePalette.fill(0);
this.skyLight = new byte[0];
this.blockLight = new byte[0];
}
@Override
public @NotNull Section clone() {
return new Section(blockPalette.clone(), biomePalette.clone(),
skyLight.clone(), blockLight.clone());
return new Section(this.blockPalette.clone(), this.biomePalette.clone());
}
@Override
@ -68,4 +48,20 @@ public final class Section implements NetworkBuffer.Writer {
writer.write(blockPalette);
writer.write(biomePalette);
}
public void setSkyLight(byte[] copyArray) {
this.skyLight.set(copyArray);
}
public void setBlockLight(byte[] copyArray) {
this.blockLight.set(copyArray);
}
public Light skyLight() {
return skyLight;
}
public Light blockLight() {
return blockLight;
}
}

View File

@ -6,6 +6,7 @@ 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.generator.Generator;
import net.minestom.server.utils.chunk.ChunkSupplier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -75,6 +76,16 @@ public class SharedInstance extends Instance {
return instanceContainer.saveChunksToStorage();
}
@Override
public void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier) {
instanceContainer.setChunkSupplier(chunkSupplier);
}
@Override
public ChunkSupplier getChunkSupplier() {
return instanceContainer.getChunkSupplier();
}
@Override
public @Nullable Generator generator() {
return instanceContainer.generator();

View File

@ -6,12 +6,16 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.instance.LightingChunk;
import net.minestom.server.instance.block.Block;
import net.minestom.server.utils.chunk.ChunkUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
@ -125,6 +129,8 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
final AbsoluteBlockBatch inverse = this.options.shouldCalculateInverse() ? new AbsoluteBlockBatch(inverseOption) : null;
synchronized (chunkBatchesMap) {
AtomicInteger counter = new AtomicInteger();
Set<Chunk> updated = ConcurrentHashMap.newKeySet();
for (var entry : Long2ObjectMaps.fastIterable(chunkBatchesMap)) {
final long chunkIndex = entry.getLongKey();
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
@ -146,6 +152,25 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
callback.run();
}
}
Set<Chunk> expanded = new HashSet<>();
for (Chunk chunk : updated) {
for (int i = -1; i <= 1; ++i) {
for (int j = -1; j <= 1; ++j) {
Chunk toAdd = instance.getChunk(chunk.getChunkX() + i, chunk.getChunkZ() + j);
if (toAdd != null) {
expanded.add(toAdd);
}
}
}
}
// Update the chunk's light
for (Chunk chunk : expanded) {
if (chunk instanceof LightingChunk dc) {
dc.sendLighting();
}
}
}
});
if (inverse != null) inverse.chunkBatchesMap.put(chunkIndex, chunkInverse);

View File

@ -0,0 +1,359 @@
package net.minestom.server.instance.light;
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
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;
import java.util.Map;
import java.util.Set;
import static net.minestom.server.instance.light.LightCompute.*;
final class BlockLight implements Light {
private final Palette blockPalette;
private byte[] content;
private byte[] contentPropagation;
private byte[] contentPropagationSwap;
private byte[][] borders;
private byte[][] bordersPropagation;
private byte[][] bordersPropagationSwap;
private boolean isValidBorders = true;
private boolean needsSend = true;
private Set<Point> toUpdateSet = new HashSet<>();
BlockLight(Palette blockPalette) {
this.blockPalette = blockPalette;
}
@Override
public Set<Point> flip() {
if (this.bordersPropagationSwap != null)
this.bordersPropagation = this.bordersPropagationSwap;
if (this.contentPropagationSwap != null)
this.contentPropagation = this.contentPropagationSwap;
this.bordersPropagationSwap = null;
this.contentPropagationSwap = null;
if (toUpdateSet == null) return Set.of();
return toUpdateSet;
}
static IntArrayFIFOQueue buildInternalQueue(Palette blockPalette, Block[] blocks) {
IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue();
// Apply section light
blockPalette.getAllPresent((x, y, z, stateId) -> {
final Block block = Block.fromStateId((short) stateId);
assert block != null;
final byte lightEmission = (byte) block.registry().lightEmission();
final int index = x | (z << 4) | (y << 8);
blocks[index] = block;
if (lightEmission > 0) {
lightSources.enqueue(index | (lightEmission << 12));
}
});
return lightSources;
}
private static Block getBlock(Palette palette, int x, int y, int z) {
return Block.fromStateId((short)palette.get(x, y, z));
}
private static IntArrayFIFOQueue buildExternalQueue(Instance instance, Block[] blocks, Map<BlockFace, Point> neighbors, byte[][] borders) {
IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue();
for (BlockFace face : BlockFace.values()) {
Point neighborSection = neighbors.get(face);
if (neighborSection == null) continue;
Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ());
if (chunk == null) continue;
byte[] neighborFace = chunk.getSection(neighborSection.blockY()).blockLight().getBorderPropagation(face.getOppositeFace());
if (neighborFace == null) continue;
for (int bx = 0; bx < 16; bx++) {
for (int by = 0; by < 16; by++) {
final int borderIndex = bx * SECTION_SIZE + by;
byte lightEmission = neighborFace[borderIndex];
if (borders != null && borders[face.ordinal()] != null) {
final int internalEmission = borders[face.ordinal()][borderIndex];
if (lightEmission <= internalEmission) continue;
}
final int k = switch (face) {
case WEST, BOTTOM, NORTH -> 0;
case EAST, TOP, SOUTH -> 15;
};
final int posTo = switch (face) {
case NORTH, SOUTH -> bx | (k << 4) | (by << 8);
case WEST, EAST -> k | (by << 4) | (bx << 8);
default -> bx | (by << 4) | (k << 8);
};
Section otherSection = chunk.getSection(neighborSection.blockY());
final Block blockFrom = (switch (face) {
case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k);
case WEST, EAST -> getBlock(otherSection.blockPalette(), 15 - k, bx, by);
default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by);
});
if (blocks == null) continue;
Block blockTo = blocks[posTo];
if (blockTo == null && blockFrom != null) {
if (blockFrom.registry().collisionShape().isOccluded(Block.AIR.registry().collisionShape(), face.getOppositeFace()))
continue;
} else if (blockTo != null && blockFrom == null) {
if (Block.AIR.registry().collisionShape().isOccluded(blockTo.registry().collisionShape(), face))
continue;
} else if (blockTo != null && blockFrom != null) {
if (blockFrom.registry().collisionShape().isOccluded(blockTo.registry().collisionShape(), face.getOppositeFace()))
continue;
}
if (lightEmission > 0) {
final int index = posTo | (lightEmission << 12);
lightSources.enqueue(index);
}
}
}
}
return lightSources;
}
@Override
public void copyFrom(byte @NotNull [] array) {
if (array.length == 0) this.content = null;
else this.content = array.clone();
}
@Override
public Light calculateInternal(Instance instance, int chunkX, int sectionY, int chunkZ) {
Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk == null) {
this.toUpdateSet = Set.of();
return this;
}
this.isValidBorders = true;
Set<Point> toUpdate = new HashSet<>();
// Update single section with base lighting changes
Block[] blocks = new Block[SECTION_SIZE * SECTION_SIZE * SECTION_SIZE];
IntArrayFIFOQueue queue = buildInternalQueue(blockPalette, blocks);
Result result = LightCompute.compute(blocks, queue);
this.content = result.light();
this.borders = result.borders();
// Propagate changes to neighbors and self
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
Chunk neighborChunk = instance.getChunk(chunkX + i, chunkZ + j);
if (neighborChunk == null) continue;
for (int k = -1; k <= 1; k++) {
Vec neighborPos = new Vec(chunkX + i, sectionY + k, chunkZ + j);
if (neighborPos.blockY() >= neighborChunk.getMinSection() && neighborPos.blockY() < neighborChunk.getMaxSection()) {
toUpdate.add(new Vec(neighborChunk.getChunkX(), neighborPos.blockY(), neighborChunk.getChunkZ()));
neighborChunk.getSection(neighborPos.blockY()).blockLight().invalidatePropagation();
}
}
}
}
toUpdate.add(new Vec(chunk.getChunkX(), sectionY, chunk.getChunkZ()));
this.toUpdateSet = toUpdate;
return this;
}
@Override
public void invalidate() {
invalidatePropagation();
}
@Override
public boolean requiresUpdate() {
return !isValidBorders;
}
@Override
public void set(byte[] copyArray) {
this.content = copyArray.clone();
}
@Override
public boolean requiresSend() {
boolean res = needsSend;
needsSend = false;
return res;
}
private void clearCache() {
this.contentPropagation = null;
this.bordersPropagation = null;
isValidBorders = true;
needsSend = true;
}
@Override
public byte[] array() {
if (content == null) return new byte[0];
if (contentPropagation == null) return content;
var res = bake(contentPropagation, content);
if (res == emptyContent) return new byte[0];
return res;
}
private boolean compareBorders(byte[] a, byte[] b) {
if (b == null && a == null) return true;
if (b == null || a == null) return false;
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] > b[i]) return false;
}
return true;
}
private Block[] blocks() {
Block[] blocks = new Block[SECTION_SIZE * SECTION_SIZE * SECTION_SIZE];
blockPalette.getAllPresent((x, y, z, stateId) -> {
final Block block = Block.fromStateId((short) stateId);
assert block != null;
final int index = x | (z << 4) | (y << 8);
blocks[index] = block;
});
return blocks;
}
@Override
public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) {
if (!isValidBorders) clearCache();
Map<BlockFace, Point> neighbors = Light.getNeighbors(chunk, sectionY);
Block[] blocks = blocks();
IntArrayFIFOQueue queue = buildExternalQueue(instance, blocks, neighbors, borders);
LightCompute.Result result = LightCompute.compute(blocks, queue);
byte[] contentPropagationTemp = result.light();
byte[][] borderTemp = result.borders();
this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp);
this.bordersPropagationSwap = combineBorders(bordersPropagation, borderTemp);
Set<Point> toUpdate = new HashSet<>();
// Propagate changes to neighbors and self
for (var entry : neighbors.entrySet()) {
var neighbor = entry.getValue();
var face = entry.getKey();
byte[] next = borderTemp[face.ordinal()];
byte[] current = getBorderPropagation(face);
if (!compareBorders(next, current)) {
toUpdate.add(neighbor);
}
}
this.toUpdateSet = toUpdate;
return this;
}
private byte[][] combineBorders(byte[][] b1, byte[][] b2) {
if (b1 == null) return b2;
byte[][] newBorder = new byte[FACES.length][];
Arrays.setAll(newBorder, i -> new byte[SIDE_LENGTH]);
for (int i = 0; i < FACES.length; i++) {
newBorder[i] = combineBorders(b1[i], b2[i]);
}
return newBorder;
}
private byte[] bake(byte[] content1, byte[] content2) {
if (content1 == null && content2 == null) return emptyContent;
if (content1 == emptyContent && content2 == emptyContent) return emptyContent;
if (content1 == null) return content2;
if (content2 == null) return content1;
byte[] lightMax = new byte[LIGHT_LENGTH];
for (int i = 0; i < content1.length; i++) {
// Lower
byte l1 = (byte) (content1[i] & 0x0F);
byte l2 = (byte) (content2[i] & 0x0F);
// Upper
byte u1 = (byte) ((content1[i] >> 4) & 0x0F);
byte u2 = (byte) ((content2[i] >> 4) & 0x0F);
byte lower = (byte) Math.max(l1, l2);
byte upper = (byte) Math.max(u1, u2);
lightMax[i] = (byte) (lower | (upper << 4));
}
return lightMax;
}
@Override
public byte[] getBorderPropagation(BlockFace face) {
if (!isValidBorders) clearCache();
if (borders == null && bordersPropagation == null) return new byte[SIDE_LENGTH];
if (borders == null) return bordersPropagation[face.ordinal()];
if (bordersPropagation == null) return borders[face.ordinal()];
return combineBorders(bordersPropagation[face.ordinal()], borders[face.ordinal()]);
}
@Override
public void invalidatePropagation() {
this.isValidBorders = false;
this.needsSend = false;
this.bordersPropagation = null;
this.contentPropagation = null;
}
@Override
public int getLevel(int x, int y, int z) {
var array = array();
int index = x | (z << 4) | (y << 8);
return LightCompute.getLight(array, index);
}
private byte[] combineBorders(byte[] b1, byte[] b2) {
byte[] newBorder = new byte[SIDE_LENGTH];
for (int i = 0; i < newBorder.length; i++) {
var previous = b2[i];
var current = b1[i];
newBorder[i] = (byte) Math.max(previous, current);
}
return newBorder;
}
}

View File

@ -0,0 +1,78 @@
package net.minestom.server.instance.light;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.block.BlockFace;
import net.minestom.server.instance.palette.Palette;
import net.minestom.server.utils.Direction;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public interface Light {
static Light sky(@NotNull Palette blockPalette) {
return new SkyLight(blockPalette);
}
static Light block(@NotNull Palette blockPalette) {
return new BlockLight(blockPalette);
}
boolean requiresSend();
@ApiStatus.Internal
byte[] array();
Set<Point> flip();
void copyFrom(byte @NotNull [] array);
@ApiStatus.Internal
Light calculateExternal(Instance instance, Chunk chunk, int sectionY);
@ApiStatus.Internal
byte[] getBorderPropagation(BlockFace oppositeFace);
@ApiStatus.Internal
void invalidatePropagation();
int getLevel(int x, int y, int z);
@ApiStatus.Internal
Light calculateInternal(Instance instance, int chunkX, int chunkY, int chunkZ);
void invalidate();
boolean requiresUpdate();
void set(byte[] copyArray);
@ApiStatus.Internal
static Map<BlockFace, Point> getNeighbors(Chunk chunk, int sectionY) {
int chunkX = chunk.getChunkX();
int chunkZ = chunk.getChunkZ();
Map<BlockFace, Point> links = new HashMap<>();
for (BlockFace face : BlockFace.values()) {
Direction direction = face.toDirection();
int x = chunkX + direction.normalX();
int z = chunkZ + direction.normalZ();
int y = sectionY + direction.normalY();
Chunk foundChunk = chunk.getInstance().getChunk(x, z);
if (foundChunk == null) continue;
if (y - foundChunk.getMinSection() > foundChunk.getMaxSection() || y - foundChunk.getMinSection() < 0) continue;
links.put(face, new Vec(foundChunk.getChunkX(), y, foundChunk.getChunkZ()));
}
return links;
}
}

View File

@ -0,0 +1,117 @@
package net.minestom.server.instance.light;
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockFace;
import net.minestom.server.instance.palette.Palette;
import net.minestom.server.utils.Direction;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedList;
import java.util.Objects;
import static net.minestom.server.instance.light.BlockLight.buildInternalQueue;
final class LightCompute {
static final BlockFace[] FACES = BlockFace.values();
static final int LIGHT_LENGTH = 16 * 16 * 16 / 2;
static final int SIDE_LENGTH = 16 * 16;
static final int SECTION_SIZE = 16;
private static final byte[][] emptyBorders = new byte[FACES.length][SIDE_LENGTH];
static final byte[] emptyContent = new byte[LIGHT_LENGTH];
static @NotNull Result compute(Palette blockPalette) {
Block[] blocks = new Block[4096];
return LightCompute.compute(blocks, buildInternalQueue(blockPalette, blocks));
}
static @NotNull Result compute(Block[] blocks, IntArrayFIFOQueue lightPre) {
if (lightPre.isEmpty()) {
return new Result(emptyContent, emptyBorders);
}
byte[][] borders = new byte[FACES.length][SIDE_LENGTH];
byte[] lightArray = new byte[LIGHT_LENGTH];
var lightSources = new LinkedList<Integer>();
while (!lightPre.isEmpty()) {
int index = lightPre.dequeueInt();
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 oldLightLevel = getLight(lightArray, newIndex);
if (oldLightLevel < newLightLevel) {
placeLight(lightArray, newIndex, newLightLevel);
lightSources.add(index);
}
}
while (!lightSources.isEmpty()) {
final int index = lightSources.poll();
final int x = index & 15;
final int z = (index >> 4) & 15;
final int y = (index >> 8) & 15;
final int lightLevel = (index >> 12) & 15;
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);
// Handler border
if (xO < 0 || xO >= SECTION_SIZE || yO < 0 || yO >= SECTION_SIZE || zO < 0 || zO >= SECTION_SIZE) {
final byte[] border = borders[face.ordinal()];
final int borderIndex = switch (face) {
case WEST, EAST -> y * SECTION_SIZE + z;
case BOTTOM, TOP -> x * SECTION_SIZE + z;
case NORTH, SOUTH -> x * SECTION_SIZE + y;
};
border[borderIndex] = newLightLevel;
continue;
}
// Section
final int newIndex = xO | (zO << 4) | (yO << 8);
if (getLight(lightArray, newIndex) + 2 <= lightLevel) {
final Block currentBlock = Objects.requireNonNullElse(blocks[x | (z << 4) | (y << 8)], Block.AIR);
final Block propagatedBlock = Objects.requireNonNullElse(blocks[newIndex], Block.AIR);
boolean airAir = currentBlock.isAir() && propagatedBlock.isAir();
if (!airAir && currentBlock.registry().collisionShape().isOccluded(propagatedBlock.registry().collisionShape(), face)) continue;
placeLight(lightArray, newIndex, newLightLevel);
lightSources.add(newIndex | (newLightLevel << 12));
}
}
}
return new Result(lightArray, borders);
}
record Result(byte[] light, byte[][] borders) {
Result {
assert light.length == LIGHT_LENGTH : "Only 16x16x16 sections are supported: " + light.length;
}
public byte getLight(int x, int y, int z) {
return (byte) LightCompute.getLight(light, x | (z << 4) | (y << 8));
}
}
private static void placeLight(byte[] light, int index, int value) {
final int shift = (index & 1) << 2;
final int i = index >>> 1;
light[i] = (byte) ((light[i] & (0xF0 >>> shift)) | (value << shift));
}
static int getLight(byte[] light, int index) {
if (index >>> 1 >= light.length) return 0;
final int value = light[index >>> 1];
return ((value >>> ((index & 1) << 2)) & 0xF);
}
}

View File

@ -0,0 +1,398 @@
package net.minestom.server.instance.light;
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Chunk;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.LightingChunk;
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;
import java.util.Map;
import java.util.Set;
import static net.minestom.server.instance.light.LightCompute.*;
final class SkyLight implements Light {
private final Palette blockPalette;
private byte[] content;
private byte[] contentPropagation;
private byte[] contentPropagationSwap;
private byte[][] borders;
private byte[][] bordersPropagation;
private byte[][] bordersPropagationSwap;
private boolean isValidBorders = true;
private boolean needsSend = true;
private Set<Point> toUpdateSet = new HashSet<>();
private boolean fullyLit = false;
private static final byte[][] bordersFullyLit = new byte[6][SIDE_LENGTH];
private static final byte[] contentFullyLit = new byte[LIGHT_LENGTH];
static {
Arrays.fill(contentFullyLit, (byte) -1);
for (byte[] border : bordersFullyLit) {
Arrays.fill(border, (byte) 14);
}
}
SkyLight(Palette blockPalette) {
this.blockPalette = blockPalette;
}
@Override
public Set<Point> flip() {
if (this.bordersPropagationSwap != null)
this.bordersPropagation = this.bordersPropagationSwap;
if (this.contentPropagationSwap != null)
this.contentPropagation = this.contentPropagationSwap;
this.bordersPropagationSwap = null;
this.contentPropagationSwap = null;
if (toUpdateSet == null) return Set.of();
return toUpdateSet;
}
static IntArrayFIFOQueue buildInternalQueue(Chunk c, int sectionY) {
IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue();
if (c instanceof LightingChunk lc) {
int[] heightmap = lc.calculateHeightMap();
int maxY = c.getInstance().getDimensionType().getMinY() + c.getInstance().getDimensionType().getHeight();
int sectionMaxY = (sectionY + 1) * 16 - 1;
int sectionMinY = sectionY * 16;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int height = heightmap[z << 4 | x];
for (int y = Math.min(sectionMaxY, maxY); y >= Math.max(height, sectionMinY); y--) {
int index = x | (z << 4) | ((y % 16) << 8);
lightSources.enqueue(index | (15 << 12));
}
}
}
}
return lightSources;
}
private static Block getBlock(Palette palette, int x, int y, int z) {
return Block.fromStateId((short)palette.get(x, y, z));
}
private static IntArrayFIFOQueue buildExternalQueue(Instance instance, Block[] blocks, Map<BlockFace, Point> neighbors, byte[][] borders) {
IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue();
for (BlockFace face : BlockFace.values()) {
Point neighborSection = neighbors.get(face);
if (neighborSection == null) continue;
Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ());
if (chunk == null) continue;
byte[] neighborFace = chunk.getSection(neighborSection.blockY()).skyLight().getBorderPropagation(face.getOppositeFace());
if (neighborFace == null) continue;
for (int bx = 0; bx < 16; bx++) {
for (int by = 0; by < 16; by++) {
final int borderIndex = bx * SECTION_SIZE + by;
byte lightEmission = neighborFace[borderIndex];
if (borders != null && borders[face.ordinal()] != null) {
final int internalEmission = borders[face.ordinal()][borderIndex];
if (lightEmission <= internalEmission) continue;
}
if (borders != null && borders[face.ordinal()] != null) {
final int internalEmission = borders[face.ordinal()][borderIndex];
if (lightEmission <= internalEmission) continue;
}
final int k = switch (face) {
case WEST, BOTTOM, NORTH -> 0;
case EAST, TOP, SOUTH -> 15;
};
final int posTo = switch (face) {
case NORTH, SOUTH -> bx | (k << 4) | (by << 8);
case WEST, EAST -> k | (by << 4) | (bx << 8);
default -> bx | (by << 4) | (k << 8);
};
Section otherSection = chunk.getSection(neighborSection.blockY());
final Block blockFrom = (switch (face) {
case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k);
case WEST, EAST -> getBlock(otherSection.blockPalette(), 15 - k, bx, by);
default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by);
});
if (blocks == null) continue;
Block blockTo = blocks[posTo];
if (blockTo == null && blockFrom != null) {
if (blockFrom.registry().collisionShape().isOccluded(Block.AIR.registry().collisionShape(), face.getOppositeFace()))
continue;
} else if (blockTo != null && blockFrom == null) {
if (Block.AIR.registry().collisionShape().isOccluded(blockTo.registry().collisionShape(), face))
continue;
} else if (blockTo != null && blockFrom != null) {
if (blockFrom.registry().collisionShape().isOccluded(blockTo.registry().collisionShape(), face.getOppositeFace()))
continue;
}
final int index = posTo | (lightEmission << 12);
if (lightEmission > 0) {
lightSources.enqueue(index);
}
}
}
}
return lightSources;
}
@Override
public void copyFrom(byte @NotNull [] array) {
if (array.length == 0) this.content = null;
else this.content = array.clone();
}
@Override
public Light calculateInternal(Instance instance, int chunkX, int sectionY, int chunkZ) {
Chunk chunk = instance.getChunk(chunkX, chunkZ);
this.isValidBorders = true;
// Update single section with base lighting changes
Block[] blocks = blocks();
int queueSize = SECTION_SIZE * SECTION_SIZE * SECTION_SIZE;
IntArrayFIFOQueue queue = new IntArrayFIFOQueue(0);
if (!fullyLit) {
queue = buildInternalQueue(chunk, sectionY);
queueSize = queue.size();
}
if (queueSize == SECTION_SIZE * SECTION_SIZE * SECTION_SIZE) {
this.fullyLit = true;
this.content = contentFullyLit;
this.borders = bordersFullyLit;
} else {
Result result = LightCompute.compute(blocks, queue);
this.content = result.light();
this.borders = result.borders();
}
Set<Point> toUpdate = new HashSet<>();
// Propagate changes to neighbors and self
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
Chunk neighborChunk = instance.getChunk(chunkX + i, chunkZ + j);
if (neighborChunk == null) continue;
for (int k = -1; k <= 1; k++) {
Vec neighborPos = new Vec(chunkX + i, sectionY + k, chunkZ + j);
if (neighborPos.blockY() >= neighborChunk.getMinSection() && neighborPos.blockY() < neighborChunk.getMaxSection()) {
toUpdate.add(new Vec(neighborChunk.getChunkX(), neighborPos.blockY(), neighborChunk.getChunkZ()));
neighborChunk.getSection(neighborPos.blockY()).skyLight().invalidatePropagation();
}
}
}
}
toUpdate.add(new Vec(chunk.getChunkX(), sectionY, chunk.getChunkZ()));
this.toUpdateSet = toUpdate;
return this;
}
@Override
public void invalidate() {
invalidatePropagation();
}
@Override
public boolean requiresUpdate() {
return !isValidBorders;
}
@Override
public void set(byte[] copyArray) {
this.content = copyArray.clone();
}
@Override
public boolean requiresSend() {
boolean res = needsSend;
needsSend = false;
return res;
}
private void clearCache() {
this.contentPropagation = null;
this.bordersPropagation = null;
isValidBorders = true;
needsSend = true;
fullyLit = false;
}
@Override
public byte[] array() {
if (content == null) return new byte[0];
if (contentPropagation == null) return content;
var res = bake(contentPropagation, content);
if (res == emptyContent) return new byte[0];
return res;
}
private boolean compareBorders(byte[] a, byte[] b) {
if (b == null && a == null) return true;
if (b == null || a == null) return false;
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] > b[i]) return false;
}
return true;
}
private Block[] blocks() {
Block[] blocks = new Block[SECTION_SIZE * SECTION_SIZE * SECTION_SIZE];
blockPalette.getAllPresent((x, y, z, stateId) -> {
final Block block = Block.fromStateId((short) stateId);
assert block != null;
final int index = x | (z << 4) | (y << 8);
blocks[index] = block;
});
return blocks;
}
@Override
public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) {
if (!isValidBorders) clearCache();
Map<BlockFace, Point> neighbors = Light.getNeighbors(chunk, sectionY);
Set<Point> toUpdate = new HashSet<>();
Block[] blocks = blocks();
IntArrayFIFOQueue queue;
byte[][] borderTemp = bordersFullyLit;
if (!fullyLit) {
queue = buildExternalQueue(instance, blocks, neighbors, borders);
LightCompute.Result result = LightCompute.compute(blocks, queue);
byte[] contentPropagationTemp = result.light();
borderTemp = result.borders();
this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp);
this.bordersPropagationSwap = combineBorders(bordersPropagation, borderTemp);
} else {
this.contentPropagationSwap = null;
this.bordersPropagationSwap = null;
}
// Propagate changes to neighbors and self
for (var entry : neighbors.entrySet()) {
var neighbor = entry.getValue();
var face = entry.getKey();
byte[] next = borderTemp[face.ordinal()];
byte[] current = getBorderPropagation(face);
if (!compareBorders(next, current)) {
toUpdate.add(neighbor);
}
}
this.toUpdateSet = toUpdate;
return this;
}
private byte[][] combineBorders(byte[][] b1, byte[][] b2) {
if (b1 == null) return b2;
byte[][] newBorder = new byte[FACES.length][];
Arrays.setAll(newBorder, i -> new byte[SIDE_LENGTH]);
for (int i = 0; i < FACES.length; i++) {
newBorder[i] = combineBorders(b1[i], b2[i]);
}
return newBorder;
}
private byte[] bake(byte[] content1, byte[] content2) {
if (content1 == null && content2 == null) return emptyContent;
if (content1 == emptyContent && content2 == emptyContent) return emptyContent;
if (content1 == null) return content2;
if (content2 == null) return content1;
byte[] lightMax = new byte[LIGHT_LENGTH];
for (int i = 0; i < content1.length; i++) {
// Lower
byte l1 = (byte) (content1[i] & 0x0F);
byte l2 = (byte) (content2[i] & 0x0F);
// Upper
byte u1 = (byte) ((content1[i] >> 4) & 0x0F);
byte u2 = (byte) ((content2[i] >> 4) & 0x0F);
byte lower = (byte) Math.max(l1, l2);
byte upper = (byte) Math.max(u1, u2);
lightMax[i] = (byte) (lower | (upper << 4));
}
return lightMax;
}
@Override
public byte[] getBorderPropagation(BlockFace face) {
if (!isValidBorders) clearCache();
if (borders == null && bordersPropagation == null) return new byte[SIDE_LENGTH];
if (borders == null) return bordersPropagation[face.ordinal()];
if (bordersPropagation == null) return borders[face.ordinal()];
return combineBorders(bordersPropagation[face.ordinal()], borders[face.ordinal()]);
}
@Override
public void invalidatePropagation() {
this.isValidBorders = false;
this.needsSend = false;
this.bordersPropagation = null;
this.contentPropagation = null;
}
@Override
public int getLevel(int x, int y, int z) {
var array = array();
int index = x | (z << 4) | (y << 8);
return LightCompute.getLight(array, index);
}
private byte[] combineBorders(byte[] b1, byte[] b2) {
byte[] newBorder = new byte[SIDE_LENGTH];
for (int i = 0; i < newBorder.length; i++) {
var previous = b2[i];
var current = b1[i];
newBorder[i] = (byte) Math.max(previous, current);
}
return newBorder;
}
}

View File

@ -170,6 +170,8 @@ public final class Registry {
private final boolean air;
private final boolean solid;
private final boolean liquid;
private final boolean occludes;
private final int lightEmission;
private final String blockEntity;
private final int blockEntityId;
private final Supplier<Material> materialSupplier;
@ -189,7 +191,9 @@ public final class Registry {
this.jumpFactor = main.getDouble("jumpFactor", 1);
this.air = main.getBoolean("air", false);
this.solid = main.getBoolean("solid");
this.occludes = main.getBoolean("occludes", true);
this.liquid = main.getBoolean("liquid", false);
this.lightEmission = main.getInt("lightEmission", 0);
{
Properties blockEntity = main.section("blockEntity");
if (blockEntity != null) {
@ -205,8 +209,9 @@ public final class Registry {
this.materialSupplier = materialNamespace != null ? () -> Material.fromNamespaceId(materialNamespace) : () -> null;
}
{
final String string = main.getString("collisionShape");
this.shape = CollisionUtils.parseBlockShape(string, this);
final String collision = main.getString("collisionShape");
final String occlusion = main.getString("occlusionShape");
this.shape = CollisionUtils.parseBlockShape(collision, occlusion, this);
}
}
@ -254,10 +259,18 @@ public final class Registry {
return solid;
}
public boolean occludes() {
return occludes;
}
public boolean isLiquid() {
return liquid;
}
public int lightEmission() {
return lightEmission;
}
public boolean isBlockEntity() {
return blockEntity != null;
}

View File

@ -0,0 +1,564 @@
package net.minestom.server.instance;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.Map.entry;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@EnvTest
public class BlockLightMergeIntegrationTest {
@Test
public void testPropagationAir(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(8, 100,8 , Block.TORCH);
Map<Vec, Integer> expectedLights = new HashMap<>();
for (int y = -15; y <= 15; ++y) {
expectedLights.put(new Vec(8, 100 + y, 8), Math.max(0, 14 - Math.abs(y)));
}
LightingChunk.relightSection(instance, 0, 6, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testTorch(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
instance.setGenerator(unit -> {
unit.modifier().fillHeight(39, 40, Block.STONE);
unit.modifier().fillHeight(50, 51, Block.STONE);
});
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(1, 40,1 , Block.TORCH);
Map<Vec, Integer> expectedLights = Map.ofEntries(
entry(new Vec(2, 40, 2), 12)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testTorch2(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(1, 40,1 , Block.TORCH);
Map<Vec, Integer> expectedLights = Map.ofEntries(
entry(new Vec(2, 40, 2), 12)
);
LightingChunk.relightSection(instance, 1, 2, 1);
assertLightInstance(instance, expectedLights);
instance.setBlock(-2, 40,-2, Block.TORCH);
expectedLights = Map.ofEntries(
entry(new Vec(2, 40, 2), 12)
);
LightingChunk.relightSection(instance, -1, 2, -1);
assertLightInstance(instance, expectedLights);
}
@Test
public void testPropagationAir2(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(4, 60,8 , Block.TORCH);
Map<Vec, Integer> expectedLights = new HashMap<>();
for (int y = -15; y <= 15; ++y) {
expectedLights.put(new Vec(8, 60 + y, 8), Math.max(0, 10 - Math.abs(y)));
}
for (int y = -15; y <= 15; ++y) {
expectedLights.put(new Vec(-2, 60 + y, 8), Math.max(0, 8 - Math.abs(y)));
}
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testPropagationAirRemoval(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(4, 100,8 , Block.TORCH);
LightingChunk.relightSection(instance, 0, 2, 0);
instance.setBlock(4, 100,8 , Block.AIR);
Map<Vec, Integer> expectedLights = new HashMap<>();
for (int y = -15; y <= 15; ++y) {
expectedLights.put(new Vec(8, 100 + y, 8), 0);
}
for (int y = -15; y <= 15; ++y) {
expectedLights.put(new Vec(-2, 100 + y, 8), 0);
}
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testBorderOcclusion(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(-1, 40, 4, Block.MAGMA_BLOCK);
instance.setBlock(-1, 40, 3, Block.MAGMA_BLOCK);
instance.setBlock(-2, 40, 3, Block.MAGMA_BLOCK);
instance.setBlock(-3, 40, 3, Block.MAGMA_BLOCK);
instance.setBlock(-3, 40, 4, Block.MAGMA_BLOCK);
instance.setBlock(-3, 40, 5, Block.MAGMA_BLOCK);
instance.setBlock(-2, 40, 5, Block.MAGMA_BLOCK);
instance.setBlock(-1, 40, 5, Block.MAGMA_BLOCK);
instance.setBlock(-2, 41, 4, Block.STONE);
instance.setBlock(-2, 40, 4, Block.TORCH);
Map<Vec, Integer> expectedLights = Map.ofEntries(
entry(new Vec(-2, 42, 4), 0),
entry(new Vec(-2, 42, 3), 1),
entry(new Vec(-2, 41, 3), 2),
entry(new Vec(0, 40, 4), 2)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testBorderOcclusion2(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(-1, 41, 4, Block.MAGMA_BLOCK);
instance.setBlock(-1, 40, 3, Block.MAGMA_BLOCK);
instance.setBlock(-2, 40, 3, Block.MAGMA_BLOCK);
instance.setBlock(-3, 40, 3, Block.MAGMA_BLOCK);
instance.setBlock(-3, 40, 4, Block.MAGMA_BLOCK);
instance.setBlock(-3, 40, 5, Block.MAGMA_BLOCK);
instance.setBlock(-2, 40, 5, Block.MAGMA_BLOCK);
instance.setBlock(-1, 40, 5, Block.MAGMA_BLOCK);
instance.setBlock(-2, 41, 4, Block.STONE);
instance.setBlock(-2, 40, 4, Block.TORCH);
Map<Vec, Integer> expectedLights = Map.ofEntries(
entry(new Vec(-2, 42, 4), 8),
entry(new Vec(-2, 40, 2), 8),
entry(new Vec(-4, 40, 4), 4)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testBorderOcclusion3(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(0, 40, 8, Block.STONE);
instance.setBlock(1, 40, 8, Block.STONE);
instance.setBlock(0, 41, 7, Block.STONE);
instance.setBlock(1, 41, 7, Block.STONE);
instance.setBlock(2, 40, 7, Block.STONE);
instance.setBlock(1, 40, 6, Block.STONE);
instance.setBlock(0, 40, 6, Block.STONE);
instance.setBlock(1, 40, 7, Block.TORCH);
instance.setBlock(0, 40, 7, Block.SANDSTONE_SLAB.withProperty("type", "bottom"));
instance.setBlock(-1, 40, 7, Block.SANDSTONE_SLAB.withProperty("type", "top"));
Map<Vec, Integer> expectedLights = Map.ofEntries(
entry(new Vec(-2, 40, 7), 0)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testBorderCrossing(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
for (int x = -2; x <= 1; ++x) {
for (int z = 5; z <= 20; ++z) {
instance.setBlock(x, 42, z, Block.STONE);
}
}
for (int z = 5; z <= 20; ++z) {
for (int y = 40; y <= 42; ++y) {
instance.setBlock(1, y, z, Block.STONE);
instance.setBlock(-2, y, z, Block.STONE);
}
}
for (int y = 40; y <= 42; ++y) {
instance.setBlock(-1, y, 6, Block.STONE);
instance.setBlock(0, y, 8, Block.STONE);
instance.setBlock(-1, y, 10, Block.STONE);
instance.setBlock(0, y, 12, Block.STONE);
instance.setBlock(-1, y, 14, Block.STONE);
instance.setBlock(0, y, 16, Block.STONE);
instance.setBlock(-1, y, 18, Block.STONE);
instance.setBlock(0, y, 20, Block.STONE);
}
instance.setBlock(-1, 40, 11, Block.TORCH);
Map<Vec, Integer> expectedLights = Map.ofEntries(
entry(new Vec(-1, 40, 19), 2),
entry(new Vec(0, 40, 19), 3),
entry(new Vec(-1, 40, 16), 7),
entry(new Vec(-1, 40, 13), 12),
entry(new Vec(-1, 40, 7), 8),
entry(new Vec(-3, 40, 4), 1),
entry(new Vec(-3, 40, 5), 0),
entry(new Vec(-1, 40, 20), 1)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testBorderOcclusionRemoval(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(-1, 41, 4, Block.MAGMA_BLOCK);
instance.setBlock(-1, 40, 3, Block.MAGMA_BLOCK);
instance.setBlock(-2, 40, 3, Block.MAGMA_BLOCK);
instance.setBlock(-3, 40, 3, Block.MAGMA_BLOCK);
instance.setBlock(-3, 40, 4, Block.MAGMA_BLOCK);
instance.setBlock(-3, 40, 5, Block.MAGMA_BLOCK);
instance.setBlock(-2, 40, 5, Block.MAGMA_BLOCK);
instance.setBlock(-1, 40, 5, Block.MAGMA_BLOCK);
instance.setBlock(-2, 41, 4, Block.STONE);
instance.setBlock(-2, 40, 4, Block.TORCH);
LightingChunk.relightSection(instance, 0, 2, 0);
instance.setBlock(-2, 40, 4, Block.STONE);
Map<Vec, Integer> expectedLights = Map.ofEntries(
entry(new Vec(-2, 42, 4), 1),
entry(new Vec(-2, 40, 2), 2),
entry(new Vec(-4, 40, 4), 2)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void chunkIntersection(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = 4; x <= 7; x++) {
for (int z = 6; z <= 8; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(94, -35, 128, Block.GLOW_LICHEN.withProperties(Map.of("west", "true")));
LightingChunk.relight(instance, instance.getChunks());
var val = instance.getChunk(5, 8).getSection(-2).blockLight().getLevel(14, 0, 0);
assertEquals(4, val);
var val2 = instance.getChunk(5, 8).getSection(-3).blockLight().getLevel(14, 15, 0);
assertEquals(5, val2);
}
@Test
public void skylight(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = 4; x <= 7; x++) {
for (int z = 6; z <= 8; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(94, 50, 128, Block.STONE);
LightingChunk.relight(instance, instance.getChunks());
var val = lightValSky(instance, new Vec(94, 41, 128));
assertEquals(14, val);
}
@Test
public void skylightContained(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = 4; x <= 7; x++) {
for (int z = 6; z <= 8; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(94, 50, 128, Block.STONE);
instance.setBlock(94, 52, 128, Block.STONE);
instance.setBlock(94, 51, 127, Block.STONE);
instance.setBlock(94, 51, 129, Block.STONE);
instance.setBlock(93, 51, 128, Block.STONE);
instance.setBlock(95, 51, 128, Block.STONE);
LightingChunk.relight(instance, instance.getChunks());
var val = lightValSky(instance, new Vec(94, 51, 128));
var val2 = lightValSky(instance, new Vec(94, 52, 128));
assertEquals(0, val2);
assertEquals(0, val);
}
@Test
public void testDiagonalRemoval(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(-2, 40, 14, Block.TORCH);
Map<Vec, Integer> expectedLights = Map.ofEntries(
entry(new Vec(-2, 40, 14), 14),
entry(new Vec(-2, 40, 18), 10),
entry(new Vec(2, 40, 18), 6)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
instance.setBlock(-2, 40, 14, Block.AIR);
expectedLights = Map.ofEntries(
entry(new Vec(-2, 40, 14), 0),
entry(new Vec(-2, 40, 18), 0),
entry(new Vec(2, 40, 18), 0)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testDiagonalRemoval2(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(1, 40, 1, Block.TORCH);
instance.setBlock(1, 40, 17, Block.TORCH);
LightingChunk.relightSection(instance, 0, 2, 0);
instance.setBlock(1, 40, 17, Block.AIR);
var expectedLights = Map.ofEntries(
entry(new Vec(-3, 40, 2), 9)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testDouble(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(-2, 40, 14, Block.TORCH);
instance.setBlock(1, 40, 27, Block.TORCH);
var expectedLights = Map.ofEntries(
entry(new Vec(-4, 40, 25), 7),
entry(new Vec(-4, 40, 18), 8)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
instance.setBlock(-2, 40, 14, Block.AIR);
expectedLights = Map.ofEntries(
entry(new Vec(-4, 40, 25), 7),
entry(new Vec(-4, 40, 18), 0)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
@Test
public void testBlockRemoval(Env env) {
Instance instance = env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
instance.setBlock(0, 40, 0, Block.STONE);
instance.setBlock(1, 40, -1, Block.STONE);
instance.setBlock(0, 40, -2, Block.STONE);
instance.setBlock(-1, 40, -1, Block.STONE);
instance.setBlock(0, 41, -1, Block.STONE);
instance.setBlock(0, 40, -1, Block.GLOWSTONE);
var expectedLights = Map.ofEntries(
entry(new Vec(-2, 40, -1), 0)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
instance.setBlock(-1, 40, -1, Block.AIR);
expectedLights = Map.ofEntries(
entry(new Vec(-2, 40, -1), 13)
);
LightingChunk.relightSection(instance, 0, 2, 0);
assertLightInstance(instance, expectedLights);
}
static byte lightVal(Instance instance, Vec pos) {
final Vec modPos = new Vec(((pos.blockX() % 16) + 16) % 16, ((pos.blockY() % 16) + 16) % 16, ((pos.blockZ() % 16) + 16) % 16);
Chunk chunk = instance.getChunkAt(pos.blockX(), pos.blockZ());
return (byte) chunk.getSectionAt(pos.blockY()).blockLight().getLevel(modPos.blockX(), modPos.blockY(), modPos.blockZ());
}
static byte lightValSky(Instance instance, Vec pos) {
final Vec modPos = new Vec(((pos.blockX() % 16) + 16) % 16, ((pos.blockY() % 16) + 16) % 16, ((pos.blockZ() % 16) + 16) % 16);
Chunk chunk = instance.getChunkAt(pos.blockX(), pos.blockZ());
return (byte) chunk.getSectionAt(pos.blockY()).skyLight().getLevel(modPos.blockX(), modPos.blockY(), modPos.blockZ());
}
public static void assertLightInstance(Instance instance, Map<Vec, Integer> expectedLights) {
List<String> errors = new ArrayList<>();
for (var entry : expectedLights.entrySet()) {
final Integer expected = entry.getValue();
final Vec pos = entry.getKey();
final byte light = lightVal(instance, pos);
if (light != expected) {
String errorLine = String.format("Expected %d at [%d,%d,%d] but got %d", expected, pos.blockX(), pos.blockY(), pos.blockZ(), light);
System.err.println();
errors.add(errorLine);
}
}
if (!errors.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (String s : errors) {
sb.append(s).append("\n");
}
System.err.println(sb);
fail();
}
}
}

View File

@ -0,0 +1,204 @@
package net.minestom.server.instance.light;
import net.minestom.server.collision.Shape;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockFace;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class BlockIsOccludedTest {
@Test
public void blockAir() {
Shape airBlock = Block.AIR.registry().collisionShape();
for (BlockFace face : BlockFace.values()) {
assertFalse(airBlock.isOccluded(airBlock, face));
}
}
@Test
public void blockLantern() {
Shape shape = Block.LANTERN.registry().collisionShape();
Shape airBlock = Block.AIR.registry().collisionShape();
for (BlockFace face : BlockFace.values()) {
assertFalse(shape.isOccluded(airBlock, face));
}
}
@Test
public void blockSpruceLeaves() {
Shape shape = Block.SPRUCE_LEAVES.registry().collisionShape();
Shape airBlock = Block.AIR.registry().collisionShape();
for (BlockFace face : BlockFace.values()) {
assertFalse(shape.isOccluded(airBlock, face));
}
}
@Test
public void blockCauldron() {
Shape shape = Block.CAULDRON.registry().collisionShape();
Shape airBlock = Block.AIR.registry().collisionShape();
for (BlockFace face : BlockFace.values()) {
assertFalse(shape.isOccluded(airBlock, face));
}
}
@Test
public void blockSlabBottomAir() {
Shape shape = Block.SANDSTONE_SLAB.registry().collisionShape();
Shape airBlock = Block.AIR.registry().collisionShape();
assertTrue(shape.isOccluded(airBlock, BlockFace.BOTTOM));
assertFalse(shape.isOccluded(airBlock, BlockFace.NORTH));
assertFalse(shape.isOccluded(airBlock, BlockFace.SOUTH));
assertFalse(shape.isOccluded(airBlock, BlockFace.EAST));
assertFalse(shape.isOccluded(airBlock, BlockFace.WEST));
assertFalse(shape.isOccluded(airBlock, BlockFace.TOP));
}
@Test
public void blockSlabTopEnchantingTable() {
Shape shape1 = Block.SANDSTONE_SLAB.withProperty("type", "top").registry().collisionShape();
Shape shape2 = Block.ENCHANTING_TABLE.registry().collisionShape();
assertFalse(shape1.isOccluded(shape2, BlockFace.BOTTOM));
assertTrue(shape1.isOccluded(shape2, BlockFace.NORTH));
assertTrue(shape1.isOccluded(shape2, BlockFace.SOUTH));
assertTrue(shape1.isOccluded(shape2, BlockFace.EAST));
assertTrue(shape1.isOccluded(shape2, BlockFace.WEST));
assertTrue(shape1.isOccluded(shape2, BlockFace.TOP));
}
@Test
public void blockStairWest() {
Shape shape = Block.SANDSTONE_STAIRS.withProperties(Map.of(
"facing", "west",
"half", "bottom",
"shape", "straight")).registry().collisionShape();
Shape airBlock = Block.AIR.registry().collisionShape();
assertTrue(shape.isOccluded(airBlock, BlockFace.WEST));
assertTrue(shape.isOccluded(airBlock, BlockFace.BOTTOM));
assertFalse(shape.isOccluded(airBlock, BlockFace.SOUTH));
assertFalse(shape.isOccluded(airBlock, BlockFace.EAST));
assertFalse(shape.isOccluded(airBlock, BlockFace.NORTH));
assertFalse(shape.isOccluded(airBlock, BlockFace.TOP));
}
@Test
public void blockSlabBottomStone() {
Shape shape = Block.SANDSTONE_SLAB.registry().collisionShape();
Shape stoneBlock = Block.STONE.registry().collisionShape();
assertTrue(shape.isOccluded(stoneBlock, BlockFace.BOTTOM));
assertTrue(shape.isOccluded(stoneBlock, BlockFace.NORTH));
assertTrue(shape.isOccluded(stoneBlock, BlockFace.SOUTH));
assertTrue(shape.isOccluded(stoneBlock, BlockFace.EAST));
assertTrue(shape.isOccluded(stoneBlock, BlockFace.WEST));
assertTrue(shape.isOccluded(stoneBlock, BlockFace.TOP));
}
@Test
public void blockStone() {
Shape shape = Block.STONE.registry().collisionShape();
Shape airBlock = Block.AIR.registry().collisionShape();
for (BlockFace face : BlockFace.values()) {
assertTrue(shape.isOccluded(airBlock, face));
}
}
@Test
public void blockStair() {
Shape shape = Block.SANDSTONE_STAIRS.registry().collisionShape();
Shape airBlock = Block.AIR.registry().collisionShape();
assertTrue(shape.isOccluded(airBlock, BlockFace.NORTH));
assertTrue(shape.isOccluded(airBlock, BlockFace.BOTTOM));
assertFalse(shape.isOccluded(airBlock, BlockFace.SOUTH));
assertFalse(shape.isOccluded(airBlock, BlockFace.EAST));
assertFalse(shape.isOccluded(airBlock, BlockFace.WEST));
assertFalse(shape.isOccluded(airBlock, BlockFace.TOP));
}
@Test
public void blockSlab() {
Shape shape = Block.SANDSTONE_SLAB.registry().collisionShape();
Shape airBlock = Block.AIR.registry().collisionShape();
assertTrue(shape.isOccluded(airBlock, BlockFace.BOTTOM));
assertFalse(shape.isOccluded(airBlock, BlockFace.NORTH));
assertFalse(shape.isOccluded(airBlock, BlockFace.SOUTH));
assertFalse(shape.isOccluded(airBlock, BlockFace.EAST));
assertFalse(shape.isOccluded(airBlock, BlockFace.WEST));
assertFalse(shape.isOccluded(airBlock, BlockFace.TOP));
}
@Test
public void blockSlabBottomAndSlabTop() {
Shape shape1 = Block.SANDSTONE_SLAB.registry().collisionShape();
Shape shape2 = Block.SANDSTONE_SLAB.withProperty("type", "top").registry().collisionShape();
assertFalse(shape1.isOccluded(shape2, BlockFace.TOP));
assertTrue(shape1.isOccluded(shape2, BlockFace.BOTTOM));
assertTrue(shape1.isOccluded(shape2, BlockFace.EAST));
assertTrue(shape1.isOccluded(shape2, BlockFace.WEST));
assertTrue(shape1.isOccluded(shape2, BlockFace.NORTH));
assertTrue(shape1.isOccluded(shape2, BlockFace.SOUTH));
}
@Test
public void blockSlabBottomAndSlabBottom() {
Shape shape = Block.SANDSTONE_SLAB.registry().collisionShape();
assertTrue(shape.isOccluded(shape, BlockFace.BOTTOM));
assertTrue(shape.isOccluded(shape, BlockFace.TOP));
assertFalse(shape.isOccluded(shape, BlockFace.EAST));
assertFalse(shape.isOccluded(shape, BlockFace.WEST));
assertFalse(shape.isOccluded(shape, BlockFace.NORTH));
assertFalse(shape.isOccluded(shape, BlockFace.SOUTH));
}
@Test
public void blockStairAndSlabBottom() {
Shape shape1 = Block.STONE_STAIRS.registry().collisionShape();
Shape shape2 = Block.SANDSTONE_SLAB.registry().collisionShape();
assertTrue(shape1.isOccluded(shape2, BlockFace.BOTTOM));
assertTrue(shape1.isOccluded(shape2, BlockFace.NORTH));
assertTrue(shape1.isOccluded(shape2, BlockFace.TOP));
assertFalse(shape1.isOccluded(shape2, BlockFace.EAST));
assertFalse(shape1.isOccluded(shape2, BlockFace.WEST));
assertFalse(shape1.isOccluded(shape2, BlockFace.SOUTH));
}
@Test
public void blockStairAndSlabTop() {
Shape shape1 = Block.STONE_STAIRS.registry().collisionShape();
Shape shape2 = Block.SANDSTONE_SLAB.withProperty("type", "top").registry().collisionShape();
assertTrue(shape1.isOccluded(shape2, BlockFace.NORTH));
assertTrue(shape1.isOccluded(shape2, BlockFace.BOTTOM));
assertTrue(shape1.isOccluded(shape2, BlockFace.EAST));
assertTrue(shape1.isOccluded(shape2, BlockFace.WEST));
assertTrue(shape1.isOccluded(shape2, BlockFace.SOUTH));
assertFalse(shape1.isOccluded(shape2, BlockFace.TOP));
}
}

View File

@ -0,0 +1,239 @@
package net.minestom.server.instance.light;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.palette.Palette;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static java.util.Map.entry;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class BlockLightTest {
@Test
public void empty() {
var palette = Palette.blocks();
var result = LightCompute.compute(palette);
for (byte light : result.light()) {
assertEquals(0, light);
}
}
@Test
public void glowstone() {
var palette = Palette.blocks();
palette.set(0, 1, 0, Block.GLOWSTONE.stateId());
var result = LightCompute.compute(palette);
assertLight(result, Map.of(
new Vec(0, 1, 0), 15,
new Vec(0, 1, 1), 14,
new Vec(0, 1, 2), 13));
}
@Test
public void doubleGlowstone() {
var palette = Palette.blocks();
palette.set(0, 1, 0, Block.GLOWSTONE.stateId());
palette.set(4, 1, 4, Block.GLOWSTONE.stateId());
var result = LightCompute.compute(palette);
assertLight(result, Map.of(
new Vec(1, 1, 3), 11,
new Vec(3, 3, 7), 9,
new Vec(1, 1, 1), 13,
new Vec(3, 1, 4), 14));
}
@Test
public void glowstoneBorder() {
var palette = Palette.blocks();
palette.set(0, 1, 0, Block.GLOWSTONE.stateId());
var result = LightCompute.compute(palette);
assertLight(result, Map.of(
// X axis
new Vec(-1, 0, 0), 13,
new Vec(-1, 1, 0), 14,
new Vec(-1, 2, 0), 13,
new Vec(-1, 3, 0), 12,
// Z axis
new Vec(0, 0, -1), 13,
new Vec(0, 1, -1), 14,
new Vec(0, 2, -1), 13,
new Vec(0, 3, -1), 12));
}
@Test
public void glowstoneBlock() {
var palette = Palette.blocks();
palette.set(0, 1, 0, Block.GLOWSTONE.stateId());
palette.set(0, 1, 1, Block.STONE.stateId());
var result = LightCompute.compute(palette);
assertLight(result, Map.of(
new Vec(0, 1, 0), 15,
new Vec(0, 1, 1), 0,
new Vec(0, 1, 2), 11));
}
@Test
public void isolated() {
var palette = Palette.blocks();
palette.set(4, 1, 4, Block.GLOWSTONE.stateId());
palette.set(3, 1, 4, Block.STONE.stateId());
palette.set(4, 1, 5, Block.STONE.stateId());
palette.set(4, 1, 3, Block.STONE.stateId());
palette.set(5, 1, 4, Block.STONE.stateId());
palette.set(4, 2, 4, Block.STONE.stateId());
palette.set(4, 0, 4, Block.STONE.stateId());
var result = LightCompute.compute(palette);
assertLight(result, Map.ofEntries(
// Glowstone
entry(new Vec(4, 1, 4), 15),
// Isolation
entry(new Vec(3, 1, 4), 0),
entry(new Vec(4, 1, 5), 0),
entry(new Vec(4, 1, 3), 0),
entry(new Vec(5, 1, 4), 0),
entry(new Vec(4, 2, 4), 0),
entry(new Vec(4, 0, 4), 0),
// Outside location
entry(new Vec(2, 2, 3), 0)));
}
@Test
public void isolatedStair() {
var palette = Palette.blocks();
palette.set(4, 1, 4, Block.GLOWSTONE.stateId());
palette.set(3, 1, 4, Block.OAK_STAIRS.withProperties(Map.of(
"facing", "east",
"half", "bottom",
"shape", "straight")).stateId());
palette.set(4, 1, 5, Block.STONE.stateId());
palette.set(4, 1, 3, Block.STONE.stateId());
palette.set(5, 1, 4, Block.STONE.stateId());
palette.set(4, 2, 4, Block.STONE.stateId());
palette.set(4, 0, 4, Block.STONE.stateId());
var result = LightCompute.compute(palette);
assertLight(result, Map.ofEntries(
// Glowstone
entry(new Vec(4, 1, 4), 15),
// Front of stair
entry(new Vec(2, 1, 4), 0)));
}
@Test
public void isolatedStairOpposite() {
var palette = Palette.blocks();
palette.set(4, 1, 4, Block.GLOWSTONE.stateId());
palette.set(3, 1, 4, Block.OAK_STAIRS.withProperties(Map.of(
"facing", "west",
"half", "bottom",
"shape", "straight")).stateId());
palette.set(4, 1, 5, Block.STONE.stateId());
palette.set(4, 1, 3, Block.STONE.stateId());
palette.set(5, 1, 4, Block.STONE.stateId());
palette.set(4, 2, 4, Block.STONE.stateId());
palette.set(4, 0, 4, Block.STONE.stateId());
var result = LightCompute.compute(palette);
assertLight(result, Map.ofEntries(
// Glowstone
entry(new Vec(4, 1, 4), 15),
// Stair
entry(new Vec(3, 1, 4), 14),
// Front of stair
entry(new Vec(2, 1, 4), 11),
// Others
entry(new Vec(3, 0, 5), 12),
entry(new Vec(3, 0, 3), 12)));
}
@Test
public void isolatedStairWest() {
var palette = Palette.blocks();
palette.set(4, 1, 4, Block.GLOWSTONE.stateId());
palette.set(3, 1, 4, Block.OAK_STAIRS.withProperties(Map.of(
"facing", "west",
"half", "bottom",
"shape", "straight")).stateId());
palette.set(4, 1, 5, Block.STONE.stateId());
palette.set(4, 1, 3, Block.STONE.stateId());
palette.set(5, 1, 4, Block.STONE.stateId());
palette.set(4, 2, 4, Block.STONE.stateId());
palette.set(4, 0, 4, Block.STONE.stateId());
var result = LightCompute.compute(palette);
assertLight(result, Map.ofEntries(
// Glowstone
entry(new Vec(4, 1, 4), 15),
// Stair
entry(new Vec(3, 1, 4), 14),
// Front of stair
entry(new Vec(2, 1, 4), 11),
// Others
entry(new Vec(3, 0, 5), 12),
entry(new Vec(3, 0, 3), 12),
entry(new Vec(3, 2, 4), 13),
entry(new Vec(3, -1, 4), 10),
entry(new Vec(2, 0, 4), 10)));
}
@Test
public void isolatedStairSouth() {
var palette = Palette.blocks();
palette.set(4, 1, 4, Block.GLOWSTONE.stateId());
palette.set(3, 1, 4, Block.OAK_STAIRS.withProperties(Map.of(
"facing", "south",
"half", "bottom",
"shape", "straight")).stateId());
palette.set(4, 1, 5, Block.STONE.stateId());
palette.set(4, 1, 3, Block.STONE.stateId());
palette.set(5, 1, 4, Block.STONE.stateId());
palette.set(4, 2, 4, Block.STONE.stateId());
palette.set(4, 0, 4, Block.STONE.stateId());
var result = LightCompute.compute(palette);
assertLight(result, Map.ofEntries(
// Glowstone
entry(new Vec(4, 1, 4), 15),
// Stair
entry(new Vec(3, 1, 4), 14),
// Front of stair
entry(new Vec(2, 1, 4), 13),
// Others
entry(new Vec(3, 0, 5), 10),
entry(new Vec(3, 0, 3), 12)));
}
void assertLight(LightCompute.Result result, Map<Vec, Integer> expectedLights) {
List<String> errors = new ArrayList<>();
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
var expected = expectedLights.get(new Vec(x, y, z));
if (expected != null) {
final byte light = result.getLight(x, y, z);
if (light != expected) {
errors.add(String.format("Expected %d at [%d,%d,%d] but got %d", expected, x, y, z, light));
}
}
}
}
}
if (!errors.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (String s : errors) {
sb.append(s).append("\n");
}
System.err.println(sb);
fail();
}
}
}

View File

@ -0,0 +1,164 @@
package net.minestom.server.instance.light;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.*;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.palette.Palette;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.jglrxavpok.hephaistos.mca.AnvilException;
import org.jglrxavpok.hephaistos.mca.BlockState;
import org.jglrxavpok.hephaistos.mca.ChunkSection;
import org.jglrxavpok.hephaistos.mca.RegionFile;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.util.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@EnvTest
public class LightParityIntegrationTest {
@Test
public void test(Env env) throws URISyntaxException, IOException, AnvilException {
assumeTrue(false);
Map<Vec, SectionEntry> sections = retrieveSections();
// Generate our own light
InstanceContainer instance = (InstanceContainer) env.createFlatInstance();
instance.setChunkSupplier(LightingChunk::new);
instance.setChunkLoader(new AnvilLoader(Path.of("./src/test/resources/net/minestom/server/instance/lighting")));
int end = 4;
// Load the chunks
for (int x = 0; x < end; x++) {
for (int z = 0; z < end; z++) {
instance.loadChunk(x, z).join();
}
}
LightingChunk.relight(instance, instance.getChunks());
int differences = 0;
int differencesZero = 0;
int blocks = 0;
int sky = 0;
for (Chunk chunk : instance.getChunks()) {
if (chunk.getChunkX() == 0 || chunk.getChunkZ() == 0) {
continue;
}
if (chunk.getChunkX() == end - 1 || chunk.getChunkZ() == end - 1) {
continue;
}
for (int sectionIndex = chunk.getMinSection(); sectionIndex < chunk.getMaxSection(); sectionIndex++) {
if (sectionIndex != 3) continue;
Section section = chunk.getSection(sectionIndex);
Light sectionLight = section.blockLight();
Light sectionSkyLight = section.skyLight();
SectionEntry sectionEntry = sections.get(new Vec(chunk.getChunkX(), sectionIndex, chunk.getChunkZ()));
if (sectionEntry == null) {
continue;
}
byte[] serverBlock = sectionLight.array();
byte[] mcaBlock = sectionEntry.block;
byte[] serverSky = sectionSkyLight.array();
byte[] mcaSky = sectionEntry.sky;
for (int x = 0; x < 16; ++x) {
for (int y = 0; y < 16; ++y) {
for (int z = 0; z < 16; ++z) {
int index = x | (z << 4) | (y << 8);
{
int serverBlockValue = LightCompute.getLight(serverBlock, index);
int mcaBlockValue = mcaBlock.length == 0 ? 0 : LightCompute.getLight(mcaBlock, index);
if (serverBlockValue != mcaBlockValue) {
if (serverBlockValue == 0) differencesZero++;
else differences++;
blocks++;
}
}
{
int serverSkyValue = LightCompute.getLight(serverSky, index);
int mcaSkyValue = mcaSky.length == 0 ? 0 : LightCompute.getLight(mcaSky, index);
if (serverSkyValue != mcaSkyValue) {
if (serverSkyValue == 0) differencesZero++;
else differences++;
sky++;
}
}
}
}
}
}
}
assertEquals(0, differences);
assertEquals(0, differencesZero);
assertEquals(0, blocks);
assertEquals(0, sky);
}
record SectionEntry(Palette blocks, byte[] sky, byte[] block) {
}
private static Map<Vec, SectionEntry> retrieveSections() throws IOException, URISyntaxException, AnvilException {
URL defaultImage = LightParityIntegrationTest.class.getResource("/net/minestom/server/instance/lighting/region/r.0.0.mca");
assert defaultImage != null;
File imageFile = new File(defaultImage.toURI());
var regionFile = new RegionFile(new RandomAccessFile(imageFile, "rw"),
0, 0, -64, 384);
Map<Vec, SectionEntry> sections = new HashMap<>();
// Read from anvil
for (int x = 1; x < 3; x++) {
for (int z = 1; z < 3; z++) {
var chunk = regionFile.getChunk(x, z);
if (chunk == null) continue;
for (int yLevel = chunk.getMinY(); yLevel <= chunk.getMaxY(); yLevel += 16) {
var section = chunk.getSection((byte) (yLevel/16));
var palette = loadBlocks(section);
var sky = section.getSkyLights();
var block = section.getBlockLights();
sections.put(new Vec(x, section.getY(), z), new SectionEntry(palette, sky, block));
}
}
}
return sections;
}
private static Palette loadBlocks(ChunkSection section) throws AnvilException {
var palette = Palette.blocks();
for (int x = 0; x < Chunk.CHUNK_SECTION_SIZE; x++) {
for (int z = 0; z < Chunk.CHUNK_SECTION_SIZE; z++) {
for (int y = 0; y < Chunk.CHUNK_SECTION_SIZE; y++) {
final BlockState blockState = section.get(x, y, z);
final String blockName = blockState.getName();
Block block = Objects.requireNonNull(Block.fromNamespaceId(blockName))
.withProperties(blockState.getProperties());
palette.set(x, y, z, block.stateId());
}
}
}
return palette;
}
}

View File

@ -0,0 +1,67 @@
package net.minestom.server.instance.light;
import net.minestom.server.ServerProcess;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.Instance;
import net.minestom.server.instance.LightingChunk;
import net.minestom.server.instance.block.Block;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.minestom.server.instance.BlockLightMergeIntegrationTest.assertLightInstance;
@EnvTest
public class WorldRelightIntegrationTest {
private @NotNull Instance createLightingInstance(@NotNull ServerProcess process) {
var instance = process.instance().createInstanceContainer();
instance.setGenerator(unit -> {
unit.modifier().fillHeight(39, 40, Block.STONE);
unit.subdivide().forEach(u -> u.modifier().setBlock(0, 10, 0, Block.GLOWSTONE));
unit.modifier().fillHeight(50, 51, Block.STONE);
});
return instance;
}
@Test
public void testBorderLava(Env env) {
Instance instance = env.createFlatInstance();
instance.loadChunk(6, 16).join();
instance.loadChunk(6, 15).join();
instance.setBlock(106, 70, 248, Block.LAVA);
instance.setBlock(106, 71, 249, Block.LAVA);
Map<Vec, Integer> expectedLights = Map.ofEntries(
entry(new Vec(105, 72, 256), 6)
);
LightingChunk.relight(instance, instance.getChunks());
assertLightInstance(instance, expectedLights);
}
@Test
public void testBlockRemoval(Env env) {
Instance instance = createLightingInstance(env.process());
for (int x = -3; x <= 3; x++) {
for (int z = -3; z <= 3; z++) {
instance.loadChunk(x, z).join();
}
}
LightingChunk.relight(instance, instance.getChunks());
var expectedLights = Map.ofEntries(
entry(new Vec(-1, 40, 0), 12),
entry(new Vec(-9, 40, 8), 0),
entry(new Vec(-1, 40, -16), 12),
entry(new Vec(-1, 37, 0), 3),
entry(new Vec(-8, 37, -8), 0)
);
assertLightInstance(instance, expectedLights);
}
}