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",
]
[[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"

View File

@@ -1,3 +1,3 @@
[workspace]
members = ["backend", "common", "frontend"]
members = [ "analysis","backend", "common", "frontend"]
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"] }
common = { path = "../common/" }
analysis = { path = "../analysis/" }
csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" }
memmap2 = { version = "0.9" }

View File

@@ -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<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();
pub async fn poll_next_task(
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 {
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 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()));
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;
})
})
.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,6 +89,8 @@ pub struct BasePlayerInfo {
pub struct BasePlayerStats {
pub kills: usize,
pub deaths: usize,
pub damage: usize,
pub assists: usize,
}
#[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 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()
}
}

View File

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

View File

@@ -8,9 +8,7 @@ struct DemoState {
upload_folder: std::path::PathBuf,
}
pub fn router<P>(
upload_folder: P,
) -> axum::Router
pub fn router<P>(upload_folder: P) -> axum::Router
where
P: Into<std::path::PathBuf>,
{
@@ -102,7 +100,8 @@ 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 {
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(),
});
@@ -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,
steam_id: steam_id.to_string(),
});
@@ -181,14 +181,27 @@ async fn info(
}
#[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
.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 {
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);
@@ -206,12 +219,18 @@ async fn scoreboard(session: UserSession, Path(demo_id): Path<i64>) -> Result<ax
let mut team1 = Vec::new();
let mut team2 = Vec::new();
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 {
name: player.name,
kills: stats.kills 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()
.nest(
"/api/",
crate::api::router(
crate::api::RouterConfig {
crate::api::router(crate::api::RouterConfig {
steam_api_key: steam_api_key.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://192.168.0.156:3000".into(),
// steam_callback_base_url: "http://localhost:3000".into(),
steam_callback_path: "/api/steam/callback".into(),
upload_dir: upload_folder.clone(),
},
),
}),
)
.layer(session_layer)
.nest_service(
@@ -85,15 +83,12 @@ pub async fn run_api(
}
#[tracing::instrument(skip(upload_folder))]
pub async fn run_analysis(
upload_folder: impl Into<std::path::PathBuf>
) {
pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
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,37 +109,69 @@ 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 {
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 {
},
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();
.unzip();
let demo_info = crate::models::DemoInfo {
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)
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)
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))
.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::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)

View File

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

View File

@@ -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)]

View File

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

View File

@@ -28,5 +28,7 @@ pub mod demo_analysis {
pub name: String,
pub kills: 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() {
Some(v) => v.map.clone(),
None => String::new(),
@@ -58,6 +64,7 @@ pub fn demo() -> impl leptos::IntoView {
view! {class = style,
<h2>Demo - { id } - { map }</h2>
<button on:click=move |_| rerun_analysis.dispatch(())>Rerun analysis</button>
<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() == "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 {
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 res = reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id))
let res =
reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/scoreboard", id))
.send()
.await
.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]| {
team.iter().map(|player| {
team.iter()
.map(|player| {
view! {
<tr>
<td> { player.name.clone() } </td>
<td> { player.kills } </td>
<td> { player.assists } </td>
<td> { player.deaths } </td>
<td> { player.damage } </td>
</tr>
}
}).collect::<Vec<_>>()
})
.collect::<Vec<_>>()
};
view! {
@@ -106,7 +121,9 @@ pub fn scoreboard() -> impl leptos::IntoView {
<tr>
<th>Name</th>
<th>Kills</th>
<th>Assists</th>
<th>Deaths</th>
<th>Damage</th>
</tr>
{
move || {
@@ -124,6 +141,10 @@ pub fn scoreboard() -> impl leptos::IntoView {
<table>
<tr>
<th>Name</th>
<th>Kills</th>
<th>Assists</th>
<th>Deaths</th>
<th>Damage</th>
</tr>
{
move || {

View File

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

BIN
testfiles/nuke.dem LFS Normal file

Binary file not shown.