inital commit
This commit is contained in:
commit
ebcffcc9e3
9 changed files with 279 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
/target
|
||||||
|
/data
|
||||||
|
|
||||||
|
Cargo.lock
|
||||||
9
Cargo.toml
Normal file
9
Cargo.toml
Normal 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
126
src/importer.rs
Normal 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
120
src/main.rs
Normal 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
1
src/statgen.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod playcount;
|
||||||
15
src/statgen/playcount.rs
Normal file
15
src/statgen/playcount.rs
Normal 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
2
src/statgen/report.rs
Normal 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
0
src/statgen/wincount.rs
Normal file
0
src/statgen/winratio.rs
Normal file
0
src/statgen/winratio.rs
Normal file
Loading…
Reference in a new issue