420 lines
10 KiB
Go
420 lines
10 KiB
Go
package module
|
|
|
|
import (
|
|
"errors"
|
|
"slices"
|
|
"strings"
|
|
|
|
"aego/contracts/scriptevents"
|
|
"aego/core/errs"
|
|
"aego/core/log"
|
|
"aego/engine"
|
|
"aego/engine/settings"
|
|
"aego/scene"
|
|
"aego/schema"
|
|
)
|
|
|
|
const EngineModuleID = "engine.modules"
|
|
|
|
type Manifest struct {
|
|
Modules []Info
|
|
Systems []engine.SystemDesc
|
|
Templates []schema.TemplateDesc
|
|
Icons []IconSet
|
|
Docs []DocSource
|
|
Events []scriptevents.Event
|
|
Sections []*Section
|
|
ScriptOwners map[string]string
|
|
EventOwners map[string]string
|
|
Dev []Hook
|
|
Editor []Hook
|
|
Warnings []string
|
|
Settings []settings.Desc
|
|
SettingVersions []settings.OwnerVersion
|
|
order []*entry
|
|
services []serviceDesc
|
|
engines []ownedEngine
|
|
inited []*entry
|
|
provided []provision
|
|
validators []ownedValidator
|
|
}
|
|
|
|
func (r *Registry) Build() (*Manifest, error) {
|
|
if r.built {
|
|
return nil, errs.New(errs.EngineModule, "module registry is already built")
|
|
}
|
|
r.built = true
|
|
order, err := r.sort()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, e := range order {
|
|
r.cur = e
|
|
if err := e.mod.Register(r); err != nil {
|
|
r.wrap(err, "module "+e.info.ID+" failed to register")
|
|
}
|
|
r.cur = nil
|
|
}
|
|
for _, f := range r.finals {
|
|
if err := f.fn(r); err != nil {
|
|
r.errs = append(r.errs, err)
|
|
}
|
|
}
|
|
icons := r.scanIcons()
|
|
r.checkComponents(icons)
|
|
r.checkSystems()
|
|
r.checkServices()
|
|
r.checkTemplates(icons)
|
|
r.checkSections()
|
|
r.checkSettings()
|
|
if len(r.errs) > 0 {
|
|
return nil, errors.Join(r.errs...)
|
|
}
|
|
return r.manifest(order), nil
|
|
}
|
|
|
|
func (r *Registry) sort() ([]*entry, error) {
|
|
byID := make(map[string]*entry, len(r.modules))
|
|
for _, e := range r.modules {
|
|
byID[e.info.ID] = e
|
|
}
|
|
var missing []error
|
|
for _, e := range r.modules {
|
|
for _, dep := range e.info.Requires {
|
|
if _, ok := byID[dep]; !ok {
|
|
missing = append(missing, errs.New(errs.EngineModule,
|
|
"module "+e.info.ID+" requires "+dep+", which is not registered", log.F("module", e.info.ID)))
|
|
}
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
return nil, errors.Join(missing...)
|
|
}
|
|
done := make(map[string]bool, len(r.modules))
|
|
out := make([]*entry, 0, len(r.modules))
|
|
for len(out) < len(r.modules) {
|
|
var next *entry
|
|
for _, e := range r.modules {
|
|
if done[e.info.ID] {
|
|
continue
|
|
}
|
|
ready := true
|
|
for _, dep := range e.info.Requires {
|
|
if !done[dep] {
|
|
ready = false
|
|
break
|
|
}
|
|
}
|
|
if ready {
|
|
next = e
|
|
break
|
|
}
|
|
}
|
|
if next == nil {
|
|
return nil, errs.New(errs.EngineModule, "module requirements form a cycle: "+strings.Join(r.cycle(byID, done), " -> "))
|
|
}
|
|
done[next.info.ID] = true
|
|
out = append(out, next)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Registry) cycle(byID map[string]*entry, done map[string]bool) []string {
|
|
state := map[string]uint8{}
|
|
var stack, found []string
|
|
var visit func(id string) bool
|
|
visit = func(id string) bool {
|
|
state[id] = 1
|
|
stack = append(stack, id)
|
|
for _, dep := range byID[id].info.Requires {
|
|
if done[dep] {
|
|
continue
|
|
}
|
|
switch state[dep] {
|
|
case 1:
|
|
found = append(slices.Clone(stack[slices.Index(stack, dep):]), dep)
|
|
return true
|
|
case 0:
|
|
if visit(dep) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
stack = stack[:len(stack)-1]
|
|
state[id] = 2
|
|
return false
|
|
}
|
|
for _, e := range r.modules {
|
|
if !done[e.info.ID] && state[e.info.ID] == 0 && visit(e.info.ID) {
|
|
return found
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Registry) checkComponents(icons map[string]bool) {
|
|
sr := r.Schema()
|
|
for _, d := range r.comps {
|
|
for _, name := range d.Requires {
|
|
td, ok := sr.ByName(name)
|
|
if !ok || td.Kind != schema.TypeComponent {
|
|
r.report(d.Owner, "component "+d.FullName+" requires "+name+", which is not a registered component")
|
|
}
|
|
}
|
|
r.useIcon(icons, d.Owner, d.Icon, "component "+d.FullName)
|
|
if t := d.Template; t != nil {
|
|
if t.Category == "" {
|
|
t.Category = d.Category
|
|
}
|
|
if t.Icon == "" {
|
|
t.Icon = d.Icon
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func reserved(p engine.Phase) bool {
|
|
return p == engine.PhaseFrameBegin || p == engine.PhasePollEvents || p == engine.PhaseFrameEnd
|
|
}
|
|
|
|
func (r *Registry) checkSystems() {
|
|
for _, s := range r.systems {
|
|
id := s.owner.info.ID
|
|
if !strings.HasPrefix(s.desc.ID, id+".") {
|
|
r.report(id, "system "+s.desc.ID+" must be named "+id+".<name>")
|
|
}
|
|
if !s.owner.info.Builtin && reserved(s.desc.Phase) {
|
|
r.report(id, "system "+s.desc.ID+" uses the reserved phase "+s.desc.Phase.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Registry) checkServices() {
|
|
seen := map[string]string{}
|
|
for _, s := range r.services {
|
|
if other, dup := seen[s.id]; dup {
|
|
r.report(s.owner, "service "+s.id+" is already registered by "+other)
|
|
continue
|
|
}
|
|
seen[s.id] = s.owner
|
|
}
|
|
}
|
|
|
|
func (r *Registry) checkTemplates(icons map[string]bool) {
|
|
seen := map[string]string{}
|
|
for _, t := range r.templates {
|
|
if t.Name == "" {
|
|
r.report(t.Owner, "template needs a name")
|
|
continue
|
|
}
|
|
if t.ID == "" {
|
|
t.ID = t.Owner + "." + strings.ToLower(t.Name)
|
|
}
|
|
if other, dup := seen[t.ID]; dup {
|
|
r.report(t.Owner, "template "+t.ID+" is already registered by "+other)
|
|
continue
|
|
}
|
|
seen[t.ID] = t.Owner
|
|
if _, ok := t.Setup.(func(*scene.Scene, scene.NodeHandle)); !ok {
|
|
r.report(t.Owner, "template "+t.ID+" needs Setup func(*scene.Scene, scene.NodeHandle)")
|
|
}
|
|
r.useIcon(icons, t.Owner, t.Icon, "template "+t.ID)
|
|
if t.Menu != "" {
|
|
r.markIcon(t.Owner, "cat."+t.Owner, "template menu "+t.Menu)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Registry) checkSections() {
|
|
seen := map[string]string{}
|
|
for _, s := range r.sections {
|
|
if s.ID != s.Owner && !strings.HasPrefix(s.ID, s.Owner+".") {
|
|
r.report(s.Owner, "project section "+s.ID+" must be named "+s.Owner+" or "+s.Owner+".<name>")
|
|
}
|
|
for _, h := range s.Heads() {
|
|
if other, dup := seen[h]; dup {
|
|
r.report(s.Owner, "project section "+h+" is already registered by "+other)
|
|
continue
|
|
}
|
|
seen[h] = s.Owner
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Registry) manifest(order []*entry) *Manifest {
|
|
m := &Manifest{
|
|
order: order, services: r.services, engines: r.engines, provided: r.provided,
|
|
Icons: r.icons, Docs: r.docs, ScriptOwners: r.owners,
|
|
Dev: r.dev, Editor: r.editor, Warnings: r.warns,
|
|
Events: r.Events(), Sections: r.sections, EventOwners: r.eventOwners(),
|
|
Settings: r.settings, SettingVersions: r.versions,
|
|
validators: r.validators,
|
|
}
|
|
for _, e := range order {
|
|
m.Modules = append(m.Modules, e.info)
|
|
}
|
|
for _, s := range r.systems {
|
|
m.Systems = append(m.Systems, s.desc)
|
|
}
|
|
for _, t := range r.templates {
|
|
m.Templates = append(m.Templates, *t)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (r *Registry) eventOwners() map[string]string {
|
|
out := make(map[string]string, len(r.events))
|
|
for _, e := range r.events {
|
|
out[e.ev.Name] = e.owner
|
|
}
|
|
return out
|
|
}
|
|
|
|
type engineModule struct {
|
|
m *Manifest
|
|
}
|
|
|
|
func (e engineModule) Info() engine.ModuleInfo {
|
|
return engine.ModuleInfo{ID: EngineModuleID, Version: engine.Version}
|
|
}
|
|
|
|
func (e engineModule) Register(reg *engine.Registry) error {
|
|
for _, d := range e.m.Systems {
|
|
if err := reg.Systems().Register(d); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, oe := range e.m.engines {
|
|
id := oe.m.Info().ID
|
|
before := len(reg.Systems().IDs())
|
|
if err := oe.m.Register(reg); err != nil {
|
|
return errs.Wrap(err, errs.EngineModule, "engine module "+id+" failed to register").With(log.F("module", oe.owner))
|
|
}
|
|
for _, sid := range reg.Systems().IDs()[before:] {
|
|
if !strings.HasPrefix(sid, oe.owner+".") {
|
|
return errs.New(errs.EngineModule, "system "+sid+" of engine module "+id+" must be named "+oe.owner+".<name>", log.F("module", oe.owner))
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e engineModule) Init(eng *engine.Engine) error {
|
|
for _, oe := range e.m.engines {
|
|
if in, ok := oe.m.(engine.ModuleIniter); ok {
|
|
if err := in.Init(eng); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manifest) Engine() engine.Module {
|
|
return engineModule{m: m}
|
|
}
|
|
|
|
func (r *Registry) checkSettings() {
|
|
seen := map[string]string{}
|
|
for _, d := range r.settings {
|
|
if !strings.HasPrefix(d.ID, d.Owner+".") || !settings.ValidPath(d.ID) {
|
|
r.report(d.Owner, "setting "+d.ID+" must be named "+d.Owner+".<name>")
|
|
}
|
|
if other, dup := seen[d.ID]; dup {
|
|
r.report(d.Owner, "setting "+d.ID+" is already registered by "+other)
|
|
continue
|
|
}
|
|
seen[d.ID] = d.Owner
|
|
}
|
|
}
|
|
|
|
func (m *Manifest) NewSettings() (*settings.Store, error) {
|
|
st := settings.New()
|
|
var all []error
|
|
for _, v := range m.SettingVersions {
|
|
if err := st.OwnerVersion(v); err != nil {
|
|
all = append(all, err)
|
|
}
|
|
}
|
|
for _, d := range m.Settings {
|
|
if _, err := st.Register(d); err != nil {
|
|
all = append(all, err)
|
|
}
|
|
}
|
|
return st, errors.Join(all...)
|
|
}
|
|
|
|
func (m *Manifest) Init(ctx *InitContext) error {
|
|
ctx.provided = m.provided
|
|
if ctx.Engine != nil {
|
|
for _, s := range m.services {
|
|
ctx.Engine.SetService(s.id, s.value)
|
|
}
|
|
}
|
|
for _, e := range m.order {
|
|
if in, ok := e.mod.(Initer); ok {
|
|
ctx.owner = e.info.ID
|
|
err := in.Init(ctx)
|
|
ctx.owner = ""
|
|
if err != nil {
|
|
m.Shutdown()
|
|
if ctx.Engine != nil {
|
|
for _, s := range m.services {
|
|
ctx.Engine.MainWorld().RemoveService(s.id)
|
|
}
|
|
}
|
|
return errs.Wrap(err, errs.EngineModule, "module "+e.info.ID+" failed to initialize").With(log.F("module", e.info.ID))
|
|
}
|
|
}
|
|
m.inited = append(m.inited, e)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manifest) Shutdown() {
|
|
for i := len(m.inited) - 1; i >= 0; i-- {
|
|
if sd, ok := m.inited[i].mod.(Shutdowner); ok {
|
|
sd.Shutdown()
|
|
}
|
|
}
|
|
m.inited = nil
|
|
}
|
|
|
|
func (m *Manifest) Attach(ctx *AttachContext) (func(), error) {
|
|
var tried []WorldAttacher
|
|
done := false
|
|
detach := func() {
|
|
if done {
|
|
return
|
|
}
|
|
done = true
|
|
for i := len(tried) - 1; i >= 0; i-- {
|
|
tried[i].DetachWorld(ctx)
|
|
}
|
|
}
|
|
for _, e := range m.order {
|
|
at, ok := e.mod.(WorldAttacher)
|
|
if !ok {
|
|
continue
|
|
}
|
|
tried = append(tried, at)
|
|
if err := at.AttachWorld(ctx); err != nil {
|
|
detach()
|
|
return nil, errs.Wrap(err, errs.EngineModule, "module "+e.info.ID+" failed to attach").With(log.F("module", e.info.ID))
|
|
}
|
|
}
|
|
return detach, nil
|
|
}
|
|
|
|
func (m *Manifest) Reconfigure(ctx *AttachContext) error {
|
|
var all []error
|
|
for _, e := range m.order {
|
|
if rc, ok := e.mod.(Reconfigurer); ok {
|
|
if err := rc.Reconfigure(ctx); err != nil {
|
|
all = append(all, errs.Wrap(err, errs.EngineModule, "module "+e.info.ID+" cannot apply the new settings").With(log.F("module", e.info.ID)))
|
|
}
|
|
}
|
|
}
|
|
return errors.Join(all...)
|
|
}
|