House, Selection, Match, and Rehost Lifecycle KB¶
This document describes the current end-to-end multiplayer lifecycle from a cold client start through House listen-server (LS) creation, character selection, House freeze, dedicated-server (DS) launch, match-end LS rehost, client rejoin, and failure recovery.
It documents the implemented authority boundaries. Blueprint steps are called out explicitly because they are required parts of the launch flow even when the underlying transport is implemented in C++ or Nakama.
The detailed backend freeze, admission, deployment, failure, and committed travel contracts are maintained in launch protocol.
Terminology and authority¶
- Local client: the game process owned by one player. The House host is both a local client and the LS authority.
- LS: the player-hosted listen server running
House_Level. It owns runtime House gameplay state, validates selection intents, assigns canonical selection revisions, and initiates House freeze. - DS: the Edgegap dedicated battle server. It consumes the finalized deployment selection snapshot and does not accept live character changes.
- EOS lobby: the persistent group and member-attribute channel used across LS -> DS -> LS.
- EOS session: the connectable House session used to resolve the LS address.
- Nakama House: the authoritative backend roster, host identity, lease, membership barrier, and House-scoped selection state.
- Freeze attempt: a backend-generated, short-lived attempt that fixes the House roster and gathers one final selection confirmation from every member.
- Deployment snapshot: the immutable House selection snapshot bound to one Edgegap deployment.
The most important authority rules are:
- EOS authentication establishes the EOS Product User ID (PUID); Nakama authentication binds the backend session to that identity.
- Nakama, not the LS, derives the authoritative House roster for a freeze.
- The LS assigns canonical live-selection revisions. Clients submit intents, not revisions.
- Only the LS persists live House selections, in a five-second multi-player batch.
- Each owning client confirms its own final selection directly to Nakama once per freeze attempt.
- Nakama's Edgegap callback finalizes the deployment snapshot. Neither the LS nor every client independently finalizes it.
- The DS spawns from the deployment-scoped snapshot; it does not accept character-selection mutation.
End-to-end overview¶
flowchart TD
A["Cold start in House_Level"] --> B["EOS login ready"]
B --> C["Nakama session and realtime ready"]
C --> D["Create EOS lobby and House session"]
D --> E["Nakama house_open"]
E --> F["House LS ready; host selection bootstrap"]
F --> G["Clients join EOS and Nakama House, then connect to LS"]
G --> H["LS bootstraps each canonical selection and pawn"]
H --> I["Live character selection"]
I --> J["LS begins House freeze"]
J --> K["Each owner confirms final selection"]
K --> L["Whole roster confirms; LS authorizes every owner to enter matchmaking"]
L --> M["Nakama verifies aggregate admission; Edgegap deployment created"]
M --> N["Nakama finalizes and verifies deployment snapshot"]
N --> O["connection-info notification; all clients travel to DS"]
O --> P["DS bootstraps players from deployment snapshot"]
P --> Q["Match end: host rehosts House; clients wait and rejoin"]
Q --> I
1. Cold start, EOS readiness, and automatic House open¶
1.1 Initial world and authentication¶
- The normal fresh entry point is
House_Levelin standalone mode. UOnlineManagerSubsystem::OnPostWorldInitializationrecords that a fresh House should be opened. It does not create a NetDriver duringLoadMap.FOnlineAuthServicelogs the local player into EOS:- Steam builds use a Steam Web API ticket.
- Other client builds use the configured device/account path.
- A DS does not perform this local-player login.
- After EOS succeeds, the auth service starts or refreshes the Nakama session for the current EOS account and generation.
- Nakama authentication completes, the realtime socket connects, and
ENakamaBackendStatebecomesReady. - EOS readiness does not choose or force a map. If the process is already in
House_Level, it only re-evaluates the fresh-House startup gate.
The fresh-House gate requires all of the following:
- the world is
House_Level; - net mode is standalone or listen server;
- EOS login is ready;
- Nakama backend and realtime are ready;
- no EOS cleanup is active;
CurrentHousehas no House, lobby, or session identity;- no match-end rehost is active; and
bSkipLobbyAutoInitAfterRehostis false.
1.2 Converting the local House world into an LS¶
Once the gate passes:
- Standalone
House_Levelis enabled as a listen server. - The EOS cleanup coordinator first removes any stale local named session/lobby state. This serializes cleanup and replacement creation.
- The host creates the EOS lobby.
- The host creates the EOS House session.
- UE calls Nakama
house_openwith the authenticated host PUID,House_Level, maximum players, EOS lobby ID, and EOS session ID.
Nakama house_open then:
- detaches the account from any previous authoritative House;
- creates a new House ID and records the authenticated Nakama user as host;
- creates the host roster member using the verified EOS PUID;
- resets that account's persistent selection lock to
falsewithout replacing its last valid canonical selection; - writes the private
user_house/currentreverse index; and - returns the House summary.
1.3 Completion order after house_open¶
UOnlineManagerSubsystem::OnHouseOpenSuccess performs these steps in order:
- populate
CurrentHousefrom the Nakama response; - start the LS House lease heartbeat (15-second interval);
- explicitly retry selection bootstrap for the local host PlayerState;
- complete the pending Host House operation; and
- invoke
ATankPlayerController::OnHouseSetupComplete(CurrentHouse).
The explicit bootstrap in step 3 is required. The local PlayerState can be created while the map is still standalone, before LS authority and house_open membership exist. Bootstrap is therefore retried after House open and before Blueprint is told setup is complete.
If any fresh-House stage fails, UE logs and displays the stage, performs best-effort EOS cleanup, clears all partial House identity, and retries through the same startup gate with backoff and jitter.
2. Joining an existing House¶
The existing-House path keeps EOS connection state and Nakama membership consistent:
- The player selects an advertised House.
- UE calls the read-only
house_join_preflightRPC before beginning EOS work. A stale list entry whose House has started freezing is rejected onscreen asHouse has started gamewithout network travel. - If the account still belongs to another House, the old Nakama membership is left or detached.
- UE joins the target EOS lobby and House session through the EOS cleanup/join coordinator.
- UE calls
house_player_joinwith the target House ID and local PUID. - Nakama rechecks the freeze barrier using optimistic House storage, verifies that the authenticated account owns that PUID, verifies lease/status/capacity, and adds the identity to the roster idempotently.
- Nakama resets only this joining account's stale selection lock to
false, retains its last valid canonical selection, and updatesuser_house/current. - UE resolves the EOS session connect string, falling back to
EOS:<HostPuid>:GameNetDriver:0, and travels to the LS. - The LS creates the joining controller and PlayerState, resets it to unready, then starts canonical selection bootstrap.
An active freeze attempt is a membership barrier. Nakama removes the House from discovery and rejects both join and leave operations while House.ActiveFreezeAttemptID is set, so the roster cannot silently change after launch finalization begins. The preflight improves stale-list UX; house_player_join remains the authoritative race gate. If network travel was already in flight, LS PreLogin rejects it with the same reason. The client recognizes both PendingNetDriver and established GameNetDriver failures, cancels the pending connection when necessary, clears EOS state, and opens a fresh House.
If Nakama has admitted a member but the LS has not created that member's PlayerState yet, the freeze roster check still rejects launch. Both the host-facing error and UE_LOG report Submitted player count mismatch with server house player count.
3. Selection bootstrap and authoritative pawn creation¶
ATankGameMode forwards player lifecycle points to FOnlineSelectionTransportService:
GenericPlayerInitialization/ player initialization;- PlayerState-ready notification; and
- logout.
For an LS joiner:
- The service waits until the PlayerState has a valid EOS PUID.
- It queues a bootstrap request and calls
sv_get_selection_by_eos. - The returned canonical catalog version, epoch, revision, lock, and equipped JSON are applied to
ATankPlayerState. - The selected
UTankCharacterDef, gameplay bundles, soft assets, and pawn class are loaded. ATankGameMode::ApplyBootstrapCharacterSelectionapplies the already-canonical selection:- it never calls
ProcessCharacterSelection; - it never submits or increments a selection revision;
- it keeps an existing pawn if its class already matches; otherwise it unpossesses and destroys the old pawn, restarts the player with the selected class, and verifies possession.
For a DS joiner, the same bootstrap framework requests sv_get_deployment_selection_by_eos using house_id and the Edgegap instance_id/ARBITRIUM_REQUEST_ID. The DS is expected to spawn from the deployment-scoped frozen record. In strict mode, a missing required deployment selection blocks normal spawn rather than falling back to mutable global state.
4. Live character selection in the House¶
4.1 Owning-client prediction and submission¶
Blueprint/UI submits a def-based equipped payload through SubmitSelectionConfirmFromDefs or the raw JSON path. The owning controller then:
- resolves the preview character locally;
- assigns a monotonically increasing client intent ID;
- predicts the next visible selection immediately;
- stores only the latest pending intent; and
- sends
ServerSubmitSelectionConfirmafter a trailing 0.5-second client debounce; every newer local change replaces the older unsent intent and restarts the timer.
Repeated changes during that window replace the pending value and reset the timer. This prevents a rapid UI sequence from becoming one server RPC per click.
4.2 LS validation and canonical revision¶
The LS rejects a submitted intent when authority, PlayerState, bootstrap, catalog, payload, ownership, character resolution, or lock requirements fail. The DS rejects all live selection changes.
For an accepted LS intent:
- the LS validates its client intent ID and equipped payload;
- the LS assigns
CanonicalRevision = CurrentRevision + 1; it does not trust a client-supplied revision; - the LS applies the new canonical struct to the owning PlayerState;
- the owner receives
ClientSelectionConfirmAckwith the canonical epoch/revision; - the server emits
OnServerSelectionBroadcastTickwith a fully loaded character definition; and - the LS replicates/broadcasts the authoritative result to other clients.
The authoritative House gameplay/Blueprint listener connected to OnServerSelectionBroadcastTick owns the normal manual character pawn replacement. The bootstrap path above is intentionally separate and must not be routed back through selection submission.
Rejection leaves the last valid canonical revision unchanged, clears local prediction, produces a UE_LOG, and displays an owning-client on-screen rejection message.
4.3 Two separate LS coalescers¶
Accepted live changes use two independent mechanisms:
- UE broadcast coalescing (1 second): the first accepted value is emitted, then only the latest pending update is retained for the next broadcast boundary. A client cannot force an LS multicast for every rapid request.
- Nakama persistence batching (5 seconds): every dirty player's latest canonical state is included in one authenticated
house_set_selection_batchcall. Five players changing during the same window still produce one Nakama RPC, not five.
player_set_selection_confirm is not part of the live-selection flow and has been removed. The only direct owning-client selection RPC to Nakama is the one-shot final confirmation during a freeze attempt.
5. Launch game: ready gate, House freeze, and matchmaking¶
5.1 Ready state¶
Each ATankPlayerState replicates bSelectionReady with RepNotify. It defaults to false, and the LS explicitly resets it to false for every House connection/reconnection. SetSelectionReady lets the owning client ask the LS to change it while selection is unlocked.
This is deliberately separate from SelectionState.bLocked:
bSelectionReadyexpresses whether a member consents to launch;bLockedexpresses whether selection mutation is currently forbidden.
The LS cannot begin finalization unless every current House PlayerState is ready and has an initialized canonical selection.
Client ready changes use the same trailing 0.5-second debounce. A newer ready/not-ready choice replaces the older unsent value and restarts the timer; only the final value is submitted to the LS.
5.2 Blueprint launch contract¶
Before tickets can form a match, the host Blueprint must submit the intended game-mode options with gm_set_pending_options; the payload is normalized to include the current house_id.
The host then calls Start House Game (Async). This wrapper owns the complete
reversible launch window from local validation through committed
connection-info. Success means DS travel has been authorized, not merely that
begin_house_freeze returned. Its Error output is LS-only and fires after
native rollback/fan-out. Every terminal error also uses TANK_PRINT, reaching
the categorized UE log, viewport console, and onscreen debug overlay.
Every local freeze validation phase reports all players that fail that phase, rather than stopping at the first player. For example, the ready gate returns Players not ready: PlayerA, PlayerB; identity/team validation, selection initialization, PlayerState continuity, and lock publication use the same aggregated form with phase-specific wording.
5.3 Beginning the freeze¶
- The LS performs a local ready/PlayerState check.
- The LS calls
begin_house_freezewith the House ID. - Nakama verifies authenticated host authority, House lease/status, and that every authoritative roster member is online.
- Nakama derives and hashes the roster from House storage. It does not accept a client count or LS-supplied roster.
- Nakama stores a pending freeze attempt, sets
House.ActiveFreezeAttemptID, and returns a generatedFreezeAttemptId. The attempt TTL is 30 seconds. - The LS also captures every ready PlayerState's canonical
MatchTeamthroughUBrickleafTeamSubsystem::GetTeams. Nakama verifies that this EOS-PUID mapping exactly covers the authoritative roster and stores it immutably in the versioned attempt. - The LS atomically applies one replicated selection struct to every PlayerState containing the canonical selection plus
bLocked=trueand the sameFreezeAttemptId. - Because authority does not receive its own RepNotify, the LS explicitly invokes the identical freeze handler for its local host controller after all PlayerStates are updated.
Keeping the lock and attempt ID in the same replicated struct guarantees that a client never reacts to a new lock while still holding a null or previous attempt ID.
5.4 Per-client final confirmation¶
On the owning PlayerState's RepNotify, the local controller ignores an empty, unlocked, duplicate, or stale attempt. For a new valid attempt it calls confirm_final_selection exactly once with:
freeze_attempt_id;- catalog version;
- selection epoch and canonical revision; and
- equipped JSON.
Nakama validates membership, attempt status/expiry, idempotency by (FreezeAttemptId, NakamaUserId), owned inventory, revision/epoch, lock state, and the unchanged roster hash.
After an individual's successful response, C++ invokes the BlueprintNativeEvent:
OnFinalSelectionConfirmationComplete(true, "")
This is only the asynchronous result of that client's confirmation. It must not add a matchmaker ticket, because another House member can still fail finalization.
5.5 Required AddMatchmaker ticket shape¶
Every House member's matchmaker ticket must contain both of these:
Query: properties.houseId:<HouseId>
String property: houseId -> <HouseId>
The query is a filter; it does not create the property it queries. Supplying only the query causes House tickets not to match, which looks like a silent launch stall. The Blueprint AddMatchmaker fix must preserve both values.
When all frozen members confirm, Nakama atomically:
- locks every member's persistent canonical selection;
- writes the complete House frozen snapshot;
- marks the attempt
completed; and - sends notification code
1010, subjecthouse-freeze-complete, to the LS host.
The launch async action remains pending after aggregate freeze completion. It
polls get_house_freeze_status every two seconds as a missed-notification
fallback and tolerates up to three consecutive status transport failures.
When the async action observes completed, the authoritative LS reliably fans
OnHouseFreezeSucceeded(FreezeAttemptId) to every PlayerController whose locked
selection state belongs to that attempt. Each owning client's Blueprint calls
AddMatchmaker from this aggregate-success event. This prevents partial tickets
from being created while other House members are still confirming.
5.6 Pre-commit launch failure¶
Any failure before committed connection-info is an aggregate House-launch
failure, including confirmation rejection/timeout, admission timeout, match
timeout, Fleet Manager failure, deployment timeout, or snapshot/commit failure.
For a healthy LS, native handling releases the barrier, clears replicated
attempt IDs/locks, resets every ready flag, and then reliably fans
OnHouseGameLaunchFailed(FreezeAttemptId, FailureStage, FailureCode,
ErrorMessage) to every owning PlayerController. The event is idempotent by
attempt ID and is the common Blueprint entry point for ticket, UI, and local
launch cleanup. It is distinct from an individual
OnFinalSelectionConfirmationComplete(false) result.
If the LS is the failed participant, Nakama closes the canonical House before
sending house-launch-failed (1011) directly to the frozen roster. Every
former member enters fresh-House recovery without relying on LS fan-out.
5.7 Match creation and DS travel¶
Once the House tickets match:
- Nakama
OnMatchmakerMatchedextracts and verifies a single House ID from the ticket properties. - It reads the House-scoped pending game-mode options.
- It asks Fleet Manager/Edgegap to create a deployment and consumes the pending options record.
- On Edgegap success, Nakama—not LS and not individual clients—finalizes the frozen snapshot under the exact
(house_id, deployment_id). - Nakama reads the finalized snapshot back. If finalization or readback fails, it sends
create-failedand blocks travel. - Nakama marks the attempt deployment status
committedwith the deployment ID, then sends each matched user notification code1001, subjectconnection-info, with host, port,InstanceId,HouseId, andFreezeAttemptId. - Each UE client caches the deployment ID, appends
instance_idandhouse_id, and travels directly to the DS. - Just before the host's committed DS travel, it best-effort publishes EOS member state
house_state=in_matchandhouse_ready_seq=0. This is stale-state hygiene; the deployment-bound rehost token remains the correctness check if the EOS update fails. - Each schema-v2 deployment selection player record includes the LS-captured
match_team_id. A team-mode DS applies it during deployment bootstrap before pawn spawn; a non-team-mode DS ignores it and retains incremental MatchTeam assignment. - Edgegap timeout (
1002) and create failure (1003) are logged and displayed on screen; no DS travel occurs.
Committed connection-info is the irreversible launch boundary. UE stores the
attempt ID before DS travel begins. From then until lifecycle cleanup, client
leave, client/LS House switching, and intentional LS close/reopen reject with
Game travel has already started. Nakama independently rejects direct leave,
join/open-another-House, and voluntary close attempts when the active attempt
has deployment_status=committed. Post-commit travel failures use individual
disconnect/fresh-House recovery and never aggregate rollback.
6. Match end: host rehost and client rejoin¶
All participants call HandleMatchEndHouseRehost(ReadySeq, WaitTimeoutSeconds, HouseMap) while retaining the original House and EOS lobby identity.
The supplied ReadySeq is not used alone. Outside PIE, C++ derives a positive token from:
HouseId + EdgegapDeploymentId + RequestedReadySeq
This matters because EOS member attributes persist across map travel. A ready value from an older LS -> DS -> LS loop cannot satisfy the next loop. LastSeenHouseReadySeq is intentionally not reset after success; the deployment-bound expected token makes every real cycle distinct, and the last-seen equality check only suppresses duplicate processing within the lifetime of the subsystem. These hash-derived tokens are identities, not monotonically increasing sequence numbers, so a numerically smaller token from a later deployment remains valid.
6.1 Host branch¶
- The host is identified by authenticated Nakama House host user ID.
bSkipLobbyAutoInitAfterRehostis set so loadingHouse_Leveldoes not accidentally create a second EOS lobby/House.- Blueprint owns any desired post-match presentation delay before invoking
HandleMatchEndHouseRehost; C++ begins rehosting immediately when invoked. - It best-effort publishes EOS member state
rehosting. BeginPostMatchHouseSelectionRecoveryclears old-world selection delegates, timers, bootstrap requests, and runtime states before travel.- The host disconnects from the DS and forcibly shuts down the old NetDriver.
- It opens
House_Level?listen, retrying briefly while an old NetDriver still exists, and arms the EOS House-ready signal. PostLoadMap/listen readiness confirms that the replacement LS is actually listening.- EOS starts publishing
house_state=ready, the deployment-boundhouse_ready_seq, andhouse_map. The publish operation is callback-checked and retried independently if EOS does not confirm it. - As soon as the replacement NetDriver is confirmed listening, host-side rehost success refreshes the local selection bootstrap; the service deduplicates an already queued/in-flight bootstrap.
- The new LS starts
house_set_selection_lock(false)so the authoritative House roster becomes selectable again in Nakama. This persistence RPC is asynchronous and does not gate the local success event. - The new local House controller receives
OnMatchEndHouseRehostSuccess(), then the subsystem-level success delegate is broadcast. This proves the host's replacement LS is live and ready publication has been dispatched; it does not mean EOS has already callback-confirmed the member-attribute write or that Nakama has already acknowledged the unlock.
Do not clear selection lifecycle state after success. At that point the replacement controller and its bootstrap already belong to the new world; clearing them would cancel the bootstrap or remove new Blueprint listeners.
6.2 Client branch¶
- A non-host resolves the EOS lobby owner PUID.
- It listens for EOS host member-attribute updates and also polls as a fallback.
- Its timeout starts when
HandleMatchEndHouseRehostis invoked; all participants should invoke it after the same Blueprint-owned presentation delay. - It accepts readiness only when all of these match:
- the update belongs to the lobby owner;
house_state == ready;house_mapmatches;house_ready_seqexactly equals the locally derived deployment-bound token; and- the token has not already been processed.
- It resolves the EOS House session connect string, falling back to
EOS:<HostPuid>:GameNetDriver:0. - It
ClientTravels to the rehosted LS. - On arrival, normal LS controller/PlayerState initialization bootstraps the previous canonical selection and possesses the correct pawn.
The client-side async operation reports success after issuing travel. OnMatchEndHouseRehostSuccess() on ATankPlayerController is intentionally invoked only for the host controller proven ready in the replacement LS world; it is not fired on the client's old DS controller.
7. Disconnect, failure, and reset behavior¶
7.1 Unexpected LS or DS transport disconnect¶
UTankGameEngine::HandleNetworkFailure handles an unexpected remote GameNetDriver loss for both LS and DS connections through the same path:
- suppress Unreal's default
MainMenu?closedtravel; - shut down the failed NetDriver;
- cancel any active/scheduled rehost state;
- asynchronously abandon and clear current EOS House session/lobby state;
- clear
CurrentHouse, pending EOS IDs, stale realtime workflow state, and auto-host flags; and - open
House_Leveldirectly.
The replacement House world then follows the normal cold-start gate and opens a fresh LS/House when EOS and Nakama are ready. It does not attempt to reuse the failed LS or DS transport.
There is deliberately one map transition: Battle/House -> House_Level. Do not insert MainMenu between them. If a future design requires multiple travel steps, it must wait for all Game Feature activation/deactivation transitions from the first travel to finish before starting the next.
7.2 Match-end rehost failure¶
Every host and client rehost failure now uses the same terminal recovery:
- stop timeout/poll/retry timers and mark the rehost inactive;
- perform best-effort EOS
AbandonCurrentHousecleanup; - clear abandoned House and realtime workflow identity; and
- open
House_Levelthrough normal fresh-House startup, which creates a new EOS lobby/session and callshouse_open.
This applies to missing House/deployment identity, host departure, ready timeout/mismatch, NetDriver/listen failure, address-resolution failure, and travel preparation failure. Recovery does not wait for a later network-failure callback.
7.3 Nakama realtime disconnect¶
An unexpected Nakama realtime disconnect moves the backend to unavailable and starts recovery. If an active House/network workflow exists, UOnlineManagerSubsystem provides a fail-closed grace period:
- if realtime returns during the grace window, pending House behavior resumes;
- if it does not return, UE clears the abandoned workflow, performs EOS cleanup, reauthenticates from the current EOS account when allowed, opens
House_Level, and creates a fresh House after readiness returns; - a terminal blocked backend state fails closed immediately and does not start a new House until the backend is usable.
7.4 Nakama session-end cleanup¶
Nakama tracks all realtime sessions per authenticated user. Cleanup runs only after that user's final tracked session ends:
- if a freeze is active, the attempt is failed and its membership barrier is released;
- if a completed frozen roster loses a final session before launch completes, the attempt can be invalidated and its persistent selection locks are released;
- a disconnected host closes the House;
- a disconnected non-host is removed from the roster; and
user_house/currentis deleted.
This prevents one account's overlapping/refresh session from prematurely destroying House membership. The disconnect-cleanup fix was deployed to the remote Nakama repository as commit 44c30f0; verify the deployed remote and local DevOps/Nakama mirror are at equivalent code before future backend edits.
7.5 Explicit client leave and intentional host closure¶
Blueprint uses the latent Leave House node for a normal non-host departure. A ready client, or a client that has observed a replicated selection lock/freeze-attempt ID, cannot start leaving. For an admitted departure, authenticated Nakama removal runs first because Nakama is the canonical House-membership authority. EOS session/lobby cleanup starts only after Nakama accepts removal. Only after EOS cleanup also succeeds does UE clear the old lifecycle, shut down the old game NetDriver, and fire success.
A Nakama rejection occurs before EOS is touched, cancels the UI transition, and leaves the client in its current House. This is the authoritative race fallback when the LS begins freezing before the replicated freeze state reaches that client. An EOS failure after Nakama has accepted removal is catastrophic because canonical membership has already changed; UE cancels the transition and enters the shared EOS-cleanup/fresh-House recovery path. An intentional host-close notification racing client leave counts as authoritative Nakama completion and permits EOS cleanup to begin.
Blueprint uses the latent Join House node for both direct joins and House switching. With no current House it follows the normal admission/EOS/Nakama/travel path. When an NM_Client is already in a different House, ready and locally frozen clients are rejected before transition work starts. An admitted switch performs an initial read-only target admission check, starts the shared WorldTravel UI transition, removes canonical Nakama membership, then cleans EOS state, waits for cleanup success and the transition Processing phase, and repeats authoritative target admission during the normal join path. The operation remains pending across ClientTravel until the configured transition Out gate and animation finish. A solo listen-server host may use the same node; both local PlayerArray checks and authoritative house_close_solo_for_switch prevent it while another member exists. Same-House joins are rejected. A target failure after old-House cleanup enters fresh-House recovery; an initial preflight or source-House Nakama-leave rejection leaves the client in its original House. After committed connection-info, every join/switch entry is rejected before transition work.
Each transition creates a fresh normal UserWidget from its data-asset definition. A custom widget may implement BrickLeafUITransitionWidgetInterface to receive direct Trigger In Phase, Trigger Process Phase, and Trigger Out Phase calls. The existing subsystem multicast events remain available as a parallel presentation hook. Enabled In and Out implementations report animation completion through the subsystem's existing completion functions; the interface does not change or bypass named gate ordering.
The transition data asset also owns root-level GlobalProcessPhaseNotifyNames and GlobalOutPhaseNotifyNames filters. Notify Transition Point(Global, Name) routes the notification into the currently active named transition only when the corresponding global filter contains Name and that transition requires Name in the same phase gate. Global notifications are not retained while no transition is active, preventing a notification from one operation satisfying a later run.
Engine paths that are about to fall back to the configured GameDefaultMap start the TravelToMainMenu transition In phase first. This includes unhandled network failures, travel failures, explicit ClientReturnToMainMenuWithTextReason, and the remaining project-owned direct HandleDisconnect calls. The transition must have a TravelToMainMenu definition in the configured data asset; the native travel remains authoritative if presentation is absent.
BrickLeafLifecycleSubsystem is the GameInstance-lifetime boundary for finalized project-owned world travel. House/backend owners retain every protocol decision, then submit only OpenLevel, ClientTravel, or ServerTravel to this subsystem. It reuses an already active operation transition such as LeaveHouse or SwitchHouse; otherwise it starts WorldTravel, waits until In has completed and the transition reaches Processing, and only then dispatches travel. TravelToEdgegapServer explicitly requests the TravelToDS transition. That definition inherits the established dedicated-server travel presentation and requires DestinationWorldLoadComplete (plus LocalCharacterInitComplete) before Out. Neither the lifecycle nor UI-transition GameInstance subsystem is created in a dedicated-server process. Missing presentation definitions travel immediately. OnNotifyPreClientTravel and PreLoadMapWithContext are deduplicated fallback observers for engine or Blueprint paths that bypass the coordinator, but those callbacks cannot delay a travel that Unreal has already committed. PostLoadMapWithWorld reports DestinationWorldLoadComplete to the active transition. Transition widgets disable UE 5.6's default auto-remove-on-world-teardown behavior so the cover remains visible until the destination Out phase. The generic WorldTravel definition has no pre-travel Process requirements and requires DestinationWorldLoadComplete for Out.
Accepted lifecycle travel also arms the UI-transition subsystem's UE 5.6 IPerformanceDataConsumer. PostLoadMapWithWorld marks the destination boundary, but frame evaluation does not begin until every configured Out requirement except ResourceStable has been reported. This makes performance recovery terminal: late character, game-feature, audio, or similar initialization cannot invalidate an already-latched stability result. Async/sync loading and meaningful streaming demand keep the detector in Loading; after those settle, ten consecutive non-hitch frames beneath the configured target-frame-time tolerance produce NotifyTransitionPoint(Global, ResourceStable). A 2.5-second timeout measured from the terminal recovery window releases the gate on hardware that cannot meet the target. The consumer is removed immediately after that notification (deferred one game-thread task because UE is iterating its consumer array during ProcessFrame). To consume the signal, add ResourceStable to the data asset's global Out allow-list and to the desired transition Out requirements.
The listen host uses Close Current House And Reopen. Nakama deletes the House and sends each non-host member a nonpersistent house-closed (1030) notification with the old House ID and the reason The host closed the House. Matching clients enter the same EOS-cleanup/fresh-House recovery used by unexpected LS/DS disconnects; stale notifications for another House are ignored. A missed notification still falls back to transport-disconnect recovery when the old listen world ends.
During Alt+F4/orderly process shutdown, UE submits a separate best-effort Nakama leave/close and best-effort EOS cleanup without waiting for callbacks. A hard crash cannot execute client cleanup code, so Nakama final-session cleanup remains authoritative.
7.6 Selection reset matrix¶
| Boundary | Runtime selection | Persistent Nakama selection | House/EOS identity |
|---|---|---|---|
house_open |
Host bootstrap is retried after open | Entering host lock reset to false; canonical selection retained | New House/lobby/session established |
house_player_join |
Joiner bootstraps on LS | Joining account lock reset to false; canonical selection retained | Target House becomes current |
| Explicit client leave | Old transient lifecycle discarded after all cleanup succeeds | Member removed, chats left, reverse membership index cleared | EOS session/lobby and old game NetDriver cleaned before async success |
| Intentional host close | Every former member enters fresh-House recovery | House deleted; reverse indexes/chats cleaned; clients notified | Host and clients abandon old EOS state and independently open fresh Houses |
| Freeze begins | Every PS locked with same attempt ID | House membership barrier active; final locks committed only at aggregate completion | Existing House retained |
| Freeze fails | Attempt IDs cleared and runtime unlocked | Attempt failed/cancelled; barrier released | Existing House retained for retry |
| LS -> DS travel | DS later applies deployment snapshot | Frozen selections remain locked | EOS lobby/House retained; deployment ID cached |
| Successful DS -> LS rehost | New LS bootstraps existing selections | Host unlocks complete authoritative roster | Existing House/lobby reused |
| Rehost failure or unexpected server disconnect | Old transient lifecycle discarded before/following transport teardown | New House entry resets each participant that recovers | Old state abandoned; fresh House created |
| Final Nakama session disconnect | Local process recovery handles its own state | Freeze invalidated as needed; host closes House or member is removed | Reverse membership index cleared |
8. RPC and notification ownership reference¶
| Operation | Caller | Frequency / protection | Purpose |
|---|---|---|---|
house_open |
New LS host | Once per fresh-House attempt | Create authoritative House and reset host lock |
house_heartbeat |
LS host | Every 15 seconds while hosting | Maintain House lease |
house_close |
LS host | Once per intentional close; best effort on shutdown | Delete House, clean member state, notify clients, then let host reopen |
house_player_join / house_player_leave |
Owning client | House membership transitions | Maintain authoritative roster; blocked during freeze |
player_get_owned_snapshot |
Owning client | On inventory refresh; backend burst 5, refill 1/sec | Populate ownership cache |
house_set_selection_batch |
LS host | At most one per dirty five-second window | Persist all dirty live selections |
begin_house_freeze |
LS host | Once per launch attempt | Derive/fix roster and create attempt |
confirm_final_selection |
Each owning client | Once per attempt; idempotent | Prove final replicated selection against account ownership |
get_house_freeze_status |
LS host | Two-second fallback polling | Recover from missed aggregate notification and track admission/deployment |
cancel_house_freeze |
LS host | Best effort on failed local publication/wait | Release failed attempt/barrier |
gm_set_pending_options |
LS/GM account | Once per launch configuration | Store House-scoped Edgegap game options |
| AddMatchmaker | Every frozen member | Once after aggregate OnHouseFreezeSucceeded |
Add House-bound matchmaking ticket only after the entire House finalizes |
house_set_selection_lock(false) |
Rehosted LS host | Once after replacement LS readiness | Unlock full roster for next House selection phase |
house-freeze-complete (1010) |
Nakama -> LS | Once per terminal attempt, with status polling fallback | Complete/fail host async operation |
house-launch-failed (1011) |
Nakama -> frozen roster | Once per pre-commit aggregate failure | Deliver terminal cleanup even when the LS failed |
house-closed (1030) |
Nakama -> former non-host members | Once after intentional authoritative deletion | Surface host intent and begin immediate fresh-House recovery |
connection-info (1001) |
Nakama -> matched users | Once after deployment snapshot finalization/readback | Authorize DS travel |
create-timeout / create-failed (1002/1003) |
Nakama -> matched users | Terminal deployment failure | Surface failure and block travel |
9. Primary implementation locations¶
- Cold start, fresh House gating, disconnect recovery, and House-open completion:
Source/Metal_terra/Private/Core/Subsystems/OnlineManagerSubsystem.cpp - EOS login and Nakama session start:
Source/Metal_terra/Private/Core/Subsystems/Online/OnlineAuthService.cpp - EOS lobby/session/listen readiness:
Source/Metal_terra/Private/Core/Subsystems/Online/OnlineEosHouseService.cpp - Nakama House UE client flow:
Source/Metal_terra/Private/Core/Subsystems/Online/OnlineNakamaHouseService.cpp - Selection bootstrap, validation, broadcast, and batch persistence:
Source/Metal_terra/Private/Core/Subsystems/Online/OnlineSelectionTransportService.cpp - Client selection coalescing and freeze BlueprintNativeEvents:
Source/Metal_terra/Private/Core/TankPlayerController.cpp - Canonical replicated selection/ready state:
Source/Metal_terra/Private/Core/TankPlayerState.cpp - Authoritative bootstrap pawn application:
Source/Metal_terra/Private/Core/TankGameMode.cpp - Freeze async orchestration:
Source/Metal_terra/Private/Core/AsyncActions/AsyncAction_FreezeHouseSelectionSnapshot.cpp - Client leave async orchestration:
Source/Metal_terra/Private/Core/AsyncActions/AsyncAction_LeaveHouse.cpp - Match-end rehost and fresh-House fallback:
Source/Metal_terra/Private/Core/Subsystems/Online/OnlineMatchEndService.cpp - Unexpected LS/DS network-failure interception:
Source/Metal_terra/Private/Core/TankGameEngine.cpp - Backend House/membership:
DevOps/Nakama/data/modules/edgegap/house.go - Backend selection/freeze:
DevOps/Nakama/data/modules/edgegap/selection.goandselection_freeze_attempt.go - Matchmaker/Edgegap callback and snapshot finalization:
DevOps/Nakama/data/modules/edgegap/matchmaker.goandselection_snapshot.go - Final-session cleanup:
DevOps/Nakama/data/modules/edgegap/sessions.go