SteonMod
View as Markdown

Router manifests and scenarios (v0)

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

FileWritten bySays
router.toml (one per game)router makerswhich blocks a game offers, what each one needs and gives
scenario (*.toml)players, or the card builder for themwhich 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]

FieldRequiredMeaning
idyeslowercase letters, digits, _, -. E.g. skyrim, minecraft. Two installed routers can't share an id.
nameyesname shown to players
versionyesthe router's own version
game_exenothe game's executable, e.g. SkyrimSE.exe
licensenothe 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
contributorsnonames of people who improved it (router propose / accept add them)
based_onnothe router this one continues, <router id>@<version> (e.g. [email protected]), when its author stopped answering
tested_buildsnogame 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.
unitsyeshow the game's numbers convert to Fusion's (below)
linknohow the game talks to Fusion (below). Default: the Fusion protocol, with the router id as discovery id
launchnohow Fusion starts the game when it isn't running (below). Every router except builtin ones should have one
detectnohow Fusion tells whether the game is installed, for My Games (below). Default: the launch recipe's steam_app, or its exe
setupnothe setup recipe: what the game needs so Fusion can fuse it, and how Fusion installs its kit in one click (below)
FieldValuesDefault
protocolfusion: 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_idthe id the game's kit looks itself up by in Fusion's discovery mappingthe 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:

PlaceholderMeans
{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:

FieldMeaning
steam_appa Steam app id. Fusion opens steam://rungameid/<id>, so Steam starts the game.
exe + argsa 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
FieldRequiredMeaning
nameyesthe anti-cheat's name, shown to players
off_modeyesthe game's own option that turns it off, as players see it
processesone of these twoprograms whose running means it's on
modulesone of these twoDLLs 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:

FieldMeaning
steam_appSteam has this app fully installed (in any Steam library).
pathsany 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.

FieldValuesDefault
lengthm, cm, mm, in, ft, unreal (= cm)required
up_axisx, y, zrequired
handednessright (Godot, Minecraft) or left (Unity, Unreal)right
health_maxthe game's health for a full-health human, so 10% damage in one game is 10% in anothernone

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

[[block]]

FieldRequiredMeaning
idyes<router id>.<name>, e.g. skyrim.npcs
nameyesname on the block's card
kindyesworld, character, npc_group, system or items (what Fusion does with each: below)
descriptionnoone or two sentences for the card
needsnoports this block needs from other blocks (or from Fusion)
givesnoports this block offers
actionsnothings Lua and tools can ask it to do (stun, set_health, ...)
costnoram_gb (number), gpu (none/low/medium/high, default low), must_stay_awake (true if the game can't sleep while this block is used)
docsnoa Markdown page about the block, relative to router.toml; it must exist
bindnohow 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
contentnowhat 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"
FieldRequiredMeaning
whatyeswhat the list is, shown on the card: mobs, items, maps
fileyeswhere 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)
insidenoa JSON or text entry inside the zip or jar to read instead
keysyeswhich names, keys or lines are items, with exactly one *; what * matches is the item's id (item.minecraft.*_spawn_egg → zombie_villager)
namenoJSON 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")
iconnoa 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).
[[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"
FieldRequiredMeaning
id, nameyes
descriptionnoone line under it on the card
kindyeschoice, number, toggle or text
choices / choices_froma choice needs oneits options, or the what of one of the block's [[block.content]] lists
defaultyes, except a choice from a listthe value when the scenario doesn't choose
min, maxnoa 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

KindWhat Fusion does with it today
worldThe 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).
characterThe 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_groupIts game's NPCs go into Fusion's entity registry (gives = ["entities"]).
system, itemsAccepted, 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 -.

TypeCarriesFusion can provide it itself?
collisionworld shape near the playeralways: Fusion's Collision Hub, which merges every block that gives it with Godot's floor and props
poseposition + skeleton
inputcontrolsyes: the player's keyboard, mouse, gamepad
layera 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)
audiothe game's sound
statehealth, weather, money, ...
entitieslists 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)
eventsdamage, item used, death, ...
steeringwaypoints from Fusion's navmesh, for NPCs walking on another game's worldyes: 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:

  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.

[[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>.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 (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 (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).