From 2bca89c5557803173defd392c3ecbe5feb524301 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Mon, 15 Jun 2026 07:59:21 +0000 Subject: [PATCH] feat(host/windows): Steam library auto-discovery on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Steam `LibraryProvider` keyed off `$HOME` + Linux paths, so the game library was empty on Windows. Add Windows discovery: the default Steam install dirs under Program Files (`ProgramFiles(x86)`/`ProgramFiles`/ `ProgramW6432`), with games on other drives picked up via each root's `libraryfolders.vdf` — whose Windows values are backslash-escaped, so unescape `\\` → `\`. The existing root-scan/dedup logic is shared via a new `steam_roots_existing` helper. The custom store (mgmt JSON CRUD) was already cross-platform; only Steam auto-discovery was Linux-only. Not yet covered: a non-default Steam install dir (the registry `Valve\Steam\InstallPath`). Degrades gracefully — no Steam → empty list. clippy -D warnings + library tests green on Windows and Linux. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/punktfunk-host/src/library.rs | 35 +++++++++++++++++++++++++--- docs/windows-host.md | 1 + 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/punktfunk-host/src/library.rs b/crates/punktfunk-host/src/library.rs index 75f2a11..7f267d8 100644 --- a/crates/punktfunk-host/src/library.rs +++ b/crates/punktfunk-host/src/library.rs @@ -127,6 +127,7 @@ fn steam_art(appid: u32) -> Artwork { } /// Candidate Steam roots (classic, Flatpak, Deck) that actually exist, canonicalized + deduped. +#[cfg(not(target_os = "windows"))] fn steam_roots() -> Vec { let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else { return Vec::new(); @@ -137,6 +138,25 @@ fn steam_roots() -> Vec { home.join(".steam/root"), home.join(".var/app/com.valvesoftware.Steam/.local/share/Steam"), // Flatpak Steam ]; + steam_roots_existing(candidates) +} + +/// Windows Steam roots: the default install dirs under Program Files. Games installed on other +/// drives are still found via each root's `libraryfolders.vdf` (see [`steam_library_dirs`]). A +/// non-default Steam install dir (registry `Valve\Steam\InstallPath`) isn't covered yet. +#[cfg(target_os = "windows")] +fn steam_roots() -> Vec { + let mut candidates = Vec::new(); + for var in ["ProgramFiles(x86)", "ProgramFiles", "ProgramW6432"] { + if let Some(pf) = std::env::var_os(var) { + candidates.push(PathBuf::from(pf).join("Steam")); + } + } + steam_roots_existing(candidates) +} + +/// Keep only the candidate roots that exist (have a `steamapps` dir), canonicalized + deduped. +fn steam_roots_existing(candidates: impl IntoIterator) -> Vec { let mut seen = HashSet::new(); let mut roots = Vec::new(); for c in candidates { @@ -174,12 +194,21 @@ fn steam_library_dirs() -> Vec { } /// Pull every `"path" ""` value out of a `libraryfolders.vdf`. We don't need a full VDF -/// parser for the two flat fields we read — Linux library paths never contain the `"` or `\` -/// that would require unescaping. +/// parser for the two flat fields we read. On Windows the values are backslash-escaped +/// (`D:\\SteamLibrary`), so unescape `\\` → `\`; Linux paths need no unescaping. fn vdf_paths(text: &str) -> Vec { text.lines() .filter_map(|l| vdf_value(l.trim(), "path")) - .map(str::to_string) + .map(|p| { + #[cfg(target_os = "windows")] + { + p.replace("\\\\", "\\") + } + #[cfg(not(target_os = "windows"))] + { + p.to_string() + } + }) .collect() } diff --git a/docs/windows-host.md b/docs/windows-host.md index a29e79a..b39ff9e 100644 --- a/docs/windows-host.md +++ b/docs/windows-host.md @@ -31,6 +31,7 @@ Every OS-touching backend is implemented behind the existing traits and **builds | Host→client audio wiring | ✅ done | builds on MSVC; `m3` `audio_thread` active on Windows (silent VM → no samples to send) | | GameStream (Moonlight) audio | ✅ done | stereo path active on Windows (WASAPI→Opus→RTP/FEC); surround stays Linux-only (libopus multistream / `audiopus_sys`) | | Rumble back-channel (ViGEm) | ✅ done | `request_notification` → background thread → 0xCA; live needs a physical pad | +| Game library (Steam discovery) | ✅ done | Windows Steam roots (Program Files) + VDF other-drive libraries; custom store already cross-platform. Non-default Steam install dir (registry) not yet covered | **Remaining for full parity:** - **Live GPU/in-session validation** — SudoVDA monitor activation, DXGI capture, NVENC encode,