Skip to content

Commit f2803af

Browse files
committed
Add support for nested paths in custom game installed names
1 parent d23a90e commit f2803af

File tree

6 files changed

+41
-38
lines changed

6 files changed

+41
-38
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## Unreleased
22

3+
* Added:
4+
* A custom game's installed name may now be set to a relative path with multiple folders,
5+
rather than only supporting a single bare folder name.
36
* Fixed:
47
* For home folder roots, Ludusavi skipped any paths containing `<storeUserId>`,
58
on the assumption that it shouldn't be applicable to non-store-specific roots.

docs/help/custom-games.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ and the placeholders defined in the
1212
If you have a folder name that contains a special glob character,
1313
you can escape it by wrapping it in brackets (e.g., `[` becomes `[[]`).
1414

15-
Installed names should be a bare folder name only,
15+
Installed names should be a bare folder name <!-- or relative path --> only<!-- (no absolute paths) -->,
1616
because Ludusavi will look for this folder in each root.
1717
Ludusavi automatically looks for the game's own name as well,
1818
so you only need to specify a custom folder name if it's different.
1919
For example, if you have an other-type root at `C:\Games`,
2020
and there's a game called `Some Game` installed at `C:\Games\sg`,
2121
then you would set the installed name as `sg`.
22+
<!--
23+
If you had a bundled game like `C:\Games\trilogy\first-game`,
24+
then you could set the installed name as `trilogy\first-game`.
25+
-->

src/path.rs

+10
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,16 @@ impl StrictPath {
859859
nearest
860860
}
861861

862+
pub fn suffix_for(&self, other: &Self) -> Option<String> {
863+
self.is_prefix_of(other).then(|| {
864+
other
865+
.render()
866+
.replacen(&self.render(), "", 1)
867+
.trim_start_matches(['/', '\\'])
868+
.to_string()
869+
})
870+
}
871+
862872
pub fn glob(&self) -> Vec<StrictPath> {
863873
self.glob_case_sensitive(Os::HOST.is_case_sensitive())
864874
}

src/resource/config.rs

+7
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,13 @@ impl Root {
476476
}
477477
}
478478

