Place biome-driven water and lava springs

This commit is contained in:
Daniar Mannanov 2026-08-10 23:29:20 +03:00
parent dd2ea56850
commit 2bfa3155f3
7 changed files with 184 additions and 6 deletions

View file

@ -89,10 +89,10 @@ func resolveOreTargets(set *worldgen.FeatureSet, config worldgen.OreFeatureConfi
func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY, originZ, size int, discard float64, targets []resolvedOreTarget) {
angle := float64(random.NextFloat()) * math.Pi
extent := float64(size) / 8.0
x0 := float64(originX+8) + math.Sin(angle)*extent
x1 := float64(originX+8) - math.Sin(angle)*extent
z0 := float64(originZ+8) + math.Cos(angle)*extent
z1 := float64(originZ+8) - math.Cos(angle)*extent
x0 := float64(originX) + math.Sin(angle)*extent
x1 := float64(originX) - math.Sin(angle)*extent
z0 := float64(originZ) + math.Cos(angle)*extent
z1 := float64(originZ) - math.Cos(angle)*extent
y0 := float64(originY + int(random.NextIntN(3)) - 2)
y1 := float64(originY + int(random.NextIntN(3)) - 2)

92
internal/world/springs.go Normal file
View file

@ -0,0 +1,92 @@
package world
import "regionio/internal/worldgen"
const fluidsStage = 8
func placeVanillaSprings(c *Chunk, seed int64, cx, cz int32, biomes *[16][16]string) {
set, err := worldgen.LoadFeatureSet()
if err != nil {
panic("world: loading feature datapack: " + err.Error())
}
random, decorationSeed := worldgen.DecorationRandom(seed, int(cx), int(cz))
seen := make(map[string]bool)
for bx := 0; bx < 16; bx += 4 {
for bz := 0; bz < 16; bz += 4 {
stages := set.Biomes[biomes[bx][bz]].Features
if len(stages) <= fluidsStage {
continue
}
for featureIndex, name := range stages[fluidsStage] {
if seen[name] {
continue
}
seen[name] = true
placed, ok := set.Placed[name]
if !ok {
continue
}
configured, ok := set.Configured[placed.Feature]
if !ok || configured.Type != "minecraft:spring_feature" {
continue
}
config, err := set.Spring(placed.Feature)
if err != nil {
panic(err)
}
plan, err := set.Placement(name)
if err != nil {
panic(err)
}
random.SetFeatureSeed(decorationSeed, featureIndex, fluidsStage)
for attempt := 0; attempt < plan.Count.Sample(random); attempt++ {
x := int(random.NextIntN(16))
z := int(random.NextIntN(16))
y := plan.SampleY(random, MinY, WorldHeight)
placeSpring(c, x, y, z, config)
}
}
}
}
}
func placeSpring(c *Chunk, x, y, z int, config worldgen.SpringFeatureConfig) {
valid := make(map[uint16]bool, len(config.ValidBlocks))
for _, name := range config.ValidBlocks {
if state, ok := nameToStateID(name, nil); ok {
valid[state] = true
}
}
state, ok := springStateID(config.State)
if !ok || !valid[c.GetBlock(x, y+1, z)] {
return
}
if config.RequiresBlockBelow && !valid[c.GetBlock(x, y-1, z)] {
return
}
rock := 0
holes := 0
for _, offset := range [][3]int{{-1, 0, 0}, {1, 0, 0}, {0, 0, -1}, {0, 0, 1}, {0, -1, 0}} {
nx, ny, nz := x+offset[0], y+offset[1], z+offset[2]
if nx < 0 || nx >= 16 || nz < 0 || nz >= 16 {
return
}
if valid[c.GetBlock(nx, ny, nz)] {
rock++
} else if c.GetBlock(nx, ny, nz) == StateAir {
holes++
}
}
if rock != config.RockCount || holes != config.HoleCount {
return
}
c.SetBlock(x, y, z, state)
}
func springStateID(state worldgen.BlockState) (uint16, bool) {
props := state.Properties
if (state.Name == "minecraft:water" || state.Name == "minecraft:lava") && props["falling"] == "true" {
props = map[string]string{"level": "8"}
}
return nameToStateID(state.Name, props)
}

View file

@ -0,0 +1,39 @@
package world
import (
"testing"
"regionio/internal/worldgen"
)
func TestSpringFeaturePlacesFallingFluid(t *testing.T) {
chunk := NewChunk(0, 0, BiomePlains)
valid := []string{"minecraft:stone"}
for _, pos := range [][3]int{{8, 11, 8}, {8, 9, 8}, {7, 10, 8}, {9, 10, 8}, {8, 10, 7}} {
chunk.setBlockRaw(pos[0], pos[1], pos[2], StateStone)
}
config := worldgen.SpringFeatureConfig{
HoleCount: 1, RequiresBlockBelow: true, RockCount: 4,
State: worldgen.BlockState{Name: "minecraft:water", Properties: map[string]string{"falling": "true"}},
ValidBlocks: valid,
}
placeSpring(chunk, 8, 10, 8, config)
falling, ok := nameToStateID("minecraft:water", map[string]string{"level": "8"})
if !ok || chunk.GetBlock(8, 10, 8) != falling {
t.Fatalf("spring state = %d, want falling water %d", chunk.GetBlock(8, 10, 8), falling)
}
}
func TestPlacedSpringsAreDeterministic(t *testing.T) {
gen := NewVanillaGenerator(12345)
a, b := gen(-3, 4), gen(-3, 4)
for y := MinY; y < MinY+WorldHeight; y++ {
for x := 0; x < 16; x++ {
for z := 0; z < 16; z++ {
if got, want := a.GetBlock(x, y, z), b.GetBlock(x, y, z); got != want {
t.Fatalf("block (%d,%d,%d): first %d second %d", x, y, z, got, want)
}
}
}
}
}

View file

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

View file

@ -479,6 +479,7 @@ func decorate(c *Chunk, od *worldgen.OverworldDensity, cx, cz int32, seed int64,
r := newChunkRand(cx, cz, seed)
placeVanillaOres(c, seed, cx, cz, biomeName)
placeVanillaSprings(c, seed, cx, cz, biomeName)
placeFlora(c, &r, surfTop, grass, biomeName)
placeDesertFeatures(c, &r, surfTop, biomeName)
placeRocks(c, &r, surfTop, grass, biomeName)