Configured carvers: caves and canyons

The router's noise caves are one kind of cave. The other kind -- the long
winding tunnels with rooms and side branches, and the ravines that cut down
through the terrain -- is walked, step by step, by a random source, and none of
it existed.

The shape of the work is unusual enough to state plainly. To carve one chunk,
vanilla replays every carver seeded in the 17x17 chunks around it and keeps only
what lands inside, so the same tunnel is walked up to 289 times across a world.
That redundancy is the point: it is what lets a chunk be carved without
generating its neighbours, which is the only way carving fits a generator that
produces one chunk at a time. A carve-once-write-into-neighbours design would be
cheaper and would not reproduce vanilla's mask and ordering.

Two primitives had to be right before any of it could be, and both are pinned
against values captured from the jar:

  * setLargeFeatureSeed, which decides which chunks start a cave. It combines
    its two products with XOR; setDecorationSeed, which it otherwise resembles,
    uses addition and forces the low bit. Getting them the wrong way round moves
    every tunnel in the world and nothing complains.
  * Mth.sin and Mth.cos, which are a 65536-entry lookup table and not libm.
    Mth.sin(-1.0) is -0.8414514 against Math.sin's -0.8414709848078965, and a
    tunnel that walks by adding cos(yaw) a hundred times ends up somewhere else
    entirely if that difference is smoothed away.

Carving lands between the surface pass and decoration, where vanilla puts it,
and both neighbours matter: the surface rules must already have placed grass for
a cave mouth to be retextured, and decoration must come after so nothing is
planted over a hole. The heights decoration plants against are recomputed
afterwards, which is why vanilla re-primes its heightmaps at the start of the
feature step.

The configs are extracted from the jar rather than transcribed, along with the
flattened #minecraft:overworld_carver_replaceables tag, so the probabilities and
Y ranges are data. Open volume below y=60 rises 28% over sixteen sampled chunks,
tunnels cut at or below y=-56 fill with lava rather than air (869 blocks, no
air), and the cost is inside the noise floor of the density pass.
This commit is contained in:
Master290 2026-07-27 03:58:56 +03:00
parent 0f76058db6
commit c6185d88c8
13 changed files with 1402 additions and 9 deletions

108
internal/world/carve.go Normal file
View file

@ -0,0 +1,108 @@
package world
import (
"sync"
"regionio/internal/worldgen"
)
// carve.go is the world side of the carvers: it hands the carver a view of the
// column array it is cutting into, and answers the two questions the carver
// cannot answer for itself — which blocks it may cut through, and what the
// surface rules want under a carved-away grass block.
//
// Carving runs between the surface pass and decoration, which is where vanilla
// puts it. That order is load-bearing in both directions: the surface rules
// must already have placed grass and podzol for the cave-mouth retexturing to
// have anything to look at, and decoration must run afterwards so trees are not
// planted over a hole.
// carveView adapts the generator's column array to worldgen.CarveTarget.
type carveView struct {
cols *[16][16][WorldHeight]uint16
od *worldgen.OverworldDensity
rules *worldgen.SurfaceRuleSet
sctx *worldgen.SurfaceContext
biomes *[16][16]string
// worldSurface is the pre-carve heightmap the steep condition reads, as in
// vanilla, where carving happens after the surface pass has already run.
worldSurface *[16][16]int
baseX, baseZ int
}
func (v *carveView) Block(lx, y, lz int) uint16 {
i := y - MinY
if i < 0 || i >= WorldHeight || lx < 0 || lx > 15 || lz < 0 || lz > 15 {
return StateAir
}
return v.cols[lx][lz][i]
}
func (v *carveView) SetBlock(lx, y, lz int, state uint16) {
i := y - MinY
if i < 0 || i >= WorldHeight || lx < 0 || lx > 15 || lz < 0 || lz > 15 {
return
}
v.cols[lx][lz][i] = state
}
func (v *carveView) Replaceable(state uint16) bool { return carverReplaceable(state) }
// TopMaterial is SurfaceSystem.topMaterial: the rule tree applied to a single
// position, with the stone depths pinned to 1 and the water height set only
// when the block that was carved out holds fluid.
func (v *carveView) TopMaterial(lx, y, lz int, underFluid bool) (uint16, bool) {
if v.rules == nil {
return 0, false
}
wx, wz := v.baseX+lx, v.baseZ+lz
v.rules.BeginColumn(v.sctx, wx, wz)
surfaceDepth := v.od.Surface.SurfaceDepth(wx, wz)
v.sctx.SeaLevel = SeaLevel
v.sctx.MinY = MinY
v.sctx.BiomeName = v.biomes[lx][lz]
v.sctx.SurfaceSecondary = v.od.Surface.SurfaceSecondary(wx, wz)
v.sctx.SurfaceDepth = surfaceDepth
v.sctx.MinSurfaceLevel = v.od.MinSurfaceLevelAt(wx, wz, surfaceDepth)
v.sctx.Steep = steepAt(v.worldSurface, lx, lz)
v.sctx.Y = y
v.sctx.StoneDepthAbove = 1
v.sctx.StoneDepthBelow = 1
v.sctx.WaterHeight = worldgen.NoWaterAbove
if underFluid {
v.sctx.WaterHeight = y + 1
}
return v.rules.Apply(v.sctx)
}
var (
carverReplaceableOnce sync.Once
carverReplaceableSet []bool
)
// initCarverReplaceable resolves the carver's block names to every state of
// each block. The tag names blocks, not states, so waterlogged and snowy
// variants qualify too — which is why this walks the full state table rather
// than the default-state table.
func initCarverReplaceable(names []string) {
carverReplaceableOnce.Do(func() {
stateByIDOnce.Do(buildStateTable)
carverReplaceableSet = make([]bool, totalBlockStates)
for _, name := range names {
for _, id := range idsByName[name] {
if int(id) < len(carverReplaceableSet) {
carverReplaceableSet[id] = true
}
}
}
})
}
// carverReplaceable reports whether a block state is in
// #minecraft:overworld_carver_replaceables.
func carverReplaceable(state uint16) bool {
if int(state) >= len(carverReplaceableSet) {
return false
}
return carverReplaceableSet[state]
}

