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.
106 lines
4.1 KiB
Go
106 lines
4.1 KiB
Go
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")
|
|
}
|
|
}
|