Guest SDK: link Fusion into your game (v0)
For programs whose source you have (open-source games, Rust rewrites, recomps, your own game), and for your own plugin inside a closed game (a BepInEx or MelonLoader plugin, an SKSE plugin, a Source server plugin, an injected DLL). They link Fusion's protocol directly, so they need no engine kit. The game's own code still runs every mechanic; the SDK only tells Fusion what the game has and shows its picture. (Closed games on Unreal or Unity IL2CPP can use an engine kit and a router script instead: router-api.md. Coming from a passthrough mod: porting.md.)
- Rust: the
fusion-guestcrate (crates/fusion-guest). - Example:
tools/sdk-world, a crate yard drawn by a small CPU ray tracer (about 200 lines), with its routerrouters/sdk-exampleand scenarioscenarios/sdk-crate-yard.toml. - Reference user:
tools/fake-guest, Fusion's test game, which shares GPU textures. - Bevy: the
fusion-guest-bevyplugin (sdks/bevy, its own workspace): addFusionGuestPlugin, mark a camera, colliders and entities with components (sdks/bevy/README.md; examplesdks/bevy/examples/world.rs, routerrouters/bevy-example, scenariobevy-yard). - C and C++:
crates/fusion-guest-c, a C API over the same Rust SDK (one implementation, so both behave the same): linkfusion_guest_c.dll(import libraryfusion_guest_c.dll.lib) and includeinclude/fusion_guest.h. Example:tools/c-world/c_world.c(a yard drawn by a small C ray tracer), routerrouters/c-example, scenarioc-yard. See "C and C++" below. - C#:
sdks/csharp/FusionGuest.cs, one file over the same C API, for BepInEx, MelonLoader and OWML plugins or any .NET program (C# 7.3, .NET Framework 3.5 and newer, nounsafe). Example:sdks/csharp/example(cs-world, the same yard drawn by C#), routerrouters/cs-example, scenariocs-yard. See "C#" below.
All of them pass the same checks: fusion-cli router conform (picture, lag, collision, entities, sleep and wake) and Fusion's guest_world_test (the player stands on the game's floor, its entity is listed and moves).
The loop¶
use fusion_guest::{Entity, Event, Guest};
let mut guest = Guest::connect("mygame")?; // waits until Fusion plays a scenario with your blocks
guest.send_collision_boxes(&[([0.0, -0.25, 0.0], [20.0, 0.25, 20.0])]); // a 40 m floor
loop {
for event in guest.update() { // heartbeat, sleep/wake, frame cap: handled here
match event {
Event::Action { request_id, name, .. } => guest.reply(request_id, name == "stop"),
Event::Spawn { request_id, .. } => guest.reply(request_id, false),
_ => {}
}
}
if guest.ended().is_some() { break; } // Fusion closed, or stopped answering for 10 s
if guest.sleeping() { continue; } // Fusion put the game to sleep: don't simulate
let (w, h) = guest.render_size((640, 360));
guest.picture(w, h, true)?; // a CPU picture with depth (once per size)
if let (Some(cam), Some(mut frame)) = (guest.camera(), guest.frame()) {
// draw from Fusion's camera: cam.camera_to_world, cam.fov_y_degrees
let (color, depth) = frame.color_and_depth();
// ... RGBA8 (alpha 0 = nothing), depth in meters in front of the camera ...
frame.publish();
}
guest.send_entities(&[Entity::new(1, "mygame.npc", "Bob", [0.0, 0.0, -3.0])]);
guest.pace(frame_start, 60.0); // keeps to Fusion's frame cap
}
What goes where¶
| Your game has | SDK call | Block port |
|---|---|---|
| ground and walls | send_collision(&triangles) or send_collision_boxes (cut into 16 m chunks for you; chunks you no longer fill are cleared) | gives = ["collision"] |
| NPCs, vehicles, crates | send_entities(&list) a few times a second (the whole list each time) | gives = ["entities"] |
| a picture | picture(w, h, depth) + frame() / publish(); or gpu_picture for shared D3D12 textures | gives = ["layer:color+depth"] |
| a character the player plays | set_pose, set_view (eyes); read input() while possessed() | kind = "character", needs = ["input"] |
| things Fusion asks | Event::Action / Spawn / Teleport → reply; Event::Query → answer | actions = [...] |
| events for Lua | damage, died, spawned, custom(name, data) | gives = ["events:damage"] |
| Fusion's collision around you | Event::Collision (a chunk, replacing that chunk) | needs = ["collision"] |
Everything is in Fusion's space: meters, Y up, right-handed, -Z forward. Convert at the edge of your game, the way kits do. Say units = { length = "m", up_axis = "y" } in your router.
C and C++¶
The header's comment has the whole loop. The same calls as the Rust SDK, with a handle:
FusionGuest *g = fusion_guest_connect("mygame"); // or fusion_guest_try_connect each frame
fusion_guest_send_boxes(g, boxes, n); // n boxes: center xyz, half size xyz
while (!fusion_guest_ended(g)) {
uint32_t n = fusion_guest_update(g); // then fusion_guest_event(g, i, &e)
FusionCamera cam; uint8_t *color; float *depth;
if (fusion_guest_camera(g, &cam) && fusion_guest_picture(g, w, h, 1) == 0
&& fusion_guest_frame(g, &color, &depth)) {
/* draw from cam into color and depth */
fusion_guest_publish(g, cam.camera_id);
}
fusion_guest_send_entities(g, list, count); // a few times a second
}
fusion_guest_close(g);
Event names (FusionEvent.name) stay valid until the next fusion_guest_update.
The C API also has everything for other blocks: a character (fusion_guest_input, fusion_guest_set_pose, fusion_guest_set_view with FUSION_VIEW_IN_WORLD), collision from other games around it (FUSION_EVENT_COLLISION + fusion_guest_collision), events for Lua (fusion_guest_damage, _died, _spawned, _custom), and GPU pictures for Direct3D 11 games (below). A handle may be used from two threads (a game's main thread and its render thread): every call holds the handle's lock.
C¶
var guest = Fusion.Guest.Connect("mygame"); // or TryConnect each frame from a plugin
guest.SendBoxes(new float[] { 0, -0.25f, 0, 20, 0.25f, 20 });
while (!guest.Ended)
{
foreach (var e in guest.Update())
if (e.Kind == Fusion.EventKind.Action) guest.Reply(e.RequestId, false);
Fusion.Camera cam;
if (guest.TryGetCamera(out cam) && guest.Picture(640, 360, true))
guest.PublishFrame(rgba8, depthMeters, cam.CameraId); // or D3D11Queue (below)
}
guest.Dispose();
The same calls as C, in C# shapes: Update() returns the events, SendEntities takes a list of Fusion.Entity, TryGetCollision(event.Index, ...), TryGetInput, SetPose, SetView. Windows must find fusion_guest_c.dll: put it next to your plugin's DLL or the game's exe.
Pictures¶
- CPU picture (
picture): each frame you fill RGBA8 pixels (sRGB, top row first; alpha below 128 means "nothing here", so Fusion's world shows through) and, with depth, onef32per pixel: the distance in front of the camera in meters. With depth, Fusion draws it in 3D, sorted against Godot's props and other games; without, flat on top like an interface. Draw fromguest.camera()so your picture lines up with Fusion's view, and setframe.camera_idto the camera you drew from (Fusion measures lag with it). Any size works (it's stretched to the screen): smaller is cheaper. - GPU picture (
gpu_picture,gpu_acquire,gpu_publish): shared D3D12 textures and a fence, no copies (protocol.md, "Picture layers"). Create your device ongpu_luid(). - GPU picture from Direct3D 11 textures (C and C#; most injected games): hand Fusion your own color and depth textures each frame and the SDK copies them into Fusion's shared textures on your GPU (the same copy the Unity and Unreal kits use). From the render thread (a
Presenthook):fusion_guest_d3d11_publish(g, context, color, depth, &mode, flags, camera_id). From an engine that draws on its own render thread (Unity):fusion_guest_d3d11_queueon the main thread, then the render thread callsfusion_guest_d3d11_render_event()with the id (Unity:CommandBuffer.IssuePluginEvent).FusionDepthsays how the depth is stored: the depth buffer's own 0..1 values (kind0, with near/far planes and reversed Z), or distances (kind1, timesscale= meters). -1 means the GPU path can't work here (another GPU than Fusion's):fusion_guest_errorsays why, and CPU pictures still do. Tested bysdk-world --d3d11(conform_d3d11: 1.7 frames behind Fusion's camera, depth matching its collision).
Your router¶
[router]
id = "mygame"
name = "My Game"
version = "0.1.0"
units = { length = "m", up_axis = "y", health_max = 100 }
[router.link]
protocol = "fusion"
[router.launch]
exe = "{game}/MyGame.exe"
[router.detect]
paths = ["%LOCALAPPDATA%/Programs/MyGame/MyGame.exe"]
[[block]]
id = "mygame.world"
name = "My Game's World"
kind = "world"
gives = ["collision", "layer:color+depth", "entities"]
Then fusion-cli router check routers/mygame, and play it from a scenario.