From 201cead3f1cd01d41470d9da75df09f8f97ba1cd Mon Sep 17 00:00:00 2001 From: Lol3rrr Date: Fri, 18 Oct 2024 17:37:02 +0200 Subject: [PATCH] Switch to using lazy parser --- Cargo.lock | 2 +- analysis/src/endofgame.rs | 90 +++++------ analysis/src/heatmap.rs | 325 +++++++++++++++++++------------------- analysis/src/perround.rs | 81 +++++----- 4 files changed, 240 insertions(+), 258 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c0f49f..0dbabae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -695,7 +695,7 @@ dependencies = [ [[package]] name = "csdemo" version = "0.1.0" -source = "git+https://github.com/Lol3rrr/csdemo.git#a0eab7c68f14e4e846823d0468ef196cf4c85fa5" +source = "git+https://github.com/Lol3rrr/csdemo.git#4b1cb505667fd024174ba91fa19f9dd7a410fd30" dependencies = [ "bitter", "phf", diff --git a/analysis/src/endofgame.rs b/analysis/src/endofgame.rs index 11ce484..f278cee 100644 --- a/analysis/src/endofgame.rs +++ b/analysis/src/endofgame.rs @@ -33,13 +33,10 @@ pub struct PlayerStats { pub fn parse(buf: &[u8]) -> Result { 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 output = csdemo::lazyparser::LazyParser::new(tmp); - let header = &output.header; + let header = output.file_header().ok_or(())?; + let player_info = output.player_info(); let mut player_stats = std::collections::HashMap::<_, PlayerStats>::new(); let mut pawn_to_player = @@ -47,7 +44,7 @@ pub fn parse(buf: &[u8]) -> Result { let mut track = false; let mut player_life = std::collections::HashMap::<_, u8>::new(); - for event in output.events.iter() { + for event in output.events().filter_map(|e| e.ok()) { match event { csdemo::DemoEvent::GameEvent(gevent) => { match gevent.as_ref() { @@ -84,15 +81,10 @@ pub fn parse(buf: &[u8]) -> Result { track = false; } csdemo::game_event::GameEvent::PlayerDeath(pdeath) if track => { - player_death(pdeath, &output.player_info, &mut player_stats); + player_death(pdeath, &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, - ); + player_hurt(phurt, &player_info, &mut player_stats, &mut player_life); } _ => {} }; @@ -104,55 +96,53 @@ pub fn parse(buf: &[u8]) -> Result { let mut teams = std::collections::HashMap::::new(); let mut entity_to_team = std::collections::HashMap::new(); - for tick_state in output.entity_states.ticks { - for state in tick_state.states { - let team = match csdemo::structured::ccsteam::CCSTeam::try_from(&state) { - Ok(t) => t, - Err(_) => continue, - }; + for (tick, state) in output.entities().filter_map(|e| e.ok()) { + let team = match csdemo::structured::ccsteam::CCSTeam::try_from(&state) { + Ok(t) => t, + Err(_) => continue, + }; - let pawns = team.player_pawns(); - let player_ids = pawns - .into_iter() - .filter_map(|pawn| pawn_to_player.get(&pawn)) - .collect::>(); - if player_ids.is_empty() { - if let Some(team_number) = entity_to_team.get(&team.entity_id()) { - if let Some(score) = team.score() { - if let Some(team_entry) = teams.get_mut(team_number) { - team_entry.end_score = score as usize; - } + let pawns = team.player_pawns(); + let player_ids = pawns + .into_iter() + .filter_map(|pawn| pawn_to_player.get(&pawn)) + .collect::>(); + if player_ids.is_empty() { + if let Some(team_number) = entity_to_team.get(&team.entity_id()) { + if let Some(score) = team.score() { + if let Some(team_entry) = teams.get_mut(team_number) { + team_entry.end_score = score as usize; } } - - continue; } - let team_number = player_ids - .iter() - .filter_map(|p| output.player_info.get(*p).map(|p| p.team)) - .next() - .unwrap(); + continue; + } - entity_to_team.insert(team.entity_id(), team_number); + let team_number = player_ids + .iter() + .filter_map(|p| player_info.get(*p).map(|p| p.team)) + .next() + .unwrap(); - let team_entry = teams.entry(team_number).or_insert_with(|| TeamInfo { - end_score: 0, - start_side: team - .team_name() - .map(|t| t.to_owned()) - .unwrap_or(String::new()), - }); - if let Some(score) = team.score() { - team_entry.end_score = score as usize; - } + entity_to_team.insert(team.entity_id(), team_number); + + let team_entry = teams.entry(team_number).or_insert_with(|| TeamInfo { + end_score: 0, + start_side: team + .team_name() + .map(|t| t.to_owned()) + .unwrap_or(String::new()), + }); + if let Some(score) = team.score() { + team_entry.end_score = score as usize; } } let mut players: Vec<_> = player_stats .into_iter() .filter_map(|(id, stats)| { - let player = output.player_info.get(&id)?; + let player = player_info.get(&id)?; Some(( PlayerInfo { diff --git a/analysis/src/heatmap.rs b/analysis/src/heatmap.rs index 4e16cd3..f515525 100644 --- a/analysis/src/heatmap.rs +++ b/analysis/src/heatmap.rs @@ -79,16 +79,13 @@ impl From for PawnID { pub fn parse(config: &Config, buf: &[u8]) -> Result { 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 output = csdemo::lazyparser::LazyParser::new(tmp); let pawn_ids = { let mut tmp = std::collections::HashMap::::new(); - for event in output.events.iter() { + for event in output.events().filter_map(|e| e.ok()) { let entry = match event { csdemo::DemoEvent::GameEvent(ge) => match ge.as_ref() { csdemo::game_event::GameEvent::PlayerSpawn(pspawn) => { @@ -120,12 +117,12 @@ pub fn parse(config: &Config, buf: &[u8]) -> Result { 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(); + for (tick, state) in output.entities().filter_map(|s| s.ok()) { + let _tracing_guard = tracing::debug_span!("Tick", ?tick).entered(); process_tick( config, - tick_state, + &state, &pawn_ids, &mut teams, &mut player_lifestate, @@ -139,7 +136,7 @@ pub fn parse(config: &Config, buf: &[u8]) -> Result { Ok(HeatMapOutput { player_heatmaps: heatmaps, - player_info: output.player_info, + player_info: output.player_info(), }) } @@ -147,7 +144,7 @@ pub const MAX_COORD: f32 = (1 << 14) as f32; fn process_tick( config: &Config, - tick_state: &csdemo::parser::EntityTickStates, + entity_state: &csdemo::parser::entities::EntityState, pawn_ids: &std::collections::HashMap, teams: &mut std::collections::HashMap, player_lifestate: &mut std::collections::HashMap, @@ -155,161 +152,159 @@ fn process_tick( player_cells: &mut std::collections::HashMap, 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); + if !matches!(entity_state.class.as_ref(), "CCSPlayerPawn" | "CCSTeam") { + return; } + + 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 => return, + }; + + 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()); + } + + return; + } + + let pawn_id = PawnID::from(entity_state.id); + let user_id = match pawn_ids.get(&pawn_id).cloned() { + Some(id) => id, + None => return, + }; + let team = match teams.get(&pawn_id).cloned() { + Some(t) => t, + None => return, + }; + + 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 { + return; + } + + // 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 { diff --git a/analysis/src/perround.rs b/analysis/src/perround.rs index 7538516..fedabd8 100644 --- a/analysis/src/perround.rs +++ b/analysis/src/perround.rs @@ -70,51 +70,48 @@ pub struct PerRound { pub fn parse(buf: &[u8]) -> Result { 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 output = csdemo::lazyparser::LazyParser::new(tmp); + + let player_info = output.player_info(); let mut rounds: Vec = Vec::new(); - for tick in output.entity_states.ticks.iter() { - for state in tick.states.iter() { - let round_start_count = state - .get_prop("CCSGameRulesProxy.CCSGameRules.m_nRoundStartCount") - .map(|v| v.value.as_u32()) - .flatten(); - if let Some(round_start_count) = round_start_count { - if rounds.len() < (round_start_count - 1) as usize { - rounds.push(Round { - winreason: WinReason::StillInProgress, - start: tick.tick, - end: u32::MAX, - events: Vec::new(), - }); - } + for (tick, state) in output.entities().filter_map(|e| e.ok()) { + let round_start_count = state + .get_prop("CCSGameRulesProxy.CCSGameRules.m_nRoundStartCount") + .map(|v| v.value.as_u32()) + .flatten(); + if let Some(round_start_count) = round_start_count { + if rounds.len() < (round_start_count - 1) as usize { + rounds.push(Round { + winreason: WinReason::StillInProgress, + start: tick, + end: u32::MAX, + events: Vec::new(), + }); } + } - let round_end_count = state - .get_prop("CCSGameRulesProxy.CCSGameRules.m_nRoundEndCount") - .map(|v| v.value.as_u32()) - .flatten(); - if let Some(round_end_count) = round_end_count { - if rounds.len() == (round_end_count - 1) as usize { - rounds.last_mut().unwrap().end = tick.tick; - } + let round_end_count = state + .get_prop("CCSGameRulesProxy.CCSGameRules.m_nRoundEndCount") + .map(|v| v.value.as_u32()) + .flatten(); + if let Some(round_end_count) = round_end_count { + if rounds.len() == (round_end_count - 1) as usize { + rounds.last_mut().unwrap().end = tick; } + } - if state.class.as_ref() == "CCSGameRulesProxy" { - let round_win_reason = state - .get_prop("CCSGameRulesProxy.CCSGameRules.m_eRoundWinReason") - .map(|p| p.value.as_i32()) - .flatten() - .map(|v| ROUND_WIN_REASON.get(&v)) - .flatten() - .filter(|r| !matches!(r, WinReason::StillInProgress)); - if let Some(round_win_reason) = round_win_reason { - rounds.last_mut().unwrap().winreason = round_win_reason.clone(); - } + if state.class.as_ref() == "CCSGameRulesProxy" { + let round_win_reason = state + .get_prop("CCSGameRulesProxy.CCSGameRules.m_eRoundWinReason") + .map(|p| p.value.as_i32()) + .flatten() + .map(|v| ROUND_WIN_REASON.get(&v)) + .flatten() + .filter(|r| !matches!(r, WinReason::StillInProgress)); + if let Some(round_win_reason) = round_win_reason { + rounds.last_mut().unwrap().winreason = round_win_reason.clone(); } } } @@ -123,7 +120,7 @@ pub fn parse(buf: &[u8]) -> Result { let mut current_tick = 0; let mut current_round = rounds_iter.next().unwrap(); - 'events: for event in output.events.iter() { + 'events: for event in output.events().filter_map(|e| e.ok()) { match event { csdemo::DemoEvent::Tick(tick) => { current_tick = tick.tick(); @@ -154,8 +151,8 @@ pub fn parse(buf: &[u8]) -> Result { None => died.clone(), }; - let died_player = output.player_info.get(&died).unwrap(); - let attacker_player = output.player_info.get(&attacker).unwrap(); + let died_player = player_info.get(&died).unwrap(); + let attacker_player = player_info.get(&attacker).unwrap(); RoundEvent::Kill { attacker: attacker_player.xuid,