443 lines
9.8 KiB
Go
443 lines
9.8 KiB
Go
package scene
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"aego/core/errs"
|
|
"aego/core/mathx"
|
|
"aego/schema"
|
|
)
|
|
|
|
func TestAddIsVisibleThroughGetImmediately(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
|
|
h := Add[Health](s, a)
|
|
if h == nil {
|
|
t.Fatal("Add returned nil")
|
|
}
|
|
h.Max = 100
|
|
got, ok := Get[Health](s, a)
|
|
if !ok || got.Max != 100 {
|
|
t.Fatal("Get must see a pending component")
|
|
}
|
|
if !Has[Health](s, a) {
|
|
t.Fatal("Has must see a pending component")
|
|
}
|
|
if Store[Health](s).Len() != 0 {
|
|
t.Fatal("pending component must not be in dense storage yet")
|
|
}
|
|
s.Flush()
|
|
if Store[Health](s).Len() != 1 {
|
|
t.Fatal("Flush must move pending into dense storage")
|
|
}
|
|
got, _ = Get[Health](s, a)
|
|
if got.Max != 100 {
|
|
t.Fatal("data lost during flush")
|
|
}
|
|
}
|
|
|
|
func TestAddIsInvisibleToIterationUntilFlush(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
Add[Health](s, a)
|
|
s.Flush()
|
|
s.UpdateTransforms()
|
|
|
|
added := 0
|
|
visited := 0
|
|
Store[Health](s).Each(func(h NodeHandle, _ *Health) {
|
|
visited++
|
|
n := s.Create(s.Root(), "spawned")
|
|
Add[Health](s, n)
|
|
added++
|
|
if visited > 100 {
|
|
t.Fatal("iteration looped over its own additions")
|
|
}
|
|
})
|
|
if visited != 1 || added != 1 {
|
|
t.Fatalf("visited=%d added=%d", visited, added)
|
|
}
|
|
s.Flush()
|
|
s.UpdateTransforms()
|
|
if n := Store[Health](s).Len(); n != 2 {
|
|
t.Fatalf("dense length = %d", n)
|
|
}
|
|
}
|
|
|
|
func TestPendingPointersSurviveMoreAdds(t *testing.T) {
|
|
s := newScene(t)
|
|
first := s.Create(s.Root(), "first")
|
|
p := Add[Health](s, first)
|
|
p.Max = 42
|
|
for i := 0; i < pendingChunk*3; i++ {
|
|
n := s.Create(s.Root(), "n")
|
|
q := Add[Health](s, n)
|
|
q.Max = float32(i)
|
|
}
|
|
if p.Max != 42 {
|
|
t.Fatal("a pending pointer was invalidated by later adds")
|
|
}
|
|
s.Flush()
|
|
got, _ := Get[Health](s, first)
|
|
if got.Max != 42 {
|
|
t.Fatal("pending data lost")
|
|
}
|
|
}
|
|
|
|
func TestRemoveIsImmediateButDeferredInStorage(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
b := s.Create(s.Root(), "B")
|
|
Add[Health](s, a)
|
|
Add[Health](s, b)
|
|
s.Flush()
|
|
s.UpdateTransforms()
|
|
|
|
Remove[Health](s, a)
|
|
if Has[Health](s, a) {
|
|
t.Fatal("Has must be false right after Remove")
|
|
}
|
|
if _, ok := Get[Health](s, a); ok {
|
|
t.Fatal("Get must be false right after Remove")
|
|
}
|
|
if Store[Health](s).Len() != 2 {
|
|
t.Fatal("storage must not shrink before Flush")
|
|
}
|
|
visited := 0
|
|
Store[Health](s).Each(func(h NodeHandle, _ *Health) {
|
|
if h == a {
|
|
t.Fatal("iteration must skip a removed component")
|
|
}
|
|
visited++
|
|
})
|
|
if visited != 1 {
|
|
t.Fatalf("visited = %d", visited)
|
|
}
|
|
s.Flush()
|
|
if Store[Health](s).Len() != 1 {
|
|
t.Fatalf("dense length after flush = %d", Store[Health](s).Len())
|
|
}
|
|
if !Has[Health](s, b) {
|
|
t.Fatal("swap-remove damaged the surviving component")
|
|
}
|
|
}
|
|
|
|
func TestSwapRemoveKeepsNeighbourData(t *testing.T) {
|
|
s := newScene(t)
|
|
var hs []NodeHandle
|
|
for i := 0; i < 8; i++ {
|
|
n := s.Create(s.Root(), "n")
|
|
c := Add[Health](s, n)
|
|
c.Max = float32(i)
|
|
hs = append(hs, n)
|
|
}
|
|
s.Flush()
|
|
s.UpdateTransforms()
|
|
|
|
Remove[Health](s, hs[2])
|
|
Remove[Health](s, hs[5])
|
|
s.Flush()
|
|
|
|
for i, h := range hs {
|
|
c, ok := Get[Health](s, h)
|
|
if i == 2 || i == 5 {
|
|
if ok {
|
|
t.Fatalf("component %d survived removal", i)
|
|
}
|
|
continue
|
|
}
|
|
if !ok || c.Max != float32(i) {
|
|
t.Fatalf("component %d damaged: ok=%v value=%v", i, ok, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddAfterRemoveInSamePhase(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
c := Add[Health](s, a)
|
|
c.Max = 5
|
|
s.Flush()
|
|
|
|
Remove[Health](s, a)
|
|
revived := Add[Health](s, a)
|
|
if revived == nil {
|
|
t.Fatal("Add after Remove returned nil")
|
|
}
|
|
if revived.Max != 0 {
|
|
t.Fatalf("revived component must be zeroed, got %v", revived.Max)
|
|
}
|
|
revived.Max = 9
|
|
s.Flush()
|
|
got, ok := Get[Health](s, a)
|
|
if !ok || got.Max != 9 {
|
|
t.Fatalf("revived component lost: ok=%v value=%v", ok, got)
|
|
}
|
|
if Store[Health](s).Len() != 1 {
|
|
t.Fatalf("dense length = %d", Store[Health](s).Len())
|
|
}
|
|
}
|
|
|
|
func TestRemovePendingComponent(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
Add[Health](s, a)
|
|
Remove[Health](s, a)
|
|
if Has[Health](s, a) {
|
|
t.Fatal("removing a pending component must hide it")
|
|
}
|
|
s.Flush()
|
|
if Store[Health](s).Len() != 0 {
|
|
t.Fatal("a removed pending component must not reach storage")
|
|
}
|
|
Add[Health](s, a)
|
|
s.Flush()
|
|
if Store[Health](s).Len() != 1 {
|
|
t.Fatal("re-adding after removing a pending component failed")
|
|
}
|
|
}
|
|
|
|
func TestDoubleAddReturnsExisting(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
first := Add[Health](s, a)
|
|
first.Max = 7
|
|
second := Add[Health](s, a)
|
|
if second.Max != 7 {
|
|
t.Fatal("second Add must return the pending component")
|
|
}
|
|
s.Flush()
|
|
third := Add[Health](s, a)
|
|
third.Current = 3
|
|
if Store[Health](s).Len() != 1 {
|
|
t.Fatal("Add must not duplicate a dense component")
|
|
}
|
|
got, _ := Get[Health](s, a)
|
|
if got.Max != 7 || got.Current != 3 {
|
|
t.Fatalf("component = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestAddToDestroyedNodeReturnsNil(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
s.Flush()
|
|
s.Destroy(a)
|
|
if p := Add[Health](s, a); p != nil {
|
|
t.Fatal("Add on a pending-destroy node must return nil")
|
|
}
|
|
s.Flush()
|
|
if p := Add[Health](s, a); p != nil {
|
|
t.Fatal("Add on a dead node must return nil")
|
|
}
|
|
}
|
|
|
|
func TestDestroyDropsComponents(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
child := s.Create(a, "Child")
|
|
Add[Health](s, a)
|
|
Add[Velocity](s, child)
|
|
s.Flush()
|
|
|
|
s.Destroy(a)
|
|
s.Flush()
|
|
if Store[Health](s).Len() != 0 || Store[Velocity](s).Len() != 0 {
|
|
t.Fatal("destroying a subtree must drop its components")
|
|
}
|
|
}
|
|
|
|
func TestComponentsOf(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
Add[Health](s, a)
|
|
Add[Tag](s, a)
|
|
s.Flush()
|
|
|
|
seen := map[schema.TypeID]bool{}
|
|
s.ComponentsOf(a, func(id schema.TypeID) { seen[id] = true })
|
|
if len(seen) != 2 {
|
|
t.Fatalf("components = %d", len(seen))
|
|
}
|
|
if !seen[Store[Health](s).Type()] || !seen[Store[Tag](s).Type()] {
|
|
t.Fatal("wrong component types reported")
|
|
}
|
|
}
|
|
|
|
func TestPropertyAccess(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
Add[Health](s, a)
|
|
s.Flush()
|
|
|
|
typeID := Store[Health](s).Type()
|
|
fd, ok := s.Registry().Field(typeID, "max")
|
|
if !ok {
|
|
t.Fatal("field descriptor missing")
|
|
}
|
|
before := s.Revision()
|
|
if err := s.SetProperty(a, typeID, fd.ID, schema.Float32(55)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Revision() == before {
|
|
t.Fatal("SetProperty must bump the revision")
|
|
}
|
|
if !s.Dirty() {
|
|
t.Fatal("scene must be dirty after SetProperty")
|
|
}
|
|
v, err := s.GetProperty(a, typeID, fd.ID)
|
|
if err != nil || v.AsFloat() != 55 {
|
|
t.Fatalf("GetProperty = %+v %v", v, err)
|
|
}
|
|
s.MarkSaved()
|
|
if s.Dirty() {
|
|
t.Fatal("MarkSaved must clear the dirty flag")
|
|
}
|
|
|
|
b := s.Create(s.Root(), "B")
|
|
s.Flush()
|
|
if err := s.SetProperty(b, typeID, fd.ID, schema.Float32(1)); !errs.Has(err, errs.SceneNoComponent) {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestByTypeAccess(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
typeID := Store[Health](s).Type()
|
|
|
|
p, err := s.AddByType(a, typeID)
|
|
if err != nil || p == nil {
|
|
t.Fatalf("AddByType: %v", err)
|
|
}
|
|
if _, ok := s.GetByType(a, typeID); !ok {
|
|
t.Fatal("GetByType must see the pending component")
|
|
}
|
|
s.Flush()
|
|
s.RemoveByType(a, typeID)
|
|
if _, ok := s.GetByType(a, typeID); ok {
|
|
t.Fatal("RemoveByType had no effect")
|
|
}
|
|
if _, err := s.AddByType(a, schema.TypeID(12345)); !errs.Has(err, errs.SceneUnknownComponent) {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestQueries(t *testing.T) {
|
|
s := newScene(t)
|
|
both := s.Create(s.Root(), "Both")
|
|
onlyHealth := s.Create(s.Root(), "Health")
|
|
onlyVelocity := s.Create(s.Root(), "Velocity")
|
|
Add[Health](s, both)
|
|
Add[Velocity](s, both)
|
|
Add[Health](s, onlyHealth)
|
|
Add[Velocity](s, onlyVelocity)
|
|
s.Flush()
|
|
s.UpdateTransforms()
|
|
|
|
if n := Query1[Health](s).Count(); n != 2 {
|
|
t.Fatalf("Query1 count = %d", n)
|
|
}
|
|
seen := 0
|
|
Query2[Health, Velocity](s).Each(func(h NodeHandle, _ *Health, _ *Velocity) {
|
|
if h != both {
|
|
t.Fatalf("unexpected node %q", s.Name(h))
|
|
}
|
|
seen++
|
|
})
|
|
if seen != 1 {
|
|
t.Fatalf("Query2 visited %d nodes", seen)
|
|
}
|
|
Query2[Velocity, Health](s).Each(func(h NodeHandle, _ *Velocity, _ *Health) {
|
|
if h != both {
|
|
t.Fatal("reversed query must give the same result")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestQuerySkipsDisabledAndPendingDestroy(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
b := s.Create(s.Root(), "B")
|
|
c := s.Create(s.Root(), "C")
|
|
for _, h := range []NodeHandle{a, b, c} {
|
|
Add[Health](s, h)
|
|
}
|
|
s.Flush()
|
|
s.UpdateTransforms()
|
|
|
|
s.SetEnabled(b, false)
|
|
s.UpdateTransforms()
|
|
s.Destroy(c)
|
|
|
|
var visited []NodeHandle
|
|
Query1[Health](s).Each(func(h NodeHandle, _ *Health) {
|
|
visited = append(visited, h)
|
|
})
|
|
if len(visited) != 1 || visited[0] != a {
|
|
t.Fatalf("visited = %v", names(s, visited))
|
|
}
|
|
all := 0
|
|
Store[Health](s).EachAll(func(NodeHandle, *Health) { all++ })
|
|
if all != 3 {
|
|
t.Fatalf("EachAll visited %d", all)
|
|
}
|
|
}
|
|
|
|
func TestEventsFire(t *testing.T) {
|
|
s := newScene(t)
|
|
var kinds []EventKind
|
|
stop := s.Events().Subscribe(func(e Event) {
|
|
kinds = append(kinds, e.Kind)
|
|
})
|
|
|
|
a := s.Create(s.Root(), "A")
|
|
Add[Health](s, a)
|
|
s.Flush()
|
|
Remove[Health](s, a)
|
|
s.SetName(a, "B")
|
|
s.Destroy(a)
|
|
s.Flush()
|
|
stop()
|
|
s.Create(s.Root(), "After")
|
|
|
|
want := map[EventKind]bool{
|
|
EvNodeCreated: true,
|
|
EvComponentAdded: true,
|
|
EvComponentRemoved: true,
|
|
EvNodeRenamed: true,
|
|
EvNodeDestroyed: true,
|
|
EvHierarchyChanged: true,
|
|
}
|
|
got := map[EventKind]bool{}
|
|
for _, k := range kinds {
|
|
got[k] = true
|
|
}
|
|
for k := range want {
|
|
if !got[k] {
|
|
t.Fatalf("event %s never fired; got %v", k, kinds)
|
|
}
|
|
}
|
|
n := len(kinds)
|
|
if n == 0 {
|
|
t.Fatal("no events")
|
|
}
|
|
if len(kinds) != n {
|
|
t.Fatal("unsubscribe failed")
|
|
}
|
|
}
|
|
|
|
func TestStatsAndVec(t *testing.T) {
|
|
s := newScene(t)
|
|
a := s.Create(s.Root(), "A")
|
|
v := Add[Velocity](s, a)
|
|
v.Value = mathx.Vec2{X: 1, Y: 2}
|
|
s.Flush()
|
|
|
|
st := s.Stats()
|
|
if st.Components != 1 || st.Alive < 2 {
|
|
t.Fatalf("stats = %+v", st)
|
|
}
|
|
}
|