diff --git a/internal/world/carve.go b/internal/world/carve.go new file mode 100644 index 0000000..6ce6576 --- /dev/null +++ b/internal/world/carve.go @@ -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] +} diff --git a/internal/world/carve_verify_test.go b/internal/world/carve_verify_test.go new file mode 100644 index 0000000..aa29feb --- /dev/null +++ b/internal/world/carve_verify_test.go @@ -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) + } +} diff --git a/internal/world/store.go b/internal/world/store.go index 3e305bc..a3d1a3d 100644 --- a/internal/world/store.go +++ b/internal/world/store.go @@ -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. diff --git a/internal/world/vanilla.go b/internal/world/vanilla.go index 6d1f267..4295a6b 100644 --- a/internal/world/vanilla.go +++ b/internal/world/vanilla.go @@ -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 diff --git a/internal/worldgen/carver.go b/internal/worldgen/carver.go new file mode 100644 index 0000000..2d59e9a --- /dev/null +++ b/internal/worldgen/carver.go @@ -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<>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 } diff --git a/internal/worldgen/carver_random_test.go b/internal/worldgen/carver_random_test.go new file mode 100644 index 0000000..a83dd01 --- /dev/null +++ b/internal/worldgen/carver_random_test.go @@ -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") + } +} diff --git a/internal/worldgen/data/carver/canyon.json b/internal/worldgen/data/carver/canyon.json new file mode 100644 index 0000000..0e00c48 --- /dev/null +++ b/internal/worldgen/data/carver/canyon.json @@ -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 + } +} \ No newline at end of file diff --git a/internal/worldgen/data/carver/cave.json b/internal/worldgen/data/carver/cave.json new file mode 100644 index 0000000..5beb3f6 --- /dev/null +++ b/internal/worldgen/data/carver/cave.json @@ -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 + } + } +} \ No newline at end of file diff --git a/internal/worldgen/data/carver/cave_extra_underground.json b/internal/worldgen/data/carver/cave_extra_underground.json new file mode 100644 index 0000000..bc1ac71 --- /dev/null +++ b/internal/worldgen/data/carver/cave_extra_underground.json @@ -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 + } + } +} \ No newline at end of file diff --git a/internal/worldgen/data/carver/replaceable.json b/internal/worldgen/data/carver/replaceable.json new file mode 100644 index 0000000..588fa99 --- /dev/null +++ b/internal/worldgen/data/carver/replaceable.json @@ -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" + ] +} diff --git a/internal/worldgen/mth.go b/internal/worldgen/mth.go new file mode 100644 index 0000000..8458a1d --- /dev/null +++ b/internal/worldgen/mth.go @@ -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 +} diff --git a/internal/worldgen/random.go b/internal/worldgen/random.go index b1e040a..e96500a 100644 --- a/internal/worldgen/random.go +++ b/internal/worldgen/random.go @@ -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. diff --git a/internal/worldgen/surface.go b/internal/worldgen/surface.go index e230445..0cd8776 100644 --- a/internal/worldgen/surface.go +++ b/internal/worldgen/surface.go @@ -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 }