diff --git a/internal/world/orevein_verify_test.go b/internal/world/orevein_verify_test.go new file mode 100644 index 0000000..4a0f99f --- /dev/null +++ b/internal/world/orevein_verify_test.go @@ -0,0 +1,86 @@ +package world + +import "testing" + +// Ore-vein block states, from OreVeinifier.VeinType. Copper's ore is the plain +// stone variant and iron's is the deepslate one; neither switches with depth. +const ( + StateGranite uint16 = 2 + StateDeepslateIronOre uint16 = 132 + StateTuff uint16 = 23452 + StateCopperOre uint16 = 25313 + StateRawIronBlock uint16 = 29577 + StateRawCopperBlock uint16 = 29578 +) + +// TestOreVeins checks the mega-veins the noise router has always described and +// nothing has ever read: copper in granite between y=0 and y=50, iron in tuff +// between y=-60 and y=-8, with a rare raw-ore block inside each. +// +// The Y windows are the sharpest assertion available. A vein's type comes from +// the sign of the veininess noise but its window comes from the type, so a +// single block outside a window means the guard is wrong. +func TestOreVeins(t *testing.T) { + gen := NewVanillaGenerator(12345) + counts := map[uint16]int{} + lowest := map[uint16]int{} + highest := map[uint16]int{} + interesting := []uint16{StateCopperOre, StateRawCopperBlock, StateGranite, + StateDeepslateIronOre, StateRawIronBlock, StateTuff} + isVein := map[uint16]bool{} + for _, id := range interesting { + isVein[id] = true + } + for cx := int32(-60); cx <= 60; cx += 20 { + for cz := int32(-60); cz <= 60; cz += 20 { + ch := gen(cx, cz) + for wy := MinY; wy < 60; wy++ { + for lx := 0; lx < 16; lx++ { + for lz := 0; lz < 16; lz++ { + b := ch.GetBlock(lx, wy, lz) + if !isVein[b] { + continue + } + if counts[b] == 0 { + lowest[b], highest[b] = wy, wy + } + counts[b]++ + lowest[b] = min(lowest[b], wy) + highest[b] = max(highest[b], wy) + } + } + } + } + } + + for _, c := range []struct { + state uint16 + name string + minY, maxY int + wantAtLeast int + }{ + {StateCopperOre, "copper ore", 0, 50, 50}, + {StateGranite, "copper vein filler", 0, 50, 100}, + {StateDeepslateIronOre, "deepslate iron ore", -60, -8, 50}, + {StateTuff, "iron vein filler", -60, -8, 100}, + } { + if counts[c.state] < c.wantAtLeast { + t.Errorf("%s: %d blocks, want at least %d", c.name, counts[c.state], c.wantAtLeast) + continue + } + if lowest[c.state] < c.minY || highest[c.state] > c.maxY { + t.Errorf("%s spans y %d..%d, outside its window %d..%d", + c.name, lowest[c.state], highest[c.state], c.minY, c.maxY) + } + } + t.Logf("copper %d (raw %d) in granite %d; iron %d (raw %d) in tuff %d", + counts[StateCopperOre], counts[StateRawCopperBlock], counts[StateGranite], + counts[StateDeepslateIronOre], counts[StateRawIronBlock], counts[StateTuff]) + + // A raw-ore block replaces an ore block with probability 0.02, so a few + // hundred ore blocks should turn up a handful. Zero means the third draw is + // never reached. + if counts[StateRawCopperBlock]+counts[StateRawIronBlock] == 0 { + t.Error("no raw ore blocks anywhere; the raw-ore roll is unreachable") + } +} diff --git a/internal/world/store.go b/internal/world/store.go index 7146d7d..3e305bc 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 = 9 +const generatorVersion = 10 // 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 e0a1b84..6d1f267 100644 --- a/internal/world/vanilla.go +++ b/internal/world/vanilla.go @@ -31,12 +31,13 @@ func NewVanillaGenerator(seed int64) Generator { panic("world: loading overworld density: " + err.Error()) } fluidPicker := worldgen.OverworldFluidPicker(od.SeaLevel) + veins := worldgen.NewOreVeinifier(od) return func(cx, cz int32) *Chunk { - return generateVanilla(od, fluidPicker, seed, cx, cz) + return generateVanilla(od, fluidPicker, veins, seed, cx, cz) } } -func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, seed int64, cx, cz int32) *Chunk { +func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, seed int64, cx, cz int32) *Chunk { c := NewChunk(cx, cz, BiomePlains) // per-cell biomes override below baseX, baseZ := int(cx)*16, int(cz)*16 @@ -108,7 +109,7 @@ func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPi interp := make([]float64, len(od.Interpolated)) for lz := 0; lz < 16; lz++ { surfTop[lx][lz], worldSurface[lx][lz], grass[lx][lz] = - fillVanillaColumn(od, aq, fluidPicker, grids, interp, &columns[lx][lz], baseX+lx, baseZ+lz, lx, lz) + fillVanillaColumn(od, aq, fluidPicker, veins, grids, interp, &columns[lx][lz], baseX+lx, baseZ+lz, lx, lz) } }(lx) } @@ -189,7 +190,7 @@ 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) (top, worldSurface int, grass bool) { +func fillVanillaColumn(od *worldgen.OverworldDensity, aq *worldgen.Aquifer, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, grids []cornerGrid, interp []float64, out *[WorldHeight]uint16, wx, wz, lx, lz int) (top, worldSurface int, grass bool) { cx0 := lx / cellWidth cz0 := lz / cellWidth fx := float64(lx%cellWidth) / cellWidth @@ -205,9 +206,9 @@ func fillVanillaColumn(od *worldgen.OverworldDensity, aq *worldgen.Aquifer, flui y := MinY + i ctx := worldgen.FunctionContext{X: float64(wx), Y: float64(y), Z: float64(wz)}.WithInterp(interp) density := od.Final.Compute(ctx) - state, isDefaultBlock := substance(aq, fluidPicker, wx, y, wz, density) + state, solid := substance(aq, fluidPicker, veins, ctx, wx, y, wz, density) out[i] = state - if isDefaultBlock { + if solid { top = i } if state != StateAir { @@ -239,23 +240,35 @@ func steepAt(worldSurface *[16][16]int, lx, lz int) bool { return worldSurface[west][lz] >= worldSurface[east][lz]+4 } -// substance resolves one position to the block the terrain pass leaves behind: -// the default block where the density is solid, otherwise whatever the aquifer -// puts there — air, water or lava. The second result says which of the two -// happened, so the caller can track the top solid block without re-testing. -func substance(aq *worldgen.Aquifer, fluidPicker worldgen.FluidPicker, x, y, z int, density float64) (state uint16, isDefaultBlock bool) { +// substance resolves one position to the block the terrain pass leaves behind, +// mirroring vanilla's MaterialRuleList: the aquifer answers first and, where it +// says the position is solid rock, the ore veinifier gets a turn before the +// default block is used. The second result says whether the position ended up +// solid, so the caller can track the top solid block without re-testing. +func substance(aq *worldgen.Aquifer, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, ctx worldgen.FunctionContext, x, y, z int, density float64) (state uint16, solid bool) { if aq == nil { // aquifers_enabled=false: Aquifer.createDisabled, the global fluid rule // with no cells and no barriers. if density > 0 { - return StateStone, true + return veinOrDefault(veins, ctx, x, y, z), true } return fluidPicker(x, y, z).At(y), false } if s, ok := aq.ComputeSubstance(x, y, z, density); ok { return s, false } - return StateStone, true + return veinOrDefault(veins, ctx, x, y, z), true +} + +// veinOrDefault is the tail of the rule list: an ore vein if one reaches here, +// otherwise the settings' default block. +func veinOrDefault(veins *worldgen.OreVeinifier, ctx worldgen.FunctionContext, x, y, z int) uint16 { + if veins != nil { + if s, ok := veins.Calculate(ctx, x, y, z); ok { + return s + } + } + return StateStone } // applySurfaceRule walks the finished column from the top down, applying the diff --git a/internal/worldgen/loader.go b/internal/worldgen/loader.go index 8934bf6..220d544 100644 --- a/internal/worldgen/loader.go +++ b/internal/worldgen/loader.go @@ -50,8 +50,10 @@ type OverworldDensity struct { AquifersEnabled bool OreVeinsEnabled bool - // AquiferRandom places the aquifer cell centres. + // AquiferRandom places the aquifer cell centres; OreRandom rolls the + // per-position ore-vein draws. AquiferRandom PositionalRandomFactory + OreRandom PositionalRandomFactory // Surface samples the noises SurfaceSystem reads per column, before the // rule tree runs. @@ -103,6 +105,7 @@ func LoadOverworldFinalDensity(seed int64) (*OverworldDensity, error) { AquifersEnabled: settings.AquifersEnabled, OreVeinsEnabled: settings.OreVeinsEnabled, AquiferRandom: l.rs.AquiferRandom(), + OreRandom: l.rs.OreRandom(), prelim: newLevelCache(), } diff --git a/internal/worldgen/orevein.go b/internal/worldgen/orevein.go new file mode 100644 index 0000000..dd703ef --- /dev/null +++ b/internal/worldgen/orevein.go @@ -0,0 +1,103 @@ +package worldgen + +import "math" + +// orevein.go ports net.minecraft.world.level.levelgen.OreVeinifier. +// +// Ore veins are not a decoration feature: they run during the density pass, as +// the second entry of the same MaterialRuleList the aquifer heads. Where the +// aquifer says "this position is solid rock", the veinifier gets a chance to +// replace what would have been plain stone with copper or iron and its +// surrounding filler. That is why veins are long branching sheets rather than +// blobs, and why they never open into a cave: a position the aquifer hollowed +// out never reaches this code. + +// Vein constants, as widened from OreVeinifier's float fields. Written out +// rather than computed so the double each float comparison widens to is +// unambiguous. +const ( + veininessThreshold = 0.4000000059604645 // (double)(float)0.4f + edgeRoundoffBegin = 20.0 // an int field, used as a double + maxEdgeRoundoff = -0.2 // a real double literal, not a widened float + minRichness = 0.10000000149011612 // (double)(float)0.1f + maxRichness = 0.30000001192092896 // (double)(float)0.3f + maxRichnessThreshold = 0.6000000238418579 // (double)(float)0.6f + skipOreIfGapNoiseIsBelow = -0.3000000119209290 // (double)(float)-0.3f + + veinSolidness float32 = 0.7 // compared as a float + chanceOfRawOreBlock float32 = 0.02 // compared as a float +) + +// veinType is OreVeinifier.VeinType. Note the asymmetry: copper's ore is the +// plain stone variant while iron's is the deepslate one, and neither switches +// with depth — the block is fixed per type. +type veinType struct { + ore, rawOreBlock, filler uint16 + minY, maxY int +} + +var ( + veinCopper = veinType{ore: 25313, rawOreBlock: 29578, filler: 2, minY: 0, maxY: 50} + veinIron = veinType{ore: 132, rawOreBlock: 29577, filler: 23452, minY: -60, maxY: -8} +) + +// OreVeinifier decides whether a solid position becomes part of an ore vein. +type OreVeinifier struct { + toggle, ridged, gap DensityFunction + random PositionalRandomFactory +} + +// NewOreVeinifier returns nil when the settings or the router leave veins off, +// which the caller reads as "always place the default block". +func NewOreVeinifier(od *OverworldDensity) *OreVeinifier { + if !od.OreVeinsEnabled || od.VeinToggle == nil || od.VeinRidged == nil || od.VeinGap == nil || od.OreRandom == nil { + return nil + } + return &OreVeinifier{toggle: od.VeinToggle, ridged: od.VeinRidged, gap: od.VeinGap, random: od.OreRandom} +} + +// Calculate returns the block a vein places at this position, or ok=false to +// leave the default block. ctx must carry the cell-interpolated values: both +// vein_toggle and vein_ridged are minecraft:interpolated in the datapack, while +// vein_gap is a plain per-block noise. +// +// The three random draws happen in a fixed order and are the whole shape of the +// output; reordering them, or hoisting the vein_gap compute above the second +// draw, changes every vein in the world. +func (o *OreVeinifier) Calculate(ctx FunctionContext, x, y, z int) (uint16, bool) { + veininess := o.toggle.Compute(ctx) + vein := &veinIron + if veininess > 0 { + vein = &veinCopper + } + // The type comes from the sign of the noise but the Y window comes from the + // type, so a position outside its type's band is simply not a vein — which + // is what keeps copper above 0 and iron below -8. + distanceToTop := vein.maxY - y + distanceToBottom := y - vein.minY + if distanceToBottom < 0 || distanceToTop < 0 { + return 0, false + } + edgeDistance := min(distanceToTop, distanceToBottom) + edgeRoundoff := clampedMap(float64(edgeDistance), 0.0, edgeRoundoffBegin, maxEdgeRoundoff, 0.0) + absVeininess := math.Abs(veininess) + if absVeininess+edgeRoundoff < veininessThreshold { + return 0, false + } + + random := o.random.At(x, y, z) + if random.NextFloat() > veinSolidness { + return 0, false + } + if o.ridged.Compute(ctx) >= 0 { + return 0, false + } + richness := clampedMap(absVeininess, veininessThreshold, maxRichnessThreshold, minRichness, maxRichness) + if float64(random.NextFloat()) < richness && o.gap.Compute(ctx) > skipOreIfGapNoiseIsBelow { + if random.NextFloat() < chanceOfRawOreBlock { + return vein.rawOreBlock, true + } + return vein.ore, true + } + return vein.filler, true +}