mirror of
https://github.com/Minestom/Minestom.git
synced 2025-01-06 16:37:38 +01:00
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:
parent
036e0b5ea3
commit
912fb34f17
@ -20,6 +20,7 @@ import net.minestom.server.event.server.ServerTickMonitorEvent;
|
|||||||
import net.minestom.server.instance.Instance;
|
import net.minestom.server.instance.Instance;
|
||||||
import net.minestom.server.instance.InstanceContainer;
|
import net.minestom.server.instance.InstanceContainer;
|
||||||
import net.minestom.server.instance.InstanceManager;
|
import net.minestom.server.instance.InstanceManager;
|
||||||
|
import net.minestom.server.instance.LightingChunk;
|
||||||
import net.minestom.server.instance.block.Block;
|
import net.minestom.server.instance.block.Block;
|
||||||
import net.minestom.server.inventory.Inventory;
|
import net.minestom.server.inventory.Inventory;
|
||||||
import net.minestom.server.inventory.InventoryType;
|
import net.minestom.server.inventory.InventoryType;
|
||||||
@ -123,6 +124,7 @@ public class PlayerInit {
|
|||||||
|
|
||||||
InstanceContainer instanceContainer = instanceManager.createInstanceContainer(DimensionType.OVERWORLD);
|
InstanceContainer instanceContainer = instanceManager.createInstanceContainer(DimensionType.OVERWORLD);
|
||||||
instanceContainer.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE));
|
instanceContainer.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE));
|
||||||
|
instanceContainer.setChunkSupplier(LightingChunk::new);
|
||||||
|
|
||||||
if (false) {
|
if (false) {
|
||||||
System.out.println("start");
|
System.out.println("start");
|
||||||
|
@ -4,6 +4,7 @@ import net.minestom.server.coordinate.Point;
|
|||||||
import net.minestom.server.coordinate.Pos;
|
import net.minestom.server.coordinate.Pos;
|
||||||
import net.minestom.server.coordinate.Vec;
|
import net.minestom.server.coordinate.Vec;
|
||||||
import net.minestom.server.entity.Entity;
|
import net.minestom.server.entity.Entity;
|
||||||
|
import net.minestom.server.instance.block.BlockFace;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
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));
|
this(width, height, depth, new Vec(-width / 2, 0, -depth / 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ApiStatus.Experimental
|
@ApiStatus.Experimental
|
||||||
public boolean intersectBox(@NotNull Point positionRelative, @NotNull BoundingBox boundingBox) {
|
public boolean intersectBox(@NotNull Point positionRelative, @NotNull BoundingBox boundingBox) {
|
||||||
|
@ -113,7 +113,7 @@ public final class CollisionUtils {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Shape parseBlockShape(String str, Registry.BlockEntry blockEntry) {
|
public static Shape parseBlockShape(String collision, String occlusion, Registry.BlockEntry blockEntry) {
|
||||||
return ShapeImpl.parseBlockFromRegistry(str, blockEntry);
|
return ShapeImpl.parseBlockFromRegistry(collision, occlusion, blockEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package net.minestom.server.collision;
|
package net.minestom.server.collision;
|
||||||
|
|
||||||
import net.minestom.server.coordinate.Point;
|
import net.minestom.server.coordinate.Point;
|
||||||
|
import net.minestom.server.instance.block.BlockFace;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@ApiStatus.Experimental
|
@ApiStatus.Experimental
|
||||||
public interface Shape {
|
public interface Shape {
|
||||||
|
boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if two bounding boxes intersect.
|
* Checks if two bounding boxes intersect.
|
||||||
*
|
*
|
||||||
|
@ -5,28 +5,37 @@ import it.unimi.dsi.fastutil.doubles.DoubleList;
|
|||||||
import net.minestom.server.coordinate.Point;
|
import net.minestom.server.coordinate.Point;
|
||||||
import net.minestom.server.coordinate.Vec;
|
import net.minestom.server.coordinate.Vec;
|
||||||
import net.minestom.server.instance.block.Block;
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import net.minestom.server.instance.block.BlockFace;
|
||||||
import net.minestom.server.registry.Registry;
|
import net.minestom.server.registry.Registry;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
final class ShapeImpl implements Shape {
|
final class ShapeImpl implements Shape {
|
||||||
private static final Pattern PATTERN = Pattern.compile("\\d.\\d{1,3}", Pattern.MULTILINE);
|
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 Point relativeStart, relativeEnd;
|
||||||
|
|
||||||
|
private final BoundingBox[] occlusionBoundingBoxes;
|
||||||
|
private final byte blockOcclusion;
|
||||||
|
private final byte airOcclusion;
|
||||||
|
|
||||||
private final Registry.BlockEntry blockEntry;
|
private final Registry.BlockEntry blockEntry;
|
||||||
private Block block;
|
private Block block;
|
||||||
|
|
||||||
private ShapeImpl(BoundingBox[] boundingBoxes, Registry.BlockEntry blockEntry) {
|
private ShapeImpl(BoundingBox[] boundingBoxes, BoundingBox[] occlusionBoundingBoxes, Registry.BlockEntry blockEntry) {
|
||||||
this.blockSections = boundingBoxes;
|
this.collisionBoundingBoxes = boundingBoxes;
|
||||||
|
this.occlusionBoundingBoxes = occlusionBoundingBoxes;
|
||||||
this.blockEntry = blockEntry;
|
this.blockEntry = blockEntry;
|
||||||
// Find bounds
|
|
||||||
|
// Find bounds of collision
|
||||||
{
|
{
|
||||||
double minX = 1, minY = 1, minZ = 1;
|
double minX = 1, minY = 1, minZ = 1;
|
||||||
double maxX = 0, maxY = 0, maxZ = 0;
|
double maxX = 0, maxY = 0, maxZ = 0;
|
||||||
for (BoundingBox blockSection : blockSections) {
|
for (BoundingBox blockSection : collisionBoundingBoxes) {
|
||||||
// Min
|
// Min
|
||||||
if (blockSection.minX() < minX) minX = blockSection.minX();
|
if (blockSection.minX() < minX) minX = blockSection.minX();
|
||||||
if (blockSection.minY() < minY) minY = blockSection.minY();
|
if (blockSection.minY() < minY) minY = blockSection.minY();
|
||||||
@ -39,16 +48,26 @@ final class ShapeImpl implements Shape {
|
|||||||
this.relativeStart = new Vec(minX, minY, minZ);
|
this.relativeStart = new Vec(minX, minY, minZ);
|
||||||
this.relativeEnd = new Vec(maxX, maxY, maxZ);
|
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);
|
final Matcher matcher = PATTERN.matcher(str);
|
||||||
DoubleList vals = new DoubleArrayList();
|
DoubleList vals = new DoubleArrayList();
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
double newVal = Double.parseDouble(matcher.group());
|
double newVal = Double.parseDouble(matcher.group());
|
||||||
vals.add(newVal);
|
vals.add(newVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
final int count = vals.size() / 6;
|
final int count = vals.size() / 6;
|
||||||
BoundingBox[] boundingBoxes = new BoundingBox[count];
|
BoundingBox[] boundingBoxes = new BoundingBox[count];
|
||||||
for (int i = 0; i < count; ++i) {
|
for (int i = 0; i < count; ++i) {
|
||||||
@ -66,7 +85,36 @@ final class ShapeImpl implements Shape {
|
|||||||
assert bb.minZ() == minZ;
|
assert bb.minZ() == minZ;
|
||||||
boundingBoxes[i] = bb;
|
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
|
@Override
|
||||||
@ -79,9 +127,32 @@ final class ShapeImpl implements Shape {
|
|||||||
return relativeEnd;
|
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
|
@Override
|
||||||
public boolean intersectBox(@NotNull Point position, @NotNull BoundingBox boundingBox) {
|
public boolean intersectBox(@NotNull Point position, @NotNull BoundingBox boundingBox) {
|
||||||
for (BoundingBox blockSection : blockSections) {
|
for (BoundingBox blockSection : collisionBoundingBoxes) {
|
||||||
if (boundingBox.intersectBox(position, blockSection)) return true;
|
if (boundingBox.intersectBox(position, blockSection)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -91,7 +162,7 @@ final class ShapeImpl implements Shape {
|
|||||||
public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection,
|
public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection,
|
||||||
@NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) {
|
@NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) {
|
||||||
boolean hitBlock = false;
|
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
|
// Update final result if the temp result collision is sooner than the current final result
|
||||||
if (RayUtils.BoundingBoxIntersectionCheck(moving, rayStart, rayDirection, blockSection, shapePos, finalResult)) {
|
if (RayUtils.BoundingBoxIntersectionCheck(moving, rayStart, rayDirection, blockSection, shapePos, finalResult)) {
|
||||||
finalResult.collidedShapePosition = shapePos;
|
finalResult.collidedShapePosition = shapePos;
|
||||||
@ -108,4 +179,75 @@ final class ShapeImpl implements Shape {
|
|||||||
if (block == null) this.block = block = Block.fromStateId((short) blockEntry.stateId());
|
if (block == null) this.block = block = Block.fromStateId((short) blockEntry.stateId());
|
||||||
return block;
|
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) { }
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ public class AnvilLoader implements IChunkLoader {
|
|||||||
|
|
||||||
final ChunkReader chunkReader = new ChunkReader(chunkData);
|
final ChunkReader chunkReader = new ChunkReader(chunkData);
|
||||||
|
|
||||||
Chunk chunk = new DynamicChunk(instance, chunkX, chunkZ);
|
Chunk chunk = instance.getChunkSupplier().createChunk(instance, chunkX, chunkZ);
|
||||||
synchronized (chunk) {
|
synchronized (chunk) {
|
||||||
var yRange = chunkReader.getYRange();
|
var yRange = chunkReader.getYRange();
|
||||||
if (yRange.getStart() < instance.getDimensionType().getMinY()) {
|
if (yRange.getStart() < instance.getDimensionType().getMinY()) {
|
||||||
@ -375,8 +375,8 @@ public class AnvilLoader implements IChunkLoader {
|
|||||||
ChunkSectionWriter sectionWriter = new ChunkSectionWriter(SupportedVersion.Companion.getLatest(), (byte) sectionY);
|
ChunkSectionWriter sectionWriter = new ChunkSectionWriter(SupportedVersion.Companion.getLatest(), (byte) sectionY);
|
||||||
|
|
||||||
Section section = chunk.getSection(sectionY);
|
Section section = chunk.getSection(sectionY);
|
||||||
sectionWriter.setSkyLights(section.getSkyLight());
|
sectionWriter.setSkyLights(section.skyLight().array());
|
||||||
sectionWriter.setBlockLights(section.getBlockLight());
|
sectionWriter.setBlockLights(section.blockLight().array());
|
||||||
|
|
||||||
BiomePalette biomePalette = new BiomePalette();
|
BiomePalette biomePalette = new BiomePalette();
|
||||||
BlockPalette blockPalette = new BlockPalette();
|
BlockPalette blockPalette = new BlockPalette();
|
||||||
|
@ -39,7 +39,7 @@ import static net.minestom.server.utils.chunk.ChunkUtils.toSectionRelativeCoordi
|
|||||||
*/
|
*/
|
||||||
public class DynamicChunk extends Chunk {
|
public class DynamicChunk extends Chunk {
|
||||||
|
|
||||||
private List<Section> sections;
|
protected List<Section> sections;
|
||||||
|
|
||||||
// Key = ChunkUtils#getBlockIndex
|
// Key = ChunkUtils#getBlockIndex
|
||||||
protected final Int2ObjectOpenHashMap<Block> entries = new Int2ObjectOpenHashMap<>(0);
|
protected final Int2ObjectOpenHashMap<Block> entries = new Int2ObjectOpenHashMap<>(0);
|
||||||
@ -47,7 +47,6 @@ public class DynamicChunk extends Chunk {
|
|||||||
|
|
||||||
private long lastChange;
|
private long lastChange;
|
||||||
final CachedPacket chunkCache = new CachedPacket(this::createChunkPacket);
|
final CachedPacket chunkCache = new CachedPacket(this::createChunkPacket);
|
||||||
final CachedPacket lightCache = new CachedPacket(this::createLightPacket);
|
|
||||||
|
|
||||||
public DynamicChunk(@NotNull Instance instance, int chunkX, int chunkZ) {
|
public DynamicChunk(@NotNull Instance instance, int chunkX, int chunkZ) {
|
||||||
super(instance, chunkX, chunkZ, true);
|
super(instance, chunkX, chunkZ, true);
|
||||||
@ -61,7 +60,7 @@ public class DynamicChunk extends Chunk {
|
|||||||
assertLock();
|
assertLock();
|
||||||
this.lastChange = System.currentTimeMillis();
|
this.lastChange = System.currentTimeMillis();
|
||||||
this.chunkCache.invalidate();
|
this.chunkCache.invalidate();
|
||||||
this.lightCache.invalidate();
|
|
||||||
// Update pathfinder
|
// Update pathfinder
|
||||||
if (columnarSpace != null) {
|
if (columnarSpace != null) {
|
||||||
final ColumnarOcclusionFieldList columnarOcclusionFieldList = columnarSpace.occlusionFields();
|
final ColumnarOcclusionFieldList columnarOcclusionFieldList = columnarSpace.occlusionFields();
|
||||||
@ -183,7 +182,7 @@ public class DynamicChunk extends Chunk {
|
|||||||
this.entries.clear();
|
this.entries.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized @NotNull ChunkDataPacket createChunkPacket() {
|
private @NotNull ChunkDataPacket createChunkPacket() {
|
||||||
final NBTCompound heightmapsNBT;
|
final NBTCompound heightmapsNBT;
|
||||||
// TODO: don't hardcode heightmaps
|
// TODO: don't hardcode heightmaps
|
||||||
// Heightmap
|
// Heightmap
|
||||||
@ -203,20 +202,25 @@ public class DynamicChunk extends Chunk {
|
|||||||
"WORLD_SURFACE", NBT.LongArray(encodeBlocks(worldSurface, bitsForHeight))));
|
"WORLD_SURFACE", NBT.LongArray(encodeBlocks(worldSurface, bitsForHeight))));
|
||||||
}
|
}
|
||||||
// Data
|
// Data
|
||||||
final byte[] data = ObjectPool.PACKET_POOL.use(buffer ->
|
|
||||||
NetworkBuffer.makeArray(networkBuffer -> {
|
final byte[] data;
|
||||||
for (Section section : sections) networkBuffer.write(section);
|
synchronized (this) {
|
||||||
}));
|
data = ObjectPool.PACKET_POOL.use(buffer ->
|
||||||
|
NetworkBuffer.makeArray(networkBuffer -> {
|
||||||
|
for (Section section : sections) networkBuffer.write(section);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return new ChunkDataPacket(chunkX, chunkZ,
|
return new ChunkDataPacket(chunkX, chunkZ,
|
||||||
new ChunkData(heightmapsNBT, data, entries),
|
new ChunkData(heightmapsNBT, data, entries),
|
||||||
createLightData());
|
createLightData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized @NotNull UpdateLightPacket createLightPacket() {
|
@NotNull UpdateLightPacket createLightPacket() {
|
||||||
return new UpdateLightPacket(chunkX, chunkZ, createLightData());
|
return new UpdateLightPacket(chunkX, chunkZ, createLightData(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LightData createLightData() {
|
protected LightData createLightData(boolean sendAll) {
|
||||||
BitSet skyMask = new BitSet();
|
BitSet skyMask = new BitSet();
|
||||||
BitSet blockMask = new BitSet();
|
BitSet blockMask = new BitSet();
|
||||||
BitSet emptySkyMask = new BitSet();
|
BitSet emptySkyMask = new BitSet();
|
||||||
@ -227,8 +231,8 @@ public class DynamicChunk extends Chunk {
|
|||||||
int index = 0;
|
int index = 0;
|
||||||
for (Section section : sections) {
|
for (Section section : sections) {
|
||||||
index++;
|
index++;
|
||||||
final byte[] skyLight = section.getSkyLight();
|
final byte[] skyLight = section.skyLight().array();
|
||||||
final byte[] blockLight = section.getBlockLight();
|
final byte[] blockLight = section.blockLight().array();
|
||||||
if (skyLight.length != 0) {
|
if (skyLight.length != 0) {
|
||||||
skyLights.add(skyLight);
|
skyLights.add(skyLight);
|
||||||
skyMask.set(index);
|
skyMask.set(index);
|
||||||
|
@ -34,6 +34,7 @@ import net.minestom.server.timer.Scheduler;
|
|||||||
import net.minestom.server.utils.ArrayUtils;
|
import net.minestom.server.utils.ArrayUtils;
|
||||||
import net.minestom.server.utils.PacketUtils;
|
import net.minestom.server.utils.PacketUtils;
|
||||||
import net.minestom.server.utils.chunk.ChunkCache;
|
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.chunk.ChunkUtils;
|
||||||
import net.minestom.server.utils.time.Cooldown;
|
import net.minestom.server.utils.time.Cooldown;
|
||||||
import net.minestom.server.utils.time.TimeUnit;
|
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);
|
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
|
* Gets the generator associated with the instance
|
||||||
*
|
*
|
||||||
|
@ -328,9 +328,11 @@ public class InstanceContainer extends Instance {
|
|||||||
if (forkChunk != null) {
|
if (forkChunk != null) {
|
||||||
applyFork(forkChunk, sectionModifier);
|
applyFork(forkChunk, sectionModifier);
|
||||||
// Update players
|
// 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.chunkCache.invalidate();
|
||||||
dynamicChunk.lightCache.invalidate();
|
|
||||||
}
|
}
|
||||||
forkChunk.sendChunk();
|
forkChunk.sendChunk();
|
||||||
} else {
|
} 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
|
* @param chunkSupplier the new {@link ChunkSupplier} of this instance, chunks need to be non-null
|
||||||
* @throws NullPointerException if {@code chunkSupplier} is null
|
* @throws NullPointerException if {@code chunkSupplier} is null
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier) {
|
public void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier) {
|
||||||
this.chunkSupplier = chunkSupplier;
|
this.chunkSupplier = chunkSupplier;
|
||||||
}
|
}
|
||||||
|
384
src/main/java/net/minestom/server/instance/LightingChunk.java
Normal file
384
src/main/java/net/minestom/server/instance/LightingChunk.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package net.minestom.server.instance;
|
package net.minestom.server.instance;
|
||||||
|
|
||||||
|
import net.minestom.server.instance.light.Light;
|
||||||
import net.minestom.server.instance.palette.Palette;
|
import net.minestom.server.instance.palette.Palette;
|
||||||
import net.minestom.server.network.NetworkBuffer;
|
import net.minestom.server.network.NetworkBuffer;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@ -7,22 +8,20 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import static net.minestom.server.network.NetworkBuffer.SHORT;
|
import static net.minestom.server.network.NetworkBuffer.SHORT;
|
||||||
|
|
||||||
public final class Section implements NetworkBuffer.Writer {
|
public final class Section implements NetworkBuffer.Writer {
|
||||||
private Palette blockPalette;
|
private final Palette blockPalette;
|
||||||
private Palette biomePalette;
|
private final Palette biomePalette;
|
||||||
private byte[] skyLight;
|
private final Light skyLight;
|
||||||
private byte[] blockLight;
|
private final Light blockLight;
|
||||||
|
|
||||||
private Section(Palette blockPalette, Palette biomePalette,
|
private Section(Palette blockPalette, Palette biomePalette) {
|
||||||
byte[] skyLight, byte[] blockLight) {
|
|
||||||
this.blockPalette = blockPalette;
|
this.blockPalette = blockPalette;
|
||||||
this.biomePalette = biomePalette;
|
this.biomePalette = biomePalette;
|
||||||
this.skyLight = skyLight;
|
this.skyLight = Light.sky(blockPalette);
|
||||||
this.blockLight = blockLight;
|
this.blockLight = Light.block(blockPalette);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Section() {
|
public Section() {
|
||||||
this(Palette.blocks(), Palette.biomes(),
|
this(Palette.blocks(), Palette.biomes());
|
||||||
new byte[0], new byte[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Palette blockPalette() {
|
public Palette blockPalette() {
|
||||||
@ -33,33 +32,14 @@ public final class Section implements NetworkBuffer.Writer {
|
|||||||
return biomePalette;
|
return biomePalette;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getSkyLight() {
|
|
||||||
return skyLight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSkyLight(byte[] skyLight) {
|
|
||||||
this.skyLight = skyLight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getBlockLight() {
|
|
||||||
return blockLight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBlockLight(byte[] blockLight) {
|
|
||||||
this.blockLight = blockLight;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
this.blockPalette.fill(0);
|
this.blockPalette.fill(0);
|
||||||
this.biomePalette.fill(0);
|
this.biomePalette.fill(0);
|
||||||
this.skyLight = new byte[0];
|
|
||||||
this.blockLight = new byte[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull Section clone() {
|
public @NotNull Section clone() {
|
||||||
return new Section(blockPalette.clone(), biomePalette.clone(),
|
return new Section(this.blockPalette.clone(), this.biomePalette.clone());
|
||||||
skyLight.clone(), blockLight.clone());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -68,4 +48,20 @@ public final class Section implements NetworkBuffer.Writer {
|
|||||||
writer.write(blockPalette);
|
writer.write(blockPalette);
|
||||||
writer.write(biomePalette);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import net.minestom.server.instance.block.Block;
|
|||||||
import net.minestom.server.instance.block.BlockFace;
|
import net.minestom.server.instance.block.BlockFace;
|
||||||
import net.minestom.server.instance.block.BlockHandler;
|
import net.minestom.server.instance.block.BlockHandler;
|
||||||
import net.minestom.server.instance.generator.Generator;
|
import net.minestom.server.instance.generator.Generator;
|
||||||
|
import net.minestom.server.utils.chunk.ChunkSupplier;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@ -75,6 +76,16 @@ public class SharedInstance extends Instance {
|
|||||||
return instanceContainer.saveChunksToStorage();
|
return instanceContainer.saveChunksToStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setChunkSupplier(@NotNull ChunkSupplier chunkSupplier) {
|
||||||
|
instanceContainer.setChunkSupplier(chunkSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChunkSupplier getChunkSupplier() {
|
||||||
|
return instanceContainer.getChunkSupplier();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Generator generator() {
|
public @Nullable Generator generator() {
|
||||||
return instanceContainer.generator();
|
return instanceContainer.generator();
|
||||||
|
@ -6,12 +6,16 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
|||||||
import net.minestom.server.instance.Chunk;
|
import net.minestom.server.instance.Chunk;
|
||||||
import net.minestom.server.instance.Instance;
|
import net.minestom.server.instance.Instance;
|
||||||
import net.minestom.server.instance.InstanceContainer;
|
import net.minestom.server.instance.InstanceContainer;
|
||||||
|
import net.minestom.server.instance.LightingChunk;
|
||||||
import net.minestom.server.instance.block.Block;
|
import net.minestom.server.instance.block.Block;
|
||||||
import net.minestom.server.utils.chunk.ChunkUtils;
|
import net.minestom.server.utils.chunk.ChunkUtils;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
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.CountDownLatch;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
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;
|
final AbsoluteBlockBatch inverse = this.options.shouldCalculateInverse() ? new AbsoluteBlockBatch(inverseOption) : null;
|
||||||
synchronized (chunkBatchesMap) {
|
synchronized (chunkBatchesMap) {
|
||||||
AtomicInteger counter = new AtomicInteger();
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
Set<Chunk> updated = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
for (var entry : Long2ObjectMaps.fastIterable(chunkBatchesMap)) {
|
for (var entry : Long2ObjectMaps.fastIterable(chunkBatchesMap)) {
|
||||||
final long chunkIndex = entry.getLongKey();
|
final long chunkIndex = entry.getLongKey();
|
||||||
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
|
final int chunkX = ChunkUtils.getChunkCoordX(chunkIndex);
|
||||||
@ -146,6 +152,25 @@ public class AbsoluteBlockBatch implements Batch<Runnable> {
|
|||||||
callback.run();
|
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);
|
if (inverse != null) inverse.chunkBatchesMap.put(chunkIndex, chunkInverse);
|
||||||
|
359
src/main/java/net/minestom/server/instance/light/BlockLight.java
Normal file
359
src/main/java/net/minestom/server/instance/light/BlockLight.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
78
src/main/java/net/minestom/server/instance/light/Light.java
Normal file
78
src/main/java/net/minestom/server/instance/light/Light.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
398
src/main/java/net/minestom/server/instance/light/SkyLight.java
Normal file
398
src/main/java/net/minestom/server/instance/light/SkyLight.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -170,6 +170,8 @@ public final class Registry {
|
|||||||
private final boolean air;
|
private final boolean air;
|
||||||
private final boolean solid;
|
private final boolean solid;
|
||||||
private final boolean liquid;
|
private final boolean liquid;
|
||||||
|
private final boolean occludes;
|
||||||
|
private final int lightEmission;
|
||||||
private final String blockEntity;
|
private final String blockEntity;
|
||||||
private final int blockEntityId;
|
private final int blockEntityId;
|
||||||
private final Supplier<Material> materialSupplier;
|
private final Supplier<Material> materialSupplier;
|
||||||
@ -189,7 +191,9 @@ public final class Registry {
|
|||||||
this.jumpFactor = main.getDouble("jumpFactor", 1);
|
this.jumpFactor = main.getDouble("jumpFactor", 1);
|
||||||
this.air = main.getBoolean("air", false);
|
this.air = main.getBoolean("air", false);
|
||||||
this.solid = main.getBoolean("solid");
|
this.solid = main.getBoolean("solid");
|
||||||
|
this.occludes = main.getBoolean("occludes", true);
|
||||||
this.liquid = main.getBoolean("liquid", false);
|
this.liquid = main.getBoolean("liquid", false);
|
||||||
|
this.lightEmission = main.getInt("lightEmission", 0);
|
||||||
{
|
{
|
||||||
Properties blockEntity = main.section("blockEntity");
|
Properties blockEntity = main.section("blockEntity");
|
||||||
if (blockEntity != null) {
|
if (blockEntity != null) {
|
||||||
@ -205,8 +209,9 @@ public final class Registry {
|
|||||||
this.materialSupplier = materialNamespace != null ? () -> Material.fromNamespaceId(materialNamespace) : () -> null;
|
this.materialSupplier = materialNamespace != null ? () -> Material.fromNamespaceId(materialNamespace) : () -> null;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
final String string = main.getString("collisionShape");
|
final String collision = main.getString("collisionShape");
|
||||||
this.shape = CollisionUtils.parseBlockShape(string, this);
|
final String occlusion = main.getString("occlusionShape");
|
||||||
|
this.shape = CollisionUtils.parseBlockShape(collision, occlusion, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,10 +259,18 @@ public final class Registry {
|
|||||||
return solid;
|
return solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean occludes() {
|
||||||
|
return occludes;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isLiquid() {
|
public boolean isLiquid() {
|
||||||
return liquid;
|
return liquid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int lightEmission() {
|
||||||
|
return lightEmission;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isBlockEntity() {
|
public boolean isBlockEntity() {
|
||||||
return blockEntity != null;
|
return blockEntity != null;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user