ludusavi 0.31.0

Game save backup tool
Documentation
use std::collections::{HashMap, HashSet};

use crate::{
    prelude::{StrictPath, ENV_DEBUG},
    resource::{config::root, manifest::Os},
    scan::{
        launchers::{heroic::find_prefix, LauncherGame},
        TitleFinder,
    },
};

pub mod library {
    use super::*;

    pub const PATH: &str = "sideload_apps/library.json";

    #[derive(serde::Deserialize)]
    pub struct Data {
        pub games: Vec<Game>,
    }

    #[derive(serde::Deserialize)]
    pub struct Game {
        pub app_name: String,
        pub title: String,
        pub install: Install,
        pub folder_name: Option<StrictPath>,
    }

    #[derive(serde::Deserialize)]
    pub struct Install {
        pub platform: Option<String>,
    }
}

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

    for (app_id, game) in get_library(&root.path) {
        let raw_title = &game.title;

        let Some(official_title) = title_finder.find_one_by_normalized_name(raw_title) else {
            log::trace!("Ignoring unrecognized game: {}", raw_title);
            if std::env::var(ENV_DEBUG).is_ok() {
                eprintln!(
                    "Ignoring unrecognized game from Heroic/sideload: {} (app = {})",
                    raw_title, &app_id
                );
            }
            continue;
        };

        log::trace!(
            "Detected game: {} | app: {}, raw title: {}",
            &official_title,
            &app_id,
            raw_title
        );
        let platform = game.install.platform.as_deref();
        let prefix = find_prefix(&root.path, &game.title, platform, &game.app_name);
        out.entry(official_title).or_default().insert(LauncherGame {
            install_dir: game.folder_name,
            prefix,
            platform: platform.map(Os::from),
        });
    }

    out
}

pub fn get_library(source: &StrictPath) -> HashMap<String, library::Game> {
    let mut out = HashMap::new();

    let file = source.joined(library::PATH);

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

    if let Ok(data) = serde_json::from_str::<library::Data>(&content) {
        for game in data.games {
            out.insert(game.app_name.clone(), game);
        }
    }

    out
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;
    use velcro::{hash_map, hash_set};

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

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

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

    #[test]
    fn scan_finds_all_games() {
        let root = root::Heroic {
            path: format!("{}/tests/launchers/heroic-sideload", repo()).into(),
        };
        let games = scan(&root, &title_finder());
        assert_eq!(
            hash_map! {
                "game-1".to_string(): hash_set![LauncherGame {
                    install_dir: Some(StrictPath::new("/games/game-1".to_string())),
                    prefix: Some(StrictPath::new("/prefixes/game-1".to_string())),
                    platform: Some(Os::Windows),
                }],
            },
            games,
        );
    }
}