RegionIO/internal/worldgen/surface_test.go
Master290 c19e5f0e4f Bind the surface rule tree to the world seed
The tree was parsed once, globally, and shared by every world -- so every
condition that needs the seed simply did not work. Compiling it per RandomState
fixes four of them at once.

noise_threshold sampled a per-column random draw and pretended it was
"minecraft:surface"; the other six noises it names were unsupported and returned
false. Each condition now holds its own seeded noise, sampled once per column
into a small cache the way vanilla's LazyXZCondition does. Powder snow, packed
ice and ice appear in the dump for the first time; calcite, swamp water windows
and gravel patches have their conditions back too.

vertical_gradient tapered through a per-column RNG shared with the other rules.
Vanilla rolls a positional random at the exact block, from a factory named by
the rule. More importantly the anchor decoder read only above_bottom and
discarded which kind of anchor it was, so the deepslate rule's absolute 0..8
collapsed onto y=-64 and **no deepslate existed anywhere in the world**. Anchors
now carry their kind and resolve against the real height bounds -- which also
retires a hardcoded 384 in y_above.

Two more stubs land with them: hole is surfaceDepth <= 0 rather than a constant
false, and steep reads the neighbouring column heights. steep needs the whole
chunk's heightmap, so the column pass is now two passes -- terrain and fluids
for all 256 columns, then surface rules -- which is the order vanilla uses
anyway (doFill, then buildSurface).

Deepslate was also missing from the block-ID table, and an unknown name resolved
to 0, which the caller read as "no block" and skipped. So even a correct rule
would have placed nothing. Unknown names are now a parse error, deepslate and
mud are in the table, and a rule that resolves to air genuinely places air --
the frozen-ocean surface asks for exactly that.

Below y=0 is now entirely deepslate, y=1..7 a scatter, above y=8 none.
2026-07-27 02:25:09 +03:00

195 lines
5.9 KiB
Go

