From 2cf6ab4775e9aa3f6b81fed3d1b4f0df2a5784a8 Mon Sep 17 00:00:00 2001 From: Daniar Mannanov Date: Mon, 17 Aug 2026 14:16:34 +0300 Subject: [PATCH] Preserve trapezoid height plateaus --- internal/worldgen/features.go | 27 +++++++++++------- internal/worldgen/features_test.go | 46 ++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/internal/worldgen/features.go b/internal/worldgen/features.go index 82a18c5..848441b 100644 --- a/internal/worldgen/features.go +++ b/internal/worldgen/features.go @@ -122,6 +122,7 @@ type PlacementPlan struct { Count CountProvider RarityChance int HeightDistribution string + HeightPlateau int MinY HeightProvider MaxY HeightProvider } @@ -195,13 +196,15 @@ func (p PlacementPlan) SampleY(r RandomSource, minY, height int) int { } span := hi - lo + 1 if p.HeightDistribution == "minecraft:trapezoid" { - // Vanilla's TrapezoidHeight works with the inclusive range distance, - // then performs two inclusive random draws around the midpoint. rangeSize := span - 1 if rangeSize <= 0 { return lo } - left := rangeSize / 2 + plateau := p.HeightPlateau + if plateau < 0 || plateau > rangeSize { + return lo + } + left := (rangeSize - plateau) / 2 right := rangeSize - left return lo + int(r.NextIntN(int32(right+1))) + int(r.NextIntN(int32(left+1))) } @@ -499,9 +502,10 @@ func (s *FeatureSet) Placement(name string) (PlacementPlan, error) { case "minecraft:height_range": var value struct { Height struct { - Type string `json:"type"` - Min json.RawMessage `json:"min_inclusive"` - Max json.RawMessage `json:"max_inclusive"` + Type string `json:"type"` + Plateau int `json:"plateau"` + Min json.RawMessage `json:"min_inclusive"` + Max json.RawMessage `json:"max_inclusive"` } `json:"height"` } if err := json.Unmarshal(modifier.Raw, &value); err != nil { @@ -519,7 +523,7 @@ func (s *FeatureSet) Placement(name string) (PlacementPlan, error) { 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 + plan.HeightDistribution, plan.HeightPlateau, plan.MinY, plan.MaxY = value.Height.Type, value.Height.Plateau, min, max case "minecraft:in_square", "minecraft:biome", "minecraft:surface_water_depth_filter", "minecraft:heightmap", "minecraft:block_predicate_filter", "minecraft:noise_threshold_count", "minecraft:random_offset": @@ -745,9 +749,10 @@ func parsePlacementIntProvider(raw json.RawMessage) (placementIntProvider, error func placementHeightPlan(raw json.RawMessage) (PlacementPlan, error) { var value struct { Height struct { - Type string `json:"type"` - Min json.RawMessage `json:"min_inclusive"` - Max json.RawMessage `json:"max_inclusive"` + Type string `json:"type"` + Plateau int `json:"plateau"` + Min json.RawMessage `json:"min_inclusive"` + Max json.RawMessage `json:"max_inclusive"` } `json:"height"` } if err := json.Unmarshal(raw, &value); err != nil { @@ -765,7 +770,7 @@ func placementHeightPlan(raw json.RawMessage) (PlacementPlan, error) { value.Height.Type != "minecraft:very_biased_to_bottom" { return PlacementPlan{}, fmt.Errorf("worldgen: unsupported height distribution %q", value.Height.Type) } - return PlacementPlan{HeightDistribution: value.Height.Type, MinY: min, MaxY: max}, nil + return PlacementPlan{HeightDistribution: value.Height.Type, HeightPlateau: value.Height.Plateau, MinY: min, MaxY: max}, nil } func parseIntProvider(raw json.RawMessage) (CountProvider, error) { diff --git a/internal/worldgen/features_test.go b/internal/worldgen/features_test.go index a0aef29..d8a293a 100644 --- a/internal/worldgen/features_test.go +++ b/internal/worldgen/features_test.go @@ -2,10 +2,35 @@ package worldgen import ( "encoding/json" + "os" "reflect" "testing" ) +func TestTrapezoidPlateauDiagnostic(t *testing.T) { + if os.Getenv("REGIONIO_TRAPEZOID_DIAGNOSTIC") != "1" { + t.Skip("set REGIONIO_TRAPEZOID_DIAGNOSTIC=1 to list non-zero height plateaus") + } + set, err := LoadFeatureSet() + if err != nil { + t.Fatal(err) + } + for name, placed := range set.Placed { + for _, modifier := range placed.Placement { + if modifier.Type != "minecraft:height_range" { + continue + } + plan, err := placementHeightPlan(modifier.Raw) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + if plan.HeightDistribution == "minecraft:trapezoid" && plan.HeightPlateau != 0 { + t.Logf("%s plateau=%d", name, plan.HeightPlateau) + } + } + } +} + func TestFeatureDatapackLoadsAndLinks(t *testing.T) { set, err := LoadFeatureSet() if err != nil { @@ -157,6 +182,27 @@ func TestVeryBiasedToBottomConsumesVanillaDraws(t *testing.T) { } } +func TestTrapezoidHeightHonorsPlateau(t *testing.T) { + plan := PlacementPlan{ + HeightDistribution: "minecraft:trapezoid", + HeightPlateau: 4, + MinY: HeightProvider{Absolute: intPtr(0)}, + MaxY: HeightProvider{Absolute: intPtr(20)}, + } + gotRandom := NewLegacy(12345) + wantRandom := NewLegacy(12345) + for sample := 0; sample < 16; sample++ { + left := (20 - 4) / 2 + want := int(wantRandom.NextIntN(int32(20-left+1))) + int(wantRandom.NextIntN(int32(left+1))) + if got := plan.SampleY(gotRandom, 0, 21); got != want { + t.Fatalf("sample %d = %d, want %d", sample, got, want) + } + } + if got, want := gotRandom.NextLong(), wantRandom.NextLong(); got != want { + t.Fatalf("random state after samples = %d, want %d", got, want) + } +} + func TestPlacementPositionsPreservesModifierOrder(t *testing.T) { modifier := func(raw string) PlacementModifier { var value PlacementModifier