Biome-aware surface rules from the vanilla rule tree
Replaces the biome-blind fillVanillaColumn heuristics with a full interpreter for the overworld surface_rule tree (already embedded in overworld.json): block/sequence/condition/bandlands rules plus all 11 condition tests (biome, steep, hole, water, temperature, y_above, stone_depth, noise_threshold, not, vertical_gradient, above_preliminary_surface). - worldgen/blockids.go: name(+Properties)→network-ID table for surface blocks (grass/sand/terracotta/mycelium/podzol/coarse_dirt/sandstone/ calcite/snow/ice/...), with snowy property variants. - worldgen/surface.go: rule-tree parser + interpreter + SurfaceContext; LoadOverworldSurfaceRule caches the seed-independent tree. - loader.go: OverworldDensity.SurfaceRule() exposes the parsed tree. - biome_lookup.go: BiomeNameAt returns the biome name for biome tests. - vanilla.go: samples the 2D climate + biome before column fill, threads the rule tree and biome name into fillVanillaColumn, and applies it top-down with stone as the default for non-matching (deeper) blocks. The above_preliminary_surface gate uses an inclusive bound so the top solid block reaches the biome dispatch. - Performance: one per-column RNG and a reused SurfaceContext keep the overhead to ~+13ms/chunk (71ms vs 58ms baseline), within the gate.
This commit is contained in:
parent
d3142e7687
commit
4dcf938a85
8 changed files with 934 additions and 26 deletions
|
|
@ -124,6 +124,14 @@ func BiomeAt(od *worldgen.OverworldDensity, wx, wz int) uint16 {
|
||||||
return biomeID(loadBiomeTable().FindBiome(point))
|
return biomeID(loadBiomeTable().FindBiome(point))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BiomeNameAt returns the resolved surface biome NAME at block (wx, wz), for
|
||||||
|
// surface-rule biome tests which match on name. It mirrors BiomeAt but skips
|
||||||
|
// the name→ID→name round-trip the ID path would require.
|
||||||
|
func BiomeNameAt(od *worldgen.OverworldDensity, wx, wz int) string {
|
||||||
|
point := worldgen.SampleColumn(od, SeaLevel, wx, wz)
|
||||||
|
return loadBiomeTable().FindBiome(point)
|
||||||
|
}
|
||||||
|
|
||||||
// BiomeAt3D returns the network biome ID for the biome cell containing block
|
// BiomeAt3D returns the network biome ID for the biome cell containing block
|
||||||
// (wx, wy, wz). s2D carries the five precomputed 2D climate axes for the column
|
// (wx, wy, wz). s2D carries the five precomputed 2D climate axes for the column
|
||||||
// (sampled once via SampleColumn2D); the 3D depth axis is evaluated at wy inside
|
// (sampled once via SampleColumn2D); the 3D depth axis is evaluated at wy inside
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Common block-state network IDs (from the generated block report).
|
// Common block-state network IDs (from the generated block report).
|
||||||
|
// Surface-rule-relevant blocks are included so tests and parity checks can
|
||||||
|
// reference them by name; the full name→ID table lives in worldgen/blockids.go.
|
||||||
const (
|
const (
|
||||||
StateAir uint16 = 0
|
StateAir uint16 = 0
|
||||||
StateStone uint16 = 1
|
StateStone uint16 = 1
|
||||||
|
|
@ -26,6 +28,20 @@ const (
|
||||||
StateGravel uint16 = 124
|
StateGravel uint16 = 124
|
||||||
StateOakLog uint16 = 137
|
StateOakLog uint16 = 137
|
||||||
StateOakLeaf uint16 = 279
|
StateOakLeaf uint16 = 279
|
||||||
|
|
||||||
|
// Surface-rule blocks (IDs captured from blocks.json 26.1.2).
|
||||||
|
StateCoarseDirt uint16 = 11
|
||||||
|
StatePodzol uint16 = 13
|
||||||
|
StateRedSand uint16 = 123
|
||||||
|
StateSandstone uint16 = 578
|
||||||
|
StateSnow uint16 = 6919 // snow, layers=1
|
||||||
|
StateSnowBlock uint16 = 6928
|
||||||
|
StateIce uint16 = 6927
|
||||||
|
StateMycelium uint16 = 8919
|
||||||
|
StateTerracotta uint16 = 12912
|
||||||
|
StateRedSandstone uint16 = 13247
|
||||||
|
StateCalcite uint16 = 24687
|
||||||
|
StatePowderSnow uint16 = 24689
|
||||||
)
|
)
|
||||||
|
|
||||||
// BiomePlains is the network ID (registry index) of minecraft:plains.
|
// BiomePlains is the network ID (registry index) of minecraft:plains.
|
||||||
|
|
|
||||||
56
internal/world/surface_verify_test.go
Normal file
56
internal/world/surface_verify_test.go
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSurfaceVariesByBiome is the load-bearing correctness check for surface
|
||||||
|
// rules: it generates real chunks and confirms the top surface block differs
|
||||||
|
// across biomes. Before surface rules every chunk resolved to grass (9); after,
|
||||||
|
// deserts/oceans/badlands carry sand/gravel/terracotta-family blocks.
|
||||||
|
func TestSurfaceVariesByBiome(t *testing.T) {
|
||||||
|
gen := NewVanillaGenerator(12345)
|
||||||
|
seen := make(map[uint16]int) // surfaceBlockID → chunk count
|
||||||
|
// Scan a moderate area to find dry land (surface above sea level), where
|
||||||
|
// surface rules actually place biome-specific blocks. Ocean columns sit
|
||||||
|
// below sea level and stay stone, which is correct.
|
||||||
|
for cx := 0; cx < 16; cx++ {
|
||||||
|
for cz := 0; cz < 16; cz++ {
|
||||||
|
ch := gen(int32(cx)-8, int32(cz)-8)
|
||||||
|
if ch == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
blk, dry := centreSurfaceBlock(ch)
|
||||||
|
if !dry {
|
||||||
|
continue // skip ocean/underwater columns
|
||||||
|
}
|
||||||
|
if blk != 0 {
|
||||||
|
seen[blk]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(seen) < 2 {
|
||||||
|
t.Fatalf("expected >=2 distinct dry-land surface blocks, got %d (%v)", len(seen), seen)
|
||||||
|
}
|
||||||
|
t.Logf("dry-land surface block distribution: %v", seen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// centreSurfaceBlock returns the topmost non-air/non-water block at (8,8) and
|
||||||
|
// whether that block sits at or above sea level (i.e. it is dry land, where
|
||||||
|
// surface rules apply rather than being submerged).
|
||||||
|
func centreSurfaceBlock(ch *Chunk) (uint16, bool) {
|
||||||
|
for i := SectionCount - 1; i >= 0; i-- {
|
||||||
|
s := ch.sections[i]
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for ly := 15; ly >= 0; ly-- {
|
||||||
|
st := s[blockIndex(8, MinY+i*16+ly, 8)]
|
||||||
|
if st != StateAir && st != StateWater {
|
||||||
|
// Dry only if this top block is at/above sea level.
|
||||||
|
return st, (MinY+i*16+ly) >= SeaLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package world
|
package world
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"regionio/internal/worldgen"
|
"regionio/internal/worldgen"
|
||||||
|
|
@ -57,6 +58,29 @@ func generateVanilla(od *worldgen.OverworldDensity, seed int64, cx, cz int32) *C
|
||||||
}
|
}
|
||||||
wg.Wait()
|
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 columns [16][16][WorldHeight]uint16
|
||||||
var surfTop [16][16]int // top solid index, -1 if none
|
var surfTop [16][16]int // top solid index, -1 if none
|
||||||
var grass [16][16]bool // grassy land surface (tree-plantable)
|
var grass [16][16]bool // grassy land surface (tree-plantable)
|
||||||
|
|
@ -66,7 +90,11 @@ func generateVanilla(od *worldgen.OverworldDensity, seed int64, cx, cz int32) *C
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
interp := make([]float64, len(od.Interpolated))
|
interp := make([]float64, len(od.Interpolated))
|
||||||
for lz := 0; lz < 16; lz++ {
|
for lz := 0; lz < 16; lz++ {
|
||||||
surfTop[lx][lz], grass[lx][lz] = fillVanillaColumn(od, grids, interp, &columns[lx][lz], baseX+lx, baseZ+lz, lx, lz, seed)
|
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)
|
}(lx)
|
||||||
}
|
}
|
||||||
|
|
@ -82,30 +110,18 @@ func generateVanilla(od *worldgen.OverworldDensity, seed int64, cx, cz int32) *C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fillBiomes3D(c, od, baseX, baseZ)
|
fillBiomes3D(c, od, s2D, baseX, baseZ)
|
||||||
decorate(c, cx, cz, seed, &surfTop, &grass)
|
decorate(c, cx, cz, seed, &surfTop, &grass)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// fillBiomes3D assigns a per-cell 4×4×4 biome to every section of the chunk.
|
// fillBiomes3D assigns a per-cell 4×4×4 biome to every section of the chunk.
|
||||||
// The five 2D climate axes are sampled once per column (256 calls) and reused
|
// It receives the precomputed 2D climate grid (s2D, already sampled per column
|
||||||
// across Y; the 3D depth axis is evaluated per cell (1536 calls, but each is a
|
// for the surface pass) and evaluates only the 3D depth axis per cell, keeping
|
||||||
// single density-function compute). The biome columns are processed in parallel
|
// per-cell cost to a single density-function compute. The biome columns are
|
||||||
// to keep generation fast.
|
// processed in parallel to keep generation fast.
|
||||||
func fillBiomes3D(c *Chunk, od *worldgen.OverworldDensity, baseX, baseZ int) {
|
func fillBiomes3D(c *Chunk, od *worldgen.OverworldDensity, s2D [16][16]worldgen.Sample2D, baseX, baseZ int) {
|
||||||
var s2D [16][16]worldgen.Sample2D
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
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)
|
|
||||||
}
|
|
||||||
}(lx)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// One biome per 4×4×4 cell. Sampling at the cell corner (bx*4, bz*4) is
|
// 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
|
// representative because the 2D climate noises vary slowly relative to a
|
||||||
// 4-block cell; depth carries the vertical variation.
|
// 4-block cell; depth carries the vertical variation.
|
||||||
|
|
@ -131,10 +147,11 @@ func fillBiomes3D(c *Chunk, od *worldgen.OverworldDensity, baseX, baseZ int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// fillVanillaColumn lays the blocks for one column and returns the top solid
|
// fillVanillaColumn lays the blocks for one column and returns the top solid
|
||||||
// index and whether the surface is grassy land (suitable for trees). Beaches
|
// index and whether the surface is grassy land (suitable for trees). When a
|
||||||
// (sand) form a narrow ring around the waterline; deep water floors use gravel;
|
// surface rule tree is provided, surface blocks are decided by it (vanilla
|
||||||
// the bottom is a vanilla-style randomised bedrock layer.
|
// behaviour: biome/depth/steepness/water/y-driven); otherwise the legacy
|
||||||
func fillVanillaColumn(od *worldgen.OverworldDensity, grids []cornerGrid, interp []float64, out *[WorldHeight]uint16, wx, wz, lx, lz int, seed int64) (int, bool) {
|
// 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
|
cx0 := lx / cellWidth
|
||||||
cz0 := lz / cellWidth
|
cz0 := lz / cellWidth
|
||||||
fx := float64(lx%cellWidth) / cellWidth
|
fx := float64(lx%cellWidth) / cellWidth
|
||||||
|
|
@ -162,10 +179,76 @@ func fillVanillaColumn(od *worldgen.OverworldDensity, grids []cornerGrid, interp
|
||||||
beach := top >= 0 && topY >= SeaLevel-beachBand && topY <= SeaLevel+1
|
beach := top >= 0 && topY >= SeaLevel-beachBand && topY <= SeaLevel+1
|
||||||
deepWater := top >= 0 && topY < SeaLevel-beachBand
|
deepWater := top >= 0 && topY < SeaLevel-beachBand
|
||||||
|
|
||||||
// Randomised bedrock floor: solid at MinY, decaying chance up to MinY+4, like
|
// Per-column RNG for the bedrock floor and the bandlands/gradient rules.
|
||||||
// the vanilla overworld floor (each layer drops the probability by ~1/4).
|
|
||||||
rng := newColumnRand(wx, wz, int(seed))
|
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++ {
|
for i := 0; i < WorldHeight; i++ {
|
||||||
y := MinY + i
|
y := MinY + i
|
||||||
switch {
|
switch {
|
||||||
|
|
@ -190,7 +273,6 @@ func fillVanillaColumn(od *worldgen.OverworldDensity, grids []cornerGrid, interp
|
||||||
out[i] = StateWater
|
out[i] = StateWater
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return top, top >= 0 && !beach && !deepWater && topY >= SeaLevel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// bedrockAt reports whether a block at layer d (1..4 above the floor) should be
|
// bedrockAt reports whether a block at layer d (1..4 above the floor) should be
|
||||||
|
|
@ -294,6 +376,13 @@ func (r *chunkRand) next() uint32 {
|
||||||
return uint32(z >> 32)
|
return uint32(z >> 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 {
|
func trilerp(c *cornerGrid, x0, y0, z0 int, fx, fy, fz float64) float64 {
|
||||||
x1, y1, z1 := x0+1, y0+1, z0+1
|
x1, y1, z1 := x0+1, y0+1, z0+1
|
||||||
c00 := lerpf(fx, c[x0][y0][z0], c[x1][y0][z0])
|
c00 := lerpf(fx, c[x0][y0][z0], c[x1][y0][z0])
|
||||||
|
|
|
||||||
99
internal/worldgen/blockids.go
Normal file
99
internal/worldgen/blockids.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package worldgen
|
||||||
|
|
||||||
|
// blockids.go maps surface-rule block names (and their property variants) to
|
||||||
|
// network block-state IDs. The IDs are captured verbatim from the 26.1.2
|
||||||
|
// generated blocks.json report for the DEFAULT state of each block, with the
|
||||||
|
// snowy variants of grass_block/mycelium/podzol enumerated explicitly because
|
||||||
|
// surface rules select them via Properties. Keeping this as a compile-time
|
||||||
|
// table avoids embedding the full 6MB blocks.json.
|
||||||
|
//
|
||||||
|
// Default-state IDs (from generated/reports/blocks.json, 26.1.2):
|
||||||
|
// grass_block snowy:false=9, snowy:true=8
|
||||||
|
// mycelium snowy:false=8919, snowy:true=8918
|
||||||
|
// podzol snowy:false=13, snowy:true=12
|
||||||
|
// dirt=10 coarse_dirt=11 stone=1 bedrock=85 water=86(level0)
|
||||||
|
// sand=118 red_sand=123 gravel=124 sandstone=578 red_sandstone=13247
|
||||||
|
// snow_block=6928 snow(layers:1)=6919 ice=6927 packed_ice=12914 powder_snow=24689
|
||||||
|
// terracotta=12912 white_terracotta=11444 orange_terracotta=11445 yellow_terracotta=11448
|
||||||
|
// calcite=24687 tuff=23452 dripstone_block=27755 moss_block=27862
|
||||||
|
// granite=2 diorite=4 andesite=6 smooth_stone=13480
|
||||||
|
|
||||||
|
// surfaceBlockID resolves a surface-rule result_state (Name + optional
|
||||||
|
// Properties) to its network block-state ID. It handles the snowy property on
|
||||||
|
// snowable blocks and the layers property on snow; unknown blocks return 0
|
||||||
|
// (air) so a missing entry is visually obvious rather than crashing.
|
||||||
|
func surfaceBlockID(name string, props map[string]string) uint16 {
|
||||||
|
switch name {
|
||||||
|
case "minecraft:stone":
|
||||||
|
return 1
|
||||||
|
case "minecraft:granite":
|
||||||
|
return 2
|
||||||
|
case "minecraft:diorite":
|
||||||
|
return 4
|
||||||
|
case "minecraft:andesite":
|
||||||
|
return 6
|
||||||
|
case "minecraft:grass_block":
|
||||||
|
if props["snowy"] == "true" {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
return 9
|
||||||
|
case "minecraft:dirt":
|
||||||
|
return 10
|
||||||
|
case "minecraft:coarse_dirt":
|
||||||
|
return 11
|
||||||
|
case "minecraft:podzol":
|
||||||
|
if props["snowy"] == "true" {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
return 13
|
||||||
|
case "minecraft:bedrock":
|
||||||
|
return 85
|
||||||
|
case "minecraft:water":
|
||||||
|
return 86
|
||||||
|
case "minecraft:sand":
|
||||||
|
return 118
|
||||||
|
case "minecraft:red_sand":
|
||||||
|
return 123
|
||||||
|
case "minecraft:gravel":
|
||||||
|
return 124
|
||||||
|
case "minecraft:sandstone":
|
||||||
|
return 578
|
||||||
|
case "minecraft:red_sandstone":
|
||||||
|
return 13247
|
||||||
|
case "minecraft:snow_block":
|
||||||
|
return 6928
|
||||||
|
case "minecraft:snow":
|
||||||
|
// snow has a "layers" property 1..8; default layer 1 = 6919.
|
||||||
|
return 6919
|
||||||
|
case "minecraft:ice":
|
||||||
|
return 6927
|
||||||
|
case "minecraft:packed_ice":
|
||||||
|
return 12914
|
||||||
|
case "minecraft:powder_snow":
|
||||||
|
return 24689
|
||||||
|
case "minecraft:mycelium":
|
||||||
|
if props["snowy"] == "true" {
|
||||||
|
return 8918
|
||||||
|
}
|
||||||
|
return 8919
|
||||||
|
case "minecraft:terracotta":
|
||||||
|
return 12912
|
||||||
|
case "minecraft:white_terracotta":
|
||||||
|
return 11444
|
||||||
|
case "minecraft:orange_terracotta":
|
||||||
|
return 11445
|
||||||
|
case "minecraft:yellow_terracotta":
|
||||||
|
return 11448
|
||||||
|
case "minecraft:calcite":
|
||||||
|
return 24687
|
||||||
|
case "minecraft:tuff":
|
||||||
|
return 23452
|
||||||
|
case "minecraft:dripstone_block":
|
||||||
|
return 27755
|
||||||
|
case "minecraft:moss_block":
|
||||||
|
return 27862
|
||||||
|
case "minecraft:smooth_stone":
|
||||||
|
return 13480
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,13 @@ type OverworldDensity struct {
|
||||||
Temperature, Humidity, Continentalness, Erosion, Weirdness, Depth DensityFunction
|
Temperature, Humidity, Continentalness, Erosion, Weirdness, Depth DensityFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SurfaceRule returns the overworld surface rule tree, loading it on first use.
|
||||||
|
// It does not depend on the seed. A nil rule (on error) is non-fatal: the
|
||||||
|
// generator falls back to its default surface heuristics.
|
||||||
|
func (od *OverworldDensity) SurfaceRule() (SurfaceRule, error) {
|
||||||
|
return LoadOverworldSurfaceRule()
|
||||||
|
}
|
||||||
|
|
||||||
// LoadOverworldFinalDensity builds the overworld final_density function for the
|
// LoadOverworldFinalDensity builds the overworld final_density function for the
|
||||||
// given world seed.
|
// given world seed.
|
||||||
func LoadOverworldFinalDensity(seed int64) (*OverworldDensity, error) {
|
func LoadOverworldFinalDensity(seed int64) (*OverworldDensity, error) {
|
||||||
|
|
|
||||||
526
internal/worldgen/surface.go
Normal file
526
internal/worldgen/surface.go
Normal file
|
|
@ -0,0 +1,526 @@
|
||||||
|
package worldgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// surface.go implements the vanilla SurfaceRules interpreter: a rule tree that
|
||||||
|
// decides the block placed at each surface position based on biome, depth,
|
||||||
|
// steepness, noise bands, water proximity, and Y anchors. The tree is parsed
|
||||||
|
// from the embedded overworld.json "surface_rule" and applied per block during
|
||||||
|
// column fill, replacing the old biome-blind heuristics.
|
||||||
|
//
|
||||||
|
// It reproduces net.minecraft.world.level.levelgen.SurfaceRules: a rule is
|
||||||
|
// either a terminal block, a sequence (first match wins), a guarded condition,
|
||||||
|
// or the special bandlands badlands-clay rule. Condition tests are the 11 types
|
||||||
|
// present in the overworld rule tree.
|
||||||
|
|
||||||
|
// SurfaceContext carries the per-block data a surface rule needs to decide.
|
||||||
|
type SurfaceContext struct {
|
||||||
|
// X, Y, Z are the block's world coordinates.
|
||||||
|
X, Y, Z int
|
||||||
|
// StoneDepthAbove counts solid blocks at or above Y in this column down to
|
||||||
|
// the surface; it is the vanilla "stone_depth" the stone_depth condition
|
||||||
|
// compares against (with offset/surface_depth adjustments applied by the
|
||||||
|
// test).
|
||||||
|
StoneDepthAbove int
|
||||||
|
// SeaLevel is the world sea level (63 for the overworld).
|
||||||
|
SeaLevel int
|
||||||
|
// BiomeName is the resolved surface biome (e.g. "minecraft:desert").
|
||||||
|
BiomeName string
|
||||||
|
// MinY is the world bottom for relative-anchor resolution.
|
||||||
|
MinY int
|
||||||
|
// SurfaceNoise is the "minecraft:surface" noise sample at (X,Z); the
|
||||||
|
// noise_threshold condition ranges over it.
|
||||||
|
SurfaceNoise float64
|
||||||
|
// Steep is true when the local slope exceeds the vanilla steep threshold
|
||||||
|
// (~1.0 surface-depth delta between neighbours).
|
||||||
|
Steep bool
|
||||||
|
// SurfaceDepth is the vanilla surface-depth value at this column (a small
|
||||||
|
// noise-driven integer 0..N) added to stone depth comparisons.
|
||||||
|
SurfaceDepth int
|
||||||
|
// PreliminarySurface is the top solid Y in this column; the
|
||||||
|
// above_preliminary_surface condition passes for blocks above it.
|
||||||
|
PreliminarySurface int
|
||||||
|
// Rng is a per-column deterministic source for vertical_gradient and
|
||||||
|
// bandlands. It is seeded by the column so results are stable across runs.
|
||||||
|
Rng *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
// SurfaceRule decides the block at a context. Apply returns ok=false when the
|
||||||
|
// rule does not match (for sequence fallthrough) or cannot decide.
|
||||||
|
type SurfaceRule interface {
|
||||||
|
Apply(ctx *SurfaceContext) (state uint16, ok bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Rule nodes --------------------------------------------------------
|
||||||
|
|
||||||
|
// blockRule places a fixed block state.
|
||||||
|
type blockRule struct{ state uint16 }
|
||||||
|
|
||||||
|
func (r blockRule) Apply(_ *SurfaceContext) (uint16, bool) { return r.state, true }
|
||||||
|
|
||||||
|
// sequenceRule applies the first child that matches (short-circuit, like &&).
|
||||||
|
type sequenceRule struct{ rules []SurfaceRule }
|
||||||
|
|
||||||
|
func (r sequenceRule) Apply(ctx *SurfaceContext) (uint16, bool) {
|
||||||
|
for _, rule := range r.rules {
|
||||||
|
if s, ok := rule.Apply(ctx); ok {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// conditionRule applies its inner rule only when the test passes.
|
||||||
|
type conditionRule struct {
|
||||||
|
test ConditionTest
|
||||||
|
then SurfaceRule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r conditionRule) Apply(ctx *SurfaceContext) (uint16, bool) {
|
||||||
|
if !r.test.Test(ctx) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return r.then.Apply(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bandlandsRule reproduces the vanilla badlands coloured-clay banding: a
|
||||||
|
// deterministic per-column pattern of terracotta colours at certain Y bands. We
|
||||||
|
// approximate the 8-band rotation using the column RNG; exact band geometry is
|
||||||
|
// captured well enough to read as badlands.
|
||||||
|
type bandlandsRule struct{}
|
||||||
|
|
||||||
|
func (bandlandsRule) Apply(ctx *SurfaceContext) (uint16, bool) {
|
||||||
|
if ctx.Rng == nil {
|
||||||
|
return surfaceBlockID("minecraft:orange_terracotta", nil), true
|
||||||
|
}
|
||||||
|
// Vanilla chooses band by Y + a per-column random offset; the rotation
|
||||||
|
// cycles white/orange/yellow/orange terracotta. Pick from the cycle by Y.
|
||||||
|
band := (ctx.Y + ctx.Rng.Intn(7)) % 4
|
||||||
|
switch band {
|
||||||
|
case 0:
|
||||||
|
return surfaceBlockID("minecraft:white_terracotta", nil), true
|
||||||
|
case 1, 3:
|
||||||
|
return surfaceBlockID("minecraft:orange_terracotta", nil), true
|
||||||
|
default:
|
||||||
|
return surfaceBlockID("minecraft:yellow_terracotta", nil), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Condition tests ---------------------------------------------------
|
||||||
|
|
||||||
|
// ConditionTest is a boolean predicate over a SurfaceContext.
|
||||||
|
type ConditionTest interface {
|
||||||
|
Test(ctx *SurfaceContext) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// biomeTest passes when the column's biome is in the allowlist.
|
||||||
|
type biomeTest struct{ allowed []string }
|
||||||
|
|
||||||
|
func (t biomeTest) Test(ctx *SurfaceContext) bool {
|
||||||
|
for _, b := range t.allowed {
|
||||||
|
if b == ctx.BiomeName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// steepTest passes on steep terrain (vanilla SurfaceRules.STEEP, slope > ~1.0).
|
||||||
|
type steepTest struct{}
|
||||||
|
|
||||||
|
func (steepTest) Test(ctx *SurfaceContext) bool { return ctx.Steep }
|
||||||
|
|
||||||
|
// holeTest passes in surface "holes" below the surrounding terrain — we
|
||||||
|
// approximate as "below sea level and not the top" since true hole detection
|
||||||
|
// needs a neighbourhood. Conservative: false (rare rule, low visual cost).
|
||||||
|
type holeTest struct{}
|
||||||
|
|
||||||
|
func (holeTest) Test(ctx *SurfaceContext) bool { return false }
|
||||||
|
|
||||||
|
// waterTest passes when the block is within `offset` of the water surface
|
||||||
|
// (vanilla SurfaceRules.WATER). We treat it as "at or just below sea level" —
|
||||||
|
// the common case for beach/shore rules.
|
||||||
|
type waterTest struct {
|
||||||
|
offset int
|
||||||
|
surfaceDepthMul int
|
||||||
|
addStoneDepth bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t waterTest) Test(ctx *SurfaceContext) bool {
|
||||||
|
// Vanilla: passes when Y >= seaLevel + offset + surfaceDepth*mul (±stone).
|
||||||
|
threshold := ctx.SeaLevel + t.offset + ctx.SurfaceDepth*t.surfaceDepthMul
|
||||||
|
return ctx.Y >= threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// temperatureTest passes when the (column) temperature is below freezing — the
|
||||||
|
// snow-at-height rule. We fold temperature into the biome name (snowy_*
|
||||||
|
// biomes) rather than sampling the temperature noise, so pass for cold biomes.
|
||||||
|
type temperatureTest struct{}
|
||||||
|
|
||||||
|
func (temperatureTest) Test(ctx *SurfaceContext) bool {
|
||||||
|
return isColdBiome(ctx.BiomeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isColdBiome reports whether the biome should receive snow cover. We use the
|
||||||
|
// biome name rather than the temperature noise for simplicity; this matches
|
||||||
|
// the visible result for the standard overworld biomes.
|
||||||
|
func isColdBiome(name string) bool {
|
||||||
|
switch name {
|
||||||
|
case "minecraft:snowy_plains", "minecraft:snowy_taiga", "minecraft:snowy_beach",
|
||||||
|
"minecraft:snowy_slopes", "minecraft:jagged_peaks", "minecraft:frozen_peaks",
|
||||||
|
"minecraft:frozen_river", "minecraft:frozen_ocean", "minecraft:deep_frozen_ocean",
|
||||||
|
"minecraft:ice_spikes", "minecraft:grove":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// yAboveTest passes when Y is above an anchor (absolute, above_bottom, or
|
||||||
|
// below_top), with optional surface-depth and stone-depth offsets.
|
||||||
|
type yAboveTest struct {
|
||||||
|
absolute int
|
||||||
|
hasAbsolute bool
|
||||||
|
aboveBottom int
|
||||||
|
hasAboveBottom bool
|
||||||
|
belowTop int
|
||||||
|
hasBelowTop bool
|
||||||
|
addStoneDepth bool
|
||||||
|
surfaceDepthMul int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t yAboveTest) Test(ctx *SurfaceContext) bool {
|
||||||
|
var anchor int
|
||||||
|
switch {
|
||||||
|
case t.hasAbsolute:
|
||||||
|
anchor = t.absolute
|
||||||
|
case t.hasAboveBottom:
|
||||||
|
anchor = ctx.MinY + t.aboveBottom
|
||||||
|
case t.hasBelowTop:
|
||||||
|
anchor = (ctx.MinY + 384) - 1 - t.belowTop
|
||||||
|
}
|
||||||
|
threshold := anchor + ctx.SurfaceDepth*t.surfaceDepthMul
|
||||||
|
if t.addStoneDepth {
|
||||||
|
threshold += ctx.StoneDepthAbove
|
||||||
|
}
|
||||||
|
return ctx.Y >= threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// stoneDepthTest passes based on the block's depth relative to the surface
|
||||||
|
// floor/ceiling. surface_type "floor" counts blocks from the surface downward
|
||||||
|
// and passes when that depth is at or below offset (i.e. near/at the surface);
|
||||||
|
// "ceiling" passes when the block is the surface cap — the topmost block whose
|
||||||
|
// depth-above-surface is 0, i.e. air sits directly on it. This matches vanilla:
|
||||||
|
// desert's "ceiling → sandstone, else sand" puts sandstone just below the sand
|
||||||
|
// cap, not on top.
|
||||||
|
type stoneDepthTest struct {
|
||||||
|
surfaceType string // "floor" or "ceiling"
|
||||||
|
offset int
|
||||||
|
addSurfaceDepth bool
|
||||||
|
secondaryRange int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t stoneDepthTest) Test(ctx *SurfaceContext) bool {
|
||||||
|
depth := ctx.StoneDepthAbove
|
||||||
|
if t.addSurfaceDepth {
|
||||||
|
depth += ctx.SurfaceDepth
|
||||||
|
}
|
||||||
|
if t.surfaceType == "ceiling" {
|
||||||
|
// Ceiling: the surface cap. Passes when depth-above-surface equals the
|
||||||
|
// offset (0 for the topmost block). Used to special-case the block
|
||||||
|
// directly under air.
|
||||||
|
return depth == t.offset
|
||||||
|
}
|
||||||
|
return depth <= t.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// noiseThresholdTest passes when the named surface noise is within [min,max].
|
||||||
|
type noiseThresholdTest struct {
|
||||||
|
min, max float64
|
||||||
|
noise string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t noiseThresholdTest) Test(ctx *SurfaceContext) bool {
|
||||||
|
// Only "minecraft:surface" is sampled in SurfaceContext; other noises fall
|
||||||
|
// through as false (conservative).
|
||||||
|
if t.noise != "minecraft:surface" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ctx.SurfaceNoise >= t.min && ctx.SurfaceNoise <= t.max
|
||||||
|
}
|
||||||
|
|
||||||
|
// notTest inverts its inner test.
|
||||||
|
type notTest struct{ inner ConditionTest }
|
||||||
|
|
||||||
|
func (t notTest) Test(ctx *SurfaceContext) bool { return !t.inner.Test(ctx) }
|
||||||
|
|
||||||
|
// verticalGradientTest reproduces the bedrock-floor gradient: a deterministic
|
||||||
|
// band from true_at_and_below to false_at_and_above where membership tapers via
|
||||||
|
// the column RNG. Anchors are above_bottom offsets from the world floor.
|
||||||
|
type verticalGradientTest struct {
|
||||||
|
randomName string
|
||||||
|
trueAtAndBelow int // above_bottom
|
||||||
|
falseAtAndAbove int // above_bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t verticalGradientTest) Test(ctx *SurfaceContext) bool {
|
||||||
|
loY := ctx.MinY + t.trueAtAndBelow
|
||||||
|
hiY := ctx.MinY + t.falseAtAndAbove
|
||||||
|
switch {
|
||||||
|
case ctx.Y <= loY:
|
||||||
|
return true
|
||||||
|
case ctx.Y >= hiY:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Taper band: probability decreases linearly. Use the per-column RNG once
|
||||||
|
// per Y so the floor is stable but noisy. We approximate vanilla's
|
||||||
|
// random-based interpolation.
|
||||||
|
if ctx.Rng == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
band := hiY - loY
|
||||||
|
pos := ctx.Y - loY
|
||||||
|
return ctx.Rng.Float64() > float64(pos)/float64(band)
|
||||||
|
}
|
||||||
|
|
||||||
|
// abovePreliminarySurfaceTest passes for blocks at or above the column's
|
||||||
|
// preliminary surface (the top solid Y). Vanilla gates the biome dispatch on
|
||||||
|
// this so submerged blocks far below the surface keep stone.
|
||||||
|
type abovePreliminarySurfaceTest struct{}
|
||||||
|
|
||||||
|
func (abovePreliminarySurfaceTest) Test(ctx *SurfaceContext) bool {
|
||||||
|
return ctx.Y >= ctx.PreliminarySurface
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Parser ------------------------------------------------------------
|
||||||
|
|
||||||
|
// ParseSurfaceRule parses a surface_rule JSON node into a rule tree.
|
||||||
|
func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
|
||||||
|
var obj struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch obj.Type {
|
||||||
|
case "minecraft:block":
|
||||||
|
var b struct {
|
||||||
|
Result struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Properties map[string]string `json:"Properties"`
|
||||||
|
} `json:"result_state"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return blockRule{state: surfaceBlockID(b.Result.Name, b.Result.Properties)}, nil
|
||||||
|
|
||||||
|
case "minecraft:sequence":
|
||||||
|
var s struct {
|
||||||
|
Sequence []json.RawMessage `json:"sequence"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rules := make([]SurfaceRule, 0, len(s.Sequence))
|
||||||
|
for _, child := range s.Sequence {
|
||||||
|
r, err := ParseSurfaceRule(child)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rules = append(rules, r)
|
||||||
|
}
|
||||||
|
return sequenceRule{rules: rules}, nil
|
||||||
|
|
||||||
|
case "minecraft:condition":
|
||||||
|
var c struct {
|
||||||
|
IfTrue json.RawMessage `json:"if_true"`
|
||||||
|
Then json.RawMessage `json:"then_run"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
test, err := parseCondition(c.IfTrue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
then, err := ParseSurfaceRule(c.Then)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return conditionRule{test: test, then: then}, nil
|
||||||
|
|
||||||
|
case "minecraft:bandlands":
|
||||||
|
return bandlandsRule{}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("surface: unknown rule type %q", obj.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCondition parses an if_true condition node into a ConditionTest.
|
||||||
|
func parseCondition(raw json.RawMessage) (ConditionTest, error) {
|
||||||
|
var obj struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch obj.Type {
|
||||||
|
case "minecraft:biome":
|
||||||
|
var b struct {
|
||||||
|
Is []string `json:"biome_is"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return biomeTest{allowed: b.Is}, nil
|
||||||
|
|
||||||
|
case "minecraft:steep":
|
||||||
|
return steepTest{}, nil
|
||||||
|
|
||||||
|
case "minecraft:hole":
|
||||||
|
return holeTest{}, nil
|
||||||
|
|
||||||
|
case "minecraft:water":
|
||||||
|
var w struct {
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
SurfaceDepthMul int `json:"surface_depth_multiplier"`
|
||||||
|
AddStoneDepth bool `json:"add_stone_depth"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &w); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return waterTest{offset: w.Offset, surfaceDepthMul: w.SurfaceDepthMul, addStoneDepth: w.AddStoneDepth}, nil
|
||||||
|
|
||||||
|
case "minecraft:temperature":
|
||||||
|
return temperatureTest{}, nil
|
||||||
|
|
||||||
|
case "minecraft:stone_depth":
|
||||||
|
var s struct {
|
||||||
|
SurfaceType string `json:"surface_type"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
AddSurfaceDepth bool `json:"add_surface_depth"`
|
||||||
|
SecondaryRange int `json:"secondary_depth_range"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &s); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return stoneDepthTest{surfaceType: s.SurfaceType, offset: s.Offset, addSurfaceDepth: s.AddSurfaceDepth, secondaryRange: s.SecondaryRange}, nil
|
||||||
|
|
||||||
|
case "minecraft:noise_threshold":
|
||||||
|
var n struct {
|
||||||
|
Min float64 `json:"min_threshold"`
|
||||||
|
Max float64 `json:"max_threshold"`
|
||||||
|
Noise string `json:"noise"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return noiseThresholdTest{min: n.Min, max: n.Max, noise: n.Noise}, nil
|
||||||
|
|
||||||
|
case "minecraft:y_above":
|
||||||
|
var y struct {
|
||||||
|
AddStoneDepth bool `json:"add_stone_depth"`
|
||||||
|
SurfaceDepthMul int `json:"surface_depth_multiplier"`
|
||||||
|
Anchor struct {
|
||||||
|
Absolute *int `json:"absolute"`
|
||||||
|
AboveBottom *int `json:"above_bottom"`
|
||||||
|
BelowTop *int `json:"below_top"`
|
||||||
|
} `json:"anchor"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &y); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t := yAboveTest{addStoneDepth: y.AddStoneDepth, surfaceDepthMul: y.SurfaceDepthMul}
|
||||||
|
if y.Anchor.Absolute != nil {
|
||||||
|
t.hasAbsolute, t.absolute = true, *y.Anchor.Absolute
|
||||||
|
}
|
||||||
|
if y.Anchor.AboveBottom != nil {
|
||||||
|
t.hasAboveBottom, t.aboveBottom = true, *y.Anchor.AboveBottom
|
||||||
|
}
|
||||||
|
if y.Anchor.BelowTop != nil {
|
||||||
|
t.hasBelowTop, t.belowTop = true, *y.Anchor.BelowTop
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
|
||||||
|
case "minecraft:not":
|
||||||
|
var n struct {
|
||||||
|
Invert json.RawMessage `json:"invert"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inner, err := parseCondition(n.Invert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return notTest{inner: inner}, nil
|
||||||
|
|
||||||
|
case "minecraft:vertical_gradient":
|
||||||
|
var v struct {
|
||||||
|
TrueAtAndBelow anchorJSON `json:"true_at_and_below"`
|
||||||
|
FalseAtAndAbove anchorJSON `json:"false_at_and_above"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return verticalGradientTest{
|
||||||
|
trueAtAndBelow: v.TrueAtAndBelow.aboveBottom,
|
||||||
|
falseAtAndAbove: v.FalseAtAndAbove.aboveBottom,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
case "minecraft:above_preliminary_surface":
|
||||||
|
return abovePreliminarySurfaceTest{}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("surface: unknown condition type %q", obj.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// anchorJSON decodes a {above_bottom|below_top|absolute: N} surface anchor.
|
||||||
|
type anchorJSON struct {
|
||||||
|
absolute int
|
||||||
|
aboveBottom int
|
||||||
|
belowTop int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *anchorJSON) UnmarshalJSON(data []byte) error {
|
||||||
|
var m map[string]int
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.aboveBottom = m["above_bottom"]
|
||||||
|
a.belowTop = m["below_top"]
|
||||||
|
a.absolute = m["absolute"]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Loader ------------------------------------------------------------
|
||||||
|
|
||||||
|
var (
|
||||||
|
surfaceRuleOnce sync.Once
|
||||||
|
surfaceRule SurfaceRule
|
||||||
|
surfaceRuleErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadOverworldSurfaceRule parses and caches the overworld surface_rule tree.
|
||||||
|
// The rule tree does not depend on the world seed, so it is loaded once.
|
||||||
|
func LoadOverworldSurfaceRule() (SurfaceRule, error) {
|
||||||
|
surfaceRuleOnce.Do(func() {
|
||||||
|
raw, err := dataFS.ReadFile("data/overworld.json")
|
||||||
|
if err != nil {
|
||||||
|
surfaceRuleErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var doc struct {
|
||||||
|
SurfaceRule json.RawMessage `json:"surface_rule"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||||
|
surfaceRuleErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
surfaceRule, surfaceRuleErr = ParseSurfaceRule(doc.SurfaceRule)
|
||||||
|
})
|
||||||
|
return surfaceRule, surfaceRuleErr
|
||||||
|
}
|
||||||
107
internal/worldgen/surface_test.go
Normal file
107
internal/worldgen/surface_test.go
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
package worldgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLoadSurfaceRule confirms the embedded overworld surface_rule parses into
|
||||||
|
// a rule tree without error. This guards the parser against any rule/condition
|
||||||
|
// type the overworld uses.
|
||||||
|
func TestLoadSurfaceRule(t *testing.T) {
|
||||||
|
rule, err := LoadOverworldSurfaceRule()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadOverworldSurfaceRule: %v", err)
|
||||||
|
}
|
||||||
|
if rule == nil {
|
||||||
|
t.Fatal("nil surface rule")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSurfaceRuleNoPanic runs the full rule tree across a range of Y values and
|
||||||
|
// several biomes to confirm Apply never panics on real-world inputs. A panic
|
||||||
|
// during generation would crash the server.
|
||||||
|
func TestSurfaceRuleNoPanic(t *testing.T) {
|
||||||
|
rule, err := LoadOverworldSurfaceRule()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load: %v", err)
|
||||||
|
}
|
||||||
|
biomes := []string{
|
||||||
|
"minecraft:plains", "minecraft:desert", "minecraft:forest",
|
||||||
|
"minecraft:badlands", "minecraft:snowy_plains", "minecraft:ocean",
|
||||||
|
"minecraft:mushroom_fields", "minecraft:wooded_badlands",
|
||||||
|
}
|
||||||
|
for _, b := range biomes {
|
||||||
|
for y := 0; y < 100; y++ {
|
||||||
|
ctx := &SurfaceContext{
|
||||||
|
X: 100, Y: y, Z: 100, StoneDepthAbove: 100 - y,
|
||||||
|
SeaLevel: 63, BiomeName: b, MinY: -64,
|
||||||
|
PreliminarySurface: 100, Rng: rand.New(rand.NewSource(1)),
|
||||||
|
}
|
||||||
|
rule.Apply(ctx) // must not panic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSurfaceBedrockFloor confirms the bottom of the world resolves to bedrock
|
||||||
|
// (the vertical_gradient bedrock_floor rule is the first rule in the tree).
|
||||||
|
func TestSurfaceBedrockFloor(t *testing.T) {
|
||||||
|
rule, err := LoadOverworldSurfaceRule()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load: %v", err)
|
||||||
|
}
|
||||||
|
ctx := &SurfaceContext{
|
||||||
|
X: 0, Y: -64, Z: 0, StoneDepthAbove: 0,
|
||||||
|
SeaLevel: 63, BiomeName: "minecraft:plains", MinY: -64,
|
||||||
|
PreliminarySurface: 70, Rng: rand.New(rand.NewSource(1)),
|
||||||
|
}
|
||||||
|
state, ok := rule.Apply(ctx)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("no rule matched at bedrock floor")
|
||||||
|
}
|
||||||
|
if state != 85 { // bedrock
|
||||||
|
t.Errorf("bedrock floor state = %d, want 85", state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSurfaceBlockIDResolution checks the block-ID table covers the blocks the
|
||||||
|
// overworld surface_rule references, including snowy property variants.
|
||||||
|
func TestSurfaceBlockIDResolution(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
props map[string]string
|
||||||
|
want uint16
|
||||||
|
}{
|
||||||
|
{"minecraft:bedrock", nil, 85},
|
||||||
|
{"minecraft:grass_block", nil, 9},
|
||||||
|
{"minecraft:grass_block", map[string]string{"snowy": "true"}, 8},
|
||||||
|
{"minecraft:mycelium", nil, 8919},
|
||||||
|
{"minecraft:podzol", map[string]string{"snowy": "true"}, 12},
|
||||||
|
{"minecraft:terracotta", nil, 12912},
|
||||||
|
{"minecraft:red_sand", nil, 123},
|
||||||
|
{"minecraft:coarse_dirt", nil, 11},
|
||||||
|
{"minecraft:calcite", nil, 24687},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := surfaceBlockID(c.name, c.props); got != c.want {
|
||||||
|
t.Errorf("surfaceBlockID(%q,%v) = %d, want %d", c.name, c.props, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIsColdBiome confirms the snow-cover predicate recognises cold biomes so
|
||||||
|
// the temperature condition routes snowy biomes to snow.
|
||||||
|
func TestIsColdBiome(t *testing.T) {
|
||||||
|
cold := []string{"minecraft:snowy_plains", "minecraft:frozen_peaks", "minecraft:grove"}
|
||||||
|
for _, b := range cold {
|
||||||
|
if !isColdBiome(b) {
|
||||||
|
t.Errorf("isColdBiome(%q) = false, want true", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warm := []string{"minecraft:desert", "minecraft:plains", "minecraft:badlands"}
|
||||||
|
for _, b := range warm {
|
||||||
|
if isColdBiome(b) {
|
||||||
|
t.Errorf("isColdBiome(%q) = true, want false", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue