Files
knifer/analysis/src/heatmap.rs
Lol3rrr a220356eaf Switch Heatmaps to be for each side respectively
The analysis and storage now contain two different heatmaps for the CT and T sides respectively.

However on the frontend I currently just have them all listed out with the side as a prefix, which is
not very useful and we should more change the data structure to essentially send the two heatmaps for
a player as a single data structure and then in the view display them side by side. However I am not
100% how to best achieve this
2024-10-11 00:42:21 +02:00

411 lines
13 KiB
Rust

pub struct Config {
pub cell_size: f32,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct HeatMap {
#[serde(default)]
min_x: usize,
#[serde(default)]
min_y: usize,
max_x: usize,
max_y: usize,
max_value: usize,
rows: Vec<Vec<usize>>,
block_size: f32,
}
impl HeatMap {
fn new(block_size: f32) -> Self {
Self {
min_x: 0,
min_y: 0,
max_x: 0,
max_y: 0,
max_value: 0,
rows: Vec::new(),
block_size,
}
}
fn increment(&mut self, x: usize, y: usize) {
if self.rows.len() <= y {
self.rows.resize(y + 1, Vec::new());
}
self.max_y = self.max_y.max(y);
let row = self.rows.get_mut(y ).unwrap();
if row.len() <= x {
row.resize(x + 1, 0);
}
self.max_x = self.max_x.max(x);
let cell = row.get_mut(x).unwrap();
*cell += 1;
self.max_value = self.max_value.max(*cell);
}
}
#[derive(Debug)]
pub struct HeatMapOutput {
pub player_heatmaps: std::collections::HashMap<(csdemo::UserId, String), HeatMap>,
pub player_info: std::collections::HashMap<csdemo::UserId, csdemo::parser::Player>,
}
#[derive(Debug)]
pub struct Team {
pub num: u32,
pub name: String,
pub players: Vec<u32>,
pub pawns: Vec<PawnID>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub struct PawnID(u32);
impl From<i32> for PawnID {
fn from(value: i32) -> Self {
Self((value & 0x7FF) as u32)
}
}
impl From<u32> for PawnID {
fn from(value: u32) -> Self {
Self(value & 0x7FF)
}
}
pub fn parse(config: &Config, buf: &[u8]) -> Result<HeatMapOutput, ()> {
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 pawn_ids = {
let mut tmp = std::collections::HashMap::<PawnID,_>::new();
for event in output.events.iter() {
let entry = match event {
csdemo::DemoEvent::GameEvent(ge) => match ge.as_ref() {
csdemo::game_event::GameEvent::PlayerSpawn(pspawn) => match pspawn.userid_pawn.as_ref() {
Some(csdemo::RawValue::I32(v)) => {
Some((PawnID::from(*v), pspawn.userid.unwrap()))
}
_ => {
None
},
},
_ => None,
},
_ => None,
};
if let Some((pawn, userid)) = entry {
if let Some(previous) = tmp.insert(pawn, userid){
assert_eq!(previous, userid);
}
}
}
tmp
};
let mut teams = std::collections::HashMap::new();
let mut player_lifestate = std::collections::HashMap::<csdemo::UserId, u32>::new();
let mut player_position = std::collections::HashMap::<csdemo::UserId, (f32, f32, f32)>::new();
let mut player_cells = std::collections::HashMap::new();
let mut heatmaps = std::collections::HashMap::new();
for tick_state in output.entity_states.ticks.iter() {
let _tracing_guard = tracing::debug_span!("Tick", tick=?tick_state.tick).entered();
process_tick(
config,
tick_state,
&pawn_ids,
&mut teams,
&mut player_lifestate,
&mut player_position,
&mut player_cells,
&mut heatmaps,
);
}
tracing::debug!("Pawn-IDs: {:?}", pawn_ids);
Ok(HeatMapOutput {
player_heatmaps: heatmaps,
player_info: output.player_info,
})
}
pub const MAX_COORD: f32 = (1 << 14) as f32;
fn process_tick(
config: &Config,
tick_state: &csdemo::parser::EntityTickStates,
pawn_ids: &std::collections::HashMap<PawnID, csdemo::UserId>,
teams: &mut std::collections::HashMap<PawnID, String>,
player_lifestate: &mut std::collections::HashMap<csdemo::UserId, u32>,
player_position: &mut std::collections::HashMap<csdemo::UserId, (f32, f32, f32)>,
player_cells: &mut std::collections::HashMap<csdemo::UserId, (u32, u32, u32)>,
heatmaps: &mut std::collections::HashMap<(csdemo::UserId, String), HeatMap>,
) {
for entity_state in tick_state
.states
.iter()
.filter(|s| matches!(s.class.as_ref(), "CCSPlayerPawn" | "CCSTeam"))
{
if entity_state.class.as_ref() == "CCSTeam" {
let raw_team_name = match entity_state.get_prop("CCSTeam.m_szTeamname").map(|p| match &p.value {
csdemo::parser::Variant::String(v) => Some(v),
_ => None,
}).flatten() {
Some(n) => n,
None => continue,
};
for prop in entity_state.props.iter().filter(|p| p.prop_info.prop_name.as_ref() == "CCSTeam.m_aPawns").filter_map(|p| p.value.as_u32().map(|v| PawnID::from(v))) {
teams.insert(prop, raw_team_name.clone());
}
continue;
}
let pawn_id = PawnID::from(entity_state.id);
let user_id = match pawn_ids.get(&pawn_id).cloned() {
Some(id) => id,
None => continue,
};
let team = match teams.get(&pawn_id).cloned() {
Some(t) => t,
None => continue,
};
let _inner_guard =
tracing::trace_span!("Entity", entity_id=?entity_state.id).entered();
let x_cell = match entity_state.get_prop("CCSPlayerPawn.CBodyComponentBaseAnimGraph.m_cellX").map(|prop| prop.value.as_u32()).flatten() {
Some(c) => c,
None => player_cells.get(&user_id).map(|(x, _, _)| *x).unwrap_or(0),
};
let y_cell = match entity_state.get_prop("CCSPlayerPawn.CBodyComponentBaseAnimGraph.m_cellY").map(|prop| prop.value.as_u32()).flatten() {
Some(c) => c,
None => player_cells.get(&user_id).map(|(_, y, _)| *y).unwrap_or(0),
};
let z_cell = match entity_state.get_prop("CCSPlayerPawn.CBodyComponentBaseAnimGraph.m_cellZ").map(|prop| prop.value.as_u32()).flatten() {
Some(c) => c,
None => player_cells.get(&user_id).map(|(_, _, z)| *z).unwrap_or(0),
};
player_cells.insert(user_id, (x_cell, y_cell, z_cell));
let x_coord = match entity_state.get_prop("CCSPlayerPawn.CBodyComponentBaseAnimGraph.m_vecX").map(|prop| prop.value.as_f32()).flatten() {
Some(c) => c,
None => player_position.get(&user_id).map(|(x, _, _)| *x).unwrap_or(0.0),
};
let y_coord = match entity_state.get_prop("CCSPlayerPawn.CBodyComponentBaseAnimGraph.m_vecY").map(|prop| prop.value.as_f32()).flatten() {
Some(c) => c,
None => player_position.get(&user_id).map(|(_, y, _)| *y).unwrap_or(0.0),
};
let z_coord = match entity_state.get_prop("CCSPlayerPawn.CBodyComponentBaseAnimGraph.m_vecZ").map(|prop| prop.value.as_f32()).flatten() {
Some(c) => c,
None => player_position.get(&user_id).map(|(_, _, z)| *z).unwrap_or(0.0),
};
player_position.insert(user_id, (x_coord, y_coord, z_coord));
assert!(x_coord >= 0.0);
assert!(y_coord >= 0.0);
assert!(z_coord >= 0.0);
let x_cell_coord = ((x_cell as f32 * (1 << 9) as f32)) as f32;
let y_cell_coord = ((y_cell as f32 * (1 << 9) as f32)) as f32;
let z_cell_coord = ((z_cell as f32 * (1 << 9) as f32)) as f32;
let x_coord = x_cell_coord + x_coord;
let y_coord = y_cell_coord + y_coord;
let z_coord = z_cell_coord + z_coord;
assert!(x_coord >= 0.0);
assert!(y_coord >= 0.0);
assert!(z_coord >= 0.0);
let x_cell = (x_coord / config.cell_size) as usize;
let y_cell = (y_coord / config.cell_size) as usize;
let n_lifestate = entity_state.props.iter().find_map(|prop| {
if prop.prop_info.prop_name.as_ref() != "CCSPlayerPawn.m_lifeState" {
return None;
}
match prop.value {
csdemo::parser::Variant::U32(v) => Some(v),
_ => None,
}
});
let lifestate = match n_lifestate {
Some(state) => {
player_lifestate.insert(user_id, state);
state
}
None => player_lifestate.get(&user_id).copied().unwrap_or(1),
};
// 0 means alive
if lifestate != 0 {
continue;
}
// tracing::trace!("Coord (X, Y, Z): {:?} -> {:?}", (x_coord, y_coord, z_coord), (x_cell, y_cell));
let heatmap = heatmaps.entry((user_id.clone(), team)).or_insert(HeatMap::new(config.cell_size));
heatmap.increment(x_cell, y_cell);
}
}
impl core::fmt::Display for HeatMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let size = self.max_value.ilog10() as usize + 1;
for row in self.rows.iter() {
for cell in row.iter().copied() {
write!(f, "{: ^width$} ", cell, width=size)?;
}
writeln!(f)?;
}
Ok(())
}
}
impl HeatMap {
pub fn coords(&self) -> ((f32, f32), (f32, f32)) {
(
(self.min_x as f32 * self.block_size - MAX_COORD, self.max_x as f32 * self.block_size - MAX_COORD),
(self.min_y as f32 * self.block_size - MAX_COORD, self.max_y as f32 * self.block_size - MAX_COORD)
)
}
pub fn as_image(&self) -> image::RgbImage {
use colors_transform::Color;
let mut buffer = image::RgbImage::new((self.max_x - self.min_x) as u32 + 1, (self.max_y - self.min_y) as u32 + 1);
for (y, row) in self.rows.iter().rev().enumerate() {
for (x, cell) in row.iter().copied().chain(core::iter::repeat(0)).enumerate().take(self.max_x - self.min_x) {
let scaled = (1.0/(1.0 + (cell as f32))) * 240.0;
let raw_rgb = colors_transform::Hsl::from(scaled, 100.0, 50.0).to_rgb();
buffer.put_pixel(x as u32, y as u32, image::Rgb([raw_rgb.get_red() as u8, raw_rgb.get_green() as u8, raw_rgb.get_blue() as u8]))
}
}
buffer
}
pub fn fit(&mut self, xs: core::ops::Range<f32>, ys: core::ops::Range<f32>) {
let min_x = (xs.start / self.block_size - self.min_x as f32) as usize;
let min_y = (ys.start / self.block_size - self.min_y as f32) as usize;
let _ = self.rows.drain(0..min_y);
for row in self.rows.iter_mut() {
let _ = row.drain(0..min_x.min(row.len()));
}
let x_steps = ((xs.end - xs.start) / self.block_size) as usize;
let y_steps = ((ys.end - ys.start) / self.block_size) as usize;
for row in self.rows.iter_mut() {
row.resize(x_steps, 0);
}
self.rows.resize_with(y_steps, || vec![0; x_steps]);
self.min_y += (0..min_y).len();
self.min_x += (0..min_x).len();
self.max_y = self.min_y + self.rows.len();
self.max_x = self.min_x + self.rows.iter().map(|r| r.len()).max().unwrap_or(0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fit_no_cutoff() {
let mut input = HeatMap::new(2.0);
input.increment(3, 3);
input.increment(2, 2);
assert_eq!(input.min_x, 0);
assert_eq!(input.min_y, 0);
assert_eq!(input.max_x, 3);
assert_eq!(input.max_y, 3);
assert_eq!(
&vec![
vec![],
vec![],
vec![0, 0, 1],
vec![0, 0, 0, 1]
],
&input.rows
);
input.fit(2.0..10.0, 2.0..10.0);
assert_eq!(
&vec![
vec![0, 0, 0, 0],
vec![0, 1, 0, 0],
vec![0, 0, 1, 0],
vec![0, 0, 0, 0],
],
&input.rows
);
}
#[test]
fn fit_cutoff() {
let mut input = HeatMap::new(2.0);
input.increment(3, 3);
input.increment(2, 2);
assert_eq!(input.min_x, 0);
assert_eq!(input.min_y, 0);
assert_eq!(input.max_x, 3);
assert_eq!(input.max_y, 3);
assert_eq!(
&vec![
vec![],
vec![],
vec![0, 0, 1],
vec![0, 0, 0, 1]
],
&input.rows
);
input.fit(6.0..10.0, 6.0..10.0);
assert_eq!(
&vec![
vec![1, 0],
vec![0, 0]
],
&input.rows
);
}
}