Real badlands clay bands and a real biome temperature table
The bandlands rule cycled four terracotta colours off a per-column random draw. Vanilla generates a 192-entry band table once per world, from a random source named clay_bands, and reads it at the block's height shifted by the clay_bands_offset noise. Brown, red and light grey terracotta were never placed anywhere; the stripes were the wrong thickness and did not line up between neighbouring columns. All seven colours now appear. The temperature condition matched a hand-written list of eleven biome names. Replacing it with the temperature field read out of the jar's 65 biome JSONs fixes one of them: deep_frozen_ocean reads cold by name but its base temperature is 0.5, so vanilla does not freeze it. taiga and the pine taigas were the other way round -- excluded by name, and correctly so, but by coincidence rather than by data. Two parts of the vanilla calculation are left out and documented where they belong: the height adjustment that cools peaks, and the "frozen" modifier that warms scattered patches of frozen ocean. Both need PerlinSimplexNoise. Neither is reachable from the overworld tree in a way that shows: the single condition that consults temperature sits under a frozen_ocean biome check, below a water check, and decides whether a hole in the ocean floor ices over. The snowy mountain tops come from biome selection, not from here -- which is not what the plan for this commit assumed. The per-column *rand.Rand threaded through SurfaceContext goes away with the old bandlands rule; nothing needs it now that vertical_gradient rolls positionally.
This commit is contained in:
parent
c19e5f0e4f
commit
3a255b52e1
8 changed files with 289 additions and 68 deletions
|
|
@ -302,6 +302,48 @@ func main() {
|
||||||
fmt.Printf(" OK: %d of %d grass columns carry 2+ blocks of dirt\n", banded, allGrass)
|
fmt.Printf(" OK: %d of %d grass columns carry 2+ blocks of dirt\n", banded, allGrass)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Badlands banding: the clay band table is 192 entries of seven terracotta
|
||||||
|
// colours. The stand-in it replaced cycled four, so brown, red and light
|
||||||
|
// grey never appeared anywhere in the world.
|
||||||
|
fmt.Println("\n=== Badlands clay bands (expect several terracotta colours down a column) ===")
|
||||||
|
terracottas := map[uint16]string{
|
||||||
|
12912: "terracotta", 11444: "white", 11445: "orange", 11448: "yellow",
|
||||||
|
11452: "light_gray", 11456: "brown", 11458: "red",
|
||||||
|
}
|
||||||
|
seenBands := map[uint16]int{}
|
||||||
|
badlandsCols := 0
|
||||||
|
for cx := int32(-300); cx < 300 && badlandsCols < 8; cx += 7 {
|
||||||
|
for cz := int32(-300); cz < 300 && badlandsCols < 8; cz += 7 {
|
||||||
|
name := world.BiomeNameAt(od, int(cx)*16+8, int(cz)*16+8)
|
||||||
|
if name != "minecraft:badlands" && name != "minecraft:eroded_badlands" && name != "minecraft:wooded_badlands" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ch := gen(cx, cz)
|
||||||
|
for lx := 0; lx < 16; lx += 4 {
|
||||||
|
for lz := 0; lz < 16; lz += 4 {
|
||||||
|
for wy := world.MinY + world.WorldHeight - 1; wy >= world.MinY; wy-- {
|
||||||
|
if _, isBand := terracottas[ch.GetBlock(lx, wy, lz)]; isBand {
|
||||||
|
seenBands[ch.GetBlock(lx, wy, lz)]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
badlandsCols++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if badlandsCols == 0 {
|
||||||
|
fmt.Println(" (no badlands in the scan area)")
|
||||||
|
} else {
|
||||||
|
for id, label := range terracottas {
|
||||||
|
fmt.Printf(" %-11s %d\n", label, seenBands[id])
|
||||||
|
}
|
||||||
|
if len(seenBands) < 6 {
|
||||||
|
fmt.Printf(" FAIL: only %d of 7 terracotta colours placed\n", len(seenBands))
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" OK: %d of 7 terracotta colours across %d badlands chunks\n", len(seenBands), badlandsCols)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Cross-section at chunk (0,0): column x=8, over full Y, ASCII.
|
// 2) Cross-section at chunk (0,0): column x=8, over full Y, ASCII.
|
||||||
fmt.Println("\n=== Cross-section chunk(0,0) z=8, x=0..15 (side view, top 96 blocks near surface) ===")
|
fmt.Println("\n=== Cross-section chunk(0,0) z=8, x=0..15 (side view, top 96 blocks near surface) ===")
|
||||||
c := gen(0, 0)
|
c := gen(0, 0)
|
||||||
|
|
|
||||||
|
|
@ -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 = 5
|
const generatorVersion = 6
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
|
||||||
|
|
@ -123,13 +123,12 @@ func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPi
|
||||||
sctx = surfaceRule.NewContext()
|
sctx = surfaceRule.NewContext()
|
||||||
}
|
}
|
||||||
for lz := 0; lz < 16; lz++ {
|
for lz := 0; lz < 16; lz++ {
|
||||||
rng := newColumnRand(baseX+lx, baseZ+lz, int(seed))
|
|
||||||
if ruleErr == nil {
|
if ruleErr == nil {
|
||||||
applySurfaceRule(od, surfaceRule, sctx, &columns[lx][lz],
|
applySurfaceRule(od, surfaceRule, sctx, &columns[lx][lz],
|
||||||
baseX+lx, baseZ+lz, lx, lz, &worldSurface, biomeName[lx][lz], rng)
|
baseX+lx, baseZ+lz, lx, lz, &worldSurface, biomeName[lx][lz])
|
||||||
} else {
|
continue
|
||||||
fillLegacySurface(&columns[lx][lz], surfTop[lx][lz], rng)
|
|
||||||
}
|
}
|
||||||
|
fillLegacySurface(&columns[lx][lz], surfTop[lx][lz], newColumnRand(baseX+lx, baseZ+lz, int(seed)))
|
||||||
}
|
}
|
||||||
}(lx)
|
}(lx)
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +274,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
|
// One *rand.Rand is created per column (not per block) — bandlands/gradient
|
||||||
// consume from it sequentially, which is correct because vanilla seeds those
|
// consume from it sequentially, which is correct because vanilla seeds those
|
||||||
// per-column too. This avoids ~98k rand.New allocations per chunk.
|
// per-column too. This avoids ~98k rand.New allocations per chunk.
|
||||||
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) {
|
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) {
|
||||||
top := -1
|
top := -1
|
||||||
for i := WorldHeight - 1; i >= 0; i-- {
|
for i := WorldHeight - 1; i >= 0; i-- {
|
||||||
if out[i] != StateAir {
|
if out[i] != StateAir {
|
||||||
|
|
@ -299,7 +298,6 @@ func applySurfaceRule(od *worldgen.OverworldDensity, rules *worldgen.SurfaceRule
|
||||||
sctx.SurfaceDepth = surfaceDepth
|
sctx.SurfaceDepth = surfaceDepth
|
||||||
sctx.MinSurfaceLevel = od.MinSurfaceLevelAt(wx, wz, surfaceDepth)
|
sctx.MinSurfaceLevel = od.MinSurfaceLevelAt(wx, wz, surfaceDepth)
|
||||||
sctx.Steep = steepAt(worldSurface, lx, lz)
|
sctx.Steep = steepAt(worldSurface, lx, lz)
|
||||||
sctx.Rng = rng.toRand()
|
|
||||||
minY := MinY
|
minY := MinY
|
||||||
stoneDepthAbove := 0
|
stoneDepthAbove := 0
|
||||||
waterHeight := worldgen.NoWaterAbove
|
waterHeight := worldgen.NoWaterAbove
|
||||||
|
|
|
||||||
87
internal/worldgen/bandlands.go
Normal file
87
internal/worldgen/bandlands.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package worldgen
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
// bandlands.go ports SurfaceSystem's badlands clay banding: a 192-entry table
|
||||||
|
// of terracotta colours generated once per world, indexed by height plus a
|
||||||
|
// noise offset. It is what gives badlands their horizontal stripes.
|
||||||
|
//
|
||||||
|
// The previous stand-in cycled four colours off a per-column random draw, which
|
||||||
|
// produced stripes of the wrong thickness in the wrong places and never used
|
||||||
|
// brown, red or light grey at all.
|
||||||
|
|
||||||
|
const clayBandCount = 192
|
||||||
|
|
||||||
|
// clayBands is the generated band table plus the noise that shifts it
|
||||||
|
// horizontally.
|
||||||
|
type clayBands struct {
|
||||||
|
bands [clayBandCount]uint16
|
||||||
|
offset *NormalNoise
|
||||||
|
}
|
||||||
|
|
||||||
|
// bandAt is SurfaceSystem.getBand.
|
||||||
|
func (c *clayBands) bandAt(x, y, z int) uint16 {
|
||||||
|
// Math.round, which is floor(v+0.5) — not Go's round-half-away-from-zero.
|
||||||
|
shift := int(math.Floor(c.offset.GetValue(float64(x), 0, float64(z))*4.0 + 0.5))
|
||||||
|
return c.bands[((y+shift)%clayBandCount+clayBandCount)%clayBandCount]
|
||||||
|
}
|
||||||
|
|
||||||
|
// newClayBands is SurfaceSystem.generateBands: terracotta everywhere, then
|
||||||
|
// orange stripes at random intervals, then runs of yellow, brown and red, then
|
||||||
|
// white bands flanked by light grey.
|
||||||
|
func newClayBands(random RandomSource, offset *NormalNoise) *clayBands {
|
||||||
|
c := &clayBands{offset: offset}
|
||||||
|
terracotta, _ := surfaceBlockID("minecraft:terracotta", nil)
|
||||||
|
orange, _ := surfaceBlockID("minecraft:orange_terracotta", nil)
|
||||||
|
yellow, _ := surfaceBlockID("minecraft:yellow_terracotta", nil)
|
||||||
|
brown, _ := surfaceBlockID("minecraft:brown_terracotta", nil)
|
||||||
|
red, _ := surfaceBlockID("minecraft:red_terracotta", nil)
|
||||||
|
white, _ := surfaceBlockID("minecraft:white_terracotta", nil)
|
||||||
|
lightGray, _ := surfaceBlockID("minecraft:light_gray_terracotta", nil)
|
||||||
|
|
||||||
|
for i := range c.bands {
|
||||||
|
c.bands[i] = terracotta
|
||||||
|
}
|
||||||
|
// The stride is added to the loop variable, so the ++ at the end of each
|
||||||
|
// iteration is part of the spacing — as it is in vanilla.
|
||||||
|
for i := 0; i < clayBandCount; i++ {
|
||||||
|
if i += int(random.NextIntN(5)) + 1; i >= clayBandCount {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.bands[i] = orange
|
||||||
|
}
|
||||||
|
c.makeBands(random, 1, yellow)
|
||||||
|
c.makeBands(random, 2, brown)
|
||||||
|
c.makeBands(random, 1, red)
|
||||||
|
|
||||||
|
whiteBandCount := nextIntBetweenInclusive(random, 9, 15)
|
||||||
|
for i, start := 0, 0; i < whiteBandCount && start < clayBandCount; i, start = i+1, start+int(random.NextIntN(16))+4 {
|
||||||
|
c.bands[start] = white
|
||||||
|
if start-1 > 0 && random.NextBoolean() {
|
||||||
|
c.bands[start-1] = lightGray
|
||||||
|
}
|
||||||
|
if start+1 >= clayBandCount || !random.NextBoolean() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c.bands[start+1] = lightGray
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeBands is SurfaceSystem.makeBands: six to fifteen runs of one colour, each
|
||||||
|
// a few bands wide, dropped at random offsets.
|
||||||
|
func (c *clayBands) makeBands(random RandomSource, baseWidth int, state uint16) {
|
||||||
|
bandCount := nextIntBetweenInclusive(random, 6, 15)
|
||||||
|
for i := 0; i < bandCount; i++ {
|
||||||
|
width := baseWidth + int(random.NextIntN(3))
|
||||||
|
start := int(random.NextIntN(clayBandCount))
|
||||||
|
for p := 0; start+p < clayBandCount && p < width; p++ {
|
||||||
|
c.bands[start+p] = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextIntBetweenInclusive is RandomSource.nextIntBetweenInclusive.
|
||||||
|
func nextIntBetweenInclusive(random RandomSource, lo, hi int) int {
|
||||||
|
return lo + int(random.NextIntN(int32(hi-lo+1)))
|
||||||
|
}
|
||||||
99
internal/worldgen/biome_temperature.go
Normal file
99
internal/worldgen/biome_temperature.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package worldgen
|
||||||
|
|
||||||
|
// biome_temperature.go holds each overworld biome's base temperature, extracted
|
||||||
|
// verbatim from the "temperature" field of data/minecraft/worldgen/biome/*.json
|
||||||
|
// inside the 26.1.2 server jar. It replaces a hand-written list of biome names
|
||||||
|
// that guessed at which ones were cold.
|
||||||
|
//
|
||||||
|
// The surface rule tree consults it through exactly one condition
|
||||||
|
// (minecraft:temperature), which decides whether a hole in a frozen ocean floor
|
||||||
|
// freezes over.
|
||||||
|
|
||||||
|
// biomeTemperature maps a biome to Biome.getBaseTemperature().
|
||||||
|
var biomeTemperature = map[string]float32{
|
||||||
|
"minecraft:badlands": 2.0,
|
||||||
|
"minecraft:bamboo_jungle": 0.95,
|
||||||
|
"minecraft:basalt_deltas": 2.0,
|
||||||
|
"minecraft:beach": 0.8,
|
||||||
|
"minecraft:birch_forest": 0.6,
|
||||||
|
"minecraft:cherry_grove": 0.5,
|
||||||
|
"minecraft:cold_ocean": 0.5,
|
||||||
|
"minecraft:crimson_forest": 2.0,
|
||||||
|
"minecraft:dark_forest": 0.7,
|
||||||
|
"minecraft:deep_cold_ocean": 0.5,
|
||||||
|
"minecraft:deep_dark": 0.8,
|
||||||
|
"minecraft:deep_frozen_ocean": 0.5, // temperature_modifier: frozen
|
||||||
|
"minecraft:deep_lukewarm_ocean": 0.5,
|
||||||
|
"minecraft:deep_ocean": 0.5,
|
||||||
|
"minecraft:desert": 2.0,
|
||||||
|
"minecraft:dripstone_caves": 0.8,
|
||||||
|
"minecraft:end_barrens": 0.5,
|
||||||
|
"minecraft:end_highlands": 0.5,
|
||||||
|
"minecraft:end_midlands": 0.5,
|
||||||
|
"minecraft:eroded_badlands": 2.0,
|
||||||
|
"minecraft:flower_forest": 0.7,
|
||||||
|
"minecraft:forest": 0.7,
|
||||||
|
"minecraft:frozen_ocean": 0.0, // temperature_modifier: frozen
|
||||||
|
"minecraft:frozen_peaks": -0.7,
|
||||||
|
"minecraft:frozen_river": 0.0,
|
||||||
|
"minecraft:grove": -0.2,
|
||||||
|
"minecraft:ice_spikes": 0.0,
|
||||||
|
"minecraft:jagged_peaks": -0.7,
|
||||||
|
"minecraft:jungle": 0.95,
|
||||||
|
"minecraft:lukewarm_ocean": 0.5,
|
||||||
|
"minecraft:lush_caves": 0.5,
|
||||||
|
"minecraft:mangrove_swamp": 0.8,
|
||||||
|
"minecraft:meadow": 0.5,
|
||||||
|
"minecraft:mushroom_fields": 0.9,
|
||||||
|
"minecraft:nether_wastes": 2.0,
|
||||||
|
"minecraft:ocean": 0.5,
|
||||||
|
"minecraft:old_growth_birch_forest": 0.6,
|
||||||
|
"minecraft:old_growth_pine_taiga": 0.3,
|
||||||
|
"minecraft:old_growth_spruce_taiga": 0.25,
|
||||||
|
"minecraft:pale_garden": 0.7,
|
||||||
|
"minecraft:plains": 0.8,
|
||||||
|
"minecraft:river": 0.5,
|
||||||
|
"minecraft:savanna": 2.0,
|
||||||
|
"minecraft:savanna_plateau": 2.0,
|
||||||
|
"minecraft:small_end_islands": 0.5,
|
||||||
|
"minecraft:snowy_beach": 0.05,
|
||||||
|
"minecraft:snowy_plains": 0.0,
|
||||||
|
"minecraft:snowy_slopes": -0.3,
|
||||||
|
"minecraft:snowy_taiga": -0.5,
|
||||||
|
"minecraft:soul_sand_valley": 2.0,
|
||||||
|
"minecraft:sparse_jungle": 0.95,
|
||||||
|
"minecraft:stony_peaks": 1.0,
|
||||||
|
"minecraft:stony_shore": 0.2,
|
||||||
|
"minecraft:sunflower_plains": 0.8,
|
||||||
|
"minecraft:swamp": 0.8,
|
||||||
|
"minecraft:taiga": 0.25,
|
||||||
|
"minecraft:the_end": 0.5,
|
||||||
|
"minecraft:the_void": 0.5,
|
||||||
|
"minecraft:warm_ocean": 0.5,
|
||||||
|
"minecraft:warped_forest": 2.0,
|
||||||
|
"minecraft:windswept_forest": 0.2,
|
||||||
|
"minecraft:windswept_gravelly_hills": 0.2,
|
||||||
|
"minecraft:windswept_hills": 0.2,
|
||||||
|
"minecraft:windswept_savanna": 2.0,
|
||||||
|
"minecraft:wooded_badlands": 2.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// coldEnoughToSnow is Biome.coldEnoughToSnow: below 0.15 the biome gets snow
|
||||||
|
// and ice rather than rain.
|
||||||
|
//
|
||||||
|
// Two parts of vanilla's calculation are not reproduced, both because they need
|
||||||
|
// PerlinSimplexNoise, which we do not have:
|
||||||
|
//
|
||||||
|
// - the height adjustment, which cools a column above sea level + 17 and so
|
||||||
|
// puts snow on peaks in otherwise temperate biomes. No rule in the
|
||||||
|
// overworld tree reaches this condition above that height.
|
||||||
|
// - the "frozen" temperature modifier, which warms scattered patches of
|
||||||
|
// frozen_ocean and deep_frozen_ocean. Its absence makes frozen-ocean ice
|
||||||
|
// uniform where vanilla leaves open water in it.
|
||||||
|
//
|
||||||
|
// An unknown biome reads as warm, which is the safe direction: it leaves the
|
||||||
|
// default block alone rather than icing something over.
|
||||||
|
func coldEnoughToSnow(biome string) bool {
|
||||||
|
temperature, ok := biomeTemperature[biome]
|
||||||
|
return ok && temperature < 0.15
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ type Loader struct {
|
||||||
rs *RandomState
|
rs *RandomState
|
||||||
dfCache map[string]DensityFunction
|
dfCache map[string]DensityFunction
|
||||||
interpolated []*Interpolated
|
interpolated []*Interpolated
|
||||||
|
bands *clayBands
|
||||||
}
|
}
|
||||||
|
|
||||||
// OverworldDensity is the parsed final_density plus the set of Interpolated
|
// OverworldDensity is the parsed final_density plus the set of Interpolated
|
||||||
|
|
@ -446,3 +447,17 @@ func (l *Loader) parseSplineValue(v any) (DensityFunction, error) {
|
||||||
}
|
}
|
||||||
return l.parseNode(v)
|
return l.parseNode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clayBands builds the world's badlands band table on first use. It is seeded
|
||||||
|
// from the root positional factory hashed by name, as SurfaceSystem does.
|
||||||
|
func (l *Loader) clayBands() (*clayBands, error) {
|
||||||
|
if l.bands != nil {
|
||||||
|
return l.bands, nil
|
||||||
|
}
|
||||||
|
offset, err := l.noiseField("minecraft:clay_bands_offset")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("clay_bands_offset noise: %w", err)
|
||||||
|
}
|
||||||
|
l.bands = newClayBands(l.rs.Positional().FromHashOf("minecraft:clay_bands"), offset)
|
||||||
|
return l.bands, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// surface.go implements the vanilla SurfaceRules interpreter: a rule tree that
|
// surface.go implements the vanilla SurfaceRules interpreter: a rule tree that
|
||||||
|
|
@ -59,10 +58,6 @@ type SurfaceContext struct {
|
||||||
// the interpolated preliminary surface level plus SurfaceDepth less 8.
|
// the interpolated preliminary surface level plus SurfaceDepth less 8.
|
||||||
// above_preliminary_surface tests Y against it.
|
// above_preliminary_surface tests Y against it.
|
||||||
MinSurfaceLevel int
|
MinSurfaceLevel int
|
||||||
// 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
|
// noiseValues holds one sample per noise the rule tree's noise_threshold
|
||||||
// conditions reference, refreshed once per column by BeginColumn. Vanilla
|
// conditions reference, refreshed once per column by BeginColumn. Vanilla
|
||||||
// caches these the same way, through LazyXZCondition.
|
// caches these the same way, through LazyXZCondition.
|
||||||
|
|
@ -107,29 +102,13 @@ func (r conditionRule) Apply(ctx *SurfaceContext) (uint16, bool) {
|
||||||
return r.then.Apply(ctx)
|
return r.then.Apply(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// bandlandsRule reproduces the vanilla badlands coloured-clay banding: a
|
// bandlandsRule reads the world's clay band table at the block's height,
|
||||||
// deterministic per-column pattern of terracotta colours at certain Y bands. We
|
// shifted horizontally by the clay_bands_offset noise. It is what stripes the
|
||||||
// approximate the 8-band rotation using the column RNG; exact band geometry is
|
// badlands.
|
||||||
// captured well enough to read as badlands.
|
type bandlandsRule struct{ bands *clayBands }
|
||||||
type bandlandsRule struct{}
|
|
||||||
|
|
||||||
func (bandlandsRule) Apply(ctx *SurfaceContext) (uint16, bool) {
|
func (r bandlandsRule) Apply(ctx *SurfaceContext) (uint16, bool) {
|
||||||
orange, _ := surfaceBlockID("minecraft:orange_terracotta", nil)
|
return r.bands.bandAt(ctx.X, ctx.Y, ctx.Z), true
|
||||||
if ctx.Rng == nil {
|
|
||||||
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.
|
|
||||||
white, _ := surfaceBlockID("minecraft:white_terracotta", nil)
|
|
||||||
yellow, _ := surfaceBlockID("minecraft:yellow_terracotta", nil)
|
|
||||||
switch (ctx.Y + ctx.Rng.Intn(7)) % 4 {
|
|
||||||
case 0:
|
|
||||||
return white, true
|
|
||||||
case 1, 3:
|
|
||||||
return orange, true
|
|
||||||
default:
|
|
||||||
return yellow, true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Condition tests ---------------------------------------------------
|
// ---- Condition tests ---------------------------------------------------
|
||||||
|
|
@ -188,27 +167,13 @@ func (t waterTest) Test(ctx *SurfaceContext) bool {
|
||||||
return y >= ctx.WaterHeight+t.offset+ctx.SurfaceDepth*t.surfaceDepthMul
|
return y >= ctx.WaterHeight+t.offset+ctx.SurfaceDepth*t.surfaceDepthMul
|
||||||
}
|
}
|
||||||
|
|
||||||
// temperatureTest passes when the (column) temperature is below freezing — the
|
// temperatureTest passes where the biome is cold enough for snow and ice
|
||||||
// snow-at-height rule. We fold temperature into the biome name (snowy_*
|
// rather than rain. The overworld tree uses it once, to freeze holes in a
|
||||||
// biomes) rather than sampling the temperature noise, so pass for cold biomes.
|
// frozen ocean floor.
|
||||||
type temperatureTest struct{}
|
type temperatureTest struct{}
|
||||||
|
|
||||||
func (temperatureTest) Test(ctx *SurfaceContext) bool {
|
func (temperatureTest) Test(ctx *SurfaceContext) bool {
|
||||||
return isColdBiome(ctx.BiomeName)
|
return coldEnoughToSnow(ctx.BiomeName)
|
||||||
}
|
|
||||||
|
|
||||||
// isColdBiome reports whether the biome should receive snow cover. We use the
|
|
||||||
// biome name rather than the temperature noise for simplicity; this matches
|
|
||||||
// the visible result for the standard overworld biomes.
|
|
||||||
func isColdBiome(name string) bool {
|
|
||||||
switch name {
|
|
||||||
case "minecraft:snowy_plains", "minecraft:snowy_taiga", "minecraft:snowy_beach",
|
|
||||||
"minecraft:snowy_slopes", "minecraft:jagged_peaks", "minecraft:frozen_peaks",
|
|
||||||
"minecraft:frozen_river", "minecraft:frozen_ocean", "minecraft:deep_frozen_ocean",
|
|
||||||
"minecraft:ice_spikes", "minecraft:grove":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// yAboveTest passes when Y clears an anchor, with optional surface-depth and
|
// yAboveTest passes when Y clears an anchor, with optional surface-depth and
|
||||||
|
|
@ -445,7 +410,11 @@ func (p *surfaceParser) parseRule(raw json.RawMessage) (SurfaceRule, error) {
|
||||||
return conditionRule{test: test, then: then}, nil
|
return conditionRule{test: test, then: then}, nil
|
||||||
|
|
||||||
case "minecraft:bandlands":
|
case "minecraft:bandlands":
|
||||||
return bandlandsRule{}, nil
|
bands, err := p.loader.clayBands()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bandlandsRule{bands: bands}, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("surface: unknown rule type %q", obj.Type)
|
return nil, fmt.Errorf("surface: unknown rule type %q", obj.Type)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
package worldgen
|
package worldgen
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"math/rand"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// loadTestRules compiles the overworld surface rule set at a fixed seed.
|
// loadTestRules compiles the overworld surface rule set at a fixed seed.
|
||||||
func loadTestRules(t *testing.T) *SurfaceRuleSet {
|
func loadTestRules(t *testing.T) *SurfaceRuleSet {
|
||||||
|
|
@ -47,7 +44,6 @@ func TestSurfaceRuleNoPanic(t *testing.T) {
|
||||||
ctx.SeaLevel, ctx.MinY = 63, -64
|
ctx.SeaLevel, ctx.MinY = 63, -64
|
||||||
ctx.MinSurfaceLevel, ctx.WaterHeight = 80, NoWaterAbove
|
ctx.MinSurfaceLevel, ctx.WaterHeight = 80, NoWaterAbove
|
||||||
ctx.SurfaceDepth = 3
|
ctx.SurfaceDepth = 3
|
||||||
ctx.Rng = rand.New(rand.NewSource(1))
|
|
||||||
for _, b := range biomes {
|
for _, b := range biomes {
|
||||||
ctx.BiomeName = b
|
ctx.BiomeName = b
|
||||||
for y := 0; y < 100; y++ {
|
for y := 0; y < 100; y++ {
|
||||||
|
|
@ -70,7 +66,6 @@ func TestSurfaceBedrockFloor(t *testing.T) {
|
||||||
ctx.BiomeName = "minecraft:plains"
|
ctx.BiomeName = "minecraft:plains"
|
||||||
ctx.MinSurfaceLevel, ctx.WaterHeight = 62, NoWaterAbove
|
ctx.MinSurfaceLevel, ctx.WaterHeight = 62, NoWaterAbove
|
||||||
ctx.SurfaceDepth = 3
|
ctx.SurfaceDepth = 3
|
||||||
ctx.Rng = rand.New(rand.NewSource(1))
|
|
||||||
state, ok := rules.Apply(ctx)
|
state, ok := rules.Apply(ctx)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("no rule matched at bedrock floor")
|
t.Fatal("no rule matched at bedrock floor")
|
||||||
|
|
@ -116,21 +111,37 @@ func TestSurfaceBlockIDResolution(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestIsColdBiome confirms the snow-cover predicate recognises cold biomes so
|
// TestColdEnoughToSnow pins the temperature predicate against the biome table
|
||||||
// the temperature condition routes snowy biomes to snow.
|
// extracted from the jar. deep_frozen_ocean is the interesting case: the name
|
||||||
func TestIsColdBiome(t *testing.T) {
|
// reads cold but its base temperature is 0.5, so vanilla does not freeze it —
|
||||||
cold := []string{"minecraft:snowy_plains", "minecraft:frozen_peaks", "minecraft:grove"}
|
// the hand-written list this replaced got it wrong.
|
||||||
|
func TestColdEnoughToSnow(t *testing.T) {
|
||||||
|
cold := []string{
|
||||||
|
"minecraft:frozen_ocean", "minecraft:frozen_peaks", "minecraft:frozen_river",
|
||||||
|
"minecraft:grove", "minecraft:ice_spikes", "minecraft:jagged_peaks",
|
||||||
|
"minecraft:snowy_beach", "minecraft:snowy_plains", "minecraft:snowy_slopes",
|
||||||
|
"minecraft:snowy_taiga",
|
||||||
|
}
|
||||||
for _, b := range cold {
|
for _, b := range cold {
|
||||||
if !isColdBiome(b) {
|
if !coldEnoughToSnow(b) {
|
||||||
t.Errorf("isColdBiome(%q) = false, want true", b)
|
t.Errorf("coldEnoughToSnow(%q) = false, want true", b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
warm := []string{"minecraft:desert", "minecraft:plains", "minecraft:badlands"}
|
warm := []string{
|
||||||
|
"minecraft:desert", "minecraft:plains", "minecraft:badlands",
|
||||||
|
"minecraft:deep_frozen_ocean", "minecraft:taiga", "minecraft:windswept_hills",
|
||||||
|
}
|
||||||
for _, b := range warm {
|
for _, b := range warm {
|
||||||
if isColdBiome(b) {
|
if coldEnoughToSnow(b) {
|
||||||
t.Errorf("isColdBiome(%q) = true, want false", b)
|
t.Errorf("coldEnoughToSnow(%q) = true, want false", b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if coldEnoughToSnow("minecraft:not_a_biome") {
|
||||||
|
t.Error("an unknown biome read as cold")
|
||||||
|
}
|
||||||
|
if len(biomeTemperature) != 65 {
|
||||||
|
t.Errorf("biome temperature table has %d entries, want 65", len(biomeTemperature))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWaterCondition pins SurfaceRules.WaterConditionSource against the
|
// TestWaterCondition pins SurfaceRules.WaterConditionSource against the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue