Add Head-to-Head kill stats
Add basic head-to-head kill analysis and the corresponding matrix display in the UI
This commit is contained in:
@@ -4,6 +4,7 @@ use diesel_async::RunQueryDsl;
|
||||
pub mod base;
|
||||
pub mod heatmap;
|
||||
pub mod perround;
|
||||
pub mod head_to_head;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AnalysisData {
|
||||
@@ -61,12 +62,13 @@ pub trait Analysis {
|
||||
>;
|
||||
}
|
||||
|
||||
pub static ANALYSIS_METHODS: std::sync::LazyLock<[std::sync::Arc<dyn Analysis + Send + Sync>; 3]> =
|
||||
pub static ANALYSIS_METHODS: std::sync::LazyLock<[std::sync::Arc<dyn Analysis + Send + Sync>; 4]> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
[
|
||||
std::sync::Arc::new(base::BaseAnalysis::new()),
|
||||
std::sync::Arc::new(heatmap::HeatmapAnalysis::new()),
|
||||
std::sync::Arc::new(perround::PerRoundAnalysis::new()),
|
||||
std::sync::Arc::new(head_to_head::HeadToHeadAnalysis::new()),
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
72
backend/src/analysis/head_to_head.rs
Normal file
72
backend/src/analysis/head_to_head.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use super::*;
|
||||
|
||||
pub struct HeadToHeadAnalysis {}
|
||||
|
||||
impl HeadToHeadAnalysis {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Analysis for HeadToHeadAnalysis {
|
||||
#[tracing::instrument(name = "HeadToHead", skip(self, input))]
|
||||
fn analyse(
|
||||
&self,
|
||||
input: AnalysisInput,
|
||||
) -> Result<
|
||||
Box<
|
||||
dyn FnOnce(
|
||||
&mut diesel_async::pg::AsyncPgConnection,
|
||||
) -> core::pin::Pin<
|
||||
Box<
|
||||
(dyn core::future::Future<Output = Result<(), diesel::result::Error>>
|
||||
+ Send
|
||||
+ '_),
|
||||
>,
|
||||
> + Send,
|
||||
>,
|
||||
(),
|
||||
> {
|
||||
tracing::info!("Performing Head-to-Head analysis");
|
||||
|
||||
let result = analysis::head_to_head::parse(input.data()).inspect_err(|e| {
|
||||
tracing::error!("{:?}", e);
|
||||
}).map_err(|e| ())?;
|
||||
|
||||
let values_to_insert: Vec<_> = result.head_to_head.into_iter().flat_map(|(user_id, enemies)| {
|
||||
enemies.into_iter().map(move |(enemy, kills)| (user_id, enemy, kills))
|
||||
}).filter_map(|(user_id, enemy_id, kills)| {
|
||||
let player = result.players.get(&user_id)?;
|
||||
let enemy = result.players.get(&enemy_id)?;
|
||||
|
||||
Some(crate::models::DemoHeadToHead {
|
||||
demo_id: input.demoid.clone(),
|
||||
player: player.xuid.to_string(),
|
||||
enemy: enemy.xuid.to_string(),
|
||||
kills: kills as i16,
|
||||
})
|
||||
}).collect();
|
||||
|
||||
Ok(Box::new(move |connection| {
|
||||
// TODO
|
||||
// Construct the actual queries
|
||||
|
||||
let query = diesel::insert_into(crate::schema::demo_head_to_head::dsl::demo_head_to_head)
|
||||
.values(values_to_insert)
|
||||
.on_conflict((
|
||||
crate::schema::demo_head_to_head::dsl::demo_id,
|
||||
crate::schema::demo_head_to_head::dsl::player,
|
||||
crate::schema::demo_head_to_head::dsl::enemy,
|
||||
))
|
||||
.do_update().set(crate::schema::demo_head_to_head::kills.eq(diesel::upsert::excluded(crate::schema::demo_head_to_head::kills)));
|
||||
|
||||
Box::pin(async move {
|
||||
// TODO
|
||||
// Execute queries
|
||||
query.execute(connection).await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ pub fn router(storage: Box<dyn crate::storage::DemoStorage>) -> axum::Router {
|
||||
.route("/:id/analysis/scoreboard", axum::routing::get(scoreboard))
|
||||
.route("/:id/analysis/perround", axum::routing::get(perround))
|
||||
.route("/:id/analysis/heatmap", axum::routing::get(heatmap))
|
||||
.route("/:id/analysis/headtohead", axum::routing::get(head_to_head))
|
||||
.with_state(Arc::new(DemoState { storage }))
|
||||
}
|
||||
|
||||
@@ -556,6 +557,49 @@ async fn perround(
|
||||
}))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(session))]
|
||||
async fn head_to_head(
|
||||
session: UserSession,
|
||||
Path(demo_id): Path<String>,
|
||||
) -> Result<axum::response::Json<common::demo_analysis::HeadToHead>, axum::http::StatusCode> {
|
||||
let mut db_con = crate::db_connection().await;
|
||||
|
||||
let head_to_head_query = crate::schema::demo_head_to_head::dsl::demo_head_to_head
|
||||
.filter(crate::schema::demo_head_to_head::dsl::demo_id.eq(demo_id.clone()));
|
||||
|
||||
let player_query = crate::schema::demo_players::dsl::demo_players
|
||||
.filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
|
||||
|
||||
let (players, head_to_head_entries) = db_con.build_transaction().read_only().run(|connection| Box::pin(async move {
|
||||
let head_to_head_entries: Vec<crate::models::DemoHeadToHead> = head_to_head_query.load(connection).await?;
|
||||
let players: Vec<crate::models::DemoPlayer> = player_query.load(connection).await?;
|
||||
|
||||
Ok::<_, diesel::result::Error>((players, head_to_head_entries))
|
||||
})).await.map_err(|e| {
|
||||
tracing::error!("Querying DB: {:?}", e);
|
||||
axum::http::StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let (mut row_team, mut column_team): (Vec<_>, Vec<_>) = players.into_iter().partition(|p| p.team == 2);
|
||||
row_team.sort_unstable_by_key(|p| p.color);
|
||||
column_team.sort_unstable_by_key(|p| p.color);
|
||||
|
||||
let results: Vec<_> = row_team.iter().map(|row_player| {
|
||||
column_team.iter().map(|column_player| {
|
||||
let row_kills = head_to_head_entries.iter().find(|entry| entry.player == row_player.steam_id && entry.enemy == column_player.steam_id);
|
||||
let column_kills = head_to_head_entries.iter().find(|entry| entry.player == column_player.steam_id && entry.enemy == row_player.steam_id);
|
||||
|
||||
(row_kills.map(|k| k.kills).unwrap_or(0), column_kills.map(|k| k.kills).unwrap_or(0))
|
||||
}).collect::<Vec<_>>()
|
||||
}).collect();
|
||||
|
||||
Ok(axum::Json(common::demo_analysis::HeadToHead {
|
||||
row_players: row_team.into_iter().map(|p| p.name).collect(),
|
||||
column_players: column_team.into_iter().map(|p| p.name).collect(),
|
||||
entries: results,
|
||||
}))
|
||||
}
|
||||
|
||||
// The corresponding values for each map can be found using the Source2 Viewer and opening the
|
||||
// files in 'game/csgo/pak01_dir.vpk' and then 'resource/overviews/{map}.txt'
|
||||
static MINIMAP_COORDINATES: phf::Map<&str, MiniMapDefinition> = phf::phf_map! {
|
||||
|
||||
@@ -120,3 +120,13 @@ pub struct DemoRound {
|
||||
pub win_reason: String,
|
||||
pub events: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Insertable, Debug)]
|
||||
#[diesel(table_name = crate::schema::demo_head_to_head)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct DemoHeadToHead {
|
||||
pub demo_id: String,
|
||||
pub player: String,
|
||||
pub enemy: String,
|
||||
pub kills: i16,
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
demo_head_to_head (demo_id, player, enemy) {
|
||||
demo_id -> Text,
|
||||
player -> Text,
|
||||
enemy -> Text,
|
||||
kills -> Int2,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
demo_heatmaps (demo_id, steam_id, team) {
|
||||
demo_id -> Text,
|
||||
@@ -97,6 +106,7 @@ diesel::table! {
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
analysis_queue,
|
||||
demo_head_to_head,
|
||||
demo_heatmaps,
|
||||
demo_info,
|
||||
demo_player_stats,
|
||||
|
||||
Reference in New Issue
Block a user