Files

286 lines
6.3 KiB
Go

package module
import (
"aego/engine/settings"
"aego/format/aetext"
"io/fs"
"reflect"
"regexp"
"strings"
"aego/core/errs"
"aego/core/log"
"aego/engine"
"aego/scene"
"aego/schema"
"aego/script/bind"
)
var moduleIDPattern = regexp.MustCompile(`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$`)
type entry struct {
mod Module
info Info
}
type ownedSystem struct {
desc engine.SystemDesc
owner *entry
}
type ownedEngine struct {
m engine.Module
owner string
}
type serviceDesc struct {
id string
value any
owner string
}
type finalizer struct {
owner string
fn func(*Registry) error
}
type DocSource struct {
Owner string
FS fs.FS
Dir string
}
type Registry struct {
types *scene.Types
script *bind.Registry
modules []*entry
cur *entry
comps []*schema.TypeDescriptor
systems []ownedSystem
events []ownedEvent
engines []ownedEngine
services []serviceDesc
templates []*schema.TemplateDesc
sections []*Section
icons []IconSet
docs []DocSource
provided []provision
settings []settings.Desc
saves []OwnedSection
versions []settings.OwnerVersion
owners map[string]string
dev []Hook
editor []Hook
finals []finalizer
warns []string
errs []error
built bool
audit *IconReport
datas []*schema.TypeDescriptor
validators []ownedValidator
dataHooks []DataHook
live []liveDecl
}
func New(types *scene.Types, script *bind.Registry) *Registry {
return &Registry{types: types, script: script, owners: map[string]string{}}
}
func (r *Registry) Types() *scene.Types {
return r.types
}
func (r *Registry) Schema() *schema.Registry {
return r.types.Schema()
}
func (r *Registry) Bindings() *bind.Registry {
return r.script
}
func (r *Registry) Components() []*schema.TypeDescriptor {
return r.comps
}
func (r *Registry) Owner() string {
if r.cur == nil {
return ""
}
return r.cur.info.ID
}
func (r *Registry) builtin() bool {
return r.cur != nil && r.cur.info.Builtin
}
func (r *Registry) fail(msg string) error {
err := errs.New(errs.EngineModule, msg, log.F("module", r.Owner()))
r.errs = append(r.errs, err)
return err
}
func (r *Registry) wrap(err error, msg string) error {
var w error = errs.Wrap(err, errs.EngineModule, msg).With(log.F("module", r.Owner()))
r.errs = append(r.errs, w)
return w
}
func (r *Registry) Warn(owner, msg string) {
r.warns = append(r.warns, "module "+owner+": "+msg)
}
func (r *Registry) report(owner, msg string) {
r.errs = append(r.errs, errs.New(errs.EngineModule, msg, log.F("module", owner)))
}
func (r *Registry) Add(m Module) error {
if r.built {
return errs.New(errs.EngineModule, "module registry is already built")
}
info := m.Info()
if info.ID == "" {
return errs.New(errs.EngineModule, "module needs an id")
}
if !moduleIDPattern.MatchString(info.ID) {
return errs.New(errs.EngineModule, "module id "+info.ID+" must be lowercase segments separated by dots", log.F("module", info.ID))
}
if !info.Builtin && (info.ID == "engine" || strings.HasPrefix(info.ID, "engine.")) {
return errs.New(errs.EngineModule, "module id "+info.ID+" is reserved for the engine", log.F("module", info.ID))
}
for _, e := range r.modules {
if e.info.ID == info.ID {
return errs.New(errs.EngineModule, "module "+info.ID+" is registered twice", log.F("module", info.ID))
}
}
r.modules = append(r.modules, &entry{mod: m, info: info})
return nil
}
func (r *Registry) System(d engine.SystemDesc) {
if r.cur == nil {
r.fail("system " + d.ID + " is registered outside a module")
return
}
r.systems = append(r.systems, ownedSystem{desc: d, owner: r.cur})
}
func (r *Registry) Use(m engine.Module) {
switch {
case m == nil:
r.fail("engine module is nil")
case r.cur == nil:
r.fail("engine module " + m.Info().ID + " is used outside a module")
default:
r.engines = append(r.engines, ownedEngine{m: m, owner: r.cur.info.ID})
}
}
func (r *Registry) Service(id string, v any) {
if id == "" || v == nil {
r.fail("service needs an id and a value")
return
}
r.services = append(r.services, serviceDesc{id: id, value: v, owner: r.Owner()})
}
func (r *Registry) ScriptModule(path string) *bind.Module {
if r.script == nil {
r.fail("script module " + path + " needs script bindings")
return bind.New().Module(path)
}
if _, ok := r.owners[path]; !ok && r.cur != nil {
r.owners[path] = r.cur.info.ID
}
return r.script.Module(path)
}
func (r *Registry) Template(t schema.TemplateDesc) {
r.addTemplate(&t)
}
func (r *Registry) addTemplate(t *schema.TemplateDesc) {
t.Owner = r.Owner()
t.Builtin = r.builtin()
r.templates = append(r.templates, t)
}
func (r *Registry) Icons(fsys fs.FS, prefix string) {
if fsys == nil {
r.fail("icon source is nil")
return
}
r.icons = append(r.icons, IconSet{Owner: r.Owner(), FS: fsys, Prefix: prefix})
}
func (r *Registry) Docs(fsys fs.FS) {
if fsys == nil {
r.fail("docs source is nil")
return
}
r.docs = append(r.docs, DocSource{Owner: r.Owner(), FS: fsys, Dir: r.docsDir()})
}
func (r *Registry) docsDir() string {
if r.cur == nil {
return ""
}
m := r.cur.mod
for {
u, ok := m.(interface{ Unwrap() Module })
if !ok {
break
}
m = u.Unwrap()
}
t := reflect.TypeOf(m)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
dir, ok := strings.CutPrefix(t.PkgPath(), "aego/")
if !ok {
return ""
}
return dir + "/res/docs"
}
func (r *Registry) Dev(fn any) {
if fn == nil {
r.fail("dev hook is nil")
return
}
r.dev = append(r.dev, Hook{Owner: r.Owner(), Fn: fn})
}
func (r *Registry) Editor(fn any) {
if fn == nil {
r.fail("editor hook is nil")
return
}
r.editor = append(r.editor, Hook{Owner: r.Owner(), Fn: fn})
}
func (r *Registry) Finalize(fn func(*Registry) error) {
if fn == nil {
r.fail("finalizer is nil")
return
}
r.finals = append(r.finals, finalizer{owner: r.Owner(), fn: fn})
}
func (r *Registry) Setting(d settings.Desc) {
if r.cur == nil {
r.fail("setting " + d.ID + " is registered outside a module")
return
}
d.Owner = r.Owner()
r.settings = append(r.settings, d)
}
func (r *Registry) SettingsVersion(v int, migrate func(n *aetext.Node, from, to int) error) {
if r.cur == nil {
r.fail("settings version is set outside a module")
return
}
r.versions = append(r.versions, settings.OwnerVersion{Owner: r.Owner(), Version: v, Migrate: migrate})
}