Skip to content

Architecture 03 Steam Client Core

Drackrath edited this page Aug 27, 2026 · 1 revision

03. Steam Client Core (steam_client)

The core Steam connection, authentication, and session layer of Aurelia. Everything that touches Steam (login, PICS appinfo, ownership tickets, downloads, cloud saves, friends, market, launching) flows through one SteamClient value built on the steam-vent crate.

Parent module src/steam_client.rs (~1844 lines)
Core methods src/steam_client/client.rs
Process control src/steam_client/process.rs
The type SteamClient at src/steam_client.rs:322

1. Purpose & role

steam_client is the session layer. It owns the single live steam_vent::connection::Connection to a Steam Connection-Manager (CM) server and exposes a high-level SteamClient API that every other subsystem calls. There is no HTTPS storefront/Web-API dependency for the core data paths: store metadata, ownership, depots, achievements, cloud saves and friends are all fetched over the CM connection using steam-vent protobuf service methods and jobs.

The module file src/steam_client.rs holds:

  • the SteamClient struct (src/steam_client.rs:322) and the mod declarations for all submodules (src/steam_client.rs:332),
  • supporting public types (LaunchInfo, LaunchTarget, StoreAppInfo, DepotInfo, GameAchievement, AccountData, SharedApp, …),
  • free helper functions: VDF/PICS parsing, appmanifest (.acf) parsing/rewriting, DLC enable/disable, achievement-schema parsing, store-item mapping, unix_to_ymd, kill_process_tree, sanitize_install_dir, registry lookups.

The impl SteamClient block is split across submodules (each does use super::*;), so the type's API surface is the union of methods defined in client.rs, process.rs, launch.rs, install.rs, content.rs, library.rs, manage.rs, manifests.rs, workshop.rs, chat.rs, friends.rs, market.rs.

The SteamClient struct

pub struct SteamClient {
    connection: Option<Connection>,          // live steam-vent CM connection
    state: LoginState,                        // Connected / Awaiting* / Complete / Offline
    connected_at: Option<Instant>,
    active_cm: Option<SocketAddr>,
    server_list: Option<ServerList>,          // discovered/bootstrapped CM list
    pending_confirmations: Vec<ConfirmationPrompt>,  // Steam Guard prompts to surface
}

SteamClient is #[derive(Clone)]. Cloning is cheap because steam_vent::Connection is Arc-backed and multiplexes jobs. The daemon relies on this to hand each request a clone-backed client over one shared session (see §6).

LoginState (src/steam_client.rs:132) is an internal state machine: Connected → AwaitingCredentialSession → AwaitingGuardConfirmation → AwaitingPollResult → AwaitingAccessTokenLogon → Complete, plus Offline.


2. Connection & authentication

All connection/auth logic lives in src/steam_client/client.rs.

CM discovery & server list

