From 4ff62d7c966de5c8afc25dce3eab049105483a15 Mon Sep 17 00:00:00 2001 From: Daniar Mannanov Date: Tue, 11 Aug 2026 13:13:02 +0300 Subject: [PATCH] Evaluate world-aware block predicates --- internal/world/block_predicate.go | 129 +++++++++++++++++++++++++ internal/world/block_predicate_test.go | 62 ++++++++++++ internal/world/decoration_region.go | 6 +- 3 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 internal/world/block_predicate.go create mode 100644 internal/world/block_predicate_test.go diff --git a/internal/world/block_predicate.go b/internal/world/block_predicate.go new file mode 100644 index 0000000..e02d83c --- /dev/null +++ b/internal/world/block_predicate.go @@ -0,0 +1,129 @@ +package world + +import ( + "encoding/json" + "fmt" + + "regionio/internal/worldgen" +) + +type rawBlockPredicate struct { + Type string `json:"type"` + Offset [3]int `json:"offset"` + Blocks json.RawMessage `json:"blocks"` + Tag string `json:"tag"` + Fluids json.RawMessage `json:"fluids"` + Predicate json.RawMessage `json:"predicate"` + Predicates []json.RawMessage `json:"predicates"` +} + +func (r *decorationRegion) testBlockPredicate(set *worldgen.FeatureSet, raw json.RawMessage, position worldgen.FeaturePosition) (bool, error) { + var predicate rawBlockPredicate + if err := json.Unmarshal(raw, &predicate); err != nil { + return false, fmt.Errorf("world: decode block predicate: %w", err) + } + position.X += predicate.Offset[0] + position.Y += predicate.Offset[1] + position.Z += predicate.Offset[2] + switch predicate.Type { + case "minecraft:true": + return true, nil + case "minecraft:inside_world_bounds": + return position.Y >= MinY && position.Y < MinY+WorldHeight, nil + case "minecraft:matching_blocks": + names, err := decodeStringList(predicate.Blocks) + if err != nil { + return false, fmt.Errorf("world: matching_blocks: %w", err) + } + state, ok := stateByID(r.getBlock(position.X, position.Y, position.Z)) + if !ok { + return false, nil + } + for _, name := range names { + if state.Name == name { + return true, nil + } + } + return false, nil + case "minecraft:matching_block_tag": + if predicate.Tag == "" { + return false, fmt.Errorf("world: matching_block_tag missing tag") + } + state, ok := stateByID(r.getBlock(position.X, position.Y, position.Z)) + if !ok { + return false, nil + } + for _, name := range flattenBlockTag(set, predicate.Tag, nil) { + if state.Name == name { + return true, nil + } + } + return false, nil + case "minecraft:matching_fluids": + fluids, err := decodeStringList(predicate.Fluids) + if err != nil { + return false, fmt.Errorf("world: matching_fluids: %w", err) + } + state := r.getBlock(position.X, position.Y, position.Z) + for _, fluid := range fluids { + if stateFlags(state)&flagFluid != 0 && + (fluid == "minecraft:water" || fluid == "minecraft:flowing_water") && isWaterState(state) { + return true, nil + } + if stateFlags(state)&flagFluid != 0 && + (fluid == "minecraft:lava" || fluid == "minecraft:flowing_lava") && isLavaState(state) { + return true, nil + } + } + return false, nil + case "minecraft:all_of": + for _, child := range predicate.Predicates { + ok, err := r.testBlockPredicate(set, child, position) + if err != nil || !ok { + return ok, err + } + } + return true, nil + case "minecraft:any_of": + for _, child := range predicate.Predicates { + ok, err := r.testBlockPredicate(set, child, position) + if err != nil { + return false, err + } + if ok { + return true, nil + } + } + return false, nil + case "minecraft:not": + if len(predicate.Predicate) == 0 { + return false, fmt.Errorf("world: not predicate missing child") + } + ok, err := r.testBlockPredicate(set, predicate.Predicate, position) + return !ok, err + default: + return false, fmt.Errorf("world: unsupported block predicate %q", predicate.Type) + } +} + +func decodeStringList(raw json.RawMessage) ([]string, error) { + var one string + if err := json.Unmarshal(raw, &one); err == nil { + return []string{one}, nil + } + var many []string + if err := json.Unmarshal(raw, &many); err != nil || len(many) == 0 { + return nil, fmt.Errorf("invalid string list %s", raw) + } + return many, nil +} + +func isWaterState(state uint16) bool { + value, ok := stateByID(state) + return ok && stateFlags(state)&flagFluid != 0 && value.Name != "minecraft:lava" +} + +func isLavaState(state uint16) bool { + value, ok := stateByID(state) + return ok && value.Name == "minecraft:lava" +} diff --git a/internal/world/block_predicate_test.go b/internal/world/block_predicate_test.go new file mode 100644 index 0000000..b44d1d8 --- /dev/null +++ b/internal/world/block_predicate_test.go @@ -0,0 +1,62 @@ +package world + +import ( + "encoding/json" + "testing" + + "regionio/internal/worldgen" +) + +func TestDecorationRegionBlockPredicates(t *testing.T) { + chunk := NewChunk(0, 0, BiomePlains) + chunk.SetBlock(4, 20, 4, StateStone) + chunk.SetBlock(5, 20, 4, StateWater) + region, err := newDecorationRegion([]*Chunk{chunk}) + if err != nil { + t.Fatal(err) + } + set, err := worldgen.LoadFeatureSet() + if err != nil { + t.Fatal(err) + } + position := worldgen.FeaturePosition{X: 4, Y: 21, Z: 4} + tests := []struct { + name string + raw string + want bool + }{ + {"block offset", `{"type":"minecraft:matching_blocks","blocks":"minecraft:stone","offset":[0,-1,0]}`, true}, + {"block list", `{"type":"minecraft:matching_blocks","blocks":["minecraft:dirt","minecraft:stone"],"offset":[0,-1,0]}`, true}, + {"air tag", `{"type":"minecraft:matching_block_tag","tag":"minecraft:air"}`, true}, + {"water", `{"type":"minecraft:matching_fluids","fluids":["minecraft:water","minecraft:flowing_water"],"offset":[1,-1,0]}`, true}, + {"all", `{"type":"minecraft:all_of","predicates":[{"type":"minecraft:inside_world_bounds"},{"type":"minecraft:matching_block_tag","tag":"minecraft:air"}]}`, true}, + {"any", `{"type":"minecraft:any_of","predicates":[{"type":"minecraft:matching_blocks","blocks":"minecraft:dirt"},{"type":"minecraft:matching_block_tag","tag":"minecraft:air"}]}`, true}, + {"not", `{"type":"minecraft:not","predicate":{"type":"minecraft:matching_blocks","blocks":"minecraft:stone"}}`, true}, + {"outside", `{"type":"minecraft:inside_world_bounds","offset":[0,-16,0]}`, true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := region.testBlockPredicate(set, json.RawMessage(test.raw), position) + if err != nil { + t.Fatal(err) + } + if got != test.want { + t.Fatalf("predicate = %v, want %v", got, test.want) + } + }) + } +} + +func TestDecorationRegionRejectsUnsupportedPredicate(t *testing.T) { + region, err := newDecorationRegion([]*Chunk{NewChunk(0, 0, BiomePlains)}) + if err != nil { + t.Fatal(err) + } + set, err := worldgen.LoadFeatureSet() + if err != nil { + t.Fatal(err) + } + if _, err := region.testBlockPredicate(set, json.RawMessage(`{"type":"minecraft:would_survive","state":{"Name":"minecraft:oak_sapling"}}`), worldgen.FeaturePosition{}); err == nil { + t.Fatal("unsupported would_survive succeeded") + } +} diff --git a/internal/world/decoration_region.go b/internal/world/decoration_region.go index c6050a8..02d166d 100644 --- a/internal/world/decoration_region.go +++ b/internal/world/decoration_region.go @@ -129,13 +129,17 @@ func (r *decorationRegion) sourceBiomes() []string { } func (r *decorationRegion) placementContext(biomeAllows func(worldgen.FeaturePosition) bool) worldgen.PlacementContext { + set, err := worldgen.LoadFeatureSet() + if err != nil { + panic("world: loading feature datapack: " + err.Error()) + } return worldgen.PlacementContext{ MinY: MinY, Height: WorldHeight, BiomeAllows: biomeAllows, HeightAt: r.heightAt, BlockPredicate: func(predicate json.RawMessage, position worldgen.FeaturePosition) (bool, error) { - return false, fmt.Errorf("world: unsupported block predicate at (%d,%d,%d): %s", position.X, position.Y, position.Z, predicate) + return r.testBlockPredicate(set, predicate, position) }, } }