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.
[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. [email protected]), 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}:
[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.
[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:
[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 steps, then Ready once installed_when exists.
[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.
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 requires, 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 |
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]] |
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.
[[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 ({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]'sexeorargs; - its kit gets them when it connects (a
settingsquestion): router scripts readRouter.settings.<id>(as text).
[[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:
[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:
[[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:
- A
[[wire]]the player chose (below) wins. collisioncomes 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.- Exactly one other block gives it: connect to that block.
- Nobody gives it, and Fusion can provide it (
input,steering): connect to Fusion. - Otherwise it's a problem the player must fix before pressing Play:
- missing: "
skyrim.npcsneedscollision, 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
instancename.
- missing: "
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.
[[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.
[[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¶
[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 Play menu with one click, without seeing cards, wiring or Lua. Example (scenarios/beacon-run.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 Play menu doesn't offer to open it in the builder),stacking(nothing can be stacked with it). Anything else is a mistake the Play menu 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>.pngnext 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 (main menu → + 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(orentities), 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) andentitiesso far.layer,state,events,audioandsteeringare 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).