Skip to content

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:

  1. EOS authentication establishes the EOS Product User ID (PUID); Nakama authentication binds the backend session to that identity.
  2. Nakama, not the LS, derives the authoritative House roster for a freeze.
  3. The LS assigns canonical live-selection revisions. Clients submit intents, not revisions.
  4. Only the LS persists live House selections, in a five-second multi-player batch.
  5. Each owning client confirms its own final selection directly to Nakama once per freeze attempt.
  6. Nakama's Edgegap callback finalizes the deployment snapshot. Neither the LS nor every client independently finalizes it.
  7. 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

  1. The normal fresh entry point is House_Level in standalone mode.
  2. UOnlineManagerSubsystem::OnPostWorldInitialization records that a fresh House should be opened. It does not create a NetDriver during LoadMap.
  3. FOnlineAuthService logs the local player into EOS:
  4. Steam builds use a Steam Web API ticket.
  5. Other client builds use the configured device/account path.
  6. A DS does not perform this local-player login.
  7. After EOS succeeds, the auth service starts or refreshes the Nakama session for the current EOS account and generation.
  8. Nakama authentication completes, the realtime socket connects, and ENakamaBackendState becomes Ready.
  9. 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;
  • CurrentHouse has no House, lobby, or session identity;
  • no match-end rehost is active; and
  • bSkipLobbyAutoInitAfterRehost is false.

1.2 Converting the local House world into an LS

Once the gate passes:

  1. Standalone House_Level is enabled as a listen server.
  2. The EOS cleanup coordinator first removes any stale local named session/lobby state. This serializes cleanup and replacement creation.
  3. The host creates the EOS lobby.
  4. The host creates the EOS House session.
  5. UE calls Nakama house_open with the authenticated host PUID, House_Level, maximum players, EOS lobby ID, and EOS session ID.

Nakama house_open then:

  1. detaches the account from any previous authoritative House;
  2. creates a new House ID and records the authenticated Nakama user as host;
  3. creates the host roster member using the verified EOS PUID;
  4. resets that account's persistent selection lock to false without replacing its last valid canonical selection;
  5. writes the private user_house/current reverse index; and
  6. returns the House summary.

1.3 Completion order after house_open

