Skip to content

Commit

Permalink
#278: Add Legendary root
Browse files Browse the repository at this point in the history
  • Loading branch information
mtkennerly committed Dec 23, 2023
1 parent da57555 commit 91f72a8
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
that might not be covered by PCGamingWiki.
* You can now configure roots for OS installations on other drives.
New root types: `Windows drive`, `Linux drive`, `Mac drive`
* Ludusavi can now scan Legendary games on their own without Heroic.
New root type: `Legendary`
* CLI: `wrap` command to do a restore before playing a game and a backup afterwards.
([Contributed by sluedecke](https://github.com/mtkennerly/ludusavi/pull/235))
* When a path or URL fails to open, additional information is now logged.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ along with the root's type:

When using Wine prefixes with Heroic, Ludusavi will back up the `*.reg` files
if the game is known to have registry-based saves.
<!--
* For a Legendary root, this should be the folder containing `installed.json`.
Currently, Ludusavi cannot detect Wine prefixes for Legendary roots.
-->
* For a Lutris root, this should be the folder containing the `games` subdirectory.

Ludusavi expects the game YAML files to contain a few fields,
Expand Down
1 change: 1 addition & 0 deletions lang/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ store-epic = Epic
store-gog = GOG
store-gog-galaxy = GOG Galaxy
store-heroic = Heroic
store-legendary = Legendary
store-lutris = Lutris
store-microsoft = Microsoft
store-origin = Origin
Expand Down
1 change: 1 addition & 0 deletions src/lang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,7 @@ impl Translator {
Store::Gog => "store-gog",
Store::GogGalaxy => "store-gog-galaxy",
Store::Heroic => "store-heroic",
Store::Legendary => "store-legendary",
Store::Lutris => "store-lutris",
Store::Microsoft => "store-microsoft",
Store::Origin => "store-origin",
Expand Down
6 changes: 5 additions & 1 deletion src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,11 @@ impl StrictPath {
}

pub fn read(&self) -> Option<String> {
std::fs::read_to_string(std::path::Path::new(&self.interpret())).ok()
self.try_read().ok()
}

pub fn try_read(&self) -> Result<String, AnyError> {
Ok(std::fs::read_to_string(std::path::Path::new(&self.interpret()))?)
}

pub fn size(&self) -> u64 {
Expand Down
3 changes: 3 additions & 0 deletions src/resource/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ pub enum Store {
GogGalaxy,
#[serde(rename = "heroic")]
Heroic,
#[serde(rename = "legendary")]
Legendary,
#[serde(rename = "lutris")]
Lutris,
#[serde(rename = "microsoft")]
Expand Down Expand Up @@ -120,6 +122,7 @@ impl Store {
Store::Gog,
Store::GogGalaxy,
Store::Heroic,
Store::Legendary,
Store::Lutris,
Store::Microsoft,
Store::Origin,
Expand Down
2 changes: 1 addition & 1 deletion src/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ pub fn parse_paths(
BASE,
&match root.store {
Store::Steam => format!("{}/steamapps/common/{}", &root_interpreted, install_dir),
Store::Heroic | Store::Lutris => full_install_dir
Store::Heroic | Store::Legendary | Store::Lutris => full_install_dir
.map(|x| x.interpret())
.unwrap_or_else(|| SKIP.to_string()),
Store::Ea
Expand Down
5 changes: 4 additions & 1 deletion src/scan/launchers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod generic;
pub mod heroic;
mod legendary;
mod lutris;

use std::collections::HashMap;
Expand Down Expand Up @@ -56,12 +57,14 @@ impl Launchers {
let mut instance = Self::default();

for root in roots {
log::debug!("Scanning launcher info: {:?} - {}", root.store, root.path.render());
let found = match root.store {
Store::Heroic => heroic::scan(root, title_finder, legendary.as_ref()),
Store::Legendary => legendary::scan(root, title_finder),
Store::Lutris => lutris::scan(root, title_finder),
_ => generic::scan(root, manifest, subjects),
};
log::trace!(
log::debug!(
"launcher games found ({:?} - {}): {:#?}",
root.store,
root.path.raw(),
Expand Down
134 changes: 134 additions & 0 deletions src/scan/launchers/legendary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::collections::HashMap;

use crate::{
prelude::StrictPath,
resource::{config::RootsConfig, manifest::Os},
scan::{launchers::LauncherGame, TitleFinder},
};

#[derive(Clone, serde::Deserialize)]
struct Game {
/// This is an opaque ID, not the human-readable title.
app_name: String,
title: String,
platform: String,
install_path: String,
}

/// installed.json
#[derive(serde::Deserialize)]
struct Library(HashMap<String, Game>);

pub fn scan(root: &RootsConfig, title_finder: &TitleFinder) -> HashMap<String, LauncherGame> {
let mut out = HashMap::new();

for game in get_games(&root.path) {
let Some(official_title) = title_finder.find_one(&[game.title.to_owned()], &None, &None, true) else {
log::trace!("Ignoring unrecognized game: {}", &game.title);
continue;
};

log::trace!(
"Detected game: {} | app: {}, raw title: {}",
&official_title,
&game.app_name,
&game.title
);
out.insert(
official_title,
LauncherGame {
install_dir: StrictPath::new(game.install_path),
prefix: None,
platform: Some(Os::from(game.platform.as_str())),
},
);
}

out
}

fn get_games(source: &StrictPath) -> Vec<Game> {
let mut out: Vec<Game> = Vec::new();

let library = source.joined("installed.json");

let content = match library.try_read() {
Ok(content) => content,
Err(e) => {
log::debug!(
"In Legendary source '{}', unable to read installed.json | {:?}",
library.render(),
e,
);
return out;
}
};

if let Ok(installed_games) = serde_json::from_str::<Library>(&content) {
out.extend(installed_games.0.into_values());
}

out
}

#[cfg(test)]
mod tests {
use maplit::hashmap;
use pretty_assertions::assert_eq;

use super::*;
use crate::{
resource::{
manifest::{Manifest, Store},
ResourceFile,
},
testing::repo,
};

fn manifest() -> Manifest {
Manifest::load_from_string(
r#"
windows-game:
files:
<base>/file1.txt: {}
proton-game:
files:
<base>/file1.txt: {}
"#,
)
.unwrap()
}

fn title_finder() -> TitleFinder {
TitleFinder::new(&manifest(), &Default::default())
}

#[test]
fn scan_finds_nothing_when_folder_does_not_exist() {
let root = RootsConfig {
path: StrictPath::new(format!("{}/tests/nonexistent", repo())),
store: Store::Legendary,
};
let games = scan(&root, &title_finder());
assert_eq!(HashMap::new(), games);
}

#[test]
fn scan_finds_all_games() {
let root = RootsConfig {
path: StrictPath::new(format!("{}/tests/launchers/legendary", repo())),
store: Store::Legendary,
};
let games = scan(&root, &title_finder());
assert_eq!(
hashmap! {
"windows-game".to_string() => LauncherGame {
install_dir: StrictPath::new("C:\\Users\\me\\Games\\Heroic\\windows-game".to_string()),
prefix: None,
platform: Some(Os::Windows),
},
},
games,
);
}
}

0 comments on commit 91f72a8

Please sign in to comment.