RegionIO/cmd/gendump/main.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

348 lines
10 KiB
Go

package main
import (
"fmt"
"sort"
"regionio/internal/world"
"regionio/internal/worldgen"
)
// gendump prints diagnostics about the current generator output so we can see
// concretely what terrain/biomes/surface look like without a client.
func main() {
const seed = 12345
od, err := worldgen.LoadOverworldFinalDensity(seed)
if err != nil {
panic(err)
}
gen := world.NewVanillaGenerator(seed)
// 1) Biome distribution over a 16x16 chunk area (surface biome per column).
biomeCounts := map[string]int{}
surfaceBlockCounts := map[uint16]int{}
var minH, maxH = 1 << 30, -(1 << 30)
sumH := 0
nH := 0
for cx := int32(-64); cx < 64; cx += 8 {
for cz := int32(-64); cz < 64; cz += 8 {
c := gen(cx, cz)
for lx := 0; lx < 16; lx += 4 {
for lz := 0; lz < 16; lz += 4 {
// surface biome
s2 := worldgen.SampleColumn2D(od, world.SeaLevel, int(cx)*16+lx, int(cz)*16+lz)
name := loadName(od, s2, int(cx)*16+lx, int(cz)*16+lz)
biomeCounts[name]++
// top solid block + height
for wy := world.MinY + world.WorldHeight - 1; wy >= world.MinY; wy-- {
b := c.GetBlock(lx, wy, lz)
if b != world.StateAir && b != world.StateWater {
surfaceBlockCounts[b]++
if wy < minH {
minH = wy
}
if wy > maxH {
maxH = wy
}
sumH += wy
nH++
break
}
}
}
}
}
}
// Climate axis ranges across the sampled area.
type ax struct{ lo, hi, sum float64; n int }
axes := map[string]*ax{"temp": {lo: 1e9, hi: -1e9}, "humid": {lo: 1e9, hi: -1e9}, "cont": {lo: 1e9, hi: -1e9}, "ero": {lo: 1e9, hi: -1e9}, "weird": {lo: 1e9, hi: -1e9}}
upd := func(name string, v float64) { a := axes[name]; if v < a.lo { a.lo = v }; if v > a.hi { a.hi = v }; a.sum += v; a.n++ }
for cx := int32(-64); cx < 64; cx += 2 {
for cz := int32(-64); cz < 64; cz += 2 {
for lx := 0; lx < 16; lx += 8 {
for lz := 0; lz < 16; lz += 8 {
s2 := worldgen.SampleColumn2D(od, world.SeaLevel, int(cx)*16+lx, int(cz)*16+lz)
upd("temp", s2.Temperature)
upd("humid", s2.Humidity)
upd("cont", s2.Continentalness)
upd("ero", s2.Erosion)
upd("weird", s2.Weirdness)
}
}
}
}
fmt.Println("=== Climate axis ranges (should span roughly [-1,1]) ===")
for _, k := range []string{"temp", "humid", "cont", "ero", "weird"} {
a := axes[k]
fmt.Printf(" %-6s min=%+.3f max=%+.3f avg=%+.3f\n", k, a.lo, a.hi, a.sum/float64(a.n))
}
fmt.Println("\n=== Surface biome distribution (seed 12345, 256 chunks sampled) ===")
printSorted(biomeCounts)
fmt.Printf("\n=== Surface height: min=%d max=%d avg=%.1f (sea=%d) ===\n", minH, maxH, float64(sumH)/float64(nH), world.SeaLevel)
fmt.Println("\n=== Top surface block IDs ===")
printSortedU(surfaceBlockCounts)
// Deep-layer composition: deepslate should dominate below y=0, stone above.
deepStone := map[string]int{}
cc := gen(0, 0)
countAt := func(yLo, yHi int, label string) {
stone, deep, other := 0, 0, 0
for wy := yLo; wy <= yHi; wy++ {
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
switch cc.GetBlock(lx, wy, lz) {
case world.StateStone:
stone++
case 27924: // minecraft:deepslate
deep++
case world.StateAir:
default:
other++
}
}
}
}
deepStone[label] = deep
fmt.Printf(" %-18s stone=%d deepslate=%d other=%d\n", label, stone, deep, other)
}
fmt.Println("\n=== Deep-layer composition, chunk(0,0) ===")
countAt(16, 40, "y=16..40")
countAt(1, 7, "y=1..7 (transition)")
countAt(-64, -1, "y<0 (deepslate)")
// Bedrock floor: y=-64 must be solid bedrock everywhere, y=-63..-59 a
// thinning scatter of bedrock over stone/deepslate, and NOTHING in that band
// may be air or water. Air here means the surface-rule loop skipped the
// bottom layers and the sub-sea-level pass then flooded them.
fmt.Println("\n=== Bedrock floor, chunk(0,0) (expect no air/water, y=-64 fully bedrock) ===")
badFloor := 0
for wy := world.MinY; wy <= world.MinY+5; wy++ {
bedrock, solid, empty := 0, 0, 0
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
switch b := cc.GetBlock(lx, wy, lz); b {
case world.StateBedrock:
bedrock++
case world.StateAir, world.StateWater:
empty++
default:
solid++
}
}
}
badFloor += empty
fmt.Printf(" y=%-4d bedrock=%-4d other-solid=%-4d air/water=%d\n", wy, bedrock, solid, empty)
}
if badFloor > 0 {
fmt.Printf(" FAIL: %d air/water blocks in the bedrock band\n", badFloor)
} else {
fmt.Println(" OK: bedrock band is fully solid")
}
// Caves are dry: the aquifer decides fluid per position, so the open volume
// underground is overwhelmingly air, with occasional aquifer pools and lava
// down low. The defect this catches is the old unconditional "flood every
// air block below sea level" pass, under which this number was 100%.
fmt.Println("\n=== Underground fluids: water fraction y=-50..40 over inland chunks, lava anywhere ===")
air, water, lava, solidU := 0, 0, 0, 0
deepLava := 0
inland := 0
for cx := int32(-12); cx <= 12; cx += 4 {
for cz := int32(-12); cz <= 12; cz += 4 {
ch := gen(cx, cz)
// Lava is counted everywhere; the water fraction only over land,
// since an ocean's water legitimately reaches its floor. Lava
// pockets cluster, so a narrow sample can miss them entirely.
land := isInland(ch)
if land {
inland++
}
for wy := world.MinY; wy <= 40; wy++ {
// The water fraction is measured over y=-50..40, above the band
// where the global fluid rule makes lava unconditional.
census := land && wy >= -50
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
switch ch.GetBlock(lx, wy, lz) {
case world.StateAir:
if census {
air++
}
case world.StateWater:
if census {
water++
}
case world.StateLava:
lava++
if wy < -54 {
deepLava++
}
if census {
air++ // open volume, just not water
}
default:
if census {
solidU++
}
}
}
}
}
}
}
open := air + water
fmt.Printf(" chunks=%d solid=%d open=%d (air+lava=%d water=%d) | lava total=%d, of it below y=-54: %d\n",
inland, solidU, open, air, water, lava, deepLava)
switch {
case open == 0:
fmt.Println(" FAIL: no open volume underground at all")
default:
frac := float64(water) / float64(open)
fmt.Printf(" water is %.1f%% of the open volume\n", frac*100)
if frac > 0.35 {
fmt.Println(" FAIL: caves are flooded; the aquifer is not deciding fluid")
} else if lava == 0 {
fmt.Println(" FAIL: no lava anywhere underground")
} else {
fmt.Println(" OK: caves are dry and lava exists")
}
}
// Subsurface banding: find grass-topped land columns and print the top ~8
// blocks (grass cap → dirt band → stone) to confirm surfaceDepth widened the
// dirt band beyond a single block.
fmt.Println("\n=== Subsurface banding (grass columns: expect grass=9, dirt=10 band, stone=1) ===")
found := 0
for cx := int32(-40); cx < 40 && found < 6; cx += 3 {
for cz := int32(-40); cz < 40 && found < 6; cz += 3 {
ch := gen(cx, cz)
for lx := 0; lx < 16 && found < 6; lx += 5 {
for lz := 0; lz < 16 && found < 6; lz += 5 {
topY := world.MinY - 1
for wy := world.MinY + world.WorldHeight - 1; wy >= world.MinY; wy-- {
b := ch.GetBlock(lx, wy, lz)
if b != world.StateAir && b != world.StateWater &&
b != world.StateOakLog && b != world.StateOakLeaf {
topY = wy
break
}
}
if topY < world.SeaLevel || ch.GetBlock(lx, topY, lz) != world.StateGrass {
continue
}
row := fmt.Sprintf(" (%d,%d)+[%d,%d] top=y%d: ", cx, cz, lx, lz, topY)
for wy := topY; wy >= topY-9 && wy >= world.MinY; wy-- {
row += fmt.Sprintf("%d ", ch.GetBlock(lx, wy, lz))
}
fmt.Println(row)
found++
}
}
}
}
if found == 0 {
fmt.Println(" (no grass columns found in scan area)")
}
// 2) Cross-section at chunk (0,0): column x=8, over full Y, ASCII.
fmt.Println("\n=== Cross-section chunk(0,0) z=8, x=0..15 (side view, top 96 blocks near surface) ===")
c := gen(0, 0)
crossSection(c)
}
func loadName(od *worldgen.OverworldDensity, s2 worldgen.Sample2D, wx, wz int) string {
return world.BiomeNameAt(od, wx, wz)
}
// isInland reports whether most of the chunk's columns break the surface above
// sea level. Ocean chunks are excluded from the cave-fluid census because their
// water legitimately reaches all the way down to the sea floor.
func isInland(c *world.Chunk) bool {
aboveSea := 0
for lx := 0; lx < 16; lx += 2 {
for lz := 0; lz < 16; lz += 2 {
for wy := world.MinY + world.WorldHeight - 1; wy >= world.MinY; wy-- {
b := c.GetBlock(lx, wy, lz)
if b == world.StateAir {
continue
}
if b != world.StateWater && wy >= world.SeaLevel {
aboveSea++
}
break
}
}
}
return aboveSea > 48 // of 64 sampled columns
}
func crossSection(c *world.Chunk) {
// vertical band from y=40..136
for wy := 130; wy >= 40; wy-- {
row := fmt.Sprintf("%4d ", wy)
for lx := 0; lx < 16; lx++ {
row += glyph(c.GetBlock(lx, wy, 8))
}
fmt.Println(row)
}
}
func glyph(b uint16) string {
switch b {
case world.StateAir:
return "."
case world.StateWater:
return "~"
case world.StateLava:
return "!"
case world.StateStone:
return "#"
case world.StateDirt:
return "d"
case world.StateGrass:
return "g"
case world.StateSand:
return "s"
case world.StateBedrock:
return "B"
case world.StateOakLog:
return "L"
case world.StateOakLeaf:
return "o"
default:
return "?"
}
}
func printSorted(m map[string]int) {
type kv struct {
k string
v int
}
var s []kv
for k, v := range m {
s = append(s, kv{k, v})
}
sort.Slice(s, func(i, j int) bool { return s[i].v > s[j].v })
for _, e := range s {
fmt.Printf(" %-40s %d\n", e.k, e.v)
}
}
func printSortedU(m map[uint16]int) {
type kv struct {
k uint16
v int
}
var s []kv
for k, v := range m {
s = append(s, kv{k, v})
}
sort.Slice(s, func(i, j int) bool { return s[i].v > s[j].v })
for _, e := range s {
fmt.Printf(" id=%-6d %d\n", e.k, e.v)
}
}