From f643fdeafd36e6db67e946a7740cf2db8cafe741 Mon Sep 17 00:00:00 2001 From: Vegard Matthey Date: Thu, 23 Jan 2025 00:25:04 +0100 Subject: [PATCH] get all achievement data for games --- src/main.rs | 226 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 195 insertions(+), 31 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9a00bd5..096468e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,37 +31,11 @@ struct JsonAchievement { defaultvalue: i32, display_name: String, hidden: i32, - description: String, + description: Option, icon: String, icongray: String, } -#[tokio::main] -async fn main() { - let client = Client::new(); - let config_home = dirs::config_dir().unwrap().to_str().unwrap().to_string(); - let api_key = - fs::read_to_string(format!("{config_home}/achievement-watcher/api-key.txt")).unwrap(); - let api_key = api_key.trim(); - let wine_home = env::var("WINEPREFIX").unwrap_or(env::var("HOME").unwrap() + ".wine"); - let dirs = vec![ - wine_home.clone() + "/drive_c/users/Public/Documents/Steam/CODEX", - wine_home.clone() + "/drive_c/users/Public/Documents/Steam/RUNE", - ]; - let ini_games = get_achievement_data_all(dirs); - println!("ini games: {:#?}", ini_games); - let game_data = get_steam_data(&client, api_key, "2310", "en").await; - println!("game_data: {:#?}", game_data); -} - -async fn get_steam_data(client: &Client, key: &str, appid: &str, lang: &str) -> JsonGame { - let url = format!("https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v0002/?key={key}&appid={appid}&l={lang}&format=json"); - let json = client.get(url).send().await.unwrap().text().await.unwrap(); - // fs::write("out.json", &json).unwrap(); - // let json = fs::read_to_string("out.json").unwrap(); - serde_json::from_str(&json).unwrap() -} - #[derive(Debug)] struct IniAchievement { name: String, @@ -77,6 +51,50 @@ struct IniGame { achievements: Vec, } +#[derive(Debug)] +struct Game { + app_id: u32, + achievements: Vec, +} + +#[derive(Debug)] +struct Achievement { + name: String, + description: Option, + achieved: bool, + hidden: bool, + icon: String, + icongray: String, + current_progress: u32, + max_progress: u32, + unlock_time: u32, +} + +#[tokio::main] +async fn main() { + let client = Client::new(); + let config_dir = dirs::config_dir().unwrap().to_str().unwrap().to_owned(); + let cache_dir = dirs::cache_dir().unwrap().to_str().unwrap().to_owned(); + let game_name_cache_path = format!("{cache_dir}/achievement-watcher/game_names.txt"); + let game_name_list = get_game_name_file_cache(&game_name_cache_path); + let api_key = + fs::read_to_string(format!("{config_dir}/achievement-watcher/api-key.txt")).unwrap(); + let api_key = api_key.trim(); + let wine_dir = env::var("WINEPREFIX").unwrap_or(env::var("HOME").unwrap() + ".wine"); + let dirs = vec![ + wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/CODEX", + wine_dir.clone() + "/drive_c/users/Public/Documents/Steam/RUNE", + ]; + let games = get_achievement_data_all( + &client, + api_key, + &game_name_cache_path, + &game_name_list, + dirs, + ).await; + println!("games: {:#?}", games); +} + impl IniGame { fn new(id: u32) -> Self { Self { @@ -98,7 +116,67 @@ impl IniAchievement { } } -fn get_achievement_data_all(dirs: Vec) -> Vec { +async fn get_steam_data(client: &Client, key: &str, appid: u32, lang: &str) -> JsonGame { + let url = format!("https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v0002/?key={key}&appid={appid}&l={lang}&format=json"); + let json = client.get(url).send().await.unwrap().text().await.unwrap(); + serde_json::from_str(&json).unwrap() +} + +async fn get_achievement_data_all( + client: &Client, + api_key: &str, + game_name_cache_path: &str, + game_name_list: &Vec, + dirs: Vec, +) -> Vec { + let mut games: Vec = Vec::new(); + let ini_games = get_achievement_data_all_ini(dirs); + for ini_game in ini_games { + let mut game = Game { + app_id: ini_game.id, + achievements: Vec::new(), + }; + let game_data = get_steam_data(&client, api_key, ini_game.id, "en").await; + println!( + "game: \"{}\", id: {}, total achievements: {}/{}", + get_game_name(&client, &game_name_cache_path, &game_name_list, ini_game.id) + .await + .unwrap(), + ini_game.id, + ini_game.achievements.len(), + game_data.game.available_game_stats.achievements.len(), + ); + for ini_achievement in ini_game.achievements { + if ini_achievement.achieved { + for json_achievement in &game_data.game.available_game_stats.achievements { + if json_achievement.name == ini_achievement.name { + game.achievements.push(Achievement { + achieved: ini_achievement.achieved, + max_progress: ini_achievement.max_progress, + current_progress: ini_achievement.current_progress, + description: json_achievement.description.clone(), + name: json_achievement.display_name.clone(), + hidden: if json_achievement.hidden == 1 { + true + } else if json_achievement.hidden == 0 { + false + } else { + panic!("Unexpected hidden value") + }, + icon: json_achievement.icon.clone(), + icongray: json_achievement.icongray.clone(), + unlock_time: ini_achievement.unlock_time, + }); + } + } + } + } + games.push(game); + } + games +} + +fn get_achievement_data_all_ini(dirs: Vec) -> Vec { let mut ini_games = Vec::new(); for dir in dirs { let game_dirs = fs::read_dir(dir).unwrap(); @@ -109,18 +187,18 @@ fn get_achievement_data_all(dirs: Vec) -> Vec { .file_name() .to_str() .unwrap() - .to_string() + .to_owned() .parse() .unwrap(), ); - let game_dir_path = game_dir.path().to_str().unwrap().to_string(); + let game_dir_path = game_dir.path().to_str().unwrap().to_owned(); let i = Ini::load_from_file(format!("{game_dir_path}/achievements.ini",)).unwrap(); for (sec, prop) in i.iter() { if let Some(section) = sec { if section == "SteamAchievements" { continue; } - let mut ini_achievement = IniAchievement::new(section.to_string()); + let mut ini_achievement = IniAchievement::new(section.to_owned()); for (k, v) in prop.iter() { match k { "Achieved" => ini_achievement.achieved = v.parse::().unwrap() == 1, @@ -138,3 +216,89 @@ fn get_achievement_data_all(dirs: Vec) -> Vec { } ini_games } + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct AppList { + applist: Apps, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Apps { + apps: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct App { + name: String, + appid: u32, +} + +async fn get_game_name( + client: &Client, + path: &str, + game_name_list: &Vec, + appid: u32, +) -> Option { + use std::fs::OpenOptions; + use std::io::prelude::*; + for app in game_name_list { + if app.appid == appid { + return Some(app.name.clone()); + } + } + let json = client + .get("https://api.steampowered.com/ISteamApps/GetAppList/v0002/?format=json") + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + let applist: AppList = serde_json::from_str(&json).unwrap(); + let applist = applist.applist.apps; + for app in applist { + if app.appid == appid { + let mut file = OpenOptions::new() + .write(true) + .append(true) + .open(path) + .unwrap(); + writeln!(file, "{}:{}", app.appid, app.name).unwrap(); + return Some(app.name); + } + } + None +} + +fn get_game_name_file_cache(path: &str) -> Vec { + std::fs::read_to_string(path) + .unwrap() + .lines() + .map(|m| { + let a = m.split_once(":").unwrap(); + App { + appid: a.0.parse().unwrap(), + name: a.1.to_owned(), + } + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn check_id_to_name() { + let client = Client::new(); + let config_dir = dirs::config_dir().unwrap().to_str().unwrap().to_owned(); + let api_key = + fs::read_to_string(format!("{config_dir}/achievement-watcher/api-key.txt")).unwrap(); + let json_game = get_steam_data(&client, &api_key, 570940, "en").await; + println!("game name: {}", json_game.game.game_name); + assert!(json_game.game.game_name.to_lowercase() == "Dark Souls Remastered"); + } +}