resolve_server_list() (client.rs:179) memoizes a ServerList:

  1. ServerList::discover() (steam-vent's CM directory),
  2. on failure, falls back to bootstrap endpoints via crate::cm_list::get_cm_endpoints() and ServerList::new(...).

connect() (client.rs:160) resolves the list, picks an active CM, sets connected_at/state, and, if the network is unavailable, attempts offline mode (try_enter_offline_mode(), client.rs:211) which succeeds only when a library_cache_path() exists, flipping state = Offline with no connection.

Robustness: access_with_retry

steam_vent's Connection::access (refresh-token logon) has no timeout: a stalled CM hangs the whole command. access_with_retry() (src/steam_client.rs:84) wraps it with a CM_CONNECT_TIMEOUT (10 s) per attempt and CM_CONNECT_ATTEMPTS (3) round-robin failovers.

Login flows

Flow Method steam-vent entry Notes
Stored refresh token restore_session() client.rs:234 access_with_retry → Connection::access Reads persisted account_name+refresh_token from load_session(). This is the daemon's normal path.
Password + Steam Guard login() client.rs:273 Connection::login(...) Three sub-paths: (a) guard_code provided up front (piped into UserProvidedAuthConfirmationHandler via an in-memory duplex), (b) interactive_pin (read code from stdin, login --pin), (c) default DeviceConfirmationHandler (mobile-app approve). Guard data persisted via FileGuardDataStore::user_cache().
Mobile confirm fallback every login() path .or(DeviceConfirmationHandler) If the account only allows mobile approval, the handler chain falls through to it.
QR code login_qr() client.rs:365 Authentication.BeginAuthSessionViaQR / PollAuthSessionStatus over Connection::anonymous Calls on_challenge(url) callback to render the QR (initially + on Steam's rotation), polls up to 180 s, then exchanges the issued refresh token via access_with_retry.

When login() hits ConnectionError::UnsupportedConfirmationAction(methods), the methods are mapped via map_confirmation() (src/steam_client.rs:892) into pending_confirmations (SteamGuardReq::EmailCode/DeviceCode/DeviceConfirmation) and surfaced to the caller. pending_confirmations() / clear_pending_confirmations() expose them.

Session persistence

A successful login builds a SessionState { account_name, steam_id, refresh_token, client_instance_id } via session_from_connection() (client.rs:464, pulling connection.access_token()) and writes it through crate::config::save_session(). logout() (client.rs:86) drops the connection and calls delete_session(). invalidate_session() (client.rs:221) drops the connection in-memory without deleting the persisted token (used by the daemon liveness loop).

Liveness

probe_alive() (client.rs:54) does a cheap CFamilyGroups_GetFamilyGroupForUser round-trip bounded by PROBE_TIMEOUT (20 s). This exists because steam-vent keeps a Connection looking alive after its socket dies (its heartbeat task only logs failures). The daemon probes proactively (see §6).


3. The SteamClient type: public API surface

Grouped by capability. Methods are split across submodules but all hang off the one SteamClient.

Lifecycle & connection (client.rs)

Method Purpose
new() client.rs:8 Empty, unconnected client.
from_shared(Connection) client.rs:24 Reuse an already-authenticated connection: no CM connect, no logon. The daemon's hot path.
connect() / resolve_server_list() Establish/cache CM server list.
is_authenticated() / is_offline() / connection() / steam_id() State accessors.
connected_seconds() / active_cm() Diagnostics.
probe_alive() Liveness check.
invalidate_session() / logout() Tear down.

Auth (client.rs)

login(), login_qr(), restore_session(), pending_confirmations(), clear_pending_confirmations(), is_auth_error_text() (client.rs:151, heuristics to detect "not logged on"/expired).

Account & ownership (client.rs)

Method Purpose
get_account_data() client.rs:115 AccountData (steam id, country, account name).
get_app_ticket(appid) client.rs:93 App-ownership ticket via CMsgClientGetAppOwnershipTicket job. Launch/ownership relevant (see §7).
cloud_client() client.rs:70 Builds a crate::cloud_sync::CloudClient over the connection.

Apps, metadata & library (content.rs, library.rs, manifests.rs, manage.rs)

PICS product info, StoreBrowse.GetItems store metadata (StoreAppInfo/StoreAppAssets), owned/shared/family games, achievements, depot/manifest resolution, install-size estimates, DLC management. Documented in the content / library / manifests / manage wiki pages.

Install / download / verify (install.rs, launch.rs)

update_game(), verify, and the download driver: see the install and launch wiki pages.

Launch & process (launch.rs, process.rs)

play_game(), launch_game(), ad-hoc spawn helpers, plus the host-Steam control and process-kill helpers covered in §4 and §7.

Social & market (friends.rs, chat.rs, market.rs)

Friends roster/watcher, chat, market price/search/inventory (separate wiki pages).


4. process.rs: Steam process & game-process lifecycle

src/steam_client/process.rs manages OS-level processes, both the host desktop Steam client and the games Aurelia launches. It does not manage the steam-vent network session (that's client.rs).

Host desktop Steam control (Windows)

Method Purpose
steam_is_running() process.rs:14 Windows: reads HKCU\Software\Valve\Steam\SteamPID. Non-Windows: always false.
shutdown_steam() process.rs:34 steam.exe -shutdown, waits ≤30 s. Editing .acf appmanifests is only reliable while Steam is stopped (Steam flushes app state on exit).
start_steam() process.rs:58 steam.exe -silent (tray, no foreground window).
write_headless_steam_cfg() process.rs:274 Writes a minimal-UI steam.cfg.

Rationale: the running Steam client caches each game's appmanifest at startup, so on-disk changes (e.g. enabling a DLC, moving a game) aren't seen until Steam re-reads them, hence shutdown/start around manifest edits.

Wine-prefix / Proton bridging helpers (Linux)

These scan /proc (scan_proc_pids() process.rs:154) to find and control Steam/Proton processes by environment:

Method Purpose
is_steam_running_in_prefix() process.rs:240 Detects steam.exe running inside a given WINEPREFIX.
kill_steam_in_prefix() process.rs:199 Kills steam.exe/steamwebhelper.exe in a prefix.
kill_wine_processes_in_prefix() process.rs:176 Kills all wine procs in a per-game prefix (never the shared master prefix).
kill_processes_for_app(app_id) process.rs:120 Kills every process carrying STEAM_COMPAT_APP_ID=<app_id>. This catches Proton-reparented game/steam.exe/wineserver processes the spawned runner tree misses.

Stopping a launched game

stop_game(app_id, force) (process.rs:87) loads the launch record (crate::running::load), sweeps the per-game prefix and STEAM_COMPAT_APP_ID processes, kills the recorded PID tree (kill_process_tree, src/steam_client.rs:361: taskkill /T on Windows, SIGTERM→SIGKILL on Unix), and clears the record.

Game spawn helpers

  • spawn_game_process() (process.rs:345): the canonical path, which builds a PipelineContext and runs crate::launch::pipeline::LaunchPipeline::with_default_stages(). This is where proton_path, force_native_engine, and steam_enabled are threaded into the launch pipeline.
  • spawn_windows_native() (process.rs:291): runs a Windows .exe directly on a Windows host (no Proton).
  • internal_legacy_launch_adhoc() (process.rs:384): legacy direct-spawn path. It deliberately rejects WindowsProton targets (process.rs:458) to force all Proton launches through the pipeline.
  • find_mangohud_lib() (src/steam_client.rs:1805): locates libMangoHud.so.

5. Module layout of steam_client/

Declared at src/steam_client.rs:332. Each submodule extends impl SteamClient (or, for friends/market, also exports free items re-exported at src/steam_client.rs:347).

Submodule Responsibility Wiki page
client.rs Lifecycle, connection, auth, account this page (§2–§3)
process.rs Steam/game process lifecycle, prefix bridging this page (§4)
content.rs PICS product info, store metadata, depots/CDN content page
install.rs Install/download driver install page
library.rs Owned/family library, roots library page
launch.rs play_game/launch_game orchestration launch page
manifests.rs Depot manifest fetch/resolve manifests page
manage.rs DLC, branches, move/relocate manage page
workshop.rs, workshop_manifest.rs Workshop items workshop page
friends.rs Friends roster + watcher (resolve_steam_id, Friend, Roster, …) friends page
chat.rs Chat messaging chat page
market.rs Community Market (market_price, InventoryItem, WalletBalance, …) market page

The parent src/steam_client.rs itself is shared infrastructure: the struct, public DTOs, and free parsing/helper functions used by all submodules.


6. Cross-module interactions

Consumer What it uses Path
Daemon (shared session) SteamClient::new() → restore_session(), then hands each request a from_shared(connection) clone src/daemon/mod.rs:159, :282 (shared_restored_client)
Daemon liveness loop probe_alive(), invalidate()/ensure_session() src/daemon/mod.rs:240, :264
Daemon friends watcher run_friends_watcher() (friends.rs) over the shared connection src/daemon/mod.rs:196
CLI (main.rs) restore_session(), login_qr(), cloud_client() src/main.rs:1121, :1297, :4164
Config load_session/save_session/delete_session, library_cache_path src/config.rs (via src/steam_client.rs:3)
Cloud sync cloud_client() → crate::cloud_sync::{CloudClient, CloudPathResolver, UfsSaveSpec} src/cloud_sync.rs
Launch pipeline spawn_game_process() builds PipelineContext, runs stages src/launch/pipeline.rs, src/launch/stages/*
Running-game registry crate::running::{record_launch, load, clear} src/running.rs
Relocate / library crate::relocate::*, crate::library::all_library_roots src/relocate.rs, src/library.rs
Proton runners steam_enabled flag consumed in wine_tkg/luxtorpeda src/infra/runners/*

Daemon connection-reuse pattern (important): the daemon holds exactly one authenticated Connection. ensure_session() restores it once, and every command gets a from_shared() clone over the same socket. This avoids per-invocation re-logon, which would trip Steam's rate limits. The liveness loop double-checks with probe_alive() before paying for a reconnect (because steam-vent hides dead sockets).


7. Launch-relevant notes (for the umu-launcher integration plan)

This section feeds the umu integration plan. launch.rs is documented separately, but here is how it hooks into this client.

The --steam bridged-launch mode (steam_enabled)

A boolean flag (steam_enabled) flows: CLI/play → play_game() (launch.rs:8) → spawn_game_process() (process.rs:345) → PipelineContext.steam_enabled → launch stages and runners.

  • Forced on for non-owned apps: play_game() sets steam_enabled = steam_enabled || !app.is_owned (launch.rs:20). A Family-Shared game can only be authorized by a running host Steam client, so it always needs the bridge.
  • Linux: when enabled, crate::utils::ensure_steam_running() is invoked (launch.rs:27) so the host client is up for Steamworks/Family-Sharing init.
  • DLL overrides differ by mode (src/utils.rs:786):
    • standalone (steam_enabled = false): Aurelia neutralizes Steam DLLs (steamclient=n, steam_api=n, lsteamclient=, …) so the game runs with no client.
    • bridged (steam_enabled = true): those overrides are left at Proton defaults so lsteamclient loads builtin and bridges to the running host client.
  • Runner wiring (src/infra/runners/wine_tkg.rs:560): when steam_enabled, the runner sets STEAM_COMPAT_CLIENT_INSTALL_PATH to the real host Steam install (crate::utils::host_steam_client_path()), falling back to a "fake steam trap" (setup_fake_steam_trap) when no host install exists.

umu note: umu-launcher itself sets STEAM_COMPAT_CLIENT_INSTALL_PATH (to its own ulwgl/umu fake-steam path) and STEAM_COMPAT_APP_ID/PROTONPATH/GAMEID. Any umu runner must reconcile with Aurelia's existing steam_enabled logic: in bridged mode it must point at the real host Steam (or the umu integration breaks Family-Sharing). In standalone mode umu's own fake-steam path is appropriate. The DLL-override split in build_dll_overrides (src/utils.rs:771) and the STEAM_COMPAT_APP_ID process-tracking contract (used by kill_processes_for_app, process.rs:120) must be preserved.

App-ownership tickets

get_app_ticket(appid) (client.rs:93) requests a CMsgClientGetAppOwnershipTicket over the CM connection and returns the raw ticket bytes (errors on an empty ticket). This is the protocol-native ownership proof. It is currently exposed on SteamClient and is the natural source if an umu/standalone launch needs to provide an ownership ticket without a running host client.

How launch.rs hooks into this client

  • play_game() (launch.rs:8): resolves launch options via get_product_info() (PICS), picks a LaunchInfo (prefers an entry whose executable exists on disk, with LaunchTarget::WindowsProton vs NativeLinux chosen from oslist/exe extension in parse_launch_info_from_vdf, src/steam_client.rs:929), runs cloud sync-down, spawns via spawn_windows_native or spawn_game_process, records the launch in crate::running, wait()s, then cloud sync-up.
  • launch_game() (launch.rs:186): thinner variant that calls spawn_game_process(..., steam_enabled = false).
  • All Proton launches are funneled through spawn_game_process → LaunchPipeline. The legacy ad-hoc path explicitly bails on Proton targets (process.rs:458).

Key types for launch: LaunchInfo (src/steam_client.rs:150), LaunchTarget (:144, NativeLinux / WindowsProton), RawLaunchOption, LaunchOptionInfo, ExtendedAppInfo.

Clone this wiki locally