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() +}