RegionIO/internal/world/vanilla.go
Master290 0a2845fa76 Fix the bedrock floor ramp and add a gendump check for it
bedrockAt had two bugs that cancelled into a deterministic, wrong-looking floor.

It took its chunkRand by value, so next() mutated a copy and all four layers
drew the same 32-bit number. The layers were then decided by successive bits of
that one draw, nesting them into a prefix condition instead of scattering them
independently.

Its ramp also ran backwards. The comment claimed d=1 -> 50% decaying upward, but
`keep := 5 - d` requires more bits set the *lower* the layer, giving 1/16 at the
floor and 1/2 four blocks up — bedrock was likelier further from the bottom.
Vanilla ramps probability linearly from 1 at y=-64 to 0 at y=-59 and tests
nextFloat() < probability, which is what it does now.

Only fillLegacySurface reaches this; the normal path lets the surface rule tree
place the floor from the same datapack vertical_gradient rule. Both should agree.

cmd/gendump is new here: a client-free diagnostic that reports biome
distribution, top surface blocks, subsurface banding, deep-layer composition and
an ASCII cross-section, so generator defects can be seen without launching a
client. Its bedrock-band check prints per-layer counts and fails on any air or
water in the floor. On chunk (0,0) at seed 12345 it now reports y=-64 fully
bedrock, 207/154/106/66 thinning above it, and zero air or water.

The same output also shows the missing subsurface banding — grass sits directly
on stone — which is a separate defect in above_preliminary_surface, not fixed
here.
2026-07-27 01:28:04 +03:00

409 lines
14 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/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())
}
return func(cx, cz int32) *Chunk {
return generateVanilla(od, seed, cx, cz)
}
}
func generateVanilla(od *worldgen.OverworldDensity, 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()
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, 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). When a
// surface rule tree is provided, surface blocks are decided by it (vanilla
// behaviour: biome/depth/steepness/water/y-driven); otherwise the legacy
// beach/grass/dirt heuristics are used as a fallback.
func fillVanillaColumn(od *worldgen.OverworldDensity, 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
var solid [WorldHeight]bool
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)
}
ctx := worldgen.FunctionContext{X: float64(wx), Y: float64(MinY + i), Z: float64(wz)}.WithInterp(interp)
if od.Final.Compute(ctx) > 0 {
solid[i] = true
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, solid, top, wx, wz, SeaLevel, MinY, biomeName, rule, rng)
} else {
fillLegacySurface(out, solid, top, beach, deepWater, topY, rng)
}
// Water fills air below sea level regardless of rule path.
for i := 0; i < WorldHeight; i++ {
if out[i] == StateAir && MinY+i < SeaLevel {
out[i] = StateWater
}
}
return top, top >= 0 && !beach && !deepWater && topY >= SeaLevel
}
// applySurfaceRule walks the column top-to-surface applying the rule tree. For
// each solid block it builds a SurfaceContext and lets the rule decide; the
// stone depth counts how far below the surface the block sits. Air blocks
// above the surface are left for the water fill.
//
// 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, solid [WorldHeight]bool, top int, wx, wz, seaLevel, minY int, biomeName string, rule worldgen.SurfaceRule, rng chunkRand) {
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 + top,
Rng: colRng,
}
for i := top; i >= 0; i-- {
if !solid[i] {
continue
}
sctx.Y = minY + i
sctx.StoneDepthAbove = top - i
// Solid blocks default to stone; the rule tree overrides only the
// surface layers it matches (grass/sand/terracotta/etc). Blocks where
// the rule does not match (depth > surface band) keep stone, matching
// vanilla: surface rules replace only the top few blocks, the column is
// otherwise stone down to bedrock.
out[i] = StateStone
if state, ok := rule.Apply(sctx); ok && state != 0 {
out[i] = state
}
}
}
// fillLegacySurface is the biome-blind heuristic used when no surface rule is
// available (parse failure). It mirrors the pre-surface-rule block switch.
func fillLegacySurface(out *[WorldHeight]uint16, solid [WorldHeight]bool, top int, beach, deepWater bool, topY int, rng chunkRand) {
for i := 0; i < WorldHeight; i++ {
y := MinY + i
switch {
case y <= MinY:
out[i] = StateBedrock
case y <= MinY+4 && solid[i] && bedrockAt(&rng, y-MinY):
out[i] = StateBedrock
case solid[i]:
switch {
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
default:
out[i] = StateStone
}
case y < SeaLevel:
out[i] = StateWater
}
}
}
// 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) }