Routers on the guest SDK: your plugin or program speaks Fusion
For games no kit covers (Unity Mono, Bethesda, Source, custom engines) through your own plugin in the game's mod loader (BepInEx, MelonLoader, OWML, SKSE/F4SE/NVSE, a Source server plugin, an injected DLL), and for open-source games. The SDK is one implementation with three faces:
- C#:
FusionGuest.cs(one file; C# 7.3, .NET Framework 3.5+, nounsafe):Fusion.Guest. - C/C++:
fusion_guest.h+fusion_guest_c.dll:fusion_guest_*. - Rust: the
fusion-guestcrate.fusion-cli whereshows where they are. Shipfusion_guest_c.dllnext to your plugin's DLL.
Porting a passthrough mod¶
| In the mod | In Fusion | Do |
|---|---|---|
its link (shared memory such as Local\SkyCraft_v1, UDP, WebSocket) | the SDK | delete; call the SDK |
| a fork of SkyCraft's Fabric mod | minecraft.player (Fusion's) | delete; your scenario uses it |
| the game draws the other game's blocks | Fusion draws every game's layers | delete |
| the game's camera drives Minecraft's | Fusion's camera drives every game | read camera(), move your camera there |
| raycasts into a collision field for Minecraft | send_triangles / send_boxes (16 m chunks done for you) | keep the raycasts, send the result |
| NPC proxies | send_entities (a block that gives entities) | keep the NPC finder, send a list |
| "host paused" | Sleep / Wake events | map to your pause |
| frame sync, fallback when the other game hangs | Fusion's compositor and heartbeats | delete |
| install scripts | [router.setup] (below) | turn them into steps |
router.toml¶
fusion-cli router new <id> --name "<Name>" --kit sdk --steam-app <N> writes it. Then:
[router]
id = "mygame"
name = "My Game"
version = "0.1.0"
game_exe = "MyGame.exe"
units = { length = "m", up_axis = "y", health_max = 100 } # you send Fusion's space
[router.link]
protocol = "fusion"
game_id = "mygame" # what your plugin passes to Connect / TryConnect
[router.launch]
steam_app = 123456
[[block]]
id = "mygame.world"
name = "My Game's World"
kind = "world"
gives = ["layer:color+depth", "collision"]
cost = { ram_gb = 3.0, gpu = "medium", must_stay_awake = true }
[[block]]
id = "mygame.npcs"
name = "My Game's NPCs"
kind = "npc_group"
gives = ["entities"]
actions = ["remove", "set_health"]
cost = { ram_gb = 0.0, gpu = "none" }
[router.setup] # one click for players: check the loader, copy your plugin
installed_when = "{game}/BepInEx/plugins/MyGameFusion/MyGameFusion.dll"
[[router.setup.require]]
name = "BepInEx 5"
check = "{game}/BepInEx/core/BepInEx.dll"
guide = "Install BepInEx 5 (x64) into the game's folder and start the game once."
url = "https://github.com/BepInEx/BepInEx/releases"
[[router.setup.step]]
copy = "{router}/plugin" # your built plugin + fusion_guest_c.dll, in the router folder
to = "{game}/BepInEx/plugins/MyGameFusion"
The plugin's loop (C#; C has the same calls)¶
Fusion.Guest guest;
void Update() // the plugin's per-frame hook
{
if (guest == null && (guest = Fusion.Guest.TryConnect("mygame")) == null) return;
foreach (var e in guest.Update()) // once per frame
{
switch (e.Kind)
{
case Fusion.EventKind.Sleep: Pause(true); break;
case Fusion.EventKind.Wake: Pause(false); break;
case Fusion.EventKind.Action: guest.Reply(e.RequestId, Act(e.Entity, e.Name, e.Args)); break;
case Fusion.EventKind.Spawn: case Fusion.EventKind.Teleport: guest.Reply(e.RequestId, false); break;
case Fusion.EventKind.Query: guest.Answer(e.RequestId, false, "no questions"); break;
}
}
if (guest.Ended) { guest.Dispose(); guest = null; return; }
guest.SendEntities(npcs); // List<Fusion.Entity>: Id, Kind "mygame.npc", Name, Feet, Health...
guest.SendTriangles(groundNearCamera); // 9 floats per triangle, Fusion's space
}
Fusion's space: meters, Y up, right-handed, -Z forward, around an origin you pick near where play starts. Unity: (x, y, -z), yaw -unityYawDegrees in radians. Z-up right-handed engines (Source in inches, Bethesda in ~1.43 cm units): (x, z, -y) * metersPerUnit, yaw = game yaw − 90°. Check with F7 (collision) and F8 (entities) in Fusion.
The picture¶
guest.TryGetCamera(out cam); move your world camera to it before it renders (position:CameraToWorld[12..14]; forward:-column 2; up:column 1;FovYDegrees), and give it back after.- Render into a texture of
RenderSize, and depth into a one-channel float texture. - GPU (Direct3D 11): from the render thread,
fusion_guest_d3d11_publish(g, ctx, color, depth, &mode, flags, cam.camera_id); from Unity's main thread,id = guest.D3D11Queue(color.GetNativeTexturePtr(), depth.GetNativeTexturePtr(), Fusion.DepthMode.Device(near, far, SystemInfo.usesReversedZBuffer), Fusion.PictureFlags.BottomUp | Fusion.PictureFlags.Opaque, cam.CameraId)thencb.IssuePluginEvent(Fusion.Guest.D3D11RenderEvent, id); Graphics.ExecuteCommandBuffer(cb). -1 →guest.Error; use the CPU path. - CPU (anything else):
guest.Picture(w, h, true)once per size, thenguest.PublishFrame(rgba8, depthMeters, cam.CameraId)(alpha below 128 = nothing there).
A character players play as (optional)¶
Block: kind = "character", needs = ["input"], gives = ["pose"]. Each frame: guest.TryGetInput(out input); while input.Possessed != 0, move with MoveX/MoveY and turn to Yaw/Pitch through the game's own movement; guest.SetPose(id, x, y, z, yaw) and guest.SetView(eyeX, eyeY, eyeZ, 0, 0, Fusion.ViewFlags.InWorld). To walk on another game's World, add needs = ["collision"] and read Collision events with TryGetCollision.
Check¶
fusion-cli router check routers/<id>, then fusion-cli router conform routers/<id> --start (the plugin installed, Fusion closed). Working examples: example-sdk.router.toml and Fusion's sdks/csharp/example (a C# world), tools/c-world (C).
Sharing a router whose package carries a DLL needs a verified author (Fusion's docs/trust.md).