Files
aego/docs/extensions.md

173 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`, `contracts/camera2d`: 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) |
A module can tune another module's behaviour without importing it by providing a value under a contract kind: `r.Provide(camera2d.AccessibilityKind, camera2d.Accessibility{ScreenShake: "...", ReducedMotion: "..."})` hands the camera module the IDs of settings it reads every frame. The consumer reads `ctx.Provided(kind)` in `Init`.
## 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>`:
```go
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
```go
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:
```go
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
//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.