Light engine

This commit is contained in:
themode 2022-03-22 03:18:59 +01:00 committed by TheMode
parent 765d6057da
commit 8dfaa90d13
21 changed files with 935 additions and 75 deletions

View File

@ -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) {

View File

@ -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;
}

View File

@ -4,6 +4,7 @@ import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity;
import net.minestom.server.instance.block.BlockFace;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@ -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";

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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) {
}
}

View File

@ -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));
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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));

View File

@ -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));

View File

@ -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));
}
}

View File

@ -0,0 +1,239 @@
package net.minestom.server.instance.light;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.palette.Palette;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static java.util.Map.entry;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class BlockLightTest {
@Test
public void empty() {
var palette = Palette.blocks();
var result = 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();
}
}
}

View File

@ -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.