Parse aquifer and ore-vein noise routers

The overworld noise_router ships fourteen keys; we read six. The eight left on
the floor are exactly the ones the aquifer, the ore veins and the preliminary
surface estimate need, so every one of those subsystems has been impossible to
write.

Wire the rest of the router into OverworldDensity: barrier,
fluid_level_floodedness, fluid_level_spread and lava for the aquifer,
vein_toggle/vein_ridged/vein_gap for the veins, and preliminary_surface_level
for both. Two node types were missing and are added with them --
minecraft:invert (the reciprocal, not negation: Mapped.Type ordinal 5 is
1.0/input) and minecraft:find_top_surface, which walks down from an upper bound
in cell_height steps looking for positive density.

PreliminarySurfaceLevelAt wraps that node the way NoiseChunk does: quart-align
the column, then memoise. The cache is per generator rather than per chunk
because the aquifer samples columns up to three chunks away, so neighbours
overlap heavily -- with a shared cache a chunk costs a few dozen evaluations
instead of a few thousand.

Also lifts sea_level, min_y, height and the aquifers/ore-veins flags out of the
settings file, and adds PositionalRandomFactory.At for the aquifer cell centres
(Mth.getSeed hashed into the low half of the factory seed).

No generator output changes yet: nothing reads the new keys.
This commit is contained in:
Master290 2026-07-27 01:44:45 +03:00
parent 90e9380ae7
commit ed045ee09d
6 changed files with 378 additions and 21 deletions

View file

