use std::collections::HashMap;
use crate::prelude::{StrictPath, ENV_DEBUG};
use crate::{
resource::{config::RootsConfig, manifest::Os},
scan::{launchers::LauncherGame, TitleFinder},
};
#[derive(serde::Deserialize)]
struct HeroicInstalledGame {
#[serde(rename = "appName")]
app_name: String,
platform: String,
install_path: String,
}
#[derive(serde::Deserialize)]
struct HeroicInstalled {
installed: Vec<HeroicInstalledGame>,
}
#[derive(serde::Deserialize)]
struct GogLibraryGame {
app_name: String,
title: String,
}
#[derive(serde::Deserialize)]
struct GogLibrary {
games: Vec<GogLibraryGame>,
}
#[derive(serde::Deserialize)]
struct LegendaryInstalledGame {
#[serde(rename = "app_name")]
app_name: String,
title: String,
platform: String,
install_path: String,
}
#[derive(serde::Deserialize)]
struct LegendaryInstalled(HashMap<String, LegendaryInstalledGame>);
#[derive(serde::Deserialize, Debug)]
struct GamesConfigWrapper(HashMap<String, GamesConfig>);
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum GamesConfig {
Config {
#[serde(rename = "winePrefix")]
wine_prefix: String,
#[serde(rename = "wineVersion")]
wine_version: GamesConfigWine,
},
IgnoreOther(serde::de::IgnoredAny),
}
#[derive(serde::Deserialize, Debug)]
struct GamesConfigWine {
#[serde(rename = "type")]
wine_type: String,
}
pub fn scan(
root: &RootsConfig,
title_finder: &TitleFinder,
legendary: Option<&StrictPath>,
) -> HashMap<String, LauncherGame> {
let mut games = HashMap::new();
games.extend(detect_legendary_games(root, title_finder, legendary));
games.extend(detect_gog_games(root, title_finder));
games
}
fn detect_legendary_games(
root: &RootsConfig,
title_finder: &TitleFinder,
legendary: Option<&StrictPath>,
) -> HashMap<String, LauncherGame> {
let mut games = HashMap::new();
log::trace!("detect_legendary_games searching for legendary config...");
let legendary_paths = match legendary {
None => vec![
StrictPath::relative("../legendary".to_string(), Some(root.path.interpret())),
StrictPath::new("~/.config/legendary".to_string()),
],
Some(x) => vec![x.clone()],
};
for legendary_path in legendary_paths {
if legendary_path.is_dir() {
log::trace!(
"detect_legendary_games checking for legendary configuration in {}",
legendary_path.interpret()
);
let legendary_installed = legendary_path.joined("installed.json");
if legendary_installed.is_file() {
if let Ok(installed_games) =
serde_json::from_str::<LegendaryInstalled>(&legendary_installed.read().unwrap_or_default())
{
for game in installed_games.0.values() {
log::trace!(
"detect_legendary_games found legendary game {} ({})",
game.title,
game.app_name
);
let official_title =
title_finder.find_one(&[game.title.to_owned()], &None, &None, true, true, false);
let prefix =
find_prefix(&root.path, &game.title, &game.platform.to_lowercase(), &game.app_name);
memorize_game(
&mut games,
&game.title,
official_title,
StrictPath::new(game.install_path.clone()),
prefix,
&game.platform,
);
}
}
} else {
log::trace!(
"detect_legendary_games no such file '{:?}', legendary probably not used yet... skipping",
legendary_installed
);
}
}
}
games
}
fn detect_gog_games(root: &RootsConfig, title_finder: &TitleFinder) -> HashMap<String, LauncherGame> {
let mut games = HashMap::new();
log::trace!(
"detect_gog_games searching for GOG information in {}",
root.path.interpret()
);
let library_path = root.path.joined("gog_store").joined("library.json");
let game_titles: HashMap<String, String> =
match serde_json::from_str::<GogLibrary>(&library_path.read().unwrap_or_default()) {
Ok(gog_library) => gog_library
.games
.iter()
.map(|game| (game.app_name.clone(), game.title.clone()))
.collect(),
Err(e) => {
log::warn!(
"detect_gog_games aborting since it could not read {}: {}",
library_path.interpret(),
e
);
return games;
}
};
log::trace!(
"detect_gog_games found {} games in {}",
game_titles.len(),
library_path.interpret()
);
let installed_path = root.path.joined("gog_store").joined("installed.json");
let content = installed_path.read();
if let Ok(installed_games) = serde_json::from_str::<HeroicInstalled>(&content.unwrap_or_default()) {
for game in installed_games.installed {
if let Some(game_title) = game_titles.get(&game.app_name) {
let gog_id: Option<u64> = game.app_name.parse().ok();
let official_title = title_finder.find_one(&[game_title.to_owned()], &None, &gog_id, true, true, false);
let prefix = find_prefix(&root.path, game_title, &game.platform, &game.app_name);
memorize_game(
&mut games,
game_title,
official_title,
StrictPath::new(game.install_path),
prefix,
&game.platform,
);
}
}
}
games
}
fn memorize_game(
games: &mut HashMap<String, LauncherGame>,
heroic_title: &str,
official_title: Option<String>,
install_dir: StrictPath,
prefix: Option<StrictPath>,
platform: &str,
) {
let platform = Some(Os::from(platform));
log::trace!(
"memorize_game memorizing info for '{:?}' (from: '{}'): install_dir={:?}, prefix={:?}",
&official_title,
heroic_title,
&install_dir,
&prefix
);
if let Some(official) = official_title {
games.insert(
official,
LauncherGame {
install_dir,
prefix,
platform,
},
);
} else {
let log_message = format!("Ignoring unrecognized Heroic game: '{}'", heroic_title);
if std::env::var(ENV_DEBUG).is_ok() {
eprintln!("{}", &log_message);
}
log::info!("{}", &log_message);
games.insert(
heroic_title.to_string(),
LauncherGame {
install_dir,
prefix,
platform,
},
);
}
}
fn find_prefix(heroic_path: &StrictPath, game_name: &str, platform: &str, app_name: &str) -> Option<StrictPath> {
match platform {
"windows" => {
log::trace!(
"find_prefix found Heroic Windows game {}, looking closer ...",
game_name
);
let games_config_path = heroic_path.joined("GamesConfig").joined(&format!("{app_name}.json"));
match serde_json::from_str::<GamesConfigWrapper>(&games_config_path.read().unwrap_or_default()) {
Ok(games_config_wrapper) => {
if let Some(game_config) = games_config_wrapper.0.get(app_name) {
match game_config {
GamesConfig::Config {
wine_version,
wine_prefix,
} => match wine_version.wine_type.as_str() {
"wine" => {
log::trace!(
"find_prefix found Heroic Wine prefix for {} ({}) -> adding {}",
game_name,
app_name,
wine_prefix
);
Some(StrictPath::new(wine_prefix.clone()))
}
"proton" => {
log::trace!(
"find_prefix found Heroic Proton prefix for {} ({}), adding... -> {}",
game_name,
app_name,
format!("{}/pfx", wine_prefix)
);
Some(StrictPath::new(format!("{}/pfx", wine_prefix)))
}
_ => {
log::info!(
"find_prefix found Heroic Windows game {} ({}), checking... unknown wine_type: {:#?} -> ignored",
game_name,
app_name,
wine_version.wine_type
);
None
}
},
GamesConfig::IgnoreOther(_) => None,
}
} else {
None
}
}
Err(e) => {
log::trace!("find_prefix error: '{}', ignoring", e);
None
}
}
}
"linux" => {
log::trace!("find_prefix found Heroic Linux game {}, ignoring", game_name);
None
}
_ => {
log::trace!(
"find_prefix found Heroic game {} with unhandled platform {}, ignoring.",
game_name,
platform,
);
None
}
}
}
#[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::Heroic,
};
let legendary = Some(StrictPath::new(format!("{}/tests/nonexistent", repo())));
let games = scan(&root, &title_finder(), legendary.as_ref());
assert_eq!(HashMap::new(), games);
}
#[test]
fn scan_finds_all_games() {
let root = RootsConfig {
path: StrictPath::new(format!("{}/tests/launchers/heroic", repo())),
store: Store::Heroic,
};
let legendary = Some(StrictPath::new(format!("{}/tests/launchers/legendary", repo())));
let games = scan(&root, &title_finder(), legendary.as_ref());
assert_eq!(
hashmap! {
"windows-game".to_string() => LauncherGame {
install_dir: StrictPath::new("C:\\Users\\me\\Games\\Heroic\\windows-game".to_string()),
prefix: Some(StrictPath::new("/home/root/Games/Heroic/Prefixes/windows-game".to_string())),
platform: Some(Os::Windows),
},
"proton-game".to_string() => LauncherGame {
install_dir: StrictPath::new("/home/root/Games/proton-game".to_string()),
prefix: Some(StrictPath::new("/home/root/Games/Heroic/Prefixes/proton-game/pfx".to_string())),
platform: Some(Os::Windows),
},
},
games,
);
}
}