Implement multiplayer persistence and vanilla lighting
This commit is contained in:
parent
8f7cacf9d9
commit
cae06eb97e
47 changed files with 3784 additions and 465 deletions
111
tools/VanillaLightDump.java
Normal file
111
tools/VanillaLightDump.java
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import java.io.BufferedOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
// Dumps protocol-775 lighting properties directly from vanilla runtime state.
|
||||
// Compile/run against the unpacked 26.1.2 server classpath and redirect stdout
|
||||
// to internal/world/light_properties.bin.
|
||||
public final class VanillaLightDump {
|
||||
private record MaskKey(byte[] data) {
|
||||
@Override public boolean equals(Object other) {
|
||||
return other instanceof MaskKey key && Arrays.equals(data, key.data);
|
||||
}
|
||||
@Override public int hashCode() { return Arrays.hashCode(data); }
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
|
||||
int count = Block.BLOCK_STATE_REGISTRY.size();
|
||||
byte[] dampening = new byte[count];
|
||||
byte[] emission = new byte[count];
|
||||
byte[] flags = new byte[count];
|
||||
int[] shapeIndex = new int[count];
|
||||
List<byte[]> shapes = new ArrayList<>();
|
||||
Map<MaskKey, Integer> indices = new HashMap<>();
|
||||
|
||||
for (BlockState state : Block.BLOCK_STATE_REGISTRY) {
|
||||
int id = Block.getId(state);
|
||||
dampening[id] = (byte) state.getLightDampening();
|
||||
emission[id] = (byte) state.getLightEmission();
|
||||
int stateFlags = 0;
|
||||
if (state.propagatesSkylightDown()) stateFlags |= 1;
|
||||
if (state.canOcclude()) stateFlags |= 2;
|
||||
if (state.useShapeForLightOcclusion()) stateFlags |= 4;
|
||||
flags[id] = (byte) stateFlags;
|
||||
|
||||
byte[] masks = faceMasks(state);
|
||||
MaskKey key = new MaskKey(masks);
|
||||
Integer index = indices.get(key);
|
||||
if (index == null) {
|
||||
index = shapes.size();
|
||||
indices.put(key, index);
|
||||
shapes.add(masks);
|
||||
}
|
||||
shapeIndex[id] = index;
|
||||
}
|
||||
|
||||
DataOutputStream out = new DataOutputStream(new BufferedOutputStream(System.out));
|
||||
out.writeInt(0x52494f4c); // RIOL
|
||||
out.writeInt(1);
|
||||
out.writeInt(count);
|
||||
out.writeInt(shapes.size());
|
||||
for (int id = 0; id < count; id++) {
|
||||
out.writeByte(dampening[id]);
|
||||
out.writeByte(emission[id]);
|
||||
out.writeByte(flags[id]);
|
||||
out.writeShort(shapeIndex[id]);
|
||||
}
|
||||
for (byte[] shape : shapes) out.write(shape);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private static byte[] faceMasks(BlockState state) {
|
||||
byte[] result = new byte[Direction.values().length * 32];
|
||||
for (Direction direction : Direction.values()) {
|
||||
VoxelShape shape = state.getFaceOcclusionShape(direction);
|
||||
List<AABB> boxes = shape.toAabbs();
|
||||
int base = direction.get3DDataValue() * 32;
|
||||
for (int v = 0; v < 16; v++) {
|
||||
for (int u = 0; u < 16; u++) {
|
||||
double du = (u + 0.5) / 16.0;
|
||||
double dv = (v + 0.5) / 16.0;
|
||||
if (covered(boxes, direction, du, dv)) {
|
||||
int bit = v * 16 + u;
|
||||
result[base + bit / 8] |= (byte) (1 << (bit % 8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean covered(List<AABB> boxes, Direction direction, double u, double v) {
|
||||
for (AABB box : boxes) {
|
||||
boolean inside = switch (direction.getAxis()) {
|
||||
case Y -> contains(box.minX, box.maxX, u) && contains(box.minZ, box.maxZ, v);
|
||||
case Z -> contains(box.minX, box.maxX, u) && contains(box.minY, box.maxY, v);
|
||||
case X -> contains(box.minZ, box.maxZ, u) && contains(box.minY, box.maxY, v);
|
||||
};
|
||||
if (inside) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean contains(double min, double max, double value) {
|
||||
return value >= min - 1.0e-7 && value <= max + 1.0e-7;
|
||||
}
|
||||
}
|
||||
148
tools/vanilla_light_fixture.go
Normal file
148
tools/vanilla_light_fixture.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// Command vanilla_light_fixture extracts a compact block-light parity fixture
|
||||
// from a vanilla world containing glowstone at (15,100,8).
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"regionio/internal/nbt"
|
||||
"regionio/internal/world"
|
||||
)
|
||||
|
||||
const (
|
||||
minX, minY, minZ = 0, 85, -7
|
||||
sizeX, sizeY, sizeZ = 31, 31, 31
|
||||
)
|
||||
|
||||
type lightChunk struct {
|
||||
sections map[int][]byte
|
||||
}
|
||||
|
||||
func main() {
|
||||
worldDir := flag.String("world", "", "vanilla overworld directory")
|
||||
output := flag.String("output", "", "fixture output path")
|
||||
flag.Parse()
|
||||
if *worldDir == "" || *output == "" {
|
||||
fmt.Fprintln(os.Stderr, "usage: go run ./tools/vanilla_light_fixture.go -world <dir> -output <file>")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
chunks := make(map[[2]int]*lightChunk)
|
||||
data := make([]byte, 0, sizeX*sizeY*sizeZ)
|
||||
for y := minY; y < minY+sizeY; y++ {
|
||||
for z := minZ; z < minZ+sizeZ; z++ {
|
||||
for x := minX; x < minX+sizeX; x++ {
|
||||
key := [2]int{x >> 4, z >> 4}
|
||||
chunk := chunks[key]
|
||||
if chunk == nil {
|
||||
var err error
|
||||
chunk, err = readLightChunk(*worldDir, key[0], key[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
chunks[key] = chunk
|
||||
}
|
||||
section := chunk.sections[y>>4]
|
||||
if section == nil {
|
||||
data = append(data, 0)
|
||||
continue
|
||||
}
|
||||
idx := (y&15)<<8 | (z&15)<<4 | (x & 15)
|
||||
value := section[idx>>1]
|
||||
if idx&1 == 0 {
|
||||
data = append(data, value&0x0f)
|
||||
} else {
|
||||
data = append(data, value>>4)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(*output, data, 0o644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func readLightChunk(worldDir string, cx, cz int) (*lightChunk, error) {
|
||||
rx, rz := floorDiv(cx, 32), floorDiv(cz, 32)
|
||||
regionDir := filepath.Join(worldDir, "dimensions", "minecraft", "overworld", "region")
|
||||
if _, err := os.Stat(regionDir); err != nil {
|
||||
regionDir = filepath.Join(worldDir, "region")
|
||||
}
|
||||
region, err := world.OpenRegion(regionDir, rx, rz)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer region.Close()
|
||||
raw, err := region.ReadChunk(cx-rx*32, cz-rz*32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("chunk (%d,%d): %w", cx, cz, err)
|
||||
}
|
||||
_, tag, err := nbt.UnmarshalNamed(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, ok := tag.(*nbt.Compound)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("chunk (%d,%d): root is not a compound", cx, cz)
|
||||
}
|
||||
if level, ok := root.Get("Level"); ok {
|
||||
root, _ = level.(*nbt.Compound)
|
||||
}
|
||||
sectionsTag, ok := root.Get("sections")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("chunk (%d,%d): missing sections", cx, cz)
|
||||
}
|
||||
sections, ok := sectionsTag.(nbt.List)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("chunk (%d,%d): sections is not a list", cx, cz)
|
||||
}
|
||||
out := &lightChunk{sections: make(map[int][]byte)}
|
||||
for _, sectionTag := range sections.Elems {
|
||||
section, ok := sectionTag.(*nbt.Compound)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
yTag, ok := section.Get("Y")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
y, ok := integerTag(yTag)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
lightTag, ok := section.Get("BlockLight")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
light, ok := lightTag.(nbt.ByteArray)
|
||||
if !ok || len(light) != 2048 {
|
||||
return nil, fmt.Errorf("chunk (%d,%d) section %d: invalid BlockLight", cx, cz, y)
|
||||
}
|
||||
out.sections[y] = append([]byte(nil), light...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func integerTag(tag nbt.Tag) (int, bool) {
|
||||
switch value := tag.(type) {
|
||||
case nbt.Byte:
|
||||
return int(value), true
|
||||
case nbt.Short:
|
||||
return int(value), true
|
||||
case nbt.Int:
|
||||
return int(value), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func floorDiv(value, divisor int) int {
|
||||
quotient := value / divisor
|
||||
if value < 0 && value%divisor != 0 {
|
||||
quotient--
|
||||
}
|
||||
return quotient
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue