389 lines
8.2 KiB
Go
389 lines
8.2 KiB
Go
package spawn
|
|
|
|
import (
|
|
"slices"
|
|
"unsafe"
|
|
|
|
"aego/contracts/persistence"
|
|
"aego/contracts/scriptevents"
|
|
"aego/contracts/scriptpool"
|
|
"aego/core/errs"
|
|
"aego/core/ids"
|
|
"aego/core/mathx"
|
|
"aego/engine"
|
|
"aego/save"
|
|
"aego/scene"
|
|
"aego/scene/sceneio"
|
|
"aego/scene/scriptcomp"
|
|
"aego/schema"
|
|
)
|
|
|
|
const poolName = "Pool"
|
|
|
|
var _ persistence.Spawner = (*Service)(nil)
|
|
|
|
type compSnap struct {
|
|
td *schema.TypeDescriptor
|
|
data unsafe.Pointer
|
|
}
|
|
|
|
type nodeSnap struct {
|
|
node scene.NodeHandle
|
|
enabled bool
|
|
pose scene.Pose
|
|
comps []compSnap
|
|
}
|
|
|
|
type member struct {
|
|
prefab ids.AssetID
|
|
nodes []nodeSnap
|
|
out bool
|
|
}
|
|
|
|
type Service struct {
|
|
scene *scene.Scene
|
|
world *engine.World
|
|
ev Events
|
|
lib *sceneio.Library
|
|
tmpl map[ids.AssetID]*sceneio.Template
|
|
free map[ids.AssetID][]scene.NodeHandle
|
|
members map[scene.NodeHandle]*member
|
|
parking []scene.NodeHandle
|
|
root scene.NodeHandle
|
|
unsub func()
|
|
pids map[scene.NodeHandle]ids.NodeID
|
|
byPID map[ids.NodeID]scene.NodeHandle
|
|
order []scene.NodeHandle
|
|
counter uint64
|
|
}
|
|
|
|
func NewService(s *scene.Scene, w *engine.World, ev Events) *Service {
|
|
svc := &Service{
|
|
scene: s,
|
|
world: w,
|
|
ev: ev,
|
|
lib: sceneio.NewLibrary(s.Registry(), s.Resolver()),
|
|
tmpl: map[ids.AssetID]*sceneio.Template{},
|
|
free: map[ids.AssetID][]scene.NodeHandle{},
|
|
members: map[scene.NodeHandle]*member{},
|
|
pids: map[scene.NodeHandle]ids.NodeID{},
|
|
byPID: map[ids.NodeID]scene.NodeHandle{},
|
|
}
|
|
svc.unsub = s.Events().Subscribe(svc.onEvent)
|
|
return svc
|
|
}
|
|
|
|
func (s *Service) Close() {
|
|
if s.unsub != nil {
|
|
s.unsub()
|
|
s.unsub = nil
|
|
}
|
|
}
|
|
|
|
func (s *Service) onEvent(ev scene.Event) {
|
|
if ev.Kind == scene.EvNodeDestroyed {
|
|
delete(s.members, ev.Node)
|
|
s.unassign(ev.Node)
|
|
}
|
|
}
|
|
|
|
func (s *Service) assign(h scene.NodeHandle, pid ids.NodeID) {
|
|
if old, ok := s.byPID[pid]; ok {
|
|
s.unassign(old)
|
|
}
|
|
s.unassign(h)
|
|
s.pids[h] = pid
|
|
s.byPID[pid] = h
|
|
s.order = append(s.order, h)
|
|
if n := save.Counter(pid); n > s.counter {
|
|
s.counter = n
|
|
}
|
|
}
|
|
|
|
func (s *Service) unassign(h scene.NodeHandle) {
|
|
pid, ok := s.pids[h]
|
|
if !ok {
|
|
return
|
|
}
|
|
delete(s.pids, h)
|
|
delete(s.byPID, pid)
|
|
if i := slices.Index(s.order, h); i >= 0 {
|
|
s.order = slices.Delete(s.order, i, i+1)
|
|
}
|
|
}
|
|
|
|
func (s *Service) PersistentID(h scene.NodeHandle) (ids.NodeID, bool) {
|
|
pid, ok := s.pids[h]
|
|
return pid, ok
|
|
}
|
|
|
|
func (s *Service) ByPersistentID(pid ids.NodeID) (scene.NodeHandle, bool) {
|
|
h, ok := s.byPID[pid]
|
|
return h, ok && s.scene.IsAlive(h)
|
|
}
|
|
|
|
func (s *Service) Prefab(h scene.NodeHandle) (ids.AssetID, bool) {
|
|
m := s.members[h]
|
|
if m == nil {
|
|
return ids.AssetID{}, false
|
|
}
|
|
return m.prefab, true
|
|
}
|
|
|
|
func (s *Service) EachSpawned(fn func(root scene.NodeHandle, prefab ids.AssetID, pid ids.NodeID)) {
|
|
for _, h := range s.order {
|
|
m := s.members[h]
|
|
if m == nil || m.out || !s.scene.IsAlive(h) {
|
|
continue
|
|
}
|
|
fn(h, m.prefab, s.pids[h])
|
|
}
|
|
}
|
|
|
|
func (s *Service) Counter() uint64 {
|
|
return s.counter
|
|
}
|
|
|
|
func (s *Service) SetCounter(n uint64) {
|
|
s.counter = max(s.counter, n)
|
|
}
|
|
|
|
func (s *Service) Pooled(prefab ids.AssetID) int {
|
|
return len(s.free[prefab])
|
|
}
|
|
|
|
func (s *Service) template(id ids.AssetID) (*sceneio.Template, error) {
|
|
if t, ok := s.tmpl[id]; ok {
|
|
return t, nil
|
|
}
|
|
if id.IsZero() {
|
|
return nil, errs.New(errs.AssetNotFound, "spawn: prefab reference is nil")
|
|
}
|
|
t, err := s.lib.Template(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.tmpl[id] = t
|
|
return t, nil
|
|
}
|
|
|
|
func (s *Service) lifecycle() scriptpool.Lifecycle {
|
|
if s.world == nil {
|
|
return nil
|
|
}
|
|
lc, _ := engine.ServiceOf[scriptpool.Lifecycle](s.world, scriptpool.ServiceID)
|
|
return lc
|
|
}
|
|
|
|
func (s *Service) emit(m *member, ev scriptevents.EventID) {
|
|
if ev == scriptevents.NoEvent || s.world == nil {
|
|
return
|
|
}
|
|
sink, ok := engine.ServiceOf[scriptevents.Sink](s.world, scriptevents.ServiceID)
|
|
if !ok {
|
|
return
|
|
}
|
|
for i := range m.nodes {
|
|
if h := m.nodes[i].node; s.scene.IsAlive(h) {
|
|
sink.Emit(h, ev, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) poolRoot() scene.NodeHandle {
|
|
if !s.scene.IsAlive(s.root) {
|
|
s.root = s.scene.Create(s.scene.Root(), poolName)
|
|
s.scene.SetEnabled(s.root, false)
|
|
}
|
|
return s.root
|
|
}
|
|
|
|
func (s *Service) snap(h scene.NodeHandle) nodeSnap {
|
|
sc := s.scene
|
|
ns := nodeSnap{node: h, enabled: sc.Enabled(h), pose: sc.Pose(h)}
|
|
reg := sc.Registry()
|
|
sc.ComponentsOf(h, func(t schema.TypeID) {
|
|
td, ok := reg.ByID(t)
|
|
if !ok || td.Hidden || t == scriptcomp.ScriptTypeID || td.New == nil || td.CloneInto == nil {
|
|
return
|
|
}
|
|
p, ok := sc.GetByType(h, t)
|
|
if !ok {
|
|
return
|
|
}
|
|
data := td.New()
|
|
td.CloneInto(data, p)
|
|
ns.comps = append(ns.comps, compSnap{td: td, data: data})
|
|
})
|
|
return ns
|
|
}
|
|
|
|
func (s *Service) capture(h scene.NodeHandle, m *member) {
|
|
m.nodes = append(m.nodes, s.snap(h))
|
|
for c := s.scene.FirstChild(h); !c.IsZero(); c = s.scene.NextSibling(c) {
|
|
s.capture(c, m)
|
|
}
|
|
}
|
|
|
|
func (s *Service) restore(m *member) {
|
|
sc := s.scene
|
|
for i := range m.nodes {
|
|
n := &m.nodes[i]
|
|
if !sc.IsAlive(n.node) {
|
|
continue
|
|
}
|
|
sc.SetEnabled(n.node, i == 0 || n.enabled)
|
|
if i > 0 {
|
|
sc.SetPose(n.node, n.pose)
|
|
}
|
|
for _, c := range n.comps {
|
|
if p, ok := sc.GetByType(n.node, c.td.ID); ok {
|
|
c.td.CloneInto(p, c.data)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) reuse(prefab ids.AssetID) (scene.NodeHandle, *member) {
|
|
list := s.free[prefab]
|
|
for len(list) > 0 {
|
|
h := list[len(list)-1]
|
|
list = list[:len(list)-1]
|
|
if m := s.members[h]; m != nil && s.scene.IsAlive(h) {
|
|
s.free[prefab] = list
|
|
return h, m
|
|
}
|
|
}
|
|
s.free[prefab] = list
|
|
return scene.NodeHandle{}, nil
|
|
}
|
|
|
|
func (s *Service) instantiate(prefab ids.AssetID, parent scene.NodeHandle) (scene.NodeHandle, *member, error) {
|
|
t, err := s.template(prefab)
|
|
if err != nil {
|
|
return scene.NodeHandle{}, nil, err
|
|
}
|
|
h, err := sceneio.Instantiate(s.scene, parent, t)
|
|
if err != nil {
|
|
return scene.NodeHandle{}, nil, err
|
|
}
|
|
m := &member{prefab: prefab}
|
|
s.capture(h, m)
|
|
s.members[h] = m
|
|
return h, m, nil
|
|
}
|
|
|
|
func (s *Service) Spawn(prefab ids.AssetID, parent scene.NodeHandle, pos mathx.Vec2, rot float32) (scene.NodeHandle, error) {
|
|
return s.SpawnAs(prefab, parent, pos, rot, save.PersistentID(s.counter+1))
|
|
}
|
|
|
|
func (s *Service) SpawnAs(prefab ids.AssetID, parent scene.NodeHandle, pos mathx.Vec2, rot float32, pid ids.NodeID) (scene.NodeHandle, error) {
|
|
sc := s.scene
|
|
switch {
|
|
case parent.IsZero():
|
|
parent = sc.Root()
|
|
case !sc.IsAlive(parent):
|
|
return scene.NodeHandle{}, errs.New(errs.SceneDeadNode, "spawn: parent is nil or destroyed")
|
|
}
|
|
h, m := s.reuse(prefab)
|
|
fresh := m == nil
|
|
if fresh {
|
|
var err error
|
|
if h, m, err = s.instantiate(prefab, parent); err != nil {
|
|
return scene.NodeHandle{}, err
|
|
}
|
|
} else {
|
|
if sc.Parent(h) != parent {
|
|
if err := sc.Reparent(h, parent, -1); err != nil {
|
|
return scene.NodeHandle{}, err
|
|
}
|
|
}
|
|
s.restore(m)
|
|
}
|
|
m.out = false
|
|
if !pid.IsZero() {
|
|
s.assign(h, pid)
|
|
}
|
|
if sc.Space(h) == scene.Space2D {
|
|
t := sc.Local(h)
|
|
t.Position, t.Rotation = pos, rot
|
|
sc.SetLocal(h, t)
|
|
}
|
|
if !fresh {
|
|
if lc := s.lifecycle(); lc != nil {
|
|
for i := range m.nodes {
|
|
lc.Revive(m.nodes[i].node)
|
|
}
|
|
}
|
|
}
|
|
s.emit(m, s.ev.Spawned)
|
|
return h, nil
|
|
}
|
|
|
|
func (s *Service) Despawn(h scene.NodeHandle) {
|
|
sc := s.scene
|
|
if !sc.IsAlive(h) {
|
|
return
|
|
}
|
|
m := s.members[h]
|
|
if m == nil {
|
|
sc.Destroy(h)
|
|
return
|
|
}
|
|
if m.out {
|
|
return
|
|
}
|
|
m.out = true
|
|
s.unassign(h)
|
|
s.emit(m, s.ev.Despawned)
|
|
sc.SetEnabled(h, false)
|
|
s.parking = append(s.parking, h)
|
|
}
|
|
|
|
func (s *Service) Pool(prefab ids.AssetID, n int) error {
|
|
root := s.poolRoot()
|
|
for range n {
|
|
h, m, err := s.instantiate(prefab, root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.out = true
|
|
s.scene.SetEnabled(h, false)
|
|
s.parking = append(s.parking, h)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) parkAll(lc scriptpool.Lifecycle, m *member) bool {
|
|
ok := true
|
|
for i := range m.nodes {
|
|
if h := m.nodes[i].node; s.scene.IsAlive(h) && !lc.Park(h) {
|
|
ok = false
|
|
}
|
|
}
|
|
return ok
|
|
}
|
|
|
|
func (s *Service) Park() {
|
|
if len(s.parking) == 0 {
|
|
return
|
|
}
|
|
lc := s.lifecycle()
|
|
keep := s.parking[:0]
|
|
for _, h := range s.parking {
|
|
m := s.members[h]
|
|
if m == nil || !m.out || !s.scene.IsAlive(h) {
|
|
continue
|
|
}
|
|
if lc != nil && !s.parkAll(lc, m) {
|
|
keep = append(keep, h)
|
|
continue
|
|
}
|
|
if root := s.poolRoot(); s.scene.Parent(h) != root {
|
|
_ = s.scene.Reparent(h, root, -1)
|
|
}
|
|
s.free[m.prefab] = append(s.free[m.prefab], h)
|
|
}
|
|
s.parking = keep
|
|
}
|