diff --git a/internal/world/placed_features.go b/internal/world/placed_features.go index 410f6ae..1ffbb3e 100644 --- a/internal/world/placed_features.go +++ b/internal/world/placed_features.go @@ -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) diff --git a/internal/world/springs.go b/internal/world/springs.go new file mode 100644 index 0000000..a7b46d4 --- /dev/null +++ b/internal/world/springs.go @@ -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) +} diff --git a/internal/world/springs_test.go b/internal/world/springs_test.go new file mode 100644 index 0000000..08e7ce0 --- /dev/null +++ b/internal/world/springs_test.go @@ -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) + } + } + } + } +} diff --git a/internal/world/store.go b/internal/world/store.go index 91cb0cb..fd0db95 100644 --- a/internal/world/store.go +++ b/internal/world/store.go @@ -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. diff --git a/internal/world/vanilla.go b/internal/world/vanilla.go index 4700331..c69e638 100644 --- a/internal/world/vanilla.go +++ b/internal/world/vanilla.go @@ -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) diff --git a/internal/worldgen/features.go b/internal/worldgen/features.go index e431f8d..55998bc 100644 --- a/internal/worldgen/features.go +++ b/internal/worldgen/features.go @@ -53,6 +53,19 @@ type OreTarget struct { } `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 { Count CountProvider RarityChance int @@ -102,6 +115,15 @@ func (p PlacementPlan) SampleY(r RandomSource, minY, height int) int { triangle := span - plateau 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))) } @@ -144,6 +166,21 @@ func (s *FeatureSet) Ore(name string) (OreFeatureConfig, error) { 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) { placed, ok := s.Placed[name] if !ok { @@ -191,7 +228,8 @@ func (s *FeatureSet) Placement(name string) (PlacementPlan, error) { if err != nil { 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) } plan.HeightDistribution, plan.MinY, plan.MaxY = value.Height.Type, min, max diff --git a/internal/worldgen/features_test.go b/internal/worldgen/features_test.go index cb6fe27..20286a2 100644 --- a/internal/worldgen/features_test.go +++ b/internal/worldgen/features_test.go @@ -33,4 +33,12 @@ func TestFeatureDatapackLoadsAndLinks(t *testing.T) { if err != nil || plan.Count.Min != 7 || plan.Count.Max != 7 || plan.MinY.AboveBottom == nil { 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) + } }