package worldgen
import (
"math/rand"
"testing"
)
// loadTestRules compiles the overworld surface rule set at a fixed seed.
func loadTestRules(t *testing.T) *SurfaceRuleSet {
t.Helper()
od, err := LoadOverworldFinalDensity(12345)
if err != nil {
t.Fatalf("load overworld density: %v", err)
}
rules, err := od.SurfaceRule()
if err != nil {
t.Fatalf("compile surface rule: %v", err)
}
if rules == nil {
t.Fatal("nil surface rule set")
}
return rules
}
// TestLoadSurfaceRule confirms the embedded overworld surface_rule parses into
// a rule tree without error, and that every noise its noise_threshold
// conditions name resolved. Six of the seven used to fall through as false.
func TestLoadSurfaceRule(t *testing.T) {
rules := loadTestRules(t)
if len(rules.noises) != 7 {
t.Errorf("rule set references %d noises, want 7", len(rules.noises))
}
}
// TestSurfaceRuleNoPanic runs the full rule tree across a range of Y values and
// several biomes to confirm Apply never panics on real-world inputs. A panic
// during generation would crash the server.
func TestSurfaceRuleNoPanic(t *testing.T) {
rules := loadTestRules(t)
biomes := []string{
"minecraft:plains", "minecraft:desert", "minecraft:forest",
"minecraft:badlands", "minecraft:snowy_plains", "minecraft:ocean",
"minecraft:mushroom_fields", "minecraft:wooded_badlands",
}
ctx := rules.NewContext()
rules.BeginColumn(ctx, 100, 100)
ctx.SeaLevel, ctx.MinY = 63, -64
ctx.MinSurfaceLevel, ctx.WaterHeight = 80, NoWaterAbove
ctx.SurfaceDepth = 3
ctx.Rng = rand.New(rand.NewSource(1))
for _, b := range biomes {
ctx.BiomeName = b
for y := 0; y < 100; y++ {
ctx.Y = y
ctx.StoneDepthAbove, ctx.StoneDepthBelow = 100-y, y+1
rules.Apply(ctx) // must not panic
}
}
}
// TestSurfaceBedrockFloor confirms the bottom of the world resolves to bedrock
// (the vertical_gradient bedrock_floor rule is the first rule in the tree).
func TestSurfaceBedrockFloor(t *testing.T) {
rules := loadTestRules(t)
ctx := rules.NewContext()
rules.BeginColumn(ctx, 0, 0)
ctx.Y = -64
ctx.StoneDepthAbove, ctx.StoneDepthBelow = 1, 1
ctx.SeaLevel, ctx.MinY = 63, -64
ctx.BiomeName = "minecraft:plains"
ctx.MinSurfaceLevel, ctx.WaterHeight = 62, NoWaterAbove
ctx.SurfaceDepth = 3
ctx.Rng = rand.New(rand.NewSource(1))
state, ok := rules.Apply(ctx)
if !ok {
t.Fatal("no rule matched at bedrock floor")
}
if state != 85 { // bedrock
t.Errorf("bedrock floor state = %d, want 85", state)
}
}
// TestSurfaceBlockIDResolution checks the block-ID table covers the blocks the
// overworld surface_rule references, including snowy property variants.
func TestSurfaceBlockIDResolution(t *testing.T) {
cases := []struct {
name string
props map[string]string
want uint16
}{
{"minecraft:bedrock", nil, 85},
{"minecraft:grass_block", nil, 9},
{"minecraft:grass_block", map[string]string{"snowy": "true"}, 8},
{"minecraft:mycelium", nil, 8919},
{"minecraft:podzol", map[string]string{"snowy": "true"}, 12},
{"minecraft:terracotta", nil, 12912},
{"minecraft:red_sand", nil, 123},
{"minecraft:coarse_dirt", nil, 11},
{"minecraft:calcite", nil, 24687},
{"minecraft:deepslate", map[string]string{"axis": "y"}, 27924},
{"minecraft:mud", nil, 27922},
{"minecraft:air", nil, 0},
}
for _, c := range cases {
got, ok := surfaceBlockID(c.name, c.props)
if !ok {
t.Errorf("surfaceBlockID(%q,%v) not in the table", c.name, c.props)
continue
}
if got != c.want {
t.Errorf("surfaceBlockID(%q,%v) = %d, want %d", c.name, c.props, got, c.want)
}
}
if _, ok := surfaceBlockID("minecraft:not_a_block", nil); ok {
t.Error("surfaceBlockID accepted an unknown name")
}
}
// TestIsColdBiome confirms the snow-cover predicate recognises cold biomes so
// the temperature condition routes snowy biomes to snow.
func TestIsColdBiome(t *testing.T) {
cold := []string{"minecraft:snowy_plains", "minecraft:frozen_peaks", "minecraft:grove"}
for _, b := range cold {
if !isColdBiome(b) {
t.Errorf("isColdBiome(%q) = false, want true", b)
}
}
warm := []string{"minecraft:desert", "minecraft:plains", "minecraft:badlands"}
for _, b := range warm {
if isColdBiome(b) {
t.Errorf("isColdBiome(%q) = true, want false", b)
}
}
}
// TestWaterCondition pins SurfaceRules.WaterConditionSource against the
// column's own water surface. The offsets are the three forms the overworld
// tree actually uses: (0,0,false) for "is this block dry", (-1,0,false) for the
// block just under the waterline, and (-6,-1,true) for the beach/shore band.
func TestWaterCondition(t *testing.T) {
cases := []struct {
name string
test waterTest
ctx SurfaceContext
want bool
}{
{
name: "no water above passes",
test: waterTest{},
ctx: SurfaceContext{Y: 20, WaterHeight: NoWaterAbove},
want: true,
},
{
name: "at the waterline passes",
test: waterTest{},
ctx: SurfaceContext{Y: 63, WaterHeight: 63},
want: true,
},
{
name: "one block under water fails",
test: waterTest{},
ctx: SurfaceContext{Y: 62, WaterHeight: 63},
want: false,
},
{
name: "offset -1 reaches one block deeper",
test: waterTest{offset: -1},
ctx: SurfaceContext{Y: 62, WaterHeight: 63},
want: true,
},
{
name: "an aquifer pool at y=-20 is measured against itself, not sea level",
test: waterTest{},
ctx: SurfaceContext{Y: -25, WaterHeight: -20, SeaLevel: 63},
want: false,
},
{
name: "stone above the same pool is dry",
test: waterTest{},
ctx: SurfaceContext{Y: -19, WaterHeight: -20, SeaLevel: 63},
want: true,
},
{
name: "add_stone_depth counts buried stone towards the threshold",
test: waterTest{offset: -6, surfaceDepthMul: -1, addStoneDepth: true},
ctx: SurfaceContext{Y: 55, WaterHeight: 63, StoneDepthAbove: 2, SurfaceDepth: 0},
want: true,
},
}
for _, c := range cases {
if got := c.test.Test(&c.ctx); got != c.want {
t.Errorf("%s: got %v, want %v", c.name, got, c.want)
}
}
}