Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds Steam/Ludusavi path-variable support end-to-end (core resolver + Steam discovery + GUI), and fixes routing for game names containing / by URL-encoding management routes.
Changes:
- Add Steam integration to discover library roots, installed games’ install dirs, and Steam user ID candidates.
- Extend path resolution and path checking to support
<root>,<base>,<game>,<storeUserId>,<storeGameId>with per-game/per-device context persisted in config. - Update GUI flows (Add Game, batch/custom import, settings) to configure/installDir metadata, select Steam user ID, and display resolved paths and kinds.
Reviewed changes
Copilot reviewed 38 out of 39 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| locales/zh_SIMPLIFIED.json | Adds new i18n strings for game roots, installDirs, store user ID, and “time ago”. |
| locales/en_US.json | English equivalents for new i18n strings. |
| crates/rgsm-core/src/vn_scanner.rs | Initializes new Game fields when creating VN drafts. |
| crates/rgsm-core/src/updater/versions/v1_4_0.rs | Sets defaults for new Game fields during migration. |
| crates/rgsm-core/src/updater/migration.rs | Updates migration tests to include new Game fields. |
| crates/rgsm-core/src/steam.rs | New Steam module: VDF parsing, library discovery, install dir resolution, Steam user ID detection + tests. |
| crates/rgsm-core/src/path_resolver.rs | Introduces PathContext; resolves <root>/<base>/<game>/<storeUserId>/<storeGameId>; updates check APIs + tests. |
| crates/rgsm-core/src/ludusavi_manifest.rs | Extracts installDir/Steam ID from manifest; uses game context + Steam cache to detect installed games. |
| crates/rgsm-core/src/lib.rs | Exposes new steam module. |
| crates/rgsm-core/src/hooks/pipeline.rs | Updates tests/fixtures for new Game fields. |
| crates/rgsm-core/src/hooks/checksum_hook.rs | Updates tests/fixtures for new Game fields. |
| crates/rgsm-core/src/device.rs | Adds per-device game_roots configuration for <root> resolution. |
| crates/rgsm-core/src/backup/tests/game.rs | Updates tests for new Game fields and new fingerprint API signature. |
| crates/rgsm-core/src/backup/tests/archive.rs | Updates archive tests for new compress/fingerprint signatures. |
| crates/rgsm-core/src/backup/state_fingerprint.rs | Plumbs PathContext into source fingerprinting. |
| crates/rgsm-core/src/backup/save_unit.rs | Plumbs PathContext into per-device path resolution. |
| crates/rgsm-core/src/backup/game.rs | Persists Ludusavi metadata + per-device store user IDs; uses path context for backup/apply/fingerprint. |
| crates/rgsm-core/src/backup/extra_backups.rs | Uses path context during restore of extra backups. |
| crates/rgsm-core/src/backup/archive/decompress.rs | Adds path_ctx to decompression path restoration. |
| crates/rgsm-core/src/backup/archive/compress.rs | Adds path_ctx to compression and source fingerprint computation. |
| crates/rgsm-core/src/backup/archive/backend.rs | Extends archive backend trait to accept path_ctx. |
| crates/rgsm-core/Cargo.toml | Adds keyvalues-serde dependency for Steam VDF parsing. |
| Cargo.lock | Locks new dependency graph for keyvalues-serde and transitive crates. |
| apps/rgsm-gui/src/pages/Settings.vue | Adds game root directory UI + auto-detect integration; populates game_roots. |
| apps/rgsm-gui/src/pages/Management/[name].vue | Uses encoded routing helpers; includes new game fields in update payload. |
| apps/rgsm-gui/src/pages/AddGame.vue | Adds installDir input, store user ID propagation, and passes context to path checks. |
| apps/rgsm-gui/src/composables/useNavigationLinks.ts | Uses encoded management route generator. |
| apps/rgsm-gui/src/composables/useGameManagementRoute.ts | New helpers for URL-encoding/decoding management routes. |
| apps/rgsm-gui/src/components/SaveLocationDrawer.vue | Initializes new game field defaults; updates path checks call signature. |
| apps/rgsm-gui/src/components/PathVariableInput.vue | Adds new variables to autocomplete and passes context to backend resolution checks. |
| apps/rgsm-gui/src/components/MainSideBar.vue | Uses encoded route generator for menu indices. |
| apps/rgsm-gui/src/components/GameImportCustomizeDialog.vue | Adds Steam user ID selector, path kind tags, and resolved path display. |
| apps/rgsm-gui/src/components/GameBatchImportDialog.vue | Adds Steam user ID selector and per-game contextual path checking. |
| apps/rgsm-gui/src/components/FavoriteSideBar.vue | Uses encoded route generator for navigation. |
| apps/rgsm-gui/src/bindings.ts | Updates IPC bindings: checkPaths signature + new commands/types. |
| apps/rgsm-gui/src-tauri/src/quick_actions/scheduler.rs | Updates test fixtures for new Game fields. |
| apps/rgsm-gui/src-tauri/src/quick_actions/manager.rs | Boxes Game in command enum to reduce move/copy size. |
| apps/rgsm-gui/src-tauri/src/lib.rs | Registers new Tauri commands for root/user-id detection. |
| apps/rgsm-gui/src-tauri/src/ipc_handler.rs | Extends check_paths to accept context; adds detect_game_roots + detect_store_user_ids. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| game.game_paths[currentDevice.value.id] = game_path.value; | ||
| } | ||
| if (storeUserId && currentDevice.value) { | ||
| game.store_user_ids = {}; |
There was a problem hiding this comment.
save() initializes store_user_ids from the existing game, but then replaces it with a fresh {} when storeUserId is set. This drops mappings for other devices when editing an existing game. Consider updating/merging only the current device’s entry while preserving the rest of the map.
| game.store_user_ids = {}; |
| function removeGameRoot(index: number) { | ||
| getCurrentGameRoots().splice(index, 1); | ||
| updateDeviceInfo(); |
There was a problem hiding this comment.
removeGameRoot() triggers updateDeviceInfo() but doesn’t await it. Since updateDeviceInfo() mutates config + calls saveConfig() + fetchDeviceInfo(), not awaiting can lead to overlapping saves/refreshes if the user removes multiple entries quickly.
| function removeGameRoot(index: number) { | |
| getCurrentGameRoots().splice(index, 1); | |
| updateDeviceInfo(); | |
| async function removeGameRoot(index: number) { | |
| getCurrentGameRoots().splice(index, 1); | |
| await updateDeviceInfo(); |
| function updateGameRoot(index: number, value: string) { | ||
| getCurrentGameRoots()[index] = value; | ||
| updateDeviceInfo(); | ||
| } |
There was a problem hiding this comment.
updateGameRoot() calls updateDeviceInfo() on every @update:model-value (i.e., every keystroke). Because updateDeviceInfo() persists config and shows a success toast, this can spam writes/notifications and race multiple saves. Consider debouncing and/or saving on blur/explicit action, and await the save to serialize updates.
| } | ||
|
|
||
| function formatTimeAgo(epochSecs: number): string { | ||
| const diffMs = Date.now() - epochSecs * 1000; |
There was a problem hiding this comment.
formatTimeAgo() can return negative values if epochSecs is in the future (clock skew, filesystem timestamps), leading to messages like “-1 min ago”. Consider clamping the diff to 0 (or returning a dedicated “just now” string) before formatting.
| const diffMs = Date.now() - epochSecs * 1000; | |
| const diffMs = Math.max(0, Date.now() - epochSecs * 1000); |
| function formatTimeAgo(epochSecs: number): string { | ||
| const diffMs = Date.now() - epochSecs * 1000; | ||
| const mins = Math.floor(diffMs / 60000); | ||
| if (mins < 60) return $t('common.minutes_ago', { n: mins }); | ||
| const hours = Math.floor(mins / 60); | ||
| if (hours < 24) return $t('common.hours_ago', { n: hours }); | ||
| const days = Math.floor(hours / 24); | ||
| return $t('common.days_ago', { n: days }); |
There was a problem hiding this comment.
formatTimeAgo() doesn’t guard against future timestamps, so mins/hours/days can become negative and render incorrectly. Clamping the computed diff to >= 0 (or handling future values explicitly) would make the label robust.
| /// Discover all Steam library paths from `libraryfolders.vdf`. | ||
| /// | ||
| /// Returns a list of library root paths (e.g. `D:\SteamLibrary`). | ||
| /// The Steam root itself is always included as the first library. | ||
| pub fn get_steam_library_paths() -> Result<Vec<PathBuf>, SteamError> { |
There was a problem hiding this comment.
The doc comment says “The Steam root itself is always included as the first library”, but library_folders is deserialized into a HashMap and iterated via into_values(), so ordering is non-deterministic and the Steam root may not be first (or even included if the VDF is missing it). Either enforce steam_root as the first entry (and dedupe) or adjust the docs.
| pub struct StoreUserIdCandidate { | ||
| pub user_id: String, | ||
| /// Seconds since UNIX epoch of the most recently modified file in the userdata dir. | ||
| pub last_modified_epoch_secs: Option<i64>, | ||
| } |
There was a problem hiding this comment.
StoreUserIdCandidate.last_modified_epoch_secs is documented as “most recently modified file in the userdata dir”, but the implementation uses the userdata/<id> directory’s own metadata().modified(). Either update the comment to reflect directory mtime, or compute the max mtime of contents if you want the documented behavior.
| let acf_content = r#" | ||
| "AppState" | ||
| { | ||
| "appid" "730" | ||
| "installdir" "Counter-Strike Global Offensive" |
There was a problem hiding this comment.
parse_appmanifest_numeric_appid claims to test unquoted numeric values, but the fixture still uses a quoted string ("730"), so it doesn’t exercise the StringOrNumber::Num branch. Consider changing the fixture to an actually unquoted numeric value so this test covers the intended case.
| // Pre-scan all installed Steam games for O(1) per-game lookup | ||
| let steam_cache = Arc::new(steam::scan_all_installed_games().unwrap_or_default()); | ||
|
|
There was a problem hiding this comment.
scan_all_installed_games().unwrap_or_default() silently discards the error when Steam scanning fails (e.g., VDF parse/read issues). This makes local-game detection harder to diagnose. Consider logging the error at least at warn! before falling back to an empty cache.
df3f251 to
f930cc8
Compare
变更内容
/的游戏名在管理页中的路由跳转问题<base>/<game>可以正确预览验证
备注
*.sav这类通配符路径暂未扩展为单文件模型,后续会单独设计closes #348, closes #334, closes #337