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:
parent
0f76058db6
commit
c6185d88c8
13 changed files with 1402 additions and 9 deletions
108
internal/world/carve.go
Normal file
108
internal/world/carve.go
Normal 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]
|
||||
}
|
||||
138
internal/world/carve_verify_test.go
Normal file
138
internal/world/carve_verify_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
700
internal/worldgen/carver.go
Normal file
700
internal/worldgen/carver.go
Normal file
|
|
@ -0,0 +1,700 @@
|
|||
package worldgen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// carver.go ports net.minecraft.world.level.levelgen.carver: the cave and
|
||||
// canyon carvers, and the driver that replays them.
|
||||
//
|
||||
// Carving is the step between the surface rules and decoration. The density
|
||||
// router already opens cheese, spaghetti and noodle caves; carvers cut the
|
||||
// other kind — the long winding tunnels with rooms and branches, and the
|
||||
// ravines that slice down through the terrain. Everything the router makes is
|
||||
// noise-shaped; everything here is walked, step by step, by a random source.
|
||||
//
|
||||
// The shape of the work is unusual and worth stating plainly: to carve one
|
||||
// chunk, vanilla replays every carver seeded in the 17x17 chunks around it and
|
||||
// keeps only what lands inside. The same tunnel is therefore walked up to 289
|
||||
// times across a world. That redundancy is not an accident to be optimised
|
||||
// away — it is what lets a chunk be carved without generating its neighbours,
|
||||
// which is the only reason carving fits into a generator that produces one
|
||||
// chunk at a time.
|
||||
|
||||
// carverRange is WorldCarver.getRange(); neither overworld carver overrides it.
|
||||
const carverRange = 4
|
||||
|
||||
// carveDistance is the tunnel length budget, SectionPos.sectionToBlockCoord(getRange()*2-1).
|
||||
const carveDistance = (carverRange*2 - 1) * 16
|
||||
|
||||
// carverNeighbourhood is applyCarvers' loop bound: dx and dz each run -8..8
|
||||
// inclusive, so 289 source chunks feed every carved chunk. It is a fixed 8, not
|
||||
// derived from getRange().
|
||||
const carverNeighbourhood = 8
|
||||
|
||||
// ---- providers ---------------------------------------------------------
|
||||
|
||||
// floatProvider is FloatProvider: a bare number is a constant, an object
|
||||
// dispatches on "type". The number of draws each kind makes is part of the
|
||||
// contract — a constant draws nothing, and that silence is load-bearing.
|
||||
type floatProvider interface {
|
||||
sample(r RandomSource) float32
|
||||
}
|
||||
|
||||
type constantFloat float32
|
||||
|
||||
func (c constantFloat) sample(RandomSource) float32 { return float32(c) }
|
||||
|
||||
type uniformFloat struct{ lo, hi float32 }
|
||||
|
||||
func (u uniformFloat) sample(r RandomSource) float32 { return randomBetween(r, u.lo, u.hi) }
|
||||
|
||||
// trapezoidFloat draws twice, in this order.
|
||||
type trapezoidFloat struct{ min, max, plateau float32 }
|
||||
|
||||
func (t trapezoidFloat) sample(r RandomSource) float32 {
|
||||
span := t.max - t.min
|
||||
slope := (span - t.plateau) / 2.0
|
||||
flat := span - slope
|
||||
return t.min + r.NextFloat()*flat + r.NextFloat()*slope
|
||||
}
|
||||
|
||||
func parseFloatProvider(raw json.RawMessage) (floatProvider, error) {
|
||||
var number float32
|
||||
if err := json.Unmarshal(raw, &number); err == nil {
|
||||
return constantFloat(number), nil
|
||||
}
|
||||
var obj struct {
|
||||
Type string `json:"type"`
|
||||
Value float32 `json:"value"`
|
||||
MinInclusive float32 `json:"min_inclusive"`
|
||||
MaxExclusive float32 `json:"max_exclusive"`
|
||||
Min float32 `json:"min"`
|
||||
Max float32 `json:"max"`
|
||||
Plateau float32 `json:"plateau"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch obj.Type {
|
||||
case "minecraft:constant":
|
||||
return constantFloat(obj.Value), nil
|
||||
case "minecraft:uniform":
|
||||
return uniformFloat{obj.MinInclusive, obj.MaxExclusive}, nil
|
||||
case "minecraft:trapezoid":
|
||||
return trapezoidFloat{obj.Min, obj.Max, obj.Plateau}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("carver: unsupported float provider %q", obj.Type)
|
||||
}
|
||||
|
||||
// heightProvider is HeightProvider. A bare vertical anchor is a constant.
|
||||
type heightProvider interface {
|
||||
sample(r RandomSource, minY, height int) int
|
||||
}
|
||||
|
||||
type constantHeight struct{ anchor anchorJSON }
|
||||
|
||||
func (c constantHeight) sample(_ RandomSource, minY, height int) int {
|
||||
return resolveAnchorY(c.anchor, minY, height)
|
||||
}
|
||||
|
||||
type uniformHeight struct{ lo, hi anchorJSON }
|
||||
|
||||
func (u uniformHeight) sample(r RandomSource, minY, height int) int {
|
||||
lo := resolveAnchorY(u.lo, minY, height)
|
||||
hi := resolveAnchorY(u.hi, minY, height)
|
||||
if lo > hi {
|
||||
// Vanilla logs and returns the low bound without drawing.
|
||||
return lo
|
||||
}
|
||||
return randomBetweenInclusive(r, lo, hi)
|
||||
}
|
||||
|
||||
func parseHeightProvider(raw json.RawMessage) (heightProvider, error) {
|
||||
var obj struct {
|
||||
Type string `json:"type"`
|
||||
MinInclusive anchorJSON `json:"min_inclusive"`
|
||||
MaxInclusive anchorJSON `json:"max_inclusive"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &obj); err == nil && obj.Type == "minecraft:uniform" {
|
||||
return uniformHeight{obj.MinInclusive, obj.MaxInclusive}, nil
|
||||
}
|
||||
var anchor anchorJSON
|
||||
if err := json.Unmarshal(raw, &anchor); err != nil {
|
||||
return nil, fmt.Errorf("carver: unsupported height provider: %w", err)
|
||||
}
|
||||
return constantHeight{anchor}, nil
|
||||
}
|
||||
|
||||
// ---- configuration -----------------------------------------------------
|
||||
|
||||
type carverKind int
|
||||
|
||||
const (
|
||||
carverCave carverKind = iota
|
||||
carverCanyon
|
||||
)
|
||||
|
||||
type canyonShape struct {
|
||||
distanceFactor floatProvider
|
||||
thickness floatProvider
|
||||
horizontalRadiusFactor floatProvider
|
||||
verticalRadiusDefaultFactor float32
|
||||
verticalRadiusCenterFactor float32
|
||||
widthSmoothness int
|
||||
}
|
||||
|
||||
type carverConfig struct {
|
||||
kind carverKind
|
||||
name string
|
||||
probability float32
|
||||
y heightProvider
|
||||
lavaLevel anchorJSON
|
||||
yScale floatProvider
|
||||
|
||||
// cave
|
||||
horizontalRadiusMultiplier floatProvider
|
||||
verticalRadiusMultiplier floatProvider
|
||||
floorLevel floatProvider
|
||||
|
||||
// canyon
|
||||
verticalRotation floatProvider
|
||||
shape canyonShape
|
||||
}
|
||||
|
||||
func loadCarverConfig(name string) (*carverConfig, error) {
|
||||
raw, err := dataFS.ReadFile("data/carver/" + name + ".json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc struct {
|
||||
Type string `json:"type"`
|
||||
Config struct {
|
||||
Probability float32 `json:"probability"`
|
||||
Y json.RawMessage `json:"y"`
|
||||
LavaLevel anchorJSON `json:"lava_level"`
|
||||
YScale json.RawMessage `json:"yScale"`
|
||||
HorizontalRadiusMultiplier json.RawMessage `json:"horizontal_radius_multiplier"`
|
||||
VerticalRadiusMultiplier json.RawMessage `json:"vertical_radius_multiplier"`
|
||||
FloorLevel json.RawMessage `json:"floor_level"`
|
||||
VerticalRotation json.RawMessage `json:"vertical_rotation"`
|
||||
Shape struct {
|
||||
DistanceFactor json.RawMessage `json:"distance_factor"`
|
||||
Thickness json.RawMessage `json:"thickness"`
|
||||
HorizontalRadiusFactor json.RawMessage `json:"horizontal_radius_factor"`
|
||||
VerticalRadiusDefaultFactor float32 `json:"vertical_radius_default_factor"`
|
||||
VerticalRadiusCenterFactor float32 `json:"vertical_radius_center_factor"`
|
||||
WidthSmoothness int `json:"width_smoothness"`
|
||||
} `json:"shape"`
|
||||
} `json:"config"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, fmt.Errorf("carver %s: %w", name, err)
|
||||
}
|
||||
c := &carverConfig{name: name, probability: doc.Config.Probability, lavaLevel: doc.Config.LavaLevel}
|
||||
if c.y, err = parseHeightProvider(doc.Config.Y); err != nil {
|
||||
return nil, fmt.Errorf("carver %s y: %w", name, err)
|
||||
}
|
||||
if c.yScale, err = parseFloatProvider(doc.Config.YScale); err != nil {
|
||||
return nil, fmt.Errorf("carver %s yScale: %w", name, err)
|
||||
}
|
||||
switch doc.Type {
|
||||
case "minecraft:cave":
|
||||
c.kind = carverCave
|
||||
for _, f := range []struct {
|
||||
raw json.RawMessage
|
||||
dst *floatProvider
|
||||
key string
|
||||
}{
|
||||
{doc.Config.HorizontalRadiusMultiplier, &c.horizontalRadiusMultiplier, "horizontal_radius_multiplier"},
|
||||
{doc.Config.VerticalRadiusMultiplier, &c.verticalRadiusMultiplier, "vertical_radius_multiplier"},
|
||||
{doc.Config.FloorLevel, &c.floorLevel, "floor_level"},
|
||||
} {
|
||||
if *f.dst, err = parseFloatProvider(f.raw); err != nil {
|
||||
return nil, fmt.Errorf("carver %s %s: %w", name, f.key, err)
|
||||
}
|
||||
}
|
||||
case "minecraft:canyon":
|
||||
c.kind = carverCanyon
|
||||
if c.verticalRotation, err = parseFloatProvider(doc.Config.VerticalRotation); err != nil {
|
||||
return nil, fmt.Errorf("carver %s vertical_rotation: %w", name, err)
|
||||
}
|
||||
s := doc.Config.Shape
|
||||
c.shape.verticalRadiusDefaultFactor = s.VerticalRadiusDefaultFactor
|
||||
c.shape.verticalRadiusCenterFactor = s.VerticalRadiusCenterFactor
|
||||
c.shape.widthSmoothness = s.WidthSmoothness
|
||||
for _, f := range []struct {
|
||||
raw json.RawMessage
|
||||
dst *floatProvider
|
||||
key string
|
||||
}{
|
||||
{s.DistanceFactor, &c.shape.distanceFactor, "distance_factor"},
|
||||
{s.Thickness, &c.shape.thickness, "thickness"},
|
||||
{s.HorizontalRadiusFactor, &c.shape.horizontalRadiusFactor, "horizontal_radius_factor"},
|
||||
} {
|
||||
if *f.dst, err = parseFloatProvider(f.raw); err != nil {
|
||||
return nil, fmt.Errorf("carver %s shape.%s: %w", name, f.key, err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("carver %s: unsupported type %q", name, doc.Type)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// overworldCarvers is the carver list every one of the 54 overworld biomes
|
||||
// carries, in the order the biome files list them. The index is part of the
|
||||
// per-chunk seed, so the order matters as much as the contents.
|
||||
var overworldCarvers = []string{"cave", "cave_extra_underground", "canyon"}
|
||||
|
||||
// ---- the carving mask --------------------------------------------------
|
||||
|
||||
// carvingMask is CarvingMask: one bit per block of the target chunk, so a
|
||||
// position already opened by an earlier carver is not reconsidered. It is what
|
||||
// makes the replay order matter.
|
||||
type carvingMask struct {
|
||||
bits []uint64
|
||||
minY int
|
||||
}
|
||||
|
||||
func newCarvingMask(minY, height int) *carvingMask {
|
||||
return &carvingMask{bits: make([]uint64, (256*height+63)/64), minY: minY}
|
||||
}
|
||||
|
||||
func (m *carvingMask) index(lx, y, lz int) int {
|
||||
return (lx & 15) | ((lz & 15) << 4) | ((y - m.minY) << 8)
|
||||
}
|
||||
|
||||
func (m *carvingMask) get(lx, y, lz int) bool {
|
||||
i := m.index(lx, y, lz)
|
||||
return m.bits[i>>6]&(1<<uint(i&63)) != 0
|
||||
}
|
||||
|
||||
func (m *carvingMask) set(lx, y, lz int) {
|
||||
i := m.index(lx, y, lz)
|
||||
m.bits[i>>6] |= 1 << uint(i&63)
|
||||
}
|
||||
|
||||
// ---- the target --------------------------------------------------------
|
||||
|
||||
// CarveTarget is the chunk being carved. Coordinates are chunk-local in x and
|
||||
// z and absolute in y; a carver never addresses a block outside the chunk it
|
||||
// was handed, however far its tunnel wandered to get there.
|
||||
type CarveTarget interface {
|
||||
Block(lx, y, lz int) uint16
|
||||
SetBlock(lx, y, lz int, state uint16)
|
||||
// Replaceable reports whether a state is in
|
||||
// #minecraft:overworld_carver_replaceables. It is a block-level test, so
|
||||
// every state of a listed block qualifies.
|
||||
Replaceable(state uint16) bool
|
||||
// TopMaterial re-runs the surface rule at one position, to retexture the
|
||||
// dirt left exposed under a carved-away grass block. ok=false leaves it.
|
||||
TopMaterial(lx, y, lz int, underFluid bool) (uint16, bool)
|
||||
}
|
||||
|
||||
// ---- the carver --------------------------------------------------------
|
||||
|
||||
// Carver replays the overworld's configured carvers over a chunk. Build one per
|
||||
// generator; it holds no per-chunk state.
|
||||
type Carver struct {
|
||||
configs []*carverConfig
|
||||
seed int64
|
||||
minY int
|
||||
height int
|
||||
// grassBlocks and mycelium are the states whose removal exposes dirt worth
|
||||
// retexturing; dirt is the state that gets retextured.
|
||||
replaceableNames []string
|
||||
}
|
||||
|
||||
// NewCarver loads the overworld carver configs. A parse failure is returned
|
||||
// rather than swallowed: silently generating an uncarved world would look like
|
||||
// the carvers simply do not work.
|
||||
func NewCarver(od *OverworldDensity, seed int64) (*Carver, error) {
|
||||
c := &Carver{seed: seed, minY: od.MinY, height: od.Height}
|
||||
for _, name := range overworldCarvers {
|
||||
cfg, err := loadCarverConfig(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.configs = append(c.configs, cfg)
|
||||
}
|
||||
names, err := loadCarverReplaceables()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.replaceableNames = names
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ReplaceableBlocks returns the block names a carver may cut through, so the
|
||||
// caller can resolve them to every state of each block.
|
||||
func (c *Carver) ReplaceableBlocks() []string { return c.replaceableNames }
|
||||
|
||||
func loadCarverReplaceables() ([]string, error) {
|
||||
raw, err := dataFS.ReadFile("data/carver/replaceable.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var doc struct {
|
||||
Values []string `json:"values"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doc.Values, nil
|
||||
}
|
||||
|
||||
// carveState is the per-chunk scratch a carve pass needs.
|
||||
type carveState struct {
|
||||
c *Carver
|
||||
target CarveTarget
|
||||
aq *Aquifer
|
||||
mask *carvingMask
|
||||
// chunkX and chunkZ are the chunk being written, which is not the chunk a
|
||||
// tunnel was seeded in.
|
||||
chunkX, chunkZ int
|
||||
lavaY int
|
||||
}
|
||||
|
||||
// CarveChunk replays every carver seeded in the 17x17 chunks around (chunkX,
|
||||
// chunkZ) and applies whatever reaches this chunk.
|
||||
//
|
||||
// The order is fixed and cannot be parallelised inside a chunk: a later carve
|
||||
// reads blocks an earlier one wrote, and they share the mask.
|
||||
func (c *Carver) CarveChunk(target CarveTarget, aq *Aquifer, chunkX, chunkZ int) {
|
||||
st := &carveState{
|
||||
c: c, target: target, aq: aq,
|
||||
mask: newCarvingMask(c.minY, c.height),
|
||||
chunkX: chunkX, chunkZ: chunkZ,
|
||||
}
|
||||
random := NewLegacy(0)
|
||||
for dx := -carverNeighbourhood; dx <= carverNeighbourhood; dx++ {
|
||||
for dz := -carverNeighbourhood; dz <= carverNeighbourhood; dz++ {
|
||||
sourceX, sourceZ := chunkX+dx, chunkZ+dz
|
||||
for index, cfg := range c.configs {
|
||||
// The carver's index in the biome's list is part of the seed,
|
||||
// which is why the list order matters as much as its contents.
|
||||
random.SetLargeFeatureSeed(c.seed+int64(index), sourceX, sourceZ)
|
||||
if random.NextFloat() > cfg.probability {
|
||||
continue
|
||||
}
|
||||
st.lavaY = resolveAnchorY(cfg.lavaLevel, c.minY, c.height)
|
||||
switch cfg.kind {
|
||||
case carverCave:
|
||||
st.carveCave(cfg, random, sourceX, sourceZ)
|
||||
case carverCanyon:
|
||||
st.carveCanyon(cfg, random, sourceX, sourceZ)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- cave --------------------------------------------------------------
|
||||
|
||||
func (s *carveState) carveCave(cfg *carverConfig, random *Legacy, sourceX, sourceZ int) {
|
||||
const caveBound = 15
|
||||
tunnelSystems := int(random.NextIntN(random.NextIntN(random.NextIntN(caveBound)+1) + 1))
|
||||
for k := 0; k < tunnelSystems; k++ {
|
||||
x := float64(sourceX<<4 + int(random.NextIntN(16)))
|
||||
y := float64(cfg.y.sample(random, s.c.minY, s.c.height))
|
||||
z := float64(sourceZ<<4 + int(random.NextIntN(16)))
|
||||
horizontalMul := float64(cfg.horizontalRadiusMultiplier.sample(random))
|
||||
verticalMul := float64(cfg.verticalRadiusMultiplier.sample(random))
|
||||
floorLevel := float64(cfg.floorLevel.sample(random))
|
||||
skip := caveSkip(floorLevel)
|
||||
|
||||
tunnels := 1
|
||||
if random.NextIntN(4) == 0 {
|
||||
// A room. It is the only place the cave carver reads yScale, it
|
||||
// ignores both radius multipliers, and it sits one block east of
|
||||
// where the tunnels start.
|
||||
roomYScale := float64(cfg.yScale.sample(random))
|
||||
radius := 1.0 + random.NextFloat()*6.0
|
||||
horizontal := 1.5 + float64(MthSin(math.Pi/2)*radius)
|
||||
s.carveEllipsoid(cfg, x+1.0, y, z, horizontal, horizontal*roomYScale, skip)
|
||||
tunnels += int(random.NextIntN(4))
|
||||
}
|
||||
for i := 0; i < tunnels; i++ {
|
||||
yaw := random.NextFloat() * 6.2831855
|
||||
pitch := (random.NextFloat() - 0.5) / 4.0
|
||||
thickness := caveThickness(random)
|
||||
branchCount := carveDistance - int(random.NextIntN(carveDistance/4))
|
||||
s.caveTunnel(cfg, random.NextLong(), x, y, z, horizontalMul, verticalMul,
|
||||
thickness, yaw, pitch, 0, branchCount, 1.0, skip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func caveThickness(r RandomSource) float32 {
|
||||
f := r.NextFloat()*2.0 + r.NextFloat()
|
||||
if r.NextIntN(10) == 0 {
|
||||
f *= r.NextFloat()*r.NextFloat()*3.0 + 1.0
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// caveSkip is CaveWorldCarver's CarveSkipChecker: an ellipsoid, with everything
|
||||
// below the sampled floor level cut flat so tunnels have a floor to walk on.
|
||||
func caveSkip(floorLevel float64) skipChecker {
|
||||
return func(relX, relY, relZ float64, _ int) bool {
|
||||
if relY <= floorLevel {
|
||||
return true
|
||||
}
|
||||
return relX*relX+relY*relY+relZ*relZ >= 1.0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *carveState) caveTunnel(cfg *carverConfig, seed int64, x, y, z, horizontalMul, verticalMul float64,
|
||||
thickness, yaw, pitch float32, branchIndex, branchCount int, yScale float64, skip skipChecker) {
|
||||
r := NewLegacy(seed)
|
||||
branchAt := int(r.NextIntN(int32(branchCount/2))) + branchCount/4
|
||||
gentle := r.NextIntN(6) == 0
|
||||
var yawDelta, pitchDelta float32
|
||||
|
||||
for j := branchIndex; j < branchCount; j++ {
|
||||
horizontal := 1.5 + float64(MthSin(float64(math.Pi*float32(j)/float32(branchCount)))*thickness)
|
||||
vertical := horizontal * yScale
|
||||
cosPitch := MthCos(float64(pitch))
|
||||
x += float64(MthCos(float64(yaw)) * cosPitch)
|
||||
y += float64(MthSin(float64(pitch)))
|
||||
z += float64(MthSin(float64(yaw)) * cosPitch)
|
||||
if gentle {
|
||||
pitch *= 0.92
|
||||
} else {
|
||||
pitch *= 0.7
|
||||
}
|
||||
pitch += pitchDelta * 0.1
|
||||
yaw += yawDelta * 0.1
|
||||
pitchDelta *= 0.9
|
||||
yawDelta *= 0.75
|
||||
pitchDelta += (r.NextFloat() - r.NextFloat()) * r.NextFloat() * 2.0
|
||||
yawDelta += (r.NextFloat() - r.NextFloat()) * r.NextFloat() * 4.0
|
||||
|
||||
if j == branchAt && thickness > 1.0 {
|
||||
// Two branches at right angles, and the parent stops. Branch
|
||||
// thickness is always below 1, so this never recurses further.
|
||||
s.caveTunnel(cfg, r.NextLong(), x, y, z, horizontalMul, verticalMul,
|
||||
r.NextFloat()*0.5+0.5, yaw-1.5707964, pitch/3.0, j, branchCount, 1.0, skip)
|
||||
s.caveTunnel(cfg, r.NextLong(), x, y, z, horizontalMul, verticalMul,
|
||||
r.NextFloat()*0.5+0.5, yaw+1.5707964, pitch/3.0, j, branchCount, 1.0, skip)
|
||||
return
|
||||
}
|
||||
if r.NextIntN(4) == 0 {
|
||||
continue // the walk advanced but carves nothing
|
||||
}
|
||||
if !s.canReach(x, z, j, branchCount, thickness) {
|
||||
return
|
||||
}
|
||||
s.carveEllipsoid(cfg, x, y, z, horizontal*horizontalMul, vertical*verticalMul, skip)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- canyon ------------------------------------------------------------
|
||||
|
||||
func (s *carveState) carveCanyon(cfg *carverConfig, random *Legacy, sourceX, sourceZ int) {
|
||||
x := float64(sourceX<<4 + int(random.NextIntN(16)))
|
||||
y := float64(cfg.y.sample(random, s.c.minY, s.c.height))
|
||||
z := float64(sourceZ<<4 + int(random.NextIntN(16)))
|
||||
yaw := random.NextFloat() * 6.2831855
|
||||
pitch := cfg.verticalRotation.sample(random)
|
||||
yScale := float64(cfg.yScale.sample(random))
|
||||
thickness := cfg.shape.thickness.sample(random)
|
||||
branchCount := int(float32(carveDistance) * cfg.shape.distanceFactor.sample(random))
|
||||
s.canyonWalk(cfg, random.NextLong(), x, y, z, thickness, yaw, pitch, 0, branchCount, yScale)
|
||||
}
|
||||
|
||||
func (s *carveState) canyonWalk(cfg *carverConfig, seed int64, x, y, z float64,
|
||||
thickness, yaw, pitch float32, branchIndex, branchCount int, yScale float64) {
|
||||
r := NewLegacy(seed)
|
||||
widthFactors := s.canyonWidthFactors(cfg, r)
|
||||
skip := canyonSkip(widthFactors, s.c.minY)
|
||||
var yawDelta, pitchDelta float32
|
||||
|
||||
for i := branchIndex; i < branchCount; i++ {
|
||||
horizontal := 1.5 + float64(MthSin(float64(float32(i)*3.1415927/float32(branchCount)))*thickness)
|
||||
vertical := horizontal * yScale
|
||||
horizontal *= float64(cfg.shape.horizontalRadiusFactor.sample(r))
|
||||
vertical = s.canyonVerticalRadius(cfg, r, vertical, float32(branchCount), float32(i))
|
||||
cosPitch := MthCos(float64(pitch))
|
||||
sinPitch := MthSin(float64(pitch))
|
||||
x += float64(MthCos(float64(yaw)) * cosPitch)
|
||||
y += float64(sinPitch)
|
||||
z += float64(MthSin(float64(yaw)) * cosPitch)
|
||||
pitch *= 0.7
|
||||
pitch += pitchDelta * 0.05
|
||||
yaw += yawDelta * 0.05
|
||||
pitchDelta *= 0.8
|
||||
yawDelta *= 0.5
|
||||
pitchDelta += (r.NextFloat() - r.NextFloat()) * r.NextFloat() * 2.0
|
||||
yawDelta += (r.NextFloat() - r.NextFloat()) * r.NextFloat() * 4.0
|
||||
if r.NextIntN(4) == 0 {
|
||||
continue
|
||||
}
|
||||
if !s.canReach(x, z, i, branchCount, thickness) {
|
||||
return
|
||||
}
|
||||
s.carveEllipsoid(cfg, x, y, z, horizontal, vertical, skip)
|
||||
}
|
||||
}
|
||||
|
||||
// canyonWidthFactors is initWidthFactors: a per-Y width multiplier that only
|
||||
// changes every few levels, which is what gives a ravine its ledges. It runs
|
||||
// once per ravine and consumes the front of the inner random stream.
|
||||
func (s *carveState) canyonWidthFactors(cfg *carverConfig, r RandomSource) []float32 {
|
||||
factors := make([]float32, s.c.height)
|
||||
value := float32(1.0)
|
||||
for i := range factors {
|
||||
if i == 0 || r.NextIntN(int32(cfg.shape.widthSmoothness)) == 0 {
|
||||
value = 1.0 + r.NextFloat()*r.NextFloat()
|
||||
}
|
||||
factors[i] = value * value
|
||||
}
|
||||
return factors
|
||||
}
|
||||
|
||||
// canyonVerticalRadius is updateVerticalRadius. With the shipped canyon config
|
||||
// the factor works out to exactly 1, but the draw still happens and removing it
|
||||
// would shift every subsequent value.
|
||||
func (s *carveState) canyonVerticalRadius(cfg *carverConfig, r RandomSource, vertical float64, branchCount, index float32) float64 {
|
||||
taper := 1.0 - float32(math.Abs(float64(0.5-index/branchCount)))*2.0
|
||||
factor := cfg.shape.verticalRadiusDefaultFactor + cfg.shape.verticalRadiusCenterFactor*taper
|
||||
return float64(factor) * vertical * float64(randomBetween(r, 0.75, 1.0))
|
||||
}
|
||||
|
||||
func canyonSkip(widthFactors []float32, minY int) skipChecker {
|
||||
return func(relX, relY, relZ float64, blockY int) bool {
|
||||
index := blockY - minY - 1
|
||||
if index < 0 || index >= len(widthFactors) {
|
||||
return true
|
||||
}
|
||||
return (relX*relX+relZ*relZ)*float64(widthFactors[index])+(relY*relY)/6.0 >= 1.0
|
||||
}
|
||||
}
|
||||
|
||||
// ---- shared carving ----------------------------------------------------
|
||||
|
||||
// skipChecker is CarveSkipChecker: given a position's offset from the ellipsoid
|
||||
// centre, decide whether to leave it alone.
|
||||
type skipChecker func(relX, relY, relZ float64, blockY int) bool
|
||||
|
||||
// canReach abandons a tunnel once it can no longer reach the chunk being
|
||||
// carved, even walking straight at it for every remaining step.
|
||||
func (s *carveState) canReach(x, z float64, branchIndex, branchCount int, thickness float32) bool {
|
||||
middleX := float64(s.chunkX<<4 + 8)
|
||||
middleZ := float64(s.chunkZ<<4 + 8)
|
||||
dx := x - middleX
|
||||
dz := z - middleZ
|
||||
remaining := float64(branchCount - branchIndex)
|
||||
reach := float64(thickness + 2.0 + 16.0)
|
||||
return dx*dx+dz*dz-remaining*remaining <= reach*reach
|
||||
}
|
||||
|
||||
// carveEllipsoid cuts one blob out of the target chunk. Everything a carver
|
||||
// writes goes through here, which is why a tunnel seeded eight chunks away can
|
||||
// never touch a block outside the chunk being generated.
|
||||
func (s *carveState) carveEllipsoid(cfg *carverConfig, x, y, z, horizontal, vertical float64, skip skipChecker) {
|
||||
middleX := float64(s.chunkX<<4 + 8)
|
||||
middleZ := float64(s.chunkZ<<4 + 8)
|
||||
bound := 16.0 + horizontal*2.0
|
||||
if math.Abs(x-middleX) > bound || math.Abs(z-middleZ) > bound {
|
||||
return
|
||||
}
|
||||
minBlockX := s.chunkX << 4
|
||||
minBlockZ := s.chunkZ << 4
|
||||
x0 := max(mthFloor(x-horizontal)-minBlockX-1, 0)
|
||||
x1 := min(mthFloor(x+horizontal)-minBlockX, 15)
|
||||
z0 := max(mthFloor(z-horizontal)-minBlockZ-1, 0)
|
||||
z1 := min(mthFloor(z+horizontal)-minBlockZ, 15)
|
||||
// The seven-block margin below the world roof is vanilla's, for chunks that
|
||||
// are not being upgraded from an older world.
|
||||
yLo := max(mthFloor(y-vertical)-1, s.c.minY+1)
|
||||
yHi := min(mthFloor(y+vertical)+1, s.c.minY+s.c.height-1-7)
|
||||
|
||||
for lx := x0; lx <= x1; lx++ {
|
||||
blockX := minBlockX + lx
|
||||
relX := (float64(blockX) + 0.5 - x) / horizontal
|
||||
for lz := z0; lz <= z1; lz++ {
|
||||
blockZ := minBlockZ + lz
|
||||
relZ := (float64(blockZ) + 0.5 - z) / horizontal
|
||||
if relX*relX+relZ*relZ >= 1.0 {
|
||||
continue
|
||||
}
|
||||
// Reset per column: a tunnel that breaks the surface in one column
|
||||
// has not broken it in the next.
|
||||
reachedSurface := false
|
||||
for by := yHi; by > yLo; by-- {
|
||||
relY := (float64(by) - 0.5 - y) / vertical
|
||||
if skip(relX, relY, relZ, by) {
|
||||
continue
|
||||
}
|
||||
if s.mask.get(lx, by, lz) {
|
||||
continue
|
||||
}
|
||||
s.mask.set(lx, by, lz)
|
||||
s.carveBlock(cfg, lx, by, lz, blockX, blockZ, &reachedSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *carveState) carveBlock(cfg *carverConfig, lx, by, lz, blockX, blockZ int, reachedSurface *bool) {
|
||||
old := s.target.Block(lx, by, lz)
|
||||
if isSurfaceTop(old) {
|
||||
*reachedSurface = true
|
||||
}
|
||||
if !s.target.Replaceable(old) {
|
||||
return
|
||||
}
|
||||
carved, ok := s.carveStateAt(blockX, by, blockZ)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
s.target.SetBlock(lx, by, lz, carved)
|
||||
if !*reachedSurface {
|
||||
return
|
||||
}
|
||||
// Cutting a grass block away leaves plain dirt showing. Vanilla re-runs the
|
||||
// surface rule on it so a cave mouth in a podzol forest is not a dirt scar.
|
||||
if s.target.Block(lx, by-1, lz) != blockDirt {
|
||||
return
|
||||
}
|
||||
if top, ok := s.target.TopMaterial(lx, by-1, lz, isCarvedFluid(carved)); ok {
|
||||
s.target.SetBlock(lx, by-1, lz, top)
|
||||
}
|
||||
}
|
||||
|
||||
// carveStateAt is getCarveState: lava below the configured level, otherwise
|
||||
// whatever the aquifer would put in an empty position. A nil answer from the
|
||||
// aquifer means the rock stays.
|
||||
func (s *carveState) carveStateAt(x, y, z int) (uint16, bool) {
|
||||
if y <= s.lavaY {
|
||||
return blockLava, true
|
||||
}
|
||||
if s.aq == nil {
|
||||
return blockAir, true
|
||||
}
|
||||
return s.aq.ComputeSubstance(x, y, z, 0.0)
|
||||
}
|
||||
|
||||
// Block states the carver compares against directly. Grass and mycelium are
|
||||
// block-level tests in vanilla, so every state counts.
|
||||
const (
|
||||
blockDirt uint16 = 10
|
||||
blockGrassSnowy uint16 = 8
|
||||
blockGrass uint16 = 9
|
||||
blockMyceliumSnowy uint16 = 8918
|
||||
blockMycelium uint16 = 8919
|
||||
)
|
||||
|
||||
func isSurfaceTop(state uint16) bool {
|
||||
switch state {
|
||||
case blockGrass, blockGrassSnowy, blockMycelium, blockMyceliumSnowy:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isCarvedFluid(state uint16) bool { return state == blockWater || state == blockLava }
|
||||
78
internal/worldgen/carver_random_test.go
Normal file
78
internal/worldgen/carver_random_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
package worldgen
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSetLargeFeatureSeedParity checks the carver seeding against values
|
||||
// captured by running WorldgenRandom against the 26.1.2 jar.
|
||||
//
|
||||
// This is the single most dangerous primitive in the carvers: it decides which
|
||||
// chunks start a cave and where. It also looks almost exactly like
|
||||
// setDecorationSeed, which combines its two products with addition rather than
|
||||
// XOR — getting them the wrong way round moves every tunnel in the world and
|
||||
// nothing else complains.
|
||||
func TestSetLargeFeatureSeedParity(t *testing.T) {
|
||||
cases := []struct {
|
||||
seed int64
|
||||
chunkX, chunkZ int
|
||||
wantFloat float32
|
||||
wantInt16 int32
|
||||
wantLong int64
|
||||
}{
|
||||
{12345, 0, 0, 0.361803055, 8, -1236052134575208584},
|
||||
{12345, 1, -3, 0.430853248, 15, 2333035266122422630},
|
||||
{12345, -17, 42, 0.756100893, 5, 3625585156103593602},
|
||||
{12346, 0, 0, 0.362071812, 11, 7828065674307726589},
|
||||
{12346, 1, -3, 0.904893756, 15, 6725298717481824139},
|
||||
{12346, -17, 42, 0.276341736, 9, 1248907841123878499},
|
||||
{12347, 0, 0, 0.361982226, 15, -7491136309694630448},
|
||||
{12347, 1, -3, 0.511853278, 14, 7423281404700161236},
|
||||
{12347, -17, 42, 0.031917453, 11, -7937379058403548373},
|
||||
}
|
||||
r := NewLegacy(0)
|
||||
for _, c := range cases {
|
||||
r.SetLargeFeatureSeed(c.seed, c.chunkX, c.chunkZ)
|
||||
if got := r.NextFloat(); math.Abs(float64(got-c.wantFloat)) > 1e-7 {
|
||||
t.Errorf("seed %d chunk (%d,%d): nextFloat = %v, want %v", c.seed, c.chunkX, c.chunkZ, got, c.wantFloat)
|
||||
}
|
||||
if got := r.NextIntN(16); got != c.wantInt16 {
|
||||
t.Errorf("seed %d chunk (%d,%d): nextInt(16) = %d, want %d", c.seed, c.chunkX, c.chunkZ, got, c.wantInt16)
|
||||
}
|
||||
if got := r.NextLong(); got != c.wantLong {
|
||||
t.Errorf("seed %d chunk (%d,%d): nextLong = %d, want %d", c.seed, c.chunkX, c.chunkZ, got, c.wantLong)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMthTrigParity pins the sine table against the jar. Mth.sin is a
|
||||
// 65536-entry lookup, not libm: it is visibly less accurate, and a tunnel that
|
||||
// walks by adding cos(yaw) a hundred times drifts somewhere else entirely if
|
||||
// the difference is smoothed away.
|
||||
func TestMthTrigParity(t *testing.T) {
|
||||
cases := []struct {
|
||||
in float64
|
||||
sin, cos float32
|
||||
}{
|
||||
{-1.0, -0.841451406, 0.540252149},
|
||||
{0.0, 0.0, 1.0},
|
||||
{0.5, 0.479409635, 0.877591252},
|
||||
{3.1415927, 0.0, -1.0},
|
||||
{-6.2831855, 0.0, 1.0},
|
||||
{100.25, -0.277322441, 0.960776925},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := MthSin(c.in); math.Abs(float64(got-c.sin)) > 1e-7 {
|
||||
t.Errorf("MthSin(%v) = %v, want %v", c.in, got, c.sin)
|
||||
}
|
||||
if got := MthCos(c.in); math.Abs(float64(got-c.cos)) > 1e-7 {
|
||||
t.Errorf("MthCos(%v) = %v, want %v", c.in, got, c.cos)
|
||||
}
|
||||
}
|
||||
// The table is deliberately coarse; if this ever matches libm the lookup
|
||||
// has been replaced by math.Sin and every carver has moved.
|
||||
if math.Abs(float64(MthSin(-1.0))-math.Sin(-1.0)) < 1e-6 {
|
||||
t.Error("MthSin agrees with math.Sin to within 1e-6; the lookup table is gone")
|
||||
}
|
||||
}
|
||||
70
internal/worldgen/data/carver/canyon.json
Normal file
70
internal/worldgen/data/carver/canyon.json
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"type": "minecraft:canyon",
|
||||
"config": {
|
||||
"debug_settings": {
|
||||
"air_state": {
|
||||
"Name": "minecraft:warped_button",
|
||||
"Properties": {
|
||||
"face": "wall",
|
||||
"facing": "north",
|
||||
"powered": "false"
|
||||
}
|
||||
},
|
||||
"barrier_state": {
|
||||
"Name": "minecraft:glass"
|
||||
},
|
||||
"lava_state": {
|
||||
"Name": "minecraft:orange_stained_glass"
|
||||
},
|
||||
"water_state": {
|
||||
"Name": "minecraft:candle",
|
||||
"Properties": {
|
||||
"candles": "1",
|
||||
"lit": "false",
|
||||
"waterlogged": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lava_level": {
|
||||
"above_bottom": 8
|
||||
},
|
||||
"probability": 0.01,
|
||||
"replaceable": "#minecraft:overworld_carver_replaceables",
|
||||
"shape": {
|
||||
"distance_factor": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": 1.0,
|
||||
"min_inclusive": 0.75
|
||||
},
|
||||
"horizontal_radius_factor": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": 1.0,
|
||||
"min_inclusive": 0.75
|
||||
},
|
||||
"thickness": {
|
||||
"type": "minecraft:trapezoid",
|
||||
"max": 6.0,
|
||||
"min": 0.0,
|
||||
"plateau": 2.0
|
||||
},
|
||||
"vertical_radius_center_factor": 0.0,
|
||||
"vertical_radius_default_factor": 1.0,
|
||||
"width_smoothness": 3
|
||||
},
|
||||
"vertical_rotation": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": 0.125,
|
||||
"min_inclusive": -0.125
|
||||
},
|
||||
"y": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": {
|
||||
"absolute": 67
|
||||
},
|
||||
"min_inclusive": {
|
||||
"absolute": 10
|
||||
}
|
||||
},
|
||||
"yScale": 3.0
|
||||
}
|
||||
}
|
||||
63
internal/worldgen/data/carver/cave.json
Normal file
63
internal/worldgen/data/carver/cave.json
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"type": "minecraft:cave",
|
||||
"config": {
|
||||
"debug_settings": {
|
||||
"air_state": {
|
||||
"Name": "minecraft:crimson_button",
|
||||
"Properties": {
|
||||
"face": "wall",
|
||||
"facing": "north",
|
||||
"powered": "false"
|
||||
}
|
||||
},
|
||||
"barrier_state": {
|
||||
"Name": "minecraft:glass"
|
||||
},
|
||||
"lava_state": {
|
||||
"Name": "minecraft:orange_stained_glass"
|
||||
},
|
||||
"water_state": {
|
||||
"Name": "minecraft:candle",
|
||||
"Properties": {
|
||||
"candles": "1",
|
||||
"lit": "false",
|
||||
"waterlogged": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
"floor_level": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": -0.4,
|
||||
"min_inclusive": -1.0
|
||||
},
|
||||
"horizontal_radius_multiplier": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": 1.4,
|
||||
"min_inclusive": 0.7
|
||||
},
|
||||
"lava_level": {
|
||||
"above_bottom": 8
|
||||
},
|
||||
"probability": 0.15,
|
||||
"replaceable": "#minecraft:overworld_carver_replaceables",
|
||||
"vertical_radius_multiplier": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": 1.3,
|
||||
"min_inclusive": 0.8
|
||||
},
|
||||
"y": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": {
|
||||
"absolute": 180
|
||||
},
|
||||
"min_inclusive": {
|
||||
"above_bottom": 8
|
||||
}
|
||||
},
|
||||
"yScale": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": 0.9,
|
||||
"min_inclusive": 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
63
internal/worldgen/data/carver/cave_extra_underground.json
Normal file
63
internal/worldgen/data/carver/cave_extra_underground.json
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"type": "minecraft:cave",
|
||||
"config": {
|
||||
"debug_settings": {
|
||||
"air_state": {
|
||||
"Name": "minecraft:oak_button",
|
||||
"Properties": {
|
||||
"face": "wall",
|
||||
"facing": "north",
|
||||
"powered": "false"
|
||||
}
|
||||
},
|
||||
"barrier_state": {
|
||||
"Name": "minecraft:glass"
|
||||
},
|
||||
"lava_state": {
|
||||
"Name": "minecraft:orange_stained_glass"
|
||||
},
|
||||
"water_state": {
|
||||
"Name": "minecraft:candle",
|
||||
"Properties": {
|
||||
"candles": "1",
|
||||
"lit": "false",
|
||||
"waterlogged": "false"
|
||||
}
|
||||
}
|
||||
},
|
||||
"floor_level": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": -0.4,
|
||||
"min_inclusive": -1.0
|
||||
},
|
||||
"horizontal_radius_multiplier": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": 1.4,
|
||||
"min_inclusive": 0.7
|
||||
},
|
||||
"lava_level": {
|
||||
"above_bottom": 8
|
||||
},
|
||||
"probability": 0.07,
|
||||
"replaceable": "#minecraft:overworld_carver_replaceables",
|
||||
"vertical_radius_multiplier": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": 1.3,
|
||||
"min_inclusive": 0.8
|
||||
},
|
||||
"y": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_inclusive": {
|
||||
"absolute": 47
|
||||
},
|
||||
"min_inclusive": {
|
||||
"above_bottom": 8
|
||||
}
|
||||
},
|
||||
"yScale": {
|
||||
"type": "minecraft:uniform",
|
||||
"max_exclusive": 0.9,
|
||||
"min_inclusive": 0.1
|
||||
}
|
||||
}
|
||||
}
|
||||
57
internal/worldgen/data/carver/replaceable.json
Normal file
57
internal/worldgen/data/carver/replaceable.json
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"_comment": "Flattened #minecraft:overworld_carver_replaceables from the 26.1.2 jar. The source tag nests other tags; this is its transitive expansion, which is what the carver's block test resolves to.",
|
||||
"values": [
|
||||
"minecraft:andesite",
|
||||
"minecraft:black_terracotta",
|
||||
"minecraft:blue_terracotta",
|
||||
"minecraft:brown_terracotta",
|
||||
"minecraft:calcite",
|
||||
"minecraft:coarse_dirt",
|
||||
"minecraft:copper_ore",
|
||||
"minecraft:cyan_terracotta",
|
||||
"minecraft:deepslate",
|
||||
"minecraft:deepslate_copper_ore",
|
||||
"minecraft:deepslate_iron_ore",
|
||||
"minecraft:diorite",
|
||||
"minecraft:dirt",
|
||||
"minecraft:granite",
|
||||
"minecraft:grass_block",
|
||||
"minecraft:gravel",
|
||||
"minecraft:gray_terracotta",
|
||||
"minecraft:green_terracotta",
|
||||
"minecraft:iron_ore",
|
||||
"minecraft:light_blue_terracotta",
|
||||
"minecraft:light_gray_terracotta",
|
||||
"minecraft:lime_terracotta",
|
||||
"minecraft:magenta_terracotta",
|
||||
"minecraft:moss_block",
|
||||
"minecraft:mud",
|
||||
"minecraft:muddy_mangrove_roots",
|
||||
"minecraft:mycelium",
|
||||
"minecraft:orange_terracotta",
|
||||
"minecraft:packed_ice",
|
||||
"minecraft:pale_moss_block",
|
||||
"minecraft:pink_terracotta",
|
||||
"minecraft:podzol",
|
||||
"minecraft:powder_snow",
|
||||
"minecraft:purple_terracotta",
|
||||
"minecraft:raw_copper_block",
|
||||
"minecraft:raw_iron_block",
|
||||
"minecraft:red_sand",
|
||||
"minecraft:red_sandstone",
|
||||
"minecraft:red_terracotta",
|
||||
"minecraft:rooted_dirt",
|
||||
"minecraft:sand",
|
||||
"minecraft:sandstone",
|
||||
"minecraft:snow",
|
||||
"minecraft:snow_block",
|
||||
"minecraft:stone",
|
||||
"minecraft:suspicious_gravel",
|
||||
"minecraft:suspicious_sand",
|
||||
"minecraft:terracotta",
|
||||
"minecraft:tuff",
|
||||
"minecraft:water",
|
||||
"minecraft:white_terracotta",
|
||||
"minecraft:yellow_terracotta"
|
||||
]
|
||||
}
|
||||
46
internal/worldgen/mth.go
Normal file
46
internal/worldgen/mth.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package worldgen
|
||||
|
||||
import "math"
|
||||
|
||||
// mth.go holds the bits of net.minecraft.util.Mth whose exact behaviour the
|
||||
// generator depends on.
|
||||
//
|
||||
// The trigonometry is a 65536-entry lookup table, not the libm functions. That
|
||||
// is not an optimisation detail to be tidied away: Mth.sin(-1.0) is -0.8414514
|
||||
// where Math.sin(-1.0) is -0.8414709848078965, and every carver tunnel walks by
|
||||
// repeatedly adding cos(yaw) and sin(pitch). Substituting math.Sin bends every
|
||||
// tunnel in the world away from vanilla's.
|
||||
|
||||
const mthSinScale = 10430.378350470453
|
||||
|
||||
var mthSinTable [65536]float32
|
||||
|
||||
func init() {
|
||||
for i := range mthSinTable {
|
||||
mthSinTable[i] = float32(math.Sin(float64(i) / mthSinScale))
|
||||
}
|
||||
}
|
||||
|
||||
// MthSin is Mth.sin. The float-to-int conversion truncates towards zero in both
|
||||
// Go and Java, and the mask makes the negative case wrap identically.
|
||||
func MthSin(d float64) float32 {
|
||||
return mthSinTable[int64(d*mthSinScale)&65535]
|
||||
}
|
||||
|
||||
// MthCos is Mth.cos: the same table, a quarter turn along.
|
||||
func MthCos(d float64) float32 {
|
||||
return mthSinTable[int64(d*mthSinScale+16384.0)&65535]
|
||||
}
|
||||
|
||||
// mthFloor is Mth.floor: a true floor, not a truncating cast.
|
||||
func mthFloor(d float64) int { return int(math.Floor(d)) }
|
||||
|
||||
// randomBetween is Mth.randomBetween: a float in [lo, hi), one draw.
|
||||
func randomBetween(r RandomSource, lo, hi float32) float32 {
|
||||
return r.NextFloat()*(hi-lo) + lo
|
||||
}
|
||||
|
||||
// randomBetweenInclusive is Mth.randomBetweenInclusive: an int in [lo, hi].
|
||||
func randomBetweenInclusive(r RandomSource, lo, hi int) int {
|
||||
return int(r.NextIntN(int32(hi-lo+1))) + lo
|
||||
}
|
||||
|
|
@ -172,7 +172,28 @@ type Legacy struct{ seed uint64 }
|
|||
|
||||
// NewLegacy seeds a Legacy source, applying Java's seed scramble.
|
||||
func NewLegacy(seed int64) *Legacy {
|
||||
return &Legacy{seed: (uint64(seed) ^ lcgMultiplier) & lcgMask}
|
||||
r := &Legacy{}
|
||||
r.SetSeed(seed)
|
||||
return r
|
||||
}
|
||||
|
||||
// SetSeed is java.util.Random.setSeed, which worldgen reseeds in place.
|
||||
func (r *Legacy) SetSeed(seed int64) {
|
||||
r.seed = (uint64(seed) ^ lcgMultiplier) & lcgMask
|
||||
}
|
||||
|
||||
// SetLargeFeatureSeed is WorldgenRandom.setLargeFeatureSeed: seed from the
|
||||
// world seed, draw two longs, and reseed from those mixed with the chunk
|
||||
// coordinates.
|
||||
//
|
||||
// The two products are combined with XOR. setDecorationSeed, which looks almost
|
||||
// identical, uses addition and forces the low bit — they are different methods
|
||||
// and confusing them silently moves every carver in the world.
|
||||
func (r *Legacy) SetLargeFeatureSeed(seed int64, chunkX, chunkZ int) {
|
||||
r.SetSeed(seed)
|
||||
a := r.NextLong()
|
||||
b := r.NextLong()
|
||||
r.SetSeed(int64(chunkX)*a ^ int64(chunkZ)*b ^ seed)
|
||||
}
|
||||
|
||||
// next returns the top `b` bits of the next LCG state.
|
||||
|
|
|
|||
|
|
@ -340,11 +340,17 @@ func (p *surfaceParser) noiseSlot(name string) (int, error) {
|
|||
|
||||
// resolveAnchor is VerticalAnchor.resolveY.
|
||||
func (p *surfaceParser) resolveAnchor(a anchorJSON) int {
|
||||
return resolveAnchorY(a, p.minY, p.height)
|
||||
}
|
||||
|
||||
// resolveAnchorY is VerticalAnchor.resolveY against explicit world bounds. The
|
||||
// carver configs use the same anchor shape as the surface rules.
|
||||
func resolveAnchorY(a anchorJSON, minY, height int) int {
|
||||
switch a.kind {
|
||||
case anchorAboveBottom:
|
||||
return p.minY + a.value
|
||||
return minY + a.value
|
||||
case anchorBelowTop:
|
||||
return p.minY + p.height - 1 - a.value
|
||||
return minY + height - 1 - a.value
|
||||
default:
|
||||
return a.value
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue