SteonMod
View as Markdown

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-guest crate (crates/fusion-guest).
  • Example: tools/sdk-world, a crate yard drawn by a small CPU ray tracer (about 200 lines), with its router routers/sdk-example and scenario scenarios/sdk-crate-yard.toml.
  • Reference user: tools/fake-guest, Fusion's test game, which shares GPU textures.
  • Bevy: the fusion-guest-bevy plugin (sdks/bevy, its own workspace): add FusionGuestPlugin, mark a camera, colliders and entities with components (sdks/bevy/README.md; example sdks/bevy/examples/world.rs, router routers/bevy-example, scenario bevy-yard).
  • C and C++: crates/fusion-guest-c, a C API over the same Rust SDK (one implementation, so both behave the same): link fusion_guest_c.dll (import library fusion_guest_c.dll.lib) and include include/fusion_guest.h. Example: tools/c-world/c_world.c (a yard drawn by a small C ray tracer), router routers/c-example, scenario c-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, no unsafe). Example: sdks/csharp/example (cs-world, the same yard drawn by C#), router routers/cs-example, scenario cs-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 hasSDK callBlock port
ground and wallssend_collision(&triangles) or send_collision_boxes (cut into 16 m chunks for you; chunks you no longer fill are cleared)gives = ["collision"]
NPCs, vehicles, cratessend_entities(&list) a few times a second (the whole list each time)gives = ["entities"]
a picturepicture(w, h, depth) + frame() / publish(); or gpu_picture for shared D3D12 texturesgives = ["layer:color+depth"]
a character the player playsset_pose, set_view (eyes); read input() while possessed()kind = "character", needs = ["input"]
things Fusion asksEvent::Action / Spawn / Teleport → reply; Event::Query → answeractions = [...]
events for Luadamage, died, spawned, custom(name, data)gives = ["events:damage"]
Fusion's collision around youEvent::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, one f32 per 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 from guest.camera() so your picture lines up with Fusion's view, and set frame.camera_id to 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 on gpu_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 Present hook): 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_queue on the main thread, then the render thread calls fusion_guest_d3d11_render_event() with the id (Unity: CommandBuffer.IssuePluginEvent). FusionDepth says how the depth is stored: the depth buffer's own 0..1 values (kind 0, with near/far planes and reversed Z), or distances (kind 1, times scale = meters). -1 means the GPU path can't work here (another GPU than Fusion's): fusion_guest_error says why, and CPU pictures still do. Tested by sdk-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.