Biome-aware surface rules from the vanilla rule tree

Replaces the biome-blind fillVanillaColumn heuristics with a full
interpreter for the overworld surface_rule tree (already embedded in
overworld.json): block/sequence/condition/bandlands rules plus all 11
condition tests (biome, steep, hole, water, temperature, y_above,
stone_depth, noise_threshold, not, vertical_gradient,
above_preliminary_surface).

- worldgen/blockids.go: name(+Properties)→network-ID table for surface
  blocks (grass/sand/terracotta/mycelium/podzol/coarse_dirt/sandstone/
  calcite/snow/ice/...), with snowy property variants.
- worldgen/surface.go: rule-tree parser + interpreter + SurfaceContext;
  LoadOverworldSurfaceRule caches the seed-independent tree.
- loader.go: OverworldDensity.SurfaceRule() exposes the parsed tree.
- biome_lookup.go: BiomeNameAt returns the biome name for biome tests.
- vanilla.go: samples the 2D climate + biome before column fill, threads
  the rule tree and biome name into fillVanillaColumn, and applies it
  top-down with stone as the default for non-matching (deeper) blocks.
  The above_preliminary_surface gate uses an inclusive bound so the top
  solid block reaches the biome dispatch.
- Performance: one per-column RNG and a reused SurfaceContext keep the
  overhead to ~+13ms/chunk (71ms vs 58ms baseline), within the gate.
This commit is contained in:
Master290 2026-06-24 19:44:07 +03:00
parent d3142e7687
commit 4dcf938a85
8 changed files with 934 additions and 26 deletions

View file

@ -0,0 +1,107 @@
package worldgen
import (
"math/rand"
"testing"
)
// TestLoadSurfaceRule confirms the embedded overworld surface_rule parses into
// a rule tree without error. This guards the parser against any rule/condition
// type the overworld uses.
func TestLoadSurfaceRule(t *testing.T) {
rule, err := LoadOverworldSurfaceRule()
if err != nil {
t.Fatalf("LoadOverworldSurfaceRule: %v", err)
}
if rule == nil {
t.Fatal("nil surface rule")
}
}
// 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) {
rule, err := LoadOverworldSurfaceRule()
if err != nil {
t.Fatalf("load: %v", err)
}
biomes := []string{
"minecraft:plains", "minecraft:desert", "minecraft:forest",
"minecraft:badlands", "minecraft:snowy_plains", "minecraft:ocean",
"minecraft:mushroom_fields", "minecraft:wooded_badlands",
}
for _, b := range biomes {
for y := 0; y < 100; y++ {
ctx := &SurfaceContext{
X: 100, Y: y, Z: 100, StoneDepthAbove: 100 - y,
SeaLevel: 63, BiomeName: b, MinY: -64,
PreliminarySurface: 100, Rng: rand.New(rand.NewSource(1)),
}
rule.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) {
rule, err := LoadOverworldSurfaceRule()
if err != nil {
t.Fatalf("load: %v", err)
}
ctx := &SurfaceContext{
X: 0, Y: -64, Z: 0, StoneDepthAbove: 0,
SeaLevel: 63, BiomeName: "minecraft:plains", MinY: -64,
PreliminarySurface: 70, Rng: rand.New(rand.NewSource(1)),
}
state, ok := rule.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},
}
for _, c := range cases {
if got := surfaceBlockID(c.name, c.props); got != c.want {
t.Errorf("surfaceBlockID(%q,%v) = %d, want %d", c.name, c.props, got, c.want)
}
}
}
// 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)
}
}
}