diff --git a/benches/example.rs b/benches/example.rs index f606323..3ba76c9 100644 --- a/benches/example.rs +++ b/benches/example.rs @@ -43,3 +43,34 @@ mod eager { } } } + +mod lazy { + #[divan::bench(max_time = std::time::Duration::from_secs(30))] + fn no_entities_mirage() { + let raw_bytes = include_bytes!("../testfiles/mirage.dem"); + + let container = csdemo::Container::parse(divan::black_box(raw_bytes.as_slice())).unwrap(); + + let demo = csdemo::lazyparser::LazyParser::new(container); + + for event in demo.events() { + divan::black_box(event); + } + } + + #[divan::bench(max_time = std::time::Duration::from_secs(30))] + fn entities_mirage() { + let raw_bytes = include_bytes!("../testfiles/mirage.dem"); + + let container = csdemo::Container::parse(divan::black_box(raw_bytes.as_slice())).unwrap(); + + let demo = csdemo::lazyparser::LazyParser::new(container); + + for event in demo.events() { + divan::black_box(event); + } + for entity in demo.entities() { + divan::black_box(entity); + } + } +} diff --git a/examples/lazy-ancient-entity.rs b/examples/lazy-ancient-entity.rs new file mode 100644 index 0000000..cbef93e --- /dev/null +++ b/examples/lazy-ancient-entity.rs @@ -0,0 +1,25 @@ +const DATA: &[u8] = include_bytes!("../testfiles/de_ancient.dem"); + +fn main() { + let container = csdemo::Container::parse(DATA).unwrap(); + + let demo = csdemo::lazyparser::LazyParser::new(container); + + for entity in demo.entities() { + core::hint::black_box(entity); + } + + /* + for tick in output.entity_states.ticks.iter() { + for state in tick.states.iter() { + if state.class.as_str() != "CCSPlayerPawn" { + continue; + } + + for prop in state.props.iter() { + println!("{:?} = {:?}", prop.prop_info.prop_name, prop.value); + } + } + } + */ +} diff --git a/src/game_event.rs b/src/game_event.rs index 884fde4..976af37 100644 --- a/src/game_event.rs +++ b/src/game_event.rs @@ -2,7 +2,7 @@ use crate::{csgo_proto, RawValue, UserId}; macro_rules! define_event { ($name:ident, $target:path $(, ($field:ident, $field_ty:ty))*) => { - #[derive(Debug)] + #[derive(Debug, PartialEq)] #[allow(dead_code)] pub struct $name { $(pub $field: Option<$field_ty>,)* @@ -231,7 +231,7 @@ type ParseFn = fn( event: csgo_proto::CMsgSource1LegacyGameEvent, ) -> Result; -#[derive(Debug)] +#[derive(Debug, PartialEq)] #[allow(dead_code)] pub enum GameEvent { HltvVersionInfo(HltvVersionInfo), diff --git a/src/lazyparser.rs b/src/lazyparser.rs new file mode 100644 index 0000000..253e3ca --- /dev/null +++ b/src/lazyparser.rs @@ -0,0 +1,53 @@ +use crate::{Container, FrameIterator}; + +use std::collections::VecDeque; + +mod events; +pub use events::LazyEventIterator; + +mod entities; +pub use entities::LazyEntityIterator; + +pub struct LazyParser<'b> { + container: Container<'b>, +} + +impl<'b> LazyParser<'b> { + pub fn new(container: Container<'b>) -> Self { + Self { container } + } + + pub fn file_header(&self) -> Option { + let mut buffer = Vec::new(); + + for frame in FrameIterator::parse(self.container.inner) { + if let crate::DemoCommand::FileHeader = frame.cmd { + let data = frame.decompress_with_buf(&mut buffer).ok()?; + let raw: crate::csgo_proto::CDemoFileHeader = prost::Message::decode(data).ok()?; + return Some(raw); + } + } + None + } + + pub fn file_info(&self) -> Option { + let mut buffer = Vec::new(); + + for frame in FrameIterator::parse(self.container.inner) { + if let crate::DemoCommand::FileInfo = frame.cmd { + let data = frame.decompress_with_buf(&mut buffer).ok()?; + let raw: crate::csgo_proto::CDemoFileInfo = prost::Message::decode(data).ok()?; + return Some(raw); + } + } + None + } + + pub fn events(&self) -> LazyEventIterator<'b> { + LazyEventIterator::new(self) + } + + pub fn entities(&self) -> LazyEntityIterator<'b> { + LazyEntityIterator::new(self) + } +} diff --git a/src/lazyparser/entities.rs b/src/lazyparser/entities.rs new file mode 100644 index 0000000..e0c5068 --- /dev/null +++ b/src/lazyparser/entities.rs @@ -0,0 +1,345 @@ +use crate::{ + parser::{ + decoder, entities, propcontroller, sendtables, update_entity, Class, FirstPassError, Paths, + }, + DemoCommand, FrameIterator, +}; + +use std::collections::VecDeque; + +pub struct LazyEntityIterator<'b> { + buffer: Vec, + frames: FrameIterator<'b>, + + current_tick: u32, + pending_entities: VecDeque<(u32, entities::EntityState)>, + + paths: Paths, + baselines: std::collections::HashMap>, + serializers: std::collections::HashMap, + qf_mapper: decoder::QfMapper, + prop_controller: propcontroller::PropController, + entity_ctx: entities::EntityContext, +} + +impl<'b> LazyEntityIterator<'b> { + pub(crate) fn new(parser: &super::LazyParser<'b>) -> Self { + Self { + buffer: Vec::new(), + frames: FrameIterator::parse(parser.container.inner), + + current_tick: 0, + pending_entities: VecDeque::with_capacity(64), + + paths: Paths::new(), + baselines: std::collections::HashMap::new(), + serializers: std::collections::HashMap::new(), + qf_mapper: decoder::QfMapper { + idx: 0, + map: std::collections::HashMap::new(), + }, + prop_controller: propcontroller::PropController::new(), + entity_ctx: entities::EntityContext { + entities: std::collections::HashMap::new(), + cls_to_class: std::collections::HashMap::new(), + filter: entities::EntityFilter::all(), + }, + } + } +} + +impl<'b> LazyEntityIterator<'b> { + fn inner_parse_packet( + raw: &crate::csgo_proto::CDemoPacket, + entity_ctx: &mut entities::EntityContext, + paths: &mut Paths, + qf_mapper: &mut decoder::QfMapper, + baselines: &mut std::collections::HashMap>, + prop_controller: &propcontroller::PropController, + entity_states: &mut VecDeque<(u32, entities::EntityState)>, + current_tick: &mut u32, + ) -> Result<(), FirstPassError> { + let mut bitreader = crate::bitreader::Bitreader::new(raw.data()); + + let mut msg_bytes = Vec::new(); + + while bitreader.bits_remaining().unwrap_or(0) > 8 { + let msg_type = bitreader.read_u_bit_var()?; + let size = bitreader.read_varint()?; + msg_bytes.clear(); + msg_bytes.resize(size as usize, 0); + bitreader.read_n_bytes_mut(size as usize, &mut msg_bytes)?; + + assert_eq!(msg_bytes.len(), size as usize); + + let net_msg_type = + match crate::netmessagetypes::NetmessageType::try_from(msg_type as i32) { + Ok(v) => v, + Err(e) => { + dbg!(e); + continue; + } + }; + + match net_msg_type { + crate::netmessagetypes::NetmessageType::net_Tick => { + let raw: crate::csgo_proto::CnetMsgTick = + prost::Message::decode(msg_bytes.as_slice())?; + + assert!( + *current_tick <= raw.tick(), + "Current Tick {} <= Tick Packet {}", + *current_tick, + raw.tick() + ); + if raw.tick() > *current_tick { + *current_tick = raw.tick(); + } + } + // TODO + // How to handle these things? + crate::netmessagetypes::NetmessageType::svc_ClearAllStringTables => {} + crate::netmessagetypes::NetmessageType::svc_CreateStringTable => {} + crate::netmessagetypes::NetmessageType::svc_UpdateStringTable => {} + crate::netmessagetypes::NetmessageType::svc_PacketEntities => { + let raw: crate::csgo_proto::CsvcMsgPacketEntities = + prost::Message::decode(msg_bytes.as_slice())?; + + if entity_ctx.filter.enabled { + let mut bitreader = crate::bitreader::Bitreader::new(raw.entity_data()); + let mut entity_id: i32 = -1; + for _ in 0..raw.updated_entries() { + entity_id += 1 + (bitreader.read_u_bit_var()? as i32); + + match bitreader.read_nbits(2)? { + 0b01 | 0b11 => { + entity_ctx.entities.remove(&entity_id); + } + 0b10 => { + let cls = + entity_ctx.create_entity(entity_id, &mut bitreader)?; + + if let Some(baseline_bytes) = baselines.get(&cls) { + let mut br = + crate::bitreader::Bitreader::new(baseline_bytes); + + // TODO + // How should we handle is this? + let _state = update_entity( + entity_id, + &mut br, + entity_ctx, + paths, + qf_mapper, + prop_controller, + )?; + } + + let state = update_entity( + entity_id, + &mut bitreader, + entity_ctx, + paths, + qf_mapper, + prop_controller, + )?; + if let Some(state) = state { + entity_states.push_back((*current_tick, state)); + } + } + 0b00 => { + if raw.has_pvs_vis_bits() > 0 + && bitreader.read_nbits(2)? & 0x01 == 1 + { + continue; + } + + let state = update_entity( + entity_id, + &mut bitreader, + entity_ctx, + paths, + qf_mapper, + prop_controller, + )?; + if let Some(state) = state { + entity_states.push_back((*current_tick, state)); + } + } + unknown => { + panic!("{:?}", unknown); + } + }; + } + } + } + _ => { + // dbg!(unknown); + } + }; + } + + Ok(()) + } +} + +impl<'b> Iterator for LazyEntityIterator<'b> { + type Item = Result<(u32, crate::parser::entities::EntityState), ()>; + + fn next(&mut self) -> Option { + if let Some(tmp) = self.pending_entities.pop_front() { + return Some(Ok(tmp)); + } + + while let Some(frame) = self.frames.next() { + match frame.cmd { + DemoCommand::SignonPacket | DemoCommand::Packet => { + let data = match frame.decompress_with_buf(&mut self.buffer) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + let raw: crate::csgo_proto::CDemoPacket = match prost::Message::decode(data) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + + if let Err(e) = Self::inner_parse_packet( + &raw, + &mut self.entity_ctx, + &mut self.paths, + &mut self.qf_mapper, + &mut self.baselines, + &mut self.prop_controller, + &mut self.pending_entities, + &mut self.current_tick, + ) { + return Some(Err(())); + } + } + DemoCommand::FullPacket => { + let data = match frame.decompress_with_buf(&mut self.buffer) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + let raw: crate::csgo_proto::CDemoFullPacket = match prost::Message::decode(data) + { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + + if let Some(packet) = raw.packet { + if let Err(e) = Self::inner_parse_packet( + &packet, + &mut self.entity_ctx, + &mut self.paths, + &mut self.qf_mapper, + &mut self.baselines, + &mut self.prop_controller, + &mut self.pending_entities, + &mut self.current_tick, + ) { + return Some(Err(())); + } + } + } + + // Handling all the "meta" related stuff + DemoCommand::StringTables => { + let data = match frame.decompress_with_buf(&mut self.buffer) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + let raw: crate::csgo_proto::CDemoStringTables = + match prost::Message::decode(data) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + + for table in raw.tables.into_iter() { + if table.table_name() == "instancebaseline" { + for item in table.items.into_iter() { + let k = item.str().parse::().unwrap_or(u32::MAX); + self.baselines.insert(k, item.data.unwrap_or(Vec::new())); + } + } + } + } + DemoCommand::SendTables => { + let data = match frame.decompress_with_buf(&mut self.buffer) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + let tables: crate::csgo_proto::CDemoSendTables = + match prost::Message::decode(data) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + + let mut bitreader = crate::bitreader::Bitreader::new(tables.data()); + + let n_bytes = match bitreader.read_varint() { + Ok(b) => b, + Err(e) => return Some(Err(())), + }; + let bytes = match bitreader.read_n_bytes(n_bytes as usize) { + Ok(b) => b, + Err(e) => return Some(Err(())), + }; + + let serializer_msg: crate::csgo_proto::CsvcMsgFlattenedSerializer = + match prost::Message::decode(bytes.as_slice()) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + + // std::fs::write("send_table.b", bytes.as_slice()); + + assert!(self.serializers.is_empty()); + self.serializers = match sendtables::get_serializers( + &serializer_msg, + &mut self.qf_mapper, + &mut self.prop_controller, + ) { + Ok(s) => s, + Err(e) => return Some(Err(())), + }; + } + DemoCommand::ClassInfo => { + let data = match frame.decompress_with_buf(&mut self.buffer) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + let raw: crate::csgo_proto::CDemoClassInfo = match prost::Message::decode(data) + { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + + self.entity_ctx.cls_to_class.clear(); + + for class_t in raw.classes { + let cls_id = class_t.class_id(); + let network_name = class_t.network_name(); + + if let Some(ser) = self.serializers.remove(network_name) { + self.entity_ctx.cls_to_class.insert( + cls_id as u32, + Class { + name: network_name.into(), + serializer: ser, + }, + ); + } + } + } + _ => continue, + }; + + if let Some(tmp) = self.pending_entities.pop_front() { + return Some(Ok(tmp)); + } + } + + None + } +} diff --git a/src/lazyparser/events.rs b/src/lazyparser/events.rs new file mode 100644 index 0000000..bc69e21 --- /dev/null +++ b/src/lazyparser/events.rs @@ -0,0 +1,213 @@ +use crate::{parser::GameEventMapping, DemoEvent, FrameIterator}; + +use std::collections::VecDeque; + +pub struct LazyEventIterator<'b> { + pub(super) buffer: Vec, + pub(super) frames: FrameIterator<'b>, + + pub(super) pending_events: VecDeque, + pub(super) event_mapper: GameEventMapping, +} + +impl<'b> LazyEventIterator<'b> { + pub(crate) fn new(parser: &super::LazyParser<'b>) -> Self { + Self { + buffer: Vec::new(), + frames: FrameIterator::parse(parser.container.inner), + + pending_events: VecDeque::with_capacity(64), + event_mapper: crate::parser::GameEventMapping { + mapping: std::collections::HashMap::new(), + }, + } + } +} + +impl<'b> LazyEventIterator<'b> { + fn inner_parse_packet( + raw: &crate::csgo_proto::CDemoPacket, + events: &mut VecDeque, + event_mapper: &mut GameEventMapping, + ) -> Result<(), ()> { + let mut bitreader = crate::bitreader::Bitreader::new(raw.data()); + + while bitreader.bits_remaining().unwrap_or(0) > 8 { + let msg_type = bitreader.read_u_bit_var().map_err(|e| ())?; + let size = bitreader.read_varint().map_err(|e| ())?; + let msg_bytes = bitreader.read_n_bytes(size as usize).map_err(|e| ())?; + + assert_eq!(msg_bytes.len(), size as usize); + + let net_msg_type = + match crate::netmessagetypes::NetmessageType::try_from(msg_type as i32) { + Ok(v) => v, + Err(e) => { + dbg!(e); + continue; + } + }; + + match net_msg_type { + crate::netmessagetypes::NetmessageType::GE_Source1LegacyGameEventList => { + let event_list: crate::csgo_proto::CsvcMsgGameEventList = + prost::Message::decode(msg_bytes.as_slice()).map_err(|e| ())?; + + event_mapper.mapping.clear(); + for event in event_list.descriptors { + event_mapper + .mapping + .insert(event.eventid(), (event.name().to_owned(), event.keys)); + } + } + crate::netmessagetypes::NetmessageType::svc_ServerInfo => { + let raw: crate::csgo_proto::CsvcMsgServerInfo = + prost::Message::decode(msg_bytes.as_slice()).map_err(|e| ())?; + + events.push_back(DemoEvent::ServerInfo(Box::new(raw))); + } + crate::netmessagetypes::NetmessageType::net_Tick => { + let raw: crate::csgo_proto::CnetMsgTick = + prost::Message::decode(msg_bytes.as_slice()).map_err(|e| ())?; + + events.push_back(DemoEvent::Tick(Box::new(raw))); + } + crate::netmessagetypes::NetmessageType::GE_Source1LegacyGameEvent => { + let raw: crate::csgo_proto::CMsgSource1LegacyGameEvent = + prost::Message::decode(msg_bytes.as_slice()).map_err(|e| ())?; + + match event_mapper.mapping.get(&raw.eventid()) { + Some((name, keys)) => { + match crate::game_event::EVENT_PARSERS.get(name) { + Some(parser) => { + let parsed = parser + .parse(keys.as_slice(), raw.clone()) + .map_err(|e| ())?; + + events.push_back(DemoEvent::GameEvent(Box::new(parsed))); + } + None => { + println!("No parser for {:?}", name); + } + }; + } + None => { + println!("Unknown Event - ID: {}", raw.eventid()); + } + }; + } + crate::netmessagetypes::NetmessageType::CS_UM_ServerRankUpdate => { + let raw: crate::csgo_proto::CcsUsrMsgServerRankUpdate = + prost::Message::decode(msg_bytes.as_slice()).map_err(|e| ())?; + + events.push_back(DemoEvent::RankUpdate(Box::new(raw))); + } + crate::netmessagetypes::NetmessageType::CS_UM_ServerRankRevealAll => { + let raw: crate::csgo_proto::CcsUsrMsgServerRankRevealAll = + prost::Message::decode(msg_bytes.as_slice()).map_err(|e| ())?; + + events.push_back(DemoEvent::RankReveal(Box::new(raw))); + } + crate::netmessagetypes::NetmessageType::net_SignonState + | crate::netmessagetypes::NetmessageType::svc_ClearAllStringTables + | crate::netmessagetypes::NetmessageType::svc_CreateStringTable + | crate::netmessagetypes::NetmessageType::svc_UpdateStringTable + | crate::netmessagetypes::NetmessageType::net_SetConVar + | crate::netmessagetypes::NetmessageType::svc_ClassInfo + | crate::netmessagetypes::NetmessageType::svc_VoiceInit + | crate::netmessagetypes::NetmessageType::svc_PacketEntities + | crate::netmessagetypes::NetmessageType::svc_UserCmds + | crate::netmessagetypes::NetmessageType::GE_SosStartSoundEvent + | crate::netmessagetypes::NetmessageType::GE_SosStopSoundEvent + | crate::netmessagetypes::NetmessageType::CS_GE_PlayerAnimationEvent + | crate::netmessagetypes::NetmessageType::CS_GE_RadioIconEvent + | crate::netmessagetypes::NetmessageType::CS_GE_FireBullets + | crate::netmessagetypes::NetmessageType::UM_SayText2 + | crate::netmessagetypes::NetmessageType::CS_UM_XpUpdate + | crate::netmessagetypes::NetmessageType::CS_UM_WeaponSound + | crate::netmessagetypes::NetmessageType::CS_UM_RadioText + | crate::netmessagetypes::NetmessageType::TE_WorldDecal + | crate::netmessagetypes::NetmessageType::TE_EffectDispatch + | crate::netmessagetypes::NetmessageType::CS_UM_EndOfMatchAllPlayersData + | crate::netmessagetypes::NetmessageType::TE_PhysicsProp + | crate::netmessagetypes::NetmessageType::UM_TextMsg + | crate::netmessagetypes::NetmessageType::CS_UM_VoteFailed + | crate::netmessagetypes::NetmessageType::net_SpawnGroup_Load + | crate::netmessagetypes::NetmessageType::CS_UM_MatchEndConditions + | crate::netmessagetypes::NetmessageType::TE_Explosion => {} + _unknown => { + // dbg!(unknown); + } + }; + } + + Ok(()) + } +} + +impl<'b> Iterator for LazyEventIterator<'b> { + type Item = Result; + + fn next(&mut self) -> Option { + use crate::DemoCommand; + + if let Some(event) = self.pending_events.pop_front() { + return Some(Ok(event)); + } + + while let Some(frame) = self.frames.next() { + match frame.cmd { + DemoCommand::SignonPacket | DemoCommand::Packet => { + let data = match frame.decompress_with_buf(&mut self.buffer) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + + let raw: crate::csgo_proto::CDemoPacket = match prost::Message::decode(data) { + Ok(p) => p, + Err(e) => return Some(Err(())), + }; + + if let Err(e) = Self::inner_parse_packet( + &raw, + &mut self.pending_events, + &mut self.event_mapper, + ) { + return Some(Err(())); + } + } + DemoCommand::FullPacket => { + let data = match frame.decompress_with_buf(&mut self.buffer) { + Ok(d) => d, + Err(e) => return Some(Err(())), + }; + + let raw: crate::csgo_proto::CDemoFullPacket = match prost::Message::decode(data) + { + Ok(p) => p, + Err(e) => return Some(Err(())), + }; + + // TODO + + if let Some(packet) = raw.packet { + if let Err(e) = Self::inner_parse_packet( + &packet, + &mut self.pending_events, + &mut self.event_mapper, + ) { + return Some(Err(())); + } + } + } + _ => {} + }; + + if let Some(event) = self.pending_events.pop_front() { + return Some(Ok(event)); + } + } + + None + } +} diff --git a/src/lib.rs b/src/lib.rs index 4d3b623..982ad12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ pub mod game_event; mod values; pub use values::*; +pub mod lazyparser; pub mod parser; pub mod csgo_proto { diff --git a/src/packet.rs b/src/packet.rs index 894dfd7..39dbc9b 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -1,6 +1,6 @@ use crate::csgo_proto; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum DemoEvent { GameEvent(Box), ServerInfo(Box), diff --git a/src/parser.rs b/src/parser.rs index 5e95029..39f44df 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,11 +3,11 @@ use crate::{packet::DemoEvent, DemoCommand, Frame, FrameDecompressError, UserId} mod fieldpath; pub use fieldpath::{FieldPath, Paths}; -mod decoder; +pub(crate) mod decoder; pub mod entities; -mod propcontroller; -mod sendtables; -mod variant; +pub(crate) mod propcontroller; +pub(crate) mod sendtables; +pub(crate) mod variant; pub use entities::EntityFilter; pub use variant::Variant; @@ -95,8 +95,8 @@ pub struct FirstPassOutput { } #[derive(Debug)] -struct GameEventMapping { - mapping: std::collections::HashMap< +pub(crate) struct GameEventMapping { + pub mapping: std::collections::HashMap< i32, ( String, @@ -107,8 +107,8 @@ struct GameEventMapping { #[derive(Debug)] pub struct Class { - name: std::sync::Arc, - serializer: sendtables::Serializer, + pub(crate) name: std::sync::Arc, + pub(crate) serializer: sendtables::Serializer, } pub fn parse<'b, FI>(frames: FI, filter: EntityFilter) -> Result @@ -248,7 +248,7 @@ where } } } - _other => { + _ => { // dbg!(other); } } @@ -501,7 +501,7 @@ fn inner_parse_packet( Ok(()) } -fn update_entity( +pub(crate) fn update_entity( entity_id: i32, bitreader: &mut crate::bitreader::Bitreader, entity_ctx: &mut entities::EntityContext, diff --git a/src/parser/entities.rs b/src/parser/entities.rs index 0b57fc4..f0e3fd4 100644 --- a/src/parser/entities.rs +++ b/src/parser/entities.rs @@ -8,7 +8,7 @@ pub struct EntityContext { pub filter: EntityFilter, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct EntityState { pub id: i32, pub class: Arc, @@ -16,9 +16,8 @@ pub struct EntityState { pub props: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct EntityProp { - pub field_info: super::sendtables::FieldInfo, pub prop_info: super::propcontroller::PropInfo, pub value: super::variant::Variant, } @@ -88,7 +87,6 @@ impl EntityContext { if let Some(fi) = field_info { if let Some(prop_info) = prop_controller.prop_infos.get(&fi.prop_id) { fields.push(EntityProp { - field_info: fi, prop_info: prop_info.clone(), value: result, }); diff --git a/src/parser/propcontroller.rs b/src/parser/propcontroller.rs index cb26c2b..c7f3be6 100644 --- a/src/parser/propcontroller.rs +++ b/src/parser/propcontroller.rs @@ -89,7 +89,7 @@ pub struct PropController { #[derive(Debug, Clone)] pub struct SpecialIDs {} -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct PropInfo { pub id: u32, // pub prop_type: PropType, diff --git a/src/parser/sendtables.rs b/src/parser/sendtables.rs index 257f2fb..4de7ad5 100644 --- a/src/parser/sendtables.rs +++ b/src/parser/sendtables.rs @@ -6,7 +6,7 @@ pub struct Serializer { pub fields: Vec, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct FieldInfo { pub decoder: decoder::Decoder, pub should_parse: bool, diff --git a/src/values.rs b/src/values.rs index 2c50742..9df67be 100644 --- a/src/values.rs +++ b/src/values.rs @@ -1,4 +1,4 @@ -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum RawValue { String(String), F32(f32), diff --git a/tests/lazy.rs b/tests/lazy.rs new file mode 100644 index 0000000..c85e010 --- /dev/null +++ b/tests/lazy.rs @@ -0,0 +1,51 @@ +#[test] +fn cmp_lazy_nonlazy_events() { + let content = std::fs::read("testfiles/mirage.dem").unwrap(); + + let container = csdemo::Container::parse(&content).unwrap(); + let demo = csdemo::parser::parse( + csdemo::FrameIterator::parse(container.inner), + csdemo::parser::EntityFilter::disabled(), + ) + .unwrap(); + + let lazy_demo = + csdemo::lazyparser::LazyParser::new(csdemo::Container::parse(&content).unwrap()); + + for (normal, lazied) in demo + .events + .into_iter() + .zip(lazy_demo.events().filter_map(|e| e.ok())) + { + assert_eq!(normal, lazied); + } +} + +#[test] +fn cmp_lazy_nonlazy_entities() { + let content = std::fs::read("testfiles/mirage.dem").unwrap(); + + let container = csdemo::Container::parse(&content).unwrap(); + let demo = csdemo::parser::parse( + csdemo::FrameIterator::parse(container.inner), + csdemo::parser::EntityFilter::all(), + ) + .unwrap(); + + let lazy_demo = + csdemo::lazyparser::LazyParser::new(csdemo::Container::parse(&content).unwrap()); + + let mut normal_iter = demo + .entity_states + .ticks + .into_iter() + .flat_map(|t| t.states.into_iter().map(move |s| (t.tick, s))); + let mut lazy_iter = lazy_demo.entities().filter_map(|e| e.ok()); + + while let Some(normal) = normal_iter.next() { + let lazy = lazy_iter.next().unwrap(); + + assert_eq!(normal, lazy); + } + assert_eq!(None, lazy_iter.next()); +} diff --git a/tests/parse.rs b/tests/parse.rs index 77297ab..eee8a0a 100644 --- a/tests/parse.rs +++ b/tests/parse.rs @@ -34,8 +34,6 @@ fn mirage_1() { } }; } - - todo!() } #[test] @@ -54,6 +52,4 @@ fn ancient_1() { .unwrap(); assert_eq!("de_ancient", output.header.map_name()); - - todo!() }