From 11440f556c59dce4ee113d3dd4f367769c7fceec Mon Sep 17 00:00:00 2001 From: Daniar Mannanov Date: Tue, 11 Aug 2026 12:33:35 +0300 Subject: [PATCH] Execute world-aware placement modifiers --- internal/worldgen/features.go | 144 ++++++++++++++++++++++++++++- internal/worldgen/features_test.go | 87 +++++++++++++++++ internal/worldgen/random.go | 53 ++++++++++- internal/worldgen/random_test.go | 9 ++ tools/VanillaPlacementVectors.java | 47 ++++++++++ 5 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 tools/VanillaPlacementVectors.java diff --git a/internal/worldgen/features.go b/internal/worldgen/features.go index 95c0179..c0aee6a 100644 --- a/internal/worldgen/features.go +++ b/internal/worldgen/features.go @@ -123,8 +123,10 @@ type FeaturePosition struct { } type PlacementContext struct { - MinY, Height int - BiomeAllows func(FeaturePosition) bool + MinY, Height int + BiomeAllows func(FeaturePosition) bool + HeightAt func(string, int, int) int + BlockPredicate func(json.RawMessage, FeaturePosition) (bool, error) } type CountProvider struct { @@ -549,6 +551,75 @@ func (s *FeatureSet) PlacementPositions(name string, r RandomSource, origin Feat } position.Y = plan.SampleY(r, context.MinY, context.Height) return next(position) + case "minecraft:heightmap": + var value struct { + Heightmap string `json:"heightmap"` + } + if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.Heightmap == "" { + return fmt.Errorf("worldgen: %s invalid heightmap placement", name) + } + if context.HeightAt == nil { + return fmt.Errorf("worldgen: %s heightmap placement requires HeightAt", name) + } + position.Y = context.HeightAt(value.Heightmap, position.X, position.Z) + if position.Y > context.MinY { + return next(position) + } + return nil + case "minecraft:surface_water_depth_filter": + var value struct { + MaxWaterDepth int `json:"max_water_depth"` + } + if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.MaxWaterDepth < 0 { + return fmt.Errorf("worldgen: %s invalid surface water depth filter", name) + } + if context.HeightAt == nil { + return fmt.Errorf("worldgen: %s surface water depth filter requires HeightAt", name) + } + oceanFloor := context.HeightAt("OCEAN_FLOOR", position.X, position.Z) + worldSurface := context.HeightAt("WORLD_SURFACE", position.X, position.Z) + if worldSurface-oceanFloor <= value.MaxWaterDepth { + return next(position) + } + return nil + case "minecraft:random_offset": + var value struct { + XZSpread json.RawMessage `json:"xz_spread"` + YSpread json.RawMessage `json:"y_spread"` + } + if err := json.Unmarshal(modifier.Raw, &value); err != nil { + return err + } + xz, err := parsePlacementIntProvider(value.XZSpread) + if err != nil { + return fmt.Errorf("worldgen: %s xz spread: %w", name, err) + } + y, err := parsePlacementIntProvider(value.YSpread) + if err != nil { + return fmt.Errorf("worldgen: %s y spread: %w", name, err) + } + position.X += xz.Sample(r) + position.Y += y.Sample(r) + position.Z += xz.Sample(r) + return next(position) + case "minecraft:block_predicate_filter": + var value struct { + Predicate json.RawMessage `json:"predicate"` + } + if err := json.Unmarshal(modifier.Raw, &value); err != nil || len(value.Predicate) == 0 { + return fmt.Errorf("worldgen: %s invalid block predicate filter", name) + } + if context.BlockPredicate == nil { + return fmt.Errorf("worldgen: %s block predicate filter requires BlockPredicate", name) + } + ok, err := context.BlockPredicate(value.Predicate, position) + if err != nil { + return err + } + if ok { + return next(position) + } + return nil case "minecraft:biome": if context.BiomeAllows == nil || context.BiomeAllows(position) { return next(position) @@ -564,6 +635,75 @@ func (s *FeatureSet) PlacementPositions(name string, r RandomSource, origin Feat return result, nil } +type placementIntProvider struct { + typeName string + min, max, plateau int + mean, deviation float32 +} + +func (p placementIntProvider) Sample(r RandomSource) int { + switch p.typeName { + case "minecraft:uniform": + return p.min + int(r.NextIntN(int32(p.max-p.min+1))) + case "minecraft:trapezoid": + if p.plateau == 0 && p.max == -p.min { + return int(r.NextIntN(int32(p.max+1))) - int(r.NextIntN(int32(p.max+1))) + } + rangeSize := p.max - p.min + if p.plateau == rangeSize { + return p.min + int(r.NextIntN(int32(rangeSize+1))) + } + left := (rangeSize - p.plateau) / 2 + right := rangeSize - left + return p.min + int(r.NextIntN(int32(right+1))) + int(r.NextIntN(int32(left+1))) + case "minecraft:clamped_normal": + value := float32(r.NextGaussian())*p.deviation + p.mean + if value < float32(p.min) { + value = float32(p.min) + } else if value > float32(p.max) { + value = float32(p.max) + } + return int(value) + default: + return p.min + } +} + +func parsePlacementIntProvider(raw json.RawMessage) (placementIntProvider, error) { + var fixed int + if err := json.Unmarshal(raw, &fixed); err == nil { + return placementIntProvider{min: fixed, max: fixed}, nil + } + var value struct { + Type string `json:"type"` + Min int `json:"min"` + Max int `json:"max"` + MinInclusive int `json:"min_inclusive"` + MaxInclusive int `json:"max_inclusive"` + Plateau int `json:"plateau"` + Mean, Deviation float32 + } + if err := json.Unmarshal(raw, &value); err != nil { + return placementIntProvider{}, err + } + provider := placementIntProvider{typeName: value.Type, plateau: value.Plateau, mean: value.Mean, deviation: value.Deviation} + switch value.Type { + case "minecraft:trapezoid": + provider.min, provider.max = value.Min, value.Max + if provider.max < provider.min || provider.plateau < 0 || provider.plateau > provider.max-provider.min { + return placementIntProvider{}, fmt.Errorf("invalid trapezoid provider %s", raw) + } + case "minecraft:uniform", "minecraft:clamped_normal": + provider.min, provider.max = value.MinInclusive, value.MaxInclusive + if provider.max < provider.min || value.Type == "minecraft:clamped_normal" && provider.deviation <= 0 { + return placementIntProvider{}, fmt.Errorf("invalid %s provider %s", value.Type, raw) + } + default: + return placementIntProvider{}, fmt.Errorf("unsupported int provider %s", raw) + } + return provider, nil +} + func placementHeightPlan(raw json.RawMessage) (PlacementPlan, error) { var value struct { Height struct { diff --git a/internal/worldgen/features_test.go b/internal/worldgen/features_test.go index ad9d29f..141beea 100644 --- a/internal/worldgen/features_test.go +++ b/internal/worldgen/features_test.go @@ -143,3 +143,90 @@ func TestPlacementPositionsPreservesModifierOrder(t *testing.T) { t.Fatalf("positions = %v, want %v", got, want) } } + +func TestPlacementPositionsWorldAwareModifiers(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:heightmap","heightmap":"MOTION_BLOCKING"}`), + modifier(`{"type":"minecraft:surface_water_depth_filter","max_water_depth":2}`), + modifier(`{"type":"minecraft:block_predicate_filter","predicate":{"type":"minecraft:matching_blocks","blocks":"minecraft:air"}}`), + }}, + }} + var calls []string + got, err := set.PlacementPositions("test", NewLegacy(1), FeaturePosition{X: 8, Y: 0, Z: 9}, PlacementContext{ + MinY: -64, + HeightAt: func(kind string, x, z int) int { + calls = append(calls, kind) + if kind == "MOTION_BLOCKING" { + return 42 + } + if kind == "OCEAN_FLOOR" { + return 40 + } + return 41 + }, + BlockPredicate: func(predicate json.RawMessage, position FeaturePosition) (bool, error) { + if string(predicate) != `{"type":"minecraft:matching_blocks","blocks":"minecraft:air"}` { + t.Fatalf("predicate = %s", predicate) + } + return position.Y == 42, nil + }, + }) + if err != nil { + t.Fatal(err) + } + want := []FeaturePosition{{X: 8, Y: 42, Z: 9}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("positions = %v, want %v", got, want) + } + if !reflect.DeepEqual(calls, []string{"MOTION_BLOCKING", "OCEAN_FLOOR", "WORLD_SURFACE"}) { + t.Fatalf("heightmap calls = %v", calls) + } +} + +func TestPlacementPositionsMatchesVanillaRandomOffsetVector(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:random_offset","xz_spread":{"type":"minecraft:trapezoid","max":4,"min":-4,"plateau":0},"y_spread":{"type":"minecraft:trapezoid","max":2,"min":-2,"plateau":0}}`), + }}, + }} + got, err := set.PlacementPositions("test", NewLegacy(12345), FeaturePosition{X: 32, Y: 10, Z: -16}, PlacementContext{}) + if err != nil { + t.Fatal(err) + } + want := []FeaturePosition{{33, 10, -20}, {30, 11, -16}, {31, 11, -17}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("positions = %v, want vanilla %v", got, want) + } +} + +func TestClampedNormalIntMatchesVanillaRuntimeVector(t *testing.T) { + provider, err := parsePlacementIntProvider(json.RawMessage(`{"type":"minecraft:clamped_normal","mean":0.0,"deviation":3.0,"min_inclusive":-10,"max_inclusive":10}`)) + if err != nil { + t.Fatal(err) + } + r := NewLegacy(12345) + want := []int{0, 1, 2, -1, -3, -2, -2, 6} + for i, value := range want { + if got := provider.Sample(r); got != value { + t.Fatalf("sample[%d] = %d, want vanilla %d", i, got, value) + } + } +} diff --git a/internal/worldgen/random.go b/internal/worldgen/random.go index 16d0089..5662b0d 100644 --- a/internal/worldgen/random.go +++ b/internal/worldgen/random.go @@ -8,6 +8,7 @@ package worldgen import ( "crypto/md5" "encoding/binary" + "math" "math/bits" ) @@ -50,6 +51,7 @@ type RandomSource interface { NextDouble() float64 NextFloat() float32 NextBoolean() bool + NextGaussian() float64 // ForkPositional returns a factory for deriving deterministic child sources // (used to seed noise octaves by name). ForkPositional() PositionalRandomFactory @@ -77,7 +79,11 @@ func positionSeed(x, y, z int) int64 { // --- Xoroshiro128++ --- // Xoroshiro is XoroshiroRandomSource backed by Xoroshiro128PlusPlus. -type Xoroshiro struct{ lo, hi uint64 } +type Xoroshiro struct { + lo, hi uint64 + gaussian float64 + haveGaussian bool +} // NewXoroshiro seeds a Xoroshiro source from a 64-bit seed. func NewXoroshiro(seed int64) *Xoroshiro { @@ -132,6 +138,25 @@ func (x *Xoroshiro) NextFloat() float32 { func (x *Xoroshiro) NextBoolean() bool { return x.nextBits()&1 != 0 } +func (x *Xoroshiro) NextGaussian() float64 { + if x.haveGaussian { + x.haveGaussian = false + return x.gaussian + } + for { + u := 2*x.NextDouble() - 1 + v := 2*x.NextDouble() - 1 + s := u*u + v*v + if s == 0 || s >= 1 { + continue + } + factor := math.Sqrt(-2 * math.Log(s) / s) + x.gaussian = v * factor + x.haveGaussian = true + return u * factor + } +} + // ConsumeCount advances the underlying generator n times. func (x *Xoroshiro) ConsumeCount(n int) { for i := 0; i < n; i++ { @@ -168,7 +193,11 @@ const ( ) // Legacy is LegacyRandomSource: java.util.Random's 48-bit LCG. -type Legacy struct{ seed uint64 } +type Legacy struct { + seed uint64 + gaussian float64 + haveGaussian bool +} // NewLegacy seeds a Legacy source, applying Java's seed scramble. func NewLegacy(seed int64) *Legacy { @@ -180,6 +209,7 @@ func NewLegacy(seed int64) *Legacy { // SetSeed is java.util.Random.setSeed, which worldgen reseeds in place. func (r *Legacy) SetSeed(seed int64) { r.seed = (uint64(seed) ^ lcgMultiplier) & lcgMask + r.haveGaussian = false } // SetLargeFeatureSeed is WorldgenRandom.setLargeFeatureSeed: seed from the @@ -246,6 +276,25 @@ func (r *Legacy) NextDouble() float64 { func (r *Legacy) NextFloat() float32 { return float32(r.next(24)) * 0x1.0p-24 } func (r *Legacy) NextBoolean() bool { return r.next(1) != 0 } +func (r *Legacy) NextGaussian() float64 { + if r.haveGaussian { + r.haveGaussian = false + return r.gaussian + } + for { + u := 2*r.NextDouble() - 1 + v := 2*r.NextDouble() - 1 + s := u*u + v*v + if s == 0 || s >= 1 { + continue + } + factor := math.Sqrt(-2 * math.Log(s) / s) + r.gaussian = v * factor + r.haveGaussian = true + return u * factor + } +} + // ConsumeCount advances the LCG n times. func (r *Legacy) ConsumeCount(n int) { for i := 0; i < n; i++ { diff --git a/internal/worldgen/random_test.go b/internal/worldgen/random_test.go index 0a08232..fc9be42 100644 --- a/internal/worldgen/random_test.go +++ b/internal/worldgen/random_test.go @@ -71,3 +71,12 @@ func TestLegacyVectors(t *testing.T) { } } } + +func TestLegacySetSeedResetsGaussianCache(t *testing.T) { + r := NewLegacy(12345) + first := r.NextGaussian() + r.SetSeed(12345) + if got := r.NextGaussian(); got != first { + t.Fatalf("NextGaussian after SetSeed = %v, want %v", got, first) + } +} diff --git a/tools/VanillaPlacementVectors.java b/tools/VanillaPlacementVectors.java new file mode 100644 index 0000000..95b48b9 --- /dev/null +++ b/tools/VanillaPlacementVectors.java @@ -0,0 +1,47 @@ +import java.util.List; +import java.util.stream.Stream; + +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.server.Bootstrap; +import net.minecraft.util.RandomSource; +import net.minecraft.util.valueproviders.ClampedNormalInt; +import net.minecraft.util.valueproviders.TrapezoidInt; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.placement.CountPlacement; +import net.minecraft.world.level.levelgen.placement.PlacementModifier; +import net.minecraft.world.level.levelgen.placement.RandomOffsetPlacement; + +// Emits placement stream vectors from the official 26.1.2 runtime. These +// modifiers do not read PlacementContext, so no world or registry is required. +public final class VanillaPlacementVectors { + public static void main(String[] args) { + SharedConstants.tryDetectVersion(); + Bootstrap.bootStrap(); + + RandomSource offsetRandom = new LegacyRandomSource(12345L); + List modifiers = List.of( + CountPlacement.of(3), + RandomOffsetPlacement.of( + TrapezoidInt.of(-4, 4, 0), + TrapezoidInt.of(-2, 2, 0))); + Stream positions = Stream.of(new BlockPos(32, 10, -16)); + for (PlacementModifier modifier : modifiers) { + positions = positions.flatMap(position -> modifier.getPositions(null, offsetRandom, position)); + } + System.out.println("offset=" + positions.map(VanillaPlacementVectors::position).toList()); + + RandomSource normalRandom = new LegacyRandomSource(12345L); + ClampedNormalInt normal = ClampedNormalInt.of(0.0F, 3.0F, -10, 10); + StringBuilder samples = new StringBuilder(); + for (int i = 0; i < 8; i++) { + if (i > 0) samples.append(','); + samples.append(normal.sample(normalRandom)); + } + System.out.println("normal=" + samples); + } + + private static String position(BlockPos position) { + return position.getX() + ":" + position.getY() + ":" + position.getZ(); + } +}