The router's noise caves are one kind of cave. The other kind -- the long
winding tunnels with rooms and side branches, and the ravines that cut down
through the terrain -- is walked, step by step, by a random source, and none of
it existed.
The shape of the work is unusual enough to state plainly. To carve one chunk,
vanilla replays every carver seeded in the 17x17 chunks around it and keeps only
what lands inside, so the same tunnel is walked up to 289 times across a world.
That redundancy is the point: it is what lets a chunk be carved without
generating its neighbours, which is the only way carving fits a generator that
produces one chunk at a time. A carve-once-write-into-neighbours design would be
cheaper and would not reproduce vanilla's mask and ordering.
Two primitives had to be right before any of it could be, and both are pinned
against values captured from the jar:
* setLargeFeatureSeed, which decides which chunks start a cave. It combines
its two products with XOR; setDecorationSeed, which it otherwise resembles,
uses addition and forces the low bit. Getting them the wrong way round moves
every tunnel in the world and nothing complains.
* Mth.sin and Mth.cos, which are a 65536-entry lookup table and not libm.
Mth.sin(-1.0) is -0.8414514 against Math.sin's -0.8414709848078965, and a
tunnel that walks by adding cos(yaw) a hundred times ends up somewhere else
entirely if that difference is smoothed away.
Carving lands between the surface pass and decoration, where vanilla puts it,
and both neighbours matter: the surface rules must already have placed grass for
a cave mouth to be retextured, and decoration must come after so nothing is
planted over a hole. The heights decoration plants against are recomputed
afterwards, which is why vanilla re-primes its heightmaps at the start of the
feature step.
The configs are extracted from the jar rather than transcribed, along with the
flattened #minecraft:overworld_carver_replaceables tag, so the probabilities and
Y ranges are data. Open volume below y=60 rises 28% over sixteen sampled chunks,
tunnels cut at or below y=-56 fill with lava rather than air (869 blocks, no
air), and the cost is inside the noise floor of the density pass.
573 lines
20 KiB
Go
573 lines
20 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)
|
||
veins := worldgen.NewOreVeinifier(od)
|
||
carver, err := worldgen.NewCarver(od, seed)
|
||
if err != nil {
|
||
panic("world: loading carvers: " + err.Error())
|
||
}
|
||
initCarverReplaceable(carver.ReplaceableBlocks())
|
||
return func(cx, cz int32) *Chunk {
|
||
return generateVanilla(od, fluidPicker, veins, carver, seed, cx, cz)
|
||
}
|
||
}
|
||
|
||
func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, 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 set is compiled against the world seed at load time. If
|
||
// it failed to parse, the surface pass falls back to biome-blind heuristics
|
||
// rather than leaving the terrain bare.
|
||
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 worldSurface [16][16]int // topmost non-air Y, the WORLD_SURFACE_WG heightmap
|
||
var grass [16][16]bool // grassy land surface (tree-plantable)
|
||
|
||
// Terrain and fluids first, for the whole chunk. The surface pass has to
|
||
// wait for all of it: the "steep" condition reads the heights of the
|
||
// column's neighbours, which vanilla takes from the heightmap that doFill
|
||
// finishes before buildSurface starts.
|
||
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++ {
|
||
surfTop[lx][lz], worldSurface[lx][lz], grass[lx][lz] =
|
||
fillVanillaColumn(od, aq, fluidPicker, veins, grids, interp, &columns[lx][lz], baseX+lx, baseZ+lz, lx, lz)
|
||
}
|
||
}(lx)
|
||
}
|
||
wg.Wait()
|
||
|
||
for lx := 0; lx < 16; lx++ {
|
||
wg.Add(1)
|
||
go func(lx int) {
|
||
defer wg.Done()
|
||
var sctx *worldgen.SurfaceContext
|
||
if ruleErr == nil {
|
||
sctx = surfaceRule.NewContext()
|
||
}
|
||
for lz := 0; lz < 16; lz++ {
|
||
if ruleErr == nil {
|
||
applySurfaceRule(od, surfaceRule, sctx, &columns[lx][lz],
|
||
baseX+lx, baseZ+lz, lx, lz, &worldSurface, biomeName[lx][lz])
|
||
continue
|
||
}
|
||
fillLegacySurface(&columns[lx][lz], surfTop[lx][lz], newColumnRand(baseX+lx, baseZ+lz, int(seed)))
|
||
}
|
||
}(lx)
|
||
}
|
||
wg.Wait()
|
||
|
||
// Carving sits between the surface pass and decoration, as it does in
|
||
// vanilla: it needs the surfaced blocks to retexture a cave mouth, and
|
||
// decoration needs the carved heights so nothing is planted over a hole.
|
||
if carver != nil && ruleErr == nil {
|
||
view := &carveView{
|
||
cols: &columns, od: od, rules: surfaceRule,
|
||
sctx: surfaceRule.NewContext(), biomes: &biomeName,
|
||
worldSurface: &worldSurface, baseX: baseX, baseZ: baseZ,
|
||
}
|
||
carver.CarveChunk(view, aq, int(cx), int(cz))
|
||
// The heights decoration plants against are the post-carve ones.
|
||
// Vanilla re-primes its heightmaps at the start of the feature step for
|
||
// the same reason.
|
||
for lx := 0; lx < 16; lx++ {
|
||
for lz := 0; lz < 16; lz++ {
|
||
surfTop[lx][lz], grass[lx][lz] = classifyColumn(&columns[lx][lz])
|
||
}
|
||
}
|
||
}
|
||
|
||
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, veins *worldgen.OreVeinifier, grids []cornerGrid, interp []float64, out *[WorldHeight]uint16, wx, wz, lx, lz int) (top, worldSurface int, grass bool) {
|
||
cx0 := lx / cellWidth
|
||
cz0 := lz / cellWidth
|
||
fx := float64(lx%cellWidth) / cellWidth
|
||
fz := float64(lz%cellWidth) / cellWidth
|
||
|
||
top, worldSurface = -1, MinY-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, solid := substance(aq, fluidPicker, veins, ctx, wx, y, wz, density)
|
||
out[i] = state
|
||
if solid {
|
||
top = i
|
||
}
|
||
if state != StateAir {
|
||
worldSurface = y
|
||
}
|
||
}
|
||
|
||
_, grass = classifyColumn(out)
|
||
return top, worldSurface, grass
|
||
}
|
||
|
||
// classifyColumn returns the top solid index and whether that surface is
|
||
// plantable grassy land. It is recomputed after carving, because a column whose
|
||
// top block a ravine removed is no longer the column decoration was told about.
|
||
func classifyColumn(col *[WorldHeight]uint16) (top int, grass bool) {
|
||
top = -1
|
||
for i := WorldHeight - 1; i >= 0; i-- {
|
||
if isStoneState(col[i]) {
|
||
top = i
|
||
break
|
||
}
|
||
}
|
||
if top < 0 {
|
||
return top, false
|
||
}
|
||
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 := topY >= SeaLevel-beachBand && topY <= SeaLevel+1
|
||
deepWater := topY < SeaLevel-beachBand
|
||
return top, !beach && !deepWater && topY >= SeaLevel
|
||
}
|
||
|
||
// steepAt is SurfaceRules.SteepMaterialCondition: true where the column's
|
||
// neighbours inside the chunk differ in height by four blocks or more. The
|
||
// neighbour indices are clamped to the chunk, as vanilla's are — the condition
|
||
// deliberately does not look at the chunk next door.
|
||
func steepAt(worldSurface *[16][16]int, lx, lz int) bool {
|
||
north := max(lz-1, 0)
|
||
south := min(lz+1, 15)
|
||
if worldSurface[lx][south] >= worldSurface[lx][north]+4 {
|
||
return true
|
||
}
|
||
west := max(lx-1, 0)
|
||
east := min(lx+1, 15)
|
||
return worldSurface[west][lz] >= worldSurface[east][lz]+4
|
||
}
|
||
|
||
// substance resolves one position to the block the terrain pass leaves behind,
|
||
// mirroring vanilla's MaterialRuleList: the aquifer answers first and, where it
|
||
// says the position is solid rock, the ore veinifier gets a turn before the
|
||
// default block is used. The second result says whether the position ended up
|
||
// solid, so the caller can track the top solid block without re-testing.
|
||
func substance(aq *worldgen.Aquifer, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, ctx worldgen.FunctionContext, x, y, z int, density float64) (state uint16, solid bool) {
|
||
if aq == nil {
|
||
// aquifers_enabled=false: Aquifer.createDisabled, the global fluid rule
|
||
// with no cells and no barriers.
|
||
if density > 0 {
|
||
return veinOrDefault(veins, ctx, x, y, z), true
|
||
}
|
||
return fluidPicker(x, y, z).At(y), false
|
||
}
|
||
if s, ok := aq.ComputeSubstance(x, y, z, density); ok {
|
||
return s, false
|
||
}
|
||
return veinOrDefault(veins, ctx, x, y, z), true
|
||
}
|
||
|
||
// veinOrDefault is the tail of the rule list: an ore vein if one reaches here,
|
||
// otherwise the settings' default block.
|
||
func veinOrDefault(veins *worldgen.OreVeinifier, ctx worldgen.FunctionContext, x, y, z int) uint16 {
|
||
if veins != nil {
|
||
if s, ok := veins.Calculate(ctx, x, y, z); ok {
|
||
return s
|
||
}
|
||
}
|
||
return StateStone
|
||
}
|
||
|
||
// 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(od *worldgen.OverworldDensity, rules *worldgen.SurfaceRuleSet, sctx *worldgen.SurfaceContext, out *[WorldHeight]uint16, wx, wz, lx, lz int, worldSurface *[16][16]int, biomeName string) {
|
||
top := -1
|
||
for i := WorldHeight - 1; i >= 0; i-- {
|
||
if out[i] != StateAir {
|
||
top = i
|
||
break
|
||
}
|
||
}
|
||
if top < 0 {
|
||
return
|
||
}
|
||
// Column-constant surface quantities, refreshed once per column exactly as
|
||
// SurfaceRules.Context.updateXZ does. The context itself is reused across
|
||
// the whole 16-column strip to avoid ~98k allocations per chunk; the fields
|
||
// that vary per block are set inside the loop below.
|
||
rules.BeginColumn(sctx, wx, wz)
|
||
surfaceDepth := od.Surface.SurfaceDepth(wx, wz)
|
||
sctx.SeaLevel = SeaLevel
|
||
sctx.BiomeName = biomeName
|
||
sctx.MinY = MinY
|
||
sctx.SurfaceSecondary = od.Surface.SurfaceSecondary(wx, wz)
|
||
sctx.SurfaceDepth = surfaceDepth
|
||
sctx.MinSurfaceLevel = od.MinSurfaceLevelAt(wx, wz, surfaceDepth)
|
||
sctx.Steep = steepAt(worldSurface, lx, lz)
|
||
minY := MinY
|
||
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
|
||
}
|
||
// A matched rule places its block even when that block is air: the
|
||
// frozen-ocean surface deliberately carves one away. Only "no rule
|
||
// matched" leaves the default block alone.
|
||
if state, ok := rules.Apply(sctx); ok {
|
||
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 set
|
||
// 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, rng chunkRand) {
|
||
const beachBand = 3
|
||
topY := MinY + top
|
||
beach := top >= 0 && topY >= SeaLevel-beachBand && topY <= SeaLevel+1
|
||
deepWater := top >= 0 && topY < SeaLevel-beachBand
|
||
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) }
|