0004: The Router API: data first, Lua 5.4 for router scripts
Date: 2026-10-04
Context¶
Every kit needs the same way to say "this is the game's player, these are its NPCs, this is how to get into its world". Before 5.2, VEIN's router was a 227-line Lua module with its own contract (update(ctx), describe, spawn), and about a third of it was generic Unreal code (traces, finding open ground, widget lookup). Agents writing routers pay for every line (docs/agent-efficiency.md), so the API has to make most routers data, and the rest short.
VISION.md §15 also left open which Lua runs router scripts on Unreal: Luau (what Fusion's addons use) or UE4SS's built-in Lua 5.4.
Decision¶
- Data first. A router's
router.tomlgets[block.bind]: the world's map and player class,[[block.bind.entity]]rules (class or base class, kind, name, health field paths, team, the asset to spawn). Fusion checks it with the rest of the manifest (unknown fields are errors). The kit receives the whole manifest as a Lua table (RouterManifest::lua_source, throughFusionKit_Router()), so there is no second copy to keep in sync. - Kit defaults from engine conventions. The Unreal kit does everything it can by itself: player controller 0's pawn is the player, it opens the bound map from a main menu, waits for World Partition and ground, picks open ground for Fusion's origin, hides the player and makes it unhurtable, lists characters, spawns bound assets feet-first, and keeps the game running when its window isn't in front (
t.IdleWhenNotForeground 0). Ports a block gives turn the kit's work on (collision→ traces,entities→ lists). - Lua only for what data can't say, through
Router.block "<id>" { ... }(functions forenter,ready,anchor,hidden,describe,spawn.<kind>), checked when the script runs: an unknown field, wrong type or unknown block id fails with the script's line and the list of what's allowed. Game.*is the reflection API, with the same names on every kit:find,find_all,object,class,get/setby field path,call,is_a,player,world,map,console,trace,spawn,widget,loaded,time,log.- Sandbox. The script runs in its own environment:
Router,Gameand Lua's safe libraries. No files, no OS, norequire/load, no UE4SS globals. - Router scripts stay on Lua 5.4 (UE4SS's own on Unreal). The Unity kit (M7) embeds Lua 5.4 too, so router scripts are one dialect everywhere. Addons stay Luau (ADR 0002). Types come from LuaLS annotations:
docs/types/router-api.luais a---@metadefinition file thatlua-language-server --checkreads (wired intorouter checkin 5.7).
Why Lua 5.4, not Luau¶
- On Unreal, reflection lives in UE4SS's Lua: every
obj.Fieldandobj:Function()goes through UE4SS's bindings. Luau there means re-binding all of Unreal's reflection to a second VM ourselves, without UE4SS's source (ADR 0003): a large job for no new ability. - Typed checks were the reason to prefer Luau (
docs/agent-efficiency.md§3). LuaLS annotations give the same class of checks (unknown fields, wrong argument types, undefined globals) from a definition file, and router scripts are now short. - Router scripts are written by router makers, not players; Luau's player-facing benefits (its sandboxing, gradual types in the editor) matter more for addons, which stay Luau.
- The common subset of Lua 5.4 and Luau is large; the API itself is dialect-neutral.
Consequences¶
- VEIN's router is now 25 lines of data in
router.tomland 36 lines of Lua (only its character creation screen), down from 227 lines of Lua. - Reflection is reflection: a router can still call any of the game's own functions. The sandbox keeps it away from files and the OS; a deny list of dangerous game functions (opening URLs and the like) belongs with trust tiers (6.2).
- The kit's checks run when the script loads in the game.
router check(5.7) will run them, plus the LuaLS types, before the game starts.