Skip to content

Architecture 05 Steam Library Launch

Drackrath edited this page Aug 27, 2026 · 1 revision

05. Steam Library & Game Launch

This page documents the two SteamClient modules that drive library discovery and game launching:

File Role
src/steam_client/library.rs Owned/Family-Shared game discovery, PICS app metadata, store info, update detection.
src/steam_client/launch.rs The launch entry point (play_game) and the update/verify download driver.

Both are impl SteamClient blocks split out of steam_client.rs for readability. The struct, shared use super::* imports, and free helpers live in the parent module (src/steam_client.rs).

Why this page matters for the umu-launcher work: launch.rs::play_game is where the native vs Proton decision begins, but it then hands off to a pipeline that ultimately builds the proton run command elsewhere. The full call path and every Proton/native decision point are traced in §3 and §6.


1. Purpose & Role

  • library.rs answers "what can this account play, and is it up to date?" It talks to Steam's CM service methods (Player.GetOwnedGames, FamilyGroups.*, StoreBrowse.GetItems) and to the PICS product-info system, then caches results to disk.
  • launch.rs answers "run this game now." play_game resolves the right launch entry, decides native-Windows vs native-Linux vs Proton, performs Cloud sync-down, spawns the process (directly or through the launch pipeline), records the running game, blocks on exit, and syncs the cloud back up. It also hosts the shared download engine used by both update_game and verify_game.

2. library.rs: Library, Metadata & Ownership

2.1 Owned games (Player.GetOwnedGames)

fetch_owned_games (library.rs:8) calls the Player.GetOwnedGames service method with include_appinfo and include_played_free_games, maps each result into an OwnedGame, and writes save_library_cache(&owned) (library.rs:44) so the library is browsable offline. refresh_owned_games (library.rs:109) is a thin re-fetch. load_cached_owned_games (library.rs:113) reads the cache.

2.2 Family Sharing (FamilyGroups.*)

