diff --git a/cmd/gendump/main.go b/cmd/gendump/main.go index 8b78e99..dd6b205 100644 --- a/cmd/gendump/main.go +++ b/cmd/gendump/main.go @@ -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 diff --git a/internal/world/deepslate_verify_test.go b/internal/world/deepslate_verify_test.go new file mode 100644 index 0000000..66e1255 --- /dev/null +++ b/internal/world/deepslate_verify_test.go @@ -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") + } +} diff --git a/internal/world/store.go b/internal/world/store.go index c1496eb..b0a3e77 100644 --- a/internal/world/store.go +++ b/internal/world/store.go @@ -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. diff --git a/internal/world/vanilla.go b/internal/world/vanilla.go index a4bf8d7..23ed4a8 100644 --- a/internal/world/vanilla.go +++ b/internal/world/vanilla.go @@ -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 @@ -92,19 +93,43 @@ 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 grass [16][16]bool // grassy land surface (tree-plantable) + 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 + 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 { - rule = surfaceRule + 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]) { diff --git a/internal/worldgen/blockids.go b/internal/worldgen/blockids.go index 9043744..557f865 100644 --- a/internal/worldgen/blockids.go +++ b/internal/worldgen/blockids.go @@ -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 } diff --git a/internal/worldgen/loader.go b/internal/worldgen/loader.go index ddc6ece..061ca29 100644 --- a/internal/worldgen/loader.go +++ b/internal/worldgen/loader.go @@ -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 } diff --git a/internal/worldgen/structure.go b/internal/worldgen/structure.go index e15d061..75eaa91 100644 --- a/internal/worldgen/structure.go +++ b/internal/worldgen/structure.go @@ -33,7 +33,7 @@ func LoadTemplate(path string) (*Template, error) { if err != nil { return nil, err } - + // NBT files are usually gzipped. var r io.Reader = bytes.NewReader(b) if len(b) >= 2 && b[0] == 0x1f && b[1] == 0x8b { @@ -81,7 +81,7 @@ func LoadTemplate(path string) (*Template, error) { nameTag, _ := stateComp.Get("Name") name := string(nameTag.(nbt.String)) props := make(map[string]string) - + if propTag, ok := stateComp.Get("Properties"); ok { if propComp, ok := propTag.(*nbt.Compound); ok { for _, k := range propComp.Keys() { @@ -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) @@ -112,7 +114,7 @@ func LoadTemplate(path string) (*Template, error) { } stateTag, _ := blockComp.Get("state") stateIdx := int(stateTag.(nbt.Int)) - + var pos [3]int if posTag, ok := blockComp.Get("pos"); ok { if posList, ok := posTag.(nbt.List); ok && len(posList.Elems) == 3 { diff --git a/internal/worldgen/surface.go b/internal/worldgen/surface.go index 96fb841..03bf43d 100644 --- a/internal/worldgen/surface.go +++ b/internal/worldgen/surface.go @@ -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 - addStoneDepth bool - surfaceDepthMul int + 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"` } @@ -413,9 +476,9 @@ func parseCondition(raw json.RawMessage) (ConditionTest, error) { case "minecraft:water": var w struct { - Offset int `json:"offset"` - SurfaceDepthMul int `json:"surface_depth_multiplier"` - AddStoneDepth bool `json:"add_stone_depth"` + 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 @@ -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"` + AddStoneDepth bool `json:"add_stone_depth"` + SurfaceDepthMul int `json:"surface_depth_multiplier"` + 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"] - return nil + 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 - } - 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 +// 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 := l.readJSON("data/overworld.json", &doc); err != nil { + return nil, err + } + 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 } diff --git a/internal/worldgen/surface_test.go b/internal/worldgen/surface_test.go index 97c46af..5fc3f4d 100644 --- a/internal/worldgen/surface_test.go +++ b/internal/worldgen/surface_test.go @@ -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