Plugins: loader, plugin API v1 and the demo plugin (#113)

admin committed 2026-09-17 19:00:04 +02:00
1 parent 54db866b93
commit 0576bbaccb
6 files changed
+85 -7

No files matched your search

+2
@@ -16,6 +16,8 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
| GET | `sounds` | `[{file,name,bytes,updatedAt}]` — what the sound picker offers |
| POST | `sounds&name=jump.wav` | **raw sound bytes** → `{file,name,bytes}` (201) |
| DELETE | `sounds/{file}` | 204 |
| GET | `plugins` | `[{file,bytes,updatedAt}]` — every `.js` in `data/plugins/`, sorted by file name |
| GET | `plugins/{file}` | the module source as `text/javascript` (`nosniff`, `no-cache`); 404 for anything but a bare file name there — see [Plugins](Plugins) |
Every project document carries `formatVersion` (`PROJECT_FORMAT_VERSION` in `core/model/Project.ts`,
currently 2). There are no load-time migrations: `serialize.ts` reads what it recognises and drops
+6 -4
@@ -88,7 +88,8 @@ src/editor/ App → Editor (layout + ticker), PlaySession (the run),
fileImport (drop/paste) → assetImport (one upload pipeline, an ImportKind per sort) ← textureImport / fontImport / soundImport,
FilePicker (the shared "+ → Texture / Font / Sound" dialog; ImagePicker/FontPicker/SoundPicker only describe their row),
framing (fit/marquee/snap maths), FormDialog (the small modal forms: label column, Cancel + confirm, one open at a time), AssetTabEditor (the shared shell of the asset tabs),
TabStrip + tabRow (the tab row and what it does when the tabs outgrow it)
TabStrip + tabRow (the tab row and what it does when the tabs outgrow it; plugin tabs are appended at run time)
src/editor/plugins/ startPlugins (list, import, host) → loader (order, checks, what became of each file), pluginApi (API v1), NodeBoard (api.ui)
src/editor/viewport/ WorldViewport base → FreeViewport (a camera of its own: fit, pan, wheel zoom) → SceneViewport, PreviewCanvas;
GameViewport follows the game's camera instead — editing tools, hence not in src/viewport/
src/editor/inspector/ Inspector panel, host (what a section builds into), field factory, previewHandlers (scrub → preview → command), valueSpecs
@@ -96,7 +97,7 @@ src/editor/inspector/ Inspector panel, host (what a section builds into), field
one section per selection kind (asset/object/node/frame), variableRows (one row shape for
every variable), portSection, aspectAction, objectQueries/vectorRows/parentRow
src/editor/panels/ AssetPanel, assetMenu (the two groups of the + menu), HierarchyPanel, Toolbar
src/editor/graph/ GraphEditor (header + picker), GraphCanvas, EdgeLayer (curves and fold marks), graphMenus,
src/editor/graph/ GraphEditor (header + picker), GraphCanvas, graphView (zoom limits, grid, wheel zoom – shared with NodeBoard), EdgeLayer (curves and fold marks), graphMenus,
NodeView, NodePalette + paletteEntries (which also ranks a search), folding (hidden links, collapsed nodes), Breadcrumb + trail (the path into nested behaviors), beads (the run's pulses),
AnnotationView + annotationDrag (groups and comments: what a drag carries, each thing moved once)
src/editor/container/ ContainerEditor (header + picker), ContainerCanvas (a SceneViewport on a template)
@@ -110,8 +111,9 @@ src/editor/particles/ ParticlesEditor (graph canvas left of a preview panel, sp
ParticlePreview (the panel: the asset's own particles, wearing its own rows)
src/editor/list/ ListEditor (a row per item), slot (which list is being edited, and how it is written back)
src/editor/console/ log (ring buffer, argument formatting, the console.* capture), ConsolePanel (the tail under the tabs)
api/ PHP backend: index.php (routing), lib/{Store,TextureStore,FontStore,SoundStore,OriginalName,Response}.php
data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ content-addressed uploads
api/ PHP backend: index.php (routing), lib/{Store,TextureStore,FontStore,SoundStore,PluginStore,OriginalName,Response}.php
data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ content-addressed uploads,
data/plugins/ editor plugins (filled from plugins/ by the build and the seeder)
tests/ Vitest unit tests tools/ seed-*, e2e (steps in tools/e2e/, rig in tools/e2e/lib/), smoke-*, lib/, assets/ (the walk demo's sheet)
```
+3 -3
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
# code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
node tools/seed-e2e.mjs && node tools/e2e.mjs # 948 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 963 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
@@ -64,7 +64,7 @@ The e2e run is a list of steps in `tools/e2e/` (hierarchy, inspector, play, asse
graph, parenting, colliders, containers, text/enabled, menus, textures, frames, overflow, aspect,
container tab, run-in-editor, pause, nested behaviors, key repeat, list tab, struct variable,
variable nodes, property nodes, animations, state machines, console, shaders, transforms, fonts,
particles, shared scope, render on change, sounds, load scene, graph groups, nine-slice) that build on each other's
particles, shared scope, render on change, sounds, load scene, graph groups, nine-slice, plugins) that build on each other's
scene state and share the CDP driver in `tools/lib/harness.mjs` — `press`, `undo`, `play`,
`object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it
exercises.
@@ -232,4 +232,4 @@ widths to find click points. The **render device is not in this repo** — `/dev
and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which
applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets
WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`,
WebGL 2.0, and the same 948 checks as here.
WebGL 2.0, and the same 963 checks as here.
+1
@@ -31,6 +31,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
| [Architecture](Architecture) | The stack, the source layout, the rules that keep it organised |
| [Development](Development) | Commands, the GPU requirement, the e2e suite, the three example games, continuous integration |
| [API](API) | The PHP JSON API and the project document |
| [Plugins](Plugins) | Extending the editor: the plugin file, how it is loaded, API v1, the demo plugin |
## Ideas, gaps and bugs
+72
@@ -0,0 +1,72 @@
A **plugin** extends the editor. It is one JavaScript file – an ES module – in `data/plugins/` on the
server, and the editor loads every file there when it starts. v1 is deliberately small: it offers what
the demo plugin needs (read the assets, add a tab, show nodes on a board) and nothing it does not.
**Where plugins come from.** Every `.js` file in `data/plugins/` is active; there is no switch yet
(#114) and no upload (#116) – a file is put there by hand. The plugins that ship with the repository
live in `plugins/` and are copied to `data/plugins/` by `npm run build` and by `tools/seed-e2e.mjs`;
a file of the same name is replaced, anything else in the folder is left alone. `data/plugins/` is
not in the repository. The setting is **server-wide**: every project on the server sees the same
plugins. Plugins run **in the editor only**, never in a running game or an export.
## The format
```js
export const meta = { id: 'demo', name: 'Demo plugin', version: '1.0.0', api: 1 };
export function activate(api) {
// register what the plugin adds
}
```
| Export | Meaning |
|---|---|
| `meta.id` | Required, unique among the active plugins. A second plugin with the same id is not activated. |
| `meta.name`, `meta.version` | Shown in reports; the name falls back to the id. |
| `meta.api` | The plugin API version the plugin was written for. Anything but `1` is not activated. |
| `activate(api)` | Required. Called once at start-up; may be `async`, and the next plugin waits for it. |
| `deactivate()` | Optional. Nothing calls it yet – it is for switching a plugin off (#114). |
**A plugin imports nothing.** Everything comes in through `api`. The editor is one WebGL canvas
with one Pixi instance: a plugin that brought its own Pixi would draw into a second renderer. For
the same reason `api.tabs.add` accepts only widgets from `api.ui`.
## Loading
The editor asks `GET ?r=plugins` for the list, then imports each file through
`GET ?r=plugins/{file}` (see [API](API)) – **in file-name order**, one after the other, after the
project has loaded. Nothing a plugin does wrong stops the editor or the other plugins. Each of these
leaves the plugin inactive, with a line in the console and a notice in the toolbar:
- the file does not load (a syntax error, a failed request),
- it exports no `meta` or no `activate`, or `meta.id` is empty,
- `meta.api` is not a version this editor offers,
- a plugin with the same `meta.id` is active already,
- `activate` throws (or its promise rejects).
What `activate` registered before it threw stays registered.
## API v1
| Member | What it does |
|---|---|
| `api.version` | `1` |
| `api.project.assets()` | The assets of the open project in list order, as `{ id, name, type }` – copies, so writing to them changes nothing. |
| `api.project.on('change', fn)` | Calls `fn` whenever the asset list changes: an asset created, renamed or deleted, by hand, by undo or by loading another project. A change that leaves ids, names and types alone (moving an object, editing a property) does not call it. Returns a function that unsubscribes. A listener that throws is reported and stays subscribed. Any event but `'change'` throws. |
| `api.tabs.add({ id, label, create })` | Adds a tab after the built-in ones. `create()` is called once and must return a widget from `api.ui`. Returns `false`, with a console warning, when the id is taken – by a built-in tab (`scene`, `graph`, …) or a plugin that came earlier; `create` is not called then. A missing `id` or `label`, or a `create` that is not a function, throws. |
| `new api.ui.NodeBoard()` | A read-only node graph: `setNodes([{ id, title, x, y }])` replaces what it shows, `fit()` frames it. It has a **Fit** button and a node count above the canvas; a drag with any button pans, the wheel zooms about the pointer. The nodes are drawn by the graph editor's own node view, empty but for their title – no ports, no editing, no selection. The first `setNodes` with anything in it frames the nodes once the tab has a size; after that the view stays where the user put it. A title that is not text is empty, a position that is not a number is 0, and something that is not a list throws. |
## The demo plugin
`plugins/demo-plugin.js` adds the tab **Assets**: one node per asset, titled with its name, four to
a row in the order of the asset list. It subscribes to `change` and hands the board the new list, so
creating, renaming or deleting an asset shows at once. e2e step 38 checks exactly that.
## Limits
- **A plugin runs with every right the editor has** – it is code on the page, and the `api` object
is a convenience, not a boundary: nothing stops a plugin from calling the PHP API itself. Accepted
for v1, open in #115.
- No writing to the project, no node types of its own, no asset types, no inspector rows, no hooks
into a running game. Those come when a plugin needs them.
- No switch to turn a plugin off (#114), no upload (#116).
+1
@@ -18,6 +18,7 @@
**Working on the code**
- [Architecture](Architecture)
- [Plugins](Plugins)
- [Development](Development)
- [API](API)
- [Issues](https://git.nockal.com/meleki/game-maker/issues)