SteonMod
View as Markdown

The Router API (v0)

A router splits one game into blocks (see block-manifest.md). The Router API is how a router tells the game's kit where those blocks are inside the game: which character is the player, which objects are its NPCs, how to get from the main menu into the world.

It's made to need as little as possible:

  1. The kit guesses first. Every kit knows its engine's conventions (on Unreal: player controller 0's pawn is the player, characters are Character actors). Most of a router is already done before you write anything.
  2. Data next. What the kit can't guess goes in router.toml, under each block's [block.bind]. Fusion checks it like the rest of the manifest.
  3. Lua last. Only what data can't say (clicking through a character creation screen) goes in router.lua, next to router.toml.

The same names work on every kit. The Unreal kit (kits/unreal) and the Unity kit (kits/unity, step 7.1) both read the data and run router.lua (see "On the Unity kit" below for what differs). The example is VEIN's router: routers/vein/router.toml and routers/vein/router.lua.

What the kit does by itself

Unreal kit defaultChange it with
The playerplayer controller 0's pawn, once it's a Characterbind.player (its class)
Getting into the worldwaits for the player; opens bind.map if the game is elsewhere (its main menu)bind.map, Lua enter
World loadedthe player exists for 3 s, World Partition finished loading, ground under it (60 s at most)Lua ready
Fusion's originopen ground near the player, facing something to seeLua anchor
Hidden from Fusion's viewthe player (Fusion's camera replaces it; it can't be hurt either)Lua hidden
The game's own viewturned off once Fusion shows the worldbind.hide_game_view = false
Loading around Fusion's cameraoffbind.player_follows_camera = true
Collisiontraced around Fusion's camera, if the world block gives collision
Entitiesbind.entity rules, then any other character, if a block gives entitiesbind.entity, Lua describe
Spawningthe spawn asset of the entity kind Fusion asks for, feet on the groundbind.entity.spawn, Lua spawn
Window focusthe game keeps running when its window isn't in front
Actionsremove, set_health (the rule's health field), move_to (Unreal's AI move helper) and stop on the entities it listed, and teleports of the player; a block's actions in router.toml say which Fusion offersLua actions
Playing as the charactera character block that needs input: Fusion's keys and mouse drive the player through Unreal's own SetControlRotation, AddMovementInput and Jump; its eyes (the game's camera) and feet go back to Fusion, whose camera follows them. Its pose goes to Fusion every frame

Data: [block.bind]

[[block]]
id = "vein.world"
kind = "world"
gives = ["layer:color+depth", "collision"]
...
[block.bind]
map = "/Game/Vein/Maps/ChamplainValleyDemo"   # opened from the main menu
player = "BP_VeinPlayerCharacter_C"           # in the world once the player is one
player_follows_camera = true                  # the game loads its world around the player

[[block]]
id = "vein.npcs"
kind = "npc_group"
gives = ["entities"]
...
[[block.bind.entity]]
class = "BP_Zombie_C"                 # exactly this class
kind = "zombie"
name = "Zombie"
health = "Health.Health"              # field paths
max_health = "Health.MaxHealth"
team = 2
spawn = "/Game/Vein/Zombies/BP_Zombie"  # what to spawn when Fusion asks for a "zombie"

[[block.bind.entity]]
is_a = "/Script/Vein.BaseVehicle"     # this class or any class made from it
kind = "vehicle"                      # name defaults to the class, made readable
FieldBlocksMeaning
mapworldthe map (level, scene) to open when the game isn't in it
playerworldthe class of the game's player character
player_follows_cameraworldkeep the game's own character under Fusion's camera (default false)
hide_game_viewworldturn the game's own view off once Fusion shows it (default true)
cameraworldthe game camera whose picture Fusion shows, by its object's name (default: the kit picks the game's main 3D camera)
anchorworldFusion's origin in the game's units and axes, [x, y, z, yaw] (yaw in degrees, where Fusion's forward faces); default: the kit picks one (Lua anchor on the Unreal kit)
entityblocks that give entitiesa list of rules, checked in order

Each entity rule has class or is_a, a kind, and optionally name, health, max_health (field paths like Health.Health), team and spawn (an asset or class path). Two rules can't spawn the same kind.

Lua: router.lua

The script runs once when the game starts. It gives blocks functions:

Router.block "vein.world" {
    enter = function()
        if Game.class_name(Game.player()) ~= "BP_CharacterCreationPawn_C" then
            return
        end
        local create = Game.widget("WBP_CharacterCreation_C")
        if create then
            create:OnRandomNameSet("Fusion Scout")
            ...
        end
    end,
}
FunctionBlocksCalledReturns
enter()worldtwice a second until the game is in its world, after the kit opened bind.mapnothing
ready(player)worldwhen the kit thinks the world is readyfalse to keep waiting
anchor(info)worldonce, in the world{ x, y, z, yaw }: Fusion's origin
hidden(info)worldwhen the captures are madea list of objects Fusion's view doesn't draw
describe(object, class, is_character)gives entities4 times a second per pawn near Fusion's camera{ kind, name, health, max_health, team }, false to leave it out, nil for the data rules
spawn = { <kind> = function(at, rotation, info) }gives entitieswhen Fusion asks for that kindthe new object, or nil
actions = { <name> = function(entity, args) }gives entitieswhen Fusion runs that action on one of the block's entities, before the kit's owntrue if done, false if not

info is { world, controller, pawn, anchor }. Router.settings holds the scenario's choices for the router's block settings ([[block.setting]], see block-manifest.md), by setting id, as text (tonumber(Router.settings.walkers)); Fusion sends them when it connects. Positions are in the game's own units and axes (router.toml's units), as { X, Y, Z }; rotations as { Pitch, Yaw, Roll }.

