Bind the surface rule tree to the world seed
The tree was parsed once, globally, and shared by every world -- so every condition that needs the seed simply did not work. Compiling it per RandomState fixes four of them at once. noise_threshold sampled a per-column random draw and pretended it was "minecraft:surface"; the other six noises it names were unsupported and returned false. Each condition now holds its own seeded noise, sampled once per column into a small cache the way vanilla's LazyXZCondition does. Powder snow, packed ice and ice appear in the dump for the first time; calcite, swamp water windows and gravel patches have their conditions back too. vertical_gradient tapered through a per-column RNG shared with the other rules. Vanilla rolls a positional random at the exact block, from a factory named by the rule. More importantly the anchor decoder read only above_bottom and discarded which kind of anchor it was, so the deepslate rule's absolute 0..8 collapsed onto y=-64 and **no deepslate existed anywhere in the world**. Anchors now carry their kind and resolve against the real height bounds -- which also retires a hardcoded 384 in y_above. Two more stubs land with them: hole is surfaceDepth <= 0 rather than a constant false, and steep reads the neighbouring column heights. steep needs the whole chunk's heightmap, so the column pass is now two passes -- terrain and fluids for all 256 columns, then surface rules -- which is the order vanilla uses anyway (doFill, then buildSurface). Deepslate was also missing from the block-ID table, and an unknown name resolved to 0, which the caller read as "no block" and skipped. So even a correct rule would have placed nothing. Unknown names are now a parse error, deepslate and mud are in the table, and a rule that resolves to air genuinely places air -- the frozen-ocean surface asks for exactly that. Below y=0 is now entirely deepslate, y=1..7 a scatter, above y=8 none.
This commit is contained in:
parent
1083e47211
commit
c19e5f0e4f
9 changed files with 478 additions and 255 deletions
|
|
@ -111,6 +111,20 @@ func main() {
|
|||
countAt(16, 40, "y=16..40")
|
||||
countAt(1, 7, "y=1..7 (transition)")
|
||||
countAt(-64, -1, "y<0 (deepslate)")
|
||||
// The deepslate rule is a vertical_gradient over absolute anchors 0..8:
|
||||
// everything solid below y=0 is deepslate, everything above y=8 is stone,
|
||||
// and the band between them is a scatter. A zero here means the rule is
|
||||
// firing but its block is being dropped, or its anchors are misread.
|
||||
switch {
|
||||
case deepStone["y<0 (deepslate)"] == 0:
|
||||
fmt.Println(" FAIL: no deepslate below y=0")
|
||||
case deepStone["y=16..40"] != 0:
|
||||
fmt.Println(" FAIL: deepslate above the transition band")
|
||||
case deepStone["y=1..7 (transition)"] == 0:
|
||||
fmt.Println(" FAIL: the stone/deepslate transition band is empty")
|
||||
default:
|
||||
fmt.Println(" OK: deepslate below y=0, scattered through y=1..7, none above")
|
||||
}
|
||||
|
||||
// Bedrock floor: y=-64 must be solid bedrock everywhere, y=-63..-59 a
|
||||
// thinning scatter of bedrock over stone/deepslate, and NOTHING in that band
|
||||
|
|
|
|||
52
internal/world/deepslate_verify_test.go
Normal file
52
internal/world/deepslate_verify_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package world
|
||||
|
||||
import "testing"
|
||||
|
||||
// StateDeepslate is minecraft:deepslate with axis=y, the upright default the
|
||||
// surface rule places.
|
||||
const StateDeepslate uint16 = 27924
|
||||
|
||||
// TestDeepslateLayer guards the stone/deepslate boundary. The rule that draws
|
||||
// it is a vertical_gradient over absolute anchors 0 and 8; the anchor decoder
|
||||
// only read above_bottom, so both collapsed onto y=-64 and the whole world was
|
||||
// stone from bedrock to sky. The block name was missing from the ID table too,
|
||||
// so even a firing rule resolved to 0 and was dropped.
|
||||
func TestDeepslateLayer(t *testing.T) {
|
||||
gen := NewVanillaGenerator(12345)
|
||||
deepBelow, stoneBelow := 0, 0
|
||||
deepAbove := 0
|
||||
transition := 0
|
||||
for _, p := range [][2]int32{{0, 0}, {5, -7}, {-13, 21}} {
|
||||
ch := gen(p[0], p[1])
|
||||
for lx := 0; lx < 16; lx++ {
|
||||
for lz := 0; lz < 16; lz++ {
|
||||
for wy := MinY; wy <= 40; wy++ {
|
||||
switch b := ch.GetBlock(lx, wy, lz); {
|
||||
case b == StateDeepslate && wy < 0:
|
||||
deepBelow++
|
||||
case b == StateDeepslate && wy >= 0 && wy < 8:
|
||||
transition++
|
||||
case b == StateDeepslate && wy >= 8:
|
||||
deepAbove++
|
||||
case b == StateStone && wy < 0:
|
||||
stoneBelow++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("deepslate below y=0: %d (stone there: %d), in y=0..7: %d, above y=8: %d",
|
||||
deepBelow, stoneBelow, transition, deepAbove)
|
||||
if deepBelow == 0 {
|
||||
t.Error("no deepslate below y=0")
|
||||
}
|
||||
if stoneBelow != 0 {
|
||||
t.Errorf("%d plain stone blocks survive below y=0; the gradient is not reaching them", stoneBelow)
|
||||
}
|
||||
if deepAbove != 0 {
|
||||
t.Errorf("%d deepslate blocks above y=8; the upper anchor is not holding", deepAbove)
|
||||
}
|
||||
if transition == 0 {
|
||||
t.Error("the y=0..7 stone/deepslate scatter is empty")
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ const dataVersion26 = 4790
|
|||
// first time it ran: chunkAt prefers the store over the generator, so the
|
||||
// already-explored area around spawn keeps its old terrain and every later fix
|
||||
// looks like it did nothing in exactly the place you are standing.
|
||||
const generatorVersion = 4
|
||||
const generatorVersion = 5
|
||||
|
||||
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||
// because it is ours, not part of the vanilla chunk format.
|
||||
|
|
|
|||
|
|
@ -79,8 +79,9 @@ func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPi
|
|||
}
|
||||
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.
|
||||
// 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
|
||||
|
|
@ -93,18 +94,42 @@ func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPi
|
|||
|
||||
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++ {
|
||||
var rule worldgen.SurfaceRule
|
||||
if ruleErr == nil {
|
||||
rule = surfaceRule
|
||||
surfTop[lx][lz], worldSurface[lx][lz], grass[lx][lz] =
|
||||
fillVanillaColumn(od, aq, fluidPicker, 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++ {
|
||||
rng := newColumnRand(baseX+lx, baseZ+lz, int(seed))
|
||||
if ruleErr == nil {
|
||||
applySurfaceRule(od, surfaceRule, sctx, &columns[lx][lz],
|
||||
baseX+lx, baseZ+lz, lx, lz, &worldSurface, biomeName[lx][lz], rng)
|
||||
} else {
|
||||
fillLegacySurface(&columns[lx][lz], surfTop[lx][lz], rng)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
@ -165,13 +190,13 @@ func fillBiomes3D(c *Chunk, od *worldgen.OverworldDensity, s2D [16][16]worldgen.
|
|||
// 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) {
|
||||
func fillVanillaColumn(od *worldgen.OverworldDensity, aq *worldgen.Aquifer, fluidPicker worldgen.FluidPicker, 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 := -1
|
||||
top, worldSurface = -1, MinY-1
|
||||
for i := 0; i < WorldHeight; i++ {
|
||||
cy0 := i / cellHeight
|
||||
fy := float64(i%cellHeight) / cellHeight
|
||||
|
|
@ -186,6 +211,9 @@ func fillVanillaColumn(od *worldgen.OverworldDensity, aq *worldgen.Aquifer, flui
|
|||
if isDefaultBlock {
|
||||
top = i
|
||||
}
|
||||
if state != StateAir {
|
||||
worldSurface = y
|
||||
}
|
||||
}
|
||||
|
||||
topY := MinY + top
|
||||
|
|
@ -194,16 +222,22 @@ func fillVanillaColumn(od *worldgen.OverworldDensity, aq *worldgen.Aquifer, flui
|
|||
const beachBand = 3
|
||||
beach := top >= 0 && topY >= SeaLevel-beachBand && topY <= SeaLevel+1
|
||||
deepWater := top >= 0 && topY < SeaLevel-beachBand
|
||||
return top, worldSurface, top >= 0 && !beach && !deepWater && topY >= SeaLevel
|
||||
}
|
||||
|
||||
// Per-column RNG for the bedrock floor and the bandlands/gradient rules.
|
||||
rng := newColumnRand(wx, wz, int(seed))
|
||||
|
||||
if rule != nil {
|
||||
applySurfaceRule(od, out, wx, wz, SeaLevel, MinY, biomeName, rule, rng)
|
||||
} else {
|
||||
fillLegacySurface(out, top, beach, deepWater, rng)
|
||||
// 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
|
||||
}
|
||||
return top, top >= 0 && !beach && !deepWater && topY >= SeaLevel
|
||||
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:
|
||||
|
|
@ -241,7 +275,7 @@ func substance(aq *worldgen.Aquifer, fluidPicker worldgen.FluidPicker, x, y, z i
|
|||
// 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, out *[WorldHeight]uint16, wx, wz, seaLevel, minY int, biomeName string, rule worldgen.SurfaceRule, rng chunkRand) {
|
||||
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, rng chunkRand) {
|
||||
top := -1
|
||||
for i := WorldHeight - 1; i >= 0; i-- {
|
||||
if out[i] != StateAir {
|
||||
|
|
@ -252,26 +286,21 @@ func applySurfaceRule(od *worldgen.OverworldDensity, out *[WorldHeight]uint16, w
|
|||
if top < 0 {
|
||||
return
|
||||
}
|
||||
// One per-column RNG for all surface rules in this column.
|
||||
colRng := rng.toRand()
|
||||
// Column-constant surface quantities, computed once per column exactly as
|
||||
// SurfaceRules.Context.updateXZ does.
|
||||
// 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)
|
||||
// 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: od.Surface.Noise(wx, wz),
|
||||
SurfaceSecondary: od.Surface.SurfaceSecondary(wx, wz),
|
||||
SurfaceDepth: surfaceDepth,
|
||||
MinSurfaceLevel: od.MinSurfaceLevelAt(wx, wz, surfaceDepth),
|
||||
Rng: colRng,
|
||||
}
|
||||
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)
|
||||
sctx.Rng = rng.toRand()
|
||||
minY := MinY
|
||||
stoneDepthAbove := 0
|
||||
waterHeight := worldgen.NoWaterAbove
|
||||
nextCeilingStoneY := math.MaxInt
|
||||
|
|
@ -309,7 +338,10 @@ func applySurfaceRule(od *worldgen.OverworldDensity, out *[WorldHeight]uint16, w
|
|||
if old != StateStone {
|
||||
continue
|
||||
}
|
||||
if state, ok := rule.Apply(sctx); ok && state != 0 {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -323,10 +355,14 @@ 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
|
||||
// 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, beach, deepWater bool, rng chunkRand) {
|
||||
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]) {
|
||||
|
|
|
|||
|
|
@ -20,80 +20,97 @@ package worldgen
|
|||
|
||||
// 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 {
|
||||
// snowable blocks and the layers property on snow.
|
||||
//
|
||||
// An unknown name is an error, not a fallback. It used to return 0, which the
|
||||
// rule application then read as "no block" and skipped — so a name missing from
|
||||
// this table silently left stone behind. That is exactly how deepslate went
|
||||
// missing from the entire world: the rule fired, resolved to 0, and was dropped.
|
||||
func surfaceBlockID(name string, props map[string]string) (uint16, bool) {
|
||||
switch name {
|
||||
case "minecraft:air":
|
||||
return 0, true
|
||||
case "minecraft:deepslate":
|
||||
// A pillar block; the surface rule always asks for the upright axis.
|
||||
return 27924, true
|
||||
case "minecraft:mud":
|
||||
return 27922, true
|
||||
case "minecraft:brown_terracotta":
|
||||
return 11456, true
|
||||
case "minecraft:red_terracotta":
|
||||
return 11458, true
|
||||
case "minecraft:light_gray_terracotta":
|
||||
return 11452, true
|
||||
case "minecraft:stone":
|
||||
return 1
|
||||
return 1, true
|
||||
case "minecraft:granite":
|
||||
return 2
|
||||
return 2, true
|
||||
case "minecraft:diorite":
|
||||
return 4
|
||||
return 4, true
|
||||
case "minecraft:andesite":
|
||||
return 6
|
||||
return 6, true
|
||||
case "minecraft:grass_block":
|
||||
if props["snowy"] == "true" {
|
||||
return 8
|
||||
return 8, true
|
||||
}
|
||||
return 9
|
||||
return 9, true
|
||||
case "minecraft:dirt":
|
||||
return 10
|
||||
return 10, true
|
||||
case "minecraft:coarse_dirt":
|
||||
return 11
|
||||
return 11, true
|
||||
case "minecraft:podzol":
|
||||
if props["snowy"] == "true" {
|
||||
return 12
|
||||
return 12, true
|
||||
}
|
||||
return 13
|
||||
return 13, true
|
||||
case "minecraft:bedrock":
|
||||
return 85
|
||||
return 85, true
|
||||
case "minecraft:water":
|
||||
return 86
|
||||
return 86, true
|
||||
case "minecraft:sand":
|
||||
return 118
|
||||
return 118, true
|
||||
case "minecraft:red_sand":
|
||||
return 123
|
||||
return 123, true
|
||||
case "minecraft:gravel":
|
||||
return 124
|
||||
return 124, true
|
||||
case "minecraft:sandstone":
|
||||
return 578
|
||||
return 578, true
|
||||
case "minecraft:red_sandstone":
|
||||
return 13247
|
||||
return 13247, true
|
||||
case "minecraft:snow_block":
|
||||
return 6928
|
||||
return 6928, true
|
||||
case "minecraft:snow":
|
||||
// snow has a "layers" property 1..8; default layer 1 = 6919.
|
||||
return 6919
|
||||
return 6919, true
|
||||
case "minecraft:ice":
|
||||
return 6927
|
||||
return 6927, true
|
||||
case "minecraft:packed_ice":
|
||||
return 12914
|
||||
return 12914, true
|
||||
case "minecraft:powder_snow":
|
||||
return 24689
|
||||
return 24689, true
|
||||
case "minecraft:mycelium":
|
||||
if props["snowy"] == "true" {
|
||||
return 8918
|
||||
return 8918, true
|
||||
}
|
||||
return 8919
|
||||
return 8919, true
|
||||
case "minecraft:terracotta":
|
||||
return 12912
|
||||
return 12912, true
|
||||
case "minecraft:white_terracotta":
|
||||
return 11444
|
||||
return 11444, true
|
||||
case "minecraft:orange_terracotta":
|
||||
return 11445
|
||||
return 11445, true
|
||||
case "minecraft:yellow_terracotta":
|
||||
return 11448
|
||||
return 11448, true
|
||||
case "minecraft:calcite":
|
||||
return 24687
|
||||
return 24687, true
|
||||
case "minecraft:tuff":
|
||||
return 23452
|
||||
return 23452, true
|
||||
case "minecraft:dripstone_block":
|
||||
return 27755
|
||||
return 27755, true
|
||||
case "minecraft:moss_block":
|
||||
return 27862
|
||||
return 27862, true
|
||||
case "minecraft:smooth_stone":
|
||||
return 13480
|
||||
return 13480, true
|
||||
}
|
||||
return 0
|
||||
return 0, false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,14 +56,17 @@ type OverworldDensity struct {
|
|||
// rule tree runs.
|
||||
Surface *SurfaceSampler
|
||||
|
||||
surfaceRule *SurfaceRuleSet
|
||||
surfaceRuleErr error
|
||||
|
||||
prelim *levelCache
|
||||
}
|
||||
|
||||
// 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()
|
||||
// SurfaceRule returns the overworld surface rule set, compiled against this
|
||||
// world's seed. A nil rule set (on error) is non-fatal: the generator falls
|
||||
// back to its biome-blind surface heuristics.
|
||||
func (od *OverworldDensity) SurfaceRule() (*SurfaceRuleSet, error) {
|
||||
return od.surfaceRule, od.surfaceRuleErr
|
||||
}
|
||||
|
||||
// LoadOverworldFinalDensity builds the overworld final_density function for the
|
||||
|
|
@ -168,6 +171,12 @@ func LoadOverworldFinalDensity(seed int64) (*OverworldDensity, error) {
|
|||
secondaryNoise: secondaryNoise,
|
||||
positionalRand: l.rs.Positional(),
|
||||
}
|
||||
|
||||
// The rule tree is seed-bound: its noise_threshold conditions sample seeded
|
||||
// noises and its vertical_gradient rolls against a seeded positional
|
||||
// factory. A failure here is reported but not fatal — the generator keeps
|
||||
// going on the fallback heuristics rather than refusing to start.
|
||||
od.surfaceRule, od.surfaceRuleErr = l.loadSurfaceRuleSet(od.MinY, od.Height)
|
||||
return od, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,9 +92,11 @@ func LoadTemplate(path string) (*Template, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
id := surfaceBlockID(name, props)
|
||||
if id == 0 && name != "minecraft:air" {
|
||||
// Fallback to default block ID for the name
|
||||
id, ok := surfaceBlockID(name, props)
|
||||
if !ok {
|
||||
// Structure templates name far more blocks than the
|
||||
// surface rules do; fall back to the broader
|
||||
// default-state table.
|
||||
id = defaultBlockIDs[name]
|
||||
}
|
||||
tmpl.Palette = append(tmpl.Palette, id)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// surface.go implements the vanilla SurfaceRules interpreter: a rule tree that
|
||||
|
|
@ -45,11 +44,8 @@ type SurfaceContext struct {
|
|||
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 is true when the column's neighbours in the chunk differ in height
|
||||
// by four or more blocks (SurfaceRules.SteepMaterialCondition).
|
||||
Steep bool
|
||||
// SurfaceDepth is how thick the biome's surface layers are at this column
|
||||
// (SurfaceSystem.getSurfaceDepth): usually 3, sometimes 0 or less, which is
|
||||
|
|
@ -63,9 +59,14 @@ type SurfaceContext struct {
|
|||
// the interpolated preliminary surface level plus SurfaceDepth less 8.
|
||||
// above_preliminary_surface tests Y against it.
|
||||
MinSurfaceLevel 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 is a per-column deterministic source for the bandlands rule. It is
|
||||
// seeded by the column so results are stable across runs.
|
||||
Rng *rand.Rand
|
||||
|
||||
// noiseValues holds one sample per noise the rule tree's noise_threshold
|
||||
// conditions reference, refreshed once per column by BeginColumn. Vanilla
|
||||
// caches these the same way, through LazyXZCondition.
|
||||
noiseValues []float64
|
||||
}
|
||||
|
||||
// SurfaceRule decides the block at a context. Apply returns ok=false when the
|
||||
|
|
@ -113,19 +114,21 @@ func (r conditionRule) Apply(ctx *SurfaceContext) (uint16, bool) {
|
|||
type bandlandsRule struct{}
|
||||
|
||||
func (bandlandsRule) Apply(ctx *SurfaceContext) (uint16, bool) {
|
||||
orange, _ := surfaceBlockID("minecraft:orange_terracotta", nil)
|
||||
if ctx.Rng == nil {
|
||||
return surfaceBlockID("minecraft:orange_terracotta", nil), true
|
||||
return orange, 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 {
|
||||
white, _ := surfaceBlockID("minecraft:white_terracotta", nil)
|
||||
yellow, _ := surfaceBlockID("minecraft:yellow_terracotta", nil)
|
||||
switch (ctx.Y + ctx.Rng.Intn(7)) % 4 {
|
||||
case 0:
|
||||
return surfaceBlockID("minecraft:white_terracotta", nil), true
|
||||
return white, true
|
||||
case 1, 3:
|
||||
return surfaceBlockID("minecraft:orange_terracotta", nil), true
|
||||
return orange, true
|
||||
default:
|
||||
return surfaceBlockID("minecraft:yellow_terracotta", nil), true
|
||||
return yellow, true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,12 +156,12 @@ 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).
|
||||
// holeTest passes where the surface depth noise came out at or below zero — a
|
||||
// bare patch with no surface layer at all, which is how coarse dirt and gravel
|
||||
// scars appear in the middle of grass.
|
||||
type holeTest struct{}
|
||||
|
||||
func (holeTest) Test(ctx *SurfaceContext) bool { return false }
|
||||
func (holeTest) Test(ctx *SurfaceContext) bool { return ctx.SurfaceDepth <= 0 }
|
||||
|
||||
// waterTest passes when the block is clear of the water above it — either there
|
||||
// is none, or it sits far enough below the water's underside
|
||||
|
|
@ -208,34 +211,21 @@ func isColdBiome(name string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// yAboveTest passes when Y is above an anchor (absolute, above_bottom, or
|
||||
// below_top), with optional surface-depth and stone-depth offsets.
|
||||
// yAboveTest passes when Y clears an anchor, with optional surface-depth and
|
||||
// stone-depth offsets. The anchor is resolved against the world's height bounds
|
||||
// at parse time.
|
||||
type yAboveTest struct {
|
||||
absolute int
|
||||
hasAbsolute bool
|
||||
aboveBottom int
|
||||
hasAboveBottom bool
|
||||
belowTop int
|
||||
hasBelowTop bool
|
||||
anchorY int
|
||||
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
|
||||
y := ctx.Y
|
||||
if t.addStoneDepth {
|
||||
threshold += ctx.StoneDepthAbove
|
||||
y += ctx.StoneDepthAbove
|
||||
}
|
||||
return ctx.Y >= threshold
|
||||
return y >= t.anchorY+ctx.SurfaceDepth*t.surfaceDepthMul
|
||||
}
|
||||
|
||||
// stoneDepthTest passes when the block is within `offset` of the surface it
|
||||
|
|
@ -266,19 +256,22 @@ func (t stoneDepthTest) Test(ctx *SurfaceContext) bool {
|
|||
return depth <= 1+t.offset+surfaceDepth+secondary
|
||||
}
|
||||
|
||||
// noiseThresholdTest passes when the named surface noise is within [min,max].
|
||||
// noiseThresholdTest passes when its noise, sampled once per column at y=0, is
|
||||
// within [min,max]. slot indexes SurfaceContext.noiseValues, which the rule set
|
||||
// refreshes per column.
|
||||
//
|
||||
// Six of the seven noises the overworld tree uses were unsupported and fell
|
||||
// through as false, so calcite on stony peaks, ice and packed ice on frozen
|
||||
// peaks, powder snow, swamp water windows and gravel patches on stony shores
|
||||
// never appeared at all.
|
||||
type noiseThresholdTest struct {
|
||||
min, max float64
|
||||
noise string
|
||||
slot int
|
||||
}
|
||||
|
||||
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
|
||||
v := ctx.noiseValues[t.slot]
|
||||
return v >= t.min && v <= t.max
|
||||
}
|
||||
|
||||
// notTest inverts its inner test.
|
||||
|
|
@ -286,33 +279,30 @@ 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.
|
||||
// verticalGradientTest is the scattered transition between two layers: true
|
||||
// below one anchor, false above another, and in between a per-position coin
|
||||
// flip whose bias falls linearly with height. It draws the bedrock floor and
|
||||
// the stone-to-deepslate boundary.
|
||||
//
|
||||
// The anchors are resolved once at parse time, so this needs the world's height
|
||||
// bounds; the random factory is named by the rule (bedrock_floor, deepslate)
|
||||
// and forked from the world seed, so the same y gets the same answer every
|
||||
// time the chunk regenerates.
|
||||
type verticalGradientTest struct {
|
||||
randomName string
|
||||
trueAtAndBelow int // above_bottom
|
||||
falseAtAndAbove int // above_bottom
|
||||
trueAtAndBelow int
|
||||
falseAtAndAbove int
|
||||
random PositionalRandomFactory
|
||||
}
|
||||
|
||||
func (t verticalGradientTest) Test(ctx *SurfaceContext) bool {
|
||||
loY := ctx.MinY + t.trueAtAndBelow
|
||||
hiY := ctx.MinY + t.falseAtAndAbove
|
||||
switch {
|
||||
case ctx.Y <= loY:
|
||||
if ctx.Y <= t.trueAtAndBelow {
|
||||
return true
|
||||
case ctx.Y >= hiY:
|
||||
}
|
||||
if ctx.Y >= t.falseAtAndAbove {
|
||||
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)
|
||||
probability := mapRange(float64(ctx.Y), float64(t.trueAtAndBelow), float64(t.falseAtAndAbove), 1.0, 0.0)
|
||||
return float64(t.random.At(ctx.X, ctx.Y, ctx.Z).NextFloat()) < probability
|
||||
}
|
||||
|
||||
// abovePreliminarySurfaceTest gates the whole biome surface subtree: below the
|
||||
|
|
@ -325,8 +315,77 @@ func (abovePreliminarySurfaceTest) Test(ctx *SurfaceContext) bool {
|
|||
|
||||
// ---- Parser ------------------------------------------------------------
|
||||
|
||||
// ParseSurfaceRule parses a surface_rule JSON node into a rule tree.
|
||||
func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
|
||||
// SurfaceRuleSet is a compiled surface rule tree together with the seeded
|
||||
// noises and random factories its conditions reference.
|
||||
//
|
||||
// The tree used to be parsed once, globally, and shared by every world: the
|
||||
// conditions that need the seed simply did not work. Binding it to a
|
||||
// RandomState is what lets noise_threshold sample a real noise and
|
||||
// vertical_gradient roll a real per-position coin.
|
||||
type SurfaceRuleSet struct {
|
||||
root SurfaceRule
|
||||
noises []*NormalNoise
|
||||
}
|
||||
|
||||
// NewContext returns a SurfaceContext sized for this rule set's per-column
|
||||
// noise cache. Reuse one per goroutine; BeginColumn refreshes it.
|
||||
func (s *SurfaceRuleSet) NewContext() *SurfaceContext {
|
||||
return &SurfaceContext{noiseValues: make([]float64, len(s.noises))}
|
||||
}
|
||||
|
||||
// BeginColumn samples every noise the tree references at (x, z) and stores the
|
||||
// column coordinates. Vanilla samples these lazily and caches them per column;
|
||||
// sampling all of them up front costs a handful of evaluations per column and
|
||||
// keeps the tree free of hidden state.
|
||||
func (s *SurfaceRuleSet) BeginColumn(ctx *SurfaceContext, x, z int) {
|
||||
ctx.X, ctx.Z = x, z
|
||||
for i, n := range s.noises {
|
||||
ctx.noiseValues[i] = n.GetValue(float64(x), 0, float64(z))
|
||||
}
|
||||
}
|
||||
|
||||
// Apply runs the tree at the context's current position.
|
||||
func (s *SurfaceRuleSet) Apply(ctx *SurfaceContext) (uint16, bool) { return s.root.Apply(ctx) }
|
||||
|
||||
// surfaceParser carries the seed-dependent state a rule tree needs while it is
|
||||
// being built: where to get noises and random factories, and the world's height
|
||||
// bounds for resolving vertical anchors.
|
||||
type surfaceParser struct {
|
||||
loader *Loader
|
||||
minY, height int
|
||||
noises []*NormalNoise
|
||||
noiseSlots map[string]int
|
||||
}
|
||||
|
||||
// noiseSlot returns the per-column cache index for a named noise, loading and
|
||||
// seeding it on first use.
|
||||
func (p *surfaceParser) noiseSlot(name string) (int, error) {
|
||||
if slot, ok := p.noiseSlots[name]; ok {
|
||||
return slot, nil
|
||||
}
|
||||
n, err := p.loader.noiseField(name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
slot := len(p.noises)
|
||||
p.noises = append(p.noises, n)
|
||||
p.noiseSlots[name] = slot
|
||||
return slot, nil
|
||||
}
|
||||
|
||||
// resolveAnchor is VerticalAnchor.resolveY.
|
||||
func (p *surfaceParser) resolveAnchor(a anchorJSON) int {
|
||||
switch a.kind {
|
||||
case anchorAboveBottom:
|
||||
return p.minY + a.value
|
||||
case anchorBelowTop:
|
||||
return p.minY + p.height - 1 - a.value
|
||||
default:
|
||||
return a.value
|
||||
}
|
||||
}
|
||||
|
||||
func (p *surfaceParser) parseRule(raw json.RawMessage) (SurfaceRule, error) {
|
||||
var obj struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
|
@ -344,7 +403,11 @@ func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
|
|||
if err := json.Unmarshal(raw, &b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return blockRule{state: surfaceBlockID(b.Result.Name, b.Result.Properties)}, nil
|
||||
state, ok := surfaceBlockID(b.Result.Name, b.Result.Properties)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("surface: no block-state ID for %q %v", b.Result.Name, b.Result.Properties)
|
||||
}
|
||||
return blockRule{state: state}, nil
|
||||
|
||||
case "minecraft:sequence":
|
||||
var s struct {
|
||||
|
|
@ -355,7 +418,7 @@ func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
|
|||
}
|
||||
rules := make([]SurfaceRule, 0, len(s.Sequence))
|
||||
for _, child := range s.Sequence {
|
||||
r, err := ParseSurfaceRule(child)
|
||||
r, err := p.parseRule(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -371,11 +434,11 @@ func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
|
|||
if err := json.Unmarshal(raw, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
test, err := parseCondition(c.IfTrue)
|
||||
test, err := p.parseCondition(c.IfTrue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
then, err := ParseSurfaceRule(c.Then)
|
||||
then, err := p.parseRule(c.Then)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -388,7 +451,7 @@ func ParseSurfaceRule(raw json.RawMessage) (SurfaceRule, error) {
|
|||
}
|
||||
|
||||
// parseCondition parses an if_true condition node into a ConditionTest.
|
||||
func parseCondition(raw json.RawMessage) (ConditionTest, error) {
|
||||
func (p *surfaceParser) parseCondition(raw json.RawMessage) (ConditionTest, error) {
|
||||
var obj struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
|
@ -446,32 +509,26 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) {
|
|||
if err := json.Unmarshal(raw, &n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return noiseThresholdTest{min: n.Min, max: n.Max, noise: n.Noise}, nil
|
||||
slot, err := p.noiseSlot(n.Noise)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("noise_threshold %q: %w", n.Noise, err)
|
||||
}
|
||||
return noiseThresholdTest{min: n.Min, max: n.Max, slot: slot}, 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"`
|
||||
Anchor anchorJSON `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
|
||||
return yAboveTest{
|
||||
anchorY: p.resolveAnchor(y.Anchor),
|
||||
addStoneDepth: y.AddStoneDepth,
|
||||
surfaceDepthMul: y.SurfaceDepthMul,
|
||||
}, nil
|
||||
|
||||
case "minecraft:not":
|
||||
var n struct {
|
||||
|
|
@ -480,7 +537,7 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) {
|
|||
if err := json.Unmarshal(raw, &n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inner, err := parseCondition(n.Invert)
|
||||
inner, err := p.parseCondition(n.Invert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -488,15 +545,20 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) {
|
|||
|
||||
case "minecraft:vertical_gradient":
|
||||
var v struct {
|
||||
RandomName string `json:"random_name"`
|
||||
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
|
||||
}
|
||||
if v.RandomName == "" {
|
||||
return nil, fmt.Errorf("vertical_gradient: missing random_name")
|
||||
}
|
||||
return verticalGradientTest{
|
||||
trueAtAndBelow: v.TrueAtAndBelow.aboveBottom,
|
||||
falseAtAndAbove: v.FalseAtAndAbove.aboveBottom,
|
||||
trueAtAndBelow: p.resolveAnchor(v.TrueAtAndBelow),
|
||||
falseAtAndAbove: p.resolveAnchor(v.FalseAtAndAbove),
|
||||
random: p.loader.rs.Positional().FromHashOf(v.RandomName).ForkPositional(),
|
||||
}, nil
|
||||
|
||||
case "minecraft:above_preliminary_surface":
|
||||
|
|
@ -505,49 +567,56 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) {
|
|||
return nil, fmt.Errorf("surface: unknown condition type %q", obj.Type)
|
||||
}
|
||||
|
||||
// anchorJSON decodes a {above_bottom|below_top|absolute: N} surface anchor.
|
||||
// anchorJSON decodes a VerticalAnchor: exactly one of absolute, above_bottom or
|
||||
// below_top. Which one it was matters — reading the value without the kind made
|
||||
// every absolute anchor resolve as an offset from the world floor, which is why
|
||||
// the deepslate rule (absolute 0 to 8) collapsed onto y=-64 and never fired.
|
||||
type anchorJSON struct {
|
||||
absolute int
|
||||
aboveBottom int
|
||||
belowTop int
|
||||
kind anchorKind
|
||||
value int
|
||||
}
|
||||
|
||||
type anchorKind int
|
||||
|
||||
const (
|
||||
anchorAbsolute anchorKind = iota
|
||||
anchorAboveBottom
|
||||
anchorBelowTop
|
||||
)
|
||||
|
||||
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"]
|
||||
for key, kind := range map[string]anchorKind{
|
||||
"absolute": anchorAbsolute,
|
||||
"above_bottom": anchorAboveBottom,
|
||||
"below_top": anchorBelowTop,
|
||||
} {
|
||||
if v, ok := m[key]; ok {
|
||||
a.kind, a.value = kind, v
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("surface: anchor has none of absolute/above_bottom/below_top")
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
}
|
||||
// loadSurfaceRuleSet parses the overworld surface_rule tree, binding its
|
||||
// conditions to this loader's seeded RandomState.
|
||||
func (l *Loader) loadSurfaceRuleSet(minY, height int) (*SurfaceRuleSet, error) {
|
||||
var doc struct {
|
||||
SurfaceRule json.RawMessage `json:"surface_rule"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
surfaceRuleErr = err
|
||||
return
|
||||
if err := l.readJSON("data/overworld.json", &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
surfaceRule, surfaceRuleErr = ParseSurfaceRule(doc.SurfaceRule)
|
||||
})
|
||||
return surfaceRule, surfaceRuleErr
|
||||
p := &surfaceParser{loader: l, minY: minY, height: height, noiseSlots: map[string]int{}}
|
||||
root, err := p.parseRule(doc.SurfaceRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SurfaceRuleSet{root: root, noises: p.noises}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,30 @@ import (
|
|||
"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()
|
||||
// loadTestRules compiles the overworld surface rule set at a fixed seed.
|
||||
func loadTestRules(t *testing.T) *SurfaceRuleSet {
|
||||
t.Helper()
|
||||
od, err := LoadOverworldFinalDensity(12345)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadOverworldSurfaceRule: %v", err)
|
||||
t.Fatalf("load overworld density: %v", err)
|
||||
}
|
||||
if rule == nil {
|
||||
t.Fatal("nil surface rule")
|
||||
rules, err := od.SurfaceRule()
|
||||
if err != nil {
|
||||
t.Fatalf("compile surface rule: %v", err)
|
||||
}
|
||||
if rules == nil {
|
||||
t.Fatal("nil surface rule set")
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// TestLoadSurfaceRule confirms the embedded overworld surface_rule parses into
|
||||
// a rule tree without error, and that every noise its noise_threshold
|
||||
// conditions name resolved. Six of the seven used to fall through as false.
|
||||
func TestLoadSurfaceRule(t *testing.T) {
|
||||
rules := loadTestRules(t)
|
||||
if len(rules.noises) != 7 {
|
||||
t.Errorf("rule set references %d noises, want 7", len(rules.noises))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -22,25 +36,24 @@ func TestLoadSurfaceRule(t *testing.T) {
|
|||
// 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)
|
||||
}
|
||||
rules := loadTestRules(t)
|
||||
biomes := []string{
|
||||
"minecraft:plains", "minecraft:desert", "minecraft:forest",
|
||||
"minecraft:badlands", "minecraft:snowy_plains", "minecraft:ocean",
|
||||
"minecraft:mushroom_fields", "minecraft:wooded_badlands",
|
||||
}
|
||||
ctx := rules.NewContext()
|
||||
rules.BeginColumn(ctx, 100, 100)
|
||||
ctx.SeaLevel, ctx.MinY = 63, -64
|
||||
ctx.MinSurfaceLevel, ctx.WaterHeight = 80, NoWaterAbove
|
||||
ctx.SurfaceDepth = 3
|
||||
ctx.Rng = rand.New(rand.NewSource(1))
|
||||
for _, b := range biomes {
|
||||
ctx.BiomeName = b
|
||||
for y := 0; y < 100; y++ {
|
||||
ctx := &SurfaceContext{
|
||||
X: 100, Y: y, Z: 100,
|
||||
StoneDepthAbove: 100 - y, StoneDepthBelow: y + 1,
|
||||
SeaLevel: 63, BiomeName: b, MinY: -64,
|
||||
MinSurfaceLevel: 80, WaterHeight: NoWaterAbove,
|
||||
Rng: rand.New(rand.NewSource(1)),
|
||||
}
|
||||
rule.Apply(ctx) // must not panic
|
||||
ctx.Y = y
|
||||
ctx.StoneDepthAbove, ctx.StoneDepthBelow = 100-y, y+1
|
||||
rules.Apply(ctx) // must not panic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,17 +61,17 @@ func TestSurfaceRuleNoPanic(t *testing.T) {
|
|||
// 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: 1, StoneDepthBelow: 1,
|
||||
SeaLevel: 63, BiomeName: "minecraft:plains", MinY: -64,
|
||||
MinSurfaceLevel: 62, WaterHeight: NoWaterAbove,
|
||||
Rng: rand.New(rand.NewSource(1)),
|
||||
}
|
||||
state, ok := rule.Apply(ctx)
|
||||
rules := loadTestRules(t)
|
||||
ctx := rules.NewContext()
|
||||
rules.BeginColumn(ctx, 0, 0)
|
||||
ctx.Y = -64
|
||||
ctx.StoneDepthAbove, ctx.StoneDepthBelow = 1, 1
|
||||
ctx.SeaLevel, ctx.MinY = 63, -64
|
||||
ctx.BiomeName = "minecraft:plains"
|
||||
ctx.MinSurfaceLevel, ctx.WaterHeight = 62, NoWaterAbove
|
||||
ctx.SurfaceDepth = 3
|
||||
ctx.Rng = rand.New(rand.NewSource(1))
|
||||
state, ok := rules.Apply(ctx)
|
||||
if !ok {
|
||||
t.Fatal("no rule matched at bedrock floor")
|
||||
}
|
||||
|
|
@ -84,12 +97,23 @@ func TestSurfaceBlockIDResolution(t *testing.T) {
|
|||
{"minecraft:red_sand", nil, 123},
|
||||
{"minecraft:coarse_dirt", nil, 11},
|
||||
{"minecraft:calcite", nil, 24687},
|
||||
{"minecraft:deepslate", map[string]string{"axis": "y"}, 27924},
|
||||
{"minecraft:mud", nil, 27922},
|
||||
{"minecraft:air", nil, 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := surfaceBlockID(c.name, c.props); got != c.want {
|
||||
got, ok := surfaceBlockID(c.name, c.props)
|
||||
if !ok {
|
||||
t.Errorf("surfaceBlockID(%q,%v) not in the table", c.name, c.props)
|
||||
continue
|
||||
}
|
||||
if got != c.want {
|
||||
t.Errorf("surfaceBlockID(%q,%v) = %d, want %d", c.name, c.props, got, c.want)
|
||||
}
|
||||
}
|
||||
if _, ok := surfaceBlockID("minecraft:not_a_block", nil); ok {
|
||||
t.Error("surfaceBlockID accepted an unknown name")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsColdBiome confirms the snow-cover predicate recognises cold biomes so
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue