"Published-build client join reloads the assembly, wiping all statics"
▸ SYMPTOM
A real friend joining from a published build enters a valid invite code, connects, but the host receives an empty invite code and rejects the join — identical symptom to the scene-handoff wipe, but this time it happens even with the reconstruct-not-reset fix in place. Local and editor-hosted test runs pass fine; only a real cross-machine published join fails.
▸ CAUSE
Networking.Connect loads the host's game package, which reloads the game assembly on the client (the engine reloads the addon DLL when the version differs from the locally cached one). Every static field resets to its default — the reconstruct-not-reset fix runs but has nothing to reconstruct from, because the statics it "preserved" were already back to null after the assembly reload.
This is the deeper layer under the scene-handoff wipe: that fix assumed statics survive the handoff ("same process/assembly") — true for local/editor hosts, false for a published client connecting to a published host.
Test blind spot: Both -joinlocal and editor-host runs structurally mask this — they keep the process assembly intact, so pre-reload and post-reload statics are identical. Only a real published friend join across machines exercises this path.
▸ FIX
Carry the join intent through a persistent store, not statics. Before calling Networking.Connect, persist the join data to disk:
// Before connect — persist join intent
var intent = new JoinIntent
{
Code = typedCode,
AttemptId = Guid.NewGuid().ToString(),
WrittenAtUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
FileSystem.Data.WriteJson("join-intent.json", intent);
Networking.Connect(target);On the other side of the reload, restore from disk:
// In the reconstruct path and at handshake send time
var intent = FileSystem.Data.ReadJson<JoinIntent>("join-intent.json");
if (intent != null)
{
var ageMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - intent.WrittenAtUnixMs;
if (ageMs < 180_000) // 3-minute freshness window
{
PendingJoinCode = intent.Code;
AttemptId = intent.AttemptId;
}
FileSystem.Data.DeleteFile("join-intent.json");
}Key details:
- Use
DateTimeOffset.UtcNowfor the freshness check — gameTime.Nowresets on the reloaded instance - Delete the file on join/abandon/fail/end/disconnect and on any stale read
- Fail-soft: no file means a clean rejection (the current behavior before the fix)
- The engine mounts the same data folder for the same published identity on both sides of the reload
▸ WHY IT WORKS
FileSystem.Data persists to the user's local data directory, which is keyed by the game's published identity (organization + package name). The data folder survives the assembly reload because it is managed by the engine runtime, not the game assembly. By writing the join intent before Connect and reading it after the reload, the state crosses the assembly boundary via disk instead of relying on in-process memory.
The freshness window prevents a stale leftover file from an abandoned join attempt being used in a later session.