inital commit

This commit is contained in:
genki_angel 2026-01-16 08:43:53 +00:00
commit ebcffcc9e3
9 changed files with 279 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.vscode
/target
/data
Cargo.lock

9
Cargo.toml Normal file
View file

@ -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"

126
src/importer.rs Normal file
View file

@ -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<ChatMessage>,
}
#[derive(Deserialize, Debug)]
struct ChatMessage {
id: String,
timestamp: String,
author: Map<String, Value>, // 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<MapStatsEmbed>, // 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<Value>, // 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<String, HashMap<String, Stat>> {
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<String, HashMap<String, Stat>> = 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;
}

120
src/main.rs Normal file
View file

@ -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<String, Stats>,
}
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)
// }
}

1
src/statgen.rs Normal file
View file

@ -0,0 +1 @@
pub mod playcount;

15
src/statgen/playcount.rs Normal file
View file

@ -0,0 +1,15 @@
use std::collections::HashMap;
use crate::importer::Stat;
pub fn sortplaycount(map_stats: HashMap<String, HashMap<String, Stat>>) -> 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;
}

2
src/statgen/report.rs Normal file
View file

@ -0,0 +1,2 @@
//given a timeframe and will compile all data into a report
//add option for a month report or year

0
src/statgen/wincount.rs Normal file
View file

0
src/statgen/winratio.rs Normal file
View file