Add basic analysis to generate heatmap
This commit is contained in:
716
Cargo.lock
generated
716
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,9 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" }
|
csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" }
|
||||||
|
tracing = { version = "0.1.4" }
|
||||||
|
image = { version = "0.25" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { version = "1.4" }
|
pretty_assertions = { version = "1.4" }
|
||||||
|
tracing-test = { version = "0.2", features = ["no-env-filter"] }
|
||||||
|
|||||||
264
analysis/src/heatmap.rs
Normal file
264
analysis/src/heatmap.rs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
pub struct Config {
|
||||||
|
pub cell_size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HeatMap {
|
||||||
|
max_x: usize,
|
||||||
|
max_y: usize,
|
||||||
|
max_value: usize,
|
||||||
|
rows: Vec<Vec<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeatMap {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
max_x: 0,
|
||||||
|
max_y: 0,
|
||||||
|
max_value: 0,
|
||||||
|
rows: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(config: &Config, buf: &[u8]) -> Result<std::collections::HashMap<csdemo::UserId, HeatMap>, ()> {
|
||||||
|
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: std::collections::HashMap<_, _> = output
|
||||||
|
.events
|
||||||
|
.iter()
|
||||||
|
.filter_map(|event| match event {
|
||||||
|
csdemo::DemoEvent::GameEvent(ge) => match ge.as_ref() {
|
||||||
|
csdemo::game_event::GameEvent::PlayerSpawn(pspawn) => match pspawn.userid_pawn {
|
||||||
|
Some(csdemo::RawValue::I32(v)) => Some((v, pspawn.userid.unwrap())),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut entity_id_to_user = std::collections::HashMap::<i32, csdemo::UserId>::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,
|
||||||
|
&mut entity_id_to_user,
|
||||||
|
&pawn_ids,
|
||||||
|
&mut player_lifestate,
|
||||||
|
&mut player_position,
|
||||||
|
&mut player_cells,
|
||||||
|
&mut heatmaps
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(heatmaps)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_tick(
|
||||||
|
config: &Config,
|
||||||
|
tick_state: &csdemo::parser::EntityTickStates,
|
||||||
|
entity_id_to_user: &mut std::collections::HashMap<i32, csdemo::UserId>,
|
||||||
|
pawn_ids: &std::collections::HashMap<i32, csdemo::UserId>,
|
||||||
|
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, HeatMap>,
|
||||||
|
) {
|
||||||
|
for entity_state in tick_state
|
||||||
|
.states
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.class == "CCSPlayerPawn")
|
||||||
|
{
|
||||||
|
let user_id_entry = entity_id_to_user.entry(entity_state.id);
|
||||||
|
let user_id = match user_id_entry {
|
||||||
|
std::collections::hash_map::Entry::Occupied(v) => v.into_mut(),
|
||||||
|
std::collections::hash_map::Entry::Vacant(v) => {
|
||||||
|
let pawn_id_prop: Option<i32> = entity_state.props.iter().find_map(|prop| {
|
||||||
|
if prop.prop_info.prop_name.as_ref() != "CCSPlayerPawn.m_nEntityId" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pawn_id: i32 = match &prop.value {
|
||||||
|
csdemo::parser::Variant::U32(v) => *v as i32,
|
||||||
|
other => panic!("Unexpected Variant: {:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(pawn_id)
|
||||||
|
});
|
||||||
|
|
||||||
|
let user_id: Option<csdemo::UserId> = pawn_id_prop
|
||||||
|
.map(|pawn_id| pawn_ids.get(&pawn_id).cloned())
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
match user_id {
|
||||||
|
Some(user_id) => v.insert(user_id),
|
||||||
|
None => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _inner_guard =
|
||||||
|
tracing::trace_span!("Entity", ?user_id, 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);
|
||||||
|
|
||||||
|
const MAX_COORD: f32 = (1 << 14) as f32;
|
||||||
|
|
||||||
|
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()).or_insert(HeatMap::new());
|
||||||
|
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 as_image(&self) -> image::RgbImage {
|
||||||
|
let mut buffer = image::RgbImage::new(self.max_x as u32 + 1, self.max_y as u32 + 1);
|
||||||
|
|
||||||
|
tracing::trace!("Creating Image with Dimensions: {}x{}", buffer.width(), buffer.height());
|
||||||
|
|
||||||
|
for (y, row) in self.rows.iter().enumerate() {
|
||||||
|
for (x, cell) in row.iter().enumerate() {
|
||||||
|
buffer.put_pixel(x as u32, y as u32, image::Rgb([*cell as u8, 0, 0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shrink(&mut self) {
|
||||||
|
let min_x = self.rows.iter().filter_map(|row| row.iter().enumerate().filter(|(_, v)| **v != 0).map(|(i, _)| i).next()).min().unwrap_or(0);
|
||||||
|
let min_y = self.rows.iter().enumerate().filter(|(y, row)| row.iter().any(|v| *v != 0)).map(|(i, _)| i).min().unwrap_or(0);
|
||||||
|
|
||||||
|
tracing::trace!("Truncate to Min-X: {} - Min-Y: {}", min_x, min_y);
|
||||||
|
|
||||||
|
let _ = self.rows.drain(0..min_y);
|
||||||
|
for row in self.rows.iter_mut() {
|
||||||
|
let _ = row.drain(0..min_x);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
self.max_y = self.rows.len();
|
||||||
|
self.max_x = self.rows.iter().map(|r| r.len()).max().unwrap_or(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
pub mod endofgame;
|
pub mod endofgame;
|
||||||
|
pub mod heatmap;
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ use analysis::endofgame;
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nuke() {
|
fn endofgame_nuke() {
|
||||||
let input_bytes = include_bytes!("../../testfiles/nuke.dem");
|
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../testfiles/nuke.dem");
|
||||||
|
dbg!(path);
|
||||||
|
let input_bytes = std::fs::read(path).unwrap();
|
||||||
|
|
||||||
let result = endofgame::parse(input_bytes).unwrap();
|
let result = endofgame::parse(&input_bytes).unwrap();
|
||||||
|
|
||||||
let expected = endofgame::EndOfGame {
|
let expected = endofgame::EndOfGame {
|
||||||
map: "de_nuke".to_owned(),
|
map: "de_nuke".to_owned(),
|
||||||
|
|||||||
22
analysis/tests/heatmap.rs
Normal file
22
analysis/tests/heatmap.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use analysis::heatmap;
|
||||||
|
use tracing_test::traced_test;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[traced_test]
|
||||||
|
fn heatmap_nuke() {
|
||||||
|
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../testfiles/nuke.dem");
|
||||||
|
dbg!(path);
|
||||||
|
let input_bytes = std::fs::read(path).unwrap();
|
||||||
|
|
||||||
|
let config = heatmap::Config { cell_size: 25.0 };
|
||||||
|
let result = heatmap::parse(&config, &input_bytes).unwrap();
|
||||||
|
|
||||||
|
for (user, mut heatmap) in result {
|
||||||
|
heatmap.shrink();
|
||||||
|
|
||||||
|
let image = heatmap.as_image();
|
||||||
|
image.save_with_format(format!("./heatmap_nuke-{}.png", user.0), image::ImageFormat::Png).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user