Files
aego/docs/extensions.md
T

11 KiB
Raw Permalink Blame History

Kernel, modules and extension points

What belongs to the kernel, what belongs to a module, and what depcheck reports when the line is crossed. The rules are in ARCHITECTURE §4; the only place to waive one is tools/depcheck/exceptions.txt (D212).

What is kernel

The kernel (schema, scene, render, asset, engine, script, ui, project, contracts) and the editor core know concepts every module can use, never a particular module:

Kernel concept Examples
Frame phases and scheduling PhasePhysics, PhasePostPhysics, SystemDesc.Before/After inside one module
Scene data presentation pose (SetPresentationLocal), component defaults, OnAdd
Script host host structs, component methods, script events (module.Registry.ScriptEvent), bind.Ctx
Contracts contracts/scriptevents, contracts/physics2d: interfaces between modules and scripts
Editor core documents, history, selection, assetops, fuzzy, gizmos, vectors, play, services (Frame and the panel services)
Editor extension points editor/ext: SettingsPage, Problems, Profiler, GameOverlay, Vectors, Overlay, ext/inspect custom inspectors

What is a violation

Code in the kernel or the editor that… depcheck rule Do instead
imports modules/X kernel-imports-module, editor-imports-module register through module.Registry from the module; compose in app (the catalog)
imports another module from modules/X module-imports-module depend through a contract in contracts/, declare Info.Requires
names a module's system in an ordering constraint caught by Build (unknown dependency) order by phases; keep Before/After inside the module (D204)
writes a module ID, <ID>.*, a module component name or a module diagnostic code as a literal module-literal let the module pass the string through its registration (category, page, section)
imports one panel from another panel-imports-panel move the shared code to editor/core or editor/ext
imports a panel from editor/ext or editor/core editor-ext-imports-panel, editor-core-imports-panel move the type to editor/ext
makes modules/X import modules/X/editor module-imports-own-editor the editor side imports the module, never the reverse
lets a runtime package reach dev-only code runtime-reaches-dev put the dev part behind !aego_runtime or in a dev package
lets the playground reach native or pipeline code wasm-reaches-forbidden keep it out of the playground profile
holds or receives *context.Editor in editor/panels/* tests/modulecheck/panels_test.go take the services the panel needs in its Deps (D216)

Proof with a fake module

A rule holds when a test module that is not the real one can use the same extension point. tests/modulecheck/fakephys registers a project section, inspector hints, a script event, component hooks, a settings page, a panel, a custom inspector, a gizmo, bounds, overlays, a vector layer and a profiler section without importing modules/physics; the guard test there checks that the kernel and the editor do not import or name any module.

Writing a module

A project module is written against aego/sdk only. examples/native-settings is a complete one: a project section, a component with a validator and a game-view overlay. tests/modulecheck/fakephys uses every extension point and runs in the editor in place of physics; it is the working reference for the full list. The engine-side names behind each facade are in D205–D211.

Lifecycle

Register → schema freeze → Init(ctx) → AttachWorld(ctx) / DetachWorld(ctx) per edit or play world → Shutdown(). Shutdown runs in reverse Init order; a failing Init shuts down the modules initialized before it. DetachWorld is called for every module whose AttachWorld was called, including a failing one, so it must tolerate a partial attach. Per-world state lives in world services, not in the module struct (D206).

Project settings sections

A module owns one block of project.aego, named after the module ID or <id>.<name>:

sdk.ProjectSection(r, sdk.SectionDesc[Settings]{
    ID:       "weather",
    Title:    "Weather",
    Order:    400,
    Default:  func() Settings { return Settings{Strength: 1} },
    Validate: func(s *Settings) error { ... },
})
  • With Decode/Encode left empty and the type registered with sdk.RegisterStruct, the schema codec reads the block and Project Settings draws a page from the fields.
  • Version and Migrate(node, from, to) upgrade old blocks; migration works on a clone, the file is rewritten only on save.
  • Former lists old block heads (render2d reads render {}).
  • Names(v, list) returns named lists for inspector hints (below).
  • sdk.Section[T](ctx, id) gives the current value. Project Settings edits a draft and replaces the value after project.aego is written, so a module reads the section when it needs it instead of caching the pointer from Init.

Blocks without a registered section stay in the file untouched. aego check validates the sections of the built-in modules; sections of native modules are checked when the project runs.

Inspector hints

Schema options on the component registration, never code in the Inspector:

Option Effect
sdk.ChoicesFrom(field, section, list) integer field picked from a named list of a section (Names)
sdk.Bitset(field, section, list) integer field edited as a mask over that list; all listed bits set is stored as -1
sdk.Runtime(fields...) shown only during Play, read-only

Component groups in the Inspector follow module order from the manifest, then registration order (D208).

Component hooks

sdk.Component[Wind](r, sdk.Name("weather.Wind")).
    OnAdd(func(ctx sdk.AddContext, w *Wind) { ... }).
    OnRemoving(func(ctx sdk.AddContext, w *Wind) { ... }).
    OnChanged(func(cs *sdk.ChangeSet) { ... }).
    Validate(func(ctx sdk.ValidateContext, w *Wind) []sdk.Diagnostic { ... })
  • OnRemoving gets the old value before the store frees it (editor remove and undo of add).
  • OnChanged runs after every committed transaction, Undo and Redo with the deduplicated ChangeSet; add and remove of one component in a batch cancel out.
  • Validate reports problems of one component; the editor runs it over open edit scenes.
  • Hooks make no structural changes outside pending. Runtime code keeps comparing values itself (D185, D209).

Diagnostics

engine/problems collects sdk.Diagnostic{Owner, Code, Severity, Message, Subject, BlocksPlay} from sources with a revision; the service rebuilds the list only when a revision changes. sdk.Diagnostics(ctx) in Init returns the service; a module adds runtime diagnostics as a sdk.ProblemSource from AttachWorld and removes it in DetachWorld. The Problems panel shows them with the owner as category; an error with BlocksPlay stops Play (D210).

Resources

Module icons are named <owner>.* or cat.<owner>; a clash with the engine atlas or another module is a build error. Built-in modules list their icon sources through IconDir(). Docs registered with Registry.Docs carry the module as owner and are left out of runtime builds (D211).

Profiling

Every system already shows up in the Profiler flame graph and in exported traces under its system ID. For finer zones inside a system, register a zone once in Init and open it around the work:

var gust sdk.TraceZone

func (m *Weather) Init(ctx *sdk.InitContext) error {
    gust = ctx.Engine.Trace().Zone("weather.gust")
    return nil
}

func (s *windSystem) Run(ctx *sdk.Context) {
    z := sdk.TracerOf(ctx).Begin(gust)
    s.applyGusts(ctx)
    z.End()
}

Zones nest, cost no allocations and are no-ops while tracing is off; every method is safe on a nil tracer (D215).

Editor extensions

Editor code lives in files tagged aego_editor and is attached with sdk.EditorHook. The hook runs once when the editor starts; everything it registers is removed together when one of its callbacks panics (D72).

//go:build aego_editor

func registerEditor(r *sdk.Registry) {
    sdk.EditorHook(r, func(e *sdk.EditorRegistry) error {
        ed := e.Editor()
        if err := e.Panel(sdk.PanelDesc{Panel: &questPanel{ed: ed}, Near: sdk.PanelInspector}); err != nil {
            return err
        }
        return e.Command(sdk.CommandDesc{ID: "game.heal_all", Name: "Heal All", Run: func() error { return heal(ed) }})
    })
}

type questPanel struct{ ed *sdk.Editor }

func (*questPanel) ID() string    { return "game.quest" }
func (*questPanel) Title() string { return "Quest" }

func (q *questPanel) Draw(f *sdk.Frame, r mathx.Rect) {
    ui := f.UI
    if q.ed.Doc() == nil {
        widgets.LabelDim(ui, "No scene open")
        return
    }
    ...
}
Extension point Registration
Panel Panel(sdk.PanelDesc{Panel, Slot, Near, Order, Hidden}); draws with Draw(*sdk.Frame, r)
Custom inspector Inspector("game.Health", sdk.InspectorFunc(fn)); fn gets the editor, the selected nodes and the default field drawer
Command Command(sdk.CommandDesc{...}); appears in its category menu and in Search Everywhere
Gizmo, bounds Gizmo(type, g), Bounds(type, fn) for picking and framing
Overlays Overlay(fn) in the Scene View, GameOverlay(fn) also in the Game View
Settings page, profiler section, vector layer SettingsPage, Profiler, Vectors

A panel gets a fresh Frame every draw (UI, drag and drop, time, focus). Anything else it needs it captures at registration through e.Editor(). Built-in panels follow a stricter rule and never see the editor (D216). PanelDesc.Slot puts a panel into a column of the default layout; Near opens it next to a panel on demand; without Order extension panels follow the built-ins in the Window menu (D217).

Try it scenes

A module can give any of its components a live example on the documentation site: put a scene in res/docs/try/<Component>.aescene, named after the last part of the component ID (try/Body2D.aescene for physics.Body2D). The page of that component gets a Try it block that runs the scene in the browser with the Inspector, selecting the first node that has the component; a guide page can show a scene with try: try/name.aescene and try_select: module.Component in its front matter.

Try scenes run on the sandbox catalog (core, input, render2d, scripting, physics, debug) with builtin assets only and no scripts. aego docs check loads each one, runs it for 60 frames and fails on a load error, a warning or a fault, and on a scene of a module outside the sandbox. Keep the scene to what the component needs, put the documented node first, and add a camera.