#![allow(clippy::missing_errors_doc)] #![allow(clippy::doc_markdown)] use std::{collections::HashMap, fmt, path::Path}; use bytes::Bytes; use serde::{Deserialize, Serialize}; /// Read version from version.ini file /// # Errors /// Returns error if file cannot be read or parsed pub fn read_version_from_ini(game_dir: &Path) -> eyre::Result> { let version_file = game_dir.join("version.ini"); if !version_file.exists() { return Ok(None); } let content = std::fs::read_to_string(&version_file)?; let version = content.trim().to_string(); // Validate format (YYYYMMDD) if version.len() == 8 && version.chars().all(|c| c.is_ascii_digit()) { Ok(Some(version)) } else { tracing::warn!( "Invalid version format in {}: {}", version_file.display(), version ); Ok(None) } } /// A game #[derive(Clone, Serialize, Deserialize)] pub struct Game { /// example: aoe2 pub id: String, /// example: Age of Empires 2 pub name: String, /// example: Dieses Paket enthält die original AoE 2 Version,... pub description: String, /// example: 1999 pub release_year: String, /// Microsoft pub publisher: String, /// example: 8 pub max_players: u32, /// example: 3.5 pub version: String, /// example: Echtzeit-Strategie pub genre: String, /// size in bytes: example: 3455063152 pub size: u64, /// thumbnail image pub thumbnail: Option, /// only relevant for client (yeah... I know) pub installed: bool, /// ETI game version from version.ini (YYYYMMDD format) (server) pub eti_game_version: Option, /// Local game version from version.ini (YYYYMMDD format) pub local_version: Option, } impl fmt::Debug for Game { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}: {} ({} MB)", self.id, self.name, self.size / 1024 / 1024, ) } } impl fmt::Display for Game { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name) } } impl PartialEq for Game { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for Game {} impl PartialOrd for Game { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for Game { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.name.cmp(&other.name) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GameDB { pub games: HashMap, } impl GameDB { #[must_use] pub fn empty() -> Self { GameDB { games: HashMap::new(), } } #[must_use] pub fn from(games: Vec) -> Self { let mut db = GameDB::empty(); for game in games { db.games.insert(game.id.clone(), game); } db } pub fn add_thumbnails(&mut self, thumbs_dir: &Path) { for game in self.games.values_mut() { let asset = thumbs_dir.join(format!("{}.jpg", game.id)); if let Ok(data) = std::fs::read(&asset) { game.thumbnail = Some(Bytes::from(data)); } else { tracing::warn!("Thumbnail missing: {}", game.id); } } } #[must_use] pub fn get_game_by_id(&self, id: S) -> Option<&Game> where S: AsRef, { self.games.get(id.as_ref()) } #[must_use] pub fn get_mut_game_by_id(&mut self, id: S) -> Option<&mut Game> where S: AsRef, { self.games.get_mut(id.as_ref()) } #[must_use] pub fn get_game_by_name(&self, name: &str) -> Option<&Game> { self.games.values().find(|game| game.name == name) } #[must_use] pub fn all_games(&self) -> Vec<&Game> { let mut games: Vec<_> = self.games.values().collect(); games.sort_by(|a, b| a.name.cmp(&b.name)); games } pub fn set_all_uninstalled(&mut self) { for game in self.games.values_mut() { game.installed = false; } } } impl Default for GameDB { fn default() -> Self { Self::empty() } } #[derive(Clone, Serialize, Deserialize)] pub struct GameFileDescription { pub game_id: String, pub relative_path: String, pub is_dir: bool, } impl GameFileDescription { #[must_use] pub fn is_version_ini(&self) -> bool { self.relative_path.ends_with("/version.ini") } } impl fmt::Debug for GameFileDescription { #[allow(clippy::cast_precision_loss)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}: [{}] path:{}", self.game_id, if self.is_dir { 'D' } else { 'F' }, self.relative_path, ) } }