# Router manifests and scenarios (v0)

Two small TOML files describe everything Fusion needs to fuse games:

| File | Written by | Says |
|---|---|---|
| **`router.toml`** (one per game) | router makers | which **blocks** a game offers, what each one needs and gives |
| **scenario** (`*.toml`) | players, or the card builder for them | which blocks to use together |

Fusion reads both, **wires the blocks together automatically**, and its session runtime
starts and connects the games they come from.

A router splits one game into several blocks (Minecraft's player and its mobs; VEIN's world
and its zombies), so a scenario can use just the parts it wants. All the blocks of one router
come from **one running copy of the game**: a scenario that uses two of them starts the game
once. The code is in
`crates/fusion-core` (`manifest.rs`, `ports.rs`, `scenario.rs`, `session.rs`). The
reference example is `routers/fake/router.toml` with
`scenarios/cube-in-playground.toml`. Fusion's
own blocks (its playground world and player) are in
`routers/fusion/router.toml`.

Check your files before sharing them:

```
fusion-cli check-router routers/fake
fusion-cli check-scenario scenarios/cube-in-playground.toml [--routers DIR]
```

Both print every problem in plain words and exit with an error if there is one.

---

**In an editor:** JSON Schemas of `router.toml`, scenarios and `package.toml` are in
`docs/schemas/` (generated from the code that reads them: `fusion-cli schema --write`). With
Taplo / Even Better TOML, put `#:schema ../../docs/schemas/router.schema.json` on a file's first
line, or map `routers/*/router.toml` and `scenarios/*.toml` to them in its settings: unknown
fields and wrong types show while typing. `fusion-cli router check` remains the real check.

## `router.toml`

One file per game. It lives in the router's folder (`routers/<id>/router.toml`), next
to the router's scripts and docs.

```toml
[router]
id = "fake"                    # short id; every block id starts with it
name = "Fake Game"             # shown to players
version = "0.1.0"              # the router's version, not the game's
game_exe = "fake-guest.exe"    # optional
tested_builds = ["sha256:9f86d0...0a08"]   # optional, see below
units = { length = "m", up_axis = "y", health_max = 100 }

[router.link]                  # how the game talks to Fusion (see below)
protocol = "fusion"
game_id = "fake"

[router.launch]                # how Fusion starts the game (see below)
exe = "{tools}/fake-guest.exe"
args = ["--game-id", "fake", "--no-window"]

[[block]]
id = "fake.cube"
name = "Spinning Cube"
kind = "character"
description = "The cube from the GPU test."
needs = ["collision", "input"]
gives = ["pose", "layer:color+depth", "state:health", "events:damage"]
actions = ["spin", "stop", "set_health"]
cost = { ram_gb = 0.1, gpu = "low" }
docs = "docs/cube.md"          # optional, relative to router.toml
```

Unknown fields are refused (a typo like `need =` is an error, not silently ignored).

### `[router]`

