RegionIO/cmd/gendump/main.go
Master290 3a255b52e1 Real badlands clay bands and a real biome temperature table
The bandlands rule cycled four terracotta colours off a per-column random draw.
Vanilla generates a 192-entry band table once per world, from a random source
named clay_bands, and reads it at the block's height shifted by the
clay_bands_offset noise. Brown, red and light grey terracotta were never placed
anywhere; the stripes were the wrong thickness and did not line up between
neighbouring columns. All seven colours now appear.

The temperature condition matched a hand-written list of eleven biome names.
Replacing it with the temperature field read out of the jar's 65 biome JSONs
fixes one of them: deep_frozen_ocean reads cold by name but its base
temperature is 0.5, so vanilla does not freeze it. taiga and the pine taigas
were the other way round -- excluded by name, and correctly so, but by
coincidence rather than by data.

Two parts of the vanilla calculation are left out and documented where they
belong: the height adjustment that cools peaks, and the "frozen" modifier that
warms scattered patches of frozen ocean. Both need PerlinSimplexNoise. Neither
is reachable from the overworld tree in a way that shows: the single condition
that consults temperature sits under a frozen_ocean biome check, below a water
check, and decides whether a hole in the ocean floor ices over. The snowy
mountain tops come from biome selection, not from here -- which is not what the
plan for this commit assumed.

The per-column *rand.Rand threaded through SurfaceContext goes away with the
old bandlands rule; nothing needs it now that vertical_gradient rolls
positionally.
2026-07-27 02:31:10 +03:00

445 lines
14 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)")
// The deepslate rule is a vertical_gradient over absolute anchors 0..8:
// everything solid below y=0 is deepslate, everything above y=8 is stone,
// and the band between them is a scatter. A zero here means the rule is
// firing but its block is being dropped, or its anchors are misread.
switch {
case deepStone["y<0 (deepslate)"] == 0:
fmt.Println(" FAIL: no deepslate below y=0")
case deepStone["y=16..40"] != 0:
fmt.Println(" FAIL: deepslate above the transition band")
case deepStone["y=1..7 (transition)"] == 0:
fmt.Println(" FAIL: the stone/deepslate transition band is empty")
default:
fmt.Println(" OK: deepslate below y=0, scattered through y=1..7, none above")
}
// 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
bandDepths := map[int]int{}
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; lx++ {
for lz := 0; lz < 16; lz++ {
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.StateLava &&
b != world.StateOakLog && b != world.StateOakLeaf {
topY = wy
break
}
}
if topY < world.SeaLevel || ch.GetBlock(lx, topY, lz) != world.StateGrass {
continue
}
depth := 0
for wy := topY - 1; wy >= topY-6 && ch.GetBlock(lx, wy, lz) == world.StateDirt; wy-- {
depth++
}
bandDepths[depth]++
}
}
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)")
}
// The band depth over every grass column scanned. Vanilla is 2..4; a
// histogram piled entirely on 0 means the biome surface subtree is gated to
// one block per column again.
banded, allGrass := 0, 0
for d, n := range bandDepths {
allGrass += n
if d >= 2 {
banded += n
}
}
fmt.Printf(" dirt-band depth over %d grass columns: %v\n", allGrass, bandDepths)
switch {
case allGrass == 0:
fmt.Println(" (no grass columns to measure)")
case banded*4 < allGrass*3:
fmt.Printf(" FAIL: only %d of %d grass columns carry 2+ blocks of dirt\n", banded, allGrass)
default:
fmt.Printf(" OK: %d of %d grass columns carry 2+ blocks of dirt\n", banded, allGrass)
}
// Badlands banding: the clay band table is 192 entries of seven terracotta
// colours. The stand-in it replaced cycled four, so brown, red and light
// grey never appeared anywhere in the world.
fmt.Println("\n=== Badlands clay bands (expect several terracotta colours down a column) ===")
terracottas := map[uint16]string{
12912: "terracotta", 11444: "white", 11445: "orange", 11448: "yellow",
11452: "light_gray", 11456: "brown", 11458: "red",
}
seenBands := map[uint16]int{}
badlandsCols := 0
for cx := int32(-300); cx < 300 && badlandsCols < 8; cx += 7 {
for cz := int32(-300); cz < 300 && badlandsCols < 8; cz += 7 {
name := world.BiomeNameAt(od, int(cx)*16+8, int(cz)*16+8)
if name != "minecraft:badlands" && name != "minecraft:eroded_badlands" && name != "minecraft:wooded_badlands" {
continue
}
ch := gen(cx, cz)
for lx := 0; lx < 16; lx += 4 {
for lz := 0; lz < 16; lz += 4 {
for wy := world.MinY + world.WorldHeight - 1; wy >= world.MinY; wy-- {
if _, isBand := terracottas[ch.GetBlock(lx, wy, lz)]; isBand {
seenBands[ch.GetBlock(lx, wy, lz)]++
}
}
}
}
badlandsCols++
}
}
if badlandsCols == 0 {
fmt.Println(" (no badlands in the scan area)")
} else {
for id, label := range terracottas {
fmt.Printf(" %-11s %d\n", label, seenBands[id])
}
if len(seenBands) < 6 {
fmt.Printf(" FAIL: only %d of 7 terracotta colours placed\n", len(seenBands))
} else {
fmt.Printf(" OK: %d of 7 terracotta colours across %d badlands chunks\n", len(seenBands), badlandsCols)
}
}
// 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)
}
}