The water condition asked "is this block at or above sea level", which is only the same question as vanilla's in an ocean. Vanilla asks how far the block sits below the water directly above it, and there is now water that is nowhere near y=63: the aquifer puts pools at their own levels, deep underground and up in the hills. Against sea level every one of those read as dry stone, and the stone above them read as lakebed. waterHeight is already tracked down the column, so the condition becomes the vanilla one: pass when there is no water above at all, otherwise when blockY (+ stoneDepthAbove where the rule asks for it) clears waterHeight + offset + surfaceDepth * multiplier. add_stone_depth was parsed and then ignored; three rules in the overworld tree set it. NoWaterAbove replaces a bare math.MinInt so a hand-built context cannot default to "water at y=0" by leaving the field unset.
483 lines
17 KiB
Go
483 lines
17 KiB
Go
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 := worldgen.NoWaterAbove
|
||
nextCeilingStoneY := math.MaxInt
|
||
for i := top; i >= 0; i-- {
|
||
y := minY + i
|
||
old := out[i]
|
||
if old == StateAir {
|
||
stoneDepthAbove = 0
|
||
waterHeight = worldgen.NoWaterAbove
|
||
continue
|
||
}
|
||
if isFluidState(old) {
|
||
if waterHeight == worldgen.NoWaterAbove {
|
||
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) }
|