| Field | Required | Meaning |
|---|---|---|
| `id` | yes | lowercase letters, digits, `_`, `-`. E.g. `skyrim`, `minecraft`. Two installed routers can't share an id. |
| `name` | yes | name shown to players |
| `version` | yes | the router's own version |
| `game_exe` | no | the game's executable, e.g. `SkyrimSE.exe` |
| `license` | no | the router's license (an SPDX id such as `CC-BY-4.0` or `MIT`). Routers on a hub use one that lets others improve them, with credit |
| `contributors` | no | names of people who improved it (`router propose` / `accept` add them) |
| `based_on` | no | the router this one continues, `<router id>@<version>` (e.g. `vein@0.3.0`), when its author stopped answering |
| `tested_builds` | no | game builds this router was tested with, each `sha256:` + 64 hex digits (the hash of the game's exe). If the list isn't empty and the installed game doesn't match, the router's blocks are **disabled with a clear message**; they never crash. An empty list allows any build. |
| `units` | yes | how the game's numbers convert to Fusion's (below) |
| `link` | no | how the game talks to Fusion (below). Default: the Fusion protocol, with the router id as discovery id |
| `launch` | no | how Fusion starts the game when it isn't running (below). Every router except `builtin` ones should have one |
| `detect` | no | how Fusion tells whether the game is installed, for My Games (below). Default: the launch recipe's `steam_app`, or its `exe` |
| `setup` | no | the **setup recipe**: what the game needs so Fusion can fuse it, and how Fusion installs its kit in one click (below) |

### `[router.link]`

| Field | Values | Default |
|---|---|---|
| `protocol` | `fusion`: the Fusion protocol, which every Fusion kit speaks (Minecraft's Fabric kit too, since BUILD.md step 1.3b). `builtin`: Fusion's own blocks, drawn by Godot (no game to start) | `fusion` |
| `game_id` | the id the game's kit looks itself up by in Fusion's discovery mapping | the router id |

### Paths and placeholders

Every path in `router.toml` (launch, detect, setup) can use these, and should start with one
of them or be absolute:

| Placeholder | Means |
|---|---|
| `{game}` | the game's own folder: its Steam folder, or what `[router.detect]` found (the folder of a file it found). Launch and setup only. |
| `{fusion}` | Fusion's own folder (the repository while developing) |
| `{tools}` | Fusion's built programs and libraries (`target/debug` while developing) |
| `{router}` | this router's folder. Launch and setup only. |
| `%NAME%` | an environment variable, e.g. `%LOCALAPPDATA%` |

### `[router.launch]`

One of:

| Field | Meaning |
|---|---|
| `steam_app` | a Steam app id. Fusion opens `steam://rungameid/<id>`, so Steam starts the game. |
| `exe` + `args` | a program and its arguments, with the placeholders above. |

**A game outside Steam:** say where it's installed in `[router.detect] paths`, and start it from
its own folder with `{game}`:

```toml
[router.launch]
exe = "{game}/DuneRacer.exe"
args = ["-windowed"]

[router.detect]
paths = ["%PROGRAMFILES%/Dune Racer/DuneRacer.exe", "D:/Games/Dune Racer/DuneRacer.exe"]
```

Fusion starts games through the Windows shell, minimized and without focus, so they inherit
nothing from Fusion. When a scenario is played, Fusion first waits about 3 seconds: a game
that's already running connects by itself in that time and is not started twice.

### `[router.anticheat]`: official anti-cheat-off modes

Only for a game whose **own launcher** can start it with anti-cheat off (Halo: The Master Chief
Collection's "Anti-Cheat Disabled (Mods and Limited Services)"; `VISION.md` §10). Fusion starts it
only that way and **refuses to attach while its anti-cheat runs**; it never disables, removes or
fools anti-cheat itself. Games with no such official mode aren't supported.

```toml
[router.launch]               # the off mode's own program and arguments (not steam_app,
exe = "{game}/mcclauncher.exe" # which starts the default mode)

[router.anticheat]
name = "Easy Anti-Cheat"
off_mode = "Anti-Cheat Disabled (Mods and Limited Services)"
processes = ["EasyAntiCheat.exe"]      # running = it's on
modules = ["EasyAntiCheat_x64.dll"]    # loaded in the game = it's on
```

| Field | Required | Meaning |
|---|---|---|
| `name` | yes | the anti-cheat's name, shown to players |
| `off_mode` | yes | the game's own option that turns it off, as players see it |
| `processes` | one of these two | programs whose running means it's on |
| `modules` | one of these two | DLLs whose presence in the game's process means it's on |

`router check` fails a router whose game folder ships anti-cheat (`router recon` lists what it
finds) without this table.

### `[router.detect]`

My Games lists every game on the PC. A router's game counts as installed if:

| Field | Meaning |
|---|---|
| `steam_app` | Steam has this app fully installed (in any Steam library). |
| `paths` | any of these files or folders exists (the first one found is `{game}`). `%NAME%`, `{fusion}` and `{tools}` work. |

Without `[router.detect]`, Fusion uses the launch recipe: its `steam_app`, or whether its
`exe` exists. Minecraft's router, for example, checks for its Prism instance:

```toml
[router.detect]
paths = ["%APPDATA%/PrismLauncher/instances/Fusion-MC-26.3"]
```

A scenario whose game isn't installed can't be played, and says so. When `tested_builds`
isn't empty, Fusion finds `game_exe` in the install folder and checks its hash: another
version shows as "Version mismatch" and its scenarios can't be played.

`fusion-cli my-games` prints what Fusion finds.

### `[router.setup]`: one-click setup

What a game needs before Fusion can fuse it. My Games shows **Needs a mod
loader** (with guided steps and a link) until every `require` is there, then **Needs setup**
with a **Set up** button that runs the `step`s, then **Ready** once `installed_when` exists.

```toml
[router.setup]
installed_when = "{game}/Vein/Binaries/Win64/ue4ss/Mods/FusionKit/dlls/main.dll"

[[router.setup.require]]          # the player installs it; Fusion only guides and links
name = "UE4SS (experimental 3.0.1)"
check = "{game}/Vein/Binaries/Win64/ue4ss/UE4SS.dll"
guide = "Download UE4SS's experimental build and unzip it into VEIN's Vein/Binaries/Win64 folder."
url = "https://github.com/UE4SS-RE/RE-UE4SS/releases"

[[router.setup.step]]             # Fusion does it
copy = "{tools}/fusion_unreal.dll"
to = "{game}/Vein/Binaries/Win64/ue4ss/Mods/FusionKit/dlls/main.dll"

[[router.setup.step]]
add_line = "FusionKit : 1"
to = "{game}/Vein/Binaries/Win64/ue4ss/Mods/mods.txt"
```

The placeholders are the ones in [Paths and placeholders](#paths-and-placeholders).

A step is either `copy` (a file or a whole folder) or `add_line` (added to a text file unless
it's already there), always with `to`. Fusion only installs from its own files: mod loaders
are `require`s, because their licenses or sources may not allow Fusion to fetch them.

**Undo:** every change is written to a journal (`%LOCALAPPDATA%/Fusion/setup/<router>/`)
before it's made, and files Fusion overwrites are backed up there. **Undo setup** in My Games
(or `fusion-cli setup <router> --undo`) puts the game back exactly as it was. Setup refuses
to run while the game is running (Windows locks its files).

### `units`

Fusion's own units are **meters, Y-up, right-handed** (Godot's convention). Every value
crossing into or out of a game is converted at the boundary, so blocks from different
games agree.

| Field | Values | Default |
|---|---|---|
| `length` | `m`, `cm`, `mm`, `in`, `ft`, `unreal` (= cm) | required |
| `up_axis` | `x`, `y`, `z` | required |
| `handedness` | `right` (Godot, Minecraft) or `left` (Unity, Unreal) | `right` |
| `health_max` | the game's health for a full-health human, so 10% damage in one game is 10% in another | none |

Units are per router, not per block: one game has one unit system.

### `[[block]]`

| Field | Required | Meaning |
|---|---|---|
| `id` | yes | `<router id>.<name>`, e.g. `skyrim.npcs` |
| `name` | yes | name on the block's card |
| `kind` | yes | `world`, `character`, `npc_group`, `system` or `items` (what Fusion does with each: below) |
| `description` | no | one or two sentences for the card |
| `needs` | no | ports this block needs from other blocks (or from Fusion) |
| `gives` | no | ports this block offers |
| `actions` | no | things Lua and tools can ask it to do (`stun`, `set_health`, ...) |
| `cost` | no | `ram_gb` (number), `gpu` (`none`/`low`/`medium`/`high`, default `low`), `must_stay_awake` (true if the game can't sleep while this block is used) |
| `docs` | no | a Markdown page about the block, relative to `router.toml`; it must exist |
| `bind` | no | how the game's kit finds this block in the game: the world's map and player class, which objects are its entities, what to spawn. See [`router-api.md`](https://steonmod.com/docs/router-api) |
| `content` | no | what the block has (mobs, items, maps), read from the player's own game files while the game is closed: see [`[[block.content]]`](#blockcontent-read-in-place) |

The declared cost is what the card's cost meter shows before Play. Fusion also measures
the real cost while running.

### `[[block.content]]`: read in place

What a block has (its mobs, items, maps), read from **the player's own installed copy** of the
game while the game is closed, so the cards (and later the spawn menu and Lua) can show it
without starting the game. The game still runs every mechanic; this is only
a list of names (and icons). Nothing read is ever packaged or shared: Fusion keeps it in a cache
on the player's PC (`%LOCALAPPDATA%/Fusion/cache/content`) and reads again when the file changes.

```toml
[[block.content]]
what = "mobs"
file = "%APPDATA%/PrismLauncher/libraries/com/mojang/minecraft/26.3/minecraft-26.3-client.jar"
inside = "assets/minecraft/lang/en_us.json"
keys = "item.minecraft.*_spawn_egg"
name = "entity.minecraft.*"
icon = "assets/minecraft/textures/item/*_spawn_egg.png"
```

| Field | Required | Meaning |
|---|---|---|
| `what` | yes | what the list is, shown on the card: `mobs`, `items`, `maps` |
| `file` | yes | where to read, with the [placeholders](#paths-and-placeholders) (`{game}/...` for the game's own folder). One of: a **folder** (the names of its files), a **zip, jar, pk3 or pak** (the names of its entries), a **JSON** file (the keys of its top object) or a **text** file (its lines: `.txt`, `.lst`, `.csv`) |
| `inside` | no | a JSON or text **entry inside** the zip or jar to read instead |
| `keys` | yes | which names, keys or lines are items, with exactly one `*`; what `*` matches is the item's id (`item.minecraft.*_spawn_egg` → `zombie_villager`) |
| `name` | no | JSON only: the key holding each item's name, with `*` for its id (`entity.minecraft.*`). Default: the key's own text, else the id made readable (`zombie_villager` → "Zombie Villager") |
| `icon` | no | a PNG per item, with `*` for its id: an entry of the same zip, or a path next to `file` |

Rules: only those file kinds are read, so a router can't point Fusion at a
player's private files; **encrypted** files (an Unreal `.pak` with a key, say) are refused, never
unlocked: use the running game for that data. `router check` reads each list on this PC and
shows the first names, or what the file starts with when `keys` matched nothing.

### `[[block.setting]]`: what players choose on the card

Settings a player picks on the block's card, without code: how many, which modpack, day or
night. The card builder shows them on the block (a dropdown, number box, checkbox or text box);
the scenario keeps the choices; the game gets them two ways:

- its **launch recipe** can use them: `{setting.<id>}` in `[router.launch]`'s `exe` or `args`;
- its **kit** gets them when it connects (a `settings` question): router scripts read
  `Router.settings.<id>` (as text).

```toml
[[block.setting]]
id = "walkers"                 # lowercase, digits, _; unique in the router
name = "Walkers"
description = "How many test NPCs walk around."
kind = "number"                # choice | number | toggle | text
default = 3
min = 0
max = 8

# Player content: the choices are one of the block's read-in-place lists ([[block.content]]):
# here the player's own Prism instances, that is their modpacks (routers/minecraft).
[[block.content]]
what = "instances"
file = "%APPDATA%/PrismLauncher/instances"
keys = "*"

[[block.setting]]
id = "instance"
name = "Instance"
kind = "choice"
choices_from = "instances"     # or: choices = ["day", "night"]
default = "Fusion-MC-26.3"
```

| Field | Required | Meaning |
|---|---|---|
| `id`, `name` | yes | |
| `description` | no | one line under it on the card |
| `kind` | yes | `choice`, `number`, `toggle` or `text` |
| `choices` / `choices_from` | a choice needs one | its options, or the `what` of one of the block's `[[block.content]]` lists |
| `default` | yes, except a choice from a list | the value when the scenario doesn't choose |
| `min`, `max` | no | a number's range |

A scenario chooses with `settings = { walkers = 5 }` on its `[[block]]`; an unknown setting or a
value that doesn't fit is a problem in plain words ("`fake.cube` setting `walkers`: 12 is outside
0..8"). A game's launch recipe gets every setting of its router at its default, then the
scenario's choices, so a scenario that uses only some of a game's blocks still starts it.

### Kinds

| Kind | What Fusion does with it today |
|---|---|
| `world` | The base of the scene. A scenario has exactly one. Fusion puts the played character at its spawn point, and a character learns about a place only once the World has sent its collision there (so nobody falls through ground that hasn't arrived yet). |
| `character` | The first one in a scenario is the one the player plays as ("possess"): Fusion's camera goes to its eyes when its game sends a view, or orbits it when the game only sends where it stands (`pose`); the player's keys and mouse go to it (`input`). |
| `npc_group` | Its game's NPCs go into Fusion's entity registry (`gives = ["entities"]`). |
| `system`, `items` | Accepted, so routers can declare them; Fusion does nothing special with them yet. |

---

## Ports

A port is written `type` or `type:detail`, e.g. `collision`, `state:health`,
`layer:color+depth`. Details are lowercase letters, digits, `_`, `+` or `-`.

| Type | Carries | Fusion can provide it itself? |
|---|---|---|
| `collision` | world shape near the player | always: Fusion's Collision Hub, which merges every block that gives it with Godot's floor and props |
| `pose` | position + skeleton | |
| `input` | controls | yes: the player's keyboard, mouse, gamepad |
| `layer` | a picture for the compositor: `layer:color+depth` (a GPU picture with depth, e.g. VEIN), `layer:mesh` (meshes Godot draws, e.g. Minecraft's blocks and body), `layer:overlay` (drawn on top, e.g. Minecraft's hand and HUD) | |
| `audio` | the game's sound | |
| `state` | health, weather, money, ... | |
| `entities` | lists of NPCs or villagers (they will become proxies in other games, with the Proxy Manager) | Fusion's entity registry collects every block that gives it, plus Godot's props (F8 shows them) |
| `events` | damage, item used, death, ... | |
| `steering` | waypoints from Fusion's navmesh, for NPCs walking on another game's world | yes: Fusion's navigation |

**Matching:** a block that gives `state:health` satisfies a need for `state:health` and
a need for plain `state` (any detail). A need for `state:money` is *not* satisfied by
`state:health`.

---

## Scenarios

A scenario lists the blocks to use. That's usually all it needs:

```toml
[scenario]
name = "Cube in the Playground"
description = "Fusion's test game draws a spinning cube into the playground."

[[block]]
use = "fusion.playground"

[[block]]
use = "fake.cube"
```

To be playable, a scenario needs **exactly one World block** (Fusion plays one World at a
time for now). The first Character block is the one the player plays as; with no
Character the player gets a free-flying camera, and also while the Character's game is
still starting.

To use the same block twice, give the copies names with `instance`:

```toml
[[block]]
use = "fake.cube"

[[block]]
use = "fake.cube"
instance = "second_cube"
```

### How Fusion wires a scenario

For every need of every block, in this order:

1. **A `[[wire]]` the player chose** (below) wins.
2. **`collision` comes from Fusion's Collision Hub**. The hub merges
   the collision of every block that gives it (the World's ground, a character's body,
   blocks another game placed) with Godot's floor and props. Each block reads everything
   except its own collision, so a character doesn't collide with its own body.
3. **Exactly one other block gives it:** connect to that block.
4. **Nobody gives it, and Fusion can provide it** (`input`, `steering`): connect to Fusion.
5. Otherwise it's a **problem** the player must fix before pressing Play:
   - *missing*: "`skyrim.npcs` needs `collision`, but no block in the scenario gives it";
   - *ambiguous*: several blocks give it, so pick one with a `[[wire]]`;
   - *unknown block*: no installed router has it;
   - *duplicate*: the same block used twice without an `instance` name.

Example output of `fusion-cli check-scenario scenarios/cube-in-playground.toml`:

```
Cube in the Playground (2 blocks; 7 blocks installed from routers)
  fake.cube collision <- Fusion (collision hub)
  fake.cube input <- Fusion
  world: Playground (fusion.playground)
  character: Spinning Cube (fake.cube)
  game: Fusion (builtin protocol)
  game: Fake Game (fusion protocol), started with `{tools}/fake-guest.exe --game-id fake --no-window --cube 0,1.5,1 --ramp 4,0.35,3 --npcs -4,0,3`
  collision hub <- fusion.playground (as `fusion`)
  collision hub <- fake.cube (as `fake.cube`)
  collision hub -> fake.cube (reads every source except fake.cube)
  fake.cube learns about a place once `fusion` has looked there
  entity registry <- fusion.playground (as `fusion`)
  entity registry <- fake.cube (as `fake.cube`)
OK: playable, 2 connections, 2 games
```

### `screen`: a game on a screen in the world

A block can put its game **on a screen** instead of around the player: DOOM on an arcade
cabinet, a game on a TV in another game's world.

```toml
[[block]]
use = "fake.cube"
screen = { at = [0.0, 0.0, -6.0], width = 3.2, yaw = 0.0 }   # bottom middle, meters, degrees
```

Its game then draws **its own view** (Fusion sends it no camera), and Fusion shows its picture on
a framed screen standing at `at` (`width` in meters, default 2.4; the height follows the picture;
`yaw` turns it, 0 faces +Z). The player walks up, looks at it and presses **E** to use it (keys and
mouse go to the game, Fusion's player stands still); **E** again steps back. A game on a screen is
only a picture: it isn't the World or the played Character, and its collision and entities stay
out of the world.

### `[[wire]]`: choosing a source by hand

The wiring view in the app writes these; players can also write them.

```toml
[[wire]]
port = "collision"   # the need
from = "fusion"      # an instance name, or "fusion" for Fusion's own services
to = "fake.cube"     # the instance that needs it
```

A wire is refused (with a message) if `to` isn't in the scenario or doesn't need that
port, if `from` doesn't give it, or if `from = "fusion"` for a port Fusion can't provide.

For `collision`, a wire narrows what the block reads from the Collision Hub to that one
block's collision (`from = "fusion.playground"` means only Godot's floor and props).

### `addons`: Lua addons it uses

```toml
[scenario]
name = "Joker Hunt"
addons = ["joker_hunt", "hud_clock"]   # folders in the addons folder
```

Sharing the scenario puts these addons in its package, and installing it installs them. An
Experience (below) runs **only** the addons it lists; a plain scenario runs all of the player's
addons.

### Experiences

An **Experience** is a scenario with an `[experience]` table: a finished
game players start from the Library with one click, without seeing cards, wiring or Lua.
Example (`scenarios/beacon-run.toml`):

```toml
[experience]
title = "Beacon Run"                # on its cover and title screen (the scenario's name if left out)
intro = "Find the green beacon before the timer runs out."   # under the title
lose_on = "timer_done"              # a signal that loses (the Timer Device sends it)
win_on = ""                         # a signal that wins; empty: finishing every goal wins
win_text = "You found the beacon in time."   # the ending screen's words
lose_text = "The timer ran out."
start = [0.0, 0.0, 8.0]             # where the player starts (the World's spawn point if left out)
play_as = "fusion.player"           # which Character, when the scenario has several
locks = ["devices"]                 # what the player can't change (below)

[[experience.goal]]
text = "Find the green beacon"
done_on = "beacon_found"            # the signal that ticks it off
```

- **Signals** come from Devices (a Trigger Zone's, a Timer's, a Quest Giver's, a Rule's) or from
  Lua (`Fusion.signal(name)`). Lua can also end it: `Experience.win(text)` / `Experience.lose(text)`.
- **Locks:** `devices` (the F5 Devices panel is off), `remix` (the Library doesn't offer to open
  it in the builder), `stacking` (nothing can be stacked with it). Anything else is a mistake the
  Library shows.
- **Tools** (`tools = ["grab", "carrot_launcher"]`): what the player gets from the sandbox:
  `grab`, `spawner`, `remover`, `weld`, `spawn_menu`, `context_menu`, `noclip`, and addons' tool
  ids. None listed: no sandbox at all (a plain scenario gets all of it).
- **Title screen:** the title, the intro, the goals and Start. The Experience's Devices start
  when the player presses Start, and Lua gets the signal `experience_start`.
- **Cover picture:** `<scenario file name>.png` next to the scenario (`scenarios/beacon-run.png`).
  Without one, the cover is the picture Fusion took the last time it was played, else the title
  on a color.

### Stacking

Players can play several items together, in an order they drag: Experiences, plain scenarios and
addons (Library → **+ Add to stack**). The v0 rule:
- the **first item** brings the World, who the player plays as, and the ending; it must be an
  Experience or a scenario;
- **later items** add their other blocks, `[[wire]]`s, Devices and addons. A later item's World
  must be the same block as the first's (then it's shared), else the stack is refused with a
  plain message. A different Character is left out, and so are later items' goals and endings;
  the menu says so;
- a block name already taken by a different block gets a number (`horde_2`); the same block
  under the same name is shared.

A stack is merged into one scenario (`FusionSessions.stack`, `experience::stack` in the core),
planned like any other, and played with only the addons it lists.

---

## Not in v0 yet

- **One collision source and one entity source per game.** A game's messages don't say which
  of its blocks they're from, so if a scenario uses two blocks of one game that both give
  `collision` (or `entities`), everything arrives under the first one. The same goes for
  reading: a game gets one collision stream, for its Character if it has one.
- Fusion acts on `collision`, `input`, `pose` (possess) and `entities` so far. `layer`,
  `state`, `events`, `audio` and `steering` are only listed on the card: the guest node shows
  whatever layers its kit sends.
- Setup recipes (mod loader, install steps, hashes) for one-click setup (step 4.2).
- Block settings with defaults (e.g. a Spawner's count) and per-instance overrides.
- Experiences can't lock the camera yet; a stack can't let a later item's ending win, or play
  two Worlds.
- Which ports the cards show by default, icons, thumbnails.
- Signatures and trust tiers (M6).
