Place biome-driven water and lava springs
This commit is contained in:
parent
dd2ea56850
commit
2bfa3155f3
7 changed files with 184 additions and 6 deletions
|
|
@ -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) {
|
func placeOreEllipsoid(c *Chunk, random worldgen.RandomSource, originX, originY, originZ, size int, discard float64, targets []resolvedOreTarget) {
|
||||||
angle := float64(random.NextFloat()) * math.Pi
|
angle := float64(random.NextFloat()) * math.Pi
|
||||||
extent := float64(size) / 8.0
|
extent := float64(size) / 8.0
|
||||||
x0 := float64(originX+8) + math.Sin(angle)*extent
|
x0 := float64(originX) + math.Sin(angle)*extent
|
||||||
x1 := float64(originX+8) - math.Sin(angle)*extent
|
x1 := float64(originX) - math.Sin(angle)*extent
|
||||||
z0 := float64(originZ+8) + math.Cos(angle)*extent
|
z0 := float64(originZ) + math.Cos(angle)*extent
|
||||||
z1 := float64(originZ+8) - math.Cos(angle)*extent
|
z1 := float64(originZ) - math.Cos(angle)*extent
|
||||||
y0 := float64(originY + int(random.NextIntN(3)) - 2)
|
y0 := float64(originY + int(random.NextIntN(3)) - 2)
|
||||||
y1 := float64(originY + int(random.NextIntN(3)) - 2)
|
y1 := float64(originY + int(random.NextIntN(3)) - 2)
|
||||||
|
|
||||||
|
|
|
||||||
92
internal/world/springs.go
Normal file
92
internal/world/springs.go
Normal 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)
|
||||||
|
}
|
||||||
39
internal/world/springs_test.go
Normal file
39
internal/world/springs_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,7 +33,7 @@ const dataVersion26 = 4790
|
||||||
// first time it ran: chunkAt prefers the store over the generator, so the
|
// first time it ran: chunkAt prefers the store over the generator, so the
|
||||||
// already-explored area around spawn keeps its old terrain and every later fix
|
// already-explored area around spawn keeps its old terrain and every later fix
|
||||||
// looks like it did nothing in exactly the place you are standing.
|
// looks like it did nothing in exactly the place you are standing.
|
||||||
const generatorVersion = 13
|
const generatorVersion = 14
|
||||||
|
|
||||||
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||||
// because it is ours, not part of the vanilla chunk format.
|
// because it is ours, not part of the vanilla chunk format.
|
||||||
|
|
|
||||||
|
|
@ -479,6 +479,7 @@ func decorate(c *Chunk, od *worldgen.OverworldDensity, cx, cz int32, seed int64,
|
||||||
r := newChunkRand(cx, cz, seed)
|
r := newChunkRand(cx, cz, seed)
|
||||||
|
|
||||||
placeVanillaOres(c, seed, cx, cz, biomeName)
|
placeVanillaOres(c, seed, cx, cz, biomeName)
|
||||||
|
placeVanillaSprings(c, seed, cx, cz, biomeName)
|
||||||
placeFlora(c, &r, surfTop, grass, biomeName)
|
placeFlora(c, &r, surfTop, grass, biomeName)
|
||||||
placeDesertFeatures(c, &r, surfTop, biomeName)
|
placeDesertFeatures(c, &r, surfTop, biomeName)
|
||||||
placeRocks(c, &r, surfTop, grass, biomeName)
|
placeRocks(c, &r, surfTop, grass, biomeName)
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,19 @@ type OreTarget struct {
|
||||||
} `json:"target"`
|
} `json:"target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SpringFeatureConfig struct {
|
||||||
|
HoleCount int `json:"hole_count"`
|
||||||
|
RequiresBlockBelow bool `json:"requires_block_below"`
|
||||||
|
RockCount int `json:"rock_count"`
|
||||||
|
State BlockState `json:"state"`
|
||||||
|
ValidBlocks []string `json:"valid_blocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockState struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Properties map[string]string `json:"Properties"`
|
||||||
|
}
|
||||||
|
|
||||||
type PlacementPlan struct {
|
type PlacementPlan struct {
|
||||||
Count CountProvider
|
Count CountProvider
|
||||||
RarityChance int
|
RarityChance int
|
||||||
|
|
@ -102,6 +115,15 @@ func (p PlacementPlan) SampleY(r RandomSource, minY, height int) int {
|
||||||
triangle := span - plateau
|
triangle := span - plateau
|
||||||
return lo + int(r.NextIntN(int32((triangle+1)/2))) + int(r.NextIntN(int32(triangle/2+1)))
|
return lo + int(r.NextIntN(int32((triangle+1)/2))) + int(r.NextIntN(int32(triangle/2+1)))
|
||||||
}
|
}
|
||||||
|
if p.HeightDistribution == "minecraft:very_biased_to_bottom" {
|
||||||
|
const inner = 8
|
||||||
|
outer := span - inner + 1
|
||||||
|
if outer <= 0 {
|
||||||
|
return lo
|
||||||
|
}
|
||||||
|
bound := int(r.NextIntN(int32(outer))) + inner
|
||||||
|
return lo + int(r.NextIntN(int32(bound)))
|
||||||
|
}
|
||||||
return lo + int(r.NextIntN(int32(span)))
|
return lo + int(r.NextIntN(int32(span)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,6 +166,21 @@ func (s *FeatureSet) Ore(name string) (OreFeatureConfig, error) {
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FeatureSet) Spring(name string) (SpringFeatureConfig, error) {
|
||||||
|
configured, ok := s.Configured[name]
|
||||||
|
if !ok || configured.Type != "minecraft:spring_feature" {
|
||||||
|
return SpringFeatureConfig{}, fmt.Errorf("worldgen: %s is not a spring feature", name)
|
||||||
|
}
|
||||||
|
var config SpringFeatureConfig
|
||||||
|
if err := json.Unmarshal(configured.Config, &config); err != nil {
|
||||||
|
return SpringFeatureConfig{}, fmt.Errorf("worldgen: decode %s: %w", name, err)
|
||||||
|
}
|
||||||
|
if config.State.Name == "" || len(config.ValidBlocks) == 0 || config.HoleCount < 0 || config.RockCount < 0 {
|
||||||
|
return SpringFeatureConfig{}, fmt.Errorf("worldgen: invalid spring config %s", name)
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *FeatureSet) Placement(name string) (PlacementPlan, error) {
|
func (s *FeatureSet) Placement(name string) (PlacementPlan, error) {
|
||||||
placed, ok := s.Placed[name]
|
placed, ok := s.Placed[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -191,7 +228,8 @@ func (s *FeatureSet) Placement(name string) (PlacementPlan, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PlacementPlan{}, err
|
return PlacementPlan{}, err
|
||||||
}
|
}
|
||||||
if value.Height.Type != "minecraft:uniform" && value.Height.Type != "minecraft:trapezoid" {
|
if value.Height.Type != "minecraft:uniform" && value.Height.Type != "minecraft:trapezoid" &&
|
||||||
|
value.Height.Type != "minecraft:very_biased_to_bottom" {
|
||||||
return PlacementPlan{}, fmt.Errorf("worldgen: %s unsupported height distribution %q", name, value.Height.Type)
|
return PlacementPlan{}, fmt.Errorf("worldgen: %s unsupported height distribution %q", name, value.Height.Type)
|
||||||
}
|
}
|
||||||
plan.HeightDistribution, plan.MinY, plan.MaxY = value.Height.Type, min, max
|
plan.HeightDistribution, plan.MinY, plan.MaxY = value.Height.Type, min, max
|
||||||
|
|
|
||||||
|
|
@ -33,4 +33,12 @@ func TestFeatureDatapackLoadsAndLinks(t *testing.T) {
|
||||||
if err != nil || plan.Count.Min != 7 || plan.Count.Max != 7 || plan.MinY.AboveBottom == nil {
|
if err != nil || plan.Count.Min != 7 || plan.Count.Max != 7 || plan.MinY.AboveBottom == nil {
|
||||||
t.Fatalf("diamond placement = %+v, err=%v", plan, err)
|
t.Fatalf("diamond placement = %+v, err=%v", plan, err)
|
||||||
}
|
}
|
||||||
|
spring, err := set.Spring("minecraft:spring_water")
|
||||||
|
if err != nil || spring.State.Name != "minecraft:water" || spring.RockCount != 4 || spring.HoleCount != 1 {
|
||||||
|
t.Fatalf("spring water = %+v, err=%v", spring, err)
|
||||||
|
}
|
||||||
|
lavaPlan, err := set.Placement("minecraft:spring_lava")
|
||||||
|
if err != nil || lavaPlan.HeightDistribution != "minecraft:very_biased_to_bottom" {
|
||||||
|
t.Fatalf("spring lava placement = %+v, err=%v", lavaPlan, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue