Add Heatmaps to UI

Add Heatmap analysis to website as well as a basic UI for viewing the Heatmaps.
There are still issues, like some players not getting a heatmap assigned and heatmaps including data
from warmup etc.
This commit is contained in:
Lol3rrr
2024-09-29 00:32:20 +02:00
parent 7f23f4882d
commit 83b4a24b15
19 changed files with 280 additions and 46 deletions

View File

@@ -63,7 +63,7 @@ pub async fn poll_next_task(
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct AnalysisInput {
pub steamid: String,
pub demoid: i64,
@@ -120,3 +120,29 @@ pub fn analyse_base(input: AnalysisInput) -> BaseInfo {
}).collect()
}
}
#[tracing::instrument(skip(input))]
pub fn analyse_heatmap(input: AnalysisInput) -> std::collections::HashMap<String, analysis::heatmap::HeatMap> {
tracing::info!("Generating HEATMAPs");
let file = std::fs::File::open(&input.path).unwrap();
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
let config = analysis::heatmap::Config {
cell_size: 5.0,
};
let (heatmaps, players) = analysis::heatmap::parse(&config, &mmap).unwrap();
tracing::info!("Got {} Heatmaps", heatmaps.len());
heatmaps.into_iter().filter_map(|(userid, heatmap)| {
let player = match players.get(&userid) {
Some(p) => p,
None => {
tracing::warn!("Could not find player: {:?}", userid);
return None;
}
};
Some((player.xuid.to_string(), heatmap))
}).collect()
}

View File

@@ -40,6 +40,8 @@ pub mod steam {
) -> Result<axum::response::Redirect, axum::http::StatusCode> {
let url = state.openid.get_redirect_url();
tracing::info!("Redirecting to {:?}", url);
Ok(axum::response::Redirect::to(url))
}

View File

@@ -22,6 +22,7 @@ where
.route("/:id/info", axum::routing::get(info))
.route("/:id/analysis/scoreboard", axum::routing::get(scoreboard))
.route("/:id/reanalyse", axum::routing::get(analyise))
.route("/:id/analysis/heatmap", axum::routing::get(heatmap))
.with_state(Arc::new(DemoState {
upload_folder: upload_folder.into(),
}))
@@ -239,3 +240,43 @@ async fn scoreboard(
team2,
}))
}
#[tracing::instrument(skip(session))]
async fn heatmap(
session: UserSession,
Path(demo_id): Path<i64>,
) -> Result<axum::response::Json<Vec<common::demo_analysis::PlayerHeatmap>>, axum::http::StatusCode> {
use base64::prelude::Engine;
let query = crate::schema::demo_players::dsl::demo_players
.inner_join(crate::schema::demo_heatmaps::dsl::demo_heatmaps.on(
crate::schema::demo_players::dsl::steam_id.eq(crate::schema::demo_heatmaps::dsl::steam_id)
.and(crate::schema::demo_players::dsl::demo_id.eq(crate::schema::demo_heatmaps::dsl::demo_id))
)).filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
let mut db_con = crate::db_connection().await;
let result: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerHeatmap)> = 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 data: Vec<common::demo_analysis::PlayerHeatmap> = result.into_iter().map(|(player, heatmap)| {
let mut heatmap: analysis::heatmap::HeatMap = serde_json::from_str(&heatmap.data).unwrap();
heatmap.shrink();
let h_image = heatmap.as_image();
let mut buffer = std::io::Cursor::new(Vec::new());
h_image.write_to(&mut buffer, image::ImageFormat::Png).unwrap();
common::demo_analysis::PlayerHeatmap {
name: player.name,
png_data: base64::prelude::BASE64_STANDARD.encode(buffer.into_inner()),
}
}).collect();
Ok(axum::Json(data))
}

View File

@@ -66,7 +66,7 @@ pub async fn run_api(
"/api/",
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://localhost:3000".into(),
steam_callback_path: "/api/steam/callback".into(),
upload_dir: upload_folder.clone(),
@@ -101,15 +101,18 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
let demo_id = input.demoid;
let result = tokio::task::spawn_blocking(move || crate::analysis::analyse_base(input))
let base_input = input.clone();
let base_result = tokio::task::spawn_blocking(move || crate::analysis::analyse_base(base_input))
.await
.unwrap();
tracing::debug!("Analysis-Result: {:?}", result);
let heatmap_result = tokio::task::spawn_blocking(move || crate::analysis::analyse_heatmap(input))
.await
.unwrap();
let mut db_con = crate::db_connection().await;
let (player_info, player_stats): (Vec<_>, Vec<_>) = result
let (player_info, player_stats): (Vec<_>, Vec<_>) = base_result
.players
.into_iter()
.map(|(info, stats)| {
@@ -133,9 +136,19 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
})
.unzip();
let player_heatmaps: Vec<_> = heatmap_result.into_iter().map(|(player, heatmap)| {
tracing::trace!("HeatMap for Player: {:?}", player);
crate::models::DemoPlayerHeatmap {
demo_id,
steam_id: player,
data: serde_json::to_string(&heatmap).unwrap(),
}
}).collect();
let demo_info = crate::models::DemoInfo {
demo_id,
map: result.map,
map: base_result.map,
};
let store_demo_info_query =
@@ -173,6 +186,11 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
crate::schema::demo_player_stats::dsl::damage,
)),
));
let store_demo_player_heatmaps_query = diesel::dsl::insert_into(crate::schema::demo_heatmaps::dsl::demo_heatmaps)
.values(player_heatmaps)
.on_conflict((crate::schema::demo_heatmaps::dsl::demo_id, crate::schema::demo_heatmaps::dsl::steam_id))
.do_update()
.set((crate::schema::demo_heatmaps::dsl::data.eq(diesel::upsert::excluded(crate::schema::demo_heatmaps::dsl::data))));
let update_process_info =
diesel::dsl::update(crate::schema::processing_status::dsl::processing_status)
.set(crate::schema::processing_status::dsl::info.eq(1))
@@ -184,6 +202,7 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
store_demo_info_query.execute(conn).await?;
store_demo_players_query.execute(conn).await?;
store_demo_player_stats_query.execute(conn).await?;
store_demo_player_heatmaps_query.execute(conn).await?;
update_process_info.execute(conn).await?;
Ok(())
})
@@ -191,7 +210,6 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
.await
.unwrap();
// TODO
// Remove task from queue
tracing::info!("Stored analysis results");
}
}

View File

@@ -27,6 +27,7 @@ async fn main() {
.with(tracing_subscriber::fmt::layer())
.with(tracing_subscriber::filter::filter_fn(|meta| {
meta.target().contains("backend")
|| meta.target().contains("analysis")
}));
tracing::subscriber::set_global_default(registry).unwrap();

View File

@@ -79,3 +79,12 @@ pub struct AnalysisTask {
pub demo_id: i64,
pub steam_id: String,
}
#[derive(Queryable, Selectable, Insertable, Debug)]
#[diesel(table_name = crate::schema::demo_heatmaps)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct DemoPlayerHeatmap {
pub demo_id: i64,
pub steam_id: String,
pub data: String,
}

View File

@@ -8,6 +8,14 @@ diesel::table! {
}
}
diesel::table! {
demo_heatmaps (demo_id, steam_id) {
demo_id -> Int8,
steam_id -> Text,
data -> Text,
}
}
diesel::table! {
demo_info (demo_id) {
demo_id -> Int8,
@@ -66,6 +74,7 @@ diesel::table! {
}
diesel::joinable!(analysis_queue -> demos (demo_id));
diesel::joinable!(demo_heatmaps -> demo_info (demo_id));
diesel::joinable!(demo_info -> demos (demo_id));
diesel::joinable!(demo_player_stats -> demo_info (demo_id));
diesel::joinable!(demo_players -> demo_info (demo_id));
@@ -73,6 +82,7 @@ diesel::joinable!(processing_status -> demos (demo_id));
diesel::allow_tables_to_appear_in_same_query!(
analysis_queue,
demo_heatmaps,
demo_info,
demo_player_stats,
demo_players,