From 7880531bdb8cf1c3b8b7f46ae14bdd2f95d9297a Mon Sep 17 00:00:00 2001 From: Master290 Date: Mon, 27 Jul 2026 03:27:09 +0300 Subject: [PATCH] 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. --- CLAUDE.md | 23 +++- internal/world/block_properties.bin | Bin 0 -> 204677 bytes ...ight_properties.go => block_properties.go} | 71 +++++++++--- internal/world/encode.go | 80 ++++++++++--- internal/world/heightmap_test.go | 106 ++++++++++++++++++ internal/world/light_properties.bin | Bin 204677 -> 0 bytes internal/world/store.go | 2 +- ...htDump.java => VanillaBlockStateDump.java} | 46 ++++++-- 8 files changed, 280 insertions(+), 48 deletions(-) create mode 100644 internal/world/block_properties.bin rename internal/world/{light_properties.go => block_properties.go} (50%) create mode 100644 internal/world/heightmap_test.go delete mode 100644 internal/world/light_properties.bin rename tools/{VanillaLightDump.java => VanillaBlockStateDump.java} (65%) diff --git a/CLAUDE.md b/CLAUDE.md index efadf03..b7d8a38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,11 +60,22 @@ threshold was pinned down: `Strategy$2` switches `{0..3}` and everything above f global palette, which `.refjava/` alone could not show. When a constant has to come from vanilla's *runtime* rather than its source or reports, dump it with -a throwaway Java program run against the jar. `tools/VanillaLightDump.java` is the precedent — it -walks the block-state registry and emits opacity, emission and voxel face shapes into -`internal/world/light_properties.bin`. Substring-matching block names is how the light table was -wrong before (`grass_block` matched "grass", `bedrock` matched "bed"); don't reintroduce that shape -of guess anywhere. +a Java program run against the jar. `tools/VanillaBlockStateDump.java` is the one that exists — it +walks the block-state registry and emits, per state, light opacity and emission, voxel face-occlusion +masks, and the `blocksMotion` / fluid / leaves flags the heightmaps need, into +`internal/world/block_properties.bin`. Rebuild it with: + +``` +CP="versions/26.1.2/server-26.1.2.jar;$(find libraries -name '*.jar' | tr '\n' ';')" +javac -nowarn -cp "$CP" -d tools/VanillaBlockStateDump.java +java -cp ";$CP" VanillaBlockStateDump > internal/world/block_properties.bin +``` + +The 39 jars under `libraries/` are required; the server jar alone will not boot the registry. Bump +the format version in both the Java and `internal/world/block_properties.go` whenever the layout or +a flag's meaning changes. Substring-matching block names is how the light table was wrong before +(`grass_block` matched "grass", `bedrock` matched "bed"); don't reintroduce that shape of guess +anywhere. ## Layout @@ -74,7 +85,7 @@ cmd/gendump/ client-free generator diagnostics — biome spread, surface bl subsurface banding, deep-layer composition, bedrock band, fluid census, cross-section cmd/genblocks/ generates internal/worldgen/generated_blocks.go from the block report -cmd/genlight/ legacy light-table generator, superseded by tools/VanillaLightDump.java +cmd/genlight/ legacy light-table generator, superseded by tools/VanillaBlockStateDump.java tools/ Java dumpers run against the jar, plus their Go-side fixtures internal/protocol/ VarInt, framing, compression, packet IDs internal/nbt/ NBT codec (modified UTF-8) diff --git a/internal/world/block_properties.bin b/internal/world/block_properties.bin new file mode 100644 index 0000000000000000000000000000000000000000..a37bb1ce88ab06a865d6e219e9e77768334d410b GIT binary patch literal 204677 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%0Dc#&+k?^E!XE@bJmj=`uYU=!HEp! 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 literal 0 HcmV?d00001 diff --git a/internal/world/light_properties.go b/internal/world/block_properties.go similarity index 50% rename from internal/world/light_properties.go rename to internal/world/block_properties.go index c864455..ef8719e 100644 --- a/internal/world/light_properties.go +++ b/internal/world/block_properties.go @@ -6,40 +6,57 @@ import ( "fmt" ) -// lightPropertiesBinary is generated by tools/VanillaLightDump.java directly -// from the 26.1.2 runtime block-state registry. +// 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 light_properties.bin -var lightPropertiesBinary []byte +//go:embed block_properties.bin +var blockPropertiesBinary []byte var ( blockOpacity [totalBlockStates]byte blockEmission [totalBlockStates]byte - blockLightFlags [totalBlockStates]byte + blockStateFlags [totalBlockStates]byte blockLightShape [totalBlockStates]uint16 lightFaceShapes [][lightShapeBytes]byte ) const ( - lightPropertiesMagic = 0x52494f4c // RIOL - lightPropertiesVersion = 1 + 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 := decodeLightProperties(lightPropertiesBinary); err != nil { - panic(fmt.Sprintf("world: decode vanilla light properties: %v", err)) + if err := decodeBlockProperties(blockPropertiesBinary); err != nil { + panic(fmt.Sprintf("world: decode vanilla block properties: %v", err)) } } -func decodeLightProperties(data []byte) error { +func decodeBlockProperties(data []byte) error { if len(data) < 16 { return fmt.Errorf("header is truncated") } - if binary.BigEndian.Uint32(data[0:4]) != lightPropertiesMagic { + if binary.BigEndian.Uint32(data[0:4]) != blockPropertiesMagic { return fmt.Errorf("invalid magic") } - if version := binary.BigEndian.Uint32(data[4:8]); version != lightPropertiesVersion { + 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])) @@ -56,7 +73,7 @@ func decodeLightProperties(data []byte) error { for id := 0; id < states; id++ { blockOpacity[id] = data[offset] blockEmission[id] = data[offset+1] - blockLightFlags[id] = data[offset+2] + 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) @@ -85,6 +102,29 @@ func lightEmission(state uint16) byte { 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 { @@ -95,10 +135,11 @@ func lightShapeOccludes(from, into uint16, direction int) bool { // 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. - if int(from) < len(blockLightFlags) && blockLightFlags[from]&6 == 6 { + const shapeAware = flagCanOcclude | flagShapeForOcclusion + if int(from) < len(blockStateFlags) && blockStateFlags[from]&shapeAware == shapeAware { fromShape = lightFaceShapes[blockLightShape[from]] } - if int(into) < len(blockLightFlags) && blockLightFlags[into]&6 == 6 { + if int(into) < len(blockStateFlags) && blockStateFlags[into]&shapeAware == shapeAware { intoShape = lightFaceShapes[blockLightShape[into]] } opposite := [...]int{1, 0, 3, 2, 5, 4} diff --git a/internal/world/encode.go b/internal/world/encode.go index 6755485..2da7c38 100644 --- a/internal/world/encode.go +++ b/internal/world/encode.go @@ -286,22 +286,36 @@ func (c *Chunk) encode() []byte { return w.Bytes() } -// Heightmap.Types ordinals sent to the client. +// Heightmap.Types ids whose Usage is CLIENT. Vanilla sends exactly these three +// and no others. const ( hmWorldSurface = 1 hmMotionBlocking = 4 hmMotionBlockingNoLeaves = 5 ) -// writeHeightmaps emits the three client-relevant heightmaps. For our blocky -// terrain (no leaves/transparency) they share the same column heights. +// writeHeightmaps emits the three heightmaps the client is sent. +// +// They are not the same map, which is what this code used to assume. Each stops +// at a different block: WORLD_SURFACE at the first thing that is not air, +// MOTION_BLOCKING at the first thing that blocks movement or holds fluid, and +// MOTION_BLOCKING_NO_LEAVES at the first such thing that is not leaves. A +// flower, a torch, a sapling or a tree canopy separates them — the client uses +// MOTION_BLOCKING to place rain and snow particles and to decide where a +// fishing bobber lands, so a canopy reported as solid ground rains indoors. func (c *Chunk) writeHeightmaps(w *protocol.Writer) { - heights := c.columnHeights() - packed := packHeightmap(heights) - + surface, motion, motionNoLeaves := c.heightmaps() w.VarInt(3) - for _, t := range []int32{hmMotionBlockingNoLeaves, hmMotionBlocking, hmWorldSurface} { - w.VarInt(t) + for _, hm := range [...]struct { + id int32 + values [256]uint16 + }{ + {hmWorldSurface, surface}, + {hmMotionBlocking, motion}, + {hmMotionBlockingNoLeaves, motionNoLeaves}, + } { + packed := packHeightmap(hm.values) + w.VarInt(hm.id) w.VarInt(int32(len(packed))) for _, v := range packed { w.Int64(int64(v)) @@ -309,24 +323,54 @@ func (c *Chunk) writeHeightmaps(w *protocol.Writer) { } } -// columnHeights returns, per column, (highestNonAirY + 1) - MinY, clamped to 0. -func (c *Chunk) columnHeights() [256]uint16 { - var h [256]uint16 +// heightmaps walks every column once from the top down, recording the first +// block that satisfies each predicate. The stored value is one above the +// matching block, relative to the world floor — what Heightmap.setHeight +// writes — so 0 means the column has no matching block at all. +func (c *Chunk) heightmaps() (surface, motion, motionNoLeaves [256]uint16) { for lx := 0; lx < 16; lx++ { for lz := 0; lz < 16; lz++ { - height := 0 - for y := MinY + WorldHeight - 1; y >= MinY; y-- { - si := (y - MinY) >> 4 + i := lz*16 + lx + var haveSurface, haveMotion, haveNoLeaves bool + for si := SectionCount - 1; si >= 0; si-- { s := c.sections[si] - if s != nil && s[blockIndex(lx, y, lz)] != StateAir { - height = y + 1 - MinY + if s == nil { + continue + } + for ly := 15; ly >= 0; ly-- { + y := MinY + si*16 + ly + state := s[blockIndex(lx, y, lz)] + if state == StateAir { + continue + } + h := uint16(y + 1 - MinY) + if !haveSurface { + surface[i], haveSurface = h, true + } + if !haveMotion && blocksMotionOrFluid(state) { + motion[i], haveMotion = h, true + } + if !haveNoLeaves && blocksMotionNoLeaves(state) { + motionNoLeaves[i], haveNoLeaves = h, true + } + if haveSurface && haveMotion && haveNoLeaves { + break + } + } + if haveSurface && haveMotion && haveNoLeaves { break } } - h[lz*16+lx] = uint16(height) } } - return h + return surface, motion, motionNoLeaves +} + +// columnHeights returns the WORLD_SURFACE heightmap on its own, for the +// on-disk Heightmaps tag and the parity test. +func (c *Chunk) columnHeights() [256]uint16 { + surface, _, _ := c.heightmaps() + return surface } // packHeightmap packs 256 column heights at 9 bits each, 7 values per long, diff --git a/internal/world/heightmap_test.go b/internal/world/heightmap_test.go new file mode 100644 index 0000000..3e97ef2 --- /dev/null +++ b/internal/world/heightmap_test.go @@ -0,0 +1,106 @@ +package world + +import "testing" + +// TestHeightmapsDiffer is the check the old code could not pass: the three +// heightmaps sent to the client are different maps. They were all written from +// one "highest non-air" array, on the stated assumption that our terrain has no +// leaves or transparency — untrue the moment the generator grew trees and +// flowers. +// +// The client reads MOTION_BLOCKING to place rain and snow and to land a fishing +// bobber, and MOTION_BLOCKING_NO_LEAVES to decide what counts as sky cover. +func TestHeightmapsDiffer(t *testing.T) { + c := NewChunk(0, 0, BiomePlains) + const floor = 64 + + dandelion := nameToStateID("minecraft:dandelion", nil) + if dandelion == StateAir { + t.Fatal("dandelion is missing from the block table") + } + for lx := 0; lx < 16; lx++ { + for lz := 0; lz < 16; lz++ { + c.SetBlock(lx, floor, lz, StateStone) + } + } + // A flower: non-air, but it neither blocks motion nor holds fluid. + c.SetBlock(1, floor+1, 1, dandelion) + // A canopy: leaves block motion, so MOTION_BLOCKING counts them and + // MOTION_BLOCKING_NO_LEAVES does not. + c.SetBlock(2, floor+3, 2, StateOakLeaf) + // Water blocks neither entities nor light but does hold fluid, so it + // counts for both motion maps. + c.SetBlock(3, floor+1, 3, StateWater) + + surface, motion, noLeaves := c.heightmaps() + at := func(h [256]uint16, lx, lz int) int { return int(h[lz*16+lx]) + MinY } + + cases := []struct { + name string + lx, lz int + wantSurface, wantMotion, wantNo int + }{ + {"plain stone: all three agree", 0, 0, floor + 1, floor + 1, floor + 1}, + {"flower: only the surface map sees it", 1, 1, floor + 2, floor + 1, floor + 1}, + {"leaves: no-leaves map falls through to the stone", 2, 2, floor + 4, floor + 4, floor + 1}, + {"water: counts as fluid for both motion maps", 3, 3, floor + 2, floor + 2, floor + 2}, + } + for _, c := range cases { + gotS := at(surface, c.lx, c.lz) + gotM := at(motion, c.lx, c.lz) + gotN := at(noLeaves, c.lx, c.lz) + if gotS != c.wantSurface || gotM != c.wantMotion || gotN != c.wantNo { + t.Errorf("%s: surface=%d motion=%d noLeaves=%d, want %d/%d/%d", + c.name, gotS, gotM, gotN, c.wantSurface, c.wantMotion, c.wantNo) + } + } + + // A column with no matching block at all stores zero, not the world floor. + // A chunk of pure air is the clearest case, and it also exercises the + // nil-section fast path. + emptySurface, emptyMotion, emptyNoLeaves := NewChunk(0, 0, BiomePlains).heightmaps() + for i := range emptySurface { + if emptySurface[i] != 0 || emptyMotion[i] != 0 || emptyNoLeaves[i] != 0 { + t.Fatalf("empty chunk column %d reported %d/%d/%d, want all zero", + i, emptySurface[i], emptyMotion[i], emptyNoLeaves[i]) + } + } +} + +// TestBlockStatePredicates spot-checks the flags the dump carries against +// blocks whose behaviour is not in doubt. A silently wrong dump would make the +// heightmaps wrong everywhere at once. +func TestBlockStatePredicates(t *testing.T) { + cases := []struct { + name string + state uint16 + motion, noLeaves, isLeaves bool + }{ + {"stone", StateStone, true, true, false}, + {"grass block", StateGrass, true, true, false}, + {"oak log", StateOakLog, true, true, false}, + {"oak leaves", StateOakLeaf, true, false, true}, + {"water", StateWater, true, true, false}, + {"lava", StateLava, true, true, false}, + {"air", StateAir, false, false, false}, + } + for _, c := range cases { + if got := blocksMotionOrFluid(c.state); got != c.motion { + t.Errorf("%s: blocksMotionOrFluid = %v, want %v", c.name, got, c.motion) + } + if got := blocksMotionNoLeaves(c.state); got != c.noLeaves { + t.Errorf("%s: blocksMotionNoLeaves = %v, want %v", c.name, got, c.noLeaves) + } + if got := stateFlags(c.state)&flagLeaves != 0; got != c.isLeaves { + t.Errorf("%s: leaves flag = %v, want %v", c.name, got, c.isLeaves) + } + } + // A flower is the case that separates WORLD_SURFACE from MOTION_BLOCKING. + dandelion := nameToStateID("minecraft:dandelion", nil) + if dandelion == StateAir { + t.Fatal("dandelion is missing from the block table") + } + if blocksMotionOrFluid(dandelion) { + t.Error("dandelion blocks motion; it should not") + } +} diff --git a/internal/world/light_properties.bin b/internal/world/light_properties.bin deleted file mode 100644 index b121079f24e30e6ecc66c25b7990f144248dd6d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204677 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 tools/VanillaBlockStateDump.java +// java -cp ";$CP" VanillaBlockStateDump > internal/world/block_properties.bin +// +// Was VanillaLightDump: it dumped only lighting until the heightmaps needed +// blocksMotion and the leaves test, neither of which is derivable from any +// report or tag file. +public final class VanillaBlockStateDump { private record MaskKey(byte[] data) { @Override public boolean equals(Object other) { return other instanceof MaskKey key && Arrays.equals(data, key.data); @@ -25,6 +37,18 @@ public final class VanillaLightDump { @Override public int hashCode() { return Arrays.hashCode(data); } } + // Format version. Bump whenever the byte layout or the meaning of a flag + // bit changes; the Go decoder rejects anything it does not know. + private static final int FORMAT_VERSION = 2; + + // Flag bits. 1..4 drive lighting, 8..32 drive the heightmaps. + private static final int FLAG_PROPAGATES_SKYLIGHT = 1; + private static final int FLAG_CAN_OCCLUDE = 2; + private static final int FLAG_SHAPE_FOR_OCCLUSION = 4; + private static final int FLAG_BLOCKS_MOTION = 8; + private static final int FLAG_FLUID = 16; + private static final int FLAG_LEAVES = 32; + public static void main(String[] args) throws Exception { SharedConstants.tryDetectVersion(); Bootstrap.bootStrap(); @@ -42,9 +66,15 @@ public final class VanillaLightDump { 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; + if (state.propagatesSkylightDown()) stateFlags |= FLAG_PROPAGATES_SKYLIGHT; + if (state.canOcclude()) stateFlags |= FLAG_CAN_OCCLUDE; + if (state.useShapeForLightOcclusion()) stateFlags |= FLAG_SHAPE_FOR_OCCLUSION; + // Heightmap.Types: MOTION_BLOCKING is blocksMotion || fluid, and + // MOTION_BLOCKING_NO_LEAVES additionally excludes LeavesBlock -- + // an instanceof, not the minecraft:leaves tag, so the two differ. + if (state.blocksMotion()) stateFlags |= FLAG_BLOCKS_MOTION; + if (!state.getFluidState().isEmpty()) stateFlags |= FLAG_FLUID; + if (state.getBlock() instanceof LeavesBlock) stateFlags |= FLAG_LEAVES; flags[id] = (byte) stateFlags; byte[] masks = faceMasks(state); @@ -60,7 +90,7 @@ public final class VanillaLightDump { DataOutputStream out = new DataOutputStream(new BufferedOutputStream(System.out)); out.writeInt(0x52494f4c); // RIOL - out.writeInt(1); + out.writeInt(FORMAT_VERSION); out.writeInt(count); out.writeInt(shapes.size()); for (int id = 0; id < count; id++) {