Evaluate world-aware block predicates
This commit is contained in:
parent
1a921434d9
commit
4ff62d7c96
3 changed files with 196 additions and 1 deletions
129
internal/world/block_predicate.go
Normal file
129
internal/world/block_predicate.go
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
62
internal/world/block_predicate_test.go
Normal file
62
internal/world/block_predicate_test.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -129,13 +129,17 @@ func (r *decorationRegion) sourceBiomes() []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *decorationRegion) placementContext(biomeAllows func(worldgen.FeaturePosition) bool) worldgen.PlacementContext {
|
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{
|
return worldgen.PlacementContext{
|
||||||
MinY: MinY,
|
MinY: MinY,
|
||||||
Height: WorldHeight,
|
Height: WorldHeight,
|
||||||
BiomeAllows: biomeAllows,
|
BiomeAllows: biomeAllows,
|
||||||
HeightAt: r.heightAt,
|
HeightAt: r.heightAt,
|
||||||
BlockPredicate: func(predicate json.RawMessage, position worldgen.FeaturePosition) (bool, error) {
|
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)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue