mirror of https://github.com/Minestom/Minestom.git
Light engine
This commit is contained in:
parent
765d6057da
commit
8dfaa90d13
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<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) {
|
||||
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<Rectangle> allRectangles = shapeImpl.computeOcclusionSet(face.getOppositeFace());
|
||||
allRectangles.addAll(computeOcclusionSet(face));
|
||||
return isFaceCovered(allRectangles) == 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean intersectBox(@NotNull Point position, @NotNull BoundingBox boundingBox) {
|
||||
for (BoundingBox blockSection : blockSections) {
|
||||
for (BoundingBox blockSection : collisionBoundingBoxes) {
|
||||
if (boundingBox.intersectBox(position, blockSection)) return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -91,7 +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<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) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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<Material> 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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<Vec, Integer> expectedLights) {
|
||||
List<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Vec, SectionEntry> sections = retrieveSections();
|
||||
// Generate our own light
|
||||
Map<Vec, BlockLightCompute.Result> 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<Vec, SectionEntry> 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<Vec, SectionEntry> 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;
|
||||
}
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue