Skip to content

Commit 16745c2

Browse files
fix(steam): follow recommended Proton runtime (#198)
## Summary - follow Darktide-specific and global Steam compatibility-tool mappings before falling back to the appinfo `recommended_runtime` - collect the recommendation and Valve-managed tool registry in one bounded appinfo scan while preserving authoritative invalid mappings - add reusable Steam Deck OS-release identity detection without coupling it to Proton selection - update Steam architecture, reference, and Steam Deck documentation with regression coverage for the live appinfo shape ## Verification - `dotnet build src/modificus-curator.sln --configuration Release` (0 warnings) - `dotnet test src/modificus-curator.sln --configuration Release` (1,855 passed) - `dotnet format src/modificus-curator.sln --verify-no-changes` - `git diff --check`
1 parent 93d9810 commit 16745c2

14 files changed

Lines changed: 1286 additions & 258 deletions

AGENTS.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -882,9 +882,20 @@ src/ Modificus Curator -- the mod manager app (.NET 10 + Avalonia 12)
882882
IProfileService.GetModList)
883883
steam/ Modificus.Curator.Steam -- Steam + Darktide + Proton discovery
884884
(multi-library + compatdata; Linux Proton resolves from Steam's
885-
CompatToolMapping in config.vdf to a custom compatibilitytool.vdf
886-
or a Valve-managed appinfo/appmanifest install, never a directory-name
887-
guess; Steam text KV1 parsing centralized through SteamTextVdf with
885+
CompatToolMapping in config.vdf, app-specific entry first then
886+
the global "0" entry, to a custom compatibilitytool.vdf
887+
or a Valve-managed appinfo/appmanifest install, never a
888+
directory-name guess; with neither mapping, Darktide's appinfo
889+
recommended_runtime (the steam_deck_compatibility metadata) is
890+
Steam's non-user default on any Linux host (one appinfo.vdf
891+
scan collects both the compat_tools registry + the
892+
recommendation; an invalid/unreadable mapping or a
893+
native/missing recommendation fails unresolved without falling
894+
through); Steam Deck identity (SteamDeckDetector +
895+
SteamDiscoveryOptions.IsSteamDeck, detected from OS release
896+
metadata ID=steamos + VARIANT_ID=steamdeck, host file first)
897+
is a generic platform identity input, not Proton policy;
898+
Steam text KV1 parsing centralized through SteamTextVdf with
888899
HasEscapeSequences always on, ValveKeyValue 0.70.0.499), the
889900
ISteamService.Discover automatic/manual mode policy + Rediscover
890901
forced-automatic surface, IsGameRunning (WinProcessLookup
@@ -1002,6 +1013,13 @@ src/ Modificus Curator -- the mod manager app (.NET 10 + Avalonia 12)
10021013
atomic missing-only persistence) + the
10031014
ModDisplayMetadataMapper normalization
10041015
Modificus.Curator.Steam.Tests/ xUnit tests for discovery + IsGameRunning
1016+
(incl. the Proton selection precedence: app-specific,
1017+
global, invalid-entry no-fall-through, + the appinfo
1018+
recommended-runtime fallback end-to-end, identical
1019+
regardless of Deck identity; the appinfo reader
1020+
one-pass snapshot against a realistic multi-entry
1021+
fixture matching the live shape; + the OS-release
1022+
Deck detector)
10051023
Modificus.Curator.RelayClient.Tests/ xUnit tests for the launch façade (dual-purpose:
10061024
`dotnet test` = xUnit; `dotnet run` = composition smoke harness);
10071025
covers RelayLaunchServiceTests (Windows + Linux arg
@@ -1264,7 +1282,7 @@ dotnet run --project src/ui --configuration Release # app shell window
12641282
boundary; ordered env-var entries + game args; validated up front via the
12651283
shared `LaunchSettingsValidator`, applied at launch; `GetLaunchSettings` is the
12661284
focused read the launch path uses),
1267-
**Steam** (Steam + Darktide + Proton discovery via Steam's CompatToolMapping + the automatic/manual mode policy + `Rediscover` + `IsGameRunning`),
1285+
**Steam** (Steam + Darktide + Proton discovery via Steam's CompatToolMapping with the appinfo recommended-runtime fallback + the automatic/manual mode policy + `Rediscover` + `IsGameRunning`),
12681286
**Integrations** (the Nexus v1 client/auth +
12691287
`IModAcquisitionService` the download + extract + place orchestrator +
12701288
`IUpdateCheckService` the Nexus-only update-check service +

STEAMDECK.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ Do not force a Proton compatibility tool on the Curator shortcut. Curator is a
5252
native Linux app. It launches Darktide through the game's own Proton
5353
environment: in automatic discovery mode (the default), Curator reads the
5454
Proton tool Steam has selected for Darktide and uses that, so changing Darktide's
55-
compatibility tool in Steam is all you need to do. The Curator shortcut itself
56-
always stays native.
55+
compatibility tool in Steam is all you need to do. If Steam has no explicit
56+
selection for Darktide, Curator follows the runtime Steam records as recommended
57+
for Darktide in its app metadata. The Curator shortcut itself always stays
58+
native.
5759

5860
Because the installer writes one stable AppImage path, the Steam shortcut stays
5961
valid across Curator's in-app updates.

docs/architecture/MODIFICUS-CURATOR.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -667,8 +667,11 @@ Responsibilities:
667667
Steam's `CompatToolMapping` (the app-specific entry for Darktide is
668668
authoritative; the global entry is the fallback), then resolving that tool
669669
name to a `proton` binary from a custom `compatibilitytools.d` manifest or a
670-
Valve-managed install. This follows Darktide's own Steam compatibility
671-
selection rather than guessing from directory names.
670+
Valve-managed install. With neither mapping, Darktide's appinfo
671+
`recommended_runtime` is Steam's non-user default on any Linux host; an
672+
invalid or unreadable mapping fails rather than bypassing a possible user
673+
choice. This follows Darktide's own Steam compatibility selection rather
674+
than guessing from directory names.
672675
- **Escape hatch:** when auto-discovery can't resolve any of the above,
673676
prompt the user for the missing path(s). This is possible only because
674677
discovery lives in the UI app, not in Relay (which has no UI and could

docs/reference/steam.md

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,12 @@ every OS-specific input + platform seam is injected:
8888
Notable fields: `LinuxDefaultSteamRoot`, `LinuxFlatpakSteamRoot`,
8989
`LinuxCompatibilityToolsDir`, `LinuxSystemCompatibilityToolsDirs` (the two
9090
standard Steam system directories, defaulting so `/usr/share/steam/compatibilitytools.d`
91-
is searched), `WindowsDefaultSteamRoot`, `DarktideAppId` (`1361210`),
92-
`DarktideCommonDir`, `GameBinaryName`, `GameProcessName`.
91+
is searched), `IsSteamDeck` (a platform identity input: whether the host is a
92+
Steam Deck; `CreateDefault()` detects it from OS release metadata --
93+
`ID=steamos` + `VARIANT_ID=steamdeck`, reading `/run/host/os-release` before
94+
`/etc/os-release`; tests can inject a fixed value), `WindowsDefaultSteamRoot`,
95+
`DarktideAppId` (`1361210`), `DarktideCommonDir`, `GameBinaryName`,
96+
`GameProcessName`.
9397
- `ISteamDiscoverer` (internal) -- `Discover() → DiscoveryResult`. The
9498
platform-specific discovery strategy. Two implementations
9599
(`LinuxSteamDiscoverer`, `WindowsSteamDiscoverer`), selected once at DI time
@@ -104,15 +108,24 @@ every OS-specific input + platform seam is injected:
104108
composition, not inheritance -- each discoverer injects the core and layers its
105109
own platform steps on top.
106110
- `ProtonResolver` (internal) -- the Linux Proton compatibility-tool resolver,
107-
constructed by `LinuxSteamDiscoverer`. Reads Steam's `CompatToolMapping` from
108-
`config.vdf`, then resolves the selected tool name to a `proton` binary as a
109-
custom tool (a `compatibilitytool.vdf` manifest) or a Valve-managed tool
110-
(appinfo + appmanifest). See [Linux Proton resolution](#linux-proton-resolution).
111+
constructed by `LinuxSteamDiscoverer`. Reads Steam's tool selection (the
112+
app-specific `CompatToolMapping` entry, then the global `"0"` entry, then the
113+
appinfo recommended runtime when both mappings are absent), then resolves the
114+
selected name to a `proton` binary as a custom tool (a
115+
`compatibilitytool.vdf` manifest) or a Valve-managed tool (appinfo +
116+
appmanifest). See [Linux Proton resolution](#linux-proton-resolution).
111117
- `SteamAppInfoReader` (internal) -- parses Steam's binary `appinfo.vdf`
112-
container (versions 39-41) and extracts the first app entry carrying a
113-
`compat_tools` collection. Used by `ProtonResolver` for Valve-managed tool
114-
resolution. Has an internal `ReadCompatTools(Stream)` overload so tests feed a
118+
container (versions 39-41) in one scan that collects the first app entry
119+
carrying a `compat_tools` collection and the requested app's recommended
120+
runtime (either may be absent; a requested app id of 0 skips the runtime
121+
lookup so the scan stops at the first registry). Used by `ProtonResolver` for
122+
Valve-managed tool resolution + the no-user-mapping runtime fallback. Has an
123+
internal `ReadSnapshot(Stream, requestedAppId)` overload so tests feed a
115124
synthetic binary fixture.
125+
- `SteamDeckDetector` (internal static) -- the production Steam Deck detection
126+
behind the `SteamDiscoveryOptions.IsSteamDeck` platform identity input (see
127+
above). Quoted values are tolerated; IO/access failures degrade to "not a
128+
Deck".
116129
- `SteamTextVdf` (internal static) -- the single entry point for Steam text KV1
117130
parsing (`config.vdf`, `compatibilitytool.vdf`, `appmanifest_*.acf`). Wraps
118131
ValveKeyValue with `HasEscapeSequences = true` always on (Steam files
@@ -222,32 +235,52 @@ flipping the mode.
222235

223236
### Linux Proton resolution
224237

225-
`ProtonResolver` reads the tool Steam actually selected for Darktide, rather
238+
`ProtonResolver` resolves the tool Steam actually selected for Darktide, rather
226239
than guessing from directory names. The steps are best-effort: a missing or
227240
unreadable file degrades to an unresolved Proton (warning), never a throw.
228241

229242
1. **Selected tool name** -- `<steamRoot>/config/config.vdf`
230-
`Software > Valve > Steam > CompatToolMapping`. The app-specific mapping for
231-
Darktide's app id (`1361210`) is authoritative when present; its `name` is
232-
used as-is, and an empty or malformed name fails resolution without falling
233-
through. The global `"0"` mapping is consulted only when the app-specific
234-
mapping is absent. No mapping at all → unresolved.
243+
`Software > Valve > Steam > CompatToolMapping`, with this precedence:
244+
- The app-specific mapping for Darktide's app id (`1361210`) is
245+
authoritative when present; its `name` is used as-is.
246+
- The global `"0"` mapping is considered only when the app-specific mapping
247+
is absent, and is authoritative when present.
248+
- Only when both are absent, Darktide's appinfo
249+
`common/steam_deck_compatibility/configuration/recommended_runtime` is
250+
Steam's non-user default and supplies the name on any Linux host; host
251+
identity (Steam Deck or not) is not consulted for this decision.
252+
- A present mapping whose `name` is missing, non-string, empty, or
253+
whitespace is **invalid**: resolution fails without falling through (this
254+
covers both the app-specific and the global entry, and an invalid
255+
app-specific entry blocks the global entry too). Likewise, a selected
256+
mapping whose named tool cannot be resolved stays authoritative and never
257+
falls through to the recommendation, and a `config.vdf` that exists but
258+
cannot be read or parsed fails unresolved rather than bypassing a
259+
possible user choice. A missing config file, or a valid config with
260+
neither key, counts as no user mapping and permits the recommended-runtime
261+
fallback.
262+
- A missing, empty, whitespace, `native`, or unresolvable recommendation
263+
yields unresolved with a warning; no other runtime is guessed.
235264
2. **Custom tool** -- a `compatibilitytool.vdf` manifest whose `compat_tools`
236-
collection defines the selected name, searched across every
237-
compatibility-tool root in order: the resolved Steam root's
238-
`compatibilitytools.d`, the configured user root, then the system roots
239-
(including `/usr/share/steam/compatibilitytools.d`). Each root is checked
240-
root-level first (Valve permits a manifest directly at the root with a
241-
relative or absolute `install_path`), then its per-tool subdirectories. The
242-
resolved `install_path`'s `proton` file must exist.
265+
collection defines the selected name, searched across every
266+
compatibility-tool root in order: the resolved Steam root's
267+
`compatibilitytools.d`, the configured user root, then the system roots
268+
(including `/usr/share/steam/compatibilitytools.d`). Each root is checked
269+
root-level first (Valve permits a manifest directly at the root with a
270+
relative or absolute `install_path`), then its per-tool subdirectories. The
271+
resolved `install_path`'s `proton` file must exist. This runs first for any
272+
selected name, including the recommended runtime.
243273
3. **Valve-managed tool** -- the `compat_tools` entry in
244-
`<steamRoot>/appcache/appinfo.vdf` (binary; parsed by
245-
`SteamAppInfoReader`) whose key or comma-separated alias matches the selected
246-
name, then `appmanifest_<appid>.acf` across the libraries, parsing its
247-
`installdir`, requiring
248-
`<library>/steamapps/common/<installdir>/proton` to exist.
274+
`<steamRoot>/appcache/appinfo.vdf` (binary; parsed by
275+
`SteamAppInfoReader`) whose key or comma-separated alias matches the selected
276+
name, then `appmanifest_<appid>.acf` across the libraries, parsing its
277+
`installdir`, requiring
278+
`<library>/steamapps/common/<installdir>/proton` to exist. One `Resolve`
279+
call parses `appinfo.vdf` at most once: when the recommended runtime
280+
supplies the name, the snapshot already read for the recommendation is
281+
reused here.
249282
4. Nothing resolves → `null` (escape hatch; UI prompts). A reason is appended to
250-
`Warnings`.
283+
`Warnings`.
251284

252285
`ProtonVersion` carries the tool's `display_name` when present (the custom
253286
manifest's, or the appinfo entry's), otherwise the internal tool name.
@@ -399,10 +432,15 @@ no-op).
399432
`Modificus.Curator.Steam.Tests` covers Linux discovery (`LinuxDiscoveryTests`,
400433
`FlatpakDiscoveryTests`), Windows discovery (`WindowsDiscoveryTests`), Proton
401434
compatibility-tool selection (`ProtonSelectionTests`, `ProtonResolverTests` --
402-
app-specific vs global mapping, custom vs Valve-managed tool resolution, root-
403-
level vs subdirectory custom manifests, system roots), the binary
404-
`appinfo.vdf` reader (`SteamAppInfoReaderTests`, against a compact synthetic v41
405-
fixture with a string table), the `SteamTextVdf` escape-semantics helper
435+
app-specific vs global mapping vs the appinfo recommended-runtime fallback
436+
(identical regardless of Deck identity) and every blocking rule around it,
437+
custom vs Valve-managed tool resolution, root-level vs subdirectory custom
438+
manifests, system roots), the binary
439+
`appinfo.vdf` reader (`SteamAppInfoReaderTests`, against compact synthetic v41
440+
fixtures plus a realistic multi-entry fixture matching the live appinfo shape --
441+
Darktide's `recommended_runtime` + the Steam Play manifest's `compat_tools` in
442+
both entry orders), the Steam Deck OS-release detector (`SteamDeckDetectorTests`), the
443+
`SteamTextVdf` escape-semantics helper
406444
(`SteamTextVdfTests`, including a sanitized realistic `config.vdf` fixture with
407445
an escaped JSON scalar), the `libraryfolders.vdf` parser
408446
(`LibraryFoldersVdfTests`), game-running detection (`GameRunningTests`,

0 commit comments

Comments
 (0)