"Joining client's static state wiped by networked scene handoff"
▸ SYMPTOM
A joining client enters a valid invite code and connects, but the host receives an empty invite code and rejects the join. The client's join UI showed the correct code, the user typed it correctly, but the wire-verify handshake gets nothing. The host logs something like "no invite code presented" from a genuine coded join.
▸ CAUSE
Networking.Connect loads the host's scene. The bootstrap creates a new instance of the session/state-machine component, and its OnEnabled "reset statics for a fresh session" (a normally-correct leak guard) runs before the join handshake sends those values. The host receives empty fields from a genuine coded join.
The whole client-side joining state machine gets amnesia at the scene load:
- Static fields survive the handoff (same process/assembly) but the reset erases them
- Instance state (e.g. a join watchdog timer) dies with the old scene
- Scene-owned UI (e.g. a join-wait overlay) is destroyed with the old scene
▸ FIX
Make OnEnabled reconstruct-not-reset when the component comes up already connected as a non-host (Networking.IsActive && !Networking.IsHost):
protected override void OnEnabled()
{
if ( Networking.IsActive && !Networking.IsHost )
{
// Reconstruct: preserve join statics, restore Joining mode,
// re-arm the watchdog with a fresh deadline,
// re-show the join-wait overlay
Mode = SessionMode.Joining;
RestoreJoinWatchdog();
ShowJoinWaitOverlay();
}
else
{
// Full reset for non-networked and host enables
ResetAllStatics();
Mode = SessionMode.Idle;
}
}Key details:
- Extract the reconstruct-vs-reset decision as a pure function and pin all four combos (connected+host, connected+client, disconnected+host, disconnected+client) in a test
- Guard the old component's
OnDisabledon_instance == thisso a late old-scene teardown cannot clobber the reconstruction - Make the reconstruction idempotent — it may fire twice per handoff (double
OnEnabledobserved in local testing)
Test blind spot: a local regression peer (-joinlocal) structurally cannot catch this bug — it never sets a code or enters the Joining state pre-handoff, so pre-wipe and post-wipe states are identical. Only a real join-UI coded join exercises the preservation path.
▸ WHY IT WORKS
By detecting the "already connected as a non-host" state in OnEnabled, the component knows it is being created by a scene handoff during an active join — not a fresh session start. It preserves the join intent instead of clearing it, so the handshake that fires shortly after has the correct values to send to the host.
The idempotency guard and the _instance == this check handle the edge cases of multiple enables and late teardowns that are inherent to scene transitions.