@ -126,6 +126,12 @@ func QuarterNegative(a DensityFunction) DensityFunction {
return x * 0.25
}}
}
// Invert is the reciprocal transform (DensityFunctions.Mapped.Type.INVERT):
// 1/x, not negation. The overworld's preliminary_surface_level upper bound is
// the only place it appears.
func Invert(a DensityFunction) DensityFunction {
return unaryOp{a, func(x float64) float64 { return 1.0 / x }}
}
func Squeeze(a DensityFunction) DensityFunction {
return unaryOp{a, func(x float64) float64 {
d := clamp(x, -1, 1)

View file

@ -29,6 +29,30 @@ type OverworldDensity struct {
// 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
prelim *levelCache
}
// SurfaceRule returns the overworld surface rule tree, loading it on first use.
@ -44,6 +68,13 @@ 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
@ -56,35 +87,67 @@ func LoadOverworldFinalDensity(seed int64) (*OverworldDensity, error) {
if err != nil {
return nil, err
}
od := &OverworldDensity{Final: final, Interpolated: l.interpolated}
// Parse the climate router keys used by the biome finder. Each key resolves
// to a density function via the same parseNode/loadRef machinery as
// final_density. A missing key is not fatal — the climate axis stays nil and
// the sampler treats it as a constant zero — but a parse error is.
climateKeys := map[string]*DensityFunction{
"temperature": &od.Temperature,
"vegetation": &od.Humidity,
"continents": &od.Continentalness,
"erosion": &od.Erosion,
"ridges": &od.Weirdness,
"depth": &od.Depth,
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(),
}
for key, dst := range climateKeys {
raw, ok := settings.NoiseRouter[key]
// 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 climate key %q: %w", key, err)
return nil, fmt.Errorf("parse router key %q: %w", rk.key, err)
}
df, err := l.parseNode(cn)
if err != nil {
return nil, fmt.Errorf("climate key %q: %w", key, err)
return nil, fmt.Errorf("router key %q: %w", rk.key, err)
}
*dst = df
*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
return od, nil
}
@ -155,12 +218,12 @@ func (l *Loader) parseObject(m map[string]any) (DensityFunction, error) {
default:
return Max(a, b), nil
}
case "abs", "square", "cube", "half_negative", "quarter_negative", "squeeze":
case "abs", "square", "cube", "half_negative", "quarter_negative", "invert", "squeeze":
a, err := arg("argument")
if err != nil {
return nil, err
}
return unaryByName(typ[10:], a), nil
return unaryByName(strings.TrimPrefix(typ, "minecraft:"), a), nil
case "clamp":
a, err := arg("input")
if err != nil {
@ -232,6 +295,25 @@ func (l *Loader) parseObject(m map[string]any) (DensityFunction, error) {
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":
@ -268,6 +350,8 @@ func unaryByName(name string, a DensityFunction) DensityFunction {
return HalfNegative(a)
case "quarter_negative":
return QuarterNegative(a)
case "invert":
return Invert(a)
default: // squeeze
return Squeeze(a)
}

View file

@ -61,6 +61,17 @@ type RandomSource interface {
type PositionalRandomFactory interface {
// FromHashOf seeds a child source from the MD5 hash of name.
FromHashOf(name string) RandomSource
// At seeds a child source from a block position, mirroring
// PositionalRandomFactory.at (used by the aquifer and ore veins).
At(x, y, z int) RandomSource
}
// positionSeed is Mth.getSeed: a scrambled hash of a block position, used to
// seed positional random factories.
func positionSeed(x, y, z int) int64 {
l := int64(int32(x)*3129871) ^ int64(z)*116129781 ^ int64(y)
l = l*l*42317861 + l*11
return l >> 16
}
// --- Xoroshiro128++ ---
@ -142,6 +153,12 @@ func (f *xoroshiroPositional) FromHashOf(name string) RandomSource {
return newXoroshiroFrom(lo^f.seedLo, hi^f.seedHi)
}
// At mirrors XoroshiroPositionalRandomFactory.at: the position hash XORed into
// the low half of the factory seed, the high half kept as is.
func (f *xoroshiroPositional) At(x, y, z int) RandomSource {
return newXoroshiroFrom(uint64(positionSeed(x, y, z))^f.seedLo, f.seedHi)
}
// --- Legacy LCG (java.util.Random) ---
const (
@ -211,6 +228,11 @@ func (f *legacyPositional) FromHashOf(name string) RandomSource {
return NewLegacy(int64(int32(javaStringHashCode(name))) ^ int64(f.seed))
}
// At mirrors LegacyPositionalRandomFactory.at.
func (f *legacyPositional) At(x, y, z int) RandomSource {
return NewLegacy(positionSeed(x, y, z) ^ int64(f.seed))
}
func javaStringHashCode(s string) int32 {
var h int32
for i := 0; i < len(s); i++ {

View file

@ -7,16 +7,32 @@ package worldgen
type RandomState struct {
factory PositionalRandomFactory
noises map[string]*NormalNoise
// aquifer and ore are the positional factories vanilla derives up front for
// the aquifer cell centres and the ore-vein placement, each a forked
// positional factory seeded from a named hash of the root factory.
aquifer PositionalRandomFactory
ore PositionalRandomFactory
}
// NewRandomState builds the seeding context for the given world seed.
func NewRandomState(seed int64) *RandomState {
root := NewXoroshiro(seed).ForkPositional()
return &RandomState{
factory: NewXoroshiro(seed).ForkPositional(),
factory: root,
noises: make(map[string]*NormalNoise),
aquifer: root.FromHashOf("minecraft:aquifer").ForkPositional(),
ore: root.FromHashOf("minecraft:ore").ForkPositional(),
}
}
// AquiferRandom returns the positional factory the aquifer uses to place its
// cell centres (RandomState.aquiferRandom).
func (rs *RandomState) AquiferRandom() PositionalRandomFactory { return rs.aquifer }
// OreRandom returns the positional factory the ore-vein placement uses
// (RandomState.oreRandom).
func (rs *RandomState) OreRandom() PositionalRandomFactory { return rs.ore }
// Noise returns the NormalNoise for the named noise parameters, seeded as
// NormalNoise.create(factory.fromHashOf(name), params) and cached.
func (rs *RandomState) Noise(name string, firstOctave int, amplitudes []float64) *NormalNoise {

View file

@ -0,0 +1,99 @@
package worldgen
import "testing"
// TestRouterKeysParse checks that every noise_router key the generator reads is
// wired up. A missing key silently degrades a whole subsystem — an absent
// barrier noise, for example, would make the aquifer place fluid with no
// pressure check at all — so the fields are asserted individually.
func TestRouterKeysParse(t *testing.T) {
od, err := LoadOverworldFinalDensity(12345)
if err != nil {
t.Fatalf("load overworld density: %v", err)
}
for name, df := range map[string]DensityFunction{
"final": od.Final,
"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": od.PreliminarySurfaceLevel,
} {
if df == nil {
t.Errorf("router key %q did not parse", name)
}
}
if od.SeaLevel != 63 || od.MinY != -64 || od.Height != 384 {
t.Errorf("settings: sea=%d minY=%d height=%d, want 63/-64/384", od.SeaLevel, od.MinY, od.Height)
}
if !od.AquifersEnabled || !od.OreVeinsEnabled {
t.Errorf("aquifers=%v oreVeins=%v, want both enabled", od.AquifersEnabled, od.OreVeinsEnabled)
}
if od.AquiferRandom == nil {
t.Fatal("aquifer positional random factory is nil")
}
}
// TestPreliminarySurfaceLevel checks that find_top_surface lands in the range a
// terrain surface can occupy, is quart-aligned (all four columns of a quart
// cell share a value), and is a multiple of the 8-block cell height.
func TestPreliminarySurfaceLevel(t *testing.T) {
od, err := LoadOverworldFinalDensity(12345)
if err != nil {
t.Fatalf("load overworld density: %v", err)
}
seen := make(map[int]int)
for x := -512; x < 512; x += 16 {
for z := -512; z < 512; z += 16 {
y := od.PreliminarySurfaceLevelAt(x, z)
if y < od.MinY || y > od.MinY+od.Height {
t.Fatalf("preliminary surface at (%d,%d) = %d, out of world", x, z, y)
}
if y%8 != 0 && y != od.MinY {
t.Fatalf("preliminary surface at (%d,%d) = %d, not a multiple of cell height 8", x, z, y)
}
seen[y]++
}
}
if len(seen) < 4 {
t.Errorf("preliminary surface took only %d distinct values over 64x64 columns; expected varied terrain", len(seen))
}
// Quart alignment: (x|3, z|3) must resolve to the same column as (x, z).
for _, p := range [][2]int{{0, 0}, {17, -35}, {-101, 250}} {
a := od.PreliminarySurfaceLevelAt(p[0], p[1])
b := od.PreliminarySurfaceLevelAt(p[0]|3, p[1]|3)
if a != b {
t.Errorf("preliminary surface not quart-aligned at (%d,%d): %d vs %d", p[0], p[1], a, b)
}
}
}
// TestPositionalRandomAt pins the positional factory's position seeding: a
// changed hash would silently move every aquifer cell centre.
func TestPositionalRandomAt(t *testing.T) {
if got := positionSeed(0, 0, 0); got != 0 {
t.Errorf("positionSeed(0,0,0) = %d, want 0", got)
}
rs := NewRandomState(12345)
f := rs.AquiferRandom()
a := f.At(1, 2, 3)
b := f.At(1, 2, 3)
if a.NextLong() != b.NextLong() {
t.Error("At is not deterministic for the same position")
}
if f.At(1, 2, 3).NextLong() == f.At(1, 2, 4).NextLong() {
t.Error("At returns the same stream for different positions")
}
if rs.OreRandom().At(1, 2, 3).NextLong() == f.At(1, 2, 3).NextLong() {
t.Error("ore and aquifer factories share a stream")
}
}

View file

@ -0,0 +1,130 @@
package worldgen
import (
"math"
"sync"
)
// surface_level.go implements the preliminary surface level: the cheap estimate
// of where the terrain surface will end up, computed without the 3D terrain
// noise. Vanilla exposes it as the noise_router key "preliminary_surface_level"
// (a minecraft:find_top_surface node) and reads it through
// NoiseChunk.preliminarySurfaceLevel, which quart-aligns the column and
// memoises the result.
//
// Two consumers need it: the aquifer, which samples 13 columns around every
// aquifer cell centre to decide whether the cell sits under the open sky, and
// the surface rule condition above_preliminary_surface.
// FindTopSurface is the minecraft:find_top_surface node: walk down from
// upperBound in cellHeight steps and return the first Y where density is
// positive, or lowerBound when there is none.
//
// The inner samples use a fresh single-point context (as vanilla's
// SinglePointContext does), so cell-interpolated values from an enclosing
// generation pass never leak into them.
type FindTopSurface struct {
Density DensityFunction
UpperBound DensityFunction
LowerBound int
CellHeight int
}
func (f FindTopSurface) Compute(c FunctionContext) float64 {
topY := int(math.Floor(f.UpperBound.Compute(c)/float64(f.CellHeight))) * f.CellHeight
if topY <= f.LowerBound {
return float64(f.LowerBound)
}
for blockY := topY; blockY >= f.LowerBound; blockY -= f.CellHeight {
p := FunctionContext{X: c.X, Y: float64(blockY), Z: c.Z}
if f.Density.Compute(p) > 0 {
return float64(blockY)
}
}
return float64(f.LowerBound)
}
// PreliminarySurfaceLevelAt returns the preliminary surface level for the
// quart-aligned column containing (x, z), mirroring
// NoiseChunk.preliminarySurfaceLevel.
//
// The value is a pure function of position, so it is memoised for the whole
// generator rather than per chunk: the aquifer samples columns up to three
// chunks away, so neighbouring chunks overlap heavily and a shared cache turns
// a few thousand evaluations per chunk into a few dozen.
func (od *OverworldDensity) PreliminarySurfaceLevelAt(x, z int) int {
qx := (x >> 2) << 2
qz := (z >> 2) << 2
if od.PreliminarySurfaceLevel == nil {
return od.MinY
}
key := uint64(uint32(qx))<<32 | uint64(uint32(qz))
if v, ok := od.prelim.get(key); ok {
return v
}
v := int(math.Floor(od.PreliminarySurfaceLevel.Compute(FunctionContext{X: float64(qx), Y: 0, Z: float64(qz)})))
od.prelim.put(key, v)
return v
}
// MaxPreliminarySurfaceLevel returns the highest preliminary surface level over
// the rectangle [x0,x1]×[z0,z1], sampled every 4 blocks
// (NoiseChunk.maxPreliminarySurfaceLevel).
func (od *OverworldDensity) MaxPreliminarySurfaceLevel(x0, z0, x1, z1 int) int {
best := math.MinInt32
for z := z0; z <= z1; z += 4 {
for x := x0; x <= x1; x += 4 {
if v := od.PreliminarySurfaceLevelAt(x, z); v > best {
best = v
}
}
}
return best
}
// levelCache is a sharded map from packed quart column to surface level.
// Sharding keeps the lock uncontended while chunk columns are filled in
// parallel; each shard drops everything once it grows past a bound, which is
// safe because every entry is recomputable.
type levelCache struct {
shards [16]levelShard
}
const levelShardCap = 1 << 16
type levelShard struct {
mu sync.RWMutex
m map[uint64]int
}
func newLevelCache() *levelCache {
c := &levelCache{}
for i := range c.shards {
c.shards[i].m = make(map[uint64]int)
}
return c
}
// shardOf mixes the packed column so neighbouring columns spread across shards.
func (c *levelCache) shardOf(key uint64) *levelShard {
h := key * 0x9E3779B97F4A7C15
return &c.shards[(h>>60)&15]
}
func (c *levelCache) get(key uint64) (int, bool) {
s := c.shardOf(key)
s.mu.RLock()
v, ok := s.m[key]
s.mu.RUnlock()
return v, ok
}
func (c *levelCache) put(key uint64, v int) {
s := c.shardOf(key)
s.mu.Lock()
if len(s.m) >= levelShardCap {
s.m = make(map[uint64]int)
}
s.m[key] = v
s.mu.Unlock()
}