use std::collections::HashMap; use std::path::PathBuf; use color_eyre::eyre::{OptionExt as _, WrapErr as _}; use color_eyre::Result; use serde::{Deserialize, Serialize}; use steamlocate::SteamDir; use time::OffsetDateTime; pub use log::*; mod log; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct ModConfigResources { pub init: PathBuf, #[serde(default, skip_serializing_if = "Option::is_none")] pub data: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub localization: Option, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum ModOrder { Before, After, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[serde(untagged)] pub enum ModDependency { ID(String), Config { id: String, order: ModOrder }, } // A bit dumb, but serde doesn't support literal values with the // `default` attribute, only paths. fn default_true() -> bool { true } // Similarly dumb, as the `skip_serializing_if` attribute needs a function fn is_true(val: &bool) -> bool { *val } #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct ModConfig { #[serde(skip)] pub dir: PathBuf, pub id: String, pub name: String, pub summary: String, pub version: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub author: Option, #[serde(skip_serializing_if = "Option::is_none")] pub image: Option, #[serde(default)] pub categories: Vec, #[serde(default)] pub packages: Vec, pub resources: ModConfigResources, #[serde(default)] pub depends: Vec, #[serde(default = "default_true", skip_serializing_if = "is_true")] pub bundled: bool, #[serde(default)] pub name_overrides: HashMap, } pub const STEAMAPP_ID: u32 = 1361210; #[derive(Debug)] pub struct GameInfo { pub path: PathBuf, pub last_updated: OffsetDateTime, } pub fn collect_game_info() -> Result> { let dir = SteamDir::locate().wrap_err("Failed to locate Steam installation")?; let found = dir .find_app(STEAMAPP_ID) .wrap_err("Failed to look up game by Steam app ID")?; let Some((app, _)) = found else { return Ok(None); }; let last_updated = app .last_updated .ok_or_eyre("Missing field 'last_updated'")?; Ok(Some(GameInfo { path: app.install_dir.into(), last_updated: last_updated.into(), })) }