479+
pub fn games_path(&self) -> StrictPath {
480+
match self.store() {
481+
Store::Steam => self.path().joined("steamapps/common"),
482+
_ => self.path().clone(),
483+
}
484+
}
485+
479486
pub fn lutris_database(&self) -> Option<&StrictPath> {
480487
match self {
481488
Self::Lutris(root) => root.database.as_ref(),

src/scan.rs

+7-29
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ pub fn parse_paths(
146146
.replace(&format!("{}*", p::STORE_USER_ID), p::STORE_USER_ID);
147147

148148
let install_dir = install_dir.map(|x| x.as_str()).unwrap_or(SKIP);
149+
let full_install_dir = full_install_dir
150+
.and_then(|x| x.interpret().ok())
151+
.unwrap_or_else(|| SKIP.to_string());
149152

150153
let Ok(root_interpreted) = root.path().interpret_unless_skip() else {
151154
return HashSet::new();
@@ -161,29 +164,7 @@ pub fn parse_paths(
161164
add_path!(path
162165
.replace(p::ROOT, &root_interpreted)
163166
.replace(p::GAME, install_dir)
164-
.replace(
165-
p::BASE,
166-
&match root.store() {
167-
Store::Steam => format!("{}/steamapps/common/{}", &root_interpreted, install_dir),
168-
Store::Heroic | Store::Legendary | Store::Lutris => full_install_dir
169-
.and_then(|x| x.interpret().ok())
170-
.unwrap_or_else(|| SKIP.to_string()),
171-
Store::Ea
172-
| Store::Epic
173-
| Store::Gog
174-
| Store::GogGalaxy
175-
| Store::Microsoft
176-
| Store::Origin
177-
| Store::Prime
178-
| Store::Uplay
179-
| Store::OtherHome
180-
| Store::OtherWine
181-
| Store::OtherWindows
182-
| Store::OtherLinux
183-
| Store::OtherMac
184-
| Store::Other => format!("{}/{}", &root_interpreted, install_dir),
185-
},
186-
)
167+
.replace(p::BASE, &full_install_dir)
187168
.replace(p::HOME, home)
188169
.replace(p::STORE_USER_ID, "*")
189170
.replace(p::OS_USER_NAME, &crate::prelude::OS_USERNAME)
@@ -262,10 +243,7 @@ pub fn parse_paths(
262243
let path2 = path
263244
.replace(p::ROOT, &root_interpreted)
264245
.replace(p::GAME, install_dir)
265-
.replace(
266-
p::BASE,
267-
&format!("{}/steamapps/common/{}", &root_interpreted, install_dir),
268-
)
246+
.replace(p::BASE, &full_install_dir)
269247
.replace(p::HOME, &format!("{}/users/steamuser", prefix))
270248
.replace(p::STORE_USER_ID, "*")
271249
.replace(p::OS_USER_NAME, "steamuser")
@@ -575,8 +553,8 @@ pub fn scan_game_for_backup(
575553

576554
if launcher_entries.peek().is_none() {
577555
let platform = Os::HOST;
578-
let install_dir = None;
579556
let full_install_dir = None;
557+
let install_dir = None;
580558

581559
candidates.extend(parse_paths(
582560
raw_path,
@@ -593,8 +571,8 @@ pub fn scan_game_for_backup(
593571
for launcher_entry in launcher_entries {
594572
log::trace!("[{name}] parsing candidates with launcher info: {:?}", &launcher_entry);
595573
let platform = launcher_entry.platform.unwrap_or(Os::HOST);
596-
let install_dir = launcher_entry.install_dir.as_ref().and_then(|x| x.leaf());
597574
let full_install_dir = launcher_entry.install_dir.as_ref();
575+
let install_dir = full_install_dir.and_then(|x| root.path().suffix_for(x));
598576

599577
candidates.extend(parse_paths(
600578
raw_path,

src/scan/launchers/generic.rs

+9-8
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ use rayon::prelude::*;
66
use crate::prelude::INVALID_FILE_CHARS;
77

88
use crate::{
9-
resource::{
10-
config::Root,
11-
manifest::{Manifest, Store},
12-
},
9+
resource::{config::Root, manifest::Manifest},
1310
scan::launchers::LauncherGame,
1411
};
1512

@@ -55,10 +52,7 @@ fn fuzzy_match(
5552
pub fn scan(root: &Root, manifest: &Manifest, subjects: &[String]) -> HashMap<String, HashSet<LauncherGame>> {
5653
log::debug!("ranking installations for root: {:?}", &root);
5754

58-
let install_parent = match root.store() {
59-
Store::Steam => root.path().joined("steamapps/common"),
60-
_ => root.path().clone(),
61-
};
55+
let install_parent = root.games_path();
6256
let matcher = make_fuzzy_matcher();
6357

6458
let actual_dirs: Vec<_> = install_parent
@@ -83,6 +77,13 @@ pub fn scan(root: &Root, manifest: &Manifest, subjects: &[String]) -> HashMap<St
8377
let mut best: Option<(i64, &String)> = None;
8478
'dirs: for expected_dir in expected_install_dirs {
8579
log::trace!("[{name}] looking for install dir: {expected_dir}");
80+
81+
if expected_dir.contains(['/', '\\']) && root.path().joined(expected_dir).is_dir() {
82+
log::trace!("[{name}] using exact nested install dir");
83+
best = Some((i64::MAX, expected_dir));
84+
break 'dirs;
85+
}
86+
8687
let ideal = matcher.fuzzy_match(expected_dir, expected_dir);
8788
for actual_dir in &actual_dirs {
8889
let score = fuzzy_match(&matcher, expected_dir, actual_dir, ideal);

0 commit comments

Comments
 (0)