RegionIO/internal/world/vanilla.go

408 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.SetBlock(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 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 {
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
}
// 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) }