loadsmith-platform 0.3.1

Platform and game detection for the loadsmith mod-manager library
Documentation
use std::path::{Path, PathBuf};

use camino::Utf8PathBuf;
use tracing::debug;
use walkdir::WalkDir;

use crate::{Error, Platform, Result};

pub fn find_executables(game_path: &Path) -> impl Iterator<Item = PathBuf> + use<> {
    const IGNORED_FILES: &[&str] = &[
        "crashpad_handler.exe",
        "UnityCrashHandler32.exe",
        "UnityCrashHandler64.exe",
    ];

    #[cfg(target_os = "windows")]
    const EXTENSIONS: &[&str] = &["exe"];

    #[cfg(target_os = "linux")]
    const EXTENSIONS: &[&str] = &["x86_64", "x86", "sh", "exe"];

    WalkDir::new(game_path)
        .into_iter()
        .filter_map(|entry| entry.ok())
        .filter(|entry| entry.file_type().is_file())
        .map(|entry| entry.into_path())
        .filter(|path| {
            let file_name = path
                .file_name()
                .and_then(|s| s.to_str())
                .unwrap_or_default();

            let extension = path
                .extension()
                .and_then(|s| s.to_str())
                .unwrap_or_default();

            EXTENSIONS.contains(&extension) && !IGNORED_FILES.contains(&file_name)
        })
}

pub fn locate_game(platform: &Platform) -> Result<Option<Utf8PathBuf>> {
    match platform {
        Platform::Steam { id } => locate_steam_game(*id),
        #[cfg(target_os = "windows")]
        Platform::XboxStore { identifier } => xbox_game_dir(identifier).map(Some),
        #[cfg(target_os = "windows")]
        Platform::EpicGames { identifier } => epic_game_dir(identifier).map(Some),
        _ => Ok(None),
    }
}

fn locate_steam_game(id: u32) -> Result<Option<Utf8PathBuf>> {
    let steam = steamlocate::SteamDir::locate()?;

    debug!(
        path = %steam.path().display(),
        "found steam installation"
    );

    let (app, lib) = steam.find_app(id)?.ok_or(Error::SteamAppNotFound)?;

    debug!(
        name = app.name,
        library_path = %lib.path().display(),
        "found game in steam library"
    );

    let path = lib.resolve_app_dir(&app);
    let utf8_path = Utf8PathBuf::try_from(path)?;

    Ok(Some(utf8_path))
}

#[cfg(target_os = "windows")]
fn xbox_game_dir(identifier: &str) -> Result<Utf8PathBuf> {
    use std::process::Command;

    let mut query = Command::new("powershell.exe");
    query.args([
        "get-appxpackage",
        "-Name",
        identifier,
        "|",
        "select",
        "-expand",
        "InstallLocation",
    ]);

    debug!(
        command = ?query,
        "running powershell query for Xbox Store game directory"
    );

    let out = query.output()?;

    if !out.status.success() {
        return Err(Error::XboxStoreQueryFailed { status: out.status });
    }

    let s = String::from_utf8(out.stdout).map_err(|source| Error::XboxNonUtf8 { source })?;
    let path = Utf8PathBuf::from(s.trim());

    Ok(path)
}

#[cfg(target_os = "windows")]
fn epic_game_dir(identifier: &str) -> Result<Utf8PathBuf> {
    use serde::Deserialize;

    let dat_path: PathBuf =
        PathBuf::from("C:\\ProgramData\\Epic\\UnrealEngineLauncher\\LauncherInstalled.dat");

    #[derive(Debug, Deserialize)]
    #[serde(rename_all = "PascalCase")]
    struct ListItem {
        install_location: Utf8PathBuf,
        app_name: String,
    }

    debug!(
        path = %dat_path.display(),
        "reading Epic Games installations",
    );

    let s = std::fs::read_to_string(&dat_path)?;
    let list: Vec<ListItem> = serde_json::from_str(&s)?;

    list.into_iter()
        .find(|item| item.app_name == identifier)
        .map(|item| item.install_location)
        .ok_or(Error::EpicGameNotFound)
}