diff --git a/internal/world/region_ores.go b/internal/world/region_ores.go index 6809909..7d356e8 100644 --- a/internal/world/region_ores.go +++ b/internal/world/region_ores.go @@ -49,12 +49,11 @@ func (r *decorationRegion) placeScheduledOresFiltered(seed int64, biomeOrder []s context := r.placementContext(func(position worldgen.FeaturePosition) bool { return r.biomeAllowsFeature(set, scheduled.Name, undergroundOresStage, position) }) - positions, err := set.PlacementPositions(scheduled.Name, random, origin, context) - if err != nil { - return err - } - for _, position := range positions { + if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error { placeOreEllipsoidRegion(r, random, position.X, position.Y, position.Z, config.Size, config.DiscardAirExposure, targets) + return nil + }); err != nil { + return err } } return nil diff --git a/internal/worldgen/features.go b/internal/worldgen/features.go index 848441b..d82d4d1 100644 --- a/internal/worldgen/features.go +++ b/internal/worldgen/features.go @@ -540,16 +540,26 @@ func (s *FeatureSet) Placement(name string) (PlacementPlan, error) { // The recursive walk mirrors Stream.flatMap: every repeated position completes // the remaining chain before the next repeated position consumes random draws. func (s *FeatureSet) PlacementPositions(name string, r RandomSource, origin FeaturePosition, context PlacementContext) ([]FeaturePosition, error) { + var result []FeaturePosition + err := s.ForEachPlacementPosition(name, r, origin, context, func(position FeaturePosition) error { + result = append(result, position) + return nil + }) + return result, err +} + +// ForEachPlacementPosition preserves vanilla's lazy placement stream: the +// configured feature consumes random draws for one position before modifiers +// produce the next repeated position. +func (s *FeatureSet) ForEachPlacementPosition(name string, r RandomSource, origin FeaturePosition, context PlacementContext, visit func(FeaturePosition) error) error { placed, ok := s.Placed[name] if !ok { - return nil, fmt.Errorf("worldgen: placed feature %s missing", name) + return fmt.Errorf("worldgen: placed feature %s missing", name) } - var result []FeaturePosition var apply func(int, FeaturePosition) error apply = func(index int, position FeaturePosition) error { if index == len(placed.Placement) { - result = append(result, position) - return nil + return visit(position) } modifier := placed.Placement[index] next := func(value FeaturePosition) error { return apply(index+1, value) } @@ -671,10 +681,7 @@ func (s *FeatureSet) PlacementPositions(name string, r RandomSource, origin Feat return fmt.Errorf("worldgen: %s unsupported executable placement modifier %q", name, modifier.Type) } } - if err := apply(0, origin); err != nil { - return nil, err - } - return result, nil + return apply(0, origin) } type placementIntProvider struct { diff --git a/internal/worldgen/features_test.go b/internal/worldgen/features_test.go index d8a293a..c8f2395 100644 --- a/internal/worldgen/features_test.go +++ b/internal/worldgen/features_test.go @@ -248,6 +248,45 @@ func TestPlacementPositionsPreservesModifierOrder(t *testing.T) { } } +func TestForEachPlacementPositionInterleavesVisitorRandomDraws(t *testing.T) { + modifier := func(raw string) PlacementModifier { + var value PlacementModifier + value.Raw = json.RawMessage(raw) + if err := json.Unmarshal(value.Raw, &value); err != nil { + t.Fatal(err) + } + return value + } + set := &FeatureSet{Placed: map[string]PlacedFeature{ + "test": {Placement: []PlacementModifier{ + modifier(`{"type":"minecraft:count","count":3}`), + modifier(`{"type":"minecraft:in_square"}`), + }}, + }} + random := NewLegacy(12345) + var got []FeaturePosition + err := set.ForEachPlacementPosition("test", random, FeaturePosition{}, PlacementContext{}, func(position FeaturePosition) error { + got = append(got, position) + random.NextLong() // configured feature draw before the next position + return nil + }) + if err != nil { + t.Fatal(err) + } + wantRandom := NewLegacy(12345) + var want []FeaturePosition + for range 3 { + want = append(want, FeaturePosition{X: int(wantRandom.NextIntN(16)), Z: int(wantRandom.NextIntN(16))}) + wantRandom.NextLong() + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("positions = %v, want interleaved %v", got, want) + } + if gotState, wantState := random.NextLong(), wantRandom.NextLong(); gotState != wantState { + t.Fatalf("random state = %d, want %d", gotState, wantState) + } +} + func TestPlacementPositionsWorldAwareModifiers(t *testing.T) { modifier := func(raw string) PlacementModifier { var value PlacementModifier diff --git a/internal/worldgen/random_test.go b/internal/worldgen/random_test.go index fc9be42..8c69c46 100644 --- a/internal/worldgen/random_test.go +++ b/internal/worldgen/random_test.go @@ -80,3 +80,43 @@ func TestLegacySetSeedResetsGaussianCache(t *testing.T) { t.Fatalf("NextGaussian after SetSeed = %v, want %v", got, first) } } + +func TestDecorationAndFeatureSeedVectors(t *testing.T) { + tests := []struct { + seed int64 + blockX, blockZ, feature, step int + decoration int64 + int16, int65 int32 + floatValue float32 + doubleValue float64 + long int64 + }{ + {12345, 0, 0, 10, 6, 12345, 1, 23, 0.659135699, 0.60100983666890360, 8913713877150976631}, + {12345, 16, -48, 10, 6, 95234183275347033, 12, 35, 0.821727931, 0.14700435702589810, -8289173915194761532}, + {12345, -272, 672, 25, 6, -8094914473946183255, 13, 24, 0.743742824, 0.35161152187643063, 6657627925144652261}, + {-987654321, 48, 80, 3, 4, 7522931011426891727, 14, 63, 0.750663280, 0.34934808320731514, -8531709575983379994}, + } + for _, test := range tests { + random := NewLegacy(0) + if got := random.SetDecorationSeed(test.seed, test.blockX, test.blockZ); got != test.decoration { + t.Errorf("seed %d block (%d,%d): decoration seed = %d, want %d", test.seed, test.blockX, test.blockZ, got, test.decoration) + continue + } + random.SetFeatureSeed(test.decoration, test.feature, test.step) + if got := random.NextIntN(16); got != test.int16 { + t.Errorf("seed %d block (%d,%d): nextInt(16) = %d, want %d", test.seed, test.blockX, test.blockZ, got, test.int16) + } + if got := random.NextIntN(65); got != test.int65 { + t.Errorf("seed %d block (%d,%d): nextInt(65) = %d, want %d", test.seed, test.blockX, test.blockZ, got, test.int65) + } + if got := random.NextFloat(); math.Abs(float64(got-test.floatValue)) > 1e-7 { + t.Errorf("seed %d block (%d,%d): nextFloat = %.9f, want %.9f", test.seed, test.blockX, test.blockZ, got, test.floatValue) + } + if got := random.NextDouble(); math.Abs(got-test.doubleValue) > 1e-15 { + t.Errorf("seed %d block (%d,%d): nextDouble = %.17f, want %.17f", test.seed, test.blockX, test.blockZ, got, test.doubleValue) + } + if got := random.NextLong(); got != test.long { + t.Errorf("seed %d block (%d,%d): nextLong = %d, want %d", test.seed, test.blockX, test.blockZ, got, test.long) + } + } +}