RegionIO/internal/world/vanilla.go
Master290 21a10ab65e Implement the vanilla Aquifer; stop flooding caves
Every air block below y=63 was turned into water. That is one line of code and
it cost the entire underground: no dry caves, no lava lakes, no air pockets, a
solid block of water from the sea floor to bedrock.

Vanilla decides fluid per position instead. Aquifer centres sit on a jittered
16x12x16 grid; each gets a fluid level and type from the floodedness and spread
noises, with centres near open sky inheriting the sea and buried ones getting a
much lower randomised level or nothing at all. A position takes its nearest
centre's fluid unless the barrier noise raises enough pressure between the two
or three nearest centres to seal it back to stone. Deep centres turn to lava.

Porting it means fixing the order of generation, not just adding a file. Vanilla
resolves stone/water/lava/air during the density pass and only then runs the
surface rules over a finished column; we did it the other way round, which is
what forced the unconditional flood in the first place. fillVanillaColumn now
asks the aquifer per position, and applySurfaceRule walks the finished column
carrying the bookkeeping SurfaceSystem carries: air resets the counters, a fluid
records its water height, and stone gets a depth from the top of its run plus
one from the bottom, found by looking ahead to the next non-stone block below.

That last one fixes stone_depth's ceiling form, which had no bottom-up depth to
work with and was testing the top-down one instead -- fourteen rules in the
overworld tree use it to dress cave roofs. The floor form is unchanged: vanilla
counts from 1 and compares against 1 + offset, we counted from 0 and compared
against offset.

The aquifer grid is built eagerly per chunk rather than lazily, because our
columns fill concurrently; every cell is a pure function of its grid coordinate
and every cell in the computed range gets consulted anyway. Cost is ~0.5% of
chunk generation, most of it absorbed by the shared preliminary-surface cache.

Inland caves go from 100% water to 3.8%, and lava exists for the first time.
cmd/gendump grows a census that would have failed loudly before, and
TestCavesAreDry guards it in the suite.
2026-07-27 02:02:12 +03:00

