-
-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture 03 Steam Client Core
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
SteamClientvalue built on thesteam-ventcrate.
| 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
|
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
SteamClientstruct (src/steam_client.rs:322) and themoddeclarations 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.
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.
All connection/auth logic lives in src/steam_client/client.rs.
resolve_server_list() (client.rs:179) memoizes a ServerList:
-
ServerList::discover()(steam-vent's CM directory), - on failure, falls back to bootstrap endpoints via
crate::cm_list::get_cm_endpoints()andServerList::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.
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.
| 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.
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).
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).
Grouped by capability. Methods are split across submodules but all hang off the one SteamClient.
| 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. |
login(), login_qr(), restore_session(), pending_confirmations(), clear_pending_confirmations(), is_auth_error_text() (client.rs:151, heuristics to detect "not logged on"/expired).
| 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. |
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.
update_game(), verify, and the download driver: see the install and launch wiki pages.
play_game(), launch_game(), ad-hoc spawn helpers, plus the host-Steam control and process-kill helpers covered in §4 and §7.
Friends roster/watcher, chat, market price/search/inventory (separate wiki pages).
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).
| 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.
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. |
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.
-
spawn_game_process()(process.rs:345): the canonical path, which builds aPipelineContextand runscrate::launch::pipeline::LaunchPipeline::with_default_stages(). This is whereproton_path,force_native_engine, andsteam_enabledare threaded into the launch pipeline. -
spawn_windows_native()(process.rs:291): runs a Windows.exedirectly on a Windows host (no Proton). -
internal_legacy_launch_adhoc()(process.rs:384): legacy direct-spawn path. It deliberately rejectsWindowsProtontargets (process.rs:458) to force all Proton launches through the pipeline. -
find_mangohud_lib()(src/steam_client.rs:1805): locateslibMangoHud.so.
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.
| 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).
This section feeds the umu integration plan. launch.rs is documented separately, but here is how it hooks into this client.
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()setssteam_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 solsteamclientloads builtin and bridges to the running host client.
-
standalone (
-
Runner wiring (
src/infra/runners/wine_tkg.rs:560): whensteam_enabled, the runner setsSTEAM_COMPAT_CLIENT_INSTALL_PATHto 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-launcheritself setsSTEAM_COMPAT_CLIENT_INSTALL_PATH(to its own ulwgl/umu fake-steam path) andSTEAM_COMPAT_APP_ID/PROTONPATH/GAMEID. Any umu runner must reconcile with Aurelia's existingsteam_enabledlogic: 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 inbuild_dll_overrides(src/utils.rs:771) and theSTEAM_COMPAT_APP_IDprocess-tracking contract (used bykill_processes_for_app,process.rs:120) must be preserved.
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.
-
play_game()(launch.rs:8): resolves launch options viaget_product_info()(PICS), picks aLaunchInfo(prefers an entry whose executable exists on disk, withLaunchTarget::WindowsProtonvsNativeLinuxchosen from oslist/exe extension inparse_launch_info_from_vdf,src/steam_client.rs:929), runs cloud sync-down, spawns viaspawn_windows_nativeorspawn_game_process, records the launch incrate::running,wait()s, then cloud sync-up. -
launch_game()(launch.rs:186): thinner variant that callsspawn_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.
Users
-
Usage
- Global behavior
- Authentication
- Library
- Store & discovery
- Collections
- Install & maintenance
- Launching
- Depots & branches
- Downgrade & pinning
- Steam Cloud
- Steam Workshop
- Friends & chat
- Inventory & market
- Configuration
- Proton & Wine
- Windows Steam runtime
- Luxtorpeda plugin
- umu-launcher plugin
- Launch scripts
- Session daemon
- Files & locations
- Exit codes & logging
- Windows Steam Runtime
Maintainers
Architecture