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:
parent
e0fdddd887
commit
0f76058db6
5 changed files with 220 additions and 15 deletions
86
internal/world/orevein_verify_test.go
Normal file
86
internal/world/orevein_verify_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ const dataVersion26 = 4790
|
||||||
// first time it ran: chunkAt prefers the store over the generator, so the
|
// 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
|
// 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.
|
// 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
|
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||||
// because it is ours, not part of the vanilla chunk format.
|
// because it is ours, not part of the vanilla chunk format.
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,13 @@ func NewVanillaGenerator(seed int64) Generator {
|
||||||
panic("world: loading overworld density: " + err.Error())
|
panic("world: loading overworld density: " + err.Error())
|
||||||
}
|
}
|
||||||
fluidPicker := worldgen.OverworldFluidPicker(od.SeaLevel)
|
fluidPicker := worldgen.OverworldFluidPicker(od.SeaLevel)
|
||||||
|
veins := worldgen.NewOreVeinifier(od)
|
||||||
return func(cx, cz int32) *Chunk {
|
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
|
c := NewChunk(cx, cz, BiomePlains) // per-cell biomes override below
|
||||||
baseX, baseZ := int(cx)*16, int(cz)*16
|
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))
|
interp := make([]float64, len(od.Interpolated))
|
||||||
for lz := 0; lz < 16; lz++ {
|
for lz := 0; lz < 16; lz++ {
|
||||||
surfTop[lx][lz], worldSurface[lx][lz], grass[lx][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)
|
}(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
|
// 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
|
// way round is what forced the old unconditional "flood everything under sea
|
||||||
// level" pass, which left every cave below y=63 underwater.
|
// 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
|
cx0 := lx / cellWidth
|
||||||
cz0 := lz / cellWidth
|
cz0 := lz / cellWidth
|
||||||
fx := float64(lx%cellWidth) / cellWidth
|
fx := float64(lx%cellWidth) / cellWidth
|
||||||
|
|
@ -205,9 +206,9 @@ func fillVanillaColumn(od *worldgen.OverworldDensity, aq *worldgen.Aquifer, flui
|
||||||
y := MinY + i
|
y := MinY + i
|
||||||
ctx := worldgen.FunctionContext{X: float64(wx), Y: float64(y), Z: float64(wz)}.WithInterp(interp)
|
ctx := worldgen.FunctionContext{X: float64(wx), Y: float64(y), Z: float64(wz)}.WithInterp(interp)
|
||||||
density := od.Final.Compute(ctx)
|
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
|
out[i] = state
|
||||||
if isDefaultBlock {
|
if solid {
|
||||||
top = i
|
top = i
|
||||||
}
|
}
|
||||||
if state != StateAir {
|
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
|
return worldSurface[west][lz] >= worldSurface[east][lz]+4
|
||||||
}
|
}
|
||||||
|
|
||||||
// substance resolves one position to the block the terrain pass leaves behind:
|
// substance resolves one position to the block the terrain pass leaves behind,
|
||||||
// the default block where the density is solid, otherwise whatever the aquifer
|
// mirroring vanilla's MaterialRuleList: the aquifer answers first and, where it
|
||||||
// puts there — air, water or lava. The second result says which of the two
|
// says the position is solid rock, the ore veinifier gets a turn before the
|
||||||
// happened, so the caller can track the top solid block without re-testing.
|
// default block is used. The second result says whether the position ended up
|
||||||
func substance(aq *worldgen.Aquifer, fluidPicker worldgen.FluidPicker, x, y, z int, density float64) (state uint16, isDefaultBlock bool) {
|
// 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 {
|
if aq == nil {
|
||||||
// aquifers_enabled=false: Aquifer.createDisabled, the global fluid rule
|
// aquifers_enabled=false: Aquifer.createDisabled, the global fluid rule
|
||||||
// with no cells and no barriers.
|
// with no cells and no barriers.
|
||||||
if density > 0 {
|
if density > 0 {
|
||||||
return StateStone, true
|
return veinOrDefault(veins, ctx, x, y, z), true
|
||||||
}
|
}
|
||||||
return fluidPicker(x, y, z).At(y), false
|
return fluidPicker(x, y, z).At(y), false
|
||||||
}
|
}
|
||||||
if s, ok := aq.ComputeSubstance(x, y, z, density); ok {
|
if s, ok := aq.ComputeSubstance(x, y, z, density); ok {
|
||||||
return s, false
|
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
|
// applySurfaceRule walks the finished column from the top down, applying the
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,10 @@ type OverworldDensity struct {
|
||||||
AquifersEnabled bool
|
AquifersEnabled bool
|
||||||
OreVeinsEnabled 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
|
AquiferRandom PositionalRandomFactory
|
||||||
|
OreRandom PositionalRandomFactory
|
||||||
|
|
||||||
// Surface samples the noises SurfaceSystem reads per column, before the
|
// Surface samples the noises SurfaceSystem reads per column, before the
|
||||||
// rule tree runs.
|
// rule tree runs.
|
||||||
|
|
@ -103,6 +105,7 @@ func LoadOverworldFinalDensity(seed int64) (*OverworldDensity, error) {
|
||||||
AquifersEnabled: settings.AquifersEnabled,
|
AquifersEnabled: settings.AquifersEnabled,
|
||||||
OreVeinsEnabled: settings.OreVeinsEnabled,
|
OreVeinsEnabled: settings.OreVeinsEnabled,
|
||||||
AquiferRandom: l.rs.AquiferRandom(),
|
AquiferRandom: l.rs.AquiferRandom(),
|
||||||
|
OreRandom: l.rs.OreRandom(),
|
||||||
prelim: newLevelCache(),
|
prelim: newLevelCache(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
103
internal/worldgen/orevein.go
Normal file
103
internal/worldgen/orevein.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue