Plugins: loader, plugin API v1 and the demo plugin (#113)
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)
|
||||
Reference in new issue
Block a user