mirror of https://github.com/Minestom/Minestom.git
hollow-cube/lighting-memory-reduction
Lighting reduce memory + Fix lighting not sending + Performance (#31) * Reduce memory * Clone * Executor pool + cleanup * Cleanup * Don't batch, it's slower * Parallel chunk loading for test * Check below chunk 6. Sky light data doesn't appear to be saved above the highest point in the chunk height map. * Fix weird locking * ඞ * Fix test * Fix indentation * Use short instead of int * Use short instead of int * Start removing borders * Borders gone * Cleanup * Cleanup * Remove borders fully - Still needs cleanup * Cleanup 1 * Cleanup 2 * Cleanup 3 * Cleanup 4 * Cache * Performance * Performance * Cleanup * Cleanup * Refactor * Cleanup from self-review
This commit is contained in:
parent
e9f62f4657
commit
12aa1e6b7b
|
@ -21,10 +21,11 @@ import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static net.minestom.server.instance.light.LightCompute.emptyContent;
|
import static net.minestom.server.instance.light.LightCompute.emptyContent;
|
||||||
|
|
||||||
|
@ -33,15 +34,23 @@ public class LightingChunk extends DynamicChunk {
|
||||||
private static final int LIGHTING_CHUNKS_PER_SEND = Integer.getInteger("minestom.lighting.chunks-per-send", 10);
|
private static final int LIGHTING_CHUNKS_PER_SEND = Integer.getInteger("minestom.lighting.chunks-per-send", 10);
|
||||||
private static final int LIGHTING_CHUNKS_SEND_DELAY = Integer.getInteger("minestom.lighting.chunks-send-delay", 100);
|
private static final int LIGHTING_CHUNKS_SEND_DELAY = Integer.getInteger("minestom.lighting.chunks-send-delay", 100);
|
||||||
|
|
||||||
|
private static final ExecutorService pool = Executors.newWorkStealingPool();
|
||||||
|
|
||||||
private int[] heightmap;
|
private int[] heightmap;
|
||||||
final CachedPacket lightCache = new CachedPacket(this::createLightPacket);
|
final CachedPacket lightCache = new CachedPacket(this::createLightPacket);
|
||||||
boolean sendNeighbours = true;
|
boolean sendNeighbours = true;
|
||||||
|
boolean chunkLoaded = false;
|
||||||
|
|
||||||
enum LightType {
|
enum LightType {
|
||||||
SKY,
|
SKY,
|
||||||
BLOCK
|
BLOCK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum QueueType {
|
||||||
|
INTERNAL,
|
||||||
|
EXTERNAL
|
||||||
|
}
|
||||||
|
|
||||||
private static final Set<NamespaceID> DIFFUSE_SKY_LIGHT = Set.of(
|
private static final Set<NamespaceID> DIFFUSE_SKY_LIGHT = Set.of(
|
||||||
Block.COBWEB.namespace(),
|
Block.COBWEB.namespace(),
|
||||||
Block.ICE.namespace(),
|
Block.ICE.namespace(),
|
||||||
|
@ -111,9 +120,10 @@ public class LightingChunk extends DynamicChunk {
|
||||||
|
|
||||||
// Invalidate neighbor chunks, since they can be updated by this block change
|
// Invalidate neighbor chunks, since they can be updated by this block change
|
||||||
int coordinate = ChunkUtils.getChunkCoordinate(y);
|
int coordinate = ChunkUtils.getChunkCoordinate(y);
|
||||||
invalidateSection(coordinate);
|
if (chunkLoaded) {
|
||||||
|
invalidateSection(coordinate);
|
||||||
this.lightCache.invalidate();
|
this.lightCache.invalidate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendLighting() {
|
public void sendLighting() {
|
||||||
|
@ -124,6 +134,7 @@ public class LightingChunk extends DynamicChunk {
|
||||||
@Override
|
@Override
|
||||||
protected void onLoad() {
|
protected void onLoad() {
|
||||||
// Prefetch the chunk packet so that lazy lighting is computed
|
// Prefetch the chunk packet so that lazy lighting is computed
|
||||||
|
chunkLoaded = true;
|
||||||
updateAfterGeneration(this);
|
updateAfterGeneration(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,7 +287,7 @@ public class LightingChunk extends DynamicChunk {
|
||||||
for (LightingChunk f : copy) {
|
for (LightingChunk f : copy) {
|
||||||
if (f.isLoaded()) {
|
if (f.isLoaded()) {
|
||||||
f.sendLighting();
|
f.sendLighting();
|
||||||
if (f.getViewers().size() == 0) return;
|
if (f.getViewers().size() == 0) continue;
|
||||||
}
|
}
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
|
@ -293,51 +304,56 @@ public class LightingChunk extends DynamicChunk {
|
||||||
lightLock.unlock();
|
lightLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void flushQueue(Instance instance, Set<Point> queue, LightType type) {
|
private static void flushQueue(Instance instance, Set<Point> queue, LightType type, QueueType queueType) {
|
||||||
var updateQueue =
|
AtomicInteger count = new AtomicInteger(0);
|
||||||
queue.parallelStream()
|
Set<Light> sections = ConcurrentHashMap.newKeySet();
|
||||||
.map(sectionLocation -> {
|
Set<Point> newQueue = ConcurrentHashMap.newKeySet();
|
||||||
Chunk chunk = instance.getChunk(sectionLocation.blockX(), sectionLocation.blockZ());
|
|
||||||
if (chunk == null) return null;
|
|
||||||
|
|
||||||
if (type == LightType.BLOCK) {
|
for (Point point : queue) {
|
||||||
return chunk.getSection(sectionLocation.blockY()).blockLight()
|
Chunk chunk = instance.getChunk(point.blockX(), point.blockZ());
|
||||||
.calculateExternal(instance, chunk, sectionLocation.blockY());
|
if (chunk == null) {
|
||||||
} else {
|
count.getAndIncrement();
|
||||||
return chunk.getSection(sectionLocation.blockY()).skyLight()
|
continue;
|
||||||
.calculateExternal(instance, chunk, sectionLocation.blockY());
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.toList()
|
|
||||||
.parallelStream()
|
|
||||||
.flatMap(light -> light.flip().stream())
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
if (updateQueue.size() > 0) {
|
var light = type == LightType.BLOCK ? chunk.getSection(point.blockY()).blockLight() : chunk.getSection(point.blockY()).skyLight();
|
||||||
flushQueue(instance, updateQueue, type);
|
|
||||||
|
pool.submit(() -> {
|
||||||
|
if (queueType == QueueType.INTERNAL) light.calculateInternal(instance, chunk.getChunkX(), point.blockY(), chunk.getChunkZ());
|
||||||
|
else light.calculateExternal(instance, chunk, point.blockY());
|
||||||
|
|
||||||
|
sections.add(light);
|
||||||
|
|
||||||
|
var toAdd = light.flip();
|
||||||
|
if (toAdd != null) newQueue.addAll(toAdd);
|
||||||
|
|
||||||
|
count.incrementAndGet();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
while (count.get() < queue.size()) { }
|
||||||
|
|
||||||
|
if (newQueue.size() > 0) {
|
||||||
|
flushQueue(instance, newQueue, type, QueueType.EXTERNAL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void relight(Instance instance, Collection<Chunk> chunks) {
|
public static void relight(Instance instance, Collection<Chunk> chunks) {
|
||||||
Set<Point> toPropagate = chunks
|
Set<Point> sections = new HashSet<>();
|
||||||
.parallelStream()
|
|
||||||
.flatMap(chunk -> IntStream
|
|
||||||
.range(chunk.getMinSection(), chunk.getMaxSection())
|
|
||||||
.mapToObj(index -> Map.entry(index, chunk)))
|
|
||||||
.map(chunkIndex -> {
|
|
||||||
final Chunk chunk = chunkIndex.getValue();
|
|
||||||
final int section = chunkIndex.getKey();
|
|
||||||
|
|
||||||
chunk.getSection(section).blockLight().invalidate();
|
for (Chunk chunk : chunks) {
|
||||||
chunk.getSection(section).skyLight().invalidate();
|
if (chunk == null) continue;
|
||||||
|
for (int section = chunk.minSection; section < chunk.maxSection; section++) {
|
||||||
|
chunk.getSection(section).blockLight().invalidate();
|
||||||
|
chunk.getSection(section).skyLight().invalidate();
|
||||||
|
|
||||||
return new Vec(chunk.getChunkX(), section, chunk.getChunkZ());
|
sections.add(new Vec(chunk.getChunkX(), section, chunk.getChunkZ()));
|
||||||
}).collect(Collectors.toSet());
|
}
|
||||||
|
}
|
||||||
|
|
||||||
synchronized (instance) {
|
synchronized (instance) {
|
||||||
relight(instance, toPropagate, LightType.BLOCK);
|
relight(instance, sections, LightType.BLOCK);
|
||||||
relight(instance, toPropagate, LightType.SKY);
|
relight(instance, sections, LightType.SKY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,33 +420,8 @@ public class LightingChunk extends DynamicChunk {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void relight(Instance instance, Set<Point> sections, LightType type) {
|
private static void relight(Instance instance, Set<Point> queue, LightType type) {
|
||||||
Set<Point> toPropagate = sections
|
flushQueue(instance, queue, type, QueueType.INTERNAL);
|
||||||
.parallelStream()
|
|
||||||
// .stream()
|
|
||||||
.map(chunkIndex -> {
|
|
||||||
final Chunk chunk = instance.getChunk(chunkIndex.blockX(), chunkIndex.blockZ());
|
|
||||||
final int section = chunkIndex.blockY();
|
|
||||||
if (chunk == null) return null;
|
|
||||||
if (type == LightType.BLOCK) return chunk.getSection(section).blockLight().calculateInternal(chunk.getInstance(), chunk.getChunkX(), section, chunk.getChunkZ());
|
|
||||||
else return chunk.getSection(section).skyLight().calculateInternal(chunk.getInstance(), chunk.getChunkX(), section, chunk.getChunkZ());
|
|
||||||
}).filter(Objects::nonNull)
|
|
||||||
.flatMap(lightSet -> lightSet.flip().stream())
|
|
||||||
.collect(Collectors.toSet())
|
|
||||||
// .stream()
|
|
||||||
.parallelStream()
|
|
||||||
.flatMap(sectionLocation -> {
|
|
||||||
final Chunk chunk = instance.getChunk(sectionLocation.blockX(), sectionLocation.blockZ());
|
|
||||||
final int section = sectionLocation.blockY();
|
|
||||||
if (chunk == null) return Stream.empty();
|
|
||||||
|
|
||||||
final Light light = type == LightType.BLOCK ? chunk.getSection(section).blockLight() : chunk.getSection(section).skyLight();
|
|
||||||
light.calculateExternal(chunk.getInstance(), chunk, section);
|
|
||||||
|
|
||||||
return light.flip().stream();
|
|
||||||
}).collect(Collectors.toSet());
|
|
||||||
|
|
||||||
flushQueue(instance, toPropagate, type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -20,6 +20,13 @@ public final class Section implements NetworkBuffer.Writer {
|
||||||
this.blockLight = Light.block(blockPalette);
|
this.blockLight = Light.block(blockPalette);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Section(Palette blockPalette, Palette biomePalette, Light skyLight, Light blockLight) {
|
||||||
|
this.blockPalette = blockPalette;
|
||||||
|
this.biomePalette = biomePalette;
|
||||||
|
this.skyLight = skyLight;
|
||||||
|
this.blockLight = blockLight;
|
||||||
|
}
|
||||||
|
|
||||||
public Section() {
|
public Section() {
|
||||||
this(Palette.blocks(), Palette.biomes());
|
this(Palette.blocks(), Palette.biomes());
|
||||||
}
|
}
|
||||||
|
@ -39,7 +46,13 @@ public final class Section implements NetworkBuffer.Writer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull Section clone() {
|
public @NotNull Section clone() {
|
||||||
return new Section(this.blockPalette.clone(), this.biomePalette.clone());
|
final Light skyLight = Light.sky(blockPalette);
|
||||||
|
final Light blockLight = Light.block(blockPalette);
|
||||||
|
|
||||||
|
skyLight.set(this.skyLight.array());
|
||||||
|
blockLight.set(this.blockLight.array());
|
||||||
|
|
||||||
|
return new Section(this.blockPalette.clone(), this.biomePalette.clone(), skyLight, blockLight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package net.minestom.server.instance.light;
|
package net.minestom.server.instance.light;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
|
import it.unimi.dsi.fastutil.shorts.ShortArrayFIFOQueue;
|
||||||
import net.minestom.server.coordinate.Point;
|
import net.minestom.server.coordinate.Point;
|
||||||
import net.minestom.server.coordinate.Vec;
|
import net.minestom.server.coordinate.Vec;
|
||||||
import net.minestom.server.instance.Chunk;
|
import net.minestom.server.instance.Chunk;
|
||||||
|
@ -15,6 +15,7 @@ import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import static net.minestom.server.instance.light.LightCompute.*;
|
import static net.minestom.server.instance.light.LightCompute.*;
|
||||||
|
|
||||||
|
@ -25,12 +26,11 @@ final class BlockLight implements Light {
|
||||||
private byte[] contentPropagation;
|
private byte[] contentPropagation;
|
||||||
private byte[] contentPropagationSwap;
|
private byte[] contentPropagationSwap;
|
||||||
|
|
||||||
private byte[][] borders;
|
|
||||||
private byte[][] bordersPropagation;
|
|
||||||
private byte[][] bordersPropagationSwap;
|
|
||||||
private boolean isValidBorders = true;
|
private boolean isValidBorders = true;
|
||||||
private boolean needsSend = true;
|
private boolean needsSend = true;
|
||||||
|
|
||||||
private Set<Point> toUpdateSet = new HashSet<>();
|
private Set<Point> toUpdateSet = new HashSet<>();
|
||||||
|
private final Section[] neighborSections = new Section[BlockFace.values().length];
|
||||||
|
|
||||||
BlockLight(Palette blockPalette) {
|
BlockLight(Palette blockPalette) {
|
||||||
this.blockPalette = blockPalette;
|
this.blockPalette = blockPalette;
|
||||||
|
@ -38,21 +38,17 @@ final class BlockLight implements Light {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<Point> flip() {
|
public Set<Point> flip() {
|
||||||
if (this.bordersPropagationSwap != null)
|
|
||||||
this.bordersPropagation = this.bordersPropagationSwap;
|
|
||||||
|
|
||||||
if (this.contentPropagationSwap != null)
|
if (this.contentPropagationSwap != null)
|
||||||
this.contentPropagation = this.contentPropagationSwap;
|
this.contentPropagation = this.contentPropagationSwap;
|
||||||
|
|
||||||
this.bordersPropagationSwap = null;
|
|
||||||
this.contentPropagationSwap = null;
|
this.contentPropagationSwap = null;
|
||||||
|
|
||||||
if (toUpdateSet == null) return Set.of();
|
if (toUpdateSet == null) return Set.of();
|
||||||
return toUpdateSet;
|
return toUpdateSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
static IntArrayFIFOQueue buildInternalQueue(Palette blockPalette, Block[] blocks) {
|
static ShortArrayFIFOQueue buildInternalQueue(Palette blockPalette) {
|
||||||
IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue();
|
ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue();
|
||||||
// Apply section light
|
// Apply section light
|
||||||
blockPalette.getAllPresent((x, y, z, stateId) -> {
|
blockPalette.getAllPresent((x, y, z, stateId) -> {
|
||||||
final Block block = Block.fromStateId((short) stateId);
|
final Block block = Block.fromStateId((short) stateId);
|
||||||
|
@ -60,9 +56,8 @@ final class BlockLight implements Light {
|
||||||
final byte lightEmission = (byte) block.registry().lightEmission();
|
final byte lightEmission = (byte) block.registry().lightEmission();
|
||||||
|
|
||||||
final int index = x | (z << 4) | (y << 8);
|
final int index = x | (z << 4) | (y << 8);
|
||||||
blocks[index] = block;
|
|
||||||
if (lightEmission > 0) {
|
if (lightEmission > 0) {
|
||||||
lightSources.enqueue(index | (lightEmission << 12));
|
lightSources.enqueue((short) (index | (lightEmission << 12)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return lightSources;
|
return lightSources;
|
||||||
|
@ -72,41 +67,55 @@ final class BlockLight implements Light {
|
||||||
return Block.fromStateId((short)palette.get(x, y, z));
|
return Block.fromStateId((short)palette.get(x, y, z));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IntArrayFIFOQueue buildExternalQueue(Instance instance, Block[] blocks, Map<BlockFace, Point> neighbors, byte[][] borders) {
|
private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockPalette, Point[] neighbors, byte[] content) {
|
||||||
IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue();
|
ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue();
|
||||||
|
|
||||||
for (BlockFace face : BlockFace.values()) {
|
for (int i = 0; i < neighbors.length; i++) {
|
||||||
Point neighborSection = neighbors.get(face);
|
var face = BlockFace.values()[i];
|
||||||
|
Point neighborSection = neighbors[i];
|
||||||
if (neighborSection == null) continue;
|
if (neighborSection == null) continue;
|
||||||
|
|
||||||
Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ());
|
Section otherSection = neighborSections[face.ordinal()];
|
||||||
if (chunk == null) continue;
|
|
||||||
|
|
||||||
byte[] neighborFace = chunk.getSection(neighborSection.blockY()).blockLight().getBorderPropagation(face.getOppositeFace());
|
if (otherSection == null) {
|
||||||
if (neighborFace == null) continue;
|
Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ());
|
||||||
|
if (chunk == null) continue;
|
||||||
|
|
||||||
|
otherSection = chunk.getSection(neighborSection.blockY());
|
||||||
|
neighborSections[face.ordinal()] = otherSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherLight = otherSection.blockLight();
|
||||||
|
|
||||||
for (int bx = 0; bx < 16; bx++) {
|
for (int bx = 0; bx < 16; bx++) {
|
||||||
for (int by = 0; by < 16; by++) {
|
for (int by = 0; by < 16; by++) {
|
||||||
final int borderIndex = bx * SECTION_SIZE + by;
|
|
||||||
byte lightEmission = neighborFace[borderIndex];
|
|
||||||
|
|
||||||
if (borders != null && borders[face.ordinal()] != null) {
|
|
||||||
final int internalEmission = borders[face.ordinal()][borderIndex];
|
|
||||||
if (lightEmission <= internalEmission) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int k = switch (face) {
|
final int k = switch (face) {
|
||||||
case WEST, BOTTOM, NORTH -> 0;
|
case WEST, BOTTOM, NORTH -> 0;
|
||||||
case EAST, TOP, SOUTH -> 15;
|
case EAST, TOP, SOUTH -> 15;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final byte lightEmission = (byte) Math.max(switch (face) {
|
||||||
|
case NORTH, SOUTH -> (byte) otherLight.getLevel(bx, by, 15 - k);
|
||||||
|
case WEST, EAST -> (byte) otherLight.getLevel(15 - k, bx, by);
|
||||||
|
default -> (byte) otherLight.getLevel(bx, 15 - k, by);
|
||||||
|
} - 1, 0);
|
||||||
|
|
||||||
final int posTo = switch (face) {
|
final int posTo = switch (face) {
|
||||||
case NORTH, SOUTH -> bx | (k << 4) | (by << 8);
|
case NORTH, SOUTH -> bx | (k << 4) | (by << 8);
|
||||||
case WEST, EAST -> k | (by << 4) | (bx << 8);
|
case WEST, EAST -> k | (by << 4) | (bx << 8);
|
||||||
default -> bx | (by << 4) | (k << 8);
|
default -> bx | (by << 4) | (k << 8);
|
||||||
};
|
};
|
||||||
|
|
||||||
Section otherSection = chunk.getSection(neighborSection.blockY());
|
if (content != null) {
|
||||||
|
final int internalEmission = (byte) (Math.max(getLight(content, posTo) - 1, 0));
|
||||||
|
if (lightEmission <= internalEmission) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Block blockTo = switch(face) {
|
||||||
|
case NORTH, SOUTH -> getBlock(blockPalette, bx, by, k);
|
||||||
|
case WEST, EAST -> getBlock(blockPalette, k, bx, by);
|
||||||
|
default -> getBlock(blockPalette, bx, k, by);
|
||||||
|
};
|
||||||
|
|
||||||
final Block blockFrom = (switch (face) {
|
final Block blockFrom = (switch (face) {
|
||||||
case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k);
|
case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k);
|
||||||
|
@ -114,9 +123,6 @@ final class BlockLight implements Light {
|
||||||
default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by);
|
default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blocks == null) continue;
|
|
||||||
Block blockTo = blocks[posTo];
|
|
||||||
|
|
||||||
if (blockTo == null && blockFrom != null) {
|
if (blockTo == null && blockFrom != null) {
|
||||||
if (blockFrom.registry().collisionShape().isOccluded(Block.AIR.registry().collisionShape(), face.getOppositeFace()))
|
if (blockFrom.registry().collisionShape().isOccluded(Block.AIR.registry().collisionShape(), face.getOppositeFace()))
|
||||||
continue;
|
continue;
|
||||||
|
@ -130,7 +136,7 @@ final class BlockLight implements Light {
|
||||||
|
|
||||||
if (lightEmission > 0) {
|
if (lightEmission > 0) {
|
||||||
final int index = posTo | (lightEmission << 12);
|
final int index = posTo | (lightEmission << 12);
|
||||||
lightSources.enqueue(index);
|
lightSources.enqueue((short) index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,12 +164,10 @@ final class BlockLight implements Light {
|
||||||
Set<Point> toUpdate = new HashSet<>();
|
Set<Point> toUpdate = new HashSet<>();
|
||||||
|
|
||||||
// Update single section with base lighting changes
|
// Update single section with base lighting changes
|
||||||
Block[] blocks = new Block[SECTION_SIZE * SECTION_SIZE * SECTION_SIZE];
|
ShortArrayFIFOQueue queue = buildInternalQueue(blockPalette);
|
||||||
IntArrayFIFOQueue queue = buildInternalQueue(blockPalette, blocks);
|
|
||||||
|
|
||||||
Result result = LightCompute.compute(blocks, queue);
|
Result result = LightCompute.compute(blockPalette, queue);
|
||||||
this.content = result.light();
|
this.content = result.light();
|
||||||
this.borders = result.borders();
|
|
||||||
|
|
||||||
// Propagate changes to neighbors and self
|
// Propagate changes to neighbors and self
|
||||||
for (int i = -1; i <= 1; i++) {
|
for (int i = -1; i <= 1; i++) {
|
||||||
|
@ -212,7 +216,6 @@ final class BlockLight implements Light {
|
||||||
|
|
||||||
private void clearCache() {
|
private void clearCache() {
|
||||||
this.contentPropagation = null;
|
this.contentPropagation = null;
|
||||||
this.bordersPropagation = null;
|
|
||||||
isValidBorders = true;
|
isValidBorders = true;
|
||||||
needsSend = true;
|
needsSend = true;
|
||||||
}
|
}
|
||||||
|
@ -226,57 +229,28 @@ final class BlockLight implements Light {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean compareBorders(byte[] a, byte[] b) {
|
|
||||||
if (b == null && a == null) return true;
|
|
||||||
if (b == null || a == null) return false;
|
|
||||||
|
|
||||||
if (a.length != b.length) return false;
|
|
||||||
for (int i = 0; i < a.length; i++) {
|
|
||||||
if (a[i] > b[i]) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Block[] blocks() {
|
|
||||||
Block[] blocks = new Block[SECTION_SIZE * SECTION_SIZE * SECTION_SIZE];
|
|
||||||
|
|
||||||
blockPalette.getAllPresent((x, y, z, stateId) -> {
|
|
||||||
final Block block = Block.fromStateId((short) stateId);
|
|
||||||
assert block != null;
|
|
||||||
final int index = x | (z << 4) | (y << 8);
|
|
||||||
blocks[index] = block;
|
|
||||||
});
|
|
||||||
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) {
|
public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) {
|
||||||
if (!isValidBorders) clearCache();
|
if (!isValidBorders) clearCache();
|
||||||
|
|
||||||
Map<BlockFace, Point> neighbors = Light.getNeighbors(chunk, sectionY);
|
Point[] neighbors = Light.getNeighbors(chunk, sectionY);
|
||||||
|
|
||||||
Block[] blocks = blocks();
|
ShortArrayFIFOQueue queue = buildExternalQueue(instance, blockPalette, neighbors, content);
|
||||||
IntArrayFIFOQueue queue = buildExternalQueue(instance, blocks, neighbors, borders);
|
LightCompute.Result result = LightCompute.compute(blockPalette, queue);
|
||||||
LightCompute.Result result = LightCompute.compute(blocks, queue);
|
|
||||||
|
|
||||||
byte[] contentPropagationTemp = result.light();
|
byte[] contentPropagationTemp = result.light();
|
||||||
byte[][] borderTemp = result.borders();
|
|
||||||
|
|
||||||
this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp);
|
this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp);
|
||||||
this.bordersPropagationSwap = combineBorders(bordersPropagation, borderTemp);
|
|
||||||
|
|
||||||
Set<Point> toUpdate = new HashSet<>();
|
Set<Point> toUpdate = new HashSet<>();
|
||||||
|
|
||||||
// Propagate changes to neighbors and self
|
// Propagate changes to neighbors and self
|
||||||
for (var entry : neighbors.entrySet()) {
|
for (int i = 0; i < neighbors.length; i++) {
|
||||||
var neighbor = entry.getValue();
|
var neighbor = neighbors[i];
|
||||||
var face = entry.getKey();
|
if (neighbor == null) continue;
|
||||||
|
var face = BlockFace.values()[i];
|
||||||
|
|
||||||
byte[] next = borderTemp[face.ordinal()];
|
if (!Light.compareBorders(content, contentPropagation, contentPropagationTemp, face)) {
|
||||||
byte[] current = getBorderPropagation(face);
|
|
||||||
|
|
||||||
if (!compareBorders(next, current)) {
|
|
||||||
toUpdate.add(neighbor);
|
toUpdate.add(neighbor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,17 +259,6 @@ final class BlockLight implements Light {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[][] combineBorders(byte[][] b1, byte[][] b2) {
|
|
||||||
if (b1 == null) return b2;
|
|
||||||
|
|
||||||
byte[][] newBorder = new byte[FACES.length][];
|
|
||||||
Arrays.setAll(newBorder, i -> new byte[SIDE_LENGTH]);
|
|
||||||
for (int i = 0; i < FACES.length; i++) {
|
|
||||||
newBorder[i] = combineBorders(b1[i], b2[i]);
|
|
||||||
}
|
|
||||||
return newBorder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] bake(byte[] content1, byte[] content2) {
|
private byte[] bake(byte[] content1, byte[] content2) {
|
||||||
if (content1 == null && content2 == null) return emptyContent;
|
if (content1 == null && content2 == null) return emptyContent;
|
||||||
if (content1 == emptyContent && content2 == emptyContent) return emptyContent;
|
if (content1 == emptyContent && content2 == emptyContent) return emptyContent;
|
||||||
|
@ -321,39 +284,18 @@ final class BlockLight implements Light {
|
||||||
return lightMax;
|
return lightMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getBorderPropagation(BlockFace face) {
|
|
||||||
if (!isValidBorders) clearCache();
|
|
||||||
|
|
||||||
if (borders == null && bordersPropagation == null) return new byte[SIDE_LENGTH];
|
|
||||||
if (borders == null) return bordersPropagation[face.ordinal()];
|
|
||||||
if (bordersPropagation == null) return borders[face.ordinal()];
|
|
||||||
|
|
||||||
return combineBorders(bordersPropagation[face.ordinal()], borders[face.ordinal()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidatePropagation() {
|
public void invalidatePropagation() {
|
||||||
this.isValidBorders = false;
|
this.isValidBorders = false;
|
||||||
this.needsSend = false;
|
this.needsSend = false;
|
||||||
this.bordersPropagation = null;
|
|
||||||
this.contentPropagation = null;
|
this.contentPropagation = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getLevel(int x, int y, int z) {
|
public int getLevel(int x, int y, int z) {
|
||||||
var array = array();
|
if (content == null) return 0;
|
||||||
int index = x | (z << 4) | (y << 8);
|
int index = x | (z << 4) | (y << 8);
|
||||||
return LightCompute.getLight(array, index);
|
if (contentPropagation == null) return LightCompute.getLight(content, index);
|
||||||
}
|
return Math.max(LightCompute.getLight(contentPropagation, index), LightCompute.getLight(content, index));
|
||||||
|
|
||||||
private byte[] combineBorders(byte[] b1, byte[] b2) {
|
|
||||||
byte[] newBorder = new byte[SIDE_LENGTH];
|
|
||||||
for (int i = 0; i < newBorder.length; i++) {
|
|
||||||
var previous = b2[i];
|
|
||||||
var current = b1[i];
|
|
||||||
newBorder[i] = (byte) Math.max(previous, current);
|
|
||||||
}
|
|
||||||
return newBorder;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,6 +14,9 @@ import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static net.minestom.server.instance.light.LightCompute.SECTION_SIZE;
|
||||||
|
import static net.minestom.server.instance.light.LightCompute.getLight;
|
||||||
|
|
||||||
public interface Light {
|
public interface Light {
|
||||||
static Light sky(@NotNull Palette blockPalette) {
|
static Light sky(@NotNull Palette blockPalette) {
|
||||||
return new SkyLight(blockPalette);
|
return new SkyLight(blockPalette);
|
||||||
|
@ -35,9 +38,6 @@ public interface Light {
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
Light calculateExternal(Instance instance, Chunk chunk, int sectionY);
|
Light calculateExternal(Instance instance, Chunk chunk, int sectionY);
|
||||||
|
|
||||||
@ApiStatus.Internal
|
|
||||||
byte[] getBorderPropagation(BlockFace oppositeFace);
|
|
||||||
|
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
void invalidatePropagation();
|
void invalidatePropagation();
|
||||||
|
|
||||||
|
@ -53,11 +53,11 @@ public interface Light {
|
||||||
void set(byte[] copyArray);
|
void set(byte[] copyArray);
|
||||||
|
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
static Map<BlockFace, Point> getNeighbors(Chunk chunk, int sectionY) {
|
static Point[] getNeighbors(Chunk chunk, int sectionY) {
|
||||||
int chunkX = chunk.getChunkX();
|
int chunkX = chunk.getChunkX();
|
||||||
int chunkZ = chunk.getChunkZ();
|
int chunkZ = chunk.getChunkZ();
|
||||||
|
|
||||||
Map<BlockFace, Point> links = new HashMap<>();
|
Point[] links = new Vec[BlockFace.values().length];
|
||||||
|
|
||||||
for (BlockFace face : BlockFace.values()) {
|
for (BlockFace face : BlockFace.values()) {
|
||||||
Direction direction = face.toDirection();
|
Direction direction = face.toDirection();
|
||||||
|
@ -70,9 +70,41 @@ public interface Light {
|
||||||
if (foundChunk == null) continue;
|
if (foundChunk == null) continue;
|
||||||
if (y - foundChunk.getMinSection() > foundChunk.getMaxSection() || y - foundChunk.getMinSection() < 0) continue;
|
if (y - foundChunk.getMinSection() > foundChunk.getMaxSection() || y - foundChunk.getMinSection() < 0) continue;
|
||||||
|
|
||||||
links.put(face, new Vec(foundChunk.getChunkX(), y, foundChunk.getChunkZ()));
|
links[face.ordinal()] = new Vec(foundChunk.getChunkX(), y, foundChunk.getChunkZ());
|
||||||
}
|
}
|
||||||
|
|
||||||
return links;
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
static boolean compareBorders(byte[] content, byte[] contentPropagation, byte[] contentPropagationTemp, BlockFace face) {
|
||||||
|
if (content == null && contentPropagation == null && contentPropagationTemp == null) return true;
|
||||||
|
|
||||||
|
final int k = switch (face) {
|
||||||
|
case WEST, BOTTOM, NORTH -> 0;
|
||||||
|
case EAST, TOP, SOUTH -> 15;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int bx = 0; bx < SECTION_SIZE; bx++) {
|
||||||
|
for (int by = 0; by < SECTION_SIZE; by++) {
|
||||||
|
final int posFrom = switch (face) {
|
||||||
|
case NORTH, SOUTH -> bx | (k << 4) | (by << 8);
|
||||||
|
case WEST, EAST -> k | (by << 4) | (bx << 8);
|
||||||
|
default -> bx | (by << 4) | (k << 8);
|
||||||
|
};
|
||||||
|
|
||||||
|
int valueFrom;
|
||||||
|
|
||||||
|
if (content == null && contentPropagation == null) valueFrom = 0;
|
||||||
|
else if (content != null && contentPropagation == null) valueFrom = getLight(content, posFrom);
|
||||||
|
else if (content == null && contentPropagation != null) valueFrom = getLight(contentPropagation, posFrom);
|
||||||
|
else valueFrom = Math.max(getLight(content, posFrom), getLight(contentPropagation, posFrom));
|
||||||
|
|
||||||
|
int valueTo = getLight(contentPropagationTemp, posFrom);
|
||||||
|
|
||||||
|
if (valueFrom < valueTo) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package net.minestom.server.instance.light;
|
package net.minestom.server.instance.light;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
|
import it.unimi.dsi.fastutil.shorts.ShortArrayFIFOQueue;
|
||||||
import net.minestom.server.instance.block.Block;
|
import net.minestom.server.instance.block.Block;
|
||||||
import net.minestom.server.instance.block.BlockFace;
|
import net.minestom.server.instance.block.BlockFace;
|
||||||
import net.minestom.server.instance.palette.Palette;
|
import net.minestom.server.instance.palette.Palette;
|
||||||
import net.minestom.server.utils.Direction;
|
import net.minestom.server.utils.Direction;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import static net.minestom.server.instance.light.BlockLight.buildInternalQueue;
|
import static net.minestom.server.instance.light.BlockLight.buildInternalQueue;
|
||||||
|
@ -15,29 +15,25 @@ import static net.minestom.server.instance.light.BlockLight.buildInternalQueue;
|
||||||
public final class LightCompute {
|
public final class LightCompute {
|
||||||
static final BlockFace[] FACES = BlockFace.values();
|
static final BlockFace[] FACES = BlockFace.values();
|
||||||
static final int LIGHT_LENGTH = 16 * 16 * 16 / 2;
|
static final int LIGHT_LENGTH = 16 * 16 * 16 / 2;
|
||||||
static final int SIDE_LENGTH = 16 * 16;
|
|
||||||
static final int SECTION_SIZE = 16;
|
static final int SECTION_SIZE = 16;
|
||||||
|
|
||||||
public static final byte[][] emptyBorders = new byte[FACES.length][SIDE_LENGTH];
|
|
||||||
public static final byte[] emptyContent = new byte[LIGHT_LENGTH];
|
public static final byte[] emptyContent = new byte[LIGHT_LENGTH];
|
||||||
|
|
||||||
static @NotNull Result compute(Palette blockPalette) {
|
static @NotNull Result compute(Palette blockPalette) {
|
||||||
Block[] blocks = new Block[4096];
|
return LightCompute.compute(blockPalette, buildInternalQueue(blockPalette));
|
||||||
return LightCompute.compute(blocks, buildInternalQueue(blockPalette, blocks));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static @NotNull Result compute(Block[] blocks, IntArrayFIFOQueue lightPre) {
|
static @NotNull Result compute(Palette blockPalette, ShortArrayFIFOQueue lightPre) {
|
||||||
if (lightPre.isEmpty()) {
|
if (lightPre.isEmpty()) {
|
||||||
return new Result(emptyContent, emptyBorders);
|
return new Result(emptyContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[][] borders = new byte[FACES.length][SIDE_LENGTH];
|
|
||||||
byte[] lightArray = new byte[LIGHT_LENGTH];
|
byte[] lightArray = new byte[LIGHT_LENGTH];
|
||||||
|
|
||||||
var lightSources = new LinkedList<Integer>();
|
var lightSources = new ArrayDeque<Short>();
|
||||||
|
|
||||||
while (!lightPre.isEmpty()) {
|
while (!lightPre.isEmpty()) {
|
||||||
int index = lightPre.dequeueInt();
|
int index = lightPre.dequeueShort();
|
||||||
|
|
||||||
final int x = index & 15;
|
final int x = index & 15;
|
||||||
final int z = (index >> 4) & 15;
|
final int z = (index >> 4) & 15;
|
||||||
|
@ -49,7 +45,7 @@ public final class LightCompute {
|
||||||
|
|
||||||
if (oldLightLevel < newLightLevel) {
|
if (oldLightLevel < newLightLevel) {
|
||||||
placeLight(lightArray, newIndex, newLightLevel);
|
placeLight(lightArray, newIndex, newLightLevel);
|
||||||
lightSources.add(index);
|
lightSources.add((short) index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,34 +62,29 @@ public final class LightCompute {
|
||||||
final int yO = y + dir.normalY();
|
final int yO = y + dir.normalY();
|
||||||
final int zO = z + dir.normalZ();
|
final int zO = z + dir.normalZ();
|
||||||
final byte newLightLevel = (byte) (lightLevel - 1);
|
final byte newLightLevel = (byte) (lightLevel - 1);
|
||||||
|
|
||||||
// Handler border
|
// Handler border
|
||||||
if (xO < 0 || xO >= SECTION_SIZE || yO < 0 || yO >= SECTION_SIZE || zO < 0 || zO >= SECTION_SIZE) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section
|
// Section
|
||||||
final int newIndex = xO | (zO << 4) | (yO << 8);
|
final int newIndex = xO | (zO << 4) | (yO << 8);
|
||||||
if (getLight(lightArray, newIndex) + 2 <= lightLevel) {
|
if (getLight(lightArray, newIndex) + 2 <= lightLevel) {
|
||||||
final Block currentBlock = Objects.requireNonNullElse(blocks[x | (z << 4) | (y << 8)], Block.AIR);
|
final Block currentBlock = Objects.requireNonNullElse(Block.fromStateId((short)blockPalette.get(x, y, z)), Block.AIR);
|
||||||
|
final Block propagatedBlock = Objects.requireNonNullElse(Block.fromStateId((short)blockPalette.get(xO, yO, zO)), Block.AIR);
|
||||||
|
|
||||||
final Block propagatedBlock = Objects.requireNonNullElse(blocks[newIndex], Block.AIR);
|
|
||||||
boolean airAir = currentBlock.isAir() && propagatedBlock.isAir();
|
boolean airAir = currentBlock.isAir() && propagatedBlock.isAir();
|
||||||
if (!airAir && currentBlock.registry().collisionShape().isOccluded(propagatedBlock.registry().collisionShape(), face)) continue;
|
if (!airAir && currentBlock.registry().collisionShape().isOccluded(propagatedBlock.registry().collisionShape(), face)) continue;
|
||||||
placeLight(lightArray, newIndex, newLightLevel);
|
placeLight(lightArray, newIndex, newLightLevel);
|
||||||
lightSources.add(newIndex | (newLightLevel << 12));
|
lightSources.add((short) (newIndex | (newLightLevel << 12)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Result(lightArray, borders);
|
return new Result(lightArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
record Result(byte[] light, byte[][] borders) {
|
record Result(byte[] light) {
|
||||||
Result {
|
Result {
|
||||||
assert light.length == LIGHT_LENGTH : "Only 16x16x16 sections are supported: " + light.length;
|
assert light.length == LIGHT_LENGTH : "Only 16x16x16 sections are supported: " + light.length;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package net.minestom.server.instance.light;
|
package net.minestom.server.instance.light;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
|
import it.unimi.dsi.fastutil.shorts.ShortArrayFIFOQueue;
|
||||||
import net.minestom.server.coordinate.Point;
|
import net.minestom.server.coordinate.Point;
|
||||||
import net.minestom.server.coordinate.Vec;
|
import net.minestom.server.coordinate.Vec;
|
||||||
import net.minestom.server.instance.Chunk;
|
import net.minestom.server.instance.Chunk;
|
||||||
|
@ -16,6 +16,7 @@ import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import static net.minestom.server.instance.light.LightCompute.*;
|
import static net.minestom.server.instance.light.LightCompute.*;
|
||||||
|
|
||||||
|
@ -26,23 +27,17 @@ final class SkyLight implements Light {
|
||||||
private byte[] contentPropagation;
|
private byte[] contentPropagation;
|
||||||
private byte[] contentPropagationSwap;
|
private byte[] contentPropagationSwap;
|
||||||
|
|
||||||
private byte[][] borders;
|
|
||||||
private byte[][] bordersPropagation;
|
|
||||||
private byte[][] bordersPropagationSwap;
|
|
||||||
private boolean isValidBorders = true;
|
private boolean isValidBorders = true;
|
||||||
private boolean needsSend = true;
|
private boolean needsSend = true;
|
||||||
|
|
||||||
private Set<Point> toUpdateSet = new HashSet<>();
|
private Set<Point> toUpdateSet = new HashSet<>();
|
||||||
|
private final Section[] neighborSections = new Section[BlockFace.values().length];
|
||||||
|
|
||||||
private boolean fullyLit = false;
|
private boolean fullyLit = false;
|
||||||
private static final byte[][] bordersFullyLit = new byte[6][SIDE_LENGTH];
|
|
||||||
private static final byte[] contentFullyLit = new byte[LIGHT_LENGTH];
|
private static final byte[] contentFullyLit = new byte[LIGHT_LENGTH];
|
||||||
|
|
||||||
static {
|
static {
|
||||||
Arrays.fill(contentFullyLit, (byte) -1);
|
Arrays.fill(contentFullyLit, (byte) -1);
|
||||||
for (byte[] border : bordersFullyLit) {
|
|
||||||
Arrays.fill(border, (byte) 14);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SkyLight(Palette blockPalette) {
|
SkyLight(Palette blockPalette) {
|
||||||
|
@ -51,21 +46,17 @@ final class SkyLight implements Light {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<Point> flip() {
|
public Set<Point> flip() {
|
||||||
if (this.bordersPropagationSwap != null)
|
|
||||||
this.bordersPropagation = this.bordersPropagationSwap;
|
|
||||||
|
|
||||||
if (this.contentPropagationSwap != null)
|
if (this.contentPropagationSwap != null)
|
||||||
this.contentPropagation = this.contentPropagationSwap;
|
this.contentPropagation = this.contentPropagationSwap;
|
||||||
|
|
||||||
this.bordersPropagationSwap = null;
|
|
||||||
this.contentPropagationSwap = null;
|
this.contentPropagationSwap = null;
|
||||||
|
|
||||||
if (toUpdateSet == null) return Set.of();
|
if (toUpdateSet == null) return Set.of();
|
||||||
return toUpdateSet;
|
return toUpdateSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
static IntArrayFIFOQueue buildInternalQueue(Chunk c, int sectionY) {
|
static ShortArrayFIFOQueue buildInternalQueue(Chunk c, int sectionY) {
|
||||||
IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue();
|
ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue();
|
||||||
|
|
||||||
if (c instanceof LightingChunk lc) {
|
if (c instanceof LightingChunk lc) {
|
||||||
int[] heightmap = lc.calculateHeightMap();
|
int[] heightmap = lc.calculateHeightMap();
|
||||||
|
@ -79,7 +70,7 @@ final class SkyLight implements Light {
|
||||||
|
|
||||||
for (int y = Math.min(sectionMaxY, maxY); y >= Math.max(height, sectionMinY); y--) {
|
for (int y = Math.min(sectionMaxY, maxY); y >= Math.max(height, sectionMinY); y--) {
|
||||||
int index = x | (z << 4) | ((y % 16) << 8);
|
int index = x | (z << 4) | ((y % 16) << 8);
|
||||||
lightSources.enqueue(index | (15 << 12));
|
lightSources.enqueue((short) (index | (15 << 12)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,45 +83,55 @@ final class SkyLight implements Light {
|
||||||
return Block.fromStateId((short)palette.get(x, y, z));
|
return Block.fromStateId((short)palette.get(x, y, z));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IntArrayFIFOQueue buildExternalQueue(Instance instance, Block[] blocks, Map<BlockFace, Point> neighbors, byte[][] borders) {
|
private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockPalette, Point[] neighbors, byte[] content) {
|
||||||
IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue();
|
ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue();
|
||||||
|
|
||||||
for (BlockFace face : BlockFace.values()) {
|
for (int i = 0; i < neighbors.length; i++) {
|
||||||
Point neighborSection = neighbors.get(face);
|
var face = BlockFace.values()[i];
|
||||||
|
Point neighborSection = neighbors[i];
|
||||||
if (neighborSection == null) continue;
|
if (neighborSection == null) continue;
|
||||||
|
|
||||||
Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ());
|
Section otherSection = neighborSections[face.ordinal()];
|
||||||
if (chunk == null) continue;
|
|
||||||
|
|
||||||
byte[] neighborFace = chunk.getSection(neighborSection.blockY()).skyLight().getBorderPropagation(face.getOppositeFace());
|
if (otherSection == null) {
|
||||||
if (neighborFace == null) continue;
|
Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ());
|
||||||
|
if (chunk == null) continue;
|
||||||
|
|
||||||
|
otherSection = chunk.getSection(neighborSection.blockY());
|
||||||
|
neighborSections[face.ordinal()] = otherSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
var otherLight = otherSection.skyLight();
|
||||||
|
|
||||||
for (int bx = 0; bx < 16; bx++) {
|
for (int bx = 0; bx < 16; bx++) {
|
||||||
for (int by = 0; by < 16; by++) {
|
for (int by = 0; by < 16; by++) {
|
||||||
final int borderIndex = bx * SECTION_SIZE + by;
|
|
||||||
byte lightEmission = neighborFace[borderIndex];
|
|
||||||
|
|
||||||
if (borders != null && borders[face.ordinal()] != null) {
|
|
||||||
final int internalEmission = borders[face.ordinal()][borderIndex];
|
|
||||||
if (lightEmission <= internalEmission) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (borders != null && borders[face.ordinal()] != null) {
|
|
||||||
final int internalEmission = borders[face.ordinal()][borderIndex];
|
|
||||||
if (lightEmission <= internalEmission) continue;
|
|
||||||
}
|
|
||||||
final int k = switch (face) {
|
final int k = switch (face) {
|
||||||
case WEST, BOTTOM, NORTH -> 0;
|
case WEST, BOTTOM, NORTH -> 0;
|
||||||
case EAST, TOP, SOUTH -> 15;
|
case EAST, TOP, SOUTH -> 15;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final byte lightEmission = (byte) Math.max(switch (face) {
|
||||||
|
case NORTH, SOUTH -> (byte) otherLight.getLevel(bx, by, 15 - k);
|
||||||
|
case WEST, EAST -> (byte) otherLight.getLevel(15 - k, bx, by);
|
||||||
|
default -> (byte) otherLight.getLevel(bx, 15 - k, by);
|
||||||
|
} - 1, 0);
|
||||||
|
|
||||||
final int posTo = switch (face) {
|
final int posTo = switch (face) {
|
||||||
case NORTH, SOUTH -> bx | (k << 4) | (by << 8);
|
case NORTH, SOUTH -> bx | (k << 4) | (by << 8);
|
||||||
case WEST, EAST -> k | (by << 4) | (bx << 8);
|
case WEST, EAST -> k | (by << 4) | (bx << 8);
|
||||||
default -> bx | (by << 4) | (k << 8);
|
default -> bx | (by << 4) | (k << 8);
|
||||||
};
|
};
|
||||||
|
|
||||||
Section otherSection = chunk.getSection(neighborSection.blockY());
|
if (content != null) {
|
||||||
|
final int internalEmission = (byte) (Math.max(getLight(content, posTo) - 1, 0));
|
||||||
|
if (lightEmission <= internalEmission) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Block blockTo = switch (face) {
|
||||||
|
case NORTH, SOUTH -> getBlock(blockPalette, bx, by, k);
|
||||||
|
case WEST, EAST -> getBlock(blockPalette, k, bx, by);
|
||||||
|
default -> getBlock(blockPalette, bx, k, by);
|
||||||
|
};
|
||||||
|
|
||||||
final Block blockFrom = (switch (face) {
|
final Block blockFrom = (switch (face) {
|
||||||
case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k);
|
case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k);
|
||||||
|
@ -138,9 +139,6 @@ final class SkyLight implements Light {
|
||||||
default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by);
|
default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (blocks == null) continue;
|
|
||||||
Block blockTo = blocks[posTo];
|
|
||||||
|
|
||||||
if (blockTo == null && blockFrom != null) {
|
if (blockTo == null && blockFrom != null) {
|
||||||
if (blockFrom.registry().collisionShape().isOccluded(Block.AIR.registry().collisionShape(), face.getOppositeFace()))
|
if (blockFrom.registry().collisionShape().isOccluded(Block.AIR.registry().collisionShape(), face.getOppositeFace()))
|
||||||
continue;
|
continue;
|
||||||
|
@ -155,7 +153,7 @@ final class SkyLight implements Light {
|
||||||
final int index = posTo | (lightEmission << 12);
|
final int index = posTo | (lightEmission << 12);
|
||||||
|
|
||||||
if (lightEmission > 0) {
|
if (lightEmission > 0) {
|
||||||
lightSources.enqueue(index);
|
lightSources.enqueue((short) index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,13 +171,15 @@ final class SkyLight implements Light {
|
||||||
@Override
|
@Override
|
||||||
public Light calculateInternal(Instance instance, int chunkX, int sectionY, int chunkZ) {
|
public Light calculateInternal(Instance instance, int chunkX, int sectionY, int chunkZ) {
|
||||||
Chunk chunk = instance.getChunk(chunkX, chunkZ);
|
Chunk chunk = instance.getChunk(chunkX, chunkZ);
|
||||||
|
if (chunk == null) {
|
||||||
|
this.toUpdateSet = Set.of();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
this.isValidBorders = true;
|
this.isValidBorders = true;
|
||||||
|
|
||||||
// Update single section with base lighting changes
|
// Update single section with base lighting changes
|
||||||
Block[] blocks = blocks();
|
|
||||||
|
|
||||||
int queueSize = SECTION_SIZE * SECTION_SIZE * SECTION_SIZE;
|
int queueSize = SECTION_SIZE * SECTION_SIZE * SECTION_SIZE;
|
||||||
IntArrayFIFOQueue queue = new IntArrayFIFOQueue(0);
|
ShortArrayFIFOQueue queue = new ShortArrayFIFOQueue(0);
|
||||||
if (!fullyLit) {
|
if (!fullyLit) {
|
||||||
queue = buildInternalQueue(chunk, sectionY);
|
queue = buildInternalQueue(chunk, sectionY);
|
||||||
queueSize = queue.size();
|
queueSize = queue.size();
|
||||||
|
@ -188,11 +188,9 @@ final class SkyLight implements Light {
|
||||||
if (queueSize == SECTION_SIZE * SECTION_SIZE * SECTION_SIZE) {
|
if (queueSize == SECTION_SIZE * SECTION_SIZE * SECTION_SIZE) {
|
||||||
this.fullyLit = true;
|
this.fullyLit = true;
|
||||||
this.content = contentFullyLit;
|
this.content = contentFullyLit;
|
||||||
this.borders = bordersFullyLit;
|
|
||||||
} else {
|
} else {
|
||||||
Result result = LightCompute.compute(blocks, queue);
|
Result result = LightCompute.compute(blockPalette, queue);
|
||||||
this.content = result.light();
|
this.content = result.light();
|
||||||
this.borders = result.borders();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<Point> toUpdate = new HashSet<>();
|
Set<Point> toUpdate = new HashSet<>();
|
||||||
|
@ -244,7 +242,6 @@ final class SkyLight implements Light {
|
||||||
|
|
||||||
private void clearCache() {
|
private void clearCache() {
|
||||||
this.contentPropagation = null;
|
this.contentPropagation = null;
|
||||||
this.bordersPropagation = null;
|
|
||||||
isValidBorders = true;
|
isValidBorders = true;
|
||||||
needsSend = true;
|
needsSend = true;
|
||||||
fullyLit = false;
|
fullyLit = false;
|
||||||
|
@ -259,63 +256,35 @@ final class SkyLight implements Light {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean compareBorders(byte[] a, byte[] b) {
|
|
||||||
if (b == null && a == null) return true;
|
|
||||||
if (b == null || a == null) return false;
|
|
||||||
|
|
||||||
if (a.length != b.length) return false;
|
|
||||||
for (int i = 0; i < a.length; i++) {
|
|
||||||
if (a[i] > b[i]) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Block[] blocks() {
|
|
||||||
Block[] blocks = new Block[SECTION_SIZE * SECTION_SIZE * SECTION_SIZE];
|
|
||||||
|
|
||||||
blockPalette.getAllPresent((x, y, z, stateId) -> {
|
|
||||||
final Block block = Block.fromStateId((short) stateId);
|
|
||||||
assert block != null;
|
|
||||||
final int index = x | (z << 4) | (y << 8);
|
|
||||||
blocks[index] = block;
|
|
||||||
});
|
|
||||||
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) {
|
public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) {
|
||||||
if (!isValidBorders) clearCache();
|
if (!isValidBorders) clearCache();
|
||||||
|
|
||||||
Map<BlockFace, Point> neighbors = Light.getNeighbors(chunk, sectionY);
|
Point[] neighbors = Light.getNeighbors(chunk, sectionY);
|
||||||
Set<Point> toUpdate = new HashSet<>();
|
Set<Point> toUpdate = new HashSet<>();
|
||||||
|
|
||||||
Block[] blocks = blocks();
|
ShortArrayFIFOQueue queue;
|
||||||
IntArrayFIFOQueue queue;
|
|
||||||
|
byte[] contentPropagationTemp = contentFullyLit;
|
||||||
|
|
||||||
byte[][] borderTemp = bordersFullyLit;
|
|
||||||
if (!fullyLit) {
|
if (!fullyLit) {
|
||||||
queue = buildExternalQueue(instance, blocks, neighbors, borders);
|
queue = buildExternalQueue(instance, blockPalette, neighbors, content);
|
||||||
LightCompute.Result result = LightCompute.compute(blocks, queue);
|
LightCompute.Result result = LightCompute.compute(blockPalette, queue);
|
||||||
|
|
||||||
byte[] contentPropagationTemp = result.light();
|
contentPropagationTemp = result.light();
|
||||||
borderTemp = result.borders();
|
|
||||||
this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp);
|
this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp);
|
||||||
this.bordersPropagationSwap = combineBorders(bordersPropagation, borderTemp);
|
|
||||||
} else {
|
} else {
|
||||||
this.contentPropagationSwap = null;
|
this.contentPropagationSwap = null;
|
||||||
this.bordersPropagationSwap = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Propagate changes to neighbors and self
|
// Propagate changes to neighbors and self
|
||||||
for (var entry : neighbors.entrySet()) {
|
for (int i = 0; i < neighbors.length; i++) {
|
||||||
var neighbor = entry.getValue();
|
var neighbor = neighbors[i];
|
||||||
var face = entry.getKey();
|
if (neighbor == null) continue;
|
||||||
|
|
||||||
byte[] next = borderTemp[face.ordinal()];
|
var face = BlockFace.values()[i];
|
||||||
byte[] current = getBorderPropagation(face);
|
|
||||||
|
|
||||||
if (!compareBorders(next, current)) {
|
if (!Light.compareBorders(content, contentPropagation, contentPropagationTemp, face)) {
|
||||||
toUpdate.add(neighbor);
|
toUpdate.add(neighbor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -324,17 +293,6 @@ final class SkyLight implements Light {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[][] combineBorders(byte[][] b1, byte[][] b2) {
|
|
||||||
if (b1 == null) return b2;
|
|
||||||
|
|
||||||
byte[][] newBorder = new byte[FACES.length][];
|
|
||||||
Arrays.setAll(newBorder, i -> new byte[SIDE_LENGTH]);
|
|
||||||
for (int i = 0; i < FACES.length; i++) {
|
|
||||||
newBorder[i] = combineBorders(b1[i], b2[i]);
|
|
||||||
}
|
|
||||||
return newBorder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] bake(byte[] content1, byte[] content2) {
|
private byte[] bake(byte[] content1, byte[] content2) {
|
||||||
if (content1 == null && content2 == null) return emptyContent;
|
if (content1 == null && content2 == null) return emptyContent;
|
||||||
if (content1 == emptyContent && content2 == emptyContent) return emptyContent;
|
if (content1 == emptyContent && content2 == emptyContent) return emptyContent;
|
||||||
|
@ -360,39 +318,18 @@ final class SkyLight implements Light {
|
||||||
return lightMax;
|
return lightMax;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getBorderPropagation(BlockFace face) {
|
|
||||||
if (!isValidBorders) clearCache();
|
|
||||||
|
|
||||||
if (borders == null && bordersPropagation == null) return new byte[SIDE_LENGTH];
|
|
||||||
if (borders == null) return bordersPropagation[face.ordinal()];
|
|
||||||
if (bordersPropagation == null) return borders[face.ordinal()];
|
|
||||||
|
|
||||||
return combineBorders(bordersPropagation[face.ordinal()], borders[face.ordinal()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void invalidatePropagation() {
|
public void invalidatePropagation() {
|
||||||
this.isValidBorders = false;
|
this.isValidBorders = false;
|
||||||
this.needsSend = false;
|
this.needsSend = false;
|
||||||
this.bordersPropagation = null;
|
|
||||||
this.contentPropagation = null;
|
this.contentPropagation = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getLevel(int x, int y, int z) {
|
public int getLevel(int x, int y, int z) {
|
||||||
var array = array();
|
if (content == null) return 0;
|
||||||
int index = x | (z << 4) | (y << 8);
|
int index = x | (z << 4) | (y << 8);
|
||||||
return LightCompute.getLight(array, index);
|
if (contentPropagation == null) return LightCompute.getLight(content, index);
|
||||||
}
|
return Math.max(LightCompute.getLight(contentPropagation, index), LightCompute.getLight(content, index));
|
||||||
|
|
||||||
private byte[] combineBorders(byte[] b1, byte[] b2) {
|
|
||||||
byte[] newBorder = new byte[SIDE_LENGTH];
|
|
||||||
for (int i = 0; i < newBorder.length; i++) {
|
|
||||||
var previous = b2[i];
|
|
||||||
var current = b1[i];
|
|
||||||
newBorder[i] = (byte) Math.max(previous, current);
|
|
||||||
}
|
|
||||||
return newBorder;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -19,12 +19,13 @@ import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
|
||||||
|
|
||||||
@EnvTest
|
@EnvTest
|
||||||
public class LightParityIntegrationTest {
|
public class LightParityIntegrationTest {
|
||||||
|
private static final int REGION_SIZE = 3;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test(Env env) throws URISyntaxException, IOException, AnvilException {
|
public void test(Env env) throws URISyntaxException, IOException, AnvilException {
|
||||||
|
@ -35,14 +36,20 @@ public class LightParityIntegrationTest {
|
||||||
instance.setChunkSupplier(LightingChunk::new);
|
instance.setChunkSupplier(LightingChunk::new);
|
||||||
instance.setChunkLoader(new AnvilLoader(Path.of("./src/test/resources/net/minestom/server/instance/lighting")));
|
instance.setChunkLoader(new AnvilLoader(Path.of("./src/test/resources/net/minestom/server/instance/lighting")));
|
||||||
|
|
||||||
int end = 4;
|
List<CompletableFuture<Chunk>> futures = new ArrayList<>();
|
||||||
|
|
||||||
|
int end = REGION_SIZE;
|
||||||
// Load the chunks
|
// Load the chunks
|
||||||
for (int x = 0; x < end; x++) {
|
for (int x = 0; x < end; x++) {
|
||||||
for (int z = 0; z < end; z++) {
|
for (int z = 0; z < end; z++) {
|
||||||
instance.loadChunk(x, z).join();
|
futures.add(instance.loadChunk(x, z));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (CompletableFuture<Chunk> future : futures) {
|
||||||
|
future.join();
|
||||||
|
}
|
||||||
|
|
||||||
LightingChunk.relight(instance, instance.getChunks());
|
LightingChunk.relight(instance, instance.getChunks());
|
||||||
|
|
||||||
int differences = 0;
|
int differences = 0;
|
||||||
|
@ -60,7 +67,7 @@ public class LightParityIntegrationTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int sectionIndex = chunk.getMinSection(); sectionIndex < chunk.getMaxSection(); sectionIndex++) {
|
for (int sectionIndex = chunk.getMinSection(); sectionIndex < chunk.getMaxSection(); sectionIndex++) {
|
||||||
if (sectionIndex != 3) continue;
|
if (sectionIndex > 6) break;
|
||||||
|
|
||||||
Section section = chunk.getSection(sectionIndex);
|
Section section = chunk.getSection(sectionIndex);
|
||||||
|
|
||||||
|
@ -93,6 +100,7 @@ public class LightParityIntegrationTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mojang's sky lighting is wrong
|
||||||
{
|
{
|
||||||
int serverSkyValue = LightCompute.getLight(serverSky, index);
|
int serverSkyValue = LightCompute.getLight(serverSky, index);
|
||||||
int mcaSkyValue = mcaSky.length == 0 ? 0 : LightCompute.getLight(mcaSky, index);
|
int mcaSkyValue = mcaSky.length == 0 ? 0 : LightCompute.getLight(mcaSky, index);
|
||||||
|
@ -109,10 +117,10 @@ public class LightParityIntegrationTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(0, differences);
|
|
||||||
assertEquals(0, differencesZero);
|
|
||||||
assertEquals(0, blocks);
|
assertEquals(0, blocks);
|
||||||
assertEquals(0, sky);
|
assertEquals(0, sky);
|
||||||
|
assertEquals(0, differences);
|
||||||
|
assertEquals(0, differencesZero);
|
||||||
}
|
}
|
||||||
|
|
||||||
record SectionEntry(Palette blocks, byte[] sky, byte[] block) {
|
record SectionEntry(Palette blocks, byte[] sky, byte[] block) {
|
||||||
|
@ -127,8 +135,8 @@ public class LightParityIntegrationTest {
|
||||||
|
|
||||||
Map<Vec, SectionEntry> sections = new HashMap<>();
|
Map<Vec, SectionEntry> sections = new HashMap<>();
|
||||||
// Read from anvil
|
// Read from anvil
|
||||||
for (int x = 1; x < 3; x++) {
|
for (int x = 1; x < REGION_SIZE - 1; x++) {
|
||||||
for (int z = 1; z < 3; z++) {
|
for (int z = 1; z < REGION_SIZE - 1; z++) {
|
||||||
var chunk = regionFile.getChunk(x, z);
|
var chunk = regionFile.getChunk(x, z);
|
||||||
if (chunk == null) continue;
|
if (chunk == null) continue;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue