Skip to content

Architecture 08 Library Local

Drackrath edited this page Aug 27, 2026 · 1 revision

08. Library, Local Install Scanning & CM Server List

This page documents the three modules that build Aurelia's view of a user's Steam games and the connection endpoints used to reach Steam:

Module File Responsibility
Library abstraction src/library.rs Merge the owned (account-licensed) library with what is installed on disk, produce the data behind aurelia list, and filter Steam tooling.
Local install scanner src/local_library.rs Discover owned games from the Steam client's own on-disk caches when Aurelia has no network session.
CM server list src/cm_list.rs Resolve the list of Steam Connection Manager (CM) endpoints to dial for the binary Steam protocol.

Note on naming: library.rs contains both the higher-level merge/filter logic and the on-disk appmanifest/libraryfolders.vdf scanner. local_library.rs, despite the name, is the owned-library-from-local-Steam-caches fallback, not the installed-game scanner. See §3.


1. Purpose & Role

There are two distinct "sources of truth" for a Steam library, and Aurelia reconciles them:

  1. Owned library (account licenses). The full set of games a Steam account is licensed to play. Normally fetched over the network (fetch_owned_games, requires aurelia login). When there is no session, it is reconstructed from the local Steam client caches by local_library.rs.
  2. Installed games (bytes on disk). What is actually downloaded into a Steam library folder, discovered by scanning appmanifest_*.acf files. This lives in library.rs (scan_* functions).

library.rs::build_game_library is the join point: it takes an owned list plus an installed map and emits a single GameLibrary of LibraryGame rows that drive aurelia list. Tooling (Proton, runtimes, redistributables) is filtered out of both sides.

network/cache owned games ─┐
                           ├─► build_game_library ─► GameLibrary { Vec<LibraryGame> }
on-disk appmanifest scan ──┘            (src/library.rs:421)

2. library.rs: Library Abstraction, Merge, Filter & On-Disk Scanner

2.1 Tooling filter (online/identity heuristics)

Steam installs runtimes/redistributables/Proton as "apps" that should never appear as launchable titles. They are hidden two ways:

Mechanism Definition Used by
App-id deny-list IGNORED_STEAM_APP_IDS: src/library.rs:13 (e.g. 228980 redistributables, 1493710 Proton Experimental, Steam Linux Runtime ids) is_ignored_steam_app
Name-prefix deny-list IGNORED_STEAM_APP_NAME_PREFIXES: src/library.rs:26 ("Steam Linux Runtime", "Proton", "Steamworks Common Redistributables") is_ignored_steam_app

is_ignored_steam_app(app_id, name) (src/library.rs:34) returns true if either the id is in the deny-list or the (left-trimmed) name starts with a deny prefix. It mirrors Heroic's ignoredSteamAppIds/ignoredSteamAppNamePrefixes. Tests at src/library.rs:544-587 confirm "The Protonist" (a real game) is not filtered.

2.2 Building the merged library: build_game_library

build_game_library(owned, installed_info, steam_id) → GameLibrary (src/library.rs:421).

Two passes:

  1. Owned pass (src/library.rs:432): for each OwnedGame (skipping tooling), look up its InstalledAppInfo in the installed map. is_installed = whether an install path was found. install_path, active_branch come from the manifest if installed. These rows are marked is_owned = true.
  2. Installed-only pass (src/library.rs:462): any app id present on disk but absent from the owned set is not licensed to this account. Marked is_owned = false. If its manifest LastOwner differs from the logged-in steam_id, it is flagged is_family_shared = true (src/library.rs:475). When the owner cannot be determined (not logged in, or no LastOwner), Family Sharing is not guessed, which avoids false positives.

owned_app_ids: HashSet (src/library.rs:429) lets the second pass skip already-emitted ids in O(1). Result is sorted by name (src/library.rs:494).

online_required and platform are left None here. online_required is populated later on demand (see §2.4).

2.3 merge_games (legacy/simple merge)

merge_games(owned: Vec<OwnedGame>, installed: Vec<LocalGame>) → Vec<GameModel> (src/library.rs:498). A lighter merge into GameModel keyed by app id: owned games seed the map, and locals fill in install_dir/proton_version (and the name when the owned name is blank). Used where the richer LibraryGame (ownership/family/branch flags) is not needed.

2.4 Online-required heuristic

build_game_library does not compute it. online_required is filled by aurelia list --online (src/main.rs:1599), which calls client.fetch_online_required(app_id) → steam_client::category_online_required (src/steam_client.rs:734): a game is "online required" when it has online multiplayer/MMO/online-co-op categories and no single-player category. Requires an authenticated, online session. Otherwise the column is left unknown (src/main.rs:1608).

2.5 On-disk install scanner (also in library.rs)

This is the installed games side. Entry points:

Function File:line Returns
scan_installed_app_info src/library.rs:87 HashMap<u32, InstalledAppInfo> (richest)
scan_installed_app_paths src/library.rs:119 HashMap<u32, String> (path strings)
scan_installed_app_paths_pathbuf src/library.rs:127 HashMap<u32, PathBuf>
find_local_games src/library.rs:73 Vec<LocalGame>
scan_library_info src/library.rs:135 scan one root + its sibling libraries
all_library_roots src/library.rs:228 every discoverable library root

Root resolution order (scan_installed_app_info, src/library.rs:87):

  1. LauncherConfig.steam_library_path if it contains a steamapps (or Steam/steamapps) dir (src/library.rs:89).
  2. detect_steam_path() (auto-detect).
  3. default_steam_root() (src/library.rs:258): platform default (~/.steam/steam on Linux, Program Files (x86)\Steam on Windows).

If windows_steam_discovery_enabled is set, it additionally scans a Windows-Steam install inside a wine prefix at drive_c/Program Files (x86)/Steam (src/library.rs:100-114), but prefers the native Linux install on duplicates (installed.entry(app_id).or_insert(info)).

Library-folder enumeration (scan_library_info, src/library.rs:135): starts with the given root, then adds:

  • folders from steamapps/libraryfolders.vdf via parse_library_folders (src/library.rs:292),
  • a probe of all connected drives via discover_drive_libraries (src/library.rs:191, Windows-only body that checks SteamLibrary, SteamGames, etc. on each drive letter for a steamapps dir).

The list is sorted/deduped, and for each root every appmanifest_*.acf (is_app_manifest, src/library.rs:284) is parsed.

libraryfolders.vdf parsing (parse_library_folders, src/library.rs:292): uses keyvalues-serde to deserialize into LibraryFoldersFile (src/library.rs:45). The LibraryFolderRecord enum (src/library.rs:51, #[serde(untagged)]) tolerates both the legacy "<n>" "<path>" form and the detailed { "path": ... } form. Only numeric keys are accepted (src/library.rs:306).

appmanifest parsing (parse_app_manifest_info, src/library.rs:324): a hand-rolled line scanner (not full VDF) using extract_quoted_values (src/library.rs:401). It reads appid, installdir, name, LastOwner, StateFlags, and (inside the userconfig block) BetaKey → active_branch. Two correctness rules:

  • LastOwner == 0 is treated as "unknown" and dropped (src/library.rs:364).
  • Installed gate: only counted installed if StateFlags & 4 (StateFullyInstalled) is set (src/library.rs:383), which prevents a cancelled partial download (StateUpdateRequired only) from reporting as installed.
  • Install path = <manifest dir>/common/<installdir> (src/library.rs:386).

2.6 Key types in library.rs

Type File:line Notes
InstalledAppInfo src/library.rs:63 install_path, active_branch, name, last_owner (SteamID64 of the install's owner, which differs from the logged-in user for Family-Shared).
LibraryFoldersFile / LibraryFolderRecord src/library.rs:44, :50 serde shapes for libraryfolders.vdf.

3. local_library.rs: Owned Library from Local Steam Caches

Purpose (module doc, src/local_library.rs:1): when Aurelia has no session / is offline, reconstruct the full owned library by reading the Steam client's local caches, with no network and no aurelia login required. On Linux the desktop Steam client is almost always signed in and keeps the whole library cached on disk.

3.1 Data sources

Source Path Used for
appinfo.vdf (binary) appcache/appinfo.vdf resolve app name + type, keep only type == "game".
localconfig.vdf (text) userdata/<id3>/config/localconfig.vdf candidate app ids + Playtime (minutes).
librarycache/ appcache/librarycache/<appid>/ broaden candidate set (Steam pre-fetches owned-title art).
loginusers.vdf (text) config/loginusers.vdf find the MostRecent signed-in account.

3.2 Flow

discover_local_owned_games() (src/local_library.rs:40) → discover_from_root (src/local_library.rs:48). Never errors, and returns empty on missing/unreadable caches so callers use it as best-effort.

  1. steam_install_root() (src/local_library.rs:32): detect_steam_path() filtered to a dir that actually has an appcache/ (the install root, not an arbitrary library folder).
  2. read_local_playtime (src/local_library.rs:91) → locate_localconfig (src/local_library.rs:103): prefer the MostRecent user (most_recent_account_id, src/local_library.rs:127), else any localconfig.vdf. The 32-bit account id (userdata/<id>) is the SteamID64 minus STEAMID64_BASE (76561197960265728, src/local_library.rs:27, account_id_from_id64).
  3. Candidate set = playtime keys ∪ read_librarycache_appids (src/local_library.rs:162).
  4. parse_appinfo (src/local_library.rs:241) resolves names/types. Keep only type == "game" (src/local_library.rs:72). Emits OwnedGame with playtime (defaulting to 0).

3.3 Parsers

  • Text VDF: parse_localconfig_apps (src/local_library.rs:182) walks brace depth, treating any direct child key of an apps object as an app id and capturing Playtime. Tokenized by quoted_tokens (src/local_library.rs:438).
  • Binary appinfo.vdf: parse_appinfo (src/local_library.rs:241) supports container versions 0x27/0x28/0x29. 0x28 adds a per-record VDF sha1. 0x29 interns keys in a trailing string table (parse_string_table, src/local_library.rs:305). Each app record is length-prefixed. walk_object (src/local_library.rs:333) recursively reads the binary KV tree and pulls name/type only when directly inside a common object. Robust: malformed records are skipped, unknown header → empty map. Round-trip test at src/local_library.rs:505.

local_library.rs does not know install paths. It only reconstructs the owned set. Install paths come from library.rs's appmanifest scan.


4. cm_list.rs: Steam Connection Manager (CM) Server List

This is not a game list. It resolves the CM (Connection Manager) endpoints, i.e. the TCP servers that speak Steam's binary protocol, which the Steam client (steam_client) dials to authenticate and fetch data.

Item File:line Notes
get_cm_endpoints() src/cm_list.rs:24 Public entry. Tries the dynamic list and falls back to hard-coded on failure/empty.
fetch_dynamic_cm_list() src/cm_list.rs:31 GETs ISteamDirectory/GetCMListForConnect/v1/?cellid=0&maxcount=20, parses the response.serverlist of "ip:port" strings into SocketAddr.
fallback_cm_endpoints() src/cm_list.rs:49 Parses DEFAULT_CM_ENDPOINTS (src/cm_list.rs:5, five :27017 IPs).
CmListResponseEnvelope / CmListResponse src/cm_list.rs:13, :18 serde shapes for the Steam Directory JSON.

Consumer: src/steam_client/client.rs:193 calls get_cm_endpoints().await to obtain the TCP server pool before connecting.


5. Key Types & Cross-Module Interactions

5.1 Shared models (src/models.rs)

Type File:line Role
OwnedGame src/models.rs:142 account-licensed game (network or local-cache derived).
LocalGame src/models.rs:161 one installed game (install_dir: PathBuf, active_branch).
GameModel src/models.rs:175 simple merged model from merge_games.
LibraryGame src/models.rs:191 rich list row: is_installed, install_path, is_owned, is_family_shared, active_branch, online_required, platform.
GameLibrary src/models.rs:231 { games: Vec<LibraryGame> }.

5.2 Call graph

aurelia list  (src/main.rs:1160-1163, load_library)
   owned  = fetch_owned_games (network)  OR  local_library::discover_local_owned_games (offline fallback)
   inst   = library::scan_installed_app_info
   library::build_game_library(owned, inst, steam_id) ─► GameLibrary
   [--online] steam_client::category_online_required fills online_required (src/main.rs:1599)
   merge_family_shared (src/main.rs:1169)

steam_client / manage / manifests
   library::all_library_roots          (src/steam_client.rs:750, manage.rs:156, manifests.rs:12)

steam_client::client
   cm_list::get_cm_endpoints           (src/steam_client/client.rs:193)

6. Launch-Relevant Notes (for a future umu-launcher integration)

A launch needs two paths: the install directory (game files / exe cwd) and the prefix location (the wine/Proton prefix umu's WINEPREFIX/GAMEID points at). Both are already resolvable in this codebase:

6.1 Install directory

Resolved by the appmanifest scan in library.rs:

  • Source of truth: parse_app_manifest_info builds install_path = <manifest dir>/common/<installdir> (src/library.rs:386), surfaced as InstalledAppInfo.install_path (src/library.rs:64) and copied onto LibraryGame.install_path (src/library.rs:438, :482).
  • For a given app id, use scan_installed_app_paths_pathbuf (src/library.rs:127) or read LibraryGame.install_path directly. The launch pipeline already does this: PipelineContext.resolved_install_dir (src/launch/pipeline.rs:88) is set from app.install_path in preflight (src/launch/stages/preflight.rs:66), and resolve_components/resolve_dll_providers join the exe onto it (src/launch/stages/resolve_components.rs:25-32, src/launch/stages/resolve_dll_providers.rs:31). A missing install path is a LaunchErrorKind::GameData ("Install path missing").
  • Note the install path can live on any library root, not just the main Steam dir (multiple steamapps roots via libraryfolders.vdf + drive probing). Always derive it from the scanned manifest, never assume ~/.steam/steam/steamapps/common.

6.2 Prefix location (compatdata)

Prefix resolution is not in these three modules. It is utils::steam_wineprefix_for_game (src/utils.rs:1225):

  • Per-game prefix (SteamPrefixMode::PerGame / use_shared_compat_data): <steam_library_path>/steamapps/compatdata/<app_id>/pfx (src/utils.rs:1244-1249). This is the standard Steam/Proton layout and the natural WINEPREFIX for umu (umu typically manages compatdata/<id>/pfx).
  • Shared/master prefix: resolve_master_wineprefix() (src/utils.rs:1251), backed by get_master_steam_config().wine_prefix (src/utils.rs:869).

The prefix path is selected per app from UserConfigStore (steam-runtime policy + prefix mode). The launch pipeline calls it in prepare_prefix (src/launch/stages/prepare_prefix.rs:36).

6.3 Branch / owner signals umu should respect

  • active_branch (InstalledAppInfo.active_branch, from manifest BetaKey), if a launch must match the installed beta branch.
  • last_owner / is_family_shared (src/library.rs:70, :475): a Family-Shared install is owned by a different SteamID64, which is relevant if umu/Proton needs the licensing account.

6.4 Suggested umu wiring (summary)

umu input Where to get it
STORE=steam, game files / exe cwd LibraryGame.install_path ← scan_installed_app_info (src/library.rs:87)
WINEPREFIX utils::steam_wineprefix_for_game (src/utils.rs:1225): compatdata/<app_id>/pfx in per-game mode
GAMEID / app id LibraryGame.app_id
native vs Proton decision LibraryGame.platform (src/models.rs:221)

Clone this wiki locally