View file

@ -0,0 +1,138 @@
package world
import (
"testing"
"regionio/internal/worldgen"
)
// TestCarversOpenTerrain compares the same chunks generated with and without
// the carvers. The density router already opens noise caves, so the question is
// not whether caves exist but whether carving adds the other kind — the walked
// tunnels and the ravines — on top of them.
func TestCarversOpenTerrain(t *testing.T) {
const seed = 12345
od, err := worldgen.LoadOverworldFinalDensity(seed)
if err != nil {
t.Fatal(err)
}
picker := worldgen.OverworldFluidPicker(od.SeaLevel)
veins := worldgen.NewOreVeinifier(od)
carver, err := worldgen.NewCarver(od, seed)
if err != nil {
t.Fatal(err)
}
initCarverReplaceable(carver.ReplaceableBlocks())
open := func(c *Chunk) int {
n := 0
for wy := MinY; wy < 60; wy++ {
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
if c.GetBlock(lx, wy, lz) == StateAir {
n++
}
}
}
}
return n
}
uncarved, carved := 0, 0
changedChunks := 0
for cx := int32(-42); cx < -38; cx++ {
for cz := int32(-40); cz < -36; cz++ {
before := open(generateVanilla(od, picker, veins, nil, seed, cx, cz))
after := open(generateVanilla(od, picker, veins, carver, seed, cx, cz))
uncarved += before
carved += after
if after != before {
changedChunks++
}
if after < before {
t.Errorf("chunk (%d,%d): carving closed %d blocks; it must only open them", cx, cz, before-after)
}
}
}
t.Logf("open blocks below y=60: %d uncarved, %d carved (+%.1f%%), %d of 16 chunks changed",
uncarved, carved, 100*float64(carved-uncarved)/float64(uncarved), changedChunks)
if carved == uncarved {
t.Fatal("carving opened nothing at all")
}
if changedChunks < 12 {
t.Errorf("only %d of 16 chunks were carved; a 17x17 neighbourhood should reach almost every chunk", changedChunks)
}
}
// TestCarversAreDeterministic guards the seeding. Carving replays 289 source
// chunks per target, and a single wrong bit in setLargeFeatureSeed would move
// every tunnel without breaking anything visibly.
func TestCarversAreDeterministic(t *testing.T) {
const seed = 4242
od, err := worldgen.LoadOverworldFinalDensity(seed)
if err != nil {
t.Fatal(err)
}
picker := worldgen.OverworldFluidPicker(od.SeaLevel)
veins := worldgen.NewOreVeinifier(od)
carver, err := worldgen.NewCarver(od, seed)
if err != nil {
t.Fatal(err)
}
initCarverReplaceable(carver.ReplaceableBlocks())
first := generateVanilla(od, picker, veins, carver, seed, 7, -3)
second := generateVanilla(od, picker, veins, carver, seed, 7, -3)
for wy := MinY; wy < MinY+WorldHeight; wy++ {
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
if a, b := first.GetBlock(lx, wy, lz), second.GetBlock(lx, wy, lz); a != b {
t.Fatalf("(%d,%d,%d): %d then %d on a second generation of the same chunk", lx, wy, lz, a, b)
}
}
}
}
}
// TestCarverLavaFloor checks getCarveState's lava level: a tunnel cut at or
// below y=-56 fills with lava rather than opening to air. The level comes from
// the carver config's above_bottom 8, not from the aquifer's own lava rule.
func TestCarverLavaFloor(t *testing.T) {
const seed = 12345
od, err := worldgen.LoadOverworldFinalDensity(seed)
if err != nil {
t.Fatal(err)
}
picker := worldgen.OverworldFluidPicker(od.SeaLevel)
carver, err := worldgen.NewCarver(od, seed)
if err != nil {
t.Fatal(err)
}
initCarverReplaceable(carver.ReplaceableBlocks())
deepLava, deepAir := 0, 0
for cx := int32(-12); cx <= 12; cx += 4 {
for cz := int32(-12); cz <= 12; cz += 4 {
ch := generateVanilla(od, picker, nil, carver, seed, cx, cz)
for wy := MinY + 1; wy <= -56; wy++ {
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
switch ch.GetBlock(lx, wy, lz) {
case StateLava:
deepLava++
case StateAir:
deepAir++
}
}
}
}
}
}
t.Logf("at or below y=-56: %d lava, %d air", deepLava, deepAir)
if deepLava == 0 {
t.Error("no lava at the carver's lava level; getCarveState is not filling deep tunnels")
}
if deepAir > deepLava {
t.Errorf("%d air against %d lava below the lava level; deep tunnels should fill", deepAir, deepLava)
}
}

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 = 10
const generatorVersion = 11
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
// because it is ours, not part of the vanilla chunk format.