fetch_family_shared_apps (library.rs:51) is a two-step CM flow:

  1. FamilyGroups.GetFamilyGroupForUser → resolves the caller's family_groupid. If 0, the account is in no family group and an empty list is returned (library.rs:69-73).
  2. FamilyGroups.GetSharedLibraryApps with include_own(false) → lists apps shared by other members (not the caller's own), capped at max_apps(10_000), producing SharedApp { app_id, name, owner_steamid }.

The owner_steamid is the first entry of owner_steamids (library.rs:102). This is the account whose license must authorise the launch, and it is exactly why family-shared launches force Steam integration on (see §3.1).

2.3 App metadata via PICS

All raw appinfo flows through fetch_pics_buffer (library.rs:228), which issues a CMsgClientPICSProductInfoRequest job and returns the app's product-info buffer (usually binary VDF, occasionally text). Consumers parse it with find_vdf_in_pics rather than the text-only parse_appinfo (a recurring comment warns that binary appinfo silently yields no DLC/depots otherwise):

Function Line Returns
get_extended_app_info library.rs:255 ExtendedAppInfo { name, DLC ids, depots, launch options, active branch }
fetch_ufs_save_specs library.rs:303 UfsSaveSpec[]: Cloud auto-cloud save rules
fetch_online_required library.rs:316 bool derived from common/category
fetch_store_apps library.rs:345 StoreAppInfo[] via StoreBrowse.GetItems (over CM, not HTTPS)
get_product_info library.rs:393 Vec<LaunchInfo>: the launch entries that drive play_game

get_product_info is the direct dependency of the launch path: it fetches the PICS buffer and calls parse_launch_info_from_vdf(appid, &raw_vdf) (library.rs:403).

Free helpers in this file:

  • launch_options_from_section (library.rs:430): raw (executable, arguments) pairs from the PICS config/launch section (the data behind the launch-options command).
  • depots_from_section (library.rs:412): numeric depot ids/names only.

2.4 Ownership & update detection

Ownership is implied: OwnedGame comes from GetOwnedGames, while SharedApp is everything available not owned. The actual depot-key check that proves ownership at download time lives in the download driver (get_depot_key skips depots that aren't owned, see launch.rs:404-414).

Update detection compares local vs remote depot manifests:

  • check_for_updates (library.rs:119) iterates installed games, reads local_manifest_info (library.rs:161) from appmanifest_<id>.acf, short-circuits to update available if Steam's StateUpdateRequired flag is set (authoritative + offline-safe), otherwise fetches remote_manifest_ids and compares.
  • installed_depots_need_update (library.rs:454): a pure helper that flags an update only when an installed depot's manifest differs from remote (non-installed platform depots are ignored). Covered by update_detection_tests (library.rs:462).

3. launch.rs: The Launch Entry Point

The public entry point is play_game (launch.rs:8-184). It is the orchestrator. It does not itself build the Proton command. Signature:

pub async fn play_game(
    &mut self,
    app: &LibraryGame,
    proton_path: Option<&str>,            // explicit/driver-supplied Proton
    user_config: Option<&UserAppConfig>,  // per-game env / launch options
    force_windows: bool,                  // --windows: run the .exe path
    force_native_engine: bool,            // --native-engine: Luxtorpeda etc.
    steam_enabled: bool,                  // --steam: bridged launch
) -> Result<LaunchInfo>

3.1 Step 1: Steam integration & Family-Sharing gate

let steam_enabled = steam_enabled || !app.is_owned;   // launch.rs:20
#[cfg(target_os = "linux")]
if steam_enabled { crate::utils::ensure_steam_running(); }   // launch.rs:25-28

A family-shared game can only be authorised by a running host Steam client, so any non-owned app forces steam_enabled on regardless of the --steam flag. When enabled, the host Steam client is started silently (Linux-only, best-effort) so Steamworks/Family-Sharing can initialise.

3.2 Step 2: Resolve the launch entry

let launch_options = self.get_product_info(app.app_id).await?;   // launch.rs:30

This is the bridge into library.rs::get_product_info → parse_launch_info_from_vdf → Vec<LaunchInfo> (the launch-options data: per-entry executable, arguments, working dir, and a LaunchTarget platform tag).

The selection logic (launch.rs:38-76) is platform-installation-aware. A game commonly advertises Windows, macOS, and Linux entries, but only one platform's depot is on disk:

  • exe_exists(o) (launch.rs:38-47): does this entry's executable actually exist under install_path (backslashes normalised)?
  • prefer_windows_target = force_windows || proton_path.is_some() (launch.rs:56): whenever we intend to run through Wine/Proton.
  • If preferring Windows (launch.rs:57-68): first an installed WindowsProton entry, else any installed entry (a Linux-only game can't honour a --proton request against a non-existent .exe), else any WindowsProton entry, else the first.
  • Otherwise (launch.rs:70-75): first installed entry, else the first.

This guards against the game_executable_not_found failure mode.

3.3 Step 3: Native vs Proton decision (FIRST decision point)

let native_windows = force_windows
    || (cfg!(target_os = "windows") && launch_info.target == LaunchTarget::WindowsProton);  // launch.rs:82-83

let chosen_proton_path = if native_windows {
    None
} else {
    match launch_info.target {
        LaunchTarget::NativeLinux  => None,
        LaunchTarget::WindowsProton => proton_path.or(Some(launcher_config.proton_version.as_str())),
    }
};   // launch.rs:85-94

Two outcomes are decided here:

Condition native_windows chosen_proton_path Spawn path
--windows, or Windows host running a Windows entry true None spawn_windows_native
Linux entry (NativeLinux) false None spawn_game_process (NativeRunner)
Windows entry on Linux false proton_path ?? launcher_config.proton_version spawn_game_process (WineTkgRunner)

launcher_config is loaded at launch.rs:78. proton_version is the global default fallback.

Proton/Wine only exists on Linux. On a Windows host, a Windows game runs natively, so the .exe is executed directly rather than routed through the Proton pipeline.

3.4 Step 4: Cloud sync-down (best effort)

If enable_cloud_sync and online (launch.rs:96-131): build a CloudClient, resolve the remote root, sync_down. Divergent saves are left untouched (never clobbered) and the game launches with whatever is on disk. UFS save specs are fetched for the later upload.

3.5 Step 5: Spawn (SECOND decision point / pipeline hand-off)

let mut child = if native_windows {
    self.spawn_windows_native(app, &launch_info, user_config).await?            // launch.rs:134
} else {
    self.spawn_game_process(app, &launch_info, chosen_proton_path,
        &launcher_config, user_config, force_native_engine, steam_enabled).await?  // launch.rs:136
};
  • spawn_windows_native (process.rs:291-343): direct, no compat layer. Normalises the exe path, builds args (VDF args + user launch_options), resolves the working dir (VDF workingdir → exe parent → install dir), writes steam_appid.txt, sets SteamAppId + user env_variables, then Command::spawn(). This path bypasses the launch pipeline entirely.
  • spawn_game_process (process.rs:345-380): the Linux compat path. It is a thin shim: it builds a PipelineContext (copying in app, launch_info, launcher_config, user_config, proton_path, force_native_engine, steam_enabled), attaches a logging session, then runs LaunchPipeline::with_default_stages(). All command/env construction and the actual Proton invocation happen inside the pipeline, not here. It returns ctx.child.

3.6 Step 6: Track, wait, sync-up

A RunningGame record is written (launch.rs:150-158) so aurelia stop <app_id> can find and kill the process. For the Proton path it also records a per-game compatdata wineprefix (and only a compatdata prefix: sweeping the shared master prefix on stop would also kill the bridged Steam client, launch.rs:141-149). Then it blocks on child.wait(), clears the running record (launch.rs:160-162), and finally sync_ups the cloud (failures logged, never surfaced as a launch error: the game already ran, launch.rs:164-181).

3.7 launch_game, --steam bridged launch, and the master runtime

launch_game (launch.rs:186-196) is a fire-and-forget variant: it spawns through spawn_game_process and returns without waiting (no cloud, no run tracking).

The --steam bridged launch is threaded as the steam_enabled boolean from play_game (§3.1) into PipelineContext.steam_enabled. The behavioural difference is realised downstream in WineTkgRunner::build_env (src/infra/runners/wine_tkg.rs):

  • Bridged (steam_enabled): STEAM_COMPAT_CLIENT_INSTALL_PATH points at the real host Steam (utils::host_steam_client_path()), and the steam-client DLL overrides are left empty so Proton's builtin lsteamclient bridges to the running client.
  • Standalone: STEAM_COMPAT_CLIENT_INSTALL_PATH points at a fake steam trap (utils::setup_fake_steam_trap: dummy steam/steam.sh scripts that exit 0), and the steam DLLs are neutralised (steamclient=n;…).

Distinct from the host bridge is the master Windows Steam runtime: when enabled, WineTkgRunner::prepare_prefix spawns a background Windows steam.exe inside the wineprefix (<proton> run …\steam.exe -silent …) and waits for readiness. It is provisioned by install_master_steam() in src/launch/mod.rs.

3.8 Download driver (update_game / verify_game)

update_game (launch.rs:198) and verify_game (launch.rs:207) both call start_manifest_download(appid, verify_mode, shared_state) (launch.rs:216-574). It resolves the install root + appmanifest, builds ManifestSelections (remote manifests for update, local InstalledDepots for verify), fetches content servers, and downloads each depot from CDN hosts (per-depot key via get_depot_key, manifest request code, CDN auth token) on a spawned Tokio task while a 250 ms ticker forwards DownloadProgress over an mpsc channel. On success it writes a fresh appmanifest. download_aborted (launch.rs:581) checks the abort signal (poison-safe). This path is independent of game launching.


4. Key Types & Functions

Types (defined in src/steam_client.rs)

Type Location Notes
LaunchTarget (enum NativeLinux / WindowsProton) steam_client.rs:143-147 The platform tag that drives the native-vs-Proton branch.
LaunchInfo { app_id, id, description, executable, arguments, workingdir, target } steam_client.rs:149-158 One resolved launch entry.
RawLaunchOption { executable, arguments } steam_client.rs:160-164 Raw PICS config/launch pair (the launch-options command output).
OwnedGame, SharedApp, LibraryGame, ExtendedAppInfo, UfsSaveSpec, StoreAppInfo steam_client.rs Library/metadata DTOs.

parse_launch_info_from_vdf (steam_client.rs:929-1012) is the platform classifier. Decision order ([steam_client.rs:952-979]):

  1. VDF config/oslist: "linux" → NativeLinux, "windows"/other non-empty → WindowsProton, "macos" → skipped.
  2. Else executable extension: .exe/.bat → WindowsProton, .sh/contains "linux" → NativeLinux.
  3. Else host OS default. Entries sorted to prefer key "0".

Functions (this page's two files)

Function File:line Role
play_game launch.rs:8 Launch orchestrator, native/Proton decision, cloud, wait.
launch_game launch.rs:186 Fire-and-forget pipeline launch.
update_game / verify_game / start_manifest_download launch.rs:198 / 207 / 216 Depot download/verify engine.
fetch_owned_games / fetch_family_shared_apps library.rs:8 / 51 Library discovery.
get_product_info library.rs:393 PICS → Vec<LaunchInfo> for the launcher.
get_extended_app_info / fetch_store_apps / fetch_ufs_save_specs / fetch_online_required library.rs:255 / 345 / 303 / 316 App/store metadata.
check_for_updates / installed_depots_need_update library.rs:119 / 454 Update detection.

5. Cross-Module Interactions

play_game (steam_client/launch.rs:8)
 ├─ get_product_info (steam_client/library.rs:393)
 │    └─ parse_launch_info_from_vdf (steam_client.rs:929)  → Vec<LaunchInfo>
 ├─ [native_windows] spawn_windows_native (steam_client/process.rs:291)  → Command::spawn  (NO pipeline, NO Proton)
 └─ [else]           spawn_game_process  (steam_client/process.rs:345)
        └─ LaunchPipeline::with_default_stages().run()   (src/launch/pipeline.rs)
             1. ResolveGameStage        (src/launch/stages/resolve_game.rs)
             2. ResolveProfileStage     (src/launch/stages/resolve_profile.rs)
             3. ResolveComponentsStage  (src/launch/stages/resolve_components.rs)  ← picks Runner
             4. ResolveDllProvidersStage(src/launch/stages/resolve_dll_providers.rs)
             5. PreparePrefixStage      (src/launch/stages/prepare_prefix.rs)      → runner.prepare_prefix
             6. BuildEnvironmentStage   (src/launch/stages/build_environment.rs)   [no-op]
             7. BuildCommandStage       (src/launch/stages/build_command.rs)       → runner.build_command → CommandSpec
             8. PreflightStage          (src/launch/stages/preflight.rs)
             9. SpawnProcessStage       (src/launch/stages/spawn_process.rs)       → runner.launch → Child
            10. FinalizeStage           (src/launch/stages/finalize.rs)
Module Path Relationship to launch.rs
Launch pipeline src/launch/pipeline.rs spawn_game_process constructs a PipelineContext and runs the default stages. Carries proton_path, force_native_engine, steam_enabled. Proton is invoked here only indirectly, via runner.build_command() (BuildCommandStage) and runner.launch() (SpawnProcessStage).
Runner selection src/launch/stages/resolve_components.rs:63-85 Second native/Proton decision. Linux + wants_luxtorpeda (force --native-engine / pinned) → LuxtorpedaRunner. Else by launch_info.target: NativeLinux → NativeRunner, WindowsProton → WineTkgRunner.
Wine/Proton runner src/infra/runners/wine_tkg.rs build_env (env: STEAM_COMPAT_*, WINEPREFIX, WINEDLLOVERRIDES, GPU PRIME, host-vs-fake-steam bridge), build_command (assembles the full argv), prepare_prefix (compatdata + master Steam), launch (spawn).
Runner command builder src/utils.rs:6-38 (build_runner_command) The single chokepoint that emits proton run vs bare wine. WineTkgRunner::build_command calls it, then appends the game exe + args.
Proton manager src/proton.rs Download/list/remove runtimes only: it does not build or run any launch command. compat_tools_dir, list_installed, install_github_package, VALVE_PROTONS. launch.rs references it only indirectly (the chosen proton_version name is later resolved by utils::resolve_runner).
Steam host helpers src/utils.rs ensure_steam_running, is_steam_running, host_steam_client_path, setup_fake_steam_trap, steam_wineprefix_for_game.
Running registry src/running.rs record_launch / clear for aurelia stop.
Cloud sync src/cloud_sync.rs CloudClient, CloudPathResolver, UfsSaveSpec.

6. Launch-Relevant Notes (umu integration)

umu-launcher replaces direct proton run invocation with a unified runtime launcher (umu-run, configured via GAMEID/PROTONPATH/STORE/WINEPREFIX env). Below is every decision point where Proton/Wine vs native is chosen and every place the command + environment is assembled, in call order.

6.1 Native-vs-Proton decision points (in order)

  1. play_game native gate: native_windows at launch.rs:82-83 and chosen_proton_path at launch.rs:85-94. If native_windows is true the entire pipeline (and thus umu) is bypassed via spawn_windows_native. umu only matters for the else branch. chosen_proton_path (proton_path ?? global proton_version) is the Proton runtime that umu would receive as PROTONPATH.
  2. play_game spawn branch (launch.rs:133-137): native_windows ? spawn_windows_native : spawn_game_process. The wineprefix-recording branch at launch.rs:141-149 also assumes Proton semantics (per-game compatdata). umu uses the same compatdata layout, so this should remain valid.
  3. ResolveComponentsStage (src/launch/stages/resolve_components.rs:63-85) is the canonical runner selection: NativeRunner / WineTkgRunner / LuxtorpedaRunner. umu replaces what WineTkgRunner does, either as a new UmuRunner selected here, or by swapping the command/env builders inside WineTkgRunner.
  4. VDF platform classifier (steam_client.rs:952-979): produces the LaunchTarget (NativeLinux/WindowsProton) that decisions 1–3 read. Not a Proton-invocation site, but the upstream source of truth.

6.2 Command + environment assembly points (where umu slots in)

Concern Location What umu changes
Runner binary → command src/utils.rs:6-38 build_runner_command The one place emitting <proton> run (line 22-25) vs bare wine (27-29). Replace with umu-run construction.
Full argv assembly WineTkgRunner::build_command (src/infra/runners/wine_tkg.rs) Calls build_runner_command, appends game exe + VDF args + user launch options → CommandSpec. umu wants umu-run <exe> <args>.
Environment WineTkgRunner::build_env (src/infra/runners/wine_tkg.rs) Sets STEAM_COMPAT_APP_ID, STEAM_COMPAT_DATA_PATH, WINEPREFIX, STEAM_COMPAT_CLIENT_INSTALL_PATH (host bridge vs fake trap), WINEDLLOVERRIDES, WINEDLLPATH/WINEPATH, GPU PRIME, MangoHud, WINEDEBUG. umu expects GAMEID, PROTONPATH, STORE, WINEPREFIX. Map them here.
Prefix / compatdata creation WineTkgRunner::prepare_prefix (src/infra/runners/wine_tkg.rs) Creates steamapps/compatdata/<appid> (umu/Proton both require it) and optionally the background Windows Steam runtime.
Final spawn WineTkgRunner::launch (src/infra/runners/wine_tkg.rs) + SpawnProcessStage Spawns the CommandSpec. Unchanged conceptually for umu.
Runtime resolution src/utils.rs resolve_runner / src/proton.rs compat_tools_dir/list_installed How the Proton runtime named by chosen_proton_path is located on disk (Steam common, compatibilitytools.d, Lutris, fuzzy). umu would consume the resolved path as PROTONPATH.

6.3 Bridged-Steam env interaction (must be preserved under umu)

The --steam/Family-Sharing bridge (§3.7) is enforced via steam_enabled and realised in build_env (host STEAM_COMPAT_CLIENT_INSTALL_PATH + empty steam-DLL overrides vs fake trap + neutralised overrides). Since umu reuses STEAM_COMPAT_CLIENT_INSTALL_PATH, the bridged-vs-standalone branch should carry over unchanged, but the steam-DLL-override neutralisation and the in-prefix master Steam runtime (prepare_prefix) must be re-applied in whatever umu-based runner replaces WineTkgRunner.

Key takeaway: launch.rs decides whether Proton is used (native_windows / chosen_proton_path) but never how. The actual Proton command is build_runner_command (utils.rs:6) wrapped by WineTkgRunner::build_command/build_env/launch. src/proton.rs is download-only. umu integration touches utils::build_runner_command and the WineTkgRunner builders, plus the runner selection in resolve_components.rs, not play_game's decision logic.

Clone this wiki locally