# Fusion Unity kit (v0)

Makes a Unity game a Fusion guest. Built in BUILD.md step 7.1 with **The Long Dark** (Unity 6,
IL2CPP, Direct3D 11) as the first game. It shows the game's world inside Fusion, drawn by the
game from Fusion's camera with its depth, sends its collision and characters, and answers
Fusion's tools (Router Studio, `fusion-cli game`).

It has two parts:

| Part | Where | Does |
|---|---|---|
| `FusionKit.dll` (C#, a MelonLoader mod) | `mod/` | Everything that needs Unity's objects: picking the game's cameras, moving the camera that draws the world to Fusion's camera while it renders, reading the picture and depth back, physics traces, finding characters, answering questions through Il2CppInterop's reflection |
| `fusion_unity.dll` (Rust) | `src/` | Everything else, through the guest SDK (`crates/fusion-guest`): the Fusion protocol, Fusion's space vs Unity's (`src/units.rs`), read-back pixels into Fusion's picture (rows, depth in meters), the collision sampler (`fusion_guest::sampler`, shared with the Unreal kit), entity lists, router files as JSON |

The router's `router.toml` and `router.lua` sit in the game's `UserData\FusionKit\` folder; the kit
reads its `[block.bind]` data and runs its script ([`docs/router-api.md`](https://steonmod.com/docs/router-api), "On the Unity kit").

## Router scripts

`router.lua` runs in Lua 5.4 inside `fusion_unity.dll` (`src/script.rs`, mlua), with the kit's
Router API module (`lua/router_api.lua`: `Router.block` and its checks, the sandbox, `Game.*`).
`Game.*` asks the C# mod (`mod/Script.cs`) through one callback: an operation and JSON
arguments in, a JSON value out. Objects cross as handles the mod keeps alive until Lua's garbage
collector lets go of them; `o.field`, `o:Method(...)` and statics (`Game.static`) are
Il2CppInterop reflection on the game's own proxies. A call from the kit may take 0.5 s at most
(2 s to load), and a script 64 MB; an endless loop is stopped with a message, not a frozen game.
A script that doesn't load stops the kit's play (questions are still answered, so Router Studio
can fix it). `fusion-cli game lua "<code>" --game-id <id>` runs a snippet on the live game.

The crate is its own Cargo workspace (`kits/unity/Cargo.toml`): mlua's Lua 5.4 can't be built
together with `fusion-lua`'s Luau. `cargo test --manifest-path kits/unity/Cargo.toml` runs its
tests (the router API with a pretend game, the sandbox, the time limit).

## How a frame gets to Fusion

1. **Two cameras** (often the same one). The *output camera* is the one whose picture reaches the
   screen (`bind.camera`, or the game's main 3D camera); the kit makes it draw into its own
   texture at the size Fusion asks for. The *view camera* draws the world: the output camera if
   it sees anything, or else the camera the kit sees rendering with the most layers. Some games
   render a hidden world camera with their own code and composite it (The Long Dark's
   `CameraGlobalRT` renders `FPSCamera`); the kit finds it through `Camera.onPreCull`.
2. **Camera.onPreCull** (the view camera is about to render): the kit moves it to Fusion's camera
   (place, rotation, field of view). If the game sets the camera's matrices itself, the kit sets
   them too: Fusion's view, and the game's own projection widened or narrowed to Fusion's field of
   view (so borders and jitter the game adds stay).
3. A **command buffer** on the view camera copies its depth (deferred: the resolved depth buffer;
   forward: the depth texture) into a float texture while it renders.
4. **End of the frame**, on **Direct3D 11 when the game and Fusion use the same GPU** (the usual
   case): a command buffer with `IssuePluginEvent` runs `fusion_unity.dll` on Unity's render
   thread after the frame's drawing. It copies both textures into a free one of Fusion's shared
   layer textures with a small compute shader (rows turned over, Unity's reversed depth into
   meters) and signals the shared fence (`fusion_win::d3d11`: the textures are made by a D3D12
   device of the kit's own on the game's GPU and opened on the game's D3D11 device, so Fusion
   opens them like any other kit's). No read-back, nothing waits.
5. **Otherwise** (another graphics API, another GPU than Fusion's, or the copy failed; or
   `FUSION_CPU_PICTURES=1` in the game's environment): the kit asks for both textures back
   (`AsyncGPUReadback`, no stall), and a few frames later `fk_publish` turns them into a Fusion
   CPU picture with depth, stamped with the camera it was drawn from.
6. Either way the game gets its cameras back as they were, so its own code never sees them moved.

On The Long Dark: the GPU path is 1.0 frame (17 ms) behind Fusion's camera, the read-back 3.7
frames (62 ms); both about 55 pictures a second at 1280x720.

## Collision, entities, questions

- **Collision**: up to 256 vertical `Physics.Raycast`s a frame where the shared sampler asks (a
  0.5 m grid in 16 m columns around Fusion's camera, nearest first, out to 40 m), triggers left
  out. Finished columns are sent as Collision Hub chunks.
- **Getting into the world**: once the scene is `bind.map` (if set) and settled, the router's
  `enter`/`ready` decide. When the game's active scene changes later (another region, an
  interior; not a scene added to it), the kit gets into the world again and picks a new origin.
- **Entities**, 4 times a second within 150 m of Fusion's camera: the router's `bind.entity` rules
  (a component class name; `health` / `max_health` as field paths on it), or with no rules every
  `NavMeshAgent`. Their box is their collider's bounds (or their renderer's).
- **Questions** ([`docs/protocol.md`](https://steonmod.com/docs/protocol)): `info`, `classes`, `objects`, `inspect`, `find`, `test`,
  `reload`. `inspect` lists every component of a GameObject with its fields (the proxies'
  properties), plain values, and the game's own methods (what a `router.lua` can call).
- **Sleep**: `Time.timeScale` 0, audio paused, the output camera off.
- **Spawn**: the router's Lua `spawn`, or a copy of the `bind.entity` rule's `spawn` prefab (a
  prefab the game has loaded, by name, or a `Resources` path) on the ground there. **Teleport**
  moves the player (`bind.player`, or the GameObject tagged `Player`). **Actions**: the router's
  Lua `actions`, or the kit's own `remove`, `set_health`, `move_to`, `stop`.
- **Focus**: Unity pauses an unfocused game; the kit turns `Application.runInBackground` on and
  patches its setter (Harmony) so the game can't turn it off again.

## Build, install, test

MelonLoader must be in the game (step 1.1) and have run once (it generates the proxy assemblies
the mod compiles against).

```powershell
kits\unity\install.ps1 -Game thelongdark       # game closed; builds fusion_unity.dll and the mod (against the game's files)
# start the game (Steam, or tld.exe --melonloader.hideconsole), then with Fusion closed:
target\debug\fusion-cli.exe host-test --game-id thelongdark --eye 0,1.7,0 --look 0,1.7,-10 --depth-fade 60 --out <dir>
target\debug\fusion-cli.exe router conform routers\thelongdark
target\debug\fusion-cli.exe game info --game-id thelongdark
```

The kit logs to MelonLoader's console and to `UserData\FusionKit\fusion_kit.log`, with a report
every 10 s (frames, traces, entity lists, frame rate, which cameras rendered how often).

Don't click into MelonLoader's console window while the game runs: Windows' console selection
pauses every program writing to it, the game included (`--melonloader.hideconsole` hides it).

## Known limits (v0)

- **GPU pictures on Direct3D 11 only.** Direct3D 12 and Vulkan Unity games use the read-back
  path (their native texture pointers aren't D3D11 textures; the kit checks).
- **The map waits for the game's boot**: the kit opens `bind.map` once the game has left its
  first scene, or after 30 s in it (The Long Dark stays in its `Empty` boot scene: 30 s). A map
  opened from a boot scene never finished loading.
- **The Long Dark's depth doesn't match its picture**: it renders its world camera several times
  a frame with different projections (an SSAO pass with a wider view, among others), and the kit
  copies the depth of the last one. Conformance passes, with the note that its depth doesn't match
  its collision.
- **The Long Dark, second session**: after a first Fusion session ends, its cameras' field of
  view reads NaN in the next session in the same game run, and the kit falls back to the weapon
  camera. Restart the game between Fusion sessions (Fusion starts it per session anyway).
- If Fusion's window has another shape than the game's, the picture is stretched (the game
  renders at its own aspect).
- Collision is a height surface from above, like the Unreal kit's: nothing under roofs and
  overhangs. Unity's colliders could be read exactly later (primitives, readable meshes, terrain).
- No possess (Character blocks) and no BepInEx adapter yet.
- A copy of a prefab may need the game's own spawner to come alive (The Long Dark's wolf prefab
  copies arrive with their animal part inactive): a router's Lua `spawn` can call the game's.
