diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c6be91b --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +testfiles/* filter=lfs diff=lfs merge=lfs -text diff --git a/Cargo.lock b/Cargo.lock index f07fbfc..52fa82d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,14 @@ dependencies = [ "memchr", ] +[[package]] +name = "analysis" +version = "0.1.0" +dependencies = [ + "csdemo", + "pretty_assertions", +] + [[package]] name = "anstream" version = "0.6.15" @@ -147,9 +155,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec" dependencies = [ "async-trait", "axum-core", @@ -174,7 +182,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -182,9 +190,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00" dependencies = [ "async-trait", "bytes", @@ -195,7 +203,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -205,6 +213,7 @@ dependencies = [ name = "backend" version = "0.1.0" dependencies = [ + "analysis", "async-trait", "axum", "clap", @@ -295,9 +304,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "camino" @@ -307,9 +316,9 @@ checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" [[package]] name = "cc" -version = "1.1.20" +version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bcde016d64c21da4be18b655631e5ab6d3107607e71a73a9f53eb48aae23fb" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" dependencies = [ "shlex", ] @@ -349,9 +358,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -359,9 +368,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -371,9 +380,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", @@ -504,13 +513,14 @@ dependencies = [ [[package]] name = "csdemo" version = "0.1.0" -source = "git+https://github.com/Lol3rrr/csdemo.git#c5237af33bc892437cf7b8658de34d7dad8947a0" +source = "git+https://github.com/Lol3rrr/csdemo.git#a2b3ee18452145e42da2667321bf33752ad31ba2" dependencies = [ "bitter", "phf", "prost", "prost-build", "prost-types", + "regex", "snap", ] @@ -655,6 +665,12 @@ dependencies = [ "syn", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -1225,7 +1241,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -1965,6 +1981,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.22" @@ -2065,9 +2091,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" dependencies = [ "bytes", "prost-derive", @@ -2075,9 +2101,9 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" +checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" dependencies = [ "bytes", "heck", @@ -2096,9 +2122,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", "itertools 0.13.0", @@ -2109,9 +2135,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" +checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670" dependencies = [ "prost", ] @@ -2471,9 +2497,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -2875,18 +2901,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -3083,6 +3109,21 @@ dependencies = [ "tokio", "tower-layer", "tower-service", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", "tracing", ] @@ -3304,9 +3345,9 @@ checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -3325,9 +3366,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" diff --git a/Cargo.toml b/Cargo.toml index ce18064..e023d75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["backend", "common", "frontend"] +members = [ "analysis","backend", "common", "frontend"] resolver = "2" diff --git a/analysis/Cargo.toml b/analysis/Cargo.toml new file mode 100644 index 0000000..6d7881e --- /dev/null +++ b/analysis/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "analysis" +version = "0.1.0" +edition = "2021" + +[dependencies] +csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" } + +[dev-dependencies] +pretty_assertions = { version = "1.4" } diff --git a/analysis/src/endofgame.rs b/analysis/src/endofgame.rs new file mode 100644 index 0000000..af590e9 --- /dev/null +++ b/analysis/src/endofgame.rs @@ -0,0 +1,179 @@ +#[derive(Debug, PartialEq)] +pub struct EndOfGame { + pub map: String, + pub players: Vec<(PlayerInfo, PlayerStats)>, +} + +#[derive(Debug, PartialEq)] +pub struct PlayerInfo { + pub name: String, + pub steam_id: String, + pub team: i32, + pub color: i32, + pub ingame_id: i32, +} + +#[derive(Debug, Default, PartialEq)] +pub struct PlayerStats { + pub kills: usize, + pub deaths: usize, + pub damage: usize, + pub team_damage: usize, + pub self_damage: usize, + pub assists: usize, +} + +pub fn parse(buf: &[u8]) -> Result { + let tmp = csdemo::Container::parse(buf).map_err(|e| ())?; + let output = csdemo::parser::parse( + csdemo::FrameIterator::parse(tmp.inner), + csdemo::parser::EntityFilter::all(), + ) + .map_err(|e| ())?; + + let header = &output.header; + + let mut player_stats = std::collections::HashMap::<_, PlayerStats>::new(); + + let mut track = false; + let mut player_life = std::collections::HashMap::<_, u8>::new(); + for event in output.events.iter() { + match event { + csdemo::DemoEvent::GameEvent(gevent) => { + match gevent.as_ref() { + csdemo::game_event::GameEvent::RoundAnnounceMatchStart(_) => { + player_stats.clear(); + track = true; + } + csdemo::game_event::GameEvent::RoundPreStart(_) => { + track = true; + } + csdemo::game_event::GameEvent::PlayerSpawn(pspawn) => { + player_life.insert(pspawn.userid.unwrap(), 100); + } + csdemo::game_event::GameEvent::WinPanelMatch(_) => { + track = false; + } + csdemo::game_event::GameEvent::RoundOfficiallyEnded(_) => { + track = false; + } + csdemo::game_event::GameEvent::PlayerDeath(pdeath) if track => { + player_death(pdeath, &output.player_info, &mut player_stats); + } + csdemo::game_event::GameEvent::PlayerHurt(phurt) if track => { + player_hurt( + phurt, + &output.player_info, + &mut player_stats, + &mut player_life, + ); + } + other => {} + }; + } + _ => {} + }; + } + + let mut players: Vec<_> = player_stats + .into_iter() + .filter_map(|(id, stats)| { + let player = output.player_info.get(&id)?; + + Some(( + PlayerInfo { + name: player.name.clone(), + steam_id: player.xuid.to_string(), + team: player.team, + color: player.color, + ingame_id: id.0, + }, + stats, + )) + }) + .collect(); + players.sort_unstable_by_key(|(p, _)| p.ingame_id); + + let map = header.map_name().to_owned(); + + Ok(EndOfGame { map, players }) +} + +fn player_death( + death: &csdemo::game_event::PlayerDeath, + player_info: &std::collections::HashMap, + player_stats: &mut std::collections::HashMap, +) { + let player_died_id = death.userid.unwrap(); + + let player_died_player = player_info.get(&player_died_id).unwrap(); + let player_died = player_stats.entry(player_died_id).or_default(); + player_died.deaths += 1; + + if let Some(attacker_id) = death.attacker.filter(|p| p.0 < 10) { + let attacker_player = player_info + .get(&attacker_id) + .expect(&format!("Attacker-ID: {:?}", attacker_id)); + if attacker_player.team == player_died_player.team { + return; + } else { + let attacker = player_stats.entry(attacker_id).or_default(); + attacker.kills += 1; + } + } + if let Some(assist_id) = death.assister.filter(|p| p.0 < 10) { + let assister_player = player_info + .get(&assist_id) + .expect(&format!("Assister-ID: {:?}", assist_id)); + + if assister_player.team == player_died_player.team { + } else { + let assister = player_stats.entry(assist_id).or_default(); + assister.assists += 1; + } + } +} + +fn player_hurt( + hurt: &csdemo::game_event::PlayerHurt, + player_info: &std::collections::HashMap, + player_stats: &mut std::collections::HashMap, + player_life: &mut std::collections::HashMap, +) { + let attacked_player = match player_info.get(hurt.userid.as_ref().unwrap()) { + Some(a) => a, + None => return, + }; + + let attacker_id = match hurt.attacker { + Some(aid) => aid, + None => return, + }; + + let attacking_player = match player_info.get(&attacker_id) { + Some(a) => a, + None => return, + }; + + let attacker = player_stats.entry(attacker_id).or_default(); + + let n_health = match hurt.health { + Some(csdemo::RawValue::F32(v)) => v as u8, + Some(csdemo::RawValue::I32(v)) => v as u8, + Some(csdemo::RawValue::U64(v)) => v as u8, + _ => 0, + }; + let dmg_dealt = player_life + .get(hurt.userid.as_ref().unwrap()) + .copied() + .unwrap_or(100) + - n_health; + + player_life.insert(hurt.userid.unwrap(), n_health); + + if attacking_player.team == attacked_player.team { + return; + } + + attacker.damage += dmg_dealt as usize; +} diff --git a/analysis/src/lib.rs b/analysis/src/lib.rs new file mode 100644 index 0000000..3743054 --- /dev/null +++ b/analysis/src/lib.rs @@ -0,0 +1 @@ +pub mod endofgame; diff --git a/analysis/tests/endofgame.rs b/analysis/tests/endofgame.rs new file mode 100644 index 0000000..04098e9 --- /dev/null +++ b/analysis/tests/endofgame.rs @@ -0,0 +1,190 @@ +use analysis::endofgame; +use pretty_assertions::assert_eq; + +#[test] +fn ancient() { + let input_bytes = include_bytes!("../../testfiles/nuke.dem"); + + let result = endofgame::parse(input_bytes).unwrap(); + + let expected = endofgame::EndOfGame { + map: "de_nuke".to_owned(), + players: vec![ + ( + endofgame::PlayerInfo { + name: "Excel".to_owned(), + steam_id: "76561198236134832".to_owned(), + team: 2, + color: 0, + ingame_id: 0, + }, + endofgame::PlayerStats { + kills: 28, + deaths: 11, + damage: 2504, + team_damage: 0, + self_damage: 0, + assists: 4, + }, + ), + ( + endofgame::PlayerInfo { + name: "Der Porzellan König".to_owned(), + steam_id: "76561198301388087".to_owned(), + team: 2, + color: 2, + ingame_id: 1, + }, + endofgame::PlayerStats { + kills: 15, + deaths: 12, + damage: 1827, + team_damage: 0, + self_damage: 0, + assists: 6, + }, + ), + ( + endofgame::PlayerInfo { + name: "Crippled Hentai addict".to_owned(), + steam_id: "76561198386810758".to_owned(), + team: 2, + color: 3, + ingame_id: 2, + }, + endofgame::PlayerStats { + kills: 11, + deaths: 16, + damage: 1394, + team_damage: 0, + self_damage: 0, + assists: 5, + }, + ), + ( + endofgame::PlayerInfo { + name: "Skalla_xD".to_owned(), + steam_id: "76561199014043225".to_owned(), + team: 2, + color: 1, + ingame_id: 3, + }, + endofgame::PlayerStats { + kills: 11, + deaths: 15, + damage: 1331, + team_damage: 0, + self_damage: 0, + assists: 3, + }, + ), + ( + endofgame::PlayerInfo { + name: "xTee".to_owned(), + steam_id: "76561199132258707".to_owned(), + team: 2, + color: 4, + ingame_id: 4, + }, + endofgame::PlayerStats { + kills: 9, + deaths: 17, + damage: 1148, + team_damage: 0, + self_damage: 0, + assists: 2, + }, + ), + ( + endofgame::PlayerInfo { + name: "cute".to_owned(), + steam_id: "76561197966517722".to_owned(), + team: 3, + color: 3, + ingame_id: 5, + }, + endofgame::PlayerStats { + kills: 17, + deaths: 16, + damage: 2143, + team_damage: 0, + self_damage: 0, + assists: 7, + }, + ), + ( + endofgame::PlayerInfo { + name: "zodiac".to_owned(), + steam_id: "76561198872143644".to_owned(), + team: 3, + color: 4, + ingame_id: 6, + }, + endofgame::PlayerStats { + kills: 7, + deaths: 15, + damage: 844, + team_damage: 0, + self_damage: 0, + assists: 4, + }, + ), + ( + endofgame::PlayerInfo { + name: "IReLaX exe".to_owned(), + steam_id: "76561199077629121".to_owned(), + team: 3, + color: 2, + ingame_id: 7, + }, + endofgame::PlayerStats { + kills: 13, + deaths: 17, + damage: 1423, + team_damage: 0, + self_damage: 0, + assists: 6, + }, + ), + ( + endofgame::PlayerInfo { + name: "Haze".to_owned(), + steam_id: "76561198375555469".to_owned(), + team: 3, + color: 0, + ingame_id: 8, + }, + endofgame::PlayerStats { + kills: 19, + deaths: 15, + damage: 1512, + team_damage: 0, + self_damage: 0, + assists: 3, + }, + ), + ( + endofgame::PlayerInfo { + name: "Know_Name".to_owned(), + steam_id: "76561198119236104".to_owned(), + team: 3, + color: 1, + ingame_id: 9, + }, + endofgame::PlayerStats { + kills: 14, + deaths: 16, + damage: 1431, + team_damage: 0, + self_damage: 0, + assists: 4, + }, + ), + ], + }; + + // TODO + // Add stats for rest of players + + assert_eq!(result, expected); +} diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7867851..f31adfa 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -24,6 +24,7 @@ diesel_async_migrations = { version = "0.15" } reqwest = { version = "0.12", features = ["json"] } common = { path = "../common/" } +analysis = { path = "../analysis/" } csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" } memmap2 = { version = "0.9" } diff --git a/backend/src/analysis.rs b/backend/src/analysis.rs index c7cc15a..9a36fb5 100644 --- a/backend/src/analysis.rs +++ b/backend/src/analysis.rs @@ -3,27 +3,51 @@ use std::path::PathBuf; use diesel::prelude::*; use diesel_async::RunQueryDsl; -pub async fn poll_next_task(upload_folder: &std::path::Path, db_con: &mut diesel_async::pg::AsyncPgConnection) -> Result { - let query = crate::schema::analysis_queue::dsl::analysis_queue.order(crate::schema::analysis_queue::dsl::created_at.asc()).limit(1).select(crate::models::AnalysisTask::as_select()).for_update().skip_locked(); +pub async fn poll_next_task( + upload_folder: &std::path::Path, + db_con: &mut diesel_async::pg::AsyncPgConnection, +) -> Result { + let query = crate::schema::analysis_queue::dsl::analysis_queue + .order(crate::schema::analysis_queue::dsl::created_at.asc()) + .limit(1) + .select(crate::models::AnalysisTask::as_select()) + .for_update() + .skip_locked(); loop { - let result = db_con.build_transaction().run::<'_, _, diesel::result::Error, _>(|conn| Box::pin(async move { - let mut results: Vec = query.load(conn).await?; - let final_result = match results.pop() { - Some(r) => r, - None => return Ok(None), - }; + let result = db_con + .build_transaction() + .run::<'_, _, diesel::result::Error, _>(|conn| { + Box::pin(async move { + let mut results: Vec = query.load(conn).await?; + let final_result = match results.pop() { + Some(r) => r, + None => return Ok(None), + }; - let delete_query = diesel::dsl::delete(crate::schema::analysis_queue::dsl::analysis_queue).filter(crate::schema::analysis_queue::dsl::demo_id.eq(final_result.demo_id)).filter(crate::schema::analysis_queue::dsl::steam_id.eq(final_result.steam_id.clone())); - delete_query.execute(conn).await?; + let delete_query = + diesel::dsl::delete(crate::schema::analysis_queue::dsl::analysis_queue) + .filter( + crate::schema::analysis_queue::dsl::demo_id + .eq(final_result.demo_id), + ) + .filter( + crate::schema::analysis_queue::dsl::steam_id + .eq(final_result.steam_id.clone()), + ); + delete_query.execute(conn).await?; - Ok(Some(final_result)) - })).await; + Ok(Some(final_result)) + }) + }) + .await; match result { Ok(Some(r)) => { return Ok(AnalysisInput { - path: upload_folder.join(&r.steam_id).join(format!("{}.dem", r.demo_id)), + path: upload_folder + .join(&r.steam_id) + .join(format!("{}.dem", r.demo_id)), steamid: r.steam_id, demoid: r.demo_id, }); @@ -65,84 +89,34 @@ pub struct BasePlayerInfo { pub struct BasePlayerStats { pub kills: usize, pub deaths: usize, + pub damage: usize, + pub assists: usize, } #[tracing::instrument(skip(input))] pub fn analyse_base(input: AnalysisInput) -> BaseInfo { - tracing::info!("Performing Base analysis"); + tracing::info!("Performing Base analysis"); let file = std::fs::File::open(&input.path).unwrap(); let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() }; - let tmp = csdemo::Container::parse(&mmap).unwrap(); - let output = csdemo::parser::parse(csdemo::FrameIterator::parse(tmp.inner)).unwrap(); - - let header = &output.header; - - tracing::info!("Header: {:?}", header); - - let mut player_stats = std::collections::HashMap::with_capacity(output.player_info.len()); - - for event in output.events.iter() { - match event { - csdemo::DemoEvent::Tick(tick) => {} - csdemo::DemoEvent::ServerInfo(info) => {} - csdemo::DemoEvent::RankUpdate(update) => {} - csdemo::DemoEvent::RankReveal(reveal) => {} - csdemo::DemoEvent::GameEvent(gevent) => { - match gevent { - csdemo::game_event::GameEvent::BeginNewMatch(_) => { - player_stats.clear(); - } - csdemo::game_event::GameEvent::PlayerTeam(pteam) => { - // tracing::info!("{:?}", pteam); - } - csdemo::game_event::GameEvent::RoundOfficiallyEnded(r_end) => { - // tracing::info!("{:?}", r_end); - } - csdemo::game_event::GameEvent::PlayerDeath(pdeath) => { - // tracing::info!("{:?}", pdeath); - - let player_died_id = pdeath.userid.unwrap(); - - let player_died = player_stats.entry(player_died_id).or_insert(BasePlayerStats { - kills: 0, - deaths: 0, - }); - player_died.deaths += 1; - - if let Some(attacker_id) = pdeath.attacker { - let attacker = player_stats.entry(attacker_id).or_insert(BasePlayerStats { - kills: 0, - deaths: 0, - }); - attacker.kills += 1; - - // tracing::trace!("{:?} killed {:?}", attacker_id, player_died_id); - } - } - other => {} - }; - } - }; - } - - let players: Vec<_> = player_stats.into_iter().filter_map(|(id, stats)| { - let player = output.player_info.get(&id)?; - - Some((BasePlayerInfo { - name: player.name.clone(), - steam_id: player.xuid.to_string(), - team: player.team, - color: player.color, - ingame_id: id.0, - }, stats)) - }).collect(); - - let map = header.map_name().to_owned(); + let result = analysis::endofgame::parse(&mmap).unwrap(); BaseInfo { - map, - players, + map: result.map, + players: result.players.into_iter().map(|(info, stats)| { + (BasePlayerInfo { + name: info.name, + steam_id: info.steam_id, + team: info.team, + ingame_id: info.ingame_id, + color: info.color, + }, BasePlayerStats { + kills: stats.kills, + assists: stats.assists, + damage: stats.damage, + deaths: stats.deaths, + }) + }).collect() } } diff --git a/backend/src/api.rs b/backend/src/api.rs index b8d57c0..122d477 100644 --- a/backend/src/api.rs +++ b/backend/src/api.rs @@ -154,9 +154,7 @@ pub struct RouterConfig { pub upload_dir: std::path::PathBuf, } -pub fn router( - config: RouterConfig, -) -> axum::Router { +pub fn router(config: RouterConfig) -> axum::Router { axum::Router::new() .nest( "/steam/", diff --git a/backend/src/api/demos.rs b/backend/src/api/demos.rs index 5610a3c..18df9e8 100644 --- a/backend/src/api/demos.rs +++ b/backend/src/api/demos.rs @@ -8,9 +8,7 @@ struct DemoState { upload_folder: std::path::PathBuf, } -pub fn router

( - upload_folder: P, -) -> axum::Router +pub fn router

(upload_folder: P) -> axum::Router where P: Into, { @@ -102,10 +100,11 @@ async fn upload( }); query.execute(&mut db_con).await.unwrap(); - let queue_query = diesel::dsl::insert_into(crate::schema::analysis_queue::dsl::analysis_queue).values(crate::models::AddAnalysisTask { - demo_id, - steam_id: steam_id.to_string(), - }); + let queue_query = diesel::dsl::insert_into(crate::schema::analysis_queue::dsl::analysis_queue) + .values(crate::models::AddAnalysisTask { + demo_id, + steam_id: steam_id.to_string(), + }); queue_query.execute(&mut db_con).await.unwrap(); let processing_query = @@ -145,10 +144,11 @@ async fn analyise( )); } - let queue_query = diesel::dsl::insert_into(crate::schema::analysis_queue::dsl::analysis_queue).values(crate::models::AddAnalysisTask { - demo_id, - steam_id: steam_id.to_string(), - }); + let queue_query = diesel::dsl::insert_into(crate::schema::analysis_queue::dsl::analysis_queue) + .values(crate::models::AddAnalysisTask { + demo_id, + steam_id: steam_id.to_string(), + }); queue_query.execute(&mut db_con).await.unwrap(); Ok(()) @@ -181,20 +181,33 @@ async fn info( } #[tracing::instrument(skip(session))] -async fn scoreboard(session: UserSession, Path(demo_id): Path) -> Result, axum::http::StatusCode> { +async fn scoreboard( + session: UserSession, + Path(demo_id): Path, +) -> Result, axum::http::StatusCode> { let query = crate::schema::demo_players::dsl::demo_players - .inner_join(crate::schema::demo_player_stats::dsl::demo_player_stats.on(crate::schema::demo_players::dsl::demo_id.eq(crate::schema::demo_player_stats::dsl::demo_id).and(crate::schema::demo_players::dsl::steam_id.eq(crate::schema::demo_player_stats::dsl::steam_id)))) + .inner_join( + crate::schema::demo_player_stats::dsl::demo_player_stats.on( + crate::schema::demo_players::dsl::demo_id + .eq(crate::schema::demo_player_stats::dsl::demo_id) + .and( + crate::schema::demo_players::dsl::steam_id + .eq(crate::schema::demo_player_stats::dsl::steam_id), + ), + ), + ) .filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id)); let mut db_con = crate::db_connection().await; - - let response: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerStats)> = match query.load(&mut db_con).await { - Ok(d) => d, - Err(e) => { - tracing::error!("Querying DB: {:?}", e); - return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR); - } - }; + + let response: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerStats)> = + match query.load(&mut db_con).await { + Ok(d) => d, + Err(e) => { + tracing::error!("Querying DB: {:?}", e); + return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR); + } + }; if response.is_empty() { tracing::error!("DB Response was empty"); @@ -206,12 +219,18 @@ async fn scoreboard(session: UserSession, Path(demo_id): Path) -> Result -) { +pub async fn run_analysis(upload_folder: impl Into) { use diesel::prelude::*; use diesel_async::{AsyncConnection, RunQueryDsl}; let upload_folder: std::path::PathBuf = upload_folder.into(); - loop { let mut db_con = db_connection().await; let input = match crate::analysis::poll_next_task(&upload_folder, &mut db_con).await { @@ -114,38 +109,70 @@ pub async fn run_analysis( let mut db_con = crate::db_connection().await; - let (player_info, player_stats): (Vec<_>, Vec<_>) = result.players.into_iter().map(|(info, stats)| { - (crate::models::DemoPlayer { - demo_id, - name: info.name, - steam_id: info.steam_id.clone(), - team: info.team as i16, - color: info.color as i16, - }, crate::models::DemoPlayerStats { - demo_id, - steam_id: info.steam_id, - deaths: stats.deaths as i16, - kills: stats.kills as i16, - }) - }).unzip(); + let (player_info, player_stats): (Vec<_>, Vec<_>) = result + .players + .into_iter() + .map(|(info, stats)| { + ( + crate::models::DemoPlayer { + demo_id, + name: info.name, + steam_id: info.steam_id.clone(), + team: info.team as i16, + color: info.color as i16, + }, + crate::models::DemoPlayerStats { + demo_id, + steam_id: info.steam_id, + deaths: stats.deaths as i16, + kills: stats.kills as i16, + damage: stats.damage as i16, + assists: stats.assists as i16, + }, + ) + }) + .unzip(); let demo_info = crate::models::DemoInfo { - demo_id, - map: result.map, - }; + demo_id, + map: result.map, + }; - let store_demo_info_query = diesel::dsl::insert_into(crate::schema::demo_info::dsl::demo_info) - .values(&demo_info).on_conflict(crate::schema::demo_info::dsl::demo_id).do_update().set(crate::schema::demo_info::dsl::map.eq(diesel::upsert::excluded(crate::schema::demo_info::dsl::map))); - let store_demo_players_query = diesel::dsl::insert_into(crate::schema::demo_players::dsl::demo_players).values(player_info) - .on_conflict_do_nothing(); - let store_demo_player_stats_query = diesel::dsl::insert_into(crate::schema::demo_player_stats::dsl::demo_player_stats) - .values(player_stats) - .on_conflict((crate::schema::demo_player_stats::dsl::demo_id, crate::schema::demo_player_stats::dsl::steam_id)) - .do_update() - .set(( - crate::schema::demo_player_stats::dsl::deaths.eq(diesel::upsert::excluded(crate::schema::demo_player_stats::dsl::deaths)), - crate::schema::demo_player_stats::dsl::kills.eq(diesel::upsert::excluded(crate::schema::demo_player_stats::dsl::kills)), - )); + let store_demo_info_query = + diesel::dsl::insert_into(crate::schema::demo_info::dsl::demo_info) + .values(&demo_info) + .on_conflict(crate::schema::demo_info::dsl::demo_id) + .do_update() + .set( + crate::schema::demo_info::dsl::map + .eq(diesel::upsert::excluded(crate::schema::demo_info::dsl::map)), + ); + let store_demo_players_query = + diesel::dsl::insert_into(crate::schema::demo_players::dsl::demo_players) + .values(player_info) + .on_conflict_do_nothing(); + let store_demo_player_stats_query = + diesel::dsl::insert_into(crate::schema::demo_player_stats::dsl::demo_player_stats) + .values(player_stats) + .on_conflict(( + crate::schema::demo_player_stats::dsl::demo_id, + crate::schema::demo_player_stats::dsl::steam_id, + )) + .do_update() + .set(( + crate::schema::demo_player_stats::dsl::deaths.eq(diesel::upsert::excluded( + crate::schema::demo_player_stats::dsl::deaths, + )), + crate::schema::demo_player_stats::dsl::kills.eq(diesel::upsert::excluded( + crate::schema::demo_player_stats::dsl::kills, + )), + crate::schema::demo_player_stats::dsl::assists.eq(diesel::upsert::excluded( + crate::schema::demo_player_stats::dsl::assists, + )), + crate::schema::demo_player_stats::dsl::damage.eq(diesel::upsert::excluded( + crate::schema::demo_player_stats::dsl::damage, + )), + )); let update_process_info = diesel::dsl::update(crate::schema::processing_status::dsl::processing_status) .set(crate::schema::processing_status::dsl::info.eq(1)) diff --git a/backend/src/main.rs b/backend/src/main.rs index 43906af..f681631 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -49,10 +49,7 @@ async fn main() { } }; - component_set.spawn(backend::run_api( - args.upload_folder.clone(), - steam_api_key, - )); + component_set.spawn(backend::run_api(args.upload_folder.clone(), steam_api_key)); } if args.analysis { component_set.spawn(backend::run_analysis(args.upload_folder.clone())); diff --git a/backend/src/models.rs b/backend/src/models.rs index 553841f..48b2006 100644 --- a/backend/src/models.rs +++ b/backend/src/models.rs @@ -52,6 +52,8 @@ pub struct DemoPlayerStats { pub steam_id: String, pub kills: i16, pub deaths: i16, + pub damage: i16, + pub assists: i16, } #[derive(Queryable, Selectable, Insertable, Debug)] diff --git a/backend/src/schema.rs b/backend/src/schema.rs index 2216239..572b476 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs @@ -21,6 +21,8 @@ diesel::table! { steam_id -> Text, kills -> Int2, deaths -> Int2, + damage -> Int2, + assists -> Int2, } } diff --git a/common/src/lib.rs b/common/src/lib.rs index b926088..ea4c32b 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -28,5 +28,7 @@ pub mod demo_analysis { pub name: String, pub kills: usize, pub deaths: usize, + pub damage: usize, + pub assists: usize, } } diff --git a/frontend/src/demo.rs b/frontend/src/demo.rs index 2f0fdbe..60708a6 100644 --- a/frontend/src/demo.rs +++ b/frontend/src/demo.rs @@ -17,6 +17,12 @@ pub fn demo() -> impl leptos::IntoView { }, ); + let rerun_analysis = create_action(move |_: &()| { + async move { + let _ = reqwasm::http::Request::get(&format!("/api/demos/{}/reanalyse", id())).send().await; + } + }); + let map = move || match demo_info.get() { Some(v) => v.map.clone(), None => String::new(), @@ -58,6 +64,7 @@ pub fn demo() -> impl leptos::IntoView { view! {class = style,

Demo - { id } - { map }

+
@@ -72,26 +79,34 @@ pub fn demo() -> impl leptos::IntoView { pub fn scoreboard() -> impl leptos::IntoView { use leptos::Suspense; - let scoreboard_resource = create_resource(leptos_router::use_params_map(), |params| async move { - let id = params.get("id").unwrap(); + let scoreboard_resource = + create_resource(leptos_router::use_params_map(), |params| async move { + let id = params.get("id").unwrap(); - let res = reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id)) - .send() + let res = + reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id)) + .send() + .await + .unwrap(); + res.json::() .await - .unwrap(); - res.json::().await.unwrap() - }); + .unwrap() + }); let team_display_func = |team: &[common::demo_analysis::ScoreBoardPlayer]| { - team.iter().map(|player| { - view! { - - { player.name.clone() } - { player.kills } - { player.deaths } - - } - }).collect::>() + team.iter() + .map(|player| { + view! { + + { player.name.clone() } + { player.kills } + { player.assists } + { player.deaths } + { player.damage } + + } + }) + .collect::>() }; view! { @@ -106,7 +121,9 @@ pub fn scoreboard() -> impl leptos::IntoView { Name Kills + Assists Deaths + Damage { move || { @@ -124,6 +141,10 @@ pub fn scoreboard() -> impl leptos::IntoView { + + + + { move || { diff --git a/migrations/2024-09-11-200628_demo_info/up.sql b/migrations/2024-09-11-200628_demo_info/up.sql index 8040891..d8063b3 100644 --- a/migrations/2024-09-11-200628_demo_info/up.sql +++ b/migrations/2024-09-11-200628_demo_info/up.sql @@ -23,5 +23,7 @@ CREATE TABLE IF NOT EXISTS demo_player_stats ( steam_id TEXT NOT NULL, kills int2 NOT NULL, deaths int2 NOT NULL, + damage int2 NOT NULL, + assists int2 NOT NULL, PRIMARY KEY (demo_id, steam_id) ); diff --git a/testfiles/nuke.dem b/testfiles/nuke.dem new file mode 100644 index 0000000..f927c85 --- /dev/null +++ b/testfiles/nuke.dem @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3438fb63af58e1d80026d44e8a55dbeea09f4d612744cdd60efcd11b8e4ee463 +size 351219360
NameKillsAssistsDeathsDamage