From ea66091f85a523183ddc3eac684701e47b7aafe0 Mon Sep 17 00:00:00 2001 From: Daniar Mannanov Date: Tue, 11 Aug 2026 12:47:59 +0300 Subject: [PATCH] Schedule source chunk features globally --- internal/world/feature_scheduler.go | 21 +++++++++++++ internal/world/feature_scheduler_test.go | 27 ++++++++++++++++ internal/worldgen/features.go | 39 ++++++++++++++++++++++++ internal/worldgen/features_test.go | 38 +++++++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 internal/world/feature_scheduler.go create mode 100644 internal/world/feature_scheduler_test.go diff --git a/internal/world/feature_scheduler.go b/internal/world/feature_scheduler.go new file mode 100644 index 0000000..97a53e4 --- /dev/null +++ b/internal/world/feature_scheduler.go @@ -0,0 +1,21 @@ +package world + +// decorationSource is a source chunk whose feature pass may inspect or write a +// target chunk. Vanilla FEATURES has a one-chunk block-state write radius. +type decorationSource struct { + X, Z int32 +} + +// decorationSources returns source chunks in deterministic X-major/Z-minor +// order. Replaying all nine against one mutable 3x3 terrain region makes target +// output independent of cache request order while preserving each source's own +// decoration seed and placement origin. +func decorationSources(targetX, targetZ int32) []decorationSource { + sources := make([]decorationSource, 0, 9) + for sourceX := targetX - 1; sourceX <= targetX+1; sourceX++ { + for sourceZ := targetZ - 1; sourceZ <= targetZ+1; sourceZ++ { + sources = append(sources, decorationSource{X: sourceX, Z: sourceZ}) + } + } + return sources +} diff --git a/internal/world/feature_scheduler_test.go b/internal/world/feature_scheduler_test.go new file mode 100644 index 0000000..75c0729 --- /dev/null +++ b/internal/world/feature_scheduler_test.go @@ -0,0 +1,27 @@ +package world + +import ( + "reflect" + "testing" +) + +func TestDecorationSourcesCoverVanillaFeatureWriteRadius(t *testing.T) { + got := decorationSources(4, -7) + want := []decorationSource{ + {3, -8}, {3, -7}, {3, -6}, + {4, -8}, {4, -7}, {4, -6}, + {5, -8}, {5, -7}, {5, -6}, + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("sources = %v, want %v", got, want) + } +} + +func TestDecorationSourcesAreRequestOrderIndependent(t *testing.T) { + first := decorationSources(-2, 9) + _ = decorationSources(100, -100) + second := decorationSources(-2, 9) + if !reflect.DeepEqual(first, second) { + t.Fatalf("sources changed after unrelated request: %v != %v", first, second) + } +} diff --git a/internal/worldgen/features.go b/internal/worldgen/features.go index c0aee6a..774f966 100644 --- a/internal/worldgen/features.go +++ b/internal/worldgen/features.go @@ -28,6 +28,14 @@ type IndexedFeature struct { Index int } +// ScheduledFeature is one feature selected for a decoration stage. Index is +// the stable FeatureSorter index within that stage and must be used when +// deriving the feature random seed; it is not the index in any one biome. +type ScheduledFeature struct { + Name string + Index int +} + type ConfiguredFeature struct { Type string Config json.RawMessage @@ -332,6 +340,37 @@ func IndexedFeatures(step []string, wanted map[string]bool) []IndexedFeature { return result } +// FeatureSchedule returns the features present in the union of sourceBiomes +// for one decoration stage. allBiomeOrder is the complete BiomeSource encounter +// order used to build FeatureSorter, not the order of the local source region. +// The result is stable and contains each placed feature at most once. +func (s *FeatureSet) FeatureSchedule(allBiomeOrder, sourceBiomes []string, stage int) ([]ScheduledFeature, error) { + steps, err := s.FeatureSteps(allBiomeOrder) + if err != nil { + return nil, err + } + if stage < 0 || stage >= len(steps) { + return nil, fmt.Errorf("worldgen: feature stage %d out of range", stage) + } + wanted := make(map[string]bool) + for _, biomeName := range sourceBiomes { + biome, ok := s.Biomes[biomeName] + if !ok || stage >= len(biome.Features) { + continue + } + for _, name := range biome.Features[stage] { + wanted[name] = true + } + } + result := make([]ScheduledFeature, 0, len(wanted)) + for index, name := range steps[stage] { + if wanted[name] { + result = append(result, ScheduledFeature{Name: name, Index: index}) + } + } + return result, nil +} + func (s *FeatureSet) Ore(name string) (OreFeatureConfig, error) { configured, ok := s.Configured[name] if !ok || configured.Type != "minecraft:ore" { diff --git a/internal/worldgen/features_test.go b/internal/worldgen/features_test.go index 141beea..23eb42f 100644 --- a/internal/worldgen/features_test.go +++ b/internal/worldgen/features_test.go @@ -82,6 +82,44 @@ func TestFeatureStepsRejectsCycles(t *testing.T) { } } +func TestFeatureScheduleUsesGlobalOrderAndSourceUnion(t *testing.T) { + set := &FeatureSet{Biomes: map[string]BiomeGeneration{ + "a": {Features: [][]string{{"f1"}, {"f3", "f4"}}}, + "b": {Features: [][]string{{"f2", "f1"}, {"f5", "f4"}}}, + "c": {Features: [][]string{{"f1"}, {"f5", "f3"}}}, + }} + got, err := set.FeatureSchedule([]string{"a", "b", "c"}, []string{"b", "c"}, 1) + if err != nil { + t.Fatal(err) + } + want := []ScheduledFeature{{Name: "f5", Index: 0}, {Name: "f3", Index: 1}, {Name: "f4", Index: 2}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("schedule = %v, want %v", got, want) + } +} + +func TestFeatureScheduleDoesNotRenumberFilteredFeatures(t *testing.T) { + set := &FeatureSet{Biomes: map[string]BiomeGeneration{ + "a": {Features: [][]string{{"f1", "f2", "f3"}}}, + "b": {Features: [][]string{{"f3"}}}, + }} + got, err := set.FeatureSchedule([]string{"a", "b"}, []string{"b"}, 0) + if err != nil { + t.Fatal(err) + } + want := []ScheduledFeature{{Name: "f3", Index: 2}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("schedule = %v, want global index %v", got, want) + } +} + +func TestFeatureScheduleRejectsInvalidStage(t *testing.T) { + set := &FeatureSet{Biomes: map[string]BiomeGeneration{"a": {Features: [][]string{{"f1"}}}}} + if _, err := set.FeatureSchedule([]string{"a"}, []string{"a"}, 1); err == nil { + t.Fatal("invalid stage succeeded") + } +} + func TestPlacementHeightDistributionsStayWithinInclusiveBounds(t *testing.T) { r := NewLegacy(12345) for _, distribution := range []string{"minecraft:trapezoid", "minecraft:very_biased_to_bottom", "minecraft:uniform"} {