diff --git a/demo/src/main/java/net/minestom/demo/PlayerInit.java b/demo/src/main/java/net/minestom/demo/PlayerInit.java index 72003b6f6..ec55b430d 100644 --- a/demo/src/main/java/net/minestom/demo/PlayerInit.java +++ b/demo/src/main/java/net/minestom/demo/PlayerInit.java @@ -17,6 +17,7 @@ import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.item.PickupItemEvent; import net.minestom.server.event.player.*; import net.minestom.server.event.server.ServerTickMonitorEvent; +import net.minestom.server.instance.DynamicChunk; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.InstanceManager; @@ -28,6 +29,7 @@ import net.minestom.server.item.Material; import net.minestom.server.item.metadata.BundleMeta; import net.minestom.server.monitoring.BenchmarkManager; import net.minestom.server.monitoring.TickMonitor; +import net.minestom.server.timer.TaskSchedule; import net.minestom.server.utils.MathUtils; import net.minestom.server.utils.chunk.ChunkUtils; import net.minestom.server.utils.time.TimeUnit; @@ -39,6 +41,7 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; public class PlayerInit { @@ -79,6 +82,24 @@ public class PlayerInit { itemEntity.setInstance(player.getInstance(), playerPos.withY(y -> y + 1.5)); Vec velocity = playerPos.direction().mul(6); itemEntity.setVelocity(velocity); + + // Stress test light engine + { + var instance = player.getInstance(); + MinecraftServer.getSchedulerManager().scheduleTask(() -> { + IntStream.range(0, 15).forEach(value -> { + int x = Math.abs(ThreadLocalRandom.current().nextInt()) % 1500 - 250; + int z = Math.abs(ThreadLocalRandom.current().nextInt()) % 1500 - 250; + var pos = new Vec(x, 40, z); + instance.setBlock(pos, Block.GLOWSTONE); + + // Force update + var chunk = (DynamicChunk) instance.getChunkAt(pos); + chunk.createLightPacket(); + }); + + }, TaskSchedule.nextTick(), TaskSchedule.nextTick()); + } }) .addListener(PlayerDisconnectEvent.class, event -> System.out.println("DISCONNECTION " + event.getPlayer().getUsername())) .addListener(PlayerLoginEvent.class, event -> { @@ -116,12 +137,17 @@ public class PlayerInit { }) .addListener(PlayerPacketEvent.class, event -> { //System.out.println("in " + event.getPacket().getClass().getSimpleName()); + }) + .addListener(ServerTickMonitorEvent.class, event -> { + System.out.println("tick " + event.getTickMonitor().getTickTime()); }); static { InstanceManager instanceManager = MinecraftServer.getInstanceManager(); InstanceContainer instanceContainer = instanceManager.createInstanceContainer(DimensionType.OVERWORLD); + instanceContainer.setTime(18000); + instanceContainer.setTimeRate(0); instanceContainer.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); if (false) { diff --git a/src/main/java/net/minestom/server/collision/BlockCollision.java b/src/main/java/net/minestom/server/collision/BlockCollision.java index 001cd90c1..6221b9bc4 100644 --- a/src/main/java/net/minestom/server/collision/BlockCollision.java +++ b/src/main/java/net/minestom/server/collision/BlockCollision.java @@ -344,9 +344,9 @@ final class BlockCollision { // If player is at block 40 we cannot place a block at block 39 with side length 1 because the block will be in [39, 40] // For this reason we subtract a small amount from the player position Point playerPos = entity.getPosition().add(entity.getPosition().sub(blockPos).mul(0.0000001)); - intersects = b.registry().collisionShape().intersectBox(playerPos.sub(blockPos), entity.getBoundingBox()); + intersects = b.registry().shape().intersectBox(playerPos.sub(blockPos), entity.getBoundingBox()); } else { - intersects = b.registry().collisionShape().intersectBox(entity.getPosition().sub(blockPos), entity.getBoundingBox()); + intersects = b.registry().shape().intersectBox(entity.getPosition().sub(blockPos), entity.getBoundingBox()); } if (intersects) return entity; } @@ -374,7 +374,7 @@ final class BlockCollision { boolean hitBlock = false; if (checkBlock.isSolid()) { final Vec blockPos = new Vec(blockX, blockY, blockZ); - hitBlock = checkBlock.registry().collisionShape().intersectBoxSwept(entityPosition, entityVelocity, blockPos, boundingBox, finalResult); + hitBlock = checkBlock.registry().shape().intersectBoxSwept(entityPosition, entityVelocity, blockPos, boundingBox, finalResult); } return hitBlock; } diff --git a/src/main/java/net/minestom/server/collision/BoundingBox.java b/src/main/java/net/minestom/server/collision/BoundingBox.java index d8e55c6d3..bd0cf7e6b 100644 --- a/src/main/java/net/minestom/server/collision/BoundingBox.java +++ b/src/main/java/net/minestom/server/collision/BoundingBox.java @@ -4,6 +4,7 @@ import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; +import net.minestom.server.instance.block.BlockFace; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -82,6 +83,11 @@ public final class BoundingBox implements Shape { return relativeEnd; } + @Override + public boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face) { + return false; + } + @Override public String toString() { String result = "BoundingBox"; diff --git a/src/main/java/net/minestom/server/collision/CollisionUtils.java b/src/main/java/net/minestom/server/collision/CollisionUtils.java index 4bc6974ca..f4488d0dc 100644 --- a/src/main/java/net/minestom/server/collision/CollisionUtils.java +++ b/src/main/java/net/minestom/server/collision/CollisionUtils.java @@ -112,7 +112,7 @@ public final class CollisionUtils { }; } - public static Shape parseBlockShape(String str, Registry.BlockEntry blockEntry) { - return ShapeImpl.parseBlockFromRegistry(str, blockEntry); + public static Shape parseBlockShape(String collision, String occlusion, Registry.BlockEntry blockEntry) { + return ShapeImpl.parseBlockFromRegistry(collision, occlusion, blockEntry); } } diff --git a/src/main/java/net/minestom/server/collision/Shape.java b/src/main/java/net/minestom/server/collision/Shape.java index c4f7a04eb..232279355 100644 --- a/src/main/java/net/minestom/server/collision/Shape.java +++ b/src/main/java/net/minestom/server/collision/Shape.java @@ -1,6 +1,7 @@ package net.minestom.server.collision; import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.BlockFace; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -41,4 +42,13 @@ public interface Shape { * @return End of shape */ @NotNull Point relativeEnd(); + + /** + * Check if addition of two shape faces is full + * + * @param shape shape to add + * @param face face to add + * @return true if combined face is full + */ + boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face); } diff --git a/src/main/java/net/minestom/server/collision/ShapeImpl.java b/src/main/java/net/minestom/server/collision/ShapeImpl.java index fc4bee640..f3d6a2a79 100644 --- a/src/main/java/net/minestom/server/collision/ShapeImpl.java +++ b/src/main/java/net/minestom/server/collision/ShapeImpl.java @@ -5,28 +5,41 @@ import it.unimi.dsi.fastutil.doubles.DoubleList; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.block.BlockFace; +import net.minestom.server.registry.Registry; +import net.minestom.server.item.Material; +import net.minestom.server.instance.block.Block; import net.minestom.server.registry.Registry; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; final class ShapeImpl implements Shape { private static final Pattern PATTERN = Pattern.compile("\\d.\\d{1,3}", Pattern.MULTILINE); - private final BoundingBox[] blockSections; + private final BoundingBox[] collisionBoundingBoxes; private final Point relativeStart, relativeEnd; + private final BoundingBox[] occlusionBoundingBoxes; + + private final byte blockOcclusion; + private final byte airOcclusion; + private final Registry.BlockEntry blockEntry; private Block block; - private ShapeImpl(BoundingBox[] boundingBoxes, Registry.BlockEntry blockEntry) { - this.blockSections = boundingBoxes; + private ShapeImpl(BoundingBox[] collisionBoundingBoxes, BoundingBox[] occlusionBoundingBoxes, Registry.BlockEntry blockEntry) { + this.collisionBoundingBoxes = collisionBoundingBoxes; + this.occlusionBoundingBoxes = occlusionBoundingBoxes; this.blockEntry = blockEntry; - // Find bounds + + // Find bounds of collision { double minX = 1, minY = 1, minZ = 1; double maxX = 0, maxY = 0, maxZ = 0; - for (BoundingBox blockSection : blockSections) { + for (BoundingBox blockSection : this.collisionBoundingBoxes) { // Min if (blockSection.minX() < minX) minX = blockSection.minX(); if (blockSection.minY() < minY) minY = blockSection.minY(); @@ -39,16 +52,26 @@ final class ShapeImpl implements Shape { this.relativeStart = new Vec(minX, minY, minZ); this.relativeEnd = new Vec(maxX, maxY, maxZ); } + + byte airFaces = 0; + byte fullFaces = 0; + for (BlockFace f : BlockFace.values()) { + final byte res = isFaceCovered(computeOcclusionSet(f)); + airFaces |= ((res == 0) ? 0b1 : 0b0) << (byte) f.ordinal(); + fullFaces |= ((res == 2) ? 0b1 : 0b0) << (byte) f.ordinal(); + } + + this.airOcclusion = airFaces; + this.blockOcclusion = fullFaces; } - static ShapeImpl parseBlockFromRegistry(String str, Registry.BlockEntry blockEntry) { + static private BoundingBox[] parseRegistryBoundingBoxString(String str) { final Matcher matcher = PATTERN.matcher(str); DoubleList vals = new DoubleArrayList(); while (matcher.find()) { double newVal = Double.parseDouble(matcher.group()); vals.add(newVal); } - final int count = vals.size() / 6; BoundingBox[] boundingBoxes = new BoundingBox[count]; for (int i = 0; i < count; ++i) { @@ -66,7 +89,34 @@ final class ShapeImpl implements Shape { assert bb.minZ() == minZ; boundingBoxes[i] = bb; } - return new ShapeImpl(boundingBoxes, blockEntry); + return boundingBoxes; + } + + /** + * Computes the occlusion for a given face. + * + * @param covering The rectangle set to check for covering. + * @return 0 if face is not covered, 1 if face is covered partially, 2 if face is fully covered. + */ + public static byte isFaceCovered(List covering) { + if (covering.isEmpty()) return 0; + Rectangle r = new Rectangle(0, 0, 1, 1); + List toCover = new ArrayList<>(); + toCover.add(r); + for (Rectangle rect : covering) { + List nextCovering = new ArrayList<>(); + for (Rectangle toCoverRect : toCover) { + List 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) { + return new ShapeImpl(parseRegistryBoundingBoxString(collision), parseRegistryBoundingBoxString(occlusion), blockEntry); } @Override @@ -79,9 +129,31 @@ final class ShapeImpl implements Shape { return relativeEnd; } + @Override + public boolean isOccluded(@NotNull Shape shape, @NotNull BlockFace face) { + final ShapeImpl shapeImpl = ((ShapeImpl) shape); + + final boolean hasAirOcclusion = (((airOcclusion >> face.ordinal()) & 1) == 1); + final boolean hasBlockOcclusion = (((blockOcclusion >> face.ordinal()) & 1) == 1); + final boolean hasBlockOcclusionOther = ((shapeImpl.blockOcclusion >> face.getOppositeFace().ordinal()) & 1) == 1; + final boolean hasAirOcclusionOther = ((shapeImpl.airOcclusion >> face.getOppositeFace().ordinal()) & 1) == 1; + if (blockEntry.lightEmission() > 0) return hasBlockOcclusionOther; + + // If either face is full, return true + if (hasBlockOcclusion || hasBlockOcclusionOther) return true; + + // If a single face is air, return false + if (hasAirOcclusion || hasAirOcclusionOther) return false; + + // Comparing two partial faces. Computation needed + List allRectangles = shapeImpl.computeOcclusionSet(face.getOppositeFace()); + allRectangles.addAll(computeOcclusionSet(face)); + return isFaceCovered(allRectangles) == 2; + } + @Override public boolean intersectBox(@NotNull Point position, @NotNull BoundingBox boundingBox) { - for (BoundingBox blockSection : blockSections) { + for (BoundingBox blockSection : collisionBoundingBoxes) { if (boundingBox.intersectBox(position, blockSection)) return true; } return false; @@ -91,7 +163,7 @@ final class ShapeImpl implements Shape { public boolean intersectBoxSwept(@NotNull Point rayStart, @NotNull Point rayDirection, @NotNull Point shapePos, @NotNull BoundingBox moving, @NotNull SweepResult finalResult) { boolean hitBlock = false; - for (BoundingBox blockSection : blockSections) { + for (BoundingBox blockSection : collisionBoundingBoxes) { // Fast check to see if a collision happens // Uses minkowski sum if (!RayUtils.BoundingBoxIntersectionCheck(moving, rayStart, rayDirection, blockSection, shapePos)) @@ -112,4 +184,76 @@ final class ShapeImpl implements Shape { if (block == null) this.block = block = Block.fromStateId((short) blockEntry.stateId()); return block; } + + private List computeOcclusionSet(BlockFace face) { + List 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 getRemaining(Rectangle covering, Rectangle toCover) { + List 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) { + } } diff --git a/src/main/java/net/minestom/server/entity/Entity.java b/src/main/java/net/minestom/server/entity/Entity.java index e43fb831b..00cc6f35e 100644 --- a/src/main/java/net/minestom/server/entity/Entity.java +++ b/src/main/java/net/minestom/server/entity/Entity.java @@ -666,7 +666,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev // Move a small amount towards the entity. If the entity is within 0.01 blocks of the block, touch will trigger Vec blockPos = new Vec(x, y, z); Point blockEntityVector = (blockPos.sub(position)).normalize().mul(0.01); - if (block.registry().collisionShape().intersectBox(position.sub(blockPos).add(blockEntityVector), boundingBox)) { + if (block.registry().shape().intersectBox(position.sub(blockPos).add(blockEntityVector), boundingBox)) { handler.onTouch(new BlockHandler.Touch(block, instance, new Vec(x, y, z), this)); } } diff --git a/src/main/java/net/minestom/server/instance/AnvilLoader.java b/src/main/java/net/minestom/server/instance/AnvilLoader.java index 87be945f3..96f0fb55f 100644 --- a/src/main/java/net/minestom/server/instance/AnvilLoader.java +++ b/src/main/java/net/minestom/server/instance/AnvilLoader.java @@ -127,8 +127,8 @@ public class AnvilLoader implements IChunkLoader { for (int sectionY = chunk.getMinSection(); sectionY < chunk.getMaxSection(); sectionY++) { var section = chunk.getSection(sectionY); var chunkSection = fileChunk.getSection((byte) sectionY); - section.setSkyLight(chunkSection.getSkyLights()); - section.setBlockLight(chunkSection.getBlockLights()); + section.skyLight().copyFrom(chunkSection.getSkyLights()); + section.blockLight().copyFrom(chunkSection.getBlockLights()); } mcaFile.forget(fileChunk); return CompletableFuture.completedFuture(chunk); diff --git a/src/main/java/net/minestom/server/instance/DynamicChunk.java b/src/main/java/net/minestom/server/instance/DynamicChunk.java index 8b17d1119..e3cf78493 100644 --- a/src/main/java/net/minestom/server/instance/DynamicChunk.java +++ b/src/main/java/net/minestom/server/instance/DynamicChunk.java @@ -52,7 +52,7 @@ public class DynamicChunk extends Chunk { public DynamicChunk(@NotNull Instance instance, int chunkX, int chunkZ) { super(instance, chunkX, chunkZ, true); var sectionsTemp = new Section[maxSection - minSection]; - Arrays.setAll(sectionsTemp, value -> new Section()); + Arrays.setAll(sectionsTemp, value -> Section.create()); this.sections = List.of(sectionsTemp); } @@ -71,6 +71,8 @@ public class DynamicChunk extends Chunk { Section section = getSectionAt(y); section.blockPalette() .set(toSectionRelativeCoordinate(x), toSectionRelativeCoordinate(y), toSectionRelativeCoordinate(z), block.stateId()); + //section.skyLight().invalidate(); TODO + section.blockLight().invalidate(); final int index = ChunkUtils.getBlockIndex(x, y, z); // Handler @@ -210,7 +212,7 @@ public class DynamicChunk extends Chunk { createLightData()); } - private synchronized @NotNull UpdateLightPacket createLightPacket() { + public synchronized @NotNull UpdateLightPacket createLightPacket() { return new UpdateLightPacket(chunkX, chunkZ, createLightData()); } @@ -225,8 +227,8 @@ public class DynamicChunk extends Chunk { int index = 0; for (Section section : sections) { index++; - final byte[] skyLight = section.getSkyLight(); - final byte[] blockLight = section.getBlockLight(); + final byte[] skyLight = section.skyLight().array(); + final byte[] blockLight = section.blockLight().array(); if (skyLight.length != 0) { skyLights.add(skyLight); skyMask.set(index); diff --git a/src/main/java/net/minestom/server/instance/GeneratorImpl.java b/src/main/java/net/minestom/server/instance/GeneratorImpl.java index e4169b8d9..1e67105fc 100644 --- a/src/main/java/net/minestom/server/instance/GeneratorImpl.java +++ b/src/main/java/net/minestom/server/instance/GeneratorImpl.java @@ -100,7 +100,7 @@ final class GeneratorImpl { this.width = 1; this.height = 1; this.depth = 1; - this.sections = List.of(section(new Section(), sectionX, sectionY, sectionZ, true)); + this.sections = List.of(section(Section.create(), sectionX, sectionY, sectionZ, true)); } else if (x < minSection.x() || y < minSection.y() || z < minSection.z() || x >= minSection.x() + width * 16 || y >= minSection.y() + height * 16 || z >= minSection.z() + depth * 16) { // Resize necessary @@ -134,7 +134,7 @@ final class GeneratorImpl { final int newX = coordinates.blockX() + startX; final int newY = coordinates.blockY() + startY; final int newZ = coordinates.blockZ() + startZ; - final GenerationUnit unit = section(new Section(), newX, newY, newZ, true); + final GenerationUnit unit = section(Section.create(), newX, newY, newZ, true); newSections[i] = unit; } } @@ -170,7 +170,7 @@ final class GeneratorImpl { for (int sectionX = minSectionX; sectionX < maxSectionX; sectionX++) { for (int sectionY = minSectionY; sectionY < maxSectionY; sectionY++) { for (int sectionZ = minSectionZ; sectionZ < maxSectionZ; sectionZ++) { - final GenerationUnit unit = section(new Section(), sectionX, sectionY, sectionZ, true); + final GenerationUnit unit = section(Section.create(), sectionX, sectionY, sectionZ, true); units[index++] = unit; } } diff --git a/src/main/java/net/minestom/server/instance/Section.java b/src/main/java/net/minestom/server/instance/Section.java index edda1189d..8d643bed9 100644 --- a/src/main/java/net/minestom/server/instance/Section.java +++ b/src/main/java/net/minestom/server/instance/Section.java @@ -1,64 +1,35 @@ package net.minestom.server.instance; +import net.minestom.server.instance.light.Light; import net.minestom.server.instance.palette.Palette; import net.minestom.server.utils.binary.BinaryWriter; import net.minestom.server.utils.binary.Writeable; import org.jetbrains.annotations.NotNull; -public final class Section implements Writeable { - private Palette blockPalette; - private Palette biomePalette; - private byte[] skyLight; - private byte[] blockLight; - - private Section(Palette blockPalette, Palette biomePalette, - byte[] skyLight, byte[] blockLight) { - this.blockPalette = blockPalette; - this.biomePalette = biomePalette; - this.skyLight = skyLight; - this.blockLight = blockLight; - } - - public Section() { - this(Palette.blocks(), Palette.biomes(), - new byte[0], new byte[0]); - } - - public Palette blockPalette() { - return blockPalette; - } - - public Palette 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 record Section(Palette blockPalette, Palette biomePalette, + Light skyLight, Light blockLight) implements Writeable { + static Section create() { + final Palette blockPalette = Palette.blocks(); + final Palette biomePalette = Palette.biomes(); + final Light skyLight = Light.sky(blockPalette); + final Light blockLight = Light.block(blockPalette); + return new Section(blockPalette, biomePalette, skyLight, blockLight); } public void clear() { this.blockPalette.fill(0); this.biomePalette.fill(0); - this.skyLight = new byte[0]; - this.blockLight = new byte[0]; + this.skyLight.invalidate(); + this.blockLight.invalidate(); } @Override public @NotNull Section clone() { - return new Section(blockPalette.clone(), biomePalette.clone(), - skyLight.clone(), blockLight.clone()); + final Palette blockPalette = this.blockPalette.clone(); + final Palette biomePalette = this.biomePalette.clone(); + final Light skyLight = Light.sky(blockPalette); + final Light blockLight = Light.block(blockPalette); + return new Section(blockPalette, biomePalette, skyLight, blockLight); } @Override diff --git a/src/main/java/net/minestom/server/instance/light/BlockLight.java b/src/main/java/net/minestom/server/instance/light/BlockLight.java new file mode 100644 index 000000000..ce3fa7c1d --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/BlockLight.java @@ -0,0 +1,38 @@ +package net.minestom.server.instance.light; + +import net.minestom.server.instance.palette.Palette; +import org.jetbrains.annotations.NotNull; + +final class BlockLight implements Light { + private final Palette blockPalette; + private volatile byte[] content; + + BlockLight(Palette blockPalette) { + this.blockPalette = blockPalette; + } + + @Override + public byte @NotNull [] array() { + byte[] content = this.content; + if (content == null) { + synchronized (this) { + content = this.content; + if (content == null) { + var result = BlockLightCompute.compute(blockPalette); + this.content = content = result.light(); + } + } + } + return content.clone(); + } + + @Override + public void copyFrom(byte @NotNull [] array) { + this.content = array.clone(); + } + + @Override + public void invalidate() { + this.content = null; + } +} diff --git a/src/main/java/net/minestom/server/instance/light/BlockLightCompute.java b/src/main/java/net/minestom/server/instance/light/BlockLightCompute.java new file mode 100644 index 000000000..560598d58 --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/BlockLightCompute.java @@ -0,0 +1,120 @@ +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.Arrays; +import java.util.Objects; + +final class BlockLightCompute { + private static final BlockFace[] FACES = BlockFace.values(); + private static final Direction[] DIRECTIONS = Direction.values(); + static final int SECTION_SIZE = 16; + static final int LIGHT_LENGTH = 16 * 16 * 16 / 2; + static final int SIDE_LENGTH = 16 * 16 * DIRECTIONS.length / 2; + + static @NotNull Result compute(Palette blockPalette) { + Block[] blocks = new Block[4096]; + byte[] lightArray = new byte[LIGHT_LENGTH]; + byte[][] borders = new byte[DIRECTIONS.length][]; + Arrays.setAll(borders, i -> new byte[SIDE_LENGTH]); + IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue(); + + 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)); + placeLight(lightArray, index, lightEmission); + } + }); + + while (!lightSources.isEmpty()) { + final int index = lightSources.dequeueInt(); + 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); + final Block currentBlock = Objects.requireNonNullElse(blocks[x | (z << 4) | (y << 8)], Block.AIR); + final Block propagatedBlock = Objects.requireNonNullElse(blocks[newIndex], Block.AIR); + if (currentBlock.registry().shape().isOccluded(propagatedBlock.registry().shape(), face)) + continue; + if (getLight(lightArray, newIndex) + 2 <= lightLevel) { + placeLight(lightArray, newIndex, newLightLevel); + lightSources.enqueue(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; + assert borders.length == FACES.length; + } + + public byte getLight(int x, int y, int z) { + final boolean outX = x < 0 || x >= SECTION_SIZE; + final boolean outY = y < 0 || y >= SECTION_SIZE; + final boolean outZ = z < 0 || z >= SECTION_SIZE; + if (outX || outY || outZ) { + final boolean multipleOut = outX ? (outY || outZ) : (outY && outZ); + if (multipleOut) + throw new IllegalArgumentException("Coordinates are out of bounds: " + x + ", " + y + ", " + z); + if (outX) { + // WEST or EAST + if (x < 0) return borders[BlockFace.WEST.ordinal()][y * SECTION_SIZE + z]; + else return borders[BlockFace.EAST.ordinal()][y * SECTION_SIZE + z]; + } else if (outY) { + // BOTTOM or TOP + if (y < 0) return borders[BlockFace.BOTTOM.ordinal()][x * SECTION_SIZE + z]; + else return borders[BlockFace.TOP.ordinal()][x * SECTION_SIZE + z]; + } else { + // NORTH or SOUTH + if (z < 0) return borders[BlockFace.NORTH.ordinal()][x * SECTION_SIZE + y]; + else return borders[BlockFace.SOUTH.ordinal()][x * SECTION_SIZE + y]; + } + } else return (byte) BlockLightCompute.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)); + } + + private static int getLight(byte[] light, int index) { + final int value = light[index >>> 1]; + return ((value >>> ((index & 1) << 2)) & 0xF); + } +} diff --git a/src/main/java/net/minestom/server/instance/light/Light.java b/src/main/java/net/minestom/server/instance/light/Light.java new file mode 100644 index 000000000..e9d2322bc --- /dev/null +++ b/src/main/java/net/minestom/server/instance/light/Light.java @@ -0,0 +1,22 @@ +package net.minestom.server.instance.light; + +import net.minestom.server.instance.palette.Palette; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +public interface Light { + static Light sky(@NotNull Palette blockPalette) { + return new BlockLight(blockPalette); + } + + static Light block(@NotNull Palette blockPalette) { + return new BlockLight(blockPalette); + } + + @ApiStatus.Internal + byte @NotNull [] array(); + + void copyFrom(byte @NotNull [] array); + + void invalidate(); +} diff --git a/src/main/java/net/minestom/server/registry/Registry.java b/src/main/java/net/minestom/server/registry/Registry.java index a9ad4b530..9b21ccc13 100644 --- a/src/main/java/net/minestom/server/registry/Registry.java +++ b/src/main/java/net/minestom/server/registry/Registry.java @@ -164,6 +164,7 @@ public final class Registry { private final boolean air; private final boolean solid; private final boolean liquid; + private final int lightEmission; private final String blockEntity; private final int blockEntityId; private final Supplier materialSupplier; @@ -184,6 +185,7 @@ public final class Registry { this.air = main.getBoolean("air", false); this.solid = main.getBoolean("solid"); this.liquid = main.getBoolean("liquid", false); + this.lightEmission = main.getInt("lightEmission", 0); { Properties blockEntity = main.section("blockEntity"); if (blockEntity != null) { @@ -199,8 +201,9 @@ public final class Registry { this.materialSupplier = materialNamespace != null ? () -> Material.fromNamespaceId(materialNamespace) : () -> null; } { - final String string = main.getString("collisionShape"); - this.shape = CollisionUtils.parseBlockShape(string, this); + final String collision = main.getString("collisionShape"); + final String occlusion = main.getString("occlusionShape"); + this.shape = CollisionUtils.parseBlockShape(collision, occlusion, this); } } @@ -252,6 +255,10 @@ public final class Registry { return liquid; } + public int lightEmission() { + return lightEmission; + } + public boolean isBlockEntity() { return blockEntity != null; } @@ -268,7 +275,7 @@ public final class Registry { return materialSupplier.get(); } - public Shape collisionShape() { + public Shape shape() { return shape; } diff --git a/src/test/java/net/minestom/server/coordinate/CoordinateTest.java b/src/test/java/net/minestom/server/coordinate/CoordinateTest.java index 560cf470c..ddc5c94e2 100644 --- a/src/test/java/net/minestom/server/coordinate/CoordinateTest.java +++ b/src/test/java/net/minestom/server/coordinate/CoordinateTest.java @@ -97,6 +97,7 @@ public class CoordinateTest { public void toSectionRelativeCoordinate() { assertEquals(8, ChunkUtils.toSectionRelativeCoordinate(-40)); assertEquals(12, ChunkUtils.toSectionRelativeCoordinate(-20)); + assertEquals(15, ChunkUtils.toSectionRelativeCoordinate(-1)); assertEquals(0, ChunkUtils.toSectionRelativeCoordinate(0)); assertEquals(5, ChunkUtils.toSectionRelativeCoordinate(5)); assertEquals(15, ChunkUtils.toSectionRelativeCoordinate(15)); diff --git a/src/test/java/net/minestom/server/instance/BlockTest.java b/src/test/java/net/minestom/server/instance/BlockTest.java index 5fa43d523..aa70bff72 100644 --- a/src/test/java/net/minestom/server/instance/BlockTest.java +++ b/src/test/java/net/minestom/server/instance/BlockTest.java @@ -79,8 +79,8 @@ public class BlockTest { @Test public void testShape() { - Point start = Block.LANTERN.registry().collisionShape().relativeStart(); - Point end = Block.LANTERN.registry().collisionShape().relativeEnd(); + Point start = Block.LANTERN.registry().shape().relativeStart(); + Point end = Block.LANTERN.registry().shape().relativeEnd(); assertEquals(start, new Vec(0.312, 0, 0.312)); assertEquals(end, new Vec(0.687, 0.562, 0.687)); diff --git a/src/test/java/net/minestom/server/instance/light/BlockIsOccludedTest.java b/src/test/java/net/minestom/server/instance/light/BlockIsOccludedTest.java new file mode 100644 index 000000000..055065bd5 --- /dev/null +++ b/src/test/java/net/minestom/server/instance/light/BlockIsOccludedTest.java @@ -0,0 +1,194 @@ +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().shape(); + + for (BlockFace face : BlockFace.values()) { + assertFalse(airBlock.isOccluded(airBlock, face)); + } + } + + @Test + public void blockLantern() { + Shape shape = Block.LANTERN.registry().shape(); + Shape airBlock = Block.AIR.registry().shape(); + + for (BlockFace face : BlockFace.values()) { + assertFalse(shape.isOccluded(airBlock, face)); + } + } + + @Test + public void blockCauldron() { + Shape shape = Block.CAULDRON.registry().shape(); + Shape airBlock = Block.AIR.registry().shape(); + + for (BlockFace face : BlockFace.values()) { + assertFalse(shape.isOccluded(airBlock, face)); + } + } + + @Test + public void blockSlabBottomAir() { + Shape shape = Block.SANDSTONE_SLAB.registry().shape(); + Shape airBlock = Block.AIR.registry().shape(); + + 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().shape(); + Shape shape2 = Block.ENCHANTING_TABLE.registry().shape(); + + 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().shape(); + + Shape airBlock = Block.AIR.registry().shape(); + + 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().shape(); + Shape stoneBlock = Block.STONE.registry().shape(); + + 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().shape(); + Shape airBlock = Block.AIR.registry().shape(); + + for (BlockFace face : BlockFace.values()) { + assertTrue(shape.isOccluded(airBlock, face)); + } + } + + @Test + public void blockStair() { + Shape shape = Block.SANDSTONE_STAIRS.registry().shape(); + Shape airBlock = Block.AIR.registry().shape(); + + 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().shape(); + Shape airBlock = Block.AIR.registry().shape(); + + 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().shape(); + Shape shape2 = Block.SANDSTONE_SLAB.withProperty("type", "top").registry().shape(); + + 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().shape(); + + 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().shape(); + Shape shape2 = Block.SANDSTONE_SLAB.registry().shape(); + + 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().shape(); + Shape shape2 = Block.SANDSTONE_SLAB.withProperty("type", "top").registry().shape(); + + 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)); + } +} diff --git a/src/test/java/net/minestom/server/instance/light/BlockLightTest.java b/src/test/java/net/minestom/server/instance/light/BlockLightTest.java new file mode 100644 index 000000000..e63af0afb --- /dev/null +++ b/src/test/java/net/minestom/server/instance/light/BlockLightTest.java @@ -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 = BlockLightCompute.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 = BlockLightCompute.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 = BlockLightCompute.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 = BlockLightCompute.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 = BlockLightCompute.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 = BlockLightCompute.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 = BlockLightCompute.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 = BlockLightCompute.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 = BlockLightCompute.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 = BlockLightCompute.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(BlockLightCompute.Result result, Map expectedLights) { + List errors = new ArrayList<>(); + for (int x = -1; x < 17; x++) { + for (int y = -1; y < 17; y++) { + for (int z = -1; z < 17; 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(); + } + } +} diff --git a/src/test/java/net/minestom/server/instance/light/LightParityTest.java b/src/test/java/net/minestom/server/instance/light/LightParityTest.java new file mode 100644 index 000000000..799f6b955 --- /dev/null +++ b/src/test/java/net/minestom/server/instance/light/LightParityTest.java @@ -0,0 +1,80 @@ +package net.minestom.server.instance.light; + +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.palette.Palette; +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.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class LightParityTest { + + @Test + public void test() throws URISyntaxException, IOException, AnvilException { + Map sections = retrieveSections(); + // Generate our own light + Map results = new HashMap<>(); + for (var entry : sections.entrySet()) { + var vec = entry.getKey(); + var palette = entry.getValue().blocks; + results.put(vec, BlockLightCompute.compute(palette)); + } + // TODO merge lights and compare + } + + record SectionEntry(Palette blocks, byte[] sky, byte[] block) { + } + + private static Map retrieveSections() throws IOException, URISyntaxException, AnvilException { + URL defaultImage = LightParityTest.class.getResource("/region.mca"); + assert defaultImage != null; + File imageFile = new File(defaultImage.toURI()); + var regionFile = new RegionFile(new RandomAccessFile(imageFile, "rw"), + 0, 0, -64, 384); + + Map sections = new HashMap<>(); + // Read from anvil + // TODO: read all 32x32 chunks + for (int x = 0; x < 4; x++) { + for (int z = 0; z < 4; z++) { + var chunk = regionFile.getChunk(x, z); + if (chunk == null) continue; + for (var section : chunk.getSections().values()) { + 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; + } +} diff --git a/src/test/resources/region.mca b/src/test/resources/region.mca new file mode 100644 index 000000000..825e34355 Binary files /dev/null and b/src/test/resources/region.mca differ