From ed045ee09d8ce7f33e2d54cdad3e1c86866663aa Mon Sep 17 00:00:00 2001 From: Master290 Date: Mon, 27 Jul 2026 01:44:45 +0300 Subject: [PATCH] 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. --- internal/worldgen/density.go | 6 ++ internal/worldgen/loader.go | 124 ++++++++++++++++++++++----- internal/worldgen/random.go | 22 +++++ internal/worldgen/randomstate.go | 18 +++- internal/worldgen/router_test.go | 99 ++++++++++++++++++++++ internal/worldgen/surface_level.go | 130 +++++++++++++++++++++++++++++ 6 files changed, 378 insertions(+), 21 deletions(-) create mode 100644 internal/worldgen/router_test.go create mode 100644 internal/worldgen/surface_level.go diff --git a/internal/worldgen/density.go b/internal/worldgen/density.go index 6b25b3f..2c980e9 100644 --- a/internal/worldgen/density.go +++ b/internal/worldgen/density.go @@ -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) diff --git a/internal/worldgen/loader.go b/internal/worldgen/loader.go index 0a6dfa6..600748f 100644 --- a/internal/worldgen/loader.go +++ b/internal/worldgen/loader.go @@ -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) } diff --git a/internal/worldgen/random.go b/internal/worldgen/random.go index d51a747..b1e040a 100644 --- a/internal/worldgen/random.go +++ b/internal/worldgen/random.go @@ -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++ { diff --git a/internal/worldgen/randomstate.go b/internal/worldgen/randomstate.go index da79f7c..239ba47 100644 --- a/internal/worldgen/randomstate.go +++ b/internal/worldgen/randomstate.go @@ -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 { diff --git a/internal/worldgen/router_test.go b/internal/worldgen/router_test.go new file mode 100644 index 0000000..64b17ed --- /dev/null +++ b/internal/worldgen/router_test.go @@ -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") + } +} diff --git a/internal/worldgen/surface_level.go b/internal/worldgen/surface_level.go new file mode 100644 index 0000000..cd2ed86 --- /dev/null +++ b/internal/worldgen/surface_level.go @@ -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() +}