use ira_models::{AchievementStatus, AssetType, Game}; use serde::Deserialize; use std::collections::HashMap; #[derive(Deserialize)] struct AppDetailsName { #[serde(default)] name: String, } pub fn read_app_name(save_dir: &str, app_id: &str) -> Option { let path = super::paths::data_dir(save_dir, app_id).join("appdetails.json"); let data = std::fs::read(&path).ok()?; let details: AppDetailsName = serde_json::from_slice(&data).ok()?; let name = details.name.trim().to_string(); if name.is_empty() { None } else { Some(name) } } pub fn populate_image_paths(image_dir: &std::path::Path, game: &mut Game) { let db_id = game.db_id; let _s = tracing::info_span!("populate_image_paths", db_id).entered(); let mut files: std::collections::HashSet = std::fs::read_dir(image_dir) .map(|d| { d.filter_map(|e| e.ok()) .filter_map(|e| e.file_name().to_str().map(String::from)) .collect() }) .unwrap_or_default(); for &at in AssetType::all() { let base = at.file_base(); let (max_w, max_h) = at.thumb_dims(); let small_webp = format!("{}_small.webp", base); let small_jpg = format!("{}_small.jpg", base); if !files.contains(&small_webp) && !files.contains(&small_jpg) { super::paths::ensure_small_image(image_dir, base, max_w, max_h); if image_dir.join(&small_webp).is_file() { files.insert(small_webp); } if image_dir.join(&small_jpg).is_file() { files.insert(small_jpg); } } } let find = |base: &str| -> Option { for ext in &["webp", "jpg"] { let name = format!("{}.{}", base, ext); if files.contains(&name) { return Some(image_dir.join(name)); } } None }; let found: Vec<(AssetType, String)> = AssetType::all() .iter() .filter_map(|&at| { find(&format!("{}_small", at.file_base())) .or_else(|| find(at.file_base())) .map(|p| (at, p.to_string_lossy().into_owned())) }) .collect(); for (at, p) in found { *game.asset_path_mut(at) = p; } } pub fn set_achievement_earned( save_dir: &str, trophy_source: ira_models::TrophySource, app_id: &str, native_id: &str, ach_name: &str, earned: bool, ) -> Result<(), String> { let status_path = super::paths::unlock_status_path(save_dir, trophy_source, app_id, native_id); // A failed read or a corrupt file must refuse the write — otherwise // the map starts empty and the write below erases every recorded // unlock the moment one new achievement is recorded. let mut status_map: HashMap = match std::fs::read(&status_path) { Ok(data) => serde_json::from_slice(&data) .map_err(|e| format!("corrupt unlock status {}: {e}", status_path.display()))?, Err(e) if e.kind() == std::io::ErrorKind::NotFound => HashMap::new(), Err(e) => return Err(format!("read {}: {e}", status_path.display())), }; status_map.insert( ach_name.to_string(), AchievementStatus { earned, earned_time: 0, }, ); let b = serde_json::to_string_pretty(&status_map).map_err(|e| e.to_string())?; std::fs::write(&status_path, b).map_err(|e| e.to_string())?; Ok(()) }