# Plugins

A plugin adds a feature to a project that not every project needs: DIS networking, an instructor console,
scorecards. A project that does not list a plugin does not load it and pays nothing for it. Some plugins ship with
the engine; others are installed by the launcher from the update source, or from a file on a site with no network
([Installing a plugin](#installing-a-plugin)). Why it works this way: [PROPOSAL-PLUGINS.md](PROPOSAL-PLUGINS.md).

In the commands, `argus` is the engine's command and `@argus` the scope of its TypeScript packages.

## Contents

1. [Adding a plugin](#adding-a-plugin)
2. [Installing a plugin](#installing-a-plugin)
3. [What a plugin may do](#what-a-plugin-may-do)
4. [Settings](#settings)
5. [Removing a plugin](#removing-a-plugin)
6. [The project file](#the-project-file)
7. [Using a plugin in a scene](#using-a-plugin-in-a-scene)
8. [Writing a plugin](#writing-a-plugin)
9. [Publishing a plugin](#publishing-a-plugin)
10. [The descriptor](#the-descriptor)
11. [Problems and their codes](#problems-and-their-codes)

## Adding a plugin

From a terminal in the project's folder (or any folder inside it):

```
argus plugin list              # what is installed, and what this project already uses (marked *)
argus plugin info dis          # what it carries, what it may do, the settings it takes
argus plugin add dis           # add it
```

`add` prints what it is about to do, then does it:

```
argus plugin add: adds "dis" (/projects/harbour-drill)
  it may:
    net.udp (opens UDP ports: binds and sends, broadcast and multicast included): sends this simulation's entities and receives other simulations' entities as DIS Entity State PDUs
    world.spawn (spawns and removes entities from outside a behaviour): spawns, moves and removes the entities other simulations publish
  entry: {"id":"dis"}
  plugins, in load order: dis
  copies dis/remote.prefab.json
wrote dis/remote.prefab.json, argus.project.json
```

- `--dry-run` prints the same and writes nothing.
- `--set key=value` sets a setting as it adds (see [Settings](#settings)); repeat it for more.
- `--version 0.1` makes the project need that release line of the plugin.
- `--project <dir>` names the project instead of the current folder; `--json` prints one JSON object instead.
- Some plugins copy starter files in (the Training plugin adds a sample scorecard). A file that already exists is
  kept, never overwritten; the output says `keeps <file>`.

**In the editor:** Project Settings > Plugins lists the installed plugins. **Add** shows what the plugin may do
and adds it when you confirm; **Remove** takes it out.

**From an agent:** the `plugin` tool (in the `release` toolset; turn it on with `tools.enable`):
`{"op": "list"}`, `{"op": "info", "id": "dis"}`, `{"op": "add", "id": "dis", "set": ["port=3001"]}`,
`{"op": "remove", "id": "dis"}`. An add answers with the same plan the CLI prints, capabilities included, and
`"dryRun": true` stops there. A `set` value is JSON when it parses, so a group goes whole:
`"origin={\"lat\": 46.80, \"lon\": 8.23}"`.

All three do exactly the same thing: they run the same code. While the editor has the project open, add and
remove plugins in Project Settings or through the agent (both go through the editor's workspace service, so the
change becomes a revision); the command line writes the file directly.

## Installing a plugin

A plugin the engine does not ship is installed once per machine by the launcher, then added to each project that
uses it. In the commands, `argus-hub` is the launcher's command.

```
argus-hub --plugins                       # what is installed here, and what the update source offers
argus-hub --install-plugin radio          # the newest release
argus-hub --install-plugin radio@1.2      # the newest 1.2.x (also: radio@1, radio@1.2.0)
argus-hub --uninstall-plugin radio@1.2.0  # one version; `radio` alone removes every version
argus plugin add radio                    # then, in the project's folder
```

- **Where it goes:** `plugins/<id>/<version>/` under the launcher's folder (`%LOCALAPPDATA%\Argus` on Windows,
  `~/.local/share/argus` elsewhere; `ARGUS_HOME` names another). One folder per version, never changed after it is
  installed. Several versions of a plugin sit side by side.
- **Which version a project gets:** the newest installed one, unless its entry names a `version`
  ([The project file](#the-project-file)); then the newest installed version that meets it. `argus plugin list`
  marks an installed plugin and names its other installed versions.
- **Checked before it lands:** the launcher verifies the publisher's signature on the plugin's manifest before it
  fetches anything, and every file's SHA-256 as it arrives. A plugin that does not verify is not installed. The same
  sign-in that downloads engines downloads plugins.
- **Access:** a plugin in the update source is closed until Cadarn gives it to a person, to an organization (every
  member) or to everyone. `argus-hub --plugins` marks one your account was not given as `(needs access: ...)`
  (`"access": false` with `--json`), and installing it is refused with that reason; your sign-in is fine. A plugin
  already installed stays. The rules are in `services/cloud/PLUGIN-ACCESS.md`.
- **No network:** on a connected machine take the plugin's `.argus-pack` file; on the site,
  `argus-hub --import-pack radio-1.2.0.argus-pack` installs it with the same checks and no connection.
- **The engine's own plugins stay the engine's.** An installed plugin with the id of one the engine ships is not
  used (`plugin list` says so): a shipped plugin's compiled-in part belongs to that engine build.
- **Code of its own:** an installed plugin carries behaviours, UI screens, a web app, agent tools, schemas and
  data, and code that runs at the engine's hook points as a
  [WebAssembly part](#a-webassembly-part). A compiled-in part cannot be installed: it is built into the engine.

## What a plugin may do

Every plugin declares what it may do beyond what any behaviour can, each with a reason, from this list:

| Capability | Allows |
|---|---|
| `net.udp` | opening UDP ports: binding and sending, broadcast and multicast included |
| `net.http.serve` | serving pages and an API under `/plugins/<id>/` on the engine's local query server, reachable from this machine only |
| `net.out` | outbound network connections, to the hosts it names |
| `session.control` | pausing, stepping and scaling time, injecting events, taking over entities |
| `world.spawn` | spawning and removing entities from outside a behaviour |
| `fs.project.write` | writing files in the project folders it names |
| `fs.project.read` | reading files in the project folders it names |
| `render.draw` | drawing meshes and textures of its own every frame |
| `render.camera` | taking over the camera, and leaving the scene's own meshes out of the frame |
| `input.pointer` | reading the mouse wheel, the pointer's movement and its buttons |

They are shown before anything is written, by `plugin add`, `plugin info`, the editor's confirmation and the agent
tool, and the engine holds a plugin to them when it runs.

**Your own server.** A plugin reaches only the hosts its `net.out` names. A project can let it reach more, for data
it serves itself: `"allowHosts": ["tiles.example.org"]` in the plugin's entry in the project file. It widens only a
plugin that declares `net.out`. `argus plugin add <id> --allow-host tiles.example.org` writes it (repeatable; the
agent's `plugin` tool takes `"allowHosts": [...]` on an add).

**Keys.** A service that needs a key gets it from this machine, never from a project or a plugin:

```sh
argus plugin key set <plugin> <host> --file <a file holding the key>
argus plugin key list
argus plugin key remove <plugin> <host>
```

- The key is kept in the launcher's folder (`keys/<plugin>/<host>`), readable by you only.
- It is sent to that host alone, on the requests of that plugin that ask for it. The plugin's code never sees it,
  and it is never written into a log, a status or an error.

## Settings

A plugin's settings live in the project file, under its entry's `config`. `argus plugin info <id>` lists them with
their defaults. Set them when adding, or later by adding again:

```
argus plugin add dis --set port=3001 --set origin.lat=46.80 --set origin.lon=8.23
argus plugin add dis --set address=192.168.1.255      # already listed: this only changes the setting
argus plugin add dis --set port=                      # an empty value goes back to the default
```

A value is read as JSON when it is JSON (`3001`, `true`, `[1, 2]`, `"text"`) and as text otherwise. Dotted names
reach inside a group (`origin.lat`). Every setting is checked against the plugin's own schema before the file is
written; a wrong one is refused with `plugin-config`, naming the setting and what it must be.

## Removing a plugin

```
argus plugin remove dis --strip
```

`--strip` also takes the components that attach the plugin's behaviours out of the project's scenes and prefabs, so
the project still runs without it: only those components change in each file, its formatting kept. Without
`--strip` the components stay, the output names them entity by entity, and those scenes will not load until the
plugin is added again or the components go (`remove <id> --strip` takes them out later too, once the plugin is no
longer listed). `--dry-run` says what it would do and writes nothing. Files the plugin copied in stay in the
project, and when the project had settings for it, the output prints the `plugin add` that puts it back as it was.
An agent takes components out with its scene tools; the editor's Inspector removes a component. A plugin another listed plugin needs cannot be removed until that one is
(`plugin-requires`).

## The project file

`plugin add` writes this into `argus.project.json`; you can also write it by hand:

```json
"plugins": [
  { "id": "dis", "config": { "port": 3001, "origin": { "lat": 46.80, "lon": 8.23 } } },
  { "id": "training" }
]
```

| Field | Meaning |
|---|---|
| `id` | the plugin, as `plugin list` shows it |
| `version` | optional: `MAJOR`, `MAJOR.MINOR` or `MAJOR.MINOR.PATCH`; the installed plugin must match the parts given. Absent: the installed one |
| `config` | optional: the plugin's settings |
| `allowHosts` | optional: hosts this project lets the plugin reach beside its own (host names: `tiles.example.org`) |

The list's order is the order plugins load and run in. An engine from before plugins refuses a project file that
has the field, as it refuses any field it does not know.

## Using a plugin in a scene

- **Its behaviours:** `{"type": "script", "module": "dis:entity", "props": {...}}`. The part before the colon is the
  plugin; a file of your own never hides a plugin's behaviour. `plugin info <id>` lists them. To attach one without
  editing the scene file: `argus plugin attach dis:entity --entity car --props '{"marking": "CAR7"}'` (the startup
  scene unless `--scene <file>`; only that component is written into the file; `--dry-run` shows it). The editor's
  Inspector and an agent's scene tools attach them too, and an entity may carry several `script` components.
- **Its TypeScript** (a plugin that ships a package; `plugin info` says "behaviours import it as ..."):
  `import { ... } from "@argus/training"` in a behaviour, once the project lists `training`.
- **Its page** (a plugin with a web app): `http://127.0.0.1:<query port>/plugins/<id>/` while the game runs;
  `GET /plugins` lists every running plugin's page.
- **Its compiled-in part's status:** `GET /plugins/<id>/status` while the game runs (refreshed every 30 ticks).
- **Its agent tools:** through the agent's `plugin` tool, `{"op": "tools", "id": "<id>"}` and `{"op": "run", "id": "<id>.<tool>"}`.
- **Its scripts** (command-line tools of its own; `argus plugin info <id>` names them):
  `argus plugin run training score runs/drive.replay --scorecard training/crossing-drive.scorecard.json`. The script runs on
  the Node the engine's tools use (an installed engine's own; `node` on PATH in a checkout; `ARGUS_NODE` names
  another) from the current folder, with every argument after its name, and the command exits with the script's
  code. In a checkout the plugin is built first when it needs to be
  (below). Options for `plugin run` itself, such as `--project`, go before the plugin's id.

**A plugin's guide.** A `README.md` at a plugin's root is its guide: `plugin info` names its path (`guide` in the
JSON form), which is where a person or an agent reads how to use the plugin. A published plugin lists it in `ship`.

The first-party plugins' own READMEs (`plugins/<id>/README.md`) say what each one does: [DIS](../plugins/dis/README.md),
[Training](../plugins/training/README.md), [Motion platform](../plugins/motion/README.md).

**When a listed plugin cannot load**, `argus run` does not start the project: it names the plugin and why
(`plugin-unknown`, `plugin-version`, `plugin-config`, `plugin-native`, ...). A training scenario that runs
without the plugin it was written for is worse than one that does not start. The editor still opens such a
project, so the plugin can be fixed or removed in Project Settings; its Play leaves the plugin out and says so in
the log.

## Writing a plugin

A plugin of your own needs no repository and no build: [A plugin of your own, with no build](#a-plugin-of-your-own-with-no-build)
is the whole recipe. First-party plugins live in the engine's repository, `plugins/<id>/`, and are listed in
`plugins/registry.json`:

```
plugins/
  registry.json                { "schemaVersion": 1, "plugins": ["dis", "training", "geodata"] }
  <id>/
    plugin.json                the descriptor (below)
    package.json, src/         the TypeScript package; its build writes dist/
    native/                    optional: the Rust crate argus-plugin-<id>
    web/  ui/  schemas/  templates/  tools.json  README.md
```

The engine finds this folder by walking up from the project (a checkout), else in the SDK it ships
(`$ARGUS_SDK/plugins`, `<exe>/sdk/plugins`); `$ARGUS_PLUGINS` names one directly. It loads the plugins of that
one folder that its `registry.json` lists, in that order; a folder it does not list is not loaded
(`plugin list` says so). Plugins shared between people and engine versions, with their own registry, come later.

A plugin with no native part needs no Rust at all: TypeScript, a web app and data are enough, and plain
JavaScript with no build is enough for behaviours.

### A plugin of your own, with no build

An installed engine's plugins are its `sdk/plugins` folder, and the engine loads one plugins folder. Copy it to a
folder of your own (an engine update then leaves your plugin alone; copy the engine's folder again after an update
to take its plugins' new versions, and keep yours), add your plugin there, list it in that folder's
`registry.json`, and point the engine at the folder:

```
my-plugins/                       a copy of <engine>/sdk/plugins
  registry.json                   { "schemaVersion": 1, "plugins": ["dis", "training", "geodata", "hello"] }
  hello/
    plugin.json
    behaviours/entries.json
    behaviours/rise.js
```

`hello/plugin.json` (the fields are [below](#the-descriptor)):

```json
{ "schemaVersion": 1, "id": "hello", "displayName": "Hello", "version": "0.1.0", "apiVersion": 1,
  "description": "Makes an entity rise.", "platforms": ["windows", "linux", "macos"],
  "behaviours": "behaviours/entries.json" }
```

`hello/behaviours/entries.json` names each behaviour and its file, relative to itself:

```json
{ "version": 1, "modules": { "rise": { "file": "rise.js", "summary": "Moves the entity up 2 m a second." } } }
```

`hello/behaviours/rise.js` is a behaviour like any other, an ES module whose default export has the hooks
([API.md](API.md) is the whole `ctx`; a first one needs little more than `ctx.state` and `ctx.props` (below),
`ctx.log(message)`, the `ctx.entity.position` property and `ctx.entity.translate([x, y, z])`,
`ctx.events.emit(name, data)`, and an `onEvent(ctx, ev)` hook, whose `ev` is `{name, data}`). A plugin's behaviours are JavaScript: the engine compiles a project's own
TypeScript behaviours, not a plugin's, so a plugin written in TypeScript builds its `.js` first.

```js
export default {
  init(ctx) { ctx.state.speed = ctx.props.speed ?? 2; ctx.log("rise: start"); },
  update(ctx, dt) { ctx.entity.translate([0, ctx.state.speed * dt, 0]); },
};
```

The default export is one object that every entity with the behaviour shares, so `this` is too: keep an
entity's own values in `ctx.state` (one per entity, kept across a hot reload) and read its props from
`ctx.props` (`{"type": "script", "module": "hello:rise", "props": {"speed": 3}}`; `{}` when it has none).

Then, with `ARGUS_PLUGINS` set to `my-plugins` in the shell (or the system's environment):

```
argus plugin info hello          # checks the descriptor
argus plugin add hello
```

`plugin info hello` lists `hello:rise` under its behaviours. To try it, save this as `scenes/hello.scene.json`
in the project (a camera and one box that carries the behaviour; every `meta` field here is required):

```json
{ "version": 1,
  "meta": { "name": "hello", "genre": "third-person", "bounds": { "min": [-10, -1, -10], "max": [10, 20, 10] },
            "budgets": { "triangles": 1000, "drawCalls": 10, "textureMB": 4, "dynamicBodies": 4 },
            "environment": { "ambient": 0.4 }, "strings": {} },
  "assets": { "cube": { "kind": "glb", "path": "builtin:cube", "triangles": 12 } },
  "entities": [
    { "id": "cam", "name": "camera", "tags": [], "transform": { "position": [0, 3, 8], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1] },
      "components": [{ "type": "camera", "kind": "fixed", "fov": 60, "primary": true }] },
    { "id": "riser", "name": "riser", "tags": [], "transform": { "position": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1] },
      "components": [{ "type": "mesh", "asset": "cube" }, { "type": "script", "module": "hello:rise" }] }
  ] }
```

and run it in the project, three simulated seconds with no window, writing the box's position once a second:

```
argus run . --scene scenes/hello.scene.json --headless --seconds 3 --record-data rise.csv --data-fields riser:position --data-every 60
```

The log says `[riser] rise: start`, and `rise.csv` has the box 2 m higher each second. Run a scene in its project
like this (`argus run <project> --scene <file>`): a scene file run on its own (`argus run scenes/hello.scene.json`)
has no project, so no plugins load.

`apiVersion` is the plugin API the engine serves (`plugin info` refuses another, saying which it serves); leave
`engine` out.

**Exports.** A desktop export of a project packs the plugins it lists at `app/plugins/**` (both exporters, the
same files and bytes): `registry.json` naming exactly those plugins, and for each what a running game reads of it
(descriptor, settings schema, contribution data, behaviours, UI screens, web app); never its sources, starter
files or agent tools, and the compiled-in parts are already in the engine. The files only authoring reads (a
`files` rule's `schema`, `tools`, `onAdd.copy[].from`) are left out, and a packed plugin is not refused for their
absence; there, `GET /plugins` names no toolset. The exported game resolves them from its
own payload and refuses to start without them, as `argus run` does. The exporter builds a plugin first when it
needs to (below). A web export of a project with plugins is refused for now (`plugins-not-exported`), and a
scene-only export cannot carry plugins (export the project).

**Installed engines** carry the first-party plugins in their SDK: `scripts/sdk/pack.mjs` copies
`plugins/registry.json` and exactly what each listed descriptor names into `sdk/plugins/` (never sources,
native crates or tests).

**Building a plugin.** What a plugin ships under `dist/` (and any other output of its build) is generated, so a
fresh checkout has none of it. Both packers, the SDK pack and the desktop export, build a plugin before packing it
when its folder has a `build` script in its `package.json` and is not fresh: never built by a packer, or a file or
folder in it, or the folder itself (`node_modules` and dot entries aside), is newer than the stamp the last such build
left in `node_modules/.cache/plugin-build/`. A folder counts because deleting a file changes only its folder's time,
so a deleted `dist/` or built file is built again. They run `pnpm run build` in the plugin's folder, its output on
stderr; packs running at the same time take a lock there, so one builds and the others wait. A failed build stops
the pack and names `pnpm --filter ./plugins/<id> build` to see why; a plugin whose built files are still missing is
refused. An installed SDK's plugins have no `package.json` and are never built. While working on one, build it yourself:
`pnpm --filter ./plugins/<id> build` (the root `pnpm build` builds every plugin too).

### A native part

For what scripts cannot do (sockets, per-entity work every tick, file formats). The crate is
`plugins/<id>/native`, package `argus-plugin-<id>`, depending on `argus-plugins` alone; the engine builds it in
behind a Cargo feature (`plugin-<id>`, in the standard builds) and lists it in `crates/app/src/plugin_native.rs`.
It exports one `start(&StartContext) -> Result<Box<dyn NativePlugin>, PluginError>` and implements:

| Hook | When | May |
|---|---|---|
| `input_phase(world)` | every tick, after session commands and before behaviours and physics, held ticks too | read `world.external_input()` (what it submitted last tick, recorded in a replay; in a playback, the recording's), spawn and move its entities. Never a socket or a clock |
| `end_of_step(world)` | every tick, after physics and behaviours | read the settled state; when `world.tick().sends()` (a first run, confirmed, live), read its sockets, `submit_external` what arrived, send what goes out |
| `status()` | on request | answer a JSON summary |
| `stop()` | a restart, a scene switch, the end of the run | close sockets |

Both hooks see one `tick()` (the input phase's tick, time and dt; the sim clock, never the wall clock). What a
part submits is the world's external input under the plugin's id (`World::submit_external`), recorded in a replay
(`"xin"` lines); in a playback `tick().mode` is `Playback`, `external_input()` hands out the recording's and a
submission is dropped (one `external-input-dropped` warning), so a replay reproduces a run with no network. The
`StartContext` grants only declared capabilities: `udp_bind` needs `net.udp`; `spawn_prefab`, `set_pose` and
`despawn` need `world.spawn`. Events (`world.emit`) reach behaviours' `onEvent` at the start of the next tick. The other way, at the end of the step `world.emitted(name)` reads the newest `ctx.events.emit(name, data)` a behaviour sent to everyone that tick: how a behaviour hands a part numbers only it knows (an engine's rpm).
A part is started when the world finishes loading and stopped on a rebuild; a project that lists no plugin with a
part runs neither hook. A part whose `start` fails (a port taken) is left out of the run, loudly: a
`plugin-failed` line and event, and its status says why. Use `argus-dmath` for transcendental maths (the sim-maths rule).

### A WebAssembly part

Code that travels with the plugin. A compiled-in part is built into the engine, so only a plugin the engine ships
can have one; a WebAssembly part is a file in the plugin's folder, so an installed plugin
([Installing a plugin](#installing-a-plugin)) can carry code of its own. The engine runs it at the same two hook
points, with the same recorded external input and the same capability checks.

**The plugin API** is `crates/plugin-wasm/wit/plugin.wit` (`engine:plugin@1`). A part exports `start(config)`,
`input-phase`, `end-of-step`, `frame`, `run-job`, `status` and `stop`, and imports five interfaces:
- `host`, the simulation: `tick`, `scripted`, `tagged`, `pose-of`, `spawn-prefab`, `set-pose`, `despawn`,
  `external-input`, `submit-external`, `emit` and `log`;
- `scene`, what the scene is and where, read-only: `georeference`, `footprint` and `ground-heights`;
- `render`, drawing: `view`, `mesh-create`, `mesh-free`, `texture-create`, `texture-write`, `texture-free`,
  `material-create`, `draw`, `set-camera`, `hide-scene`, `scene-fade`, `set-far`, `set-sky`, `set-credits` and
  `pointer`;
- `jobs`, work off the engine's threads: `submit`, `take-finished`, `pending` and `workers`;
- `data`, files and the network, for jobs only: `read`, `write`, `exists`, `entries`, `fetch` and `fetch-with`.

Settings, props, statuses and event data cross as JSON text.

**Drawing** (needs `render.draw`). `frame` is called once for each frame drawn, before the frame's draw list is
built. There a part hands the renderer meshes, RGBA textures and materials (lit, or unlit for imagery that
carries its own lighting; an unlit material casts no shadow), each by a handle of its own, and calls `draw` with the whole list of what it draws from
now on: a mesh, a material, a transform in scene metres and a fade. `view` answers the camera and the frame's size.
- Visual only: nothing in `render` reaches the simulation, and `frame` is not part of a tick, so a replay or a
  headless run calls it a different number of times, or never.
- The renderer keeps a material for the whole run: make a fixed set and reuse them, writing new pixels into a
  pool of textures (`texture-write`) as the globe's imagery does.
- **The camera** (needs `render.camera`): `set-camera` takes the view for the frames from now on, with an eye, a
  target, a field of view and its own nearest and farthest distances (a part that draws a planet sets both from
  its altitude); `none` gives it back to the game. `hide-scene` leaves the scene's own meshes out of the frame, for
  a part that shows another place until it hands over. A view somebody else set (the editor's, a replay camera)
  is never taken away.
- **Handing over to the scene** (needs `render.camera`): `scene-fade` draws the scene's own meshes thinned out
  (0 whole, 1 not at all), so a part that hid the scene brings it in without a pop; `set-far` draws the game's own
  camera out to a distance the part names, for a part that draws a horizon around the scene and leaves the camera
  to the game.
- **The sky:** `set-sky` (needs `render.camera`) draws two colours behind the scene, straight up and at the
  horizon, in place of the scene's own sky, which still lights it: a part whose camera leaves the ground shows the
  black of space there. `none` gives the scene's own back.
- **Credits:** `set-credits` gives the engine a line it draws small at the bottom right of every frame from then
  on, a recording without its HUD included, wrapped to the frame's width: the sources whose licences ask to be
  named. It is drawn with the project's font (`ui/fonts/`); in a project with none the log says so once, with the
  credits.
- **The pointer** (needs `input.pointer`): `pointer` answers the wheel, the movement and the buttons since the
  part last asked.
- One part holds at most 16,384 meshes, 2,048 textures and 4,096 materials, and draws at most 65,536 things a
  frame; past that a call answers an error. What a part made is given back when the run ends.

**The scene** (no capability: it changes nothing). `scene.georeference` answers the scene's place on the Earth
(its `meta.georeference`, the defaults filled in), `scene.footprint` its rectangle on the ground (its terrain's,
else its bounds) and `scene.ground-heights` its terrain's height under a list of points. They answer the same at a
tick's hooks and at the frame hook, and nothing in a job: a job that needs the ground is handed it in its input.

**Writing one in Rust** (`fixtures/plugins/wasm-mover` is a whole plugin to copy):

```
# Cargo.toml: crate-type = ["cdylib"], wit-bindgen = "=0.57.1"
# src/lib.rs: wit_bindgen::generate!({ path: "<the folder of plugin.wit>", world: "plugin" });
#             impl Guest for Part { ... }   export!(Part);
cargo build --release --target wasm32-unknown-unknown
argus-plugin-compile component target/wasm32-unknown-unknown/release/<crate>.wasm --out plugin.wasm
```

and in `plugin.json`: `"wasm": { "component": "plugin.wasm" }`. `plugin pack` packs it like any other file the
descriptor names. A plugin has a compiled-in part or a WebAssembly part at run time, not both.

**Jobs.** `jobs.submit(kind, input)` queues a call of the part's own `run-job(kind, input)`, which runs in another
instance of the part on a worker thread and answers bytes or an error; `take-finished` hands back what finished
since the part last asked, by id. Decoding, meshing and anything else too slow for a frame goes there.
- A job sees its input and nothing else: none of the part's state, nothing of the world, and it queues no jobs.
- When a job finishes depends on the machine. Nothing the simulation computes may depend on it: jobs are for what
  is drawn.
- One call of `take-finished` hands over about 4 MB of results at most (always one job at least), and the next call
  the rest: many jobs finishing together do not all land, and upload, in one frame.
- A part gets one to eight workers, by the machine's cores; `jobs.workers` says how many, so a part that fetches
  can keep some free of jobs that wait on a server.
- A job runs on the same fuel and memory budget as any call. One that traps answers its error, and the next job
  gets a fresh instance. A part has at most 1,024 jobs queued or running, each taking and answering at most 64 MB.

**Files and the network** are for jobs only, so a frame never waits on a disk or a server and the simulation
never depends on either. From anywhere else a call answers an error.
- `read`, `write`, `exists` and `entries` take a place and a relative path that never leaves it:
  - `plugin`: the plugin's own folder, read only;
  - `project`: the project's folder, under the folders its `fs.project.read` capability names (read) and
    `fs.project.write` names (read and write);
  - `cache`: a folder of the plugin's own on this machine, kept between runs (`cache/plugin-data/<id>` under the
    launcher's folder).
- `fetch(url)` answers the body of an HTTPS GET that returns 200, from a host the plugin's `net.out` capability
  names or the project lets it reach (`allowHosts`), and no other; no redirect is followed. The standard engine has
  the network client (feature `net-tls`). It answers an error on an offline install (the `Offline` policy), and in a
  build without the client, which an export is unless it is built with the feature.
- `fetch-with(url, headers)` is `fetch` with request headers: `authorization`, `accept`, and names that start
  `x-`, nothing that changes where a request goes. `{key}` in a header's value stands for the host's stored key
  too (`Bearer {key}`), for a service that takes its key or a token in a header.
- `{key}` in the address's query stands for the key the user stored for that host (`plugin key set`): the engine
  puts it there for that request, and what the part hears if the request fails names the address as the part wrote
  it. A part never holds a key.

**What a part cannot do.**
- It imports the plugin API and nothing else: no clock, no randomness, no sockets, and no files or network but
  the `data` interface inside a job. `argus-plugin-compile` refuses a component that asks for anything more, by
  name, so build for `wasm32-unknown-unknown` (no WASI).
- It changes the world only as the plugin's capabilities allow: `spawn-prefab`, `set-pose` and `despawn` answer an
  error without `world.spawn`.
- Each call runs on a fuel budget (about two billion instructions) and the part's memory stops at 512 MB. Both are
  counted the same on every machine. A part that runs past either, or that panics, is stopped for the rest of the
  run: behaviours hear a `plugin-stopped` event `{plugin, message}`, the plugin's status says why, and the game
  goes on.
- A part whose `start` answers an error (settings it refuses) is left out of the run, as a compiled-in part is:
  a `plugin-failed` line and event, and its status says why.

**The same run every time.** The same component runs the same instructions on every platform; a NaN it computes
is the canonical one; it reads nothing from outside the simulation but its recorded external input. Two runs of a
project with a part are the same run, bit for bit.

**Compiled on your machine.** A plugin ships the component, never compiled code. The first time a project runs a
part, the engine has `argus-plugin-compile` (beside the engine, or what `ARGUS_PLUGIN_COMPILE` names) compile it for
this machine and keeps the result in `cache/plugin-code` under the launcher's folder, named by the component's
SHA-256 and the runtime's version. A later run loads it from there in well under a millisecond. Without the
compiler and without a cached copy the part does not start, and its status says so.

**What a part costs a frame.** With `ARGUS_PLUGIN_TIMING` set (to anything), the log says every 300 frames how
long each part's `frame` took a frame, and how many things the part draws.

**Not yet:** an exported game does not carry WebAssembly parts, and the web build does not run them. The part's
own `status` is asked once every 30 steps.

## Publishing a plugin

A published plugin is a signed manifest over its files in the update source, as an engine version is
(docs/PLAN-LAUNCHER.md, section 4). Four steps, the last three with the release tool (`argus-release`):

```
argus plugin pack radio --out build/radio                      # the files the descriptor names, in a new folder
argus-release stage-plugin --src build/radio --tree release    # objects and an unsigned manifest
argus-release sign --key release.pem release/plugins/radio/1.2.0/manifest.json
argus-release promote-plugin --tree release --key release.pem --id radio --version 1.2.0 --summary "A radio."
```

- **`plugin pack`** takes the plugin's id or its folder and writes exactly what its descriptor names: the
  descriptor, schemas, contribution data, the tools file and the scripts its tools run, starter files, the behaviours
  manifest and every module it names, the folders of the TypeScript entry and types, the UI and web folders, and
  whatever `ship` lists, and its WebAssembly part. Never sources or tests. A file the descriptor names that is
  missing stops it (build the plugin first), and so does a compiled-in part.
- **The id, version and plugin API** in the manifest come from the plugin's own `plugin.json`. A published version
  is immutable: publish a new version, never the same one twice.
- **`promote-plugin`** adds the version to `plugins/index.json`, the signed list the launcher reads. Earlier
  versions stay listed, since a project may ask for one.
- **An offline pack** of a staged, signed version, in the same file format as an engine's:
  `argus-release pack --tree release --manifest plugins/radio/1.2.0/manifest.json --out radio-1.2.0.argus-pack`.

Uploading the release tree to the update source is the release process's step, as for an engine.

## The descriptor

`plugins/<id>/plugin.json`. Paths are relative to the plugin's folder and must stay inside it.

```json
{
  "schemaVersion": 1,
  "id": "training",
  "displayName": "Training",
  "version": "0.1.0",
  "apiVersion": 1,
  "description": "Instructor console, scorecards and after-action review.",
  "platforms": ["windows", "linux", "macos", "web"],
  "capabilities": [
    { "id": "net.http.serve", "why": "serves the instructor console page" },
    { "id": "session.control", "why": "pauses, injects events and takes over entities for the instructor" },
    { "id": "fs.project.write", "why": "writes reports", "paths": ["output/training/"] }
  ],
  "ts": { "entry": "dist/lib/index.js", "types": "dist/types/index.d.ts" },
  "behaviours": "dist/entries.json",
  "ui": "ui/",
  "web": { "root": "web/" },
  "files": [{ "match": "**/*.scorecard.json", "schema": "schemas/scorecard.schema.json" }],
  "contributes": { "journal.kinds": "data/kinds.json", "journal.metrics": "data/metrics.json" },
  "tools": "tools.json",
  "configSchema": "schemas/config.schema.json",
  "onAdd": { "copy": [{ "from": "templates/sample.scorecard.json", "to": "training/sample.scorecard.json" }] },
  "requires": { "plugins": [] }
}
```

| Field | Required | Meaning |
|---|---|---|
| `schemaVersion` | yes | `1` |
| `id` | yes | lowercase letters, digits and single hyphens, starting with a letter, at most 32; the folder's name |
| `displayName`, `description` | yes | one line each, for lists |
| `version` | yes | the plugin's own `MAJOR.MINOR.PATCH` |
| `apiVersion` | yes | the plugin API it was written for; this engine serves `1` |
| `engine` | no | the engine series it was built for (`"0.8"`), for plugins shared apart from the engine (later); first-party plugins and your own leave it out |
| `platforms` | yes | any of `windows`, `linux`, `macos`, `web` |
| `capabilities` | no | `[{id, why}]` from [the list](#what-a-plugin-may-do); `fs.project.write` adds `paths`, `net.out` adds `hosts` |
| `ts` | no | the built package: `entry` (JavaScript), `types` (declarations) |
| `behaviours` | no | the built behaviours' `entries.json` (the `@argus/gameplay` shape); scenes name them `<id>:<name>` |
| `ui` | no | a folder of UI screens, registered as `<id>/<screen>` |
| `web` | no | `{root}`: a folder served at `/plugins/<id>/` |
| `files` | no | `[{match, schema}]`: JSON Schemas for the plugin's own project files; `*` matches within a folder, `**` across |
| `contributes` | no | data for engine systems, point name to file (`journal.kinds`, `journal.metrics`: Training Core) |
| `tools` | no | the agent tools file (below) |
| `native` | no | `{crate, runtime}`: the compiled-in part, `argus-plugin-<id>`; `runtime: false` for an authoring-only part |
| `configSchema` | no | the JSON Schema a project's `config` must meet. Absent: the plugin takes no settings |
| `onAdd` | no | `{copy: [{from, to}]}`: starter files copied into the project on add, never overwriting |
| `requires` | no | `{plugins: [...]}`: other plugins the project must list before this one |
| `ship` | no | other files or folders an installed engine carries for the plugin that no field above names (a command-line tool of its own). Each JavaScript file it names is a script `argus plugin run` starts |

Files you write (schemas, templates, `tools.json`, the web root, the UI folder) must exist when the descriptor is
read; files a build makes (`ts`, `behaviours`) are checked when the plugin loads.

**Settings schemas** use the part of JSON Schema settings need: `type`, `properties`, `required`,
`additionalProperties`, `items`, `enum`, `const`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`,
`minLength`, `maxLength`, `minItems`, `maxItems`, and the annotations `$schema`, `$id`, `title`, `description`,
`default`, `examples`. Any other keyword is refused when the plugin is read, so nothing a schema says goes
unchecked. Give every setting a `default` and a `description`: `plugin info` shows both.

**Agent tools** (`tools.json`):

```json
{ "toolset": "training",
  "tools": [{ "name": "training.report", "description": "...", "inputSchema": { "type": "object" },
              "run": { "node": "dist/tools/report.js" } }] }
```

`run` is `{"node": file}` (a module exporting `run(args, ctx)`, run on the engine's Node) or
`{"query": "GET /plugins/<id>/..."}` (a call to the running game); `{"native": op}` is reserved for compiled-in
parts. A Node tool's `ctx` is `{ projectRoot, play, log }`: `play` is null when no game is running, else
`get(path)` and `post(path, body)` against the running game's query API, answered through the agent's own session
(so they pass the query server's guard; there is no URL to fetch directly), a body that is not JSON coming back as
its text. Names are `<id>.<name>`, with dots like every engine tool.

Agents reach them through the one `plugin` tool, so a plugin adds nothing to any agent's tool list:
`{"op": "tools", "id": "training"}` lists the plugin's tools with their input schemas, and
`{"op": "run", "id": "training.report", "args": {...}}` runs one in a project that lists the plugin.

## Problems and their codes

| Code | Means | Do |
|---|---|---|
| `project-plugin` | an entry's id is not a plugin id, is listed twice, or its `version` is not `MAJOR[.MINOR[.PATCH]]` | fix the entry in the project file |
| `plugin-unknown` | no installed plugin has that id | `argus plugin list` shows the installed ones |
| `plugin-version` | the installed plugin does not match the project's `version` | set `version` to the installed line, or remove it |
| `plugin-config` | a setting is not one the plugin has, or not a value it takes | `argus plugin info <id>` lists the settings |
| `plugin-requires` | the plugin needs another the project does not list first (or a remove would leave one without it) | add the other plugin first, or remove the dependent one first |
| `plugin-api` | the plugin was written for another plugin API or engine series | use the plugin release made for this engine |
| `plugin-platform` | the plugin does not run on this target (a native-only plugin in a web export) | leave the plugin out of that target's project |
| `plugin-native` | the plugin's compiled-in part is not in this engine build | use a build that has it (the standard engine builds have every first-party part) |
| `plugin-descriptor` | a plugin's `plugin.json` is broken or names a missing file | for plugin authors: the message names the field |
| `plugin-start` | the plugin's compiled-in part could not start: a UDP port it binds is taken, a multicast group refused it | free the port or set another (`argus plugin add <id> --set port=...`); the line names the address |
| `plugin-project` | the project file could not be read or written, or changed while the edit was made | run the command again |
| `plugin-not-listed` | a remove names a plugin the project does not list | `argus plugin list` marks the listed ones |