A mistake stops the script with its line and what's allowed:

router.lua:15: Router.block "vein.world": unknown field `entr` (a world block can have: anchor, enter, hidden, ready)

The kit then logs it in fusion_kit.log (next to the kit's main.dll) and does nothing else, so a broken router can't half-run.

Game: the game's objects

Function
Game.find(class) / Game.find_all(class)the first / every live object of a class (short name, subclasses too)
Game.object(path)an object by full path, e.g. /Script/Engine.Character
Game.class(path)a class by path, loading its asset if needed (/Game/Zombies/BP_Zombie)
Game.class_name(o), Game.full_name(o), Game.valid(o), Game.is_a(o, class)about an object
Game.get(o, "A.B") / Game.set(o, "A.B", v)a field by path; nil / false instead of an error
Game.call(o, "Name", ...)a method by name (the same as o:Name(...))
Game.controller(), Game.player(), Game.world(), Game.map()the player controller, its pawn, the world, the map's path
Game.console(command)runs a console command
Game.trace(from, to, ignore)what a line hits (sight), or nil
Game.spawn(class, at, rotation)a new object, or nil
Game.widget(class)the live widget of that class on screen, or nil
Game.loaded()true once the world around the player finished loading
Game.time(), Game.log(fmt, ...)seconds; a line in fusion_kit.log

Objects also have the game's own fields and methods: zombie.Health.MaxHealth, widget:OnRandomNameSet("Fusion Scout"). Use Game.get when a field might be missing.

The sandbox

The script sees Router, Game, and Lua's safe libraries (math, string, table, utf8, os.clock/time/date, pairs, pcall, ...). It can't open files, run programs, load other code (require, load) or use the kit's own functions. It can do anything the game itself can do through its objects: that's what reflection is (trust tiers, step 6.2, decide who may publish what).

Language and types

Router scripts are Lua 5.4 (on Unreal, UE4SS's own Lua), not Luau like addons: docs/decisions/0004-router-api.md says why. Keep to the common subset when you can. docs/types/router-api.lua has the types for lua-language-server, so an editor or lua-language-server --check catches a wrong function name or argument before the game starts.

Installing a router into a game

Fusion's one-click setup copies the kit and the router (its router.toml and router.lua) into the game ([router.setup], see block-manifest.md). For development: kits\unreal\install.ps1 -Game vein [-Dev].

Router Studio

Router Studio (F4 in a session, or "Router Studio" in the main menu) writes the same [block.bind] data by pointing at the running game's objects: "This is the player", "This map", "These are NPCs" (with health fields found by clicking through an object's fields). "Test live" has the kit try the router on the running game, "Apply live" makes the game use it now (the kit's reload). fusion-cli game asks the same questions from the command line, e.g. fusion-cli game find zombie --game-id vein.

Not in v0 yet

  • Playing as the character on the Unity kit (no common way to move a Unity game's player).
  • Hooks on the game's own functions ("when a zombie dies").
  • Reloading router.lua by itself without restarting the game (step 5.7); a new router.toml reloads live (Router Studio's "Apply live"), and runs router.lua again with it.
  • router check running the types and these checks before the game starts (step 5.7).

On the Unity kit (step 7.1, v0)

The Unity kit (kits/unity, a MelonLoader mod; kits/unity/README.md) reads the same [block.bind] data. What it does by itself:

Unity kit defaultChange it with
Getting into the worldopens bind.map (a scene name) once Fusion asks, if the game is elsewherebind.map
The picturethe game's main 3D camera's final picture (with the game's own post effects), drawn from Fusion's camerabind.camera
Camera rigsthe camera seen rendering the world with the most layers is moved to Fusion's camera (games that render a hidden world camera themselves)
Fusion's originthe ground under the game's own view, facing where it looksbind.anchor
Collisionvertical physics traces around Fusion's camera, if the world block gives collision
Entitiesbind.entity rules (class = a component's class name, health = a field path on it); with none, anything with a NavMeshAgentbind.entity
Window focusthe game keeps running when its window isn't in front (even if it tries to turn that off)

| The player | bind.player (a component class), or the GameObject tagged Player | bind.player | | In the world | the scene is bind.map (if set) and no scene loaded for a second | Lua enter, ready | | Spawning | a copy of the spawn prefab of the entity kind Fusion asks for (a prefab or object the game has loaded, by name, or a Resources path), on the ground there | bind.entity.spawn, Lua spawn | | Teleport | moves the player (its CharacterController off while it moves) | | | Actions | remove, set_health (the rule's health field), move_to and stop (the entity's NavMeshAgent) | Lua actions |

router.lua on Unity runs in the kit's own Lua 5.4 (inside fusion_unity.dll), with the same Router.block checks and sandbox. Game.* reaches the game's objects through Il2CppInterop's reflection: o.field, o.field = v and o:Method(...) work on any component (panel:IsEnabled(), ai.m_CurrentHP), o.gameObject.name follows properties, list[1] reads an element (counting from 1). Enums arrive as their names ("Wolf"). Positions are { X, Y, Z } (also x, y, z), Unity's own meters. Unity has no player controllers or console (Game.controller() is nil, Game.console() false), and adds:

Function
Game.static(class, name)a static field or property (singletons: Game.static("GameManager", "m_Instance")), or a function calling a static method
Game.load_scene(name)opens a scene
Game.component(o, class)a component on the same GameObject (or its children)
Game.position(o)where an object is
Game.members(o)what any object has (fields and methods), for exploring a game with fusion-cli game lua

Game.class(name) is what to spawn: a loaded prefab or object by name, or a Resources path.

Try any of it on the running game: fusion-cli game lua "Game.find('Panel_MainMenu'):IsEnabled()" --game-id thelongdark runs the snippet in the router's sandbox and prints what it returns.

Not yet on the Unity kit: player_follows_camera, possessing a Character block.