Execute placement modifiers in order

This commit is contained in:
Daniar Mannanov 2026-08-11 09:55:50 +03:00
parent 674d0192c9
commit 053c5bec6a
2 changed files with 153 additions and 0 deletions

View file

@ -118,6 +118,15 @@ type PlacementPlan struct {
MaxY HeightProvider
}
type FeaturePosition struct {
X, Y, Z int
}
type PlacementContext struct {
MinY, Height int
BiomeAllows func(FeaturePosition) bool
}
type CountProvider struct {
Min, Max int
Weighted []WeightedInt
@ -483,6 +492,104 @@ func (s *FeatureSet) Placement(name string) (PlacementPlan, error) {
return plan, nil
}
// PlacementPositions executes a placed feature's modifiers in declared order.
// 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) {
placed, ok := s.Placed[name]
if !ok {
return nil, 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
}
modifier := placed.Placement[index]
next := func(value FeaturePosition) error { return apply(index+1, value) }
switch modifier.Type {
case "minecraft:count":
var value struct {
Count json.RawMessage `json:"count"`
}
if err := json.Unmarshal(modifier.Raw, &value); err != nil {
return err
}
count, err := parseIntProvider(value.Count)
if err != nil {
return err
}
for n := count.Sample(r); n > 0; n-- {
if err := next(position); err != nil {
return err
}
}
return nil
case "minecraft:rarity_filter":
var value struct {
Chance int `json:"chance"`
}
if err := json.Unmarshal(modifier.Raw, &value); err != nil || value.Chance < 1 {
return fmt.Errorf("worldgen: %s invalid rarity filter", name)
}
if r.NextFloat() < 1/float32(value.Chance) {
return next(position)
}
return nil
case "minecraft:in_square":
position.X += int(r.NextIntN(16))
position.Z += int(r.NextIntN(16))
return next(position)
case "minecraft:height_range":
plan, err := placementHeightPlan(modifier.Raw)
if err != nil {
return err
}
position.Y = plan.SampleY(r, context.MinY, context.Height)
return next(position)
case "minecraft:biome":
if context.BiomeAllows == nil || context.BiomeAllows(position) {
return next(position)
}
return nil
default:
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
}
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"`
} `json:"height"`
}
if err := json.Unmarshal(raw, &value); err != nil {
return PlacementPlan{}, err
}
min, err := parseFeatureHeight(value.Height.Min)
if err != nil {
return PlacementPlan{}, err
}
max, err := parseFeatureHeight(value.Height.Max)
if err != nil {
return PlacementPlan{}, err
}
if value.Height.Type != "minecraft:uniform" && value.Height.Type != "minecraft:trapezoid" &&
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
}
func parseIntProvider(raw json.RawMessage) (CountProvider, error) {
var fixed int
if err := json.Unmarshal(raw, &fixed); err == nil {

View file

@ -1,6 +1,7 @@
package worldgen
import (
"encoding/json"
"reflect"
"testing"
)
@ -97,3 +98,48 @@ func TestPlacementHeightDistributionsStayWithinInclusiveBounds(t *testing.T) {
}
}
}
func TestPlacementPositionsPreservesModifierOrder(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"}`),
modifier(`{"type":"minecraft:height_range","height":{"type":"minecraft:uniform","min_inclusive":{"absolute":-2},"max_inclusive":{"absolute":2}}}`),
modifier(`{"type":"minecraft:biome"}`),
}},
}}
r := NewLegacy(12345)
got, err := set.PlacementPositions("test", r, FeaturePosition{X: 32, Y: -64, Z: -16}, PlacementContext{
MinY: -64, Height: 384,
BiomeAllows: func(position FeaturePosition) bool { return position.Y >= 0 },
})
if err != nil {
t.Fatal(err)
}
wantRandom := NewLegacy(12345)
var want []FeaturePosition
for range 3 {
x := 32 + int(wantRandom.NextIntN(16))
z := -16 + int(wantRandom.NextIntN(16))
y := -2 + int(wantRandom.NextIntN(5))
position := FeaturePosition{
X: x,
Y: y,
Z: z,
}
if position.Y >= 0 {
want = append(want, position)
}
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("positions = %v, want %v", got, want)
}
}