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.
This commit is contained in:
Master290 2026-07-27 01:28:04 +03:00
parent c399070f59
commit 0a2845fa76
2 changed files with 275 additions and 19 deletions

View file

@ -254,7 +254,7 @@ func fillLegacySurface(out *[WorldHeight]uint16, solid [WorldHeight]bool, top in
switch {
case y <= MinY:
out[i] = StateBedrock
case y <= MinY+4 && solid[i] && bedrockAt(rng, y-MinY):
case y <= MinY+4 && solid[i] && bedrockAt(&rng, y-MinY):
out[i] = StateBedrock
case solid[i]:
switch {
@ -275,26 +275,27 @@ func fillLegacySurface(out *[WorldHeight]uint16, solid [WorldHeight]bool, top in
}
}
// bedrockAt reports whether a block at layer d (1..4 above the floor) should be
// bedrock, consuming randomness from rng. Vanilla's floor has probability ~1 at
// the bottom layer dropping to 0 a few blocks up; we approximate the decay with
// a 1/4 chance per step up from the solid floor.
func bedrockAt(rng chunkRand, d int) bool {
// Probability per layer: d=1 → 50%, d=2 → 25%, d=3 → 12.5%, d=4 → 6.25%.
// Need (5-d) high bits from a 32-bit draw; compare against a per-step mask.
keep := 5 - d // 4..1
if keep <= 0 {
// 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
}
// Each surviving bit roughly halves the chance; draw once and check `keep`
// of its low bits.
r := rng.next()
for b := 0; b < keep; b++ {
if (r>>uint(b))&1 == 0 {
return false
}
}
return true
return rng.nextFloat() < 1.0-float64(d)/5.0
}
// decorate places simple oak trees on grassy columns. Trunks are kept two