From ef32a5fba23f6dcaf25bb2b6c407bae55a7cb445 Mon Sep 17 00:00:00 2001 From: mtkennerly Date: Sat, 21 Dec 2024 23:51:06 -0500 Subject: [PATCH] #434: Allow specifying install dirs for custom games --- CHANGELOG.md | 4 ++++ docs/help/custom-games.md | 12 +++++++++-- lang/en-US.ftl | 2 ++ src/gui/app.rs | 30 +++++++++++++++++++++++++++ src/gui/common.rs | 2 ++ src/gui/editor.rs | 43 +++++++++++++++++++++++++++++++++++++++ src/gui/shortcuts.rs | 12 +++++++++++ src/lang.rs | 4 ++++ src/resource/config.rs | 34 +++++++++++++++++++++++++++---- src/resource/manifest.rs | 13 ++++++++++++ 10 files changed, 150 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9258e8..b6bd965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ * On Linux, for Lutris roots that point to a Flatpak installation, Ludusavi now checks `$XDG_DATA_HOME` and `$XDG_CONFIG_HOME` inside of the Flatpak installation of Lutris. + * Custom games now let you specify installed folder names. + This can be used to satisfy the `` and `` path placeholders + in cases where Ludusavi can't automatically detect the right folder. + For more info, [see the custom games document](/docs/help/custom-games.md). * Changed: * When the game list is filtered, the summary line (e.g., "1 of 10 games") now reflects the filtered totals. diff --git a/docs/help/custom-games.md b/docs/help/custom-games.md index c1def55..ab245ba 100644 --- a/docs/help/custom-games.md +++ b/docs/help/custom-games.md @@ -5,11 +5,19 @@ If the game name exactly matches a known game, then your custom entry will overr For file paths, you can click the browse button to quickly select a folder. The path can be a file too, but the browse button only lets you choose folders at this time. You can just type in the file name afterwards. -You can also use [globs] +You can also use [globs](https://en.wikipedia.org/wiki/Glob_(programming)) (e.g., `C:/example/*.txt` selects all TXT files in that folder) and the placeholders defined in the [Ludusavi Manifest format](https://github.com/mtkennerly/ludusavi-manifest). If you have a folder name that contains a special glob character, you can escape it by wrapping it in brackets (e.g., `[` becomes `[[]`). -[globs]: https://en.wikipedia.org/wiki/Glob_(programming) + diff --git a/lang/en-US.ftl b/lang/en-US.ftl index e152af8..c78847c 100644 --- a/lang/en-US.ftl +++ b/lang/en-US.ftl @@ -160,6 +160,8 @@ label-source = Source label-primary-manifest = Primary manifest # This refers to how we integrate a custom game with the manifest data. label-integration = Integration +# This is a folder name where a specific game is installed +label-installed-name = Installed name store-ea = EA store-epic = Epic diff --git a/src/gui/app.rs b/src/gui/app.rs index 2b661b4..a0e9c15 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -1163,6 +1163,7 @@ impl App { prefer_alias: false, files: standard.files.keys().cloned().collect(), registry: standard.registry.keys().cloned().collect(), + install_dir: standard.install_dir.keys().filter(|x| *x != &name).cloned().collect(), expanded: true, } } else { @@ -1174,6 +1175,7 @@ impl App { prefer_alias: false, files: vec![], registry: vec![], + install_dir: vec![], expanded: true, } }; @@ -1198,6 +1200,7 @@ impl App { prefer_alias: true, files: vec![], registry: vec![], + install_dir: vec![], expanded: true, }; @@ -1628,6 +1631,29 @@ impl App { self.config.custom_games[game_index].registry.swap(index, offset); } }, + config::Event::CustomGameInstallDir(game_index, action) => match action { + EditAction::Add => { + self.text_histories.custom_games[game_index] + .install_dir + .push(Default::default()); + self.config.custom_games[game_index].install_dir.push("".to_string()); + } + EditAction::Change(index, value) => { + self.text_histories.custom_games[game_index].install_dir[index].push(&value); + self.config.custom_games[game_index].install_dir[index] = value; + } + EditAction::Remove(index) => { + self.text_histories.custom_games[game_index].install_dir.remove(index); + self.config.custom_games[game_index].install_dir.remove(index); + } + EditAction::Move(index, direction) => { + let offset = direction.shift(index); + self.text_histories.custom_games[game_index] + .install_dir + .swap(index, offset); + self.config.custom_games[game_index].install_dir.swap(index, offset); + } + }, config::Event::ExcludeStoreScreenshots(enabled) => { self.config.backup.filter.exclude_store_screenshots = enabled; } @@ -2387,6 +2413,10 @@ impl App { &mut self.config.custom_games[i].registry[j], &mut self.text_histories.custom_games[i].registry[j], ), + UndoSubject::CustomGameInstallDir(i, j) => shortcut.apply_to_string_field( + &mut self.config.custom_games[i].install_dir[j], + &mut self.text_histories.custom_games[i].install_dir[j], + ), UndoSubject::BackupFilterIgnoredPath(i) => shortcut.apply_to_strict_path_field( &mut self.config.backup.filter.ignored_paths[i], &mut self.text_histories.backup_filter_ignored_paths[i], diff --git a/src/gui/common.rs b/src/gui/common.rs index 98a722d..add71e3 100644 --- a/src/gui/common.rs +++ b/src/gui/common.rs @@ -642,6 +642,7 @@ pub enum UndoSubject { CustomGameAlias(usize), CustomGameFile(usize, usize), CustomGameRegistry(usize, usize), + CustomGameInstallDir(usize, usize), BackupFilterIgnoredPath(usize), BackupFilterIgnoredRegistry(usize), RcloneExecutable, @@ -669,6 +670,7 @@ impl UndoSubject { | UndoSubject::CustomGameAlias(_) | UndoSubject::CustomGameFile(_, _) | UndoSubject::CustomGameRegistry(_, _) + | UndoSubject::CustomGameInstallDir(_, _) | UndoSubject::BackupFilterIgnoredPath(_) | UndoSubject::BackupFilterIgnoredRegistry(_) | UndoSubject::RcloneExecutable diff --git a/src/gui/editor.rs b/src/gui/editor.rs index cd8466d..6639d20 100644 --- a/src/gui/editor.rs +++ b/src/gui/editor.rs @@ -508,6 +508,49 @@ pub fn custom_games<'a>( i, )), ) + }) + .push_if(config.custom_games[i].kind() == CustomGameKind::Game, || { + Row::new() + .spacing(10) + .push( + Column::new() + .width(left_side) + .padding(padding::top(top_side)) + .push(text(TRANSLATOR.field(&TRANSLATOR.custom_installed_name_label()))), + ) + .push( + x.install_dir + .iter() + .enumerate() + .fold(Column::new().spacing(4), |column, (ii, _)| { + column.push( + Row::new() + .align_y(Alignment::Center) + .spacing(20) + .push(button::move_up_nested( + Message::config2(config::Event::CustomGameInstallDir), + i, + ii, + )) + .push(button::move_down_nested( + Message::config2(config::Event::CustomGameInstallDir), + i, + ii, + x.install_dir.len(), + )) + .push(histories.input(UndoSubject::CustomGameInstallDir(i, ii))) + .push(button::remove_nested( + Message::config2(config::Event::CustomGameInstallDir), + i, + ii, + )), + ) + }) + .push(button::add_nested( + Message::config2(config::Event::CustomGameInstallDir), + i, + )), + ) }); } diff --git a/src/gui/shortcuts.rs b/src/gui/shortcuts.rs index 879b540..edcf2b5 100644 --- a/src/gui/shortcuts.rs +++ b/src/gui/shortcuts.rs @@ -208,6 +208,7 @@ pub struct CustomGameHistory { pub alias: TextHistory, pub files: Vec, pub registry: Vec, + pub install_dir: Vec, } #[derive(Default)] @@ -297,6 +298,7 @@ impl TextHistories { alias: TextHistory::raw(&game.alias.clone().unwrap_or_default()), files: game.files.iter().map(|x| TextHistory::raw(x)).collect(), registry: game.registry.iter().map(|x| TextHistory::raw(x)).collect(), + install_dir: game.install_dir.iter().map(|x| TextHistory::raw(x)).collect(), }; self.custom_games.push(history); } @@ -341,6 +343,11 @@ impl TextHistories { .get(*i) .and_then(|x| x.registry.get(*j).map(|y| y.current())) .unwrap_or_default(), + UndoSubject::CustomGameInstallDir(i, j) => self + .custom_games + .get(*i) + .and_then(|x| x.install_dir.get(*j).map(|y| y.current())) + .unwrap_or_default(), UndoSubject::BackupFilterIgnoredPath(i) => self .backup_filter_ignored_paths .get(*i) @@ -404,6 +411,9 @@ impl TextHistories { UndoSubject::CustomGameRegistry(i, j) => Box::new(Message::config(move |value| { config::Event::CustomGameRegistry(i, EditAction::Change(j, value)) })), + UndoSubject::CustomGameInstallDir(i, j) => Box::new(Message::config(move |value| { + config::Event::CustomGameInstallDir(i, EditAction::Change(j, value)) + })), UndoSubject::BackupFilterIgnoredPath(i) => Box::new(Message::config(move |value| { config::Event::BackupFilterIgnoredPath(EditAction::Change(i, value)) })), @@ -444,6 +454,7 @@ impl TextHistories { UndoSubject::CustomGameAlias(_) => TRANSLATOR.custom_game_name_placeholder(), UndoSubject::CustomGameFile(_, _) => "".to_string(), UndoSubject::CustomGameRegistry(_, _) => "".to_string(), + UndoSubject::CustomGameInstallDir(_, _) => "".to_string(), UndoSubject::BackupFilterIgnoredPath(_) => "".to_string(), UndoSubject::BackupFilterIgnoredRegistry(_) => "".to_string(), UndoSubject::RcloneExecutable => TRANSLATOR.executable_label(), @@ -462,6 +473,7 @@ impl TextHistories { | UndoSubject::RedirectSource(_) | UndoSubject::RedirectTarget(_) | UndoSubject::CustomGameFile(_, _) + | UndoSubject::CustomGameInstallDir(_, _) | UndoSubject::BackupFilterIgnoredPath(_) | UndoSubject::RcloneExecutable => (!path_appears_valid(¤t)).then_some(ERROR_ICON), UndoSubject::CustomGameName(_) | UndoSubject::CustomGameAlias(_) => { diff --git a/src/lang.rs b/src/lang.rs index 293e957..370e936 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -924,6 +924,10 @@ impl Translator { translate("field-custom-registry") } + pub fn custom_installed_name_label(&self) -> String { + translate("label-installed-name") + } + pub fn sort_label(&self) -> String { translate("field-sort") } diff --git a/src/resource/config.rs b/src/resource/config.rs index 7ea69b8..9509b13 100644 --- a/src/resource/config.rs +++ b/src/resource/config.rs @@ -47,6 +47,7 @@ pub enum Event { CustomGaleAliasDisplay(usize, bool), CustomGameFile(usize, EditAction), CustomGameRegistry(usize, EditAction), + CustomGameInstallDir(usize, EditAction), ExcludeStoreScreenshots(bool), CloudFilter(CloudFilter), BackupFilterIgnoredPath(EditAction), @@ -1224,6 +1225,8 @@ pub struct CustomGame { pub files: Vec, /// Any registry keys you want to back up. pub registry: Vec, + /// Bare folder names where the game has been installed. + pub install_dir: Vec, #[serde(skip)] pub expanded: bool, } @@ -2140,6 +2143,10 @@ mod tests { - Custom Registry 1 - Custom Registry 2 - Custom Registry 2 + installDir: + - Custom Install Dir 1 + - Custom Install Dir 2 + - Custom Install Dir 2 "#, ) .unwrap(); @@ -2218,6 +2225,7 @@ mod tests { prefer_alias: false, files: vec![], registry: vec![], + install_dir: vec![], expanded: false, }, CustomGame { @@ -2226,8 +2234,13 @@ mod tests { integration: Integration::Override, alias: None, prefer_alias: false, - files: vec![s("Custom File 1"), s("Custom File 2"), s("Custom File 2"),], - registry: vec![s("Custom Registry 1"), s("Custom Registry 2"), s("Custom Registry 2"),], + files: vec![s("Custom File 1"), s("Custom File 2"), s("Custom File 2")], + registry: vec![s("Custom Registry 1"), s("Custom Registry 2"), s("Custom Registry 2")], + install_dir: vec![ + s("Custom Install Dir 1"), + s("Custom Install Dir 2"), + s("Custom Install Dir 2") + ], expanded: false, }, ], @@ -2326,6 +2339,7 @@ customGames: integration: override files: [] registry: [] + installDir: [] - name: Custom Game 2 integration: extend files: @@ -2336,11 +2350,16 @@ customGames: - Custom Registry 1 - Custom Registry 2 - Custom Registry 2 + installDir: + - Custom Install Dir 1 + - Custom Install Dir 2 + - Custom Install Dir 2 - name: Alias integration: override alias: Other files: [] registry: [] + installDir: [] "# .trim(), serde_yaml::to_string(&Config { @@ -2415,6 +2434,7 @@ customGames: prefer_alias: false, files: vec![], registry: vec![], + install_dir: vec![], expanded: false, }, CustomGame { @@ -2423,8 +2443,13 @@ customGames: integration: Integration::Extend, alias: None, prefer_alias: false, - files: vec![s("Custom File 1"), s("Custom File 2"), s("Custom File 2"),], - registry: vec![s("Custom Registry 1"), s("Custom Registry 2"), s("Custom Registry 2"),], + files: vec![s("Custom File 1"), s("Custom File 2"), s("Custom File 2")], + registry: vec![s("Custom Registry 1"), s("Custom Registry 2"), s("Custom Registry 2")], + install_dir: vec![ + s("Custom Install Dir 1"), + s("Custom Install Dir 2"), + s("Custom Install Dir 2") + ], expanded: false, }, CustomGame { @@ -2435,6 +2460,7 @@ customGames: prefer_alias: false, files: vec![], registry: vec![], + install_dir: vec![], expanded: false, }, ], diff --git a/src/resource/manifest.rs b/src/resource/manifest.rs index fad3743..9446ee4 100644 --- a/src/resource/manifest.rs +++ b/src/resource/manifest.rs @@ -663,6 +663,11 @@ impl Manifest { .into_iter() .map(|x| (x, GameRegistryEntry::default())) .collect(); + stored.install_dir = custom + .install_dir + .into_iter() + .map(|x| (x, GameInstallDirEntry::default())) + .collect(); // We intentionally don't carry over the cloud info for custom games. // If you choose not to back up games with cloud support, // you probably still want to back up your customized versions of such games. @@ -677,6 +682,9 @@ impl Manifest { for item in custom.registry { stored.registry.entry(item).or_default(); } + for item in custom.install_dir { + stored.install_dir.entry(item).or_default(); + } stored.cloud = CloudMetadata::default(); stored.sources.insert(Source::Custom); } @@ -694,6 +702,11 @@ impl Manifest { .into_iter() .map(|x| (x, GameRegistryEntry::default())) .collect(), + install_dir: custom + .install_dir + .into_iter() + .map(|x| (x, GameInstallDirEntry::default())) + .collect(), sources: BTreeSet::from_iter([Source::Custom]), ..Default::default() };