Move analysis into own crate and some more improvements all around

This commit is contained in:
Lol3rrr
2024-09-23 17:12:52 +02:00
parent 118a25aa9a
commit 7b50da640c
19 changed files with 676 additions and 206 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
testfiles/* filter=lfs diff=lfs merge=lfs -text

113
Cargo.lock generated
View File

@@ -26,6 +26,14 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "analysis"
version = "0.1.0"
dependencies = [
"csdemo",
"pretty_assertions",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.15" version = "0.6.15"
@@ -147,9 +155,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.7.5" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
@@ -174,7 +182,7 @@ dependencies = [
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.1", "sync_wrapper 1.0.1",
"tokio", "tokio",
"tower", "tower 0.5.1",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -182,9 +190,9 @@ dependencies = [
[[package]] [[package]]
name = "axum-core" name = "axum-core"
version = "0.4.3" version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@@ -195,7 +203,7 @@ dependencies = [
"mime", "mime",
"pin-project-lite", "pin-project-lite",
"rustversion", "rustversion",
"sync_wrapper 0.1.2", "sync_wrapper 1.0.1",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -205,6 +213,7 @@ dependencies = [
name = "backend" name = "backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"analysis",
"async-trait", "async-trait",
"axum", "axum",
"clap", "clap",
@@ -295,9 +304,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.7.1" version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
[[package]] [[package]]
name = "camino" name = "camino"
@@ -307,9 +316,9 @@ checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.20" version = "1.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bcde016d64c21da4be18b655631e5ab6d3107607e71a73a9f53eb48aae23fb" checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@@ -349,9 +358,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.17" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -359,9 +368,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.17" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -371,9 +380,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.13" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -504,13 +513,14 @@ dependencies = [
[[package]] [[package]]
name = "csdemo" name = "csdemo"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Lol3rrr/csdemo.git#c5237af33bc892437cf7b8658de34d7dad8947a0" source = "git+https://github.com/Lol3rrr/csdemo.git#a2b3ee18452145e42da2667321bf33752ad31ba2"
dependencies = [ dependencies = [
"bitter", "bitter",
"phf", "phf",
"prost", "prost",
"prost-build", "prost-build",
"prost-types", "prost-types",
"regex",
"snap", "snap",
] ]
@@ -655,6 +665,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -1225,7 +1241,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio", "tokio",
"tower", "tower 0.4.13",
"tower-service", "tower-service",
"tracing", "tracing",
] ]
@@ -1965,6 +1981,16 @@ dependencies = [
"zerocopy", "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]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.22" version = "0.2.22"
@@ -2065,9 +2091,9 @@ dependencies = [
[[package]] [[package]]
name = "prost" name = "prost"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f"
dependencies = [ dependencies = [
"bytes", "bytes",
"prost-derive", "prost-derive",
@@ -2075,9 +2101,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-build" name = "prost-build"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15"
dependencies = [ dependencies = [
"bytes", "bytes",
"heck", "heck",
@@ -2096,9 +2122,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-derive" name = "prost-derive"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.13.0", "itertools 0.13.0",
@@ -2109,9 +2135,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-types" name = "prost-types"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670"
dependencies = [ dependencies = [
"prost", "prost",
] ]
@@ -2471,9 +2497,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework-sys" name = "security-framework-sys"
version = "2.11.1" version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -2875,18 +2901,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.63" version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.63" version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3083,6 +3109,21 @@ dependencies = [
"tokio", "tokio",
"tower-layer", "tower-layer",
"tower-service", "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", "tracing",
] ]
@@ -3304,9 +3345,9 @@ checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.23" version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
dependencies = [ dependencies = [
"tinyvec", "tinyvec",
] ]
@@ -3325,9 +3366,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "untrusted" name = "untrusted"

View File

@@ -1,3 +1,3 @@
[workspace] [workspace]
members = ["backend", "common", "frontend"] members = [ "analysis","backend", "common", "frontend"]
resolver = "2" resolver = "2"

10
analysis/Cargo.toml Normal file
View File

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

179
analysis/src/endofgame.rs Normal file
View File

@@ -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<EndOfGame, ()> {
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<csdemo::UserId, csdemo::parser::Player>,
player_stats: &mut std::collections::HashMap<csdemo::UserId, PlayerStats>,
) {
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<csdemo::UserId, csdemo::parser::Player>,
player_stats: &mut std::collections::HashMap<csdemo::UserId, PlayerStats>,
player_life: &mut std::collections::HashMap<csdemo::UserId, u8>,
) {
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;
}

1
analysis/src/lib.rs Normal file
View File

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

190
analysis/tests/endofgame.rs Normal file
View File

@@ -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);
}

View File

@@ -24,6 +24,7 @@ diesel_async_migrations = { version = "0.15" }
reqwest = { version = "0.12", features = ["json"] } reqwest = { version = "0.12", features = ["json"] }
common = { path = "../common/" } common = { path = "../common/" }
analysis = { path = "../analysis/" }
csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" } csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" }
memmap2 = { version = "0.9" } memmap2 = { version = "0.9" }

View File

@@ -3,27 +3,51 @@ use std::path::PathBuf;
use diesel::prelude::*; use diesel::prelude::*;
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
pub async fn poll_next_task(upload_folder: &std::path::Path, db_con: &mut diesel_async::pg::AsyncPgConnection) -> Result<AnalysisInput, ()> { pub async fn poll_next_task(
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(); upload_folder: &std::path::Path,
db_con: &mut diesel_async::pg::AsyncPgConnection,
) -> Result<AnalysisInput, ()> {
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 { loop {
let result = db_con.build_transaction().run::<'_, _, diesel::result::Error, _>(|conn| Box::pin(async move { let result = db_con
.build_transaction()
.run::<'_, _, diesel::result::Error, _>(|conn| {
Box::pin(async move {
let mut results: Vec<crate::models::AnalysisTask> = query.load(conn).await?; let mut results: Vec<crate::models::AnalysisTask> = query.load(conn).await?;
let final_result = match results.pop() { let final_result = match results.pop() {
Some(r) => r, Some(r) => r,
None => return Ok(None), 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())); 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?; delete_query.execute(conn).await?;
Ok(Some(final_result)) Ok(Some(final_result))
})).await; })
})
.await;
match result { match result {
Ok(Some(r)) => { Ok(Some(r)) => {
return Ok(AnalysisInput { 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, steamid: r.steam_id,
demoid: r.demo_id, demoid: r.demo_id,
}); });
@@ -65,6 +89,8 @@ pub struct BasePlayerInfo {
pub struct BasePlayerStats { pub struct BasePlayerStats {
pub kills: usize, pub kills: usize,
pub deaths: usize, pub deaths: usize,
pub damage: usize,
pub assists: usize,
} }
#[tracing::instrument(skip(input))] #[tracing::instrument(skip(input))]
@@ -74,75 +100,23 @@ pub fn analyse_base(input: AnalysisInput) -> BaseInfo {
let file = std::fs::File::open(&input.path).unwrap(); let file = std::fs::File::open(&input.path).unwrap();
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() }; let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
let tmp = csdemo::Container::parse(&mmap).unwrap(); let result = analysis::endofgame::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();
BaseInfo { BaseInfo {
map, map: result.map,
players, 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()
} }
} }

View File

@@ -154,9 +154,7 @@ pub struct RouterConfig {
pub upload_dir: std::path::PathBuf, pub upload_dir: std::path::PathBuf,
} }
pub fn router( pub fn router(config: RouterConfig) -> axum::Router {
config: RouterConfig,
) -> axum::Router {
axum::Router::new() axum::Router::new()
.nest( .nest(
"/steam/", "/steam/",

View File

@@ -8,9 +8,7 @@ struct DemoState {
upload_folder: std::path::PathBuf, upload_folder: std::path::PathBuf,
} }
pub fn router<P>( pub fn router<P>(upload_folder: P) -> axum::Router
upload_folder: P,
) -> axum::Router
where where
P: Into<std::path::PathBuf>, P: Into<std::path::PathBuf>,
{ {
@@ -102,7 +100,8 @@ async fn upload(
}); });
query.execute(&mut db_con).await.unwrap(); 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 { let queue_query = diesel::dsl::insert_into(crate::schema::analysis_queue::dsl::analysis_queue)
.values(crate::models::AddAnalysisTask {
demo_id, demo_id,
steam_id: steam_id.to_string(), steam_id: steam_id.to_string(),
}); });
@@ -145,7 +144,8 @@ async fn analyise(
)); ));
} }
let queue_query = diesel::dsl::insert_into(crate::schema::analysis_queue::dsl::analysis_queue).values(crate::models::AddAnalysisTask { let queue_query = diesel::dsl::insert_into(crate::schema::analysis_queue::dsl::analysis_queue)
.values(crate::models::AddAnalysisTask {
demo_id, demo_id,
steam_id: steam_id.to_string(), steam_id: steam_id.to_string(),
}); });
@@ -181,14 +181,27 @@ async fn info(
} }
#[tracing::instrument(skip(session))] #[tracing::instrument(skip(session))]
async fn scoreboard(session: UserSession, Path(demo_id): Path<i64>) -> Result<axum::response::Json<common::demo_analysis::ScoreBoard>, axum::http::StatusCode> { async fn scoreboard(
session: UserSession,
Path(demo_id): Path<i64>,
) -> Result<axum::response::Json<common::demo_analysis::ScoreBoard>, axum::http::StatusCode> {
let query = crate::schema::demo_players::dsl::demo_players 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)); .filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
let mut db_con = crate::db_connection().await; let mut db_con = crate::db_connection().await;
let response: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerStats)> = match query.load(&mut db_con).await { let response: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerStats)> =
match query.load(&mut db_con).await {
Ok(d) => d, Ok(d) => d,
Err(e) => { Err(e) => {
tracing::error!("Querying DB: {:?}", e); tracing::error!("Querying DB: {:?}", e);
@@ -206,12 +219,18 @@ async fn scoreboard(session: UserSession, Path(demo_id): Path<i64>) -> Result<ax
let mut team1 = Vec::new(); let mut team1 = Vec::new();
let mut team2 = Vec::new(); let mut team2 = Vec::new();
for (player, stats) in response { for (player, stats) in response {
let team_vec = if player.team == team1_number { &mut team1 } else { &mut team2 }; let team_vec = if player.team == team1_number {
&mut team1
} else {
&mut team2
};
team_vec.push(common::demo_analysis::ScoreBoardPlayer { team_vec.push(common::demo_analysis::ScoreBoardPlayer {
name: player.name, name: player.name,
kills: stats.kills as usize, kills: stats.kills as usize,
deaths: stats.deaths as usize, deaths: stats.deaths as usize,
damage: stats.damage as usize,
assists: stats.assists as usize,
}); });
} }

View File

@@ -64,15 +64,13 @@ pub async fn run_api(
let router = axum::Router::new() let router = axum::Router::new()
.nest( .nest(
"/api/", "/api/",
crate::api::router( crate::api::router(crate::api::RouterConfig {
crate::api::RouterConfig {
steam_api_key: steam_api_key.into(), steam_api_key: steam_api_key.into(),
// steam_callback_base_url: "http://192.168.0.156:3000".into(), steam_callback_base_url: "http://192.168.0.156:3000".into(),
steam_callback_base_url: "http://localhost:3000".into(), // steam_callback_base_url: "http://localhost:3000".into(),
steam_callback_path: "/api/steam/callback".into(), steam_callback_path: "/api/steam/callback".into(),
upload_dir: upload_folder.clone(), upload_dir: upload_folder.clone(),
}, }),
),
) )
.layer(session_layer) .layer(session_layer)
.nest_service( .nest_service(
@@ -85,15 +83,12 @@ pub async fn run_api(
} }
#[tracing::instrument(skip(upload_folder))] #[tracing::instrument(skip(upload_folder))]
pub async fn run_analysis( pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
upload_folder: impl Into<std::path::PathBuf>
) {
use diesel::prelude::*; use diesel::prelude::*;
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
let upload_folder: std::path::PathBuf = upload_folder.into(); let upload_folder: std::path::PathBuf = upload_folder.into();
loop { loop {
let mut db_con = db_connection().await; let mut db_con = db_connection().await;
let input = match crate::analysis::poll_next_task(&upload_folder, &mut db_con).await { let input = match crate::analysis::poll_next_task(&upload_folder, &mut db_con).await {
@@ -114,37 +109,69 @@ pub async fn run_analysis(
let mut db_con = crate::db_connection().await; let mut db_con = crate::db_connection().await;
let (player_info, player_stats): (Vec<_>, Vec<_>) = result.players.into_iter().map(|(info, stats)| { let (player_info, player_stats): (Vec<_>, Vec<_>) = result
(crate::models::DemoPlayer { .players
.into_iter()
.map(|(info, stats)| {
(
crate::models::DemoPlayer {
demo_id, demo_id,
name: info.name, name: info.name,
steam_id: info.steam_id.clone(), steam_id: info.steam_id.clone(),
team: info.team as i16, team: info.team as i16,
color: info.color as i16, color: info.color as i16,
}, crate::models::DemoPlayerStats { },
crate::models::DemoPlayerStats {
demo_id, demo_id,
steam_id: info.steam_id, steam_id: info.steam_id,
deaths: stats.deaths as i16, deaths: stats.deaths as i16,
kills: stats.kills as i16, kills: stats.kills as i16,
damage: stats.damage as i16,
assists: stats.assists as i16,
},
)
}) })
}).unzip(); .unzip();
let demo_info = crate::models::DemoInfo { let demo_info = crate::models::DemoInfo {
demo_id, demo_id,
map: result.map, map: result.map,
}; };
let store_demo_info_query = diesel::dsl::insert_into(crate::schema::demo_info::dsl::demo_info) let store_demo_info_query =
.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))); diesel::dsl::insert_into(crate::schema::demo_info::dsl::demo_info)
let store_demo_players_query = diesel::dsl::insert_into(crate::schema::demo_players::dsl::demo_players).values(player_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(); .on_conflict_do_nothing();
let store_demo_player_stats_query = diesel::dsl::insert_into(crate::schema::demo_player_stats::dsl::demo_player_stats) let store_demo_player_stats_query =
diesel::dsl::insert_into(crate::schema::demo_player_stats::dsl::demo_player_stats)
.values(player_stats) .values(player_stats)
.on_conflict((crate::schema::demo_player_stats::dsl::demo_id, crate::schema::demo_player_stats::dsl::steam_id)) .on_conflict((
crate::schema::demo_player_stats::dsl::demo_id,
crate::schema::demo_player_stats::dsl::steam_id,
))
.do_update() .do_update()
.set(( .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::deaths.eq(diesel::upsert::excluded(
crate::schema::demo_player_stats::dsl::kills.eq(diesel::upsert::excluded(crate::schema::demo_player_stats::dsl::kills)), 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 = let update_process_info =
diesel::dsl::update(crate::schema::processing_status::dsl::processing_status) diesel::dsl::update(crate::schema::processing_status::dsl::processing_status)

View File

@@ -49,10 +49,7 @@ async fn main() {
} }
}; };
component_set.spawn(backend::run_api( component_set.spawn(backend::run_api(args.upload_folder.clone(), steam_api_key));
args.upload_folder.clone(),
steam_api_key,
));
} }
if args.analysis { if args.analysis {
component_set.spawn(backend::run_analysis(args.upload_folder.clone())); component_set.spawn(backend::run_analysis(args.upload_folder.clone()));

View File

@@ -52,6 +52,8 @@ pub struct DemoPlayerStats {
pub steam_id: String, pub steam_id: String,
pub kills: i16, pub kills: i16,
pub deaths: i16, pub deaths: i16,
pub damage: i16,
pub assists: i16,
} }
#[derive(Queryable, Selectable, Insertable, Debug)] #[derive(Queryable, Selectable, Insertable, Debug)]

View File

@@ -21,6 +21,8 @@ diesel::table! {
steam_id -> Text, steam_id -> Text,
kills -> Int2, kills -> Int2,
deaths -> Int2, deaths -> Int2,
damage -> Int2,
assists -> Int2,
} }
} }

View File

@@ -28,5 +28,7 @@ pub mod demo_analysis {
pub name: String, pub name: String,
pub kills: usize, pub kills: usize,
pub deaths: usize, pub deaths: usize,
pub damage: usize,
pub assists: usize,
} }
} }

View File

@@ -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() { let map = move || match demo_info.get() {
Some(v) => v.map.clone(), Some(v) => v.map.clone(),
None => String::new(), None => String::new(),
@@ -58,6 +64,7 @@ pub fn demo() -> impl leptos::IntoView {
view! {class = style, view! {class = style,
<h2>Demo - { id } - { map }</h2> <h2>Demo - { id } - { map }</h2>
<button on:click=move |_| rerun_analysis.dispatch(())>Rerun analysis</button>
<div class="analysis_bar"> <div class="analysis_bar">
<div class="analysis_selector" class:current=move || selected_tab() == "scoreboard"><A href="scoreboard"><span>Scoreboard</span></A></div> <div class="analysis_selector" class:current=move || selected_tab() == "scoreboard"><A href="scoreboard"><span>Scoreboard</span></A></div>
<div class="analysis_selector" class:current=move || selected_tab() == "perround"><A href="perround"><span>Per Round</span></A></div> <div class="analysis_selector" class:current=move || selected_tab() == "perround"><A href="perround"><span>Per Round</span></A></div>
@@ -72,26 +79,34 @@ pub fn demo() -> impl leptos::IntoView {
pub fn scoreboard() -> impl leptos::IntoView { pub fn scoreboard() -> impl leptos::IntoView {
use leptos::Suspense; use leptos::Suspense;
let scoreboard_resource = create_resource(leptos_router::use_params_map(), |params| async move { let scoreboard_resource =
create_resource(leptos_router::use_params_map(), |params| async move {
let id = params.get("id").unwrap(); let id = params.get("id").unwrap();
let res = reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id)) let res =
reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id))
.send() .send()
.await .await
.unwrap(); .unwrap();
res.json::<common::demo_analysis::ScoreBoard>().await.unwrap() res.json::<common::demo_analysis::ScoreBoard>()
.await
.unwrap()
}); });
let team_display_func = |team: &[common::demo_analysis::ScoreBoardPlayer]| { let team_display_func = |team: &[common::demo_analysis::ScoreBoardPlayer]| {
team.iter().map(|player| { team.iter()
.map(|player| {
view! { view! {
<tr> <tr>
<td> { player.name.clone() } </td> <td> { player.name.clone() } </td>
<td> { player.kills } </td> <td> { player.kills } </td>
<td> { player.assists } </td>
<td> { player.deaths } </td> <td> { player.deaths } </td>
<td> { player.damage } </td>
</tr> </tr>
} }
}).collect::<Vec<_>>() })
.collect::<Vec<_>>()
}; };
view! { view! {
@@ -106,7 +121,9 @@ pub fn scoreboard() -> impl leptos::IntoView {
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Kills</th> <th>Kills</th>
<th>Assists</th>
<th>Deaths</th> <th>Deaths</th>
<th>Damage</th>
</tr> </tr>
{ {
move || { move || {
@@ -124,6 +141,10 @@ pub fn scoreboard() -> impl leptos::IntoView {
<table> <table>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Kills</th>
<th>Assists</th>
<th>Deaths</th>
<th>Damage</th>
</tr> </tr>
{ {
move || { move || {

View File

@@ -23,5 +23,7 @@ CREATE TABLE IF NOT EXISTS demo_player_stats (
steam_id TEXT NOT NULL, steam_id TEXT NOT NULL,
kills int2 NOT NULL, kills int2 NOT NULL,
deaths int2 NOT NULL, deaths int2 NOT NULL,
damage int2 NOT NULL,
assists int2 NOT NULL,
PRIMARY KEY (demo_id, steam_id) PRIMARY KEY (demo_id, steam_id)
); );

BIN
testfiles/nuke.dem LFS Normal file

Binary file not shown.