Skip to content

Feat/path variable#374

Merged
mcthesw merged 4 commits intodevfrom
feat/path-variable
Apr 12, 2026
Merged

Feat/path variable#374
mcthesw merged 4 commits intodevfrom
feat/path-variable

Conversation

@mcthesw
Copy link
Copy Markdown
Owner

@mcthesw mcthesw commented Apr 12, 2026

变更内容

  • 修复包含 / 的游戏名在管理页中的路由跳转问题
  • Add Game 支持配置 installDir,让 <base> / <game> 可以正确预览
  • 自定义导入界面将解析后的路径显示到输入框下方,并补充文件夹 / 文件 / 注册表类型标签
  • 补齐相关路径检查上下文与界面文案

验证

  • pnpm web:lint
  • pnpm web:typecheck

备注

  • *.sav 这类通配符路径暂未扩展为单文件模型,后续会单独设计

closes #348, closes #334, closes #337

Copilot AI review requested due to automatic review settings April 12, 2026 14:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/rgsm-gui/src/pages/AddGame.vue Outdated
game.game_paths[currentDevice.value.id] = game_path.value;
}
if (storeUserId && currentDevice.value) {
game.store_user_ids = {};
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
game.store_user_ids = {};

Copilot uses AI. Check for mistakes.
Comment thread apps/rgsm-gui/src/pages/Settings.vue Outdated
Comment on lines +381 to +383
function removeGameRoot(index: number) {
getCurrentGameRoots().splice(index, 1);
updateDeviceInfo();
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
function removeGameRoot(index: number) {
getCurrentGameRoots().splice(index, 1);
updateDeviceInfo();
async function removeGameRoot(index: number) {
getCurrentGameRoots().splice(index, 1);
await updateDeviceInfo();

Copilot uses AI. Check for mistakes.
Comment on lines +386 to +389
function updateGameRoot(index: number, value: string) {
getCurrentGameRoots()[index] = value;
updateDeviceInfo();
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}

function formatTimeAgo(epochSecs: number): string {
const diffMs = Date.now() - epochSecs * 1000;
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
const diffMs = Date.now() - epochSecs * 1000;
const diffMs = Math.max(0, Date.now() - epochSecs * 1000);

Copilot uses AI. Check for mistakes.
Comment on lines +253 to +260
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 });
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +102
/// 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> {
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +312 to +316
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>,
}
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +432 to +436
let acf_content = r#"
"AppState"
{
"appid" "730"
"installdir" "Counter-Strike Global Offensive"
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +400 to +402
// 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());

Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@mcthesw mcthesw force-pushed the feat/path-variable branch from df3f251 to f930cc8 Compare April 12, 2026 15:01
@mcthesw mcthesw merged commit 1d31ab5 into dev Apr 12, 2026
2 checks passed
@mcthesw mcthesw deleted the feat/path-variable branch April 12, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants