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.
This commit is contained in:
Master290 2026-07-27 02:25:09 +03:00
parent 1083e47211
commit c19e5f0e4f
9 changed files with 478 additions and 255 deletions

View file

@ -20,80 +20,97 @@ package worldgen
// surfaceBlockID resolves a surface-rule result_state (Name + optional
// Properties) to its network block-state ID. It handles the snowy property on
// snowable blocks and the layers property on snow; unknown blocks return 0
// (air) so a missing entry is visually obvious rather than crashing.
func surfaceBlockID(name string, props map[string]string) uint16 {
// snowable blocks and the layers property on snow.
//
// An unknown name is an error, not a fallback. It used to return 0, which the
// rule application then read as "no block" and skipped — so a name missing from
// this table silently left stone behind. That is exactly how deepslate went
// missing from the entire world: the rule fired, resolved to 0, and was dropped.
func surfaceBlockID(name string, props map[string]string) (uint16, bool) {
switch name {
case "minecraft:air":
return 0, true
case "minecraft:deepslate":
// A pillar block; the surface rule always asks for the upright axis.
return 27924, true
case "minecraft:mud":
return 27922, true
case "minecraft:brown_terracotta":
return 11456, true
case "minecraft:red_terracotta":
return 11458, true
case "minecraft:light_gray_terracotta":
return 11452, true
case "minecraft:stone":
return 1
return 1, true
case "minecraft:granite":
return 2
return 2, true
case "minecraft:diorite":
return 4
return 4, true
case "minecraft:andesite":
return 6
return 6, true
case "minecraft:grass_block":
if props["snowy"] == "true" {
return 8
return 8, true
}
return 9
return 9, true
case "minecraft:dirt":
return 10
return 10, true
case "minecraft:coarse_dirt":
return 11
return 11, true
case "minecraft:podzol":
if props["snowy"] == "true" {
return 12
return 12, true
}
return 13
return 13, true
case "minecraft:bedrock":
return 85
return 85, true
case "minecraft:water":
return 86
return 86, true
case "minecraft:sand":
return 118
return 118, true
case "minecraft:red_sand":
return 123
return 123, true
case "minecraft:gravel":
return 124
return 124, true
case "minecraft:sandstone":
return 578
return 578, true
case "minecraft:red_sandstone":
return 13247
return 13247, true
case "minecraft:snow_block":
return 6928
return 6928, true
case "minecraft:snow":
// snow has a "layers" property 1..8; default layer 1 = 6919.
return 6919
return 6919, true
case "minecraft:ice":
return 6927
return 6927, true
case "minecraft:packed_ice":
return 12914
return 12914, true
case "minecraft:powder_snow":
return 24689
return 24689, true
case "minecraft:mycelium":
if props["snowy"] == "true" {
return 8918
return 8918, true
}
return 8919
return 8919, true
case "minecraft:terracotta":
return 12912
return 12912, true
case "minecraft:white_terracotta":
return 11444
return 11444, true
case "minecraft:orange_terracotta":
return 11445
return 11445, true
case "minecraft:yellow_terracotta":
return 11448
return 11448, true
case "minecraft:calcite":
return 24687
return 24687, true
case "minecraft:tuff":
return 23452
return 23452, true
case "minecraft:dripstone_block":
return 27755
return 27755, true
case "minecraft:moss_block":
return 27862
return 27862, true
case "minecraft:smooth_stone":
return 13480
return 13480, true
}
return 0
return 0, false
}

View file

@ -56,14 +56,17 @@ type OverworldDensity struct {
// rule tree runs.
Surface *SurfaceSampler
surfaceRule *SurfaceRuleSet
surfaceRuleErr error
prelim *levelCache
}
// SurfaceRule returns the overworld surface rule tree, loading it on first use.
// It does not depend on the seed. A nil rule (on error) is non-fatal: the
// generator falls back to its default surface heuristics.
func (od *OverworldDensity) SurfaceRule() (SurfaceRule, error) {
return LoadOverworldSurfaceRule()
// SurfaceRule returns the overworld surface rule set, compiled against this
// world's seed. A nil rule set (on error) is non-fatal: the generator falls
// back to its biome-blind surface heuristics.
func (od *OverworldDensity) SurfaceRule() (*SurfaceRuleSet, error) {
return od.surfaceRule, od.surfaceRuleErr
}
// LoadOverworldFinalDensity builds the overworld final_density function for the
@ -168,6 +171,12 @@ func LoadOverworldFinalDensity(seed int64) (*OverworldDensity, error) {
secondaryNoise: secondaryNoise,
positionalRand: l.rs.Positional(),
}
// The rule tree is seed-bound: its noise_threshold conditions sample seeded
// noises and its vertical_gradient rolls against a seeded positional
// factory. A failure here is reported but not fatal — the generator keeps
// going on the fallback heuristics rather than refusing to start.
od.surfaceRule, od.surfaceRuleErr = l.loadSurfaceRuleSet(od.MinY, od.Height)
return od, nil
}

View file

@ -33,7 +33,7 @@ func LoadTemplate(path string) (*Template, error) {
if err != nil {
return nil, err
}
// NBT files are usually gzipped.
var r io.Reader = bytes.NewReader(b)
if len(b) >= 2 && b[0] == 0x1f && b[1] == 0x8b {
@ -81,7 +81,7 @@ func LoadTemplate(path string) (*Template, error) {
nameTag, _ := stateComp.Get("Name")
name := string(nameTag.(nbt.String))
props := make(map[string]string)
if propTag, ok := stateComp.Get("Properties"); ok {
if propComp, ok := propTag.(*nbt.Compound); ok {
for _, k := range propComp.Keys() {
@ -92,9 +92,11 @@ func LoadTemplate(path string) (*Template, error) {
}
}
}
id := surfaceBlockID(name, props)
if id == 0 && name != "minecraft:air" {
// Fallback to default block ID for the name
id, ok := surfaceBlockID(name, props)
if !ok {
// Structure templates name far more blocks than the
// surface rules do; fall back to the broader
// default-state table.
id = defaultBlockIDs[name]
}
tmpl.Palette = append(tmpl.Palette, id)
@ -112,7 +114,7 @@ func LoadTemplate(path string) (*Template, error) {
}
stateTag, _ := blockComp.Get("state")
stateIdx := int(stateTag.(nbt.Int))
var pos [3]int
if posTag, ok := blockComp.Get("pos"); ok {
if posList, ok := posTag.(nbt.List); ok && len(posList.Elems) == 3 {

View file

@ -5,7 +5,6 @@ import (
"fmt"
"math"
"math/rand"
"sync"
)
// surface.go implements the vanilla SurfaceRules interpreter: a rule tree that
@ -45,11 +44,8 @@ type SurfaceContext struct {
BiomeName string
// MinY is the world bottom for relative-anchor resolution.
MinY int
// SurfaceNoise is the "minecraft:surface" noise sample at (X,Z); the
// noise_threshold condition ranges over it.
SurfaceNoise float64
// Steep is true when the local slope exceeds the vanilla steep threshold
// (~1.0 surface-depth delta between neighbours).
// Steep is true when the column's neighbours in the chunk differ in height
// by four or more blocks (SurfaceRules.SteepMaterialCondition).
Steep bool
// SurfaceDepth is how thick the biome's surface layers are at this column
// (SurfaceSystem.getSurfaceDepth): usually 3, sometimes 0 or less, which is
@ -63,9 +59,14 @@ type SurfaceContext struct {
// the interpolated preliminary surface level plus SurfaceDepth less 8.
// above_preliminary_surface tests Y against it.
MinSurfaceLevel int
// Rng is a per-column deterministic source for vertical_gradient and
// bandlands. It is seeded by the column so results are stable across runs.
// Rng is a per-column deterministic source for the bandlands rule. It is
// seeded by the column so results are stable across runs.
Rng *rand.Rand
// noiseValues holds one sample per noise the rule tree's noise_threshold
// conditions reference, refreshed once per column by BeginColumn. Vanilla
// caches these the same way, through LazyXZCondition.
noiseValues []float64
}
// SurfaceRule decides the block at a context. Apply returns ok=false when the
@ -113,19 +114,21 @@ func (r conditionRule) Apply(ctx *SurfaceContext) (uint16, bool) {
type bandlandsRule struct{}
func (bandlandsRule) Apply(ctx *SurfaceContext) (uint16, bool) {
orange, _ := surfaceBlockID("minecraft:orange_terracotta", nil)
if ctx.Rng == nil {
return surfaceBlockID("minecraft:orange_terracotta", nil), true
return orange, true
}
// Vanilla chooses band by Y + a per-column random offset; the rotation
// cycles white/orange/yellow/orange terracotta. Pick from the cycle by Y.
band := (ctx.Y + ctx.Rng.Intn(7)) % 4
switch band {
white, _ := surfaceBlockID("minecraft:white_terracotta", nil)
yellow, _ := surfaceBlockID("minecraft:yellow_terracotta", nil)
switch (ctx.Y + ctx.Rng.Intn(7)) % 4 {
case 0:
return surfaceBlockID("minecraft:white_terracotta", nil), true
return white, true
case 1, 3:
return surfaceBlockID("minecraft:orange_terracotta", nil), true
return orange, true
default:
return surfaceBlockID("minecraft:yellow_terracotta", nil), true
return yellow, true
}
}
@ -153,12 +156,12 @@ type steepTest struct{}
func (steepTest) Test(ctx *SurfaceContext) bool { return ctx.Steep }
// holeTest passes in surface "holes" below the surrounding terrain — we
// approximate as "below sea level and not the top" since true hole detection
// needs a neighbourhood. Conservative: false (rare rule, low visual cost).
// holeTest passes where the surface depth noise came out at or below zero — a
// bare patch with no surface layer at all, which is how coarse dirt and gravel
// scars appear in the middle of grass.
type holeTest struct{}
func (holeTest) Test(ctx *SurfaceContext) bool { return false }
func (holeTest) Test(ctx *SurfaceContext) bool { return ctx.SurfaceDepth <= 0 }
// waterTest passes when the block is clear of the water above it — either there
// is none, or it sits far enough below the water's underside
@ -208,34 +211,21 @@ func isColdBiome(name string) bool {
return false
}
// yAboveTest passes when Y is above an anchor (absolute, above_bottom, or
// below_top), with optional surface-depth and stone-depth offsets.
// yAboveTest passes when Y clears an anchor, with optional surface-depth and
// stone-depth offsets. The anchor is resolved against the world's height bounds
// at parse time.
type yAboveTest struct {
absolute int
hasAbsolute bool
aboveBottom int
hasAboveBottom bool
belowTop int
hasBelowTop bool
addStoneDepth bool
surfaceDepthMul int
anchorY int
addStoneDepth bool
surfaceDepthMul int
}
func (t yAboveTest) Test(ctx *SurfaceContext) bool {
var anchor int
switch {
case t.hasAbsolute:
anchor = t.absolute
case t.hasAboveBottom:
anchor = ctx.MinY + t.aboveBottom
case t.hasBelowTop:
anchor = (ctx.MinY + 384) - 1 - t.belowTop
}
threshold := anchor + ctx.SurfaceDepth*t.surfaceDepthMul
y := ctx.Y
if t.addStoneDepth {
threshold += ctx.StoneDepthAbove
y += ctx.StoneDepthAbove
}
return ctx.Y >= threshold
return y >= t.anchorY+ctx.SurfaceDepth*t.surfaceDepthMul
}
// stoneDepthTest passes when the block is within `offset` of the surface it
@ -266,19 +256,22 @@ func (t stoneDepthTest) Test(ctx *SurfaceContext) bool {
return depth <= 1+t.offset+surfaceDepth+secondary
}
// noiseThresholdTest passes when the named surface noise is within [min,max].
// noiseThresholdTest passes when its noise, sampled once per column at y=0, is
// within [min,max]. slot indexes SurfaceContext.noiseValues, which the rule set
// refreshes per column.
//
// Six of the seven noises the overworld tree uses were unsupported and fell
// through as false, so calcite on stony peaks, ice and packed ice on frozen
// peaks, powder snow, swamp water windows and gravel patches on stony shores
// never appeared at all.
type noiseThresholdTest struct {
min, max float64
noise string
slot int
}
func (t noiseThresholdTest) Test(ctx *SurfaceContext) bool {
// Only "minecraft:surface" is sampled in SurfaceContext; other noises fall
// through as false (conservative).
if t.noise != "minecraft:surface" {
return false
}
return ctx.SurfaceNoise >= t.min && ctx.SurfaceNoise <= t.max
v := ctx.noiseValues[t.slot]
return v >= t.min && v <= t.max
}
// notTest inverts its inner test.
@ -286,33 +279,30 @@ type notTest struct{ inner ConditionTest }
func (t notTest) Test(ctx *SurfaceContext) bool { return !t.inner.Test(ctx) }
// verticalGradientTest reproduces the bedrock-floor gradient: a deterministic
// band from true_at_and_below to false_at_and_above where membership tapers via
// the column RNG. Anchors are above_bottom offsets from the world floor.
// verticalGradientTest is the scattered transition between two layers: true
// below one anchor, false above another, and in between a per-position coin
// flip whose bias falls linearly with height. It draws the bedrock floor and
// the stone-to-deepslate boundary.
//
// The anchors are resolved once at parse time, so this needs the world's height
// bounds; the random factory is named by the rule (bedrock_floor, deepslate)
// and forked from the world seed, so the same y gets the same answer every
// time the chunk regenerates.
type verticalGradientTest struct {
randomName string
trueAtAndBelow int // above_bottom
falseAtAndAbove int // above_bottom
trueAtAndBelow int
falseAtAndAbove int
random PositionalRandomFactory
}
func (t verticalGradientTest) Test(ctx *SurfaceContext) bool {
loY := ctx.MinY + t.trueAtAndBelow
hiY := ctx.MinY + t.falseAtAndAbove
switch {
case ctx.Y <= loY:
if ctx.Y <= t.trueAtAndBelow {
return true
case ctx.Y >= hiY:
}
if ctx.Y >= t.falseAtAndAbove {
return false
}
// Taper band: probability decreases linearly. Use the per-column RNG once
// per Y so the floor is stable but noisy. We approximate vanilla's
// random-based interpolation.
if ctx.Rng == nil {
return false
}
band := hiY - loY
pos := ctx.Y - loY
return ctx.Rng.Float64() > float64(pos)/float64(band)
probability := mapRange(float64(ctx.Y), float64(t.trueAtAndBelow), float64(t.falseAtAndAbove), 1.0, 0.0)
return float64(t.random.At(ctx.X, ctx.Y, ctx.Z).NextFloat()) < probability
}
// abovePreliminarySurfaceTest gates the whole biome surface subtree: below the
@ -325,8 +315,77 @@ func (abovePreliminarySurfaceTest) Test(ctx *SurfaceContext) bool {
// ---- Parser ------------------------------------------------------------
// ParseSurfaceRule parses a surface_rule JSON node into a rule tree.
func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
// SurfaceRuleSet is a compiled surface rule tree together with the seeded
// noises and random factories its conditions reference.
//
// The tree used to be parsed once, globally, and shared by every world: the
// conditions that need the seed simply did not work. Binding it to a
// RandomState is what lets noise_threshold sample a real noise and
// vertical_gradient roll a real per-position coin.
type SurfaceRuleSet struct {
root SurfaceRule
noises []*NormalNoise
}
// NewContext returns a SurfaceContext sized for this rule set's per-column
// noise cache. Reuse one per goroutine; BeginColumn refreshes it.
func (s *SurfaceRuleSet) NewContext() *SurfaceContext {
return &SurfaceContext{noiseValues: make([]float64, len(s.noises))}
}
// BeginColumn samples every noise the tree references at (x, z) and stores the
// column coordinates. Vanilla samples these lazily and caches them per column;
// sampling all of them up front costs a handful of evaluations per column and
// keeps the tree free of hidden state.
func (s *SurfaceRuleSet) BeginColumn(ctx *SurfaceContext, x, z int) {
ctx.X, ctx.Z = x, z
for i, n := range s.noises {
ctx.noiseValues[i] = n.GetValue(float64(x), 0, float64(z))
}
}
// Apply runs the tree at the context's current position.
func (s *SurfaceRuleSet) Apply(ctx *SurfaceContext) (uint16, bool) { return s.root.Apply(ctx) }
// surfaceParser carries the seed-dependent state a rule tree needs while it is
// being built: where to get noises and random factories, and the world's height
// bounds for resolving vertical anchors.
type surfaceParser struct {
loader *Loader
minY, height int
noises []*NormalNoise
noiseSlots map[string]int
}
// noiseSlot returns the per-column cache index for a named noise, loading and
// seeding it on first use.
func (p *surfaceParser) noiseSlot(name string) (int, error) {
if slot, ok := p.noiseSlots[name]; ok {
return slot, nil
}
n, err := p.loader.noiseField(name)
if err != nil {
return 0, err
}
slot := len(p.noises)
p.noises = append(p.noises, n)
p.noiseSlots[name] = slot
return slot, nil
}
// resolveAnchor is VerticalAnchor.resolveY.
func (p *surfaceParser) resolveAnchor(a anchorJSON) int {
switch a.kind {
case anchorAboveBottom:
return p.minY + a.value
case anchorBelowTop:
return p.minY + p.height - 1 - a.value
default:
return a.value
}
}
func (p *surfaceParser) parseRule(raw json.RawMessage) (SurfaceRule, error) {
var obj struct {
Type string `json:"type"`
}
@ -344,7 +403,11 @@ func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
if err := json.Unmarshal(raw, &b); err != nil {
return nil, err
}
return blockRule{state: surfaceBlockID(b.Result.Name, b.Result.Properties)}, nil
state, ok := surfaceBlockID(b.Result.Name, b.Result.Properties)
if !ok {
return nil, fmt.Errorf("surface: no block-state ID for %q %v", b.Result.Name, b.Result.Properties)
}
return blockRule{state: state}, nil
case "minecraft:sequence":
var s struct {
@ -355,7 +418,7 @@ func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
}
rules := make([]SurfaceRule, 0, len(s.Sequence))
for _, child := range s.Sequence {
r, err := ParseSurfaceRule(child)
r, err := p.parseRule(child)
if err != nil {
return nil, err
}
@ -371,11 +434,11 @@ func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
if err := json.Unmarshal(raw, &c); err != nil {
return nil, err
}
test, err := parseCondition(c.IfTrue)
test, err := p.parseCondition(c.IfTrue)
if err != nil {
return nil, err
}
then, err := ParseSurfaceRule(c.Then)
then, err := p.parseRule(c.Then)
if err != nil {
return nil, err
}
@ -388,7 +451,7 @@ func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
}
// parseCondition parses an if_true condition node into a ConditionTest.
func parseCondition(raw json.RawMessage) (ConditionTest, error) {
func (p *surfaceParser) parseCondition(raw json.RawMessage) (ConditionTest, error) {
var obj struct {
Type string `json:"type"`
}
@ -413,9 +476,9 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) {
case "minecraft:water":
var w struct {
Offset int `json:"offset"`
SurfaceDepthMul int `json:"surface_depth_multiplier"`
AddStoneDepth bool `json:"add_stone_depth"`
Offset int `json:"offset"`
SurfaceDepthMul int `json:"surface_depth_multiplier"`
AddStoneDepth bool `json:"add_stone_depth"`
}
if err := json.Unmarshal(raw, &w); err != nil {
return nil, err
@ -446,32 +509,26 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) {
if err := json.Unmarshal(raw, &n); err != nil {
return nil, err
}
return noiseThresholdTest{min: n.Min, max: n.Max, noise: n.Noise}, nil
slot, err := p.noiseSlot(n.Noise)
if err != nil {
return nil, fmt.Errorf("noise_threshold %q: %w", n.Noise, err)
}
return noiseThresholdTest{min: n.Min, max: n.Max, slot: slot}, nil
case "minecraft:y_above":
var y struct {
AddStoneDepth bool `json:"add_stone_depth"`
SurfaceDepthMul int `json:"surface_depth_multiplier"`
Anchor struct {
Absolute *int `json:"absolute"`
AboveBottom *int `json:"above_bottom"`
BelowTop *int `json:"below_top"`
} `json:"anchor"`
AddStoneDepth bool `json:"add_stone_depth"`
SurfaceDepthMul int `json:"surface_depth_multiplier"`
Anchor anchorJSON `json:"anchor"`
}
if err := json.Unmarshal(raw, &y); err != nil {
return nil, err
}
t := yAboveTest{addStoneDepth: y.AddStoneDepth, surfaceDepthMul: y.SurfaceDepthMul}
if y.Anchor.Absolute != nil {
t.hasAbsolute, t.absolute = true, *y.Anchor.Absolute
}
if y.Anchor.AboveBottom != nil {
t.hasAboveBottom, t.aboveBottom = true, *y.Anchor.AboveBottom
}
if y.Anchor.BelowTop != nil {
t.hasBelowTop, t.belowTop = true, *y.Anchor.BelowTop
}
return t, nil
return yAboveTest{
anchorY: p.resolveAnchor(y.Anchor),
addStoneDepth: y.AddStoneDepth,
surfaceDepthMul: y.SurfaceDepthMul,
}, nil
case "minecraft:not":
var n struct {
@ -480,7 +537,7 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) {
if err := json.Unmarshal(raw, &n); err != nil {
return nil, err
}
inner, err := parseCondition(n.Invert)
inner, err := p.parseCondition(n.Invert)
if err != nil {
return nil, err
}
@ -488,15 +545,20 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) {
case "minecraft:vertical_gradient":
var v struct {
RandomName string `json:"random_name"`
TrueAtAndBelow anchorJSON `json:"true_at_and_below"`
FalseAtAndAbove anchorJSON `json:"false_at_and_above"`
}
if err := json.Unmarshal(raw, &v); err != nil {
return nil, err
}
if v.RandomName == "" {
return nil, fmt.Errorf("vertical_gradient: missing random_name")
}
return verticalGradientTest{
trueAtAndBelow: v.TrueAtAndBelow.aboveBottom,
falseAtAndAbove: v.FalseAtAndAbove.aboveBottom,
trueAtAndBelow: p.resolveAnchor(v.TrueAtAndBelow),
falseAtAndAbove: p.resolveAnchor(v.FalseAtAndAbove),
random: p.loader.rs.Positional().FromHashOf(v.RandomName).ForkPositional(),
}, nil
case "minecraft:above_preliminary_surface":
@ -505,49 +567,56 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) {
return nil, fmt.Errorf("surface: unknown condition type %q", obj.Type)
}
// anchorJSON decodes a {above_bottom|below_top|absolute: N} surface anchor.
// anchorJSON decodes a VerticalAnchor: exactly one of absolute, above_bottom or
// below_top. Which one it was matters — reading the value without the kind made
// every absolute anchor resolve as an offset from the world floor, which is why
// the deepslate rule (absolute 0 to 8) collapsed onto y=-64 and never fired.
type anchorJSON struct {
absolute int
aboveBottom int
belowTop int
kind anchorKind
value int
}
type anchorKind int
const (
anchorAbsolute anchorKind = iota
anchorAboveBottom
anchorBelowTop
)
func (a *anchorJSON) UnmarshalJSON(data []byte) error {
var m map[string]int
if err := json.Unmarshal(data, &m); err != nil {
return err
}
a.aboveBottom = m["above_bottom"]
a.belowTop = m["below_top"]
a.absolute = m["absolute"]
return nil
for key, kind := range map[string]anchorKind{
"absolute": anchorAbsolute,
"above_bottom": anchorAboveBottom,
"below_top": anchorBelowTop,
} {
if v, ok := m[key]; ok {
a.kind, a.value = kind, v
return nil
}
}
return fmt.Errorf("surface: anchor has none of absolute/above_bottom/below_top")
}
// ---- Loader ------------------------------------------------------------
var (
surfaceRuleOnce sync.Once
surfaceRule SurfaceRule
surfaceRuleErr error
)
// LoadOverworldSurfaceRule parses and caches the overworld surface_rule tree.
// The rule tree does not depend on the world seed, so it is loaded once.
func LoadOverworldSurfaceRule() (SurfaceRule, error) {
surfaceRuleOnce.Do(func() {
raw, err := dataFS.ReadFile("data/overworld.json")
if err != nil {
surfaceRuleErr = err
return
}
var doc struct {
SurfaceRule json.RawMessage `json:"surface_rule"`
}
if err := json.Unmarshal(raw, &doc); err != nil {
surfaceRuleErr = err
return
}
surfaceRule, surfaceRuleErr = ParseSurfaceRule(doc.SurfaceRule)
})
return surfaceRule, surfaceRuleErr
// loadSurfaceRuleSet parses the overworld surface_rule tree, binding its
// conditions to this loader's seeded RandomState.
func (l *Loader) loadSurfaceRuleSet(minY, height int) (*SurfaceRuleSet, error) {
var doc struct {
SurfaceRule json.RawMessage `json:"surface_rule"`
}
if err := l.readJSON("data/overworld.json", &doc); err != nil {
return nil, err
}
p := &surfaceParser{loader: l, minY: minY, height: height, noiseSlots: map[string]int{}}
root, err := p.parseRule(doc.SurfaceRule)
if err != nil {
return nil, err
}
return &SurfaceRuleSet{root: root, noises: p.noises}, nil
}

View file

@ -5,16 +5,30 @@ import (
"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()
// 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("LoadOverworldSurfaceRule: %v", err)
t.Fatalf("load overworld density: %v", err)
}
if rule == nil {
t.Fatal("nil surface rule")
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))
}
}
@ -22,25 +36,24 @@ func TestLoadSurfaceRule(t *testing.T) {
// 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)
}
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 := &SurfaceContext{
X: 100, Y: y, Z: 100,
StoneDepthAbove: 100 - y, StoneDepthBelow: y + 1,
SeaLevel: 63, BiomeName: b, MinY: -64,
MinSurfaceLevel: 80, WaterHeight: NoWaterAbove,
Rng: rand.New(rand.NewSource(1)),
}
rule.Apply(ctx) // must not panic
ctx.Y = y
ctx.StoneDepthAbove, ctx.StoneDepthBelow = 100-y, y+1
rules.Apply(ctx) // must not panic
}
}
}
@ -48,17 +61,17 @@ func TestSurfaceRuleNoPanic(t *testing.T) {
// 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: 1, StoneDepthBelow: 1,
SeaLevel: 63, BiomeName: "minecraft:plains", MinY: -64,
MinSurfaceLevel: 62, WaterHeight: NoWaterAbove,
Rng: rand.New(rand.NewSource(1)),
}
state, ok := rule.Apply(ctx)
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")
}
@ -84,12 +97,23 @@ func TestSurfaceBlockIDResolution(t *testing.T) {
{"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 {
if got := surfaceBlockID(c.name, c.props); got != c.want {
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