feat(steam): beta branch selector in the STEAM dropdown#2
Open
Leb-Sun wants to merge 26 commits into
Open
Conversation
5 tasks
15ba5f7 to
4d287bd
Compare
Adds a "Launch Options" item to the STEAM menu on the game detail screen that lists the game's appinfo config.launch entries (e.g. ARK's "Launch ARK (No BattlEye)") with a checkmark on the current choice. Selecting one persists as the game's default: - exe -> shortcut extra launch_exe_path + container.executablePath (the priority resolveRelativeGameExe already honors; non-default exes ride the existing WN_STEAM_DIRECT_EXE override) - the option's own arguments -> new shortcut extra launch_exe_args, kept separate from the user's custom execArgs LaunchInfo gains an `arguments` field (defaulted, so cached appinfo JSON stays decodable) parsed from appinfo config/launch/arguments. A shared SteamUtils.combineSteamLaunchArgs joins option args with custom args across all three launch channels (direct command line, ColdClientLoader.ini ExeCommandLine, localconfig.vdf LaunchOptions / GetLaunchCommandLine). It honors Steam's %command% placeholder in custom args. The steam-env warm stamp now includes the effective LaunchOptions line so a changed selection re-runs the localconfig edit, and warm launches re-push the per-process launch command line. The submenu reuses the existing StoreSourceActionPopup surface and StoreSourceMenuItem styling; hidden unless the game is installed and has 2+ distinct options. https://claude.ai/code/session_01EUsZxzAWCjSh8LYRS9W3vT
The v1 selector only existed on the store detail screen (StoreGameDetailScreen); tapping an installed game in the library grid opens LibraryGameDetailDialog -> LibraryGameLaunchScreen, which has its own duplicated STEAM dropdown - so the feature was invisible from the library. Mirror the submenu there (same two-page popup, using this screen's Launch* styling) and wire it in LibraryGameDetailDialog for Steam games only. Also fix two latent issues that could hide or weaken the selector: - Cached appinfo rows predate LaunchInfo.arguments, so options that differ only by args deduped down to one entry and fell under the 2-option visibility gate. Dedup now includes the option label, and both dialogs do a two-phase load: show cached options immediately, then SteamService.refreshAppInfoFromPics (new public wrapper around fetchLatestSteamAppInfo + persistLatestSteamAppInfo) re-fetches the app's PICS appinfo and reloads, healing upgraded installs on first open. - Extracted the option-list builder and selection persistence into shared UnifiedActivity helpers used by both screens, replacing GameManagerDialog's inline copies. https://claude.ai/code/session_01EUsZxzAWCjSh8LYRS9W3vT
The in-popup submenu rendered as an awkward full-width overlay. Replace it with a centered modal window that follows the Workshop window's design language: dimmed scrim, 560dp-max rounded Surface, header with an accent icon badge + LAUNCH OPTIONS overline + game title + count chip + close button, and a divided row list. Rows show the option label with its executable/arguments as the secondary line; tapping persists the selection and moves the check mark. - New LaunchOptionsScreen.kt mirroring WorkshopScreen's palette and layout (all vector icons; no new drawable assets needed). - Both STEAM dropdowns return to a flat menu whose "Launch Options" item opens the window (showLaunchOptions/onLaunchOptions params replace the list plumbing through the SourceTags). - LaunchOptionsDialog host added next to WorkshopDialog in both GameManagerDialog and LibraryGameDetailDialog. https://claude.ai/code/session_01EUsZxzAWCjSh8LYRS9W3vT
Findings from a full-branch review applied: - PICS appinfo re-fetch now runs once per app per process (retried on failure) instead of on every game-detail open, and the two-phase load + refresh logic is shared by both dialogs via loadSteamLaunchOptionsRefreshing — drops a network round-trip and a duplicate shortcut scan from repeat opens. - LaunchOptionsDialog owns selection persistence (appId + onSelectionSaved), removing the duplicated persist lambda from both host sites. - XServerDisplayActivity: single combineWithSelectedLaunchArgs resolver replaces the getExtra+combine pattern repeated across the direct-launch and both ColdClient INI sites. - Steam-env stamp stores the raw effective LaunchOptions line instead of its hashCode — exact equality, no silent collision skips. - StoreLaunchOptionItem moved to LaunchOptionsScreen.kt where the picker lives. https://claude.ai/code/session_01EUsZxzAWCjSh8LYRS9W3vT
Adds a "Beta Branch" menu item to the STEAM ▾ dropdown on both game detail screens (StoreGameDetailScreen / LibraryGameLaunchScreen). Tapping it opens a Workshop-styled picker (BetaBranchScreen.kt) listing the game's PICS branches with the current one check-marked. Password-protected branches are shown disabled (no beta-password flow exists in this app). • BetaBranchScreen.kt — new Workshop-shaped modal; mirrors the structure of LaunchOptionsScreen.kt with AltRoute icon and per-file palette constants. • Menu item hidden when the game has only one branch (public), mirroring the >= 2 gate used by Launch Options. • Persistence: SteamService.setSelectedBetaBranch() stores the choice as the "selectedBranch" shortcut extra (blank = public/default); SteamService.resolveSelectedBetaName() refactored to reuse findSteamShortcut() instead of its own loadShortcuts() scan. • Downstream already honors the selection (resolveDepotManifestInfo, setAppCurrentBeta, ACF buildId); after a successful save the dialog calls startUpdateCheck() so the new branch's build downloads. • SteamUtils.writeCompleteSettingsDir: configs.app.ini [app::general] now writes is_beta_branch/branch_name from the actual selected branch instead of always hardcoding public, so gbe_fork's GetCurrentBetaName() / GetAppBuildId() return correct values. branches.json already lists all branches correctly — no change needed. • Appinfo refresh guard generalised: launchOptionsRefreshedApps renamed to appinfoRefreshedApps so one PICS round-trip per process feeds both the Launch Options and Beta Branch pickers. https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
startUpdateCheck was calling checkForAppUpdate(appId) without a branch, so the comparison always ran against the public manifests and reported "no updates found" even after switching branches. The private downloadApp overload (routing downloadAppForUpdate) also hardcoded branch = "public", so the download itself would have fetched the wrong manifests even if the check had passed. Fix: resolve resolveSelectedBetaName before the update check and pass it to checkForAppUpdate; resolve it again inside the private downloadApp overload instead of hardcoding "public". Fresh installs (no shortcut yet) resolve to "" → "public" unchanged. https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
…ownload hot paths
Clicking install/check-update crashed after the branch-selector fix:
resolveSelectedBetaName ran ContainerManager().loadShortcuts(), whose
Shortcut constructor decodes full cover-art bitmaps for every shortcut,
runs PE icon extraction over game exes and can rewrite .desktop files.
Doing that on every download/update click spikes the heap (OOM lands on
whatever thread allocates next — runCatching can't save the app) and
races the UI's own .desktop reads.
resolveSelectedBetaName now parses the [Extra Data] section of the
.desktop files directly — no Shortcut construction, no bitmap decode,
no writes. The heavyweight findSteamShortcut path remains only in the
one-off setter (setSelectedBetaBranch).
Also:
• checkForAppUpdate / isUpdatePending default their branch parameter to
the game's selected beta, so every caller (startUpdateCheck, the
store dialog's onDownloadUpdate) is branch-aware without per-site
resolution; the UnifiedActivity call site is back to one line.
• startUpdateCheck wraps downloadAppForUpdate in runCatching — a failed
update start now shows the failure popup instead of crashing.
• completeAppDownload prunes stale "{depotId}_{gid}.manifest" caches
not matching depot.config — branch switches no longer accumulate old
manifests, and checkForAppUpdate's cache fallback can't mistake a
previously-downloaded branch for the installed one. Legacy installs
without depot.config are left untouched.
• BetaBranchScreen: remember() the formatted date; ktlint-safe wrapping.
Known limitation (documented, native-side): the Rust depot writer does
not delete game files absent from the new branch's manifest, so files
removed between branches linger until a fresh install. Fixing that needs
a manifest-diff pass in wn-steam-client.
https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
Applied from a seven-angle review pass (line-scan, removed-behavior, cross-file tracing, reuse, simplification, efficiency, altitude): • "public" comparisons now case-insensitive (equals ignoreCase), matching the existing convention in branches.json / manifest resolution — covers the row label, sort order, selection fallback, blank-mapping on save, and configs.app.ini is_beta_branch. • Branch picker closes after a successful save so the update-check popup it triggers is what the user sees next, not a stale window. • Install/DLC size estimates in GameManagerDialog are sized against the selected branch (resolved once per load) — previously the cards showed public-branch sizes while the download fetched the beta. • readSelectedBranchExtra uses FileUtils.readLines (same reader as Shortcut's parser): one corrupt .desktop file no longer aborts the scan of remaining containers. • Stale-manifest prune extracted to pruneStaleDepotManifestCache next to the other cleanup helpers. • Store onInstall download start wrapped in runCatching — an exception there killed the app (plain coroutine launch, no handler). • Dropped the redundant pwdRequired guard in the picker row callback; the row-level clickable gate already enforces it. https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
getSelectedLaunchOption ran ContainerManager().loadShortcuts() on every game-detail open. Each Shortcut constructed that way decodes the full cover-art bitmap, runs PE icon extraction over the game exe and can rewrite .desktop files — a large transient heap spike and a write race against the UI's own shortcut reads, just to fetch two strings. New readSteamShortcutExtras parses the [Extra Data] section straight from the .desktop files and returns the owning container dir; the container.executablePath fallback is read from the .container config JSON directly. The heavyweight findSteamShortcut path remains only in setSelectedLaunchOption, which must materialize the shortcut to write. https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
The Steam store tab crashed on game tap while the library detail worked. GameManagerDialog runs three data-loading LaunchedEffects the library dialog doesn't all share — and an exception inside a LaunchedEffect tears down the whole Activity. All data-loading effects in GameManagerDialog and the shared launch-options/beta-branches loader effects now degrade on failure (logged, menu items hidden / zero sizes) instead of crashing, with CancellationException rethrown so effect restarts still work. Also dedupes the merged shortcut readers: resolveSelectedBetaName now uses readSteamShortcutExtras from the launch-options branch (which gained the same lightweight .desktop reader for getSelectedLaunchOption) and the selectedBranch-specific copy is gone. Read and write paths now agree on "first STEAM shortcut for the appId". https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
…een signature
The store-tab crash was never a logic bug: ART's bytecode verifier
rejected StoreGameDetailScreenKt itself —
VerifyError: [0x17BC] wide register v5 has type Low-half
Constant/Zero/null
a D8/Compose codegen defect triggered by the composable's huge
parameter list (≈44 params incl. several longs + default masks). The
launch-options build, two parameters smaller, verified fine on the
same device; adding showBetaBranches/onBetaBranches pushed the
generated default-argument handler over the edge. The class failed to
load the moment the store dialog composed it — which is why it crashed
instantly, for every game, store tab only, and no runCatching could
help.
Fix: fold each show-flag + callback pair into a single nullable
callback (null = menu item hidden) in StoreGameDetailScreen,
LibraryGameLaunchScreen and their source-tag menus. That returns
StoreGameDetailScreen to exactly the parameter count that demonstrably
passes the verifier, with a slightly cleaner API.
Also fixes the PR CI compile failure: the merge had interleaved a
duplicate com.winlator.cmod.shared.io.FileUtils import in
SteamService.kt ("Conflicting import: name is ambiguous").
https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
…rifier Second VerifyError from the same device, different instruction: [0x1749] copy-cat1 v0<-v260 type=Reference: WindowInsets v260 is the tell: with the 40+-param defaulted signature and the whole screen body compiled into one method, the frame needed 260+ registers, and at that pressure D8's prologue emits register copies this device's ART verifier rejects. Reshaping parameters (previous commit) just moved the failure — the giant shared frame is the trigger. Fix splits the two pressure sources into separate methods: • StoreGameDetailScreen keeps the exact public defaulted signature but its body is now only "pack args into StoreGameDetailParams, call content" — a small frame containing the default-mask prologue. • StoreGameDetailContent(p) holds the original body behind local rebinds (body text unchanged) — a big body but a one-param method with no default machinery, so the frame stays well under the high-register regime. Call sites are untouched; behavior is identical. https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
Debloat from the crash investigation, now that the real cause (VerifyError, fixed by the wrapper/content split) is known: • Reverted the speculative runCatching guards added while hunting blind — GameManagerDialog's install-data load, the DLC size effect and the onInstall download call are back to the unguarded house style they had on main. Kept the two intentional guards: the launch-options/beta loader effects (network-dependent PICS refresh; failure should hide menu items, not kill the dialog) and startUpdateCheck's downloadAppForUpdate (our feature's primary flow). • Extracted refreshAppInfoFromPicsOnce — the once-per-process PICS refresh guard previously duplicated inside both loader functions. • Trimmed stale verifier-saga comments from the nullable menu-callback params; the full story lives once, on StoreGameDetailParams where the split it explains actually is. https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
Pre-install, the store screen's Download pill now has a small segment "sliced off" its left edge (AltRoute icon, 48dp — min touch target, mirrored 14dp/4dp corners with the house 8dp gap) that opens the existing beta-branch picker. The pick is STAGED in memory only (pendingBetaBranch); size cards retarget to the staged branch immediately, and nothing persists or downloads until the Download button commits it. • StoreCtaButton gains defaulted modifier/shape params; the glass brushes are extracted into storeCtaGlassBrushes shared with the new StoreCtaCutoutButton. Zero new StoreGameDetailScreen params — the existing nullable onBetaBranches drives the cutout pre-install and the STEAM-menu item post-install, so Epic/GOG (null) never show it. • GameManagerDialog loads beta branches for both install states (after the install state resolves, so the cutout can't flash on installed games); launch options stay install-gated. • BetaBranchesDialog is now a dumb shell with a single onSelect — the installed hosts (GameManager + library) persist and start the update check exactly as before; the pre-install host just stages and closes. • Download commit: persists a custom install path BEFORE the branch write (setSelectedBetaBranch creates the shortcut, which snapshots game_install_path and is never rewritten — committing in the other order would freeze the default path and break launch from custom dirs), then downloads; pause/resume re-resolves the branch from the now-persisted shortcut. If the shortcut can't be created yet (setup incomplete) the install proceeds on public with the existing beta-branch-failed toast. https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
Reinstalling WinNative wipes app-private shortcuts (where the selectedBranch extra lives) while game files on external storage survive — so a reinstalled app showed a beta install as "public", and worse, a tapped update check would diff the beta files against public manifests and silently downgrade the game. Steam survives this because it persists the beta key WITH the install (appmanifest ACF, UserConfig/betakey). Our durable analogs live in the surviving game dir, used in order of trust by the new recoverSelectedBetaName(appId): 1. .DepotDownloader/depot.config — the installed manifest gids matched exactly against each beta's PICS manifests (evidence of what the files actually are; requires all overlapping depots to match and at least one gid to differ from public, single candidate only); 2. steam_settings/configs.app.ini branch_name — the selection recorded at the game's last launch (checked at the game-dir root and next to the exe, the two settings-dir locations the launch path writes). A successful inference heals the shortcut via setSelectedBetaBranch, so every downstream consumer is consistent again. Wired at the three chokepoints: the branch-picker loader (UI), checkForAppUpdate / isUpdatePending (branch param now nullable, null = recover-and-resolve) and prepareLibSteamClientForLaunch (game launch). Known limitation: a never-launched install whose beta has since published a new build matches neither source and stays public until re-picked. https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
…ecovery From a seven-angle review of the two newest commits: • Branch-name casing: an ini-recovered name is now canonicalized to the PICS branches-map key before healing — downstream buildId lookups (app.branches[name]) are exact-key and would silently miss an ini-cased variant. • Recovery is memoized per process (betaRecoveryAttemptedApps): it can only succeed right after a WinNative reinstall, so permanently-public games no longer rerun the full inference (DB read + depot.config parse + ini probes) on every dialog open, update check and launch. • readBranchNameFromSettingsIni derives the exe dir from the SteamApp already in hand instead of getWindowsLaunchInfos' redundant blocking DB fetch of the same row. • pendingBetaBranch is cleared when a game resolves to installed — a staged pre-install pick was either committed by Download or abandoned, and could otherwise leak into a later session of the dialog. The host's selectedBranch expression simplifies to pending ?: persisted on that invariant. • The branch cutout now shares the Download button's enabled gate so users can't stage picks an actively-blocked download won't honor. • The 20-line commit block in onInstall is extracted to commitPendingBetaBranch, mirroring the persist helper next to it. Refuted during verification, for the record: cutout-without-betas (the host gates onBetaBranches on branches.size >= 2), cross-game state leaks (remember(app.id) keying), setSelectedBetaBranch false-success (it returns false on a null shortcut), customPath double-application (idempotent by construction), and the lost explicit-public API (callers can pass requestedBranch = "public"). https://claude.ai/code/session_01EJFSZYUPkR1Cwer6CC2cNa
The beta-branch cutout and the Download segment each painted the full teal->purple gradient, so it repeated. Each segment now paints only its slice of a single gradient spanning the whole row, matching the look of the undivided Download button. https://claude.ai/code/session_013E3pTpGg4C6ropUPA9QyNB
The depot writer only ever wrote new/changed files, so switching beta branches (or any update that drops files) left the previous build's files on disk until a fresh install — leftover binaries could break the game. New depot_cleanup.rs runs a manifest-diff pass after every fully successful download: before a depot moves off its installed manifest, a .stalecleanup marker records the old gid; once all depots commit, files listed in the old manifest but absent from the union of all currently-installed manifests are deleted (logged per file), Steamless .original.exe backups go with their primary, and emptied directories are pruned. Candidates come only from the old manifest, so user files, saves, steam_settings/ and other non-manifest content can never be touched. If the keep-union can't be built completely (missing key or unreadable cached manifest), the pass defers and the marker survives for the next successful download; pause/cancel never deletes anything. pruneStaleDepotManifestCache now keeps manifest caches that a pending marker still needs and drops orphaned markers. https://claude.ai/code/session_01RPe9jUg4EkHe7in3RzDra3
…en deletion
Multi-angle review fixes for the manifest-diff deletion pass:
- Real branch switches arrive as narrowed updates (only changed depots,
per-app DLC batches), so the keep-union could never collect keys for
every installed depot and the pass deferred forever. Each download now
persists a decrypted "{depot}_{gid}.filelist" sidecar (and backfills
one when skipping an already-installed depot), so the union and the
old-build file lists read without depot keys. Union building is
best-effort over readable lists — same model as the reference
DepotDownloader — instead of all-or-nothing.
- Per-batch fresh downloads (Verify Files) wiped all of depot.config,
blinding the union to other batches' depots and opening a window to
delete files a still-installed depot ships. fresh now discards only
the operation's own depots.
- A marker whose old list is on disk but unreadable in this op is
deferred, not dropped; dropped only when the data is gone for good.
- Steamless backup deletion is gated: exe primaries only, skipped when a
current manifest ships that exact backup name, only after the primary
was actually deleted.
- Keep-union and candidates normalize through the same component-level
canonicalization ("./a", "a//b" spellings match), and candidates
behind a symlinked directory are skipped so manifest-created symlinks
can't redirect deletion outside the install dir.
- Kotlin manifest-cache prune handles .filelist sidecars with the same
keep/prune lifecycle and drops markers only when neither manifest nor
sidecar remains.
https://claude.ai/code/session_01RPe9jUg4EkHe7in3RzDra3
…cancelled A cancelled branch-switch update left the disk a mix of the old build plus partial new-build files, selectedBranch pointing at a build that never landed, and no committed manifest the stale-file pass could diff the leftovers against (the documented last limitation). Native: the target build's .filelist sidecar is written before any game file is touched, and a failed/cancelled depot write records an aborted- build .stalecleanup marker for the target gid. The marker resolves like any other — dropped if the build later commits (resume), otherwise the next successful download diffs the aborted build's files away. Kotlin: picking a branch records the replaced selection as a previousBranch shortcut extra (first un-committed switch only; recovery heal and the restore itself don't record). Cancelling an update task reverts selectedBranch to it, clears the restore point, toasts, and — when files were already touched — enqueues the verify flow, which resolves the now-restored branch and repairs mismatched chunks; the aborted-build marker then reclaims the orphans. A committed download clears the restore point. Safety prerequisite: cancelRunning treated only TASK_UPDATE as an update, so a cancelled TASK_VERIFY fell through to the install-cancel branch and deleted the game directory. VERIFY now cleans up like an update — required since the recovery auto-enqueues verifies, and cancelling a verify of an installed game must never delete it. https://claude.ai/code/session_01RPe9jUg4EkHe7in3RzDra3
…tore download Testing exposed that the cancelled-switch restore left the install desynced (wrong file sizes, "no updates" on a different branch) — mostly because the restore verify was unlabelled and freely cancellable, so it got aborted mid-repair. This round makes the recovery converge and gives the restore a first-class, guarded UX. Recovery convergence: - checkForAppUpdate no longer trusts a stale cached .manifest when depot.config exists but lacks the depot's entry (the false "no updates"); only true legacy installs with no depot.config use the cache fallback. - pruneInProgressDepotConfigEntries drops INVALID_MANIFEST_ID entries a cancelled begin_depot left behind, on every cancel path, so update detection and the branch selector reflect committed depots only. - previousBranch is kept until the repair COMPLETES (cleared in completeAppDownload), doubling as the durable "restore in progress" signal across requeue/restart. - cancelRunning now distinguishes UPDATE cancel (revert + repair), restore-verify cancel (abandon to not-installed, files kept for re-download, no re-arm), and normal-verify cancel (stays installed). Never-launched beta: - persistInstalledBranchName records branch_name in configs.app.ini at download completion, so an install whose build no longer matches any branch's current PICS manifest is still recoverable after a WinNative reinstall (launch later rewrites the file in full). Restore UX (reuses existing Material 3 surfaces): - DownloadInfo.isRepair drives a "Restoring" status label and a repair-specific cancel-warning dialog (cancelling means re-downloading from the store) via the existing LaunchDangerConfirmDialog. Cleanup: - Removed the dead staging-restore block from cleanupCancelledUpdate (the depot writer patches in place and never populates .DepotDownloader/staging). https://claude.ai/code/session_01RPe9jUg4EkHe7in3RzDra3
…patch + queue cancel warning Multi-angle review fixes for the cancelled-switch recovery: - HIGH: pruneInProgressDepotConfigEntries could delete a valid depot. Steam manifest GIDs with the high bit set read back as Long.MAX_VALUE through org.json's optLong (u64 parsed as Double, saturated), making a legitimate entry indistinguishable from INVALID_MANIFEST_ID — and a rewrite would corrupt it. Removed the Kotlin depot.config rewrite entirely; the native fresh verify's discard_depots already converges state with exact u64 handling, and checkForAppUpdate already treats a lingering INVALID/missing entry as needs-download. - MED: a null restore-verify dispatch (e.g. transient depot-resolution failure, before any coordinator record exists) left previousBranch dangling forever, which would later restore the WRONG branch. Now abandons cleanly to not-installed on null dispatch. - MED: the queue-screen cancel button showed the generic delete warning for an in-progress restore; thread isRepair through DownloadCancelRequest so it shows the restore-specific warning there too. https://claude.ai/code/session_01RPe9jUg4EkHe7in3RzDra3
…abelled
Restoring to public after a cancelled switch wrote a blank selectedBranch,
which made recoverSelectedBetaName fall into its infer-from-installed-
manifests path. On a still-mixed install that inference misidentifies the
branch, so the STEAM-dropdown / store selector showed the wrong branch.
Store the restored branch (including "public") verbatim — all readers
normalise via ifBlank{"public"}, and reinstall recovery still triggers
because a reinstall wipes the whole shortcut, not just the value.
https://claude.ai/code/session_01RPe9jUg4EkHe7in3RzDra3
…re reclaimed The stale-file pass only diffed builds that had a pending .stalecleanup marker, so an aborted build whose marker was dropped/deferred left files behind after a restore. It now sweeps every cached old build (any .filelist/.manifest whose gid isn't the currently-installed one) minus the union of all current builds — reclaiming orphans regardless of marker state, while still only ever deleting files that appear in some Steam manifest. User-added mod files (in no manifest) are never touched; a file a mod overwrote is reverted by the download itself, as on real Steam. Also replaces the global abort-on-in-progress with per-depot deferral: an INVALID (mid-download) depot is protected and its old builds skipped, without blocking cleanup of the other depots. A current depot whose own file list is unreadable is likewise deferred to avoid over-deletion. https://claude.ai/code/session_01RPe9jUg4EkHe7in3RzDra3
Multi-angle review found a cross-depot over-deletion: the sweep's per-depot deferral protected an in-flight depot only from its own cached builds, so a file that moved from another depot's old build into an in-flight (or unreadable) depot — whose target build cleanup can't see — could be deleted as stale. Require a complete keep-union instead: if any installed depot is in progress (INVALID) or its file list is unreadable, defer the whole pass until a later op can read every depot. This never blocks the restore (the verify writes a sidecar for every depot before cleanup runs), so the leftover-reclaiming sweep still does its job there. Adds a regression test for the cross-depot-into-in-flight-depot case. https://claude.ai/code/session_01RPe9jUg4EkHe7in3RzDra3
4d287bd to
be2c368
Compare
…ranch-selector-ckipz0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds Steam beta branch support to WinNative end to end: a Workshop-styled picker reachable from the STEAM ▾ menu on installed games and from a segment "sliced off" the Download button before install. Selecting a branch persists it, retargets size estimates, and drives the download/update flow so the branch's build is what actually lands on disk. The backend (PICS
branches, depot manifest resolution,setAppCurrentBeta) already existed — this PR wires the UI to it and fixes the gaps found along the way.Features
Beta Branch picker (installed games)
AltRouteiconBetaBranchScreen.kt): branch name (public (default)labeled), build id + updated date, check mark on the active branch, password-protected branches shown locked/disabledPre-download branch selection (store tab)
Reinstall / never-launched recovery
recoverSelectedBetaNameinfers the branch from the surviving game dir after a WinNative reinstall. The installed branch is now also written tosteam_settings/configs.app.iniat download completion, so even a never-launched install (whose build may no longer match any branch's current PICS manifest) recovers correctly.Stale-file cleanup on branch switch — mod-safe sweep
The native depot writer only writes new/changed files, so switching branches would leave files the new branch no longer ships.
depot_cleanup.rsreclaims them after a successful download by sweeping every cached old build (any.filelist/.manifestwhose gid isn't the currently-installed one) and deleting its files minus the union of all current builds..filelistsidecars (written before any file is touched) make it key-independent, so it works in narrowed updates and per-app DLC batches. Candidates reject../absolute paths and skip symlinked-directory ancestors; per-batch fresh downloads don't wipe other batches'depot.config.Cancelled-switch recovery (restore last committed branch)
WinNative patches files in place, so a branch-switch download cancelled mid-write leaves a half-old/half-new mix. Recovery converges it:
previousBranchextra; on cancel, the selection reverts to it (stored verbatim, including "public", so the selector reads it authoritatively instead of guessing from mixed files) and a labelled "Restoring" download repairs the files back, after which the sweep removes the abandoned branch's leftovers.previousBranchis held until the repair completes (survives requeue/restart, reliably labels the restore); a committed download clears it. Update detection no longer trusts a stale cached manifest whendepot.configis present but incomplete (fixes a false "no updates").cancelRunningclassified onlyTASK_UPDATEas an update, so cancelling aTASK_VERIFYfell through to the install-cancel branch and deleted the game directory; verify now cleans up like an update.Fixes shipped along the way
configs.app.ini[app::general]reflects the selected branch (was hardcodedbranch_name=public)checkForAppUpdate/downloadApphonor the selected branch; stale{depot}_{gid}.manifest/.filelistcaches pruned after completed downloads.desktopparser for shortcut extras in hot paths instead ofloadShortcuts()StoreGameDetailScreensplit into a thin wrapper + content method (a ~260-register method made D8 emit bytecode some vendor ART verifiers reject — instant crash opening any store game)Known limitations (documented in code)
CheckAppBetaPassword) support; protected branches are visible but lockedTest checklist
cargo testgreen (191 tests: stale-file sweep incl. marker-free reclamation, mod preservation, multi-depot moves, per-depot deferral, path-safety/symlink skips, aborted-switch recovery)