diff --git a/internal/world/base_terrain_diagnostic_test.go b/internal/world/base_terrain_diagnostic_test.go index 5859ecc..af8695d 100644 --- a/internal/world/base_terrain_diagnostic_test.go +++ b/internal/world/base_terrain_diagnostic_test.go @@ -10,20 +10,20 @@ import ( ) // base_terrain_diagnostic_test.go splits the vanilla fixture's block -// mismatches into classes that name the responsible subsystem, instead of -// leaving one undifferentiated pile. The ore-replay diagnostics measure how -// feature placement drifts; this one answers why: which cells already differ -// before any feature runs. +// mismatches into classes by what could have written each side. The ore-replay +// diagnostics measure how feature placement drifts; this one attributes it. // -// Every fixture cell is compared against the undecorated base chunk. A cell -// where both sides are plain states (air family, fluids, bedrock, stone, -// deepslate) cannot be a feature write on either side — it is a direct -// base-terrain defect (aquifer edge, carver boundary, surface rule) and comes -// with sample coordinates for targeted fixes. Cells where exactly one side is -// a recognizably feature-placed state count as a flip toward that side; the -// remaining ambiguous pairs (disk outputs such as clay or gravel look like -// natural terrain) are bucketed separately so they cannot pollute the plain -// signal. +// Every fixture cell is compared against the undecorated base chunk. Plain- +// versus-plain pairs (air family, fluids, bedrock, stone, deepslate) used to +// be read as direct base-terrain defects, but TestVanillaBaseTerrainParity +// settled that question: against a vanilla capture whose biomes carry no +// features, our undecorated pipeline matches exactly. Plain pairs here are +// therefore feature outputs wearing plain states — monster-room cave_air, +// geode voids, lava-lake lava — and the sample coordinates point at the +// feature, not at the noise stack. Cells where exactly one side is a +// recognizably feature-placed state count as a flip toward that side; the +// remaining ambiguous pairs (disk outputs such as clay look like natural +// terrain) are bucketed separately. // // Run with REGIONIO_BASE_TERRAIN_DIAGNOSTIC=1. diff --git a/internal/world/testdata/vanilla_base_12345.bin b/internal/world/testdata/vanilla_base_12345.bin new file mode 100644 index 0000000..4b68fe7 Binary files /dev/null and b/internal/world/testdata/vanilla_base_12345.bin differ diff --git a/internal/world/vanilla_base_test.go b/internal/world/vanilla_base_test.go new file mode 100644 index 0000000..fadb434 --- /dev/null +++ b/internal/world/vanilla_base_test.go @@ -0,0 +1,177 @@ +package world + +import ( + "encoding/binary" + "fmt" + "io" + "os" + "sort" + "strings" + "testing" + + "regionio/internal/worldgen" +) + +// vanillaBaseFixture holds vanilla overworld terrain captured with every biome +// feature stage and structure set emptied out (cmd/vanillacapture +// -featureless -blocks-only). It is ground truth for exactly what our +// undecorated pipeline reproduces: density terrain, surface rules, carvers, +// aquifers, and noise-router veins — with none of the feature drift that makes +// the full fixture's mismatches hard to attribute. +const vanillaBaseFixture = "testdata/vanilla_base_12345.bin" + +func TestVanillaBaseTerrainParity(t *testing.T) { + f, err := os.Open(vanillaBaseFixture) + if err != nil { + if os.Getenv("REGIONIO_REQUIRE_BASE_PARITY") == "1" { + t.Fatalf("required base-terrain fixture: %v", err) + } + t.Skip("base-terrain fixture not installed; run cmd/vanillacapture -featureless -blocks-only") + } + defer f.Close() + + var header [16]byte + if _, err := io.ReadFull(f, header[:]); err != nil { + t.Fatal(err) + } + if string(header[:8]) != "RIOBASE1" { + t.Fatalf("bad base fixture magic %q", header[:8]) + } + seed := int64(binary.BigEndian.Uint64(header[8:16])) + if seed != 12345 { + t.Fatalf("base fixture seed=%d", seed) + } + + type baseChunk struct { + x, z int32 + blocks []uint16 + } + var chunks []baseChunk + for { + var coords [8]byte + if _, err := io.ReadFull(f, coords[:]); err != nil { + if err == io.EOF { + break + } + t.Fatal(err) + } + chunk := baseChunk{ + x: int32(binary.BigEndian.Uint32(coords[:4])), + z: int32(binary.BigEndian.Uint32(coords[4:])), + blocks: make([]uint16, 16*16*WorldHeight), + } + var state [2]byte + for index := range chunk.blocks { + if _, err := io.ReadFull(f, state[:]); err != nil { + t.Fatal(err) + } + chunk.blocks[index] = binary.BigEndian.Uint16(state[:]) + } + chunks = append(chunks, chunk) + } + if len(chunks) == 0 { + t.Fatal("empty base fixture") + } + + // The capture must actually be featureless: any state that only a feature + // places means the derived datapack did not load and this test would + // compare against decorated terrain without saying so. Copper and + // deepslate iron ores are exempt — the noise-router OreVeinifier owns + // those, and the veins belong to base terrain. + veinOres := map[string]bool{ + "minecraft:copper_ore": true, + "minecraft:deepslate_copper_ore": true, + "minecraft:iron_ore": true, + "minecraft:deepslate_iron_ore": true, + } + for _, chunk := range chunks { + for _, state := range chunk.blocks { + name := stateLabel(state) + featureOnly := strings.HasSuffix(name, "_log") || + strings.HasSuffix(name, "_leaves") || strings.HasSuffix(name, "_planks") || + name == "minecraft:moss_block" || name == "minecraft:magma_block" || + name == "minecraft:calcite" || name == "minecraft:smooth_basalt" || + name == "minecraft:amethyst_block" || name == "minecraft:budding_amethyst" || + (strings.HasSuffix(name, "_ore") && !veinOres[name]) + if featureOnly { + t.Fatalf("capture at (%d,%d) contains %s; the featureless datapack did not load", + chunk.x, chunk.z, name) + } + } + } + + od, err := worldgen.LoadOverworldFinalDensity(seed) + if err != nil { + t.Fatal(err) + } + fluidPicker := worldgen.OverworldFluidPicker(od.SeaLevel) + veins := worldgen.NewOreVeinifier(od) + carver, err := worldgen.NewCarver(od, seed) + if err != nil { + t.Fatal(err) + } + initCarverReplaceable(carver.ReplaceableBlocks()) + + var total, exact int + pairs := make(map[[2]uint16]int) + samples := make(map[[2]uint16][]string) + yRange := make(map[[2]uint16][2]int) + for _, chunk := range chunks { + got := generateVanillaWithoutDecoration(od, fluidPicker, veins, carver, seed, chunk.x, chunk.z) + index := 0 + for y := MinY; y < MinY+WorldHeight; y++ { + for z := 0; z < 16; z++ { + for x := 0; x < 16; x++ { + ours := got.GetBlock(x, y, z) + want := chunk.blocks[index] + index++ + total++ + if ours == want { + exact++ + continue + } + key := [2]uint16{ours, want} + pairs[key]++ + rng := yRange[key] + if rng[0] == 0 || y < rng[0] { + rng[0] = y + } + if y > rng[1] { + rng[1] = y + } + yRange[key] = rng + if len(samples[key]) < 10 { + absX, absZ := int(chunk.x)*16+x, int(chunk.z)*16+z + samples[key] = append(samples[key], fmt.Sprintf("(%d,%d,%d)", absX, y, absZ)) + } + } + } + } + } + + keys := make([][2]uint16, 0, len(pairs)) + for key := range pairs { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + if pairs[keys[i]] != pairs[keys[j]] { + return pairs[keys[i]] > pairs[keys[j]] + } + return stateLabel(keys[i][0])+stateLabel(keys[i][1]) < stateLabel(keys[j][0])+stateLabel(keys[j][1]) + }) + t.Logf("undecorated base parity: exact %d/%d (%.3f%%), mismatched pairs %d", + exact, total, percent(exact, total), len(pairs)) + const maxPairs = 25 + for i, key := range keys { + if i >= maxPairs { + t.Logf("... %d more pairs", len(keys)-maxPairs) + break + } + rng := yRange[key] + t.Logf("base %s -> %s: %d, y=%d..%d, samples %v", + stateLabel(key[0]), stateLabel(key[1]), pairs[key], rng[0], rng[1], samples[key]) + } + if os.Getenv("REGIONIO_REQUIRE_BASE_PARITY") == "1" && exact != total { + t.Fatalf("base-terrain parity failed: %d mismatches", total-exact) + } +}