From ebcffcc9e3df41d3ce0ccaa5b07c4bbe1eb8422b Mon Sep 17 00:00:00 2001 From: genki_angel Date: Fri, 16 Jan 2026 08:43:53 +0000 Subject: [PATCH] inital commit --- .gitignore | 6 ++ Cargo.toml | 9 +++ src/importer.rs | 126 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 120 +++++++++++++++++++++++++++++++++++++ src/statgen.rs | 1 + src/statgen/playcount.rs | 15 +++++ src/statgen/report.rs | 2 + src/statgen/wincount.rs | 0 src/statgen/winratio.rs | 0 9 files changed, 279 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/importer.rs create mode 100644 src/main.rs create mode 100644 src/statgen.rs create mode 100644 src/statgen/playcount.rs create mode 100644 src/statgen/report.rs create mode 100644 src/statgen/wincount.rs create mode 100644 src/statgen/winratio.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..926828e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.vscode + +/target +/data + +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c53b718 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "zz-map-stats-analyser" +version = "0.1.0" +edition = "2024" + +[dependencies] +chrono = "0.4.42" +serde = {version = "1.0.228", features = ["derive"]} +serde_json = "1.0" diff --git a/src/importer.rs b/src/importer.rs new file mode 100644 index 0000000..50a9edc --- /dev/null +++ b/src/importer.rs @@ -0,0 +1,126 @@ +use std::{collections::HashMap, fs}; + +use chrono::{DateTime, Datelike}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +use crate::{MAPSTAT_AUTHOR_ID, SERVER, TARGET_YEAR}; + +//structure of the message with the data we want to extract +#[derive(Deserialize, Debug)] +struct ChatData { + messages: Vec, +} + +#[derive(Deserialize, Debug)] +struct ChatMessage { + id: String, + timestamp: String, + author: Map, // there are 2 bots, one that announces a map and the other that announce the end result. For now only mapstats is important. + // getting data from maunzbot can provide extra metrics like map time and player retention, getting those from only mapstats is too unreliable. + embeds: Vec, // all data is shown in embeds, embeds are represented as arrays but there will always only be 1 object +} + +#[derive(Deserialize, Debug)] +struct MapStatsEmbed { + title: String, // Won or Lost + fields: Vec, // Map, Server, Players, Wave +} + +#[derive(Clone, Serialize)] +pub struct Stat { + pub won: bool, + pub timestamp: String, // the time the map was declared finished + pub wave_ended: i8, + pub player_count: i32, +} + +pub fn import() -> HashMap> { + let chatdatafile = "./data/rawdata.json"; + let jsonstring = fs::read_to_string(chatdatafile).unwrap(); + let jsondata: ChatData = serde_json::from_str(&jsonstring).unwrap(); + + let mut map_stats: HashMap> = HashMap::new(); + + for message in &jsondata.messages { + if message.author["id"] == MAPSTAT_AUTHOR_ID { + let timestamp = DateTime::parse_from_rfc3339(&message.timestamp).unwrap(); + + + if timestamp.year().to_string().as_str() != TARGET_YEAR { + // println!("year is {}, skipping", timestamp.year()); + continue; + } + let mut map_name = "".to_string(); + let mut stat = Stat { + won: false, + timestamp: timestamp.to_string(), + wave_ended: 0, + player_count: 0, + }; + + let map_won = &message.embeds[0].title; + if map_won == "Won" { + stat.won = true + } else if map_won == "Lost" { + // do nothing :) + } else { + continue; + } + + let chat_embed = &message.embeds[0].fields; + for embed_value in chat_embed { + let key = embed_value["name"].as_str().unwrap(); + + match &key { + &"Server" => { + // println!("{}", embed_value["value"]); + if embed_value["value"].as_str().unwrap() != SERVER { + println!("server is: {} - skipping", embed_value["value"]); + continue; + } + } + &"Map" => { + let map_string = &embed_value["value"].to_string().replace('"', ""); + map_name = map_string.clone(); + } + &"Players" => { + let player_count = embed_value["value"].to_string().replace('"', ""); + stat.player_count = player_count.parse().unwrap(); + } + &"Wave" => { + let wave_count = &embed_value["value"].to_string().replace('"', ""); + stat.wave_ended = wave_count.parse().unwrap(); + } + + _ => {} // do nothing :3 + } + // if embed_value["name"] == "Map" { + // println!("{}", embed_value["value"]) + // } + } + // println!( + // "Map: {} - [Won?: {}, Wave ended: {}, Player count: {}] - {}", + // map_name, stat.won, stat.wave_ended, stat.player_count, stat.timestamp + // ); + + if map_name.as_str() == "" { + // println!("no mapname set, skipping stat..."); + continue; + } + + if map_stats.contains_key(&map_name) { + // println!("map exists, adding stats"); + let mut statvec = map_stats.get(&map_name).unwrap().clone(); + statvec.insert(message.id.clone(), stat); + map_stats.insert(map_name, statvec); + } else { + let mut statvec = HashMap::new(); + statvec.insert(message.id.clone(), stat); + map_stats.insert(map_name, statvec); + } + } + } + + return map_stats; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ca4ef8d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,120 @@ +use std::{ + collections::HashMap, + fs::{File, create_dir_all}, + io::Write, +}; + +use serde::Serialize; +use serde_json::Value; + +mod importer; + +// struct MapStat { +// map: String, +// wins: i64, +// lost: i64, +// players: i32, +// } + +#[derive(Serialize)] +struct Stats { + most_played: Vec<(String, usize)>, + most_players: Vec<(String, String, i32)>, + most_wins: Vec<(String, usize)>, + most_lost: Vec<(String, usize)>, + most_human_sided: Vec<(String, f32)>, +} + +#[derive(Serialize)] +struct OverallStats { + standard: Stats, + obj: Stats, + monthly: HashMap, +} + +const MAPSTAT_AUTHOR_ID: &str = "879429830628753429"; +const SERVER: &str = "wzs"; +const TARGET_YEAR: &str = "2025"; + +const RESULT_PATH: &str = "./data/results/"; + +fn main() { + //check if directory exists and creates it + create_dir_all(RESULT_PATH).expect("Failed to create directory..."); + + let map_stats = importer::import(); + + let processed_data_path = RESULT_PATH.to_owned() + "alldata.json"; + + match serde_json::to_string(&map_stats) { + Ok(json) => { + let mut file = File::create(processed_data_path).expect("Unable to write file..."); + file.write_all(json.as_bytes()) + .expect("Failed to write data..."); + } + Err(e) => println!("Serialization error: {}", e), + } + + // let most_played = sortplaycount(map_stats.clone()); + + let mut most_played = vec![]; + let mut highest_playercount = vec![]; + let mut most_wins = vec![]; + let mut most_lost = vec![]; + let mut most_human = vec![]; + + for (map, stats) in map_stats.iter() { + most_played.push((map.clone(), stats.len())); + let mut wins = vec![]; + let mut lost = vec![]; + for (id, stat) in stats.iter() { + highest_playercount.push((map.clone(), stat.timestamp.clone(), stat.player_count)); + if stat.won { + wins.push(id); + } else { + lost.push(id); + } + } + most_wins.push((map.clone(), wins.len())); + most_lost.push((map.clone(), lost.len())); + let win_ratio: f32 = wins.len() as f32 / stats.len() as f32; + most_human.push((map.clone(), win_ratio)); + } + + most_played.sort_by(|a, b| b.1.cmp(&a.1)); + highest_playercount.sort_by(|a, b| b.2.cmp(&a.2)); + most_wins.sort_by(|a, b| b.1.cmp(&a.1)); + most_lost.sort_by(|a, b| b.1.cmp(&a.1)); + most_human.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + + let report: Stats = Stats { + most_played, + most_players: highest_playercount, + most_wins: most_wins, + most_lost: most_lost, + most_human_sided: most_human, + }; + + match serde_json::to_string(&report) { + Ok(json) => { + let mut file = File::create(RESULT_PATH.to_owned() + "report.json").expect("Unable to write file..."); + file.write_all(json.as_bytes()) + .expect("Failed to write data..."); + } + Err(e) => println!("Serialization error: {}", e), + } + + // for (map, playcount) in most_played.iter() { + // println!("map: {} - played {} times", map, playcount) + // } + + // for (map, stats) in map_stats.iter() { + // for (_id, stat) in stats.iter() { + // highest_playercount.push((map, stat.timestamp.clone(), stat.player_count)); + // } + // } + + // for (map, time, playercount) in highest_playercount { + // println!("map: {} - player count: {} @ {}", map, playercount, time) + // } +} diff --git a/src/statgen.rs b/src/statgen.rs new file mode 100644 index 0000000..1999b8f --- /dev/null +++ b/src/statgen.rs @@ -0,0 +1 @@ +pub mod playcount; \ No newline at end of file diff --git a/src/statgen/playcount.rs b/src/statgen/playcount.rs new file mode 100644 index 0000000..8b31398 --- /dev/null +++ b/src/statgen/playcount.rs @@ -0,0 +1,15 @@ +use std::collections::HashMap; + +use crate::importer::Stat; + +pub fn sortplaycount(map_stats: HashMap>) -> Vec<(String, usize)> { + let mut most_played = vec![]; + + for (map, stats) in map_stats.iter() { + let play_count = (map.clone(), stats.len()); + most_played.push(play_count); + } + + most_played.sort_by(|a, b| b.1.cmp(&a.1)); + return most_played; +} \ No newline at end of file diff --git a/src/statgen/report.rs b/src/statgen/report.rs new file mode 100644 index 0000000..715ebc2 --- /dev/null +++ b/src/statgen/report.rs @@ -0,0 +1,2 @@ +//given a timeframe and will compile all data into a report +//add option for a month report or year \ No newline at end of file diff --git a/src/statgen/wincount.rs b/src/statgen/wincount.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/statgen/winratio.rs b/src/statgen/winratio.rs new file mode 100644 index 0000000..e69de29