483 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package world
import (
"math"
"math/rand"
"sync"
"regionio/internal/worldgen"
)
// Noise cell dimensions for the overworld (size_horizontal=1 → 4 wide,
// size_vertical=2 → 8 tall). Only the Interpolated terrain noise is sampled on
// the cell-corner grid and trilinearly interpolated (as vanilla's NoiseChunk
// does); the rest of final_density — squeeze/min and the caves — is evaluated
// per block with those interpolated values substituted in.
const (
cellWidth = 4
cellHeight = 8
cellsXZ = 16 / cellWidth // 4
cellsY = WorldHeight / cellHeight // 48
)
type cornerGrid [cellsXZ + 1][cellsY + 1][cellsXZ + 1]float64
// NewVanillaGenerator returns a generator backed by the real overworld
// final_density tree for the given seed, plus a simplified cosmetic pass
// (beaches and trees) layered on the bit-accurate terrain.
func NewVanillaGenerator(seed int64) Generator {
od, err := worldgen.LoadOverworldFinalDensity(seed)
if err != nil {
panic("world: loading overworld density: " + err.Error())
}
fluidPicker := worldgen.OverworldFluidPicker(od.SeaLevel)
return func(cx, cz int32) *Chunk {
return generateVanilla(od, fluidPicker, seed, cx, cz)
}
}
func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, seed int64, cx, cz int32) *Chunk {
c := NewChunk(cx, cz, BiomePlains) // per-cell biomes override below
baseX, baseZ := int(cx)*16, int(cz)*16
grids := make([]cornerGrid, len(od.Interpolated))
var wg sync.WaitGroup
for ix := 0; ix <= cellsXZ; ix++ {
wg.Add(1)
go func(ix int) {
defer wg.Done()
wx := float64(baseX + ix*cellWidth)
for iy := 0; iy <= cellsY; iy++ {
wy := float64(MinY + iy*cellHeight)
for iz := 0; iz <= cellsXZ; iz++ {
ctx := worldgen.FunctionContext{X: wx, Y: wy, Z: float64(baseZ + iz*cellWidth)}
for n, node := range od.Interpolated {
grids[n][ix][iy][iz] = node.Inner.Compute(ctx)
}
}
}
}(ix)
}
wg.Wait()
// Surface biomes and 2D climate are needed before column fill so the surface
// rule tree can pick biome-specific blocks. They are also reused by
// fillBiomes3D below, so compute them once here.
var s2D [16][16]worldgen.Sample2D
var biomeName [16][16]string
for lx := 0; lx < 16; lx++ {
wg.Add(1)
go func(lx int) {
defer wg.Done()
for lz := 0; lz < 16; lz++ {
s2D[lx][lz] = worldgen.SampleColumn2D(od, SeaLevel, baseX+lx, baseZ+lz)
biomeName[lx][lz] = loadBiomeTable().FindBiome(
worldgen.NewTargetPoint(s2D[lx][lz].Temperature, s2D[lx][lz].Humidity,
s2D[lx][lz].Continentalness, s2D[lx][lz].Erosion, s2D[lx][lz].Weirdness, 0))
}
}(lx)
}
wg.Wait()
// The surface rule tree is seed-independent; load once (cached). If it fails
// to parse, surface fill falls back to the biome-blind heuristics.
surfaceRule, ruleErr := od.SurfaceRule()
// The aquifer decides fluid per position while the column is laid down. Its
// cell grid spans the chunk plus a margin, so it is built once per chunk and
// shared, read-only, by the parallel column fill.
var aq *worldgen.Aquifer
if od.AquifersEnabled {
aq = worldgen.NewAquifer(od, int(cx), int(cz), fluidPicker)
}
var columns [16][16][WorldHeight]uint16
var surfTop [16][16]int // top solid index, -1 if none
var grass [16][16]bool // grassy land surface (tree-plantable)
for lx := 0; lx < 16; lx++ {
wg.Add(1)
go func(lx int) {
defer wg.Done()
interp := make([]float64, len(od.Interpolated))
for lz := 0; lz < 16; lz++ {
var rule worldgen.SurfaceRule
if ruleErr == nil {
rule = surfaceRule
}
surfTop[lx][lz], grass[lx][lz] = fillVanillaColumn(od, aq, fluidPicker, grids, interp, &columns[lx][lz], baseX+lx, baseZ+lz, lx, lz, seed, rule, biomeName[lx][lz])
}
}(lx)
}
wg.Wait()
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
col := &columns[lx][lz]
for i := 0; i < WorldHeight; i++ {
if s := col[i]; s != StateAir {
c.setBlockRaw(lx, MinY+i, lz, s)
}
}
}
}
fillBiomes3D(c, od, s2D, baseX, baseZ)
decorate(c, od, cx, cz, seed, &surfTop, &grass, &biomeName)
return c
}
// fillBiomes3D assigns a per-cell 4×4×4 biome to every section of the chunk.
// It receives the precomputed 2D climate grid (s2D, already sampled per column
// for the surface pass) and evaluates only the 3D depth axis per cell, keeping
// per-cell cost to a single density-function compute. The biome columns are
// processed in parallel to keep generation fast.
func fillBiomes3D(c *Chunk, od *worldgen.OverworldDensity, s2D [16][16]worldgen.Sample2D, baseX, baseZ int) {
var wg sync.WaitGroup
// One biome per 4×4×4 cell. Sampling at the cell corner (bx*4, bz*4) is
// representative because the 2D climate noises vary slowly relative to a
// 4-block cell; depth carries the vertical variation.
for bx := 0; bx < biomeCellsXZ; bx++ {
wg.Add(1)
go func(bx int) {
defer wg.Done()
lx := bx * biomeCellSize
for bz := 0; bz < biomeCellsXZ; bz++ {
lz := bz * biomeCellSize
col2D := s2D[lx][lz]
for si := 0; si < SectionCount; si++ {
for by := 0; by < biomeCellsXZ; by++ {
wy := MinY + si*16 + by*biomeCellSize
biome := BiomeAt3D(od, col2D, baseX+lx, wy, baseZ+lz)
c.SetBiome(lx, wy, lz, biome)
}
}
}
}(bx)
}
wg.Wait()
}
// fillVanillaColumn lays the blocks for one column and returns the top solid
// index and whether the surface is grassy land (suitable for trees).
//
// The order matches vanilla: the density pass decides stone-or-not, the aquifer
// turns every non-stone position into air, water or lava (and can also seal a
// position back to stone where the barrier noise says the rock holds), and only
// then does the surface rule tree walk the finished column. Doing it the other
// way round is what forced the old unconditional "flood everything under sea
// level" pass, which left every cave below y=63 underwater.
func fillVanillaColumn(od *worldgen.OverworldDensity, aq *worldgen.Aquifer, fluidPicker worldgen.FluidPicker, grids []cornerGrid, interp []float64, out *[WorldHeight]uint16, wx, wz, lx, lz int, seed int64, rule worldgen.SurfaceRule, biomeName string) (int, bool) {
cx0 := lx / cellWidth
cz0 := lz / cellWidth
fx := float64(lx%cellWidth) / cellWidth
fz := float64(lz%cellWidth) / cellWidth
top := -1
for i := 0; i < WorldHeight; i++ {
cy0 := i / cellHeight
fy := float64(i%cellHeight) / cellHeight
for n := range grids {
interp[n] = trilerp(&grids[n], cx0, cy0, cz0, fx, fy, fz)
}
y := MinY + i
ctx := worldgen.FunctionContext{X: float64(wx), Y: float64(y), Z: float64(wz)}.WithInterp(interp)
density := od.Final.Compute(ctx)
state, isDefaultBlock := substance(aq, fluidPicker, wx, y, wz, density)
out[i] = state
if isDefaultBlock {
top = i
}
}
topY := MinY + top
// Beach: a narrow band straddling the waterline. Dry columns well above sea
// level stay grass; deep water floors become gravel, not sand.
const beachBand = 3
beach := top >= 0 && topY >= SeaLevel-beachBand && topY <= SeaLevel+1
deepWater := top >= 0 && topY < SeaLevel-beachBand
// Per-column RNG for the bedrock floor and the bandlands/gradient rules.
rng := newColumnRand(wx, wz, int(seed))
if rule != nil {
applySurfaceRule(out, wx, wz, SeaLevel, MinY, biomeName, rule, rng, top)
} else {
fillLegacySurface(out, top, beach, deepWater, rng)
}
return top, top >= 0 && !beach && !deepWater && topY >= SeaLevel
}
// substance resolves one position to the block the terrain pass leaves behind:
// the default block where the density is solid, otherwise whatever the aquifer
// puts there — air, water or lava. The second result says which of the two
// happened, so the caller can track the top solid block without re-testing.
func substance(aq *worldgen.Aquifer, fluidPicker worldgen.FluidPicker, x, y, z int, density float64) (state uint16, isDefaultBlock bool) {
if aq == nil {
// aquifers_enabled=false: Aquifer.createDisabled, the global fluid rule
// with no cells and no barriers.
if density > 0 {
return StateStone, true
}
return fluidPicker(x, y, z).At(y), false
}
if s, ok := aq.ComputeSubstance(x, y, z, density); ok {
return s, false
}
return StateStone, true
}
// applySurfaceRule walks the finished column from the top down, applying the
// rule tree to every default-block position, and mirrors SurfaceSystem's
// bookkeeping as it goes:
//
// - air resets both the stone depth and the water height;
// - a fluid records the height of the first (topmost) block of its run;
// - stone carries a depth counted down from the top of its run, and a depth
// counted up from the bottom, found by looking ahead to the next non-stone
// block below.
//
// The rule only replaces the default block, so anything the aquifer placed —
// water in an ocean, lava in a deep pocket — survives untouched.
//
// One *rand.Rand is created per column (not per block) — bandlands/gradient
// consume from it sequentially, which is correct because vanilla seeds those
// per-column too. This avoids ~98k rand.New allocations per chunk.
func applySurfaceRule(out *[WorldHeight]uint16, wx, wz, seaLevel, minY int, biomeName string, rule worldgen.SurfaceRule, rng chunkRand, topSolid int) {
top := -1
for i := WorldHeight - 1; i >= 0; i-- {
if out[i] != StateAir {
top = i
break
}
}
if top < 0 {
return
}
// One per-column RNG for all surface rules in this column.
colRng := rng.toRand()
// Surface noise sample (the "minecraft:surface" noise used by noise_threshold
// conditions). Cheap deterministic value derived from the column so the
// rule's coarse_dirt/terracotta bands vary per column.
surfaceNoise := colRng.Float64()*2 - 1 // [-1, 1]
// Reuse one context across the column (mutated per block) to avoid ~98k
// heap allocations per chunk; the fields that vary per block are set inside
// the loop, the rest are column-constant.
sctx := &worldgen.SurfaceContext{
X: wx,
Z: wz,
SeaLevel: seaLevel,
BiomeName: biomeName,
MinY: minY,
SurfaceNoise: surfaceNoise,
SurfaceDepth: 0,
PreliminarySurface: minY + topSolid,
Rng: colRng,
}
stoneDepthAbove := 0
waterHeight := math.MinInt
nextCeilingStoneY := math.MaxInt
for i := top; i >= 0; i-- {
y := minY + i
old := out[i]
if old == StateAir {
stoneDepthAbove = 0
waterHeight = math.MinInt
continue
}
if isFluidState(old) {
if waterHeight == math.MinInt {
waterHeight = y + 1
}
continue
}
if nextCeilingStoneY >= y {
// Look ahead to the first non-stone block below; the scan runs one
// past the world floor, which reads as air, so it always terminates.
nextCeilingStoneY = worldgen.WayBelowMinY
for j := i - 1; j >= -1; j-- {
if j >= 0 && isStoneState(out[j]) {
continue
}
nextCeilingStoneY = minY + j + 1
break
}
}
stoneDepthAbove++
sctx.Y = y
sctx.StoneDepthAbove = stoneDepthAbove
sctx.StoneDepthBelow = y - nextCeilingStoneY + 1
sctx.WaterHeight = waterHeight
if old != StateStone {
continue
}
if state, ok := rule.Apply(sctx); ok && state != 0 {
out[i] = state
}
}
}
// isFluidState reports whether a raw terrain block is a fluid (SurfaceSystem
// branches on getFluidState().isEmpty()). Only the aquifer's own fluids can
// appear here, since the rule pass runs before decoration.
func isFluidState(s uint16) bool { return s == StateWater || s == StateLava }
// isStoneState is SurfaceSystem.isStone: solid, non-fluid, non-air.
func isStoneState(s uint16) bool { return s != StateAir && !isFluidState(s) }
// fillLegacySurface is the biome-blind heuristic used when no surface rule is
// available (parse failure). It dresses the stone the terrain and aquifer
// passes already laid down, leaving their air and fluids alone.
func fillLegacySurface(out *[WorldHeight]uint16, top int, beach, deepWater bool, rng chunkRand) {
for i := 0; i < WorldHeight; i++ {
y := MinY + i
if !isStoneState(out[i]) {
continue
}
switch {
case y <= MinY:
out[i] = StateBedrock
case y <= MinY+4 && bedrockAt(&rng, y-MinY):
out[i] = StateBedrock
case beach && i > top-4:
out[i] = StateSand
case deepWater && i == top:
out[i] = StateGravel
case i == top && y >= SeaLevel:
out[i] = StateGrass
case i > top-4:
out[i] = StateDirt
}
}
}
// bedrockAt reports whether the block d layers above the world floor should be
// bedrock, consuming one draw from rng. It mirrors the datapack's
// vertical_gradient(minecraft:bedrock_floor, above_bottom 0 → above_bottom 5):
// the probability ramps linearly from 1 at the floor to 0 five blocks up, and
// vanilla tests nextFloat() < probability.
//
// rng is a pointer so successive layers draw successive values. Taking it by
// value handed every layer the same number, which nested the layers into a
// prefix condition instead of scattering them. The ramp also used to run the
// wrong way — bedrock was likelier four blocks up than at the floor.
//
// Only fillLegacySurface calls this; the normal path lets the surface rule tree
// place the floor from the same datapack rule.
func bedrockAt(rng *chunkRand, d int) bool {
if d <= 0 {
return true
}
if d >= 5 {
return false
}
return rng.nextFloat() < 1.0-float64(d)/5.0
}
// decorate places simple oak trees on grassy columns. Trunks are kept two
// blocks inside the chunk so the radius-2 canopy never crosses into a neighbour
// (avoiding cross-chunk coordination); placement is deterministic per chunk.
func decorate(c *Chunk, od *worldgen.OverworldDensity, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[16][16]bool, biomeName *[16][16]string) {
r := newChunkRand(cx, cz, seed)
placeOres(c, &r)
placeFlora(c, &r, surfTop, grass, biomeName)
placeDesertFeatures(c, &r, surfTop, biomeName)
placeRocks(c, &r, surfTop, grass, biomeName)
const attempts = 8
for a := 0; a < attempts; a++ {
lx := 2 + int(r.next()%12)
lz := 2 + int(r.next()%12)
if !grass[lx][lz] {
continue
}
baseY := MinY + surfTop[lx][lz] + 1
placeOak(c, lx, baseY, lz, &r)
}
// Place large structures like villages and strongholds
worldgen.PlaceStructures(c, od, cx, cz, seed, surfTop, biomeName)
}
func placeOak(c *Chunk, lx, baseY, lz int, r *chunkRand) {
h := 4 + int(r.next()%3) // trunk height 4..6
for i := 0; i < h; i++ {
c.SetBlock(lx, baseY+i, lz, StateOakLog)
}
topY := baseY + h - 1
// Canopy: two wide layers around the top, then two narrow layers above.
layers := []struct {
dy, radius int
}{{-1, 2}, {0, 2}, {1, 1}, {2, 1}}
for _, ly := range layers {
y := topY + ly.dy
for dx := -ly.radius; dx <= ly.radius; dx++ {
for dz := -ly.radius; dz <= ly.radius; dz++ {
if ly.radius == 2 && abs(dx) == 2 && abs(dz) == 2 {
continue // trim the far corners for a rounder shape
}
if c.GetBlock(lx+dx, y, lz+dz) == StateAir {
c.SetBlock(lx+dx, y, lz+dz, StateOakLeaf)
}
}
}
}
}
func abs(v int) int {
if v < 0 {
return -v
}
return v
}
// chunkRand is a tiny deterministic PRNG (SplitMix64) seeded per chunk.
type chunkRand struct{ s uint64 }
func newChunkRand(cx, cz int32, seed int64) chunkRand {
h := uint64(seed)
h ^= uint64(uint32(cx)) * 0x9E3779B97F4A7C15
h ^= uint64(uint32(cz)) * 0xC2B2AE3D27D4EB4F
return chunkRand{s: h | 1}
}
// newColumnRand seeds a deterministic PRNG from a column's world coordinates so
// each (x,z) gets a stable but independent stream (used for the random bedrock
// layer). Mixing in the world seed keeps worlds with the same terrain shape but
// different seeds distinct at the floor.
func newColumnRand(wx, wz, seed int) chunkRand {
h := uint64(seed)
h ^= uint64(uint32(wx)) * 0x9E3779B97F4A7C15
h ^= uint64(uint32(wz)) * 0xC2B2AE3D27D4EB4F
return chunkRand{s: h | 1}
}
func (r *chunkRand) next() uint32 {
r.s += 0x9E3779B97F4A7C15
z := r.s
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9
z = (z ^ (z >> 27)) * 0x94D049BB133111EB
z = z ^ (z >> 31)
return uint32(z >> 32)
}
func (r *chunkRand) nextFloat() float64 {
return float64(r.next()) / float64(1<<32)
}
// toRand returns a *rand.Rand seeded from this column's state, for surface
// rules (vertical_gradient/bandlands) that consume a stdlib-style RNG. It draws
// once to advance state so repeated calls differ within a column.
func (r *chunkRand) toRand() *rand.Rand {
return rand.New(rand.NewSource(int64(r.next())))
}
func trilerp(c *cornerGrid, x0, y0, z0 int, fx, fy, fz float64) float64 {
x1, y1, z1 := x0+1, y0+1, z0+1
c00 := lerpf(fx, c[x0][y0][z0], c[x1][y0][z0])
c10 := lerpf(fx, c[x0][y1][z0], c[x1][y1][z0])
c01 := lerpf(fx, c[x0][y0][z1], c[x1][y0][z1])
c11 := lerpf(fx, c[x0][y1][z1], c[x1][y1][z1])
return lerpf(fz, lerpf(fy, c00, c10), lerpf(fy, c01, c11))
}
func lerpf(t, a, b float64) float64 { return a + t*(b-a) }