Send three real heightmaps instead of one repeated three times

writeHeightmaps computed "highest non-air" once and wrote the same 37 longs
under all three ids, on the stated assumption that our terrain has no leaves or
transparency. That stopped being true the moment the generator grew trees and
flowers.

Vanilla's three client heightmaps stop at different blocks: WORLD_SURFACE at the
first thing that is not air, MOTION_BLOCKING at the first that blocks motion or
holds fluid, MOTION_BLOCKING_NO_LEAVES at the first such thing that is not a
LeavesBlock -- an instanceof, not the minecraft:leaves tag. The client places
rain and snow particles off MOTION_BLOCKING and lands a fishing bobber on it, so
a tree canopy reported as solid ground rains under itself.

Neither blocksMotion() nor the leaves test is derivable from blocks.json: the
first reads cached VoxelShape collision geometry and the forceSolidOn/Off
properties, the second is a Java class check. So the Java dumper grows three
flag bits and the whole thing is renamed for what it now is -- block state
properties, not just lighting. tools/VanillaBlockStateDump.java writes
internal/world/block_properties.bin at format 2; the light bytes are unchanged
byte for byte and only the previously unused high flag bits moved.

Verified the dumper round trip while doing it: recompiling the old
VanillaLightDump against the jar reproduces the committed binary exactly, so the
data really does come from the runtime registry and not from a stale checkout.
CLAUDE.md now carries the command to rebuild it.
This commit is contained in:
Master290 2026-07-27 03:27:09 +03:00
parent 57214fbd76
commit 7880531bdb
8 changed files with 280 additions and 48 deletions

View file

@ -0,0 +1,154 @@
package world
import (
_ "embed"
"encoding/binary"
"fmt"
)
// blockPropertiesBinary is generated by tools/VanillaBlockStateDump.java
// directly from the 26.1.2 runtime block-state registry. None of what it holds
// appears in generated/reports/blocks.json or in any tag file: light dampening,
// emission, voxel face-occlusion masks, whether a state blocks motion, whether
// it holds a fluid, and whether its block is a LeavesBlock all exist only once
// vanilla has built the registry.
//
//go:embed block_properties.bin
var blockPropertiesBinary []byte
var (
blockOpacity [totalBlockStates]byte
blockEmission [totalBlockStates]byte
blockStateFlags [totalBlockStates]byte
blockLightShape [totalBlockStates]uint16
lightFaceShapes [][lightShapeBytes]byte
)
const (
blockPropertiesMagic = 0x52494f4c // RIOL
blockPropertiesVersion = 2
lightShapeBytes = 6 * 32 // six 16x16 face masks
)
// Per-state flag bits, as written by tools/VanillaBlockStateDump.java. The
// first three drive lighting; the rest drive the heightmaps, and none of them
// can be derived from blocks.json or from a tag file — they only exist once
// vanilla's block-state registry is built.
const (
flagPropagatesSkylight = 1 << iota
flagCanOcclude
flagShapeForOcclusion
flagBlocksMotion
flagFluid
flagLeaves
)
func init() {
if err := decodeBlockProperties(blockPropertiesBinary); err != nil {
panic(fmt.Sprintf("world: decode vanilla block properties: %v", err))
}
}
func decodeBlockProperties(data []byte) error {
if len(data) < 16 {
return fmt.Errorf("header is truncated")
}
if binary.BigEndian.Uint32(data[0:4]) != blockPropertiesMagic {
return fmt.Errorf("invalid magic")
}
if version := binary.BigEndian.Uint32(data[4:8]); version != blockPropertiesVersion {
return fmt.Errorf("unsupported version %d", version)
}
states := int(binary.BigEndian.Uint32(data[8:12]))
shapes := int(binary.BigEndian.Uint32(data[12:16]))
if states != totalBlockStates {
return fmt.Errorf("state count %d, want %d", states, totalBlockStates)
}
want := 16 + states*5 + shapes*lightShapeBytes
if len(data) != want {
return fmt.Errorf("length %d, want %d", len(data), want)
}
offset := 16
for id := 0; id < states; id++ {
blockOpacity[id] = data[offset]
blockEmission[id] = data[offset+1]
blockStateFlags[id] = data[offset+2]
blockLightShape[id] = binary.BigEndian.Uint16(data[offset+3 : offset+5])
if int(blockLightShape[id]) >= shapes {
return fmt.Errorf("state %d references shape %d of %d", id, blockLightShape[id], shapes)
}
offset += 5
}
lightFaceShapes = make([][lightShapeBytes]byte, shapes)
for i := range lightFaceShapes {
copy(lightFaceShapes[i][:], data[offset:offset+lightShapeBytes])
offset += lightShapeBytes
}
return nil
}
func lightOpacity(state uint16) byte {
if int(state) >= len(blockOpacity) {
return 15
}
return blockOpacity[state]
}
func lightEmission(state uint16) byte {
if int(state) >= len(blockEmission) {
return 0
}
return blockEmission[state]
}
func stateFlags(state uint16) byte {
if int(state) >= len(blockStateFlags) {
return 0
}
return blockStateFlags[state]
}
// blocksMotionOrFluid is Heightmap.Types.MOTION_BLOCKING's predicate:
// blocksMotion() || !getFluidState().isEmpty(). Note that a waterlogged block
// satisfies the fluid half, which is why the flag is per state and not per
// block.
func blocksMotionOrFluid(state uint16) bool {
return stateFlags(state)&(flagBlocksMotion|flagFluid) != 0
}
// blocksMotionNoLeaves is MOTION_BLOCKING_NO_LEAVES's predicate: the same, less
// anything that is a LeavesBlock. Vanilla tests instanceof, not the
// minecraft:leaves tag, and the two sets are not the same.
func blocksMotionNoLeaves(state uint16) bool {
f := stateFlags(state)
return f&(flagBlocksMotion|flagFluid) != 0 && f&flagLeaves == 0
}
// lightShapeOccludes mirrors Shapes.faceShapeOccludes for the 1/16-resolution
// face masks emitted from vanilla's VoxelShape data.
func lightShapeOccludes(from, into uint16, direction int) bool {
if direction < 0 || direction >= 6 {
return false
}
var fromShape, intoShape [lightShapeBytes]byte
// Vanilla substitutes an empty shape unless both flags are true. Ordinary
// full cubes are handled by dampening; only shape-aware blocks (slabs,
// stairs, etc.) participate in face occlusion.
const shapeAware = flagCanOcclude | flagShapeForOcclusion
if int(from) < len(blockStateFlags) && blockStateFlags[from]&shapeAware == shapeAware {
fromShape = lightFaceShapes[blockLightShape[from]]
}
if int(into) < len(blockStateFlags) && blockStateFlags[into]&shapeAware == shapeAware {
intoShape = lightFaceShapes[blockLightShape[into]]
}
opposite := [...]int{1, 0, 3, 2, 5, 4}
fromOffset := direction * 32
intoOffset := opposite[direction] * 32
for i := 0; i < 32; i++ {
if fromShape[fromOffset+i]|intoShape[intoOffset+i] != 0xff {
return false
}
}
return true
}