Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 27 additions & 1 deletion apps/rgsm-gui/src-tauri/src/ipc_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use rgsm_core::ludusavi_manifest::{self, ImportableGame, LudusaviManifestStatus,
use rgsm_core::path_resolver;
use rgsm_core::preclude::*;
use rgsm_core::services::ServiceContext;
use rgsm_core::steam;
use rgsm_core::vn_scanner;
use rgsm_core::{backup, config, system_fonts};

Expand Down Expand Up @@ -1000,9 +1001,34 @@ pub fn reset_ludusavi_manifest_to_bundled() -> Result<LudusaviManifestStatus, St
#[specta::specta]
pub async fn check_paths(
paths: Vec<String>,
store_user_id: Option<String>,
install_dirs: Option<Vec<String>>,
steam_id: Option<u32>,
) -> Result<Vec<path_resolver::PathCheckResult>, String> {
let config = get_config().map_err(|e| e.to_string())?;
Ok(path_resolver::check_paths(&paths, &config))
let device = config.devices.get(get_current_device_id());
let ctx = path_resolver::PathContext {
install_dirs: install_dirs.unwrap_or_default(),
steam_id,
install_dir_cache: None,
game_roots: device.map(|d| d.game_roots.clone()).unwrap_or_default(),
store_user_id,
};
Ok(path_resolver::check_paths(&paths, Some(&ctx), &config))
}

#[tauri::command]
#[specta::specta]
pub async fn detect_game_roots() -> Result<Vec<String>, String> {
info!(target:"rgsm::ipc", "Detecting game root directories");
steam::detect_game_roots().map_err(|e| e.to_string())
}

#[tauri::command]
#[specta::specta]
pub async fn detect_store_user_ids() -> Result<Vec<steam::StoreUserIdCandidate>, String> {
info!(target:"rgsm::ipc", "Detecting Steam user IDs");
steam::detect_steam_user_ids().map_err(|e| e.to_string())
}

/// Gets a list of system font family names
Expand Down
2 changes: 2 additions & 0 deletions apps/rgsm-gui/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ pub fn run() -> anyhow::Result<()> {
ipc_handler::update_ludusavi_manifest,
ipc_handler::reset_ludusavi_manifest_to_bundled,
ipc_handler::check_paths,
ipc_handler::detect_game_roots,
ipc_handler::detect_store_user_ids,
ipc_handler::get_system_fonts,
ipc_handler::get_sync_state,
ipc_handler::scan_vns,
Expand Down
6 changes: 3 additions & 3 deletions apps/rgsm-gui/src-tauri/src/quick_actions/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub enum QuickActionCommand {
game_item: tauri::menu::MenuItem<tauri::Wry>,
},
SetCurrentGame {
game: Game,
game: Box<Game>,
respond_to: oneshot::Sender<anyhow::Result<()>>,
},
TriggerBackup(QuickActionType),
Expand Down Expand Up @@ -75,7 +75,7 @@ impl QuickActionManager {
let (tx, rx) = oneshot::channel();
self.command_tx
.send(QuickActionCommand::SetCurrentGame {
game,
game: Box::new(game),
respond_to: tx,
})
.context("failed to send SetCurrentGame command")?;
Expand Down Expand Up @@ -173,7 +173,7 @@ impl QuickActionWorker {
self.handle_register_tray(game_item)
}
QuickActionCommand::SetCurrentGame { game, respond_to } => {
let result = self.handle_set_current_game(game).await;
let result = self.handle_set_current_game(*game).await;
let _ = respond_to.send(result);
}
QuickActionCommand::TriggerBackup(trigger) => {
Expand Down
2 changes: 2 additions & 0 deletions apps/rgsm-gui/src-tauri/src/quick_actions/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ mod tests {
next_save_unit_id: 0,
cloud_sync_enabled: true,
auto_backup,
ludusavi_meta: None,
store_user_ids: std::collections::HashMap::new(),
}
}

Expand Down
76 changes: 71 additions & 5 deletions apps/rgsm-gui/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,25 @@ async resetLudusaviManifestToBundled() : Promise<Result<LudusaviManifestStatus,
else return { status: "error", error: e as any };
}
},
async checkPaths(paths: string[]) : Promise<Result<PathCheckResult[], string>> {
async checkPaths(paths: string[], storeUserId: string | null, installDirs: string[] | null, steamId: number | null) : Promise<Result<PathCheckResult[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("check_paths", { paths }) };
return { status: "ok", data: await TAURI_INVOKE("check_paths", { paths, storeUserId, installDirs, steamId }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async detectGameRoots() : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("detect_game_roots") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async detectStoreUserIds() : Promise<Result<StoreUserIdCandidate[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("detect_store_user_ids") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
Expand Down Expand Up @@ -615,7 +631,12 @@ export type CreatedBy =
* Forward-compat catch-all for variants added in future versions.
*/
"Unknown"
export type Device = { id: string; name: string }
export type Device = { id: string; name: string;
/**
* User-configured root directories for `<root>` path variable resolution.
* First entry is used as `<root>`; empty = auto-detect Steam root.
*/
game_roots?: string[] }
export type ExtraBackupItem = {
/**
* Filename without extension, e.g. `Overwrite_2025-12-22_12-34-56`.
Expand Down Expand Up @@ -651,12 +672,29 @@ cloud_sync_enabled?: boolean;
/**
* Per-game auto-backup configuration. `None` = disabled.
*/
auto_backup?: AutoBackupConfig | null }
auto_backup?: AutoBackupConfig | null;
/**
* Metadata from Ludusavi manifest import. `None` for manually added games.
*/
ludusavi_meta?: LudusaviMeta | null;
/**
* Per-device store user ID for `<storeUserId>` resolution.
* Key: DeviceId, Value: store-specific user ID (e.g. Steam user ID).
*/
store_user_ids: Partial<{ [key in string]: string }> }
/**
* Frontend/IPC input shape for creating/updating a game.
* Save-unit IDs are assigned and normalized in backend domain logic.
*/
export type GameDraft = { name: string; save_paths: SaveUnitDraft[]; game_paths?: Partial<{ [key in string]: string }> }
export type GameDraft = { name: string; save_paths: SaveUnitDraft[]; game_paths?: Partial<{ [key in string]: string }>;
/**
* Metadata from Ludusavi manifest import (optional for manually added games).
*/
ludusavi_meta?: LudusaviMeta | null;
/**
* Per-device store user ID for `<storeUserId>` resolution.
*/
store_user_ids: Partial<{ [key in string]: string }> }
/**
* A backup list info is a json file in a backup folder for a game.
* It contains the name of the game,
Expand Down Expand Up @@ -693,6 +731,10 @@ name: string;
* Steam ID if available
*/
steamId: number | null;
/**
* Install directory names from manifest's `installDir` field
*/
installDirs: string[];
/**
* Whether this game is already managed
*/
Expand Down Expand Up @@ -731,6 +773,22 @@ localBytes: number | null;
* Bundled manifest size in bytes.
*/
bundledBytes: number }
/**
* Manifest-derived metadata for games imported from Ludusavi.
*
* These fields are **device-independent** (directory names and IDs, not paths)
* and are safe to persist in config and sync across devices.
*/
export type LudusaviMeta = {
/**
* Install directory names from the manifest's `installDir` field.
* e.g. `["100 Orange Juice"]`. Used to resolve `<game>` and `<base>`.
*/
installDirs?: string[];
/**
* Steam App ID from the manifest's `steam.id`. Used to resolve `<storeGameId>`.
*/
steamId?: number | null }
export type NotificationLevel = "info" | "warning" | "error"
/**
* Result of checking a single path
Expand Down Expand Up @@ -849,6 +907,14 @@ device_id?: string | null;
* How this snapshot was created.
*/
created_by?: CreatedBy }
/**
* A candidate Steam user ID with metadata for UI display.
*/
export type StoreUserIdCandidate = { userId: string;
/**
* Seconds since UNIX epoch of the most recently modified file in the userdata dir.
*/
lastModifiedEpochSecs: number | null }
export type SyncGameOutcome = "already_in_sync" | "uploaded" | "downloaded" | "merged" | "conflict"
export type SyncResult = "success" | { error: string } | "conflict" | "cancelled"
export type SyncState = { schema_version: number; backend_fingerprint?: string; current_device_id?: string; config_state?: GameSyncState; games?: Partial<{ [key in string]: GameSyncState }> }
Expand Down
3 changes: 2 additions & 1 deletion apps/rgsm-gui/src/components/FavoriteSideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
import type { AllowDropType, NodeDropType } from 'element-plus/es/components/tree/src/tree.type';
import type { FavoriteTreeNode, Game } from '~/bindings';
import { Close, EditPen, FolderAdd, Plus } from '@element-plus/icons-vue';
import { getGameManagementPath } from '../composables/useGameManagementRoute';

const { config, saveConfig } = useConfig();
const { showWarning, showSuccess, showError } = useNotification();
Expand Down Expand Up @@ -63,7 +64,7 @@ function favorite_click_handler(node: FavoriteTreeNode) {
showWarning({ message: $t('favorite.game_not_found') + ': ' + node.label });
return;
}
navigateTo('/Management/' + node.label);
navigateTo(getGameManagementPath(node.label));
}

function remove_node(node: Node, data: FavoriteTreeNode) {
Expand Down
Loading
Loading