391 lines
12 KiB
Go
391 lines
12 KiB
Go
package sceneio
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"aego/core/ids"
|
|
"aego/scene"
|
|
"aego/schema"
|
|
)
|
|
|
|
type pfHealth struct {
|
|
Max float32 `aego:"max"`
|
|
Target scene.NodeRef `aego:"target"`
|
|
}
|
|
|
|
type pfTag struct {
|
|
Label string `aego:"label"`
|
|
}
|
|
|
|
const (
|
|
enemyPrefab = "f0000000000000000000000000000001"
|
|
squadPrefab = "f0000000000000000000000000000002"
|
|
enemyRoot = "a0000000000000000000000000000001"
|
|
enemyStomp = "a0000000000000000000000000000002"
|
|
sceneRoot = "b0000000000000000000000000000001"
|
|
sceneStomp = "b0000000000000000000000000000002"
|
|
squadRoot = "d0000000000000000000000000000001"
|
|
)
|
|
|
|
const enemySrc = `prefab version=1 {
|
|
node "a0000000000000000000000000000001" name="Enemy" enabled=true layers=1 {
|
|
transform {
|
|
position=[0, 0]
|
|
rotation=0
|
|
scale=[1, 1]
|
|
}
|
|
component "test.Health" version=1 {
|
|
max=5
|
|
target=node("a0000000000000000000000000000002")
|
|
}
|
|
node "a0000000000000000000000000000002" name="Stomp" enabled=true layers=1 {
|
|
transform {
|
|
position=[0, -1]
|
|
rotation=0
|
|
scale=[1, 1]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const enemyNoStompSrc = `prefab version=1 {
|
|
node "a0000000000000000000000000000001" name="Enemy" enabled=true layers=1 {
|
|
transform {
|
|
position=[0, 0]
|
|
rotation=0
|
|
scale=[1, 1]
|
|
}
|
|
component "test.Health" version=1 {
|
|
max=6
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const squadSrc = `prefab version=1 {
|
|
node "c0000000000000000000000000000001" name="Squad" enabled=true layers=1 {
|
|
transform {
|
|
position=[0, 0]
|
|
rotation=0
|
|
scale=[1, 1]
|
|
}
|
|
node "c0000000000000000000000000000002" prefab=asset("f0000000000000000000000000000001") {
|
|
override "a0000000000000000000000000000001" prop="transform.position" value=[2, 0] {}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const enemyCycleSrc = `prefab version=1 {
|
|
node "a0000000000000000000000000000001" name="Enemy" enabled=true layers=1 {
|
|
transform {
|
|
position=[0, 0]
|
|
rotation=0
|
|
scale=[1, 1]
|
|
}
|
|
node "e0000000000000000000000000000001" prefab=asset("f0000000000000000000000000000002") {}
|
|
}
|
|
}
|
|
`
|
|
|
|
const instanceSrc = `scene version=1 {
|
|
node "b0000000000000000000000000000001" prefab=asset("f0000000000000000000000000000001") {
|
|
id "a0000000000000000000000000000001" node=node("b0000000000000000000000000000001") {}
|
|
id "a0000000000000000000000000000002" node=node("b0000000000000000000000000000002") {}
|
|
override "a0000000000000000000000000000001" prop="test.Health.max" value=7 {}
|
|
override "a0000000000000000000000000000001" prop="transform.position" value=[3, 0] {}
|
|
}
|
|
}
|
|
`
|
|
|
|
const squadSceneSrc = `scene version=1 {
|
|
node "d0000000000000000000000000000001" prefab=asset("f0000000000000000000000000000002") {
|
|
id "c0000000000000000000000000000001" node=node("d0000000000000000000000000000001") {}
|
|
}
|
|
}
|
|
`
|
|
|
|
type fixture struct {
|
|
t *testing.T
|
|
types *scene.Types
|
|
lib *Library
|
|
}
|
|
|
|
func nodeID(t *testing.T, s string) ids.NodeID {
|
|
t.Helper()
|
|
id, err := ids.ParseNodeID(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
func assetID(t *testing.T, s string) ids.AssetID {
|
|
t.Helper()
|
|
id, err := ids.ParseAssetID(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
func newFixture(t *testing.T, prefabs map[string]string) *fixture {
|
|
t.Helper()
|
|
types := scene.NewTypes(schema.NewRegistry())
|
|
if err := RegisterUnknown(types); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := scene.RegisterComponent[pfHealth](types, schema.Name("test.Health")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := scene.RegisterComponent[pfTag](types, schema.Name("test.Tag")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := types.Schema().Freeze(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f := &fixture{t: t, types: types, lib: NewLibrary(types.Schema(), nil)}
|
|
for id, src := range prefabs {
|
|
f.setPrefab(id, src)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (f *fixture) setPrefab(id, src string) {
|
|
f.t.Helper()
|
|
p, err := ParsePrefab([]byte(src), id, f.types.Schema())
|
|
if err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
f.lib.Set(assetID(f.t, id), p)
|
|
}
|
|
|
|
func (f *fixture) load(data string) (*scene.Scene, *Report) {
|
|
f.t.Helper()
|
|
s, rep, err := Load([]byte(data), "t.aescene", f.types, nil, LoadOptions{KeepIDs: true, Prefabs: f.lib})
|
|
if err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
return s, rep
|
|
}
|
|
|
|
func (f *fixture) save(s *scene.Scene) string {
|
|
return string(SaveWith(s, f.lib))
|
|
}
|
|
|
|
func (f *fixture) stable(s *scene.Scene) string {
|
|
f.t.Helper()
|
|
out := f.save(s)
|
|
back, _ := f.load(out)
|
|
if again := f.save(back); again != out {
|
|
f.t.Fatalf("save is not stable:\n%s\n---\n%s", out, again)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func mustContain(t *testing.T, out string, want ...string) {
|
|
t.Helper()
|
|
for _, w := range want {
|
|
if !strings.Contains(out, w) {
|
|
t.Fatalf("missing %q in:\n%s", w, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstanceRoundTrip(t *testing.T) {
|
|
f := newFixture(t, map[string]string{enemyPrefab: enemySrc})
|
|
s, rep := f.load(instanceSrc)
|
|
if len(rep.Warnings) != 0 || len(rep.Unresolved) != 0 {
|
|
t.Fatalf("report = %+v", rep)
|
|
}
|
|
root := s.Handle(nodeID(t, sceneRoot))
|
|
stomp := s.Handle(nodeID(t, sceneStomp))
|
|
if root.IsZero() || stomp.IsZero() || s.Parent(stomp) != root || s.Name(stomp) != "Stomp" {
|
|
t.Fatal("instance nodes must keep their scene ids")
|
|
}
|
|
h, ok := scene.Get[pfHealth](s, root)
|
|
if !ok || h.Max != 7 || s.Local(root).Position.X != 3 {
|
|
t.Fatalf("overrides not applied: %+v", h)
|
|
}
|
|
if got, ok := h.Target.Resolve(s); !ok || got != stomp {
|
|
t.Fatal("internal reference must point at the instance node")
|
|
}
|
|
if pi, ok := InstanceOf(s, root); !ok || !pi.Owner || pi.Keys[nodeID(t, sceneStomp)] == "" {
|
|
t.Fatalf("instance metadata = %+v", pi)
|
|
}
|
|
out := f.stable(s)
|
|
mustContain(t, out, `prefab=asset("`+enemyPrefab+`")`, `prop="test.Health.max" value=7`, `prop="transform.position" value=[3, 0]`)
|
|
if strings.Contains(out, `name="Stomp"`) || strings.Contains(out, "component ") {
|
|
t.Fatalf("instance was flattened:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestOverridesFollowEdits(t *testing.T) {
|
|
f := newFixture(t, map[string]string{enemyPrefab: enemySrc})
|
|
s, _ := f.load(instanceSrc)
|
|
root := s.Handle(nodeID(t, sceneRoot))
|
|
stomp := s.Handle(nodeID(t, sceneStomp))
|
|
h, _ := scene.Get[pfHealth](s, root)
|
|
h.Max = 9
|
|
mustContain(t, f.save(s), `prop="test.Health.max" value=9`)
|
|
h.Max = 5
|
|
if out := f.save(s); strings.Contains(out, "test.Health.max") {
|
|
t.Fatalf("value equal to the prefab must not be an override:\n%s", out)
|
|
}
|
|
s.SetName(stomp, "Feet")
|
|
mustContain(t, f.stable(s), `override "`+enemyStomp+`" prop="node.name" value="Feet"`)
|
|
}
|
|
|
|
func TestPrefabChangeReachesClosedScene(t *testing.T) {
|
|
f := newFixture(t, map[string]string{enemyPrefab: enemySrc})
|
|
s, _ := f.load(instanceSrc)
|
|
s.SetName(s.Handle(nodeID(t, sceneStomp)), "Feet")
|
|
out := f.save(s)
|
|
f.setPrefab(enemyPrefab, enemyNoStompSrc)
|
|
s2, rep := f.load(out)
|
|
root := s2.Handle(nodeID(t, sceneRoot))
|
|
if !s2.Child(root, "Feet").IsZero() || s2.ChildCount(root) != 0 {
|
|
t.Fatal("node removed from the prefab must leave the instance")
|
|
}
|
|
if h, _ := scene.Get[pfHealth](s2, root); h.Max != 7 {
|
|
t.Fatal("instance override must survive the prefab change")
|
|
}
|
|
if len(rep.Warnings) == 0 {
|
|
t.Fatal("lost targets must be reported")
|
|
}
|
|
out2 := f.stable(s2)
|
|
mustContain(t, out2, `prop="node.name" value="Feet" orphan=true`)
|
|
f.setPrefab(enemyPrefab, enemySrc)
|
|
s3, _ := f.load(out2)
|
|
if s3.Child(s3.Handle(nodeID(t, sceneRoot)), "Stomp").IsZero() {
|
|
t.Fatal("restored prefab node must come back, orphans are not applied")
|
|
}
|
|
}
|
|
|
|
func TestMissingPrefabKeepsBlock(t *testing.T) {
|
|
f := newFixture(t, nil)
|
|
s, rep := f.load(instanceSrc)
|
|
if len(rep.Missing) != 1 || rep.Missing[0].Prefab != assetID(t, enemyPrefab) {
|
|
t.Fatalf("missing = %+v", rep.Missing)
|
|
}
|
|
root := s.Handle(nodeID(t, sceneRoot))
|
|
if root.IsZero() || s.Name(root) != "Missing prefab" {
|
|
t.Fatal("missing prefab must leave a placeholder")
|
|
}
|
|
if _, ok := scene.Get[pfHealth](s, root); ok {
|
|
t.Fatal("placeholder must be empty")
|
|
}
|
|
out := f.save(s)
|
|
mustContain(t, out, `prop="test.Health.max" value=7`, `id "`+enemyStomp+`" node=node("`+sceneStomp+`")`)
|
|
f.setPrefab(enemyPrefab, enemySrc)
|
|
s2, _ := f.load(out)
|
|
if h, ok := scene.Get[pfHealth](s2, s2.Handle(nodeID(t, sceneRoot))); !ok || h.Max != 7 {
|
|
t.Fatal("instance must come back once the prefab exists")
|
|
}
|
|
}
|
|
|
|
func TestAddedNodesComponentsAndOrder(t *testing.T) {
|
|
f := newFixture(t, map[string]string{enemyPrefab: enemySrc})
|
|
s, _ := f.load(instanceSrc)
|
|
root := s.Handle(nodeID(t, sceneRoot))
|
|
stomp := s.Handle(nodeID(t, sceneStomp))
|
|
extra := s.Create(root, "Extra")
|
|
scene.Add[pfTag](s, extra).Label = "x"
|
|
scene.Add[pfTag](s, stomp).Label = "t"
|
|
s.Flush()
|
|
out := f.stable(s)
|
|
mustContain(t, out, `addNode "`+enemyRoot+`"`, `addComponent "`+enemyStomp+`"`)
|
|
s2, _ := f.load(out)
|
|
root2 := s2.Handle(nodeID(t, sceneRoot))
|
|
e2 := s2.Child(root2, "Extra")
|
|
if tag, ok := scene.Get[pfTag](s2, e2); !ok || tag.Label != "x" || s2.ID(e2) != s.ID(extra) {
|
|
t.Fatal("added node lost")
|
|
}
|
|
if tag, ok := scene.Get[pfTag](s2, s2.Handle(nodeID(t, sceneStomp))); !ok || tag.Label != "t" {
|
|
t.Fatal("added component lost")
|
|
}
|
|
if err := s2.Reparent(e2, root2, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s2.Flush()
|
|
out2 := f.stable(s2)
|
|
mustContain(t, out2, `order "`+enemyRoot+`"`)
|
|
s3, _ := f.load(out2)
|
|
root3 := s3.Handle(nodeID(t, sceneRoot))
|
|
if s3.Name(s3.FirstChild(root3)) != "Extra" {
|
|
t.Fatal("child order lost")
|
|
}
|
|
s3.Destroy(s3.Handle(nodeID(t, sceneStomp)))
|
|
s3.Flush()
|
|
out3 := f.stable(s3)
|
|
mustContain(t, out3, `remove "`+enemyStomp+`"`)
|
|
s4, _ := f.load(out3)
|
|
if !s4.Child(s4.Handle(nodeID(t, sceneRoot)), "Stomp").IsZero() {
|
|
t.Fatal("removed node came back")
|
|
}
|
|
}
|
|
|
|
func TestTemplateInstantiate(t *testing.T) {
|
|
f := newFixture(t, map[string]string{enemyPrefab: enemySrc})
|
|
tmpl, err := f.lib.Template(assetID(t, enemyPrefab))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s := scene.New(f.types, nil, scene.Options{})
|
|
a, err := Instantiate(s, scene.NodeHandle{}, tmpl)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := Instantiate(s, scene.NodeHandle{}, tmpl)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Flush()
|
|
if s.ID(a) == s.ID(b) {
|
|
t.Fatal("every copy needs fresh ids")
|
|
}
|
|
for _, r := range []scene.NodeHandle{a, b} {
|
|
stomp := s.Child(r, "Stomp")
|
|
h, ok := scene.Get[pfHealth](s, r)
|
|
if got, rok := h.Target.Resolve(s); !ok || !rok || got != stomp {
|
|
t.Fatal("internal reference must point into the same copy")
|
|
}
|
|
if root, ok := PrefabRoot(s, stomp); !ok || root != r {
|
|
t.Fatal("prefab root of a child must be its copy")
|
|
}
|
|
if pi, _ := InstanceOf(s, r); pi.Owner {
|
|
t.Fatal("runtime copy must not own overrides")
|
|
}
|
|
}
|
|
mustContain(t, f.save(s), `instanceOf=asset("`+enemyPrefab+`")`)
|
|
}
|
|
|
|
func TestNestedPrefabAndCycle(t *testing.T) {
|
|
f := newFixture(t, map[string]string{enemyPrefab: enemySrc, squadPrefab: squadSrc})
|
|
s, _ := f.load(squadSceneSrc)
|
|
root := s.Handle(nodeID(t, squadRoot))
|
|
enemy := s.Child(root, "Enemy")
|
|
stomp := s.Child(enemy, "Stomp")
|
|
if s.Name(root) != "Squad" || enemy.IsZero() || stomp.IsZero() || s.Local(enemy).Position.X != 2 {
|
|
t.Fatal("nested prefab not expanded")
|
|
}
|
|
if h, _ := scene.Get[pfHealth](s, enemy); func() bool { got, ok := h.Target.Resolve(s); return !ok || got != stomp }() {
|
|
t.Fatal("nested reference must point into the nested copy")
|
|
}
|
|
if pi, ok := InstanceOf(s, enemy); !ok || !pi.Nested {
|
|
t.Fatal("nested root must be marked")
|
|
}
|
|
if r, _ := PrefabRoot(s, stomp); r != enemy {
|
|
t.Fatal("prefab root of a nested child is the nested root")
|
|
}
|
|
s.SetName(stomp, "Feet")
|
|
mustContain(t, f.stable(s), `override "c0000000000000000000000000000002/`+enemyStomp+`" prop="node.name" value="Feet"`)
|
|
f.setPrefab(enemyPrefab, enemyCycleSrc)
|
|
_, rep := f.load(squadSceneSrc)
|
|
if len(rep.Missing) != 1 {
|
|
t.Fatal("a prefab cycle must load as a missing instance")
|
|
}
|
|
}
|