UOnlineManagerSubsystem::OnHouseOpenSuccess performs these steps in order:

  1. populate CurrentHouse from the Nakama response;
  2. start the LS House lease heartbeat (15-second interval);
  3. explicitly retry selection bootstrap for the local host PlayerState;
  4. complete the pending Host House operation; and
  5. 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:

  1. The player selects an advertised House.
  2. UE calls the read-only house_join_preflight RPC before beginning EOS work. A stale list entry whose House has started freezing is rejected onscreen as House has started game without network travel.
  3. If the account still belongs to another House, the old Nakama membership is left or detached.
  4. UE joins the target EOS lobby and House session through the EOS cleanup/join coordinator.
  5. UE calls house_player_join with the target House ID and local PUID.
  6. 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.
  7. Nakama resets only this joining account's stale selection lock to false, retains its last valid canonical selection, and updates user_house/current.
  8. UE resolves the EOS session connect string, falling back to EOS:<HostPuid>:GameNetDriver:0, and travels to the LS.
  9. 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:

  1. The service waits until the PlayerState has a valid EOS PUID.
  2. It queues a bootstrap request and calls sv_get_selection_by_eos.
  3. The returned canonical catalog version, epoch, revision, lock, and equipped JSON are applied to ATankPlayerState.
  4. The selected UTankCharacterDef, gameplay bundles, soft assets, and pawn class are loaded.
  5. ATankGameMode::ApplyBootstrapCharacterSelection applies the already-canonical selection:
  6. it never calls ProcessCharacterSelection;
  7. it never submits or increments a selection revision;
  8. 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:

  1. resolves the preview character locally;
  2. assigns a monotonically increasing client intent ID;
  3. predicts the next visible selection immediately;
  4. stores only the latest pending intent; and
  5. sends ServerSubmitSelectionConfirm after 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:

  1. the LS validates its client intent ID and equipped payload;
  2. the LS assigns CanonicalRevision = CurrentRevision + 1; it does not trust a client-supplied revision;
  3. the LS applies the new canonical struct to the owning PlayerState;
  4. the owner receives ClientSelectionConfirmAck with the canonical epoch/revision;
  5. the server emits OnServerSelectionBroadcastTick with a fully loaded character definition; and
  6. 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_batch call. 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:

  • bSelectionReady expresses whether a member consents to launch;
  • bLocked expresses 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

  1. The LS performs a local ready/PlayerState check.
  2. The LS calls begin_house_freeze with the House ID.
  3. Nakama verifies authenticated host authority, House lease/status, and that every authoritative roster member is online.
  4. Nakama derives and hashes the roster from House storage. It does not accept a client count or LS-supplied roster.
  5. Nakama stores a pending freeze attempt, sets House.ActiveFreezeAttemptID, and returns a generated FreezeAttemptId. The attempt TTL is 30 seconds.
  6. The LS also captures every ready PlayerState's canonical MatchTeam through UBrickleafTeamSubsystem::GetTeams. Nakama verifies that this EOS-PUID mapping exactly covers the authoritative roster and stores it immutably in the versioned attempt.
  7. The LS atomically applies one replicated selection struct to every PlayerState containing the canonical selection plus bLocked=true and the same FreezeAttemptId.
  8. 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:

  1. locks every member's persistent canonical selection;
  2. writes the complete House frozen snapshot;
  3. marks the attempt completed; and
  4. sends notification code 1010, subject house-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:

  1. Nakama OnMatchmakerMatched extracts and verifies a single House ID from the ticket properties.
  2. It reads the House-scoped pending game-mode options.
  3. It asks Fleet Manager/Edgegap to create a deployment and consumes the pending options record.
  4. On Edgegap success, Nakama—not LS and not individual clients—finalizes the frozen snapshot under the exact (house_id, deployment_id).
  5. Nakama reads the finalized snapshot back. If finalization or readback fails, it sends create-failed and blocks travel.
  6. Nakama marks the attempt deployment status committed with the deployment ID, then sends each matched user notification code 1001, subject connection-info, with host, port, InstanceId, HouseId, and FreezeAttemptId.
  7. Each UE client caches the deployment ID, appends instance_id and house_id, and travels directly to the DS.
  8. Just before the host's committed DS travel, it best-effort publishes EOS member state house_state=in_match and house_ready_seq=0. This is stale-state hygiene; the deployment-bound rehost token remains the correctness check if the EOS update fails.
  9. 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.
  10. 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

  1. The host is identified by authenticated Nakama House host user ID.
  2. bSkipLobbyAutoInitAfterRehost is set so loading House_Level does not accidentally create a second EOS lobby/House.
  3. Blueprint owns any desired post-match presentation delay before invoking HandleMatchEndHouseRehost; C++ begins rehosting immediately when invoked.
  4. It best-effort publishes EOS member state rehosting.
  5. BeginPostMatchHouseSelectionRecovery clears old-world selection delegates, timers, bootstrap requests, and runtime states before travel.
  6. The host disconnects from the DS and forcibly shuts down the old NetDriver.
  7. It opens House_Level?listen, retrying briefly while an old NetDriver still exists, and arms the EOS House-ready signal.
  8. PostLoadMap/listen readiness confirms that the replacement LS is actually listening.
  9. EOS starts publishing house_state=ready, the deployment-bound house_ready_seq, and house_map. The publish operation is callback-checked and retried independently if EOS does not confirm it.
  10. 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.
  11. 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.
  12. 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

  1. A non-host resolves the EOS lobby owner PUID.
  2. It listens for EOS host member-attribute updates and also polls as a fallback.
  3. Its timeout starts when HandleMatchEndHouseRehost is invoked; all participants should invoke it after the same Blueprint-owned presentation delay.
  4. It accepts readiness only when all of these match:
  5. the update belongs to the lobby owner;
  6. house_state == ready;
  7. house_map matches;
  8. house_ready_seq exactly equals the locally derived deployment-bound token; and
  9. the token has not already been processed.
  10. It resolves the EOS House session connect string, falling back to EOS:<HostPuid>:GameNetDriver:0.
  11. It ClientTravels to the rehosted LS.
  12. 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:

  1. suppress Unreal's default MainMenu?closed travel;
  2. shut down the failed NetDriver;
  3. cancel any active/scheduled rehost state;
  4. asynchronously abandon and clear current EOS House session/lobby state;
  5. clear CurrentHouse, pending EOS IDs, stale realtime workflow state, and auto-host flags; and
  6. open House_Level directly.

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:

  1. stop timeout/poll/retry timers and mark the rehost inactive;
  2. perform best-effort EOS AbandonCurrentHouse cleanup;
  3. clear abandoned House and realtime workflow identity; and
  4. open House_Level through normal fresh-House startup, which creates a new EOS lobby/session and calls house_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/current is 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.go and selection_freeze_attempt.go
  • Matchmaker/Edgegap callback and snapshot finalization: DevOps/Nakama/data/modules/edgegap/matchmaker.go and selection_snapshot.go
  • Final-session cleanup: DevOps/Nakama/data/modules/edgegap/sessions.go