Every land column was one block of grass sitting straight on stone. No dirt
under grass, no sandstone under sand, nothing. Two stubs did it together:
above_preliminary_surface compared blockY against the column's actual top block,
so of every position in the column exactly one passed -- and the entire
biome-specific half of the surface rule tree hangs under that condition.
Vanilla compares against a minimum surface level: the preliminary surface level
sampled at the four corners of the 16-block cell, bilinearly interpolated, plus
the surface depth less 8. That is about twenty blocks of reach on ordinary
terrain, which is what the biome subtree is written against.
Surface depth was hardcoded to 0. Vanilla is surfaceNoise*2.75 + 3 with a
per-column jitter, so it comes out around three; it sets how thick the band is
and feeds every add_surface_depth term in the tree. Zero collapsed them all.
Also samples surface_secondary, so stone_depth's secondary_depth_range widens
its band instead of being parsed and dropped.
Grass columns now read grass, two to four dirt, stone -- the histogram over 256
columns is {2: 223, 3: 33}, against vanilla's 2..4. gendump prints it and fails
if the band collapses again; TestGrassColumnsHaveDirt guards it in the suite.
439 lines
13 KiB
Go
439 lines
13 KiB
Go
package worldgen
|
|
|
|
import (
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
//go:embed data
|
|
var dataFS embed.FS
|
|
|
|
// Loader parses the embedded datapack density-function tree into evaluatable
|
|
// nodes, seeding noises through a RandomState. Shared sub-functions are cached
|
|
// by name so the DAG is built once.
|
|
type Loader struct {
|
|
rs *RandomState
|
|
dfCache map[string]DensityFunction
|
|
interpolated []*Interpolated
|
|
}
|
|
|
|
// OverworldDensity is the parsed final_density plus the set of Interpolated
|
|
// nodes that the generator samples on the cell grid.
|
|
type OverworldDensity struct {
|
|
Final DensityFunction
|
|
Interpolated []*Interpolated
|
|
// Climate parameters sampled by the biome finder. Read from the same
|
|
// noise_router as final_density. The router keys map to climate axes:
|
|
// temperature→Temperature, vegetation→Humidity, continents→Continentalness,
|
|
// erosion→Erosion, ridges→Weirdness, depth→Depth.
|
|
Temperature, Humidity, Continentalness, Erosion, Weirdness, Depth DensityFunction
|
|
// Aquifer inputs (NoiseRouter.barrierNoise and friends). Barrier is the
|
|
// pressure noise that seals an aquifer off from the surrounding stone;
|
|
// FluidLevelFloodedness and FluidLevelSpread decide whether a cell holds
|
|
// fluid and at what level; Lava turns deep aquifers into lava.
|
|
Barrier, FluidLevelFloodedness, FluidLevelSpread, Lava DensityFunction
|
|
// Ore-vein inputs (unused until the OreVeinifier lands, but parsed here so
|
|
// the whole router is wired in one place).
|
|
VeinToggle, VeinRidged, VeinGap DensityFunction
|
|
// PreliminarySurfaceLevel is the cheap surface estimate used by the aquifer
|
|
// and by the above_preliminary_surface surface-rule condition. Read it
|
|
// through PreliminarySurfaceLevelAt, which quart-aligns and memoises.
|
|
PreliminarySurfaceLevel DensityFunction
|
|
|
|
// Settings read from the same noise settings file.
|
|
SeaLevel int
|
|
MinY int
|
|
Height int
|
|
AquifersEnabled bool
|
|
OreVeinsEnabled bool
|
|
|
|
// AquiferRandom places the aquifer cell centres.
|
|
AquiferRandom PositionalRandomFactory
|
|
|
|
// Surface samples the noises SurfaceSystem reads per column, before the
|
|
// rule tree runs.
|
|
Surface *SurfaceSampler
|
|
|
|
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()
|
|
}
|
|
|
|
// LoadOverworldFinalDensity builds the overworld final_density function for the
|
|
// given world seed.
|
|
func LoadOverworldFinalDensity(seed int64) (*OverworldDensity, error) {
|
|
l := &Loader{rs: NewRandomState(seed), dfCache: make(map[string]DensityFunction)}
|
|
var settings struct {
|
|
NoiseRouter map[string]json.RawMessage `json:"noise_router"`
|
|
SeaLevel int `json:"sea_level"`
|
|
Noise struct {
|
|
MinY int `json:"min_y"`
|
|
Height int `json:"height"`
|
|
} `json:"noise"`
|
|
AquifersEnabled bool `json:"aquifers_enabled"`
|
|
OreVeinsEnabled bool `json:"ore_veins_enabled"`
|
|
}
|
|
if err := l.readJSON("data/overworld.json", &settings); err != nil {
|
|
return nil, err
|
|
}
|
|
var node any
|
|
if err := json.Unmarshal(settings.NoiseRouter["final_density"], &node); err != nil {
|
|
return nil, err
|
|
}
|
|
final, err := l.parseNode(node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
od := &OverworldDensity{
|
|
Final: final,
|
|
SeaLevel: settings.SeaLevel,
|
|
MinY: settings.Noise.MinY,
|
|
Height: settings.Noise.Height,
|
|
AquifersEnabled: settings.AquifersEnabled,
|
|
OreVeinsEnabled: settings.OreVeinsEnabled,
|
|
AquiferRandom: l.rs.AquiferRandom(),
|
|
prelim: newLevelCache(),
|
|
}
|
|
|
|
// Parse the remaining router keys. Each resolves to a density function via
|
|
// the same parseNode/loadRef machinery as final_density. A missing key is
|
|
// not fatal — the field stays nil and its consumer treats it as absent —
|
|
// but a parse error is.
|
|
//
|
|
// The climate keys feed the biome finder (temperature→Temperature,
|
|
// vegetation→Humidity, continents→Continentalness, erosion→Erosion,
|
|
// ridges→Weirdness, depth→Depth); the rest feed the aquifer, the ore veins
|
|
// and the preliminary surface estimate.
|
|
//
|
|
// The order is fixed rather than a map range: parsing assigns Interpolated
|
|
// node indices in encounter order, and those indices address the cell-corner
|
|
// grids the generator fills.
|
|
routerKeys := []struct {
|
|
key string
|
|
dst *DensityFunction
|
|
}{
|
|
{"temperature", &od.Temperature},
|
|
{"vegetation", &od.Humidity},
|
|
{"continents", &od.Continentalness},
|
|
{"erosion", &od.Erosion},
|
|
{"ridges", &od.Weirdness},
|
|
{"depth", &od.Depth},
|
|
{"barrier", &od.Barrier},
|
|
{"fluid_level_floodedness", &od.FluidLevelFloodedness},
|
|
{"fluid_level_spread", &od.FluidLevelSpread},
|
|
{"lava", &od.Lava},
|
|
{"vein_toggle", &od.VeinToggle},
|
|
{"vein_ridged", &od.VeinRidged},
|
|
{"vein_gap", &od.VeinGap},
|
|
{"preliminary_surface_level", &od.PreliminarySurfaceLevel},
|
|
}
|
|
for _, rk := range routerKeys {
|
|
raw, ok := settings.NoiseRouter[rk.key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
var cn any
|
|
if err := json.Unmarshal(raw, &cn); err != nil {
|
|
return nil, fmt.Errorf("parse router key %q: %w", rk.key, err)
|
|
}
|
|
df, err := l.parseNode(cn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("router key %q: %w", rk.key, err)
|
|
}
|
|
*rk.dst = df
|
|
}
|
|
// Interpolated nodes are collected as the whole router is parsed, so the
|
|
// list has to be taken after the loop, not just after final_density.
|
|
od.Interpolated = l.interpolated
|
|
|
|
// SurfaceSystem's own noises. They are not router keys: vanilla pulls them
|
|
// straight out of the noise registry when it builds the SurfaceSystem.
|
|
surfaceNoise, err := l.noiseField("minecraft:surface")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("surface noise: %w", err)
|
|
}
|
|
secondaryNoise, err := l.noiseField("minecraft:surface_secondary")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("surface_secondary noise: %w", err)
|
|
}
|
|
od.Surface = &SurfaceSampler{
|
|
surfaceNoise: surfaceNoise,
|
|
secondaryNoise: secondaryNoise,
|
|
positionalRand: l.rs.Positional(),
|
|
}
|
|
return od, nil
|
|
}
|
|
|
|
func (l *Loader) readJSON(path string, v any) error {
|
|
b, err := dataFS.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
return json.Unmarshal(b, v)
|
|
}
|
|
|
|
// parseNode builds a density function from a decoded JSON value: a number is a
|
|
// constant, a string is a reference to another density-function file, and an
|
|
// object is a typed node.
|
|
func (l *Loader) parseNode(v any) (DensityFunction, error) {
|
|
switch t := v.(type) {
|
|
case float64:
|
|
return Constant(t), nil
|
|
case string:
|
|
return l.loadRef(t)
|
|
case map[string]any:
|
|
return l.parseObject(t)
|
|
default:
|
|
return nil, fmt.Errorf("unexpected density-function node %T", v)
|
|
}
|
|
}
|
|
|
|
// loadRef loads and caches a density function referenced by resource location.
|
|
func (l *Loader) loadRef(name string) (DensityFunction, error) {
|
|
if df, ok := l.dfCache[name]; ok {
|
|
return df, nil
|
|
}
|
|
path := "data/density_function/" + strings.TrimPrefix(name, "minecraft:") + ".json"
|
|
var node any
|
|
if err := l.readJSON(path, &node); err != nil {
|
|
return nil, err
|
|
}
|
|
df, err := l.parseNode(node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("in %s: %w", name, err)
|
|
}
|
|
l.dfCache[name] = df
|
|
return df, nil
|
|
}
|
|
|
|
func (l *Loader) parseObject(m map[string]any) (DensityFunction, error) {
|
|
typ, _ := m["type"].(string)
|
|
arg := func(k string) (DensityFunction, error) { return l.parseNode(m[k]) }
|
|
num := func(k string) float64 { f, _ := m[k].(float64); return f }
|
|
|
|
switch strings.TrimPrefix(typ, "minecraft:") {
|
|
case "add", "mul", "min", "max":
|
|
a, err := arg("argument1")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := arg("argument2")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch typ[10:] {
|
|
case "add":
|
|
return Add(a, b), nil
|
|
case "mul":
|
|
return Mul(a, b), nil
|
|
case "min":
|
|
return Min(a, b), nil
|
|
default:
|
|
return Max(a, b), nil
|
|
}
|
|
case "abs", "square", "cube", "half_negative", "quarter_negative", "invert", "squeeze":
|
|
a, err := arg("argument")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return unaryByName(strings.TrimPrefix(typ, "minecraft:"), a), nil
|
|
case "clamp":
|
|
a, err := arg("input")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Clamp(a, num("min"), num("max")), nil
|
|
case "range_choice":
|
|
in, err := arg("input")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
whenIn, err := arg("when_in_range")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
whenOut, err := arg("when_out_of_range")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return RangeChoice{in, num("min_inclusive"), num("max_exclusive"), whenIn, whenOut}, nil
|
|
case "y_clamped_gradient":
|
|
return YClampedGradient{num("from_y"), num("to_y"), num("from_value"), num("to_value")}, nil
|
|
case "noise":
|
|
n, err := l.noiseField(m["noise"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NoiseDF{Noise: n, XZScale: num("xz_scale"), YScale: num("y_scale")}, nil
|
|
case "shifted_noise":
|
|
sx, err := arg("shift_x")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sy, err := arg("shift_y")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sz, err := arg("shift_z")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := l.noiseField(m["noise"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ShiftedNoise{sx, sy, sz, num("xz_scale"), num("y_scale"), n}, nil
|
|
case "shift_a", "shift_b":
|
|
n, err := l.noiseField(m["argument"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if typ[10:] == "shift_a" {
|
|
return ShiftA{n}, nil
|
|
}
|
|
return ShiftB{n}, nil
|
|
case "old_blended_noise":
|
|
return l.rs.BlendedNoise(num("xz_scale"), num("y_scale"), num("xz_factor"), num("y_factor"), num("smear_scale_multiplier")), nil
|
|
case "weird_scaled_sampler":
|
|
in, err := arg("input")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := l.noiseField(m["noise"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rarity := SpaghettiRarity3D
|
|
if s, _ := m["rarity_value_mapper"].(string); s == "type_2" {
|
|
rarity = SpaghettiRarity2D
|
|
}
|
|
return WeirdScaledSampler{in, n, rarity}, nil
|
|
case "find_top_surface":
|
|
density, err := arg("density")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
upper, err := arg("upper_bound")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cellHeight := int(num("cell_height"))
|
|
if cellHeight <= 0 {
|
|
return nil, fmt.Errorf("find_top_surface: cell_height must be positive, got %d", cellHeight)
|
|
}
|
|
return FindTopSurface{
|
|
Density: density,
|
|
UpperBound: upper,
|
|
LowerBound: int(num("lower_bound")),
|
|
CellHeight: cellHeight,
|
|
}, nil
|
|
case "spline":
|
|
return l.parseSpline(m["spline"])
|
|
case "blend_alpha":
|
|
return Constant(1.0), nil // no blending: alpha = 1
|
|
case "blend_offset":
|
|
return Constant(0.0), nil // no blending: offset = 0
|
|
case "interpolated":
|
|
inner, err := arg("argument")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n := &Interpolated{Inner: inner, Index: len(l.interpolated)}
|
|
l.interpolated = append(l.interpolated, n)
|
|
return n, nil
|
|
case "blend_density", "flat_cache", "cache_2d", "cache_once", "cache_all_in_cell":
|
|
// 2D caches and blend wrappers are value-preserving for per-point
|
|
// evaluation (recomputed rather than cached); only the 3D interpolated
|
|
// marker changes the result and is handled above.
|
|
return arg("argument")
|
|
default:
|
|
return nil, fmt.Errorf("unsupported density-function type %q", typ)
|
|
}
|
|
}
|
|
|
|
func unaryByName(name string, a DensityFunction) DensityFunction {
|
|
switch name {
|
|
case "abs":
|
|
return Abs(a)
|
|
case "square":
|
|
return Square(a)
|
|
case "cube":
|
|
return Cube(a)
|
|
case "half_negative":
|
|
return HalfNegative(a)
|
|
case "quarter_negative":
|
|
return QuarterNegative(a)
|
|
case "invert":
|
|
return Invert(a)
|
|
default: // squeeze
|
|
return Squeeze(a)
|
|
}
|
|
}
|
|
|
|
// noiseField resolves a noise reference (a "minecraft:<name>" key, or an object
|
|
// with a "noise" key) to a seeded NormalNoise.
|
|
func (l *Loader) noiseField(v any) (*NormalNoise, error) {
|
|
var key string
|
|
switch t := v.(type) {
|
|
case string:
|
|
key = t
|
|
case map[string]any:
|
|
key, _ = t["noise"].(string)
|
|
}
|
|
if key == "" {
|
|
return nil, fmt.Errorf("missing noise reference")
|
|
}
|
|
var params struct {
|
|
FirstOctave int `json:"firstOctave"`
|
|
Amplitudes []float64 `json:"amplitudes"`
|
|
}
|
|
path := "data/noise/" + strings.TrimPrefix(key, "minecraft:") + ".json"
|
|
if err := l.readJSON(path, ¶ms); err != nil {
|
|
return nil, err
|
|
}
|
|
return l.rs.Noise(key, params.FirstOctave, params.Amplitudes), nil
|
|
}
|
|
|
|
func (l *Loader) parseSpline(v any) (DensityFunction, error) {
|
|
m, ok := v.(map[string]any)
|
|
if !ok {
|
|
return nil, fmt.Errorf("spline is not an object")
|
|
}
|
|
coord, err := l.parseNode(m["coordinate"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pts, _ := m["points"].([]any)
|
|
s := &CubicSpline{coordinate: coord}
|
|
for _, p := range pts {
|
|
pm := p.(map[string]any)
|
|
loc, _ := pm["location"].(float64)
|
|
der, _ := pm["derivative"].(float64)
|
|
val, err := l.parseSplineValue(pm["value"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.locations = append(s.locations, float32(loc))
|
|
s.derivatives = append(s.derivatives, float32(der))
|
|
s.values = append(s.values, val)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// parseSplineValue handles a spline point's value: a number (constant), a raw
|
|
// nested spline (object with "coordinate"), or a density-function node.
|
|
func (l *Loader) parseSplineValue(v any) (DensityFunction, error) {
|
|
if m, ok := v.(map[string]any); ok {
|
|
if _, hasCoord := m["coordinate"]; hasCoord {
|
|
return l.parseSpline(m)
|
|
}
|
|
}
|
|
return l.parseNode(v)
|
|
}
|