Migrating from Photon Fusion 2.1 to Lattice¶
fusion2lattice is an assisted migration tool that ports a Unity game from
Photon Fusion 2.1 to Lattice. It rewrites the
high-confidence, mechanical parts of the Fusion API surface to their Lattice equivalents, and
produces a per-file report telling you exactly what still needs human work and how to do it.
This is assisted migration, not a one-click port
The tool does not produce a finished, compiling port. It produces a structured starting point plus a precise to-do list. The hard parts of a netcode port — prediction / tick timing, the prefab+transform → type+POD-state spawn model, RPC argument marshalling, and input routing — do not have clean 1:1 mappings. The tool reports them; it does not solve them. Plan on reviewing and hand-finishing the output before it compiles or runs.
What the tool actually does¶
- Analyzer + codemod (
convert) — scans a Unity C# project directory, rewrites the high-confidence Fusion → Lattice mappings, and writes the converted.csinto a separate output tree. Your input is never modified. Every change and every un-mappable construct is recorded as a finding. - Migration report — a
*.report.md/*.report.jsonper file plus a top-levelMIGRATION-SUMMARY.*. Each finding carries the source line, the Fusion construct, the Lattice result, and concrete guidance, bucketed by confidence (Auto / Review / Manual). - Guide (
guide) — emitsFUSION-MIGRATION-GUIDE.md: the full concept-mapping table and per-feature notes. It is generated from the same mapping table the codemod uses, so the guide can never drift from the tool's behaviour.
The codemod uses Roslyn (Microsoft.CodeAnalysis.CSharp), not regex: it parses each file to a
syntax tree and rewrites it with a CSharpSyntaxRewriter, so it matches real attributes, base
lists, member access and invocations, and preserves your formatting and trivia.
The rewrite is syntactic, not semantic
There is no Photon Fusion SDK in this environment, so the tool reasons about names, attributes and call shapes — exactly what you read off the page — not resolved Fusion symbols. A confusingly-named local symbol can produce a false positive. That is what the report is for: review every finding against your actual code.
Honest limitations — read before trusting the output¶
- Output does not compile as-is. It needs the Lattice Unity package (
com.lattice.netcode) referenced and every Review/Manual item resolved. Each generated file starts with a banner that says exactly this. - No real-Fusion-SDK validation. The tool was developed against the Lattice target API
(
lattice.h, the Unity binding, and design 05 — Engine Integration) plus a synthetic Fusion sample. It has not been validated against the real Photon Fusion SDK, and the converted output has not been compiled against either SDK. - The shipped binding is thinner than the docs' ideal in places. Design 05
shows a richer intended idiom (typed
GetInput<T>, a two-arg[Rpc(source, target)]). The shippedcom.lattice.netcodebinding exposes a singleLatticeRpcTargetand no typedGetInput. The tool deliberately targets the real binding and flags the gap, rather than emitting calls that don't exist yet.
Prerequisites¶
- .NET 8 SDK (the tool is a
net8.0console app). - The tool itself — under
tools/fusion2lattice/in the Lattice repo. - For the output: the Lattice Unity package (
com.lattice.netcode) to reference once you start fixing up the converted files.
Build & run¶
The tool ships with filesystem-safe wrapper scripts. The repo lives on a mounted filesystem that
rejects the chmod/utime calls dotnet makes in obj//bin/, so the build redirects
artifacts to /tmp/f2l-build and runs the tool as a DLL via dotnet (the .csproj sets
UseAppHost=false). The wrappers handle all of this:
cd tools/fusion2lattice
./build.sh # builds to $BUILD_DIR (default /tmp/f2l-build)
./run.sh convert <UnityProjectDir> <outDir> [--kcc <adapter|report>] # scan + rewrite + report
./run.sh guide FUSION-MIGRATION-GUIDE.md # (re)generate the concept guide
convert takes an optional --kcc <adapter|report> flag controlling how a detected Fusion KCC
processor is handled:
adapter(the default) — detect + auto-convert eachKCCProcessor/NetworkKCCProcessorinto a LatticeIKccProcessor(base type,Processsignature, andKCCData.X → data.Xmember mapping). Collision/sweep calls and movement feel are flagged for you to finish; see Porting a Fusion KCC + physics game §1.report— detect only; emit the KCC constructs as Manual findings so you port them by hand.
(convert also takes --exclude <glob> and --no-default-excludes; see ./run.sh with no args for
the full usage.)
On a normal filesystem you can skip the wrappers and use dotnet run -- convert <in> <out>
directly.
convert always exits 0 even when there are Manual findings — Manual findings are expected
output, not a failure. A non-zero exit means a usage or I/O error (e.g. the input directory does
not exist, or the output path equals the input path — the tool refuses to clobber your sources).
Walkthrough: the bundled sample project¶
The repo ships a synthetic Fusion-2.1-style Unity project under sample-fusion-project/ (several
NetworkBehaviours, a KCCProcessor, an INetworkInput struct, and a launcher) that deliberately
exercises the tricky surface: [Networked]/OnChanged, [Rpc] with sources+targets,
FixedUpdateNetwork, GetInput, authority checks, Runner.Spawn, StartGame(StartGameArgs),
INetworkRunnerCallbacks, INetworkInputProvider, networked collections, AreaOfInterest, and a
KCC KCCProcessor (so you can see the --kcc adapter conversion).
Run the tool on it:
You will see:
Scanned sample-fusion-project
files with Fusion constructs: 6
auto-converted: 35, review: 20, manual: 11
KCC processors converted: 1 (--kcc adapter)
skipped: 0 file(s) (globs: **/Photon/**)
Output + reports written to .../converted-output
see converted-output/MIGRATION-SUMMARY.md
That is 66 detected constructs across 6 files: 35 auto-converted, 20 converted-needs-review,
11 manual — plus 1 KCC processor auto-converted by the default --kcc adapter mode (its base type,
Process signature, and KCCData member access are rewritten; the collision wiring is left as Review
TODOs). The converted tree and its reports are committed under converted-output/ as proof the tool
runs end-to-end.
Step 1 — read the generated banner¶
Every converted file opens with a banner. It is not decoration — it is the contract:
// =====================================================================
// GENERATED by fusion2lattice -- assisted Fusion 2.1 -> Lattice migration.
// Source: Scripts/PlayerController.cs
// THIS IS NOT A FINISHED PORT. It will not compile until you:
// 1. reference the Lattice Unity package (com.lattice.netcode),
// 2. address every REVIEW/MANUAL item in the matching .report.md,
// 3. wire spawn-by-type, input routing, and RPC payload marshalling.
// This file: 5 item(s) to review, 3 item(s) need manual work.
// =====================================================================
Step 2 — look at the before → after¶
The tool does the mechanical rewrites for you. Here are the real transforms on the sample.
Namespace and base class (Auto). using Fusion; becomes using Lattice;, and the base class
is fully qualified:
[Networked(OnChanged = ...)] → split attribute (Review). Lattice splits the change callback
into a separate [OnChanged(...)] attribute. The handler signature differs — see the
mapping table — so this is flagged Review:
RPC source+target → single target + authority guard (Review). Fusion's [Rpc(source, target)]
becomes Lattice's single-target [Rpc(LatticeRpcTarget.X)]. The source (who is allowed to call)
does not disappear — the report tells you to enforce it in the method body with a guard:
[Rpc(LatticeRpcTarget.Server)]
public void Fire(Vector3 direction)
{
// report says: guard with `if (!HasInputAuthority) return;`
// report says: Spawn(...) is left in place — register a NetworkType + build initial state
if (HasStateAuthority)
Runner.Spawn(projectilePrefab, Position, Quaternion.identity, Object.Owner);
}
Notice two things the tool did and did not do: it rewrote Object.InputAuthority →
Object.Owner (Review), but it left Runner.Spawn(...) in place — the prefab+transform spawn
model has no mechanical equivalent, so it is reported, not guessed.
Authority checks (Auto). Object.HasStateAuthority → HasStateAuthority (the Object.
prefix drops because NetworkBehaviour exposes it directly).
Input struct interface drop (Review). struct PlayerInput : INetworkInput becomes a plain
struct PlayerInput — Lattice has no INetworkInput marker; the interface is removed and the
input-routing wiring is flagged as manual:
Interface + sub-namespace removal (Review / Manual). On the launcher,
MonoBehaviour, INetworkRunnerCallbacks, INetworkInputProvider collapses to plain MonoBehaviour
(Lattice uses C# events on NetworkRunner, not a callbacks interface), and
using Fusion.Sockets; is removed (no Lattice equivalent).
Step 3 — read the per-file report¶
Each file gets a *.report.md. The buckets mean different things — read them carefully:
- Auto — mechanically rewritten; a faithful 1:1. Still compile + smoke-test, but no decision needed. (~50% of detected constructs on the sample.)
- Review — rewritten, but the semantics, timing or signature differ. You must verify each one. "Mechanically handled" is not the same as "done".
- Manual — no clean 1:1; the tool detects it, leaves it in place with a guidance comment, and reports it. You write the replacement.
Here is the head of Scripts/PlayerController.cs.report.md:
# Migration report: `Scripts/PlayerController.cs`
- Auto-converted (verify-not-required): 6
- Converted, needs review: 5
- Manual work required: 3
A representative Review finding — note it gives you the exact line, the before, the after, and what to do:
### Line 36 -- Rpc
- Source: `[Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)]`
- Fusion: [Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)]
- Lattice: [Rpc(LatticeRpcTarget.Server)]
- Guidance: Lattice's [Rpc] takes ONE routing target (Server/Owner/All/AllButOwner). The Fusion
SOURCE is enforced in code instead: Guard the body with `if (!HasInputAuthority) return;`. ALSO:
Fusion auto-marshals RPC parameters; the Lattice binding carries a byte[] payload -- serialize
args yourself in the body / OnRpc handler.
And a representative Manual finding — left in place, with the alternative spelled out:
### Line 29 -- Runner.DeltaTime
- Source: `Runner.DeltaTime`
- Lattice: (fixed dt: 1.0/tickRateHz)
- Guidance: NetworkRunner has no DeltaTime in this binding. Use the fixed step you tick with
(1.0/tickRateHz, or Time.fixedDeltaTime in the editor). Left unchanged.
Step 4 — read the summary¶
converted-output/MIGRATION-SUMMARY.md rolls everything up: counts, a per-file table, and a
"Manual work, by kind" list so you can see your biggest rocks at a glance.
| File | Auto | Review | Manual |
|---|---|---|---|
Scripts/GameLauncher.cs |
1 | 3 | 3 |
Scripts/GameManager.cs |
8 | 1 | 3 |
Scripts/PlayerController.cs |
6 | 5 | 3 |
Scripts/PlayerInput.cs |
1 | 1 | 0 |
Scripts/PlayerMovementProcessor.cs |
14 | 10 | 1 |
Scripts/Projectile.cs |
5 | 0 | 1 |
The summary also carries a KCC conversions section when --kcc adapter (the default) converts a
processor — here Scripts/PlayerMovementProcessor.cs (KCCProcessor → IKccProcessor), with the
collision routing and any unmapped KCCData members listed as Review TODOs to finish by hand (search
the converted files for TODO[fusion2lattice]).
Honest coverage estimate¶
On the common Fusion API surface the tool mechanically handles ~80% of detected constructs
(on the sample, with --kcc adapter: (35 auto + 20 review) / 66 = 83%). Read that carefully:
- "Mechanically handled" ≠ "done". The Review items are rewritten but have differing semantics/signatures you must verify — and that includes the bulk of a converted KCC processor (collision routing + unmapped members land in Review). Only the Auto items (~half of detected constructs) are faithful 1:1s.
- The percentage is over detected constructs in idiomatic Fusion code. A project leaning heavily
on networked collections,
NetworkTransform/NetworkRigidbody, prediction-timing tricks, or AOI will skew toward Manual. - It measures API-surface mechanics, not behaviour. The hard parts of a netcode port — prediction/rollback timing, the spawn model, RPC marshalling, input routing — are deliberately reported, not auto-solved.
Concept mapping table¶
This is the full Fusion 2.1 → Lattice mapping the codemod implements (Auto/Review rows are
rewritten; Manual rows are detected and reported but never rewritten). It is the same table that
backs FUSION-MIGRATION-GUIDE.md.
| Category | Fusion 2.1 | Lattice | Confidence |
|---|---|---|---|
| Base class | class X : NetworkBehaviour |
class X : Lattice.NetworkBehaviour |
Auto |
| Base class | class X : SimulationBehaviour |
class X : Lattice.NetworkBehaviour |
Review |
| Lifecycle | Spawned() / Despawned() |
Spawned() / Despawned() |
Auto |
| Tick | FixedUpdateNetwork() |
FixedUpdateNetwork() |
Review |
| Delta time | Runner.DeltaTime |
(double) dt passed to Tick / Runner fixed step |
Manual |
| Replicated property | [Networked] public int Health { get; set; } |
[Networked] public int Health { get; set; } |
Auto |
| Change callback | [Networked(OnChanged = nameof(H))] |
[Networked][OnChanged(nameof(H))] |
Review |
| Interpolation hint | [Networked] + [Interpolation] |
[Networked(Interpolated = true)] |
Review |
| Networked collections | NetworkLinkedList<T> / NetworkArray<T> / NetworkDictionary<,> |
(none — fixed POD state block) | Manual |
| RPC | [Rpc(RpcSources.X, RpcTargets.Y)] void Foo() |
[Rpc(LatticeRpcTarget.Y)] void Foo() (+ Runner.Rpc/Object.Rpc) |
Review |
| RPC parameters | RPC method with parameters (sent automatically) | byte[] payload (manual marshal) OR a custom event |
Manual |
| RPC info | RpcInfo info parameter |
(sender available via OnRpc / OnEvent args) |
Manual |
| Authority check | Object.HasStateAuthority / HasStateAuthority |
HasStateAuthority |
Auto |
| Authority check | Object.HasInputAuthority / HasInputAuthority |
HasInputAuthority |
Auto |
| Authority id | Object.StateAuthority / Object.InputAuthority (PlayerRef) |
Object.Owner (ulong) / Runner.LocalPlayer |
Review |
| Authority transfer | Object.RequestStateAuthority() (shared) |
Object.RequestAuthority() / Runner.RequestAuthority(netid) |
Review |
| Spawn | Runner.Spawn(prefab, position, rotation, owner) |
Runner.Spawn<T>(NetworkType, in T initialState, ulong owner) |
Review |
| Despawn | Runner.Despawn(Object) / Runner.Despawn(netObj) |
Runner.Despawn(NetworkObject) / Runner.Despawn(netid) |
Auto |
| Find object | Runner.FindObject(id) / Runner.TryGetNetworkObject |
Runner.Find(netid) |
Review |
| Startup | runner.StartGame(new StartGameArgs { GameMode = ... }) |
runner.StartGame(LatticeGameMode.Host, port) |
Review |
| Runner creation | AddComponent<NetworkRunner>() / runner prefab |
new NetworkRunner(tickRateHz) (headless) or AddComponent (editor) |
Review |
| Input struct | struct X : INetworkInput |
struct X (plain POD) |
Review |
| Input read | GetInput(out MyInput input) |
GetInput<T>(out T input) (binding helper; see manual notes) |
Manual |
| Input provider | INetworkInputProvider / OnInput(runner, input) / AddInputProvider |
(runner input-poll callback in host) | Manual |
| Networked transform | NetworkTransform / NetworkRigidbody / NetworkRigidbody2D |
[Networked] Vector3 Position / Quaternion Rotation + apply in StateUpdated() |
Manual |
| Interest management | AreaOfInterest / SetPlayerAlwaysInterested / AOI |
(core interest management; no per-object AOI API in binding) | Manual |
| Runner callbacks | INetworkRunnerCallbacks (OnPlayerJoined/Left/…) |
NetworkRunner.OnConnected/OnDisconnected/OnSpawned/OnDespawned/OnRpc/OnEvent events |
Review |
| Tick / prediction timing | Runner.Tick / Tick.Raw / prediction & rollback |
authority-driven tick + snapshot interpolation | Manual |
| KCC processor | class X : KCCProcessor / NetworkKCCProcessor (+ KCCData member access) |
class X : IKccProcessor (+ KccData member mapping) via --kcc adapter |
Review (scaffold) |
The four big rocks¶
These are the most labour-intensive parts of any real Fusion → Lattice port, and the tool flags them rather than guessing:
- Spawn model. Fusion spawns a prefab + transform; Lattice spawns a registered
NetworkType - an initial POD state block. Register the type once (
Runner.RegisterType), build the initial-state struct (position/rotation become[Networked]fields you set), thenSpawn<T>(type, in state, owner). - RPC payloads. Fusion auto-marshals RPC args; the Lattice binding carries a
byte[]payload you serialize/deserialize yourself in the method body /OnRpchandler. The arity also differs: Fusion's(source, target)becomes a singleLatticeRpcTargetplus an in-body authority guard. - Input routing. Lattice has no
INetworkInputinterface and (in this binding) no typedGetInput<T>onNetworkBehaviour. Input collection is a host concern (theon_input_pollcallback in the C ABI); move yourOnInputlogic into the runner host's poll. - Prediction / tick timing. Fusion forward-predicts and resimulates on the client; Lattice predicts in the core with snapshot interpolation on remotes. Any logic that depended on Fusion's per-tick resimulation, tick numbers in resim, input-buffering windows, or reconciliation must be re-reasoned by hand.
Post-conversion: next steps¶
- Reference the Lattice Unity package (
com.lattice.netcode) in your project so theLattice.*types resolve. - Work the Review items. Each is rewritten but needs a decision — verify the
[OnChanged]handler signature, add the authority guards the RPC findings call for, confirmObject.Ownersemantics, and reworkStartGameintoStartGame(mode, port)(session/scene/player-count move to the director/lobby layer). - Hand-do the Manual items. Replace networked collections with a fixed-capacity array or a
serialized
Bytesfield; replaceNetworkTransformwith[Networked]position/rotation applied inStateUpdated(); move input collection into the host poll; marshal RPC payloads; re-reason any prediction/tick-timing logic. - Compile against the binding, fix what the compiler finds, then test — start with a smoke test of the Auto-converted paths, then validate behaviour on the Review/Manual paths.
Pair this with the dev skill
The lattice-gamedev Claude Code skill teaches an AI assistant the
Lattice mental model and the real lattice.h API surface — useful when you are hand-finishing
the Review/Manual items, since it knows the exact spawn / RPC / input / authority idioms the
report points you toward.
Reference¶
- Tool source, sample, and committed converted output:
tools/fusion2lattice/ - Generated concept guide:
tools/fusion2lattice/FUSION-MIGRATION-GUIDE.md - Lattice engine integration (the target idiom): design 05 — Engine Integration
- The C ABI the binding wraps: API Reference