1 client multiplayer connection flow and session cleanup
forgejo-actions edited this page 2026-07-15 12:43:13 +00:00

Client Multiplayer Connection Flow And Session Cleanup

Client multiplayer connection flow coordinates HTTP authentication, realm and character selection, handoff, game handshake, QUIC realtime startup, reconnect status, and teardown. MultiplayerConnectionState is the persistent Bevy resource that carries those phases across standalone screens.

Ownership And Data Flow

MultiplayerConnectionState owns:

  • login endpoint and client compatibility inputs;
  • account login response and access token;
  • realm list and selected realm index;
  • character roster and selected character index;
  • handoff token response;
  • game handshake response;
  • optional live RealtimeSessionHandle;
  • one optional asynchronous PendingJob and player-facing status.

MultiplayerUiState is intentionally separate. Chat draft, menus, dialogue bubbles, keybind capture, emote state, and telemetry history are presentation concerns and do not establish login or world authority.

The shared multiplayer Tokio runtime uses a dedicated two-worker thread pool. Bevy starts work through IoTaskPool, while poll_pending_job reduces completion back into the connection resource on the update schedule.

Connection Paths

Login only

start_login_flow optionally registers the account, authenticates, fetches realms, and fetches characters. Success populates authenticated state but does not request a handoff or start realtime.

Selected character entry

start_character_connect requires an authenticated account, selected realm, and selected character. It requests v1/select-character, follows bounded game-server handshake redirects, and submits GameHello plus the handoff token to v1/handshake.

An accepted handshake must include both a game session ticket and RealtimeSessionActivation. The client selects a healthy QUIC gameplay endpoint, applies activation locally, sends realtime hello, validates the welcome session token, and reports transport ready only after that welcome arrives.

Quick connect

start_quick_connect performs authentication and world entry in one pending job using the first returned realm and first returned character. Empty realms or characters fail the job.

Character create and delete

Create and delete require authenticated state and return a replacement character roster. They do not create a realtime session. Selection is clamped to the resulting roster length when the network snapshot is applied.

Only one pending job can exist. Every start method returns without doing work while another job is present.

Lifecycle And State Transitions

The connection reducer handles a small event model:

Event Result
Started Applies the flow cleanup scope, stores the task/stage, and sets a pending status
Pending Keeps the job and refreshes its pending status
Succeeded Merges only populated NetworkSnapshot fields and derives authenticated or connected status
Failed Applies cleanup for the failed stage and stores Error: ...
RuntimeUnavailable Applies the flow cleanup scope and stores the runtime error
ValidationFailed Changes status only; no network task starts
Reset Drops the pending job, clears the authenticated session scope, and returns to Idle

The realtime transport itself transitions through Connecting, Live, Reconnecting, Transferring, DisconnectedByServer, TimedOut, and ClosedByUser. Reconnect attempts retain only commands allowed by the reconnect journal policy. Cross-instance transfer replaces endpoint/session activation and clears the old replicated world baseline.

Cleanup Scopes

Cleanup is deliberately scoped:

WorldEntry

This scope requests shutdown on the existing realtime handle and clears handoff, handshake, and realtime state. It preserves login response, realms, character roster, and selections.

It is used for connect, create-character, and delete-character starts/failures, and for zone-transfer recovery. A failed world-entry attempt can therefore return to character or realm selection without forcing reauthentication.

AuthenticatedSession

This scope includes world-entry cleanup and also clears realms, login response, character roster, and selection indexes. It is used for authenticate and quick-connect starts/failures and full reset/logout recovery.

Validation errors do not apply cleanup because no operation started. Starting a new flow applies cleanup before the asynchronous task runs, preventing stale handoff or realtime handles from surviving into the new attempt.

World-Entry Readiness

World entry is ready only when both conditions are true:

  1. the HTTP game handshake exists and has accepted = true;
  2. realtime transport health is Live and its world projection contains the authoritative local player.

A session ticket, a realtime handle, an accepted handshake alone, or a live transport without a local player is not sufficient. The standalone loading screen waits for world_entry_ready() before loading the assigned scene and entering gameplay.

