Ore veins from the noise router

The router has carried vein_toggle, vein_ridged and vein_gap since the whole of
it was parsed, and nothing read them. So the copper and iron mega-veins -- the
long branching sheets that run through the deepslate and the copper band, not
the small scattered ore blobs -- did not exist.

OreVeinifier is not a decoration feature. It is the second entry of the same
MaterialRuleList the aquifer heads: the aquifer answers first, and only where it
says the position is solid rock does the veinifier get a turn at what would
otherwise be plain stone. That is why a vein never opens into a cave, and it is
why this lands in the density pass rather than in decorate.

Three random draws per position, in a fixed order, from a factory hashed off
"minecraft:ore": solidness, then richness, then the rare raw-ore block.
Reordering them or hoisting the vein_gap compute above the second draw would
change every vein in the world, so the code follows the bytecode's order rather
than the one that reads better.

The Y windows are the sharp edge worth knowing about: a vein's type comes from
the sign of the veininess noise, but its window comes from the type, so
veininess <= 0 anywhere outside -60..-8 is simply not a vein. Over 49 sampled
chunks copper lands in 0..49 and iron in -57..-8, and raw ore comes out at 1.9%
of ore blocks against vanilla's 2%.
This commit is contained in:
Master290 2026-07-27 03:48:48 +03:00
parent e0fdddd887
commit 0f76058db6
5 changed files with 220 additions and 15 deletions

View file

@ -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")
}
}

View file

@ -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.

View file

@ -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