Skip to content

Commit

Permalink
#434: Allow specifying install dirs for custom games
Browse files Browse the repository at this point in the history
  • Loading branch information
mtkennerly committed Dec 22, 2024
1 parent d1158be commit ef32a5f
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<base>` and `<game>` 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.
Expand Down
12 changes: 10 additions & 2 deletions docs/help/custom-games.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<!--
Installed names should be a bare folder name only,
because Ludusavi will look for this folder in each root.
Ludusavi automatically looks for the game's own name as well,
so you only need to specify a custom folder name if it's different.
For example, if you have an other-type root at `C:\Games`,
and there's a game called `Some Game` installed at `C:\Games\sg`,
then you would set the installed name as `sg`.
-->
2 changes: 2 additions & 0 deletions lang/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions src/gui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -1174,6 +1175,7 @@ impl App {
prefer_alias: false,
files: vec![],
registry: vec![],
install_dir: vec![],
expanded: true,
}
};
Expand All @@ -1198,6 +1200,7 @@ impl App {
prefer_alias: true,
files: vec![],
registry: vec![],
install_dir: vec![],
expanded: true,
};

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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],
Expand Down
2 changes: 2 additions & 0 deletions src/gui/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ pub enum UndoSubject {
CustomGameAlias(usize),
CustomGameFile(usize, usize),
CustomGameRegistry(usize, usize),
CustomGameInstallDir(usize, usize),
BackupFilterIgnoredPath(usize),
BackupFilterIgnoredRegistry(usize),
RcloneExecutable,
Expand Down Expand Up @@ -669,6 +670,7 @@ impl UndoSubject {
| UndoSubject::CustomGameAlias(_)
| UndoSubject::CustomGameFile(_, _)
| UndoSubject::CustomGameRegistry(_, _)
| UndoSubject::CustomGameInstallDir(_, _)
| UndoSubject::BackupFilterIgnoredPath(_)
| UndoSubject::BackupFilterIgnoredRegistry(_)
| UndoSubject::RcloneExecutable
Expand Down
43 changes: 43 additions & 0 deletions src/gui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)),
)
});
}

Expand Down
12 changes: 12 additions & 0 deletions src/gui/shortcuts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ pub struct CustomGameHistory {
pub alias: TextHistory,
pub files: Vec<TextHistory>,
pub registry: Vec<TextHistory>,
pub install_dir: Vec<TextHistory>,
}

#[derive(Default)]
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
})),
Expand Down Expand Up @@ -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(),
Expand All @@ -462,6 +473,7 @@ impl TextHistories {
| UndoSubject::RedirectSource(_)
| UndoSubject::RedirectTarget(_)
| UndoSubject::CustomGameFile(_, _)
| UndoSubject::CustomGameInstallDir(_, _)
| UndoSubject::BackupFilterIgnoredPath(_)
| UndoSubject::RcloneExecutable => (!path_appears_valid(&current)).then_some(ERROR_ICON),
UndoSubject::CustomGameName(_) | UndoSubject::CustomGameAlias(_) => {
Expand Down
4 changes: 4 additions & 0 deletions src/lang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
34 changes: 30 additions & 4 deletions src/resource/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -1224,6 +1225,8 @@ pub struct CustomGame {
pub files: Vec<String>,
/// Any registry keys you want to back up.
pub registry: Vec<String>,
/// Bare folder names where the game has been installed.
pub install_dir: Vec<String>,
#[serde(skip)]
pub expanded: bool,
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -2218,6 +2225,7 @@ mod tests {
prefer_alias: false,
files: vec![],
registry: vec![],
install_dir: vec![],
expanded: false,
},
CustomGame {
Expand All @@ -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,
},
],
Expand Down Expand Up @@ -2326,6 +2339,7 @@ customGames:
integration: override
files: []
registry: []
installDir: []
- name: Custom Game 2
integration: extend
files:
Expand All @@ -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 {
Expand Down Expand Up @@ -2415,6 +2434,7 @@ customGames:
prefer_alias: false,
files: vec![],
registry: vec![],
install_dir: vec![],
expanded: false,
},
CustomGame {
Expand All @@ -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 {
Expand All @@ -2435,6 +2460,7 @@ customGames:
prefer_alias: false,
files: vec![],
registry: vec![],
install_dir: vec![],
expanded: false,
},
],
Expand Down
13 changes: 13 additions & 0 deletions src/resource/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
}
Expand All @@ -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()
};
Expand Down

0 comments on commit ef32a5f

Please sign in to comment.