View file

@ -32,12 +32,17 @@ func NewVanillaGenerator(seed int64) Generator {
}
fluidPicker := worldgen.OverworldFluidPicker(od.SeaLevel)
veins := worldgen.NewOreVeinifier(od)
carver, err := worldgen.NewCarver(od, seed)
if err != nil {
panic("world: loading carvers: " + err.Error())
}
initCarverReplaceable(carver.ReplaceableBlocks())
return func(cx, cz int32) *Chunk {
return generateVanilla(od, fluidPicker, veins, seed, cx, cz)
return generateVanilla(od, fluidPicker, veins, carver, seed, cx, cz)
}
}
func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, seed int64, cx, cz int32) *Chunk {
func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPicker, veins *worldgen.OreVeinifier, carver *worldgen.Carver, seed int64, cx, cz int32) *Chunk {
c := NewChunk(cx, cz, BiomePlains) // per-cell biomes override below
baseX, baseZ := int(cx)*16, int(cz)*16
@ -135,6 +140,26 @@ func generateVanilla(od *worldgen.OverworldDensity, fluidPicker worldgen.FluidPi
}
wg.Wait()
// Carving sits between the surface pass and decoration, as it does in
// vanilla: it needs the surfaced blocks to retexture a cave mouth, and
// decoration needs the carved heights so nothing is planted over a hole.
if carver != nil && ruleErr == nil {
view := &carveView{
cols: &columns, od: od, rules: surfaceRule,
sctx: surfaceRule.NewContext(), biomes: &biomeName,
worldSurface: &worldSurface, baseX: baseX, baseZ: baseZ,
}
carver.CarveChunk(view, aq, int(cx), int(cz))
// The heights decoration plants against are the post-carve ones.
// Vanilla re-primes its heightmaps at the start of the feature step for
// the same reason.
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
surfTop[lx][lz], grass[lx][lz] = classifyColumn(&columns[lx][lz])
}
}
}
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
col := &columns[lx][lz]
@ -216,13 +241,31 @@ func fillVanillaColumn(od *worldgen.OverworldDensity, aq *worldgen.Aquifer, flui
}
}
_, grass = classifyColumn(out)
return top, worldSurface, grass
}
// classifyColumn returns the top solid index and whether that surface is
// plantable grassy land. It is recomputed after carving, because a column whose
// top block a ravine removed is no longer the column decoration was told about.
func classifyColumn(col *[WorldHeight]uint16) (top int, grass bool) {
top = -1
for i := WorldHeight - 1; i >= 0; i-- {
if isStoneState(col[i]) {
top = i
break
}
}
if top < 0 {
return top, false
}
topY := MinY + top
// Beach: a narrow band straddling the waterline. Dry columns well above sea
// level stay grass; deep water floors become gravel, not sand.
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
beach := topY >= SeaLevel-beachBand && topY <= SeaLevel+1
deepWater := topY < SeaLevel-beachBand
return top, !beach && !deepWater && topY >= SeaLevel
}
// steepAt is SurfaceRules.SteepMaterialCondition: true where the column's