This distinction protects against showing the world during the gap between handshake, activation, QUIC welcome, and local-player authority.

Disconnect And Logout

Clean logout queues a realtime disconnect and sets Disconnecting from realtime session... only when has_realtime_session() reports a live transport with an authoritative local player. In that case, the standalone shell waits until transport is ClosedByUser and the last message is no longer disconnect requested, then unloads the scene, resets connection and multiplayer UI state, and returns to login. During connecting, reconnecting, transfer, or an already-closed state, the shell skips that wait and proceeds with reset because there is no fully established realtime session to drain.

reset_runtime_session is stronger: it discards any pending job, requests transport shutdown through handle cleanup, clears authenticated state, and returns to Idle. Unexpected runtime connection loss uses this full reset and unloads the scene. Zone-transfer failure instead uses world-entry cleanup so account and roster state survive for retry.

Extending The Flow

When adding a connection operation:

  1. Add a MultiplayerConnectionFlow and PendingJobStage only if its lifecycle differs from an existing operation.
  2. Decide whether failure invalidates only world entry or the full authenticated session.
  3. Validate required local state before spawning a task. Validation failure should not partially clear a usable session.
  4. Return a NetworkSnapshot whose Option fields represent the exact state updated by the operation.
  5. Keep task completion inside reduce_connection_flow; do not update status and cleanup ad hoc from screens.
  6. Define loading/menu disabled behavior for the pending stage.
  7. Add success, failure, runtime-unavailable, and reset tests, plus a readiness test if world entry changes.

Rules And Invariants

  • At most one connection job is pending.
  • Selected realm and character indexes are always clamped after list replacement.
  • A successful handshake must publish a session ticket and activation before realtime connect starts.
  • Realtime welcome must acknowledge the activation session token.
  • World-entry readiness requires accepted handshake, live transport, and authoritative local player.
  • World-entry cleanup must preserve authenticated account and roster state.
  • Authenticated-session cleanup must request realtime shutdown before dropping the handle.
  • A rejected handshake may be retained for status, but it must not produce a realtime handle.
  • UI flow state is not evidence of network authority.

Validation And Debugging

Run focused connection tests with:

cargo test -p aether has_realtime_session_requires_authoritative_local_player
cargo test -p aether world_entry_ready_requires_an_accepted_handshake
cargo test -p aether world_entry_ready_requires_live_authoritative_realtime_player
cargo test -p aether status_helpers_format_errors_and_cleanup_failed_job_stages
cargo test -p aether realtime_disconnect_finished_requires_disconnect_request_to_complete

Diagnose a stuck flow by checking these fields in sequence:

  1. pending_job.stage and status: is Bevy still polling the intended task?
  2. login_response, realms, and characters: did HTTP authentication/bootstrap complete?
  3. handoff and handshake.accepted: did selection and game handshake complete, or is redirect/rejection visible?
  4. Handshake session and activation: an accepted response without either is a hard client error.
  5. Realtime transport health, endpoint, reconnect attempt, last packet, and last message.
  6. World projection local player: transport Live without this player remains loading by design.

Common status interpretations:

  • Error: no authenticated account is available: a selected-character operation was called before login state existed.
  • Handshake accepted but loading never finishes: inspect QUIC endpoint health, welcome token validation, transport health, and local activation player.
  • Login vanished after a connect failure: the operation was probably classified as authenticate/quick-connect instead of world-entry scope.
  • Logout hangs at disconnecting: inspect whether the transport reached ClosedByUser and replaced the disconnect requested message.
  • Transfer recovery returns to login: use world-entry recovery rather than full reset for a recoverable transfer failure.

Concrete Flow Example

For selected-character entry, the expected state progression is:

authenticated roster
  -> ConnectCharacter started (old handoff/realtime cleared)
  -> handoff token
  -> accepted game handshake with ticket + activation
  -> QUIC hello/welcome token match
  -> transport Live + authoritative local player
  -> world_entry_ready

Any failure before the last line clears only handoff, handshake, and realtime state, leaving the authenticated roster available for another selection.