From 3083f836cbae2defeba8881e13afad22a76a2522 Mon Sep 17 00:00:00 2001 From: Andrei Benea Date: Fri, 9 Feb 2024 20:13:30 +0100 Subject: [PATCH] Move the CS:GO code from csdemoparser into the csgo_demo crate. Also parse the header in both csgo_demo and cs2_demo. --- Cargo.lock | 11 - Cargo.toml | 2 - cs2-demo/src/demo_command.rs | 7 +- cs2-demo/src/error.rs | 4 +- cs2-demo/src/lib.rs | 2 +- cs2-demo/src/visit.rs | 4 +- csdemoparser/Cargo.toml | 4 +- csdemoparser/src/cs2.rs | 4 +- csdemoparser/src/csgo.rs | 30 +- csdemoparser/src/demoinfo.rs | 2 +- csdemoparser/src/last_jump.rs | 2 +- csdemoparser/src/lib.rs | 35 +-- csdemoparser/src/string_table.rs | 216 -------------- csgo-demo/Cargo.toml | 3 +- csgo-demo/src/console_command.rs | 5 +- {csdemoparser => csgo-demo}/src/entity.rs | 41 ++- .../src/entity/serverclass.rs | 72 +++-- csgo-demo/src/error.rs | 13 +- csgo-demo/src/header.rs | 20 +- csgo-demo/src/lib.rs | 28 +- {demo-format => csgo-demo}/src/read.rs | 0 csgo-demo/src/read_to_terminator.rs | 3 +- csgo-demo/src/string_table.rs | 263 ++++++++++++++++-- demo-format/Cargo.toml | 9 - demo-format/src/lib.rs | 4 - parsetest/Cargo.toml | 1 - 26 files changed, 388 insertions(+), 397 deletions(-) delete mode 100644 csdemoparser/src/string_table.rs rename {csdemoparser => csgo-demo}/src/entity.rs (84%) rename {csdemoparser => csgo-demo}/src/entity/serverclass.rs (92%) rename {demo-format => csgo-demo}/src/read.rs (100%) delete mode 100644 demo-format/Cargo.toml delete mode 100644 demo-format/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b015575..009647e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,11 +271,9 @@ version = "0.1.0" dependencies = [ "anyhow", "assert-json-diff", - "bitstream-io", "byteorder", "cs2-demo", "csgo-demo", - "demo-format", "protobuf", "serde", "serde_json", @@ -290,7 +288,6 @@ version = "0.0.0" dependencies = [ "bitstream-io", "byteorder", - "demo-format", "getset", "paste", "protobuf", @@ -299,13 +296,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "demo-format" -version = "0.1.0" -dependencies = [ - "bitstream-io", -] - [[package]] name = "either" version = "1.9.0" @@ -512,7 +502,6 @@ dependencies = [ "console", "glob", "indicatif", - "once_cell", "rayon", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 1bf38fa..c4a86a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,8 @@ members = [ "csgo-demo", "cs2-demo", "csdemoparser", - "demo-format", "parsetest", ] [workspace.dependencies] bitstream-io = "1.7" -byteorder = "1.4" diff --git a/cs2-demo/src/demo_command.rs b/cs2-demo/src/demo_command.rs index da1277a..a2ece64 100644 --- a/cs2-demo/src/demo_command.rs +++ b/cs2-demo/src/demo_command.rs @@ -80,7 +80,12 @@ pub struct DemoParser<'a> { } impl<'a> DemoParser<'a> { - pub fn try_new_after_demo_type(read: &'a mut dyn std::io::Read) -> Result { + pub fn try_new(read: &'a mut dyn std::io::Read) -> Result { + let mut demo_type = [0; 8]; + read.read_exact(&mut demo_type)?; + if &demo_type != b"PBDEMS2\0" { + return Err(Error::InvalidDemoType(Box::new(demo_type))); + } let mut reader = CodedInputStream::new(read); reader.skip_raw_bytes(8)?; Ok(Self { reader }) diff --git a/cs2-demo/src/error.rs b/cs2-demo/src/error.rs index 73562af..3980465 100644 --- a/cs2-demo/src/error.rs +++ b/cs2-demo/src/error.rs @@ -5,8 +5,8 @@ pub enum Error { Io(#[from] std::io::Error), #[error(transparent)] Protobuf(#[from] protobuf::Error), - #[error("invalid demo type (expected: PBDEMS2, found: {found})")] - InvalidDemoType { found: String }, + #[error("invalid demo type (expected: PBDEMS2, found: {0:?})")] + InvalidDemoType(Box<[u8]>), #[error("unknown packet command found: {0}")] UnknownPacketCommand(u32), #[error(transparent)] diff --git a/cs2-demo/src/lib.rs b/cs2-demo/src/lib.rs index 5653d0e..8255b50 100644 --- a/cs2-demo/src/lib.rs +++ b/cs2-demo/src/lib.rs @@ -14,7 +14,7 @@ mod visit; pub use crate::error::{Error, Result}; pub use crate::game_event::GameEventDescriptors; pub use crate::string_table::{PlayerInfo, UserInfo}; -pub use crate::visit::{parse_after_demo_type, Visitor}; +pub use crate::visit::{parse, Visitor}; pub type Tick = i32; type BitReader<'a> = bitstream_io::BitReader<&'a [u8], bitstream_io::LittleEndian>; diff --git a/cs2-demo/src/visit.rs b/cs2-demo/src/visit.rs index 8884493..610456e 100644 --- a/cs2-demo/src/visit.rs +++ b/cs2-demo/src/visit.rs @@ -42,8 +42,8 @@ pub trait Visitor { } } -pub fn parse_after_demo_type(read: &mut dyn Read, visitor: &mut dyn Visitor) -> Result<()> { - DemoVisit::new(DemoParser::try_new_after_demo_type(read)?, visitor).parse() +pub fn parse(read: &mut dyn Read, visitor: &mut dyn Visitor) -> Result<()> { + DemoVisit::new(DemoParser::try_new(read)?, visitor).parse() } struct DemoVisit<'a> { diff --git a/csdemoparser/Cargo.toml b/csdemoparser/Cargo.toml index 9e58a33..1a586a9 100644 --- a/csdemoparser/Cargo.toml +++ b/csdemoparser/Cargo.toml @@ -10,11 +10,9 @@ tracing = ["dep:tracing-subscriber"] [dependencies] anyhow = { version = "1.0", features = ["backtrace"] } -bitstream-io.workspace = true -byteorder.workspace = true +byteorder = "1.4" csgo-demo = { path = "../csgo-demo" } cs2-demo = { path = "../cs2-demo" } -demo-format = { path = "../demo-format" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" diff --git a/csdemoparser/src/cs2.rs b/csdemoparser/src/cs2.rs index 9a80afa..b3c0f75 100644 --- a/csdemoparser/src/cs2.rs +++ b/csdemoparser/src/cs2.rs @@ -5,18 +5,18 @@ use crate::demoinfo::{ use crate::game_event::GameEvent; use crate::last_jump::LastJump; +use crate::Tick; use crate::{DemoInfo, Slot, UserId}; use cs2_demo::entity::Entities; use cs2_demo::proto::demo::CDemoFileHeader; use cs2_demo::proto::gameevents::CMsgSource1LegacyGameEvent; use cs2_demo::{GameEventDescriptors, UserInfo, Visitor}; -use demo_format::Tick; use std::collections::{hash_map, HashMap}; use tracing::{instrument, trace}; pub fn parse(read: &mut dyn std::io::Read) -> anyhow::Result { let mut state = GameState::new(); - cs2_demo::parse_after_demo_type(read, &mut state)?; + cs2_demo::parse(read, &mut state)?; state.get_info() } diff --git a/csdemoparser/src/csgo.rs b/csdemoparser/src/csgo.rs index 2d84086..a040b32 100644 --- a/csdemoparser/src/csgo.rs +++ b/csdemoparser/src/csgo.rs @@ -1,17 +1,14 @@ mod game_event; -use crate::entity::{Entities, Entity, EntityId, PropValue, Scalar}; -use crate::entity::{ServerClasses, TrackProp}; +use csgo_demo::entity::{Entities, Entity, EntityId, PropValue, Scalar, ServerClasses, TrackProp}; use crate::geometry::{through_smoke, Point}; use crate::last_jump::LastJump; -use crate::string_table::{self, PlayerInfo, StringTables}; use crate::{account_id_to_xuid, guid_to_xuid, maybe_get_i32, maybe_get_u16, DemoInfo, TeamScore}; use anyhow::bail; use csgo_demo::proto::netmessages::CSVCMsg_GameEvent; -use csgo_demo::Message; -use csgo_demo::PacketContent; -use csgo_demo::StringTable; -use demo_format::Tick; +use csgo_demo::{Message, PacketContent}; +use csgo_demo::string_table::{parse_player_infos, PlayerInfo, StringTable, StringTables}; +use crate::Tick; use serde_json::json; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; @@ -32,7 +29,7 @@ const GAME_RESTART: &str = "m_bGameRestart"; const TEAM_CLASS: &str = "CCSTeam"; pub fn parse(read: &mut dyn io::Read) -> anyhow::Result { - let mut parser = csgo_demo::DemoParser::try_new_after_demo_type(read)?; + let mut parser = csgo_demo::DemoParser::try_new(read)?; let server_name = parser.header().server_name().to_string(); let mut server_classes = None; let mut packets = vec![]; @@ -191,13 +188,8 @@ impl<'a> HeadshotBoxParser<'a> { fn handle_string_tables(&mut self, st: Vec) -> anyhow::Result<()> { // demoinfogo clears the players but I don't think this is correct self.players.clear(); - for st in st.iter().filter(|st| st.name() == "userinfo") { - for (entity_id, string) in st.strings().iter().enumerate() { - if let Some(data) = string.data() { - let player_info = string_table::parse_player_info(data, entity_id as i32)?; - Self::update_players(&mut self.players, &self.demoinfo, player_info); - } - } + for player_info in parse_player_infos(st)? { + Self::update_players(&mut self.players, &self.demoinfo, player_info); } Ok(()) } @@ -211,17 +203,19 @@ impl<'a> HeadshotBoxParser<'a> { } Message::CreateStringTable(table) => { let mut updates = self.string_tables.create_string_table(&table); - while let Some(player_info) = updates.next()? { + while let Some(player_info) = updates.next_player_info()? { Self::update_players(&mut self.players, &self.demoinfo, player_info); } } Message::UpdateStringTable(table) => { let mut updates = self.string_tables.update_string_table(&table)?; - while let Some(player_info) = updates.next()? { + while let Some(player_info) = updates.next_player_info()? { Self::update_players(&mut self.players, &self.demoinfo, player_info); } } - Message::GameEventList(gel) => self.game_event_descriptors = self::game_event::parse_game_event_list(gel), + Message::GameEventList(gel) => { + self.game_event_descriptors = self::game_event::parse_game_event_list(gel) + } Message::GameEvent(event) => { if let Some(descriptor) = self.game_event_descriptors.get(&event.eventid()) { let attrs = self.event_map(event, descriptor, tick)?; diff --git a/csdemoparser/src/demoinfo.rs b/csdemoparser/src/demoinfo.rs index 68d019d..59f241c 100644 --- a/csdemoparser/src/demoinfo.rs +++ b/csdemoparser/src/demoinfo.rs @@ -1,4 +1,4 @@ -use demo_format::Tick; +use crate::Tick; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/csdemoparser/src/last_jump.rs b/csdemoparser/src/last_jump.rs index daedaaa..5c3b18f 100644 --- a/csdemoparser/src/last_jump.rs +++ b/csdemoparser/src/last_jump.rs @@ -1,4 +1,4 @@ -use demo_format::Tick; +use crate::Tick; use std::collections::HashMap; #[derive(Default)] diff --git a/csdemoparser/src/lib.rs b/csdemoparser/src/lib.rs index 7785449..8ff70a7 100644 --- a/csdemoparser/src/lib.rs +++ b/csdemoparser/src/lib.rs @@ -1,23 +1,27 @@ mod cs2; mod csgo; pub mod demoinfo; -mod entity; mod game_event; mod geometry; mod last_jump; -mod string_table; -use crate::entity::{Entity, EntityId, PropValue, Scalar}; -use demo_format::read::ReadExt; +use csgo_demo::entity::{Entity, EntityId, PropValue, Scalar}; use demoinfo::DemoInfo; -use std::io; +use std::{ + fs::File, + io::{Read, Seek}, +}; -const SOURCE1_DEMO_TYPE: &str = "HL2DEMO"; -const SOURCE2_DEMO_TYPE: &str = "PBDEMS2"; +type Tick = i32; -pub fn parse(mut read: &mut dyn io::Read) -> anyhow::Result { - let demo_type = read.read_string_limited(8)?; - match demo_type.as_str() { +const SOURCE1_DEMO_TYPE: &[u8; 8] = b"HL2DEMO\0"; +const SOURCE2_DEMO_TYPE: &[u8; 8] = b"PBDEMS2\0"; + +pub fn parse(read: &mut File) -> anyhow::Result { + let mut demo_type = [0; 8]; + read.read_exact(&mut demo_type)?; + read.rewind()?; + match &demo_type { SOURCE1_DEMO_TYPE => csgo::parse(read), SOURCE2_DEMO_TYPE => { if std::env::var("CS2_EXPERIMENTAL_PARSER").is_ok() { @@ -26,7 +30,7 @@ pub fn parse(mut read: &mut dyn io::Read) -> anyhow::Result { panic!("CS2 demo parser is not complete. You can test it by seting the CS2_EXPERIMENTAL_PARSER environment variable.") } } - _ => Err(cs2_demo::Error::InvalidDemoType { found: demo_type }.into()), + _ => Err(cs2_demo::Error::InvalidDemoType(Box::new(demo_type)).into()), } } @@ -94,15 +98,6 @@ fn maybe_get_i32(v: Option<&serde_json::Value>) -> Option { Some(v?.as_i64()? as i32) } -// Number of bits needed to represent values in the 0..=n interval. -fn num_bits(n: u32) -> u32 { - if n == 0 { - 1 - } else { - u32::BITS - n.leading_zeros() - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/csdemoparser/src/string_table.rs b/csdemoparser/src/string_table.rs deleted file mode 100644 index 33b8413..0000000 --- a/csdemoparser/src/string_table.rs +++ /dev/null @@ -1,216 +0,0 @@ -use std::ffi::CStr; -use std::io::{BufRead, Cursor}; - -use anyhow::bail; -use bitstream_io::BitRead; -use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; -use csgo_demo::proto::netmessages::{CSVCMsg_CreateStringTable, CSVCMsg_UpdateStringTable}; -use demo_format::BitReader; - -use crate::num_bits; - -#[derive(Debug, Clone)] -struct StringTableDescriptor { - name: String, - max_entries: u32, - user_data_fixed_size: bool, -} - -pub(crate) struct StringTables { - string_tables: Vec, -} - -impl StringTables { - pub(crate) fn new() -> Self { - Self { - string_tables: Vec::new(), - } - } - - pub(crate) fn create_string_table<'a, 's: 'a>( - &'s mut self, - table: &'a CSVCMsg_CreateStringTable, - ) -> StringTableUpdates<'a> { - self.string_tables.push(StringTableDescriptor { - name: table.name().to_string(), - max_entries: table.max_entries() as u32, - user_data_fixed_size: table.user_data_fixed_size(), - }); - StringTableUpdates::new( - self.string_tables.last().unwrap(), - table.num_entries(), - table.string_data(), - ) - } - - pub(crate) fn update_string_table<'a, 's: 'a>( - &'s mut self, - table: &'a CSVCMsg_UpdateStringTable, - ) -> anyhow::Result> { - if let Some(table_descriptor) = self.string_tables.get(table.table_id() as usize) { - Ok(StringTableUpdates::new( - table_descriptor, - table.num_changed_entries(), - table.string_data(), - )) - } else { - bail!("got bad index for UpdateStringTable") - } - } -} - -#[allow(dead_code)] -#[derive(Debug, Default)] -pub(crate) struct PlayerInfo { - pub version: u64, - pub xuid: u64, - pub name: String, - pub user_id: i32, - pub guid: String, - pub friends_id: i32, - pub friends_name: String, - pub fakeplayer: bool, - pub is_hltv: bool, - pub files_downloaded: u8, - pub entity_id: i32, -} - -pub(crate) struct StringTableUpdates<'a> { - table_descriptor: &'a StringTableDescriptor, - entries: i32, - reader: BitReader<'a>, - entry: i32, - next_entity_id: i32, - entry_bits: u32, -} - -impl<'a> StringTableUpdates<'a> { - fn new(table_descriptor: &'a StringTableDescriptor, entries: i32, data: &'a [u8]) -> Self { - let entry_bits = num_bits(table_descriptor.max_entries - 1); - Self { - table_descriptor, - entries, - reader: BitReader::new(data), - entry: 0, - next_entity_id: 0, - entry_bits, - } - } - - pub(crate) fn next(&mut self) -> anyhow::Result> { - if self.entry >= self.entries { - return Ok(None); - } - if self.entry == 0 { - if self.table_descriptor.name != "userinfo" { - return Ok(None); - } - if self.table_descriptor.user_data_fixed_size { - bail!("userinfo should not be fixed data"); - } - if self.reader.read_bit()? { - bail!("cannot decode string table encoded with dictionaries"); - } - } - let max_entries = self.table_descriptor.max_entries; - while self.entry < self.entries { - let entity_id = if !self.reader.read_bit()? { - self.reader.read::(self.entry_bits)? as i32 - } else { - self.next_entity_id - }; - self.next_entity_id = entity_id + 1; - if entity_id >= max_entries as i32 { - bail!("update_string_table got a bad index"); - } - if self.reader.read_bit()? { - if self.reader.read_bit()? { - bail!("substrings not implemented"); - } else { - // I don't know what this is, ignore the string. - while self.reader.read::(8)? != 0 {} - } - } - if self.reader.read_bit()? { - let num_bytes = self.reader.read::(14)? as usize; - let mut buf = vec![0; num_bytes]; - self.reader.read_bytes(buf.as_mut_slice())?; - let player_info = parse_player_info(&buf, entity_id)?; - self.entry += 1; - return Ok(Some(player_info)); - } else { - self.entry += 1 - } - } - Ok(None) - } -} - -pub(crate) fn parse_player_info(buf: &[u8], entity_id: i32) -> anyhow::Result { - const PLAYER_NAME_LENGTH: usize = 128; - const GUID_LENGTH: usize = 33; - let mut reader = Cursor::new(buf); - let version = reader.read_u64::()?; - let xuid = reader.read_u64::()?; - let name = read_cstring_buffer(&mut reader, PLAYER_NAME_LENGTH)?; - let user_id = reader.read_i32::()?; - let guid = read_cstring_buffer(&mut reader, GUID_LENGTH)?; - // Skip padding. - reader.consume(3); - let friends_id = reader.read_i32::()?; - let friends_name = read_cstring_buffer(&mut reader, PLAYER_NAME_LENGTH)?; - let fakeplayer = reader.read_u8()? != 0; - let is_hltv = reader.read_u8()? != 0; - // Skip padding. - reader.consume(2); - // Ignore custom_files (4 CRC32 values). - reader.consume(4 * std::mem::size_of::()); - let files_downloaded = reader.read_u8()?; - let player_info = PlayerInfo { - version, - xuid, - name, - user_id, - guid, - friends_id, - friends_name, - fakeplayer, - is_hltv, - files_downloaded, - entity_id, - }; - Ok(player_info) -} - -fn read_cstring_buffer(cursor: &mut Cursor<&[u8]>, size: usize) -> anyhow::Result { - let cstr = CStr::from_bytes_until_nul(&cursor.get_ref()[cursor.position() as usize..])?; - cursor.consume(size); - Ok(cstr.to_string_lossy().into_owned()) -} - -#[cfg(test)] -mod tests { - use super::StringTables; - use protobuf::text_format::parse_from_str; - - #[test] - fn userinfo_empty_update() -> anyhow::Result<()> { - let mut st = StringTables::new(); - let create = parse_from_str( - r#"name: "userinfo" - max_entries: 256 - user_data_fixed_size: false - string_data: "\0""#, - )?; - let update = parse_from_str( - r#"table_id: 0 - num_changed_entries: 1 - string_data: "\020\260""#, - )?; - let mut updates = st.create_string_table(&create); - while (updates.next()?).is_some() {} - let mut updates = st.update_string_table(&update)?; - while (updates.next()?).is_some() {} - Ok(()) - } -} diff --git a/csgo-demo/Cargo.toml b/csgo-demo/Cargo.toml index d63771a..2f1cfdd 100644 --- a/csgo-demo/Cargo.toml +++ b/csgo-demo/Cargo.toml @@ -7,8 +7,7 @@ edition = "2021" [dependencies] bitstream-io.workspace = true -byteorder.workspace = true -demo-format = { path = "../demo-format" } +byteorder = "1.4" getset = "0.1" protobuf = { version = "3.2.0", features = ["with-bytes"] } thiserror = "1.0" diff --git a/csgo-demo/src/console_command.rs b/csgo-demo/src/console_command.rs index 43aafbd..8f07be6 100644 --- a/csgo-demo/src/console_command.rs +++ b/csgo-demo/src/console_command.rs @@ -1,7 +1,8 @@ -use crate::Result; -use demo_format::read::ReadExt; use protobuf::CodedInputStream; +use crate::Result; +use crate::read::ReadExt; + #[derive(Debug)] pub(crate) struct ConsoleCommand { pub command: String, diff --git a/csdemoparser/src/entity.rs b/csgo-demo/src/entity.rs similarity index 84% rename from csdemoparser/src/entity.rs rename to csgo-demo/src/entity.rs index 916f4b7..856e317 100644 --- a/csdemoparser/src/entity.rs +++ b/csgo-demo/src/entity.rs @@ -1,17 +1,17 @@ /// https://developer.valvesoftware.com/wiki/Networking_Entities mod serverclass; -use anyhow::{bail, Context}; use bitstream_io::BitRead; -use csgo_demo::proto::netmessages::CSVCMsg_PacketEntities; -use demo_format::read::ValveBitReader; -use demo_format::BitReader; -use demo_format::Tick; use serverclass::ServerClass; -pub(crate) use serverclass::ServerClasses; use std::io; use std::rc::Rc; +use crate::proto::netmessages::CSVCMsg_PacketEntities; +use crate::read::ValveBitReader; +use crate::{BitReader, Error, Result, Tick}; + +pub use serverclass::ServerClasses; + const MAX_ENTITIES: u32 = 2048; type PropChange = Rc; @@ -35,7 +35,7 @@ pub struct Entities<'a> { } impl<'a> Entities<'a> { - pub(crate) fn new(server_classes: &'a ServerClasses) -> Self { + pub fn new(server_classes: &'a ServerClasses) -> Self { Self { server_classes, entities: (0..MAX_ENTITIES).map(|_| None).collect(), @@ -47,18 +47,14 @@ impl<'a> Entities<'a> { self.entities.get(id as usize)?.as_ref() } - pub fn read_packet_entities( - &mut self, - msg: CSVCMsg_PacketEntities, - tick: Tick, - ) -> anyhow::Result<()> { + pub fn read_packet_entities(&mut self, msg: CSVCMsg_PacketEntities, tick: Tick) -> Result<()> { let mut next_entity_id = 0; let mut reader = BitReader::new(msg.entity_data()); for _ in 0..msg.updated_entries() { let entity_id = next_entity_id + reader.read_ubitvar()?; next_entity_id = entity_id + 1; if entity_id >= MAX_ENTITIES { - bail!("invalid entity_id"); + return Err(Error::Entity("invalid entity_id")); } let remove = reader.read_bit()?; let new = reader.read_bit()?; @@ -67,7 +63,7 @@ impl<'a> Entities<'a> { if let Some(entity) = self.entities[entity_id as usize].as_mut() { entity.read_props(&mut reader, &mut self.field_indices, tick)?; } else { - bail!("entity id not found"); + return Err(Error::Entity("entity id not found")); } } (false, true) => { @@ -76,7 +72,7 @@ impl<'a> Entities<'a> { .server_classes .server_classes .get(class_id as usize) - .ok_or_else(|| anyhow::anyhow!("class id not found"))?; + .ok_or_else(|| Error::Entity("class id not found"))?; // Discard serial_num. reader.read::(10)?; let mut entity = Entity::new(entity_id as EntityId, class); @@ -85,10 +81,14 @@ impl<'a> Entities<'a> { } (true, _) => { if !msg.is_delta() { - bail!("Entities should not be deleted in a full update"); + return Err(Error::Entity( + "Entities should not be deleted in a full update", + )); } if self.entities[entity_id as usize].take().is_none() { - bail!("Tried to remove an entity which doesn't exist") + return Err(Error::Entity( + "Tried to remove an entity which doesn't exist", + )); } } }; @@ -104,7 +104,6 @@ pub struct Vector { pub z: f32, } -#[allow(dead_code)] #[derive(Debug)] pub enum Scalar { I32(i32), @@ -159,7 +158,7 @@ impl Entity<'_> { reader: &mut BitReader, field_indices: &mut Vec, tick: Tick, - ) -> anyhow::Result<()> { + ) -> Result<()> { let new_way = reader.read_bit()?; field_indices.clear(); let mut index = -1; @@ -167,7 +166,7 @@ impl Entity<'_> { index = val; field_indices.push(index); if field_indices.len() > 20000 { - bail!("found too many entity field indices, probably corrupt demo") + return Err(Error::Entity("found too many entity field indices, probably corrupt demo")) } } for i in field_indices { @@ -175,7 +174,7 @@ impl Entity<'_> { .class .props .get(*i as usize) - .context("invalid prop index")?; + .ok_or(Error::Entity("invalid prop index"))?; match &descriptor.track { TrackProp::No => descriptor.skip(reader)?, TrackProp::Value => self.props[*i as usize] = Some(descriptor.decode(reader)?), diff --git a/csdemoparser/src/entity/serverclass.rs b/csgo-demo/src/entity/serverclass.rs similarity index 92% rename from csdemoparser/src/entity/serverclass.rs rename to csgo-demo/src/entity/serverclass.rs index d4be0ad..3521745 100644 --- a/csdemoparser/src/entity/serverclass.rs +++ b/csgo-demo/src/entity/serverclass.rs @@ -1,12 +1,10 @@ use crate::entity::{BitReader, PropValue, Scalar, ValveBitReader, Vector}; -use crate::num_bits; -use anyhow::bail; -use bitstream_io::BitRead; -use csgo_demo::proto::netmessages::{ +use crate::proto::netmessages::{ csvcmsg_class_info, csvcmsg_send_table::Sendprop_t, CSVCMsg_SendTable, }; -use csgo_demo::DataTables; -use demo_format::read::CoordType; +use crate::read::CoordType; +use crate::{num_bits, DataTables, Error, Result}; +use bitstream_io::BitRead; use std::collections::HashMap; use std::io; use std::string::FromUtf8Error; @@ -27,13 +25,13 @@ impl ServerClass { } } -pub(crate) struct ServerClasses { - pub(crate) bits: u32, - pub(crate) server_classes: Vec, +pub struct ServerClasses { + pub bits: u32, + pub server_classes: Vec, } impl ServerClasses { - pub(crate) fn try_new(data_tables: DataTables) -> anyhow::Result { + pub fn try_new(data_tables: DataTables) -> Result { let bits = num_bits(data_tables.server_classes().len() as u32); Ok(Self { bits, @@ -128,7 +126,7 @@ struct ArrayPropDescriptor { } impl ArrayPropDescriptor { - fn try_new(sendprop: &Sendprop_t, array_element: &Sendprop_t) -> anyhow::Result { + fn try_new(sendprop: &Sendprop_t, array_element: &Sendprop_t) -> Result { Ok(Self { num_elements: sendprop.num_elements() as u32, elem_type: ScalarPropDescriptor::try_new(array_element)?, @@ -151,14 +149,14 @@ impl PropDescriptor { } } - pub(crate) fn decode(&self, reader: &mut BitReader) -> DecodeResult { + pub(crate) fn decode(&self, reader: &mut BitReader) -> std::io::Result { match &self.type_ { PropDescriptorType::Scalar(s) => Ok(PropValue::Scalar(self.decode_scalar(s, reader)?)), PropDescriptorType::Array(a) => Ok(PropValue::Array(self.decode_array(a, reader)?)), } } - pub(crate) fn skip(&self, reader: &mut BitReader) -> DecodeResult<()> { + pub(crate) fn skip(&self, reader: &mut BitReader) -> std::io::Result<()> { match &self.type_ { PropDescriptorType::Scalar(s) => self.skip_scalar(s, reader), PropDescriptorType::Array(a) => self.skip_array(a, reader), @@ -169,7 +167,7 @@ impl PropDescriptor { &self, type_: &ScalarPropDescriptor, reader: &mut BitReader, - ) -> DecodeResult { + ) -> std::io::Result { match type_ { ScalarPropDescriptor::Int(int) => Ok(Scalar::I32(self.decode_int(int, reader)?)), ScalarPropDescriptor::Float(f) => Ok(Scalar::F32(self.decode_float(f, reader)?)), @@ -188,7 +186,7 @@ impl PropDescriptor { &self, type_: &ScalarPropDescriptor, reader: &mut BitReader, - ) -> DecodeResult<()> { + ) -> std::io::Result<()> { match type_ { ScalarPropDescriptor::Int(int) => Ok(self.skip_int(int, reader)?), ScalarPropDescriptor::Float(f) => Ok(self.skip_float(f, reader)?), @@ -203,7 +201,7 @@ impl PropDescriptor { &self, type_: &ArrayPropDescriptor, reader: &mut BitReader, - ) -> DecodeResult> { + ) -> std::io::Result> { let len_bits = num_bits(type_.num_elements); let len = reader.read::(len_bits)?; let mut array = Vec::with_capacity(len as usize); @@ -213,7 +211,7 @@ impl PropDescriptor { Ok(array) } - fn skip_array(&self, type_: &ArrayPropDescriptor, reader: &mut BitReader) -> DecodeResult<()> { + fn skip_array(&self, type_: &ArrayPropDescriptor, reader: &mut BitReader) -> std::io::Result<()> { let len_bits = num_bits(type_.num_elements); let len = reader.read::(len_bits)?; for _ in 0..len { @@ -350,15 +348,16 @@ impl PropDescriptor { } } - fn decode_string(&self, reader: &mut BitReader) -> DecodeResult { + fn decode_string(&self, reader: &mut BitReader) -> std::io::Result { let len = reader.read::(9)? as usize; let mut buf = vec![0; len]; reader.read_bytes(buf.as_mut_slice())?; - let val = String::from_utf8(buf)?; + let val = String::from_utf8(buf) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; Ok(val) } - fn skip_string(&self, reader: &mut BitReader) -> DecodeResult<()> { + fn skip_string(&self, reader: &mut BitReader) -> std::io::Result<()> { let len = reader.read::(9)?; reader.skip(len * 8)?; Ok(()) @@ -373,8 +372,6 @@ pub enum DecodeError { Protobuf(#[from] FromUtf8Error), } -pub type DecodeResult = std::result::Result; - #[derive(Debug)] enum ScalarPropDescriptor { Int(IntPropDescriptor), @@ -386,7 +383,7 @@ enum ScalarPropDescriptor { } impl ScalarPropDescriptor { - fn try_new(sendprop: &Sendprop_t) -> anyhow::Result { + fn try_new(sendprop: &Sendprop_t) -> Result { Ok(match sendprop.type_() { DPT_INT => Self::Int(IntPropDescriptor::from(sendprop)), DPT_FLOAT => Self::Float(FloatPropDescriptor::from(sendprop)), @@ -397,7 +394,9 @@ impl ScalarPropDescriptor { DPT_VECTOR_XY => Self::VectorXY(FloatPropDescriptor::from(sendprop)), DPT_STRING => Self::String, DPT_INT64 => Self::Int64, - _ => bail!("invalid scalar sendprop type"), + _ => Err(Error::ServerClass( + "invalid scalar sendprop type".to_string(), + ))?, }) } } @@ -416,7 +415,7 @@ impl<'a> ServerClassesParser<'a> { pub(crate) fn parse( send_tables: &'a [CSVCMsg_SendTable], server_classes: &'a [csvcmsg_class_info::Class_t], - ) -> anyhow::Result> { + ) -> Result> { let send_tables: HashMap<&str, &CSVCMsg_SendTable> = send_tables .iter() .map(|st| (st.net_table_name(), st)) @@ -424,20 +423,22 @@ impl<'a> ServerClassesParser<'a> { Self { send_tables }.parse_server_classes(server_classes) } - fn lookup_data_table(&self, name: &str) -> anyhow::Result<&'a CSVCMsg_SendTable> { + fn lookup_data_table(&self, name: &str) -> Result<&'a CSVCMsg_SendTable> { self.send_tables .get(name) .copied() - .ok_or_else(|| anyhow::anyhow!(format!("table name {name} not found"))) + .ok_or_else(|| Error::ServerClass(format!("table name {name} not found"))) } fn parse_server_classes( &self, server_classes: &'a [csvcmsg_class_info::Class_t], - ) -> anyhow::Result> { + ) -> Result> { for (i, sc) in server_classes.iter().enumerate() { if i != sc.class_id() as usize { - bail!("server class id not sequential"); + return Err(Error::ServerClass( + "server class id not sequential".to_string(), + )); } } server_classes @@ -454,10 +455,7 @@ impl<'a> ServerClassesParser<'a> { /// except for excluded properties. See [Valve doc]. /// /// [Valve doc]: https://developer.valvesoftware.com/wiki/Networking_Entities#Network_Data_Tables - fn parse_server_class( - &self, - class_ref: &csvcmsg_class_info::Class_t, - ) -> anyhow::Result { + fn parse_server_class(&self, class_ref: &csvcmsg_class_info::Class_t) -> Result { let table = self.lookup_data_table(class_ref.data_table_name())?; let mut excludes = Vec::new(); self.gather_excludes(table, &mut excludes)?; @@ -475,7 +473,7 @@ impl<'a> ServerClassesParser<'a> { &self, table: &'a CSVCMsg_SendTable, excludes: &mut Vec<(&'a str, &'a str)>, - ) -> anyhow::Result<()> { + ) -> Result<()> { for prop in &table.props { if prop.flags() & SPROP_EXCLUDE != 0 { excludes.push((prop.dt_name(), prop.var_name())) @@ -492,7 +490,7 @@ impl<'a> ServerClassesParser<'a> { table: &CSVCMsg_SendTable, excludes: &Vec<(&str, &str)>, result: &mut Vec<(i32, PropDescriptor)>, - ) -> Result<(), anyhow::Error> { + ) -> Result<()> { let mut tmp = Vec::new(); self.gather_props(table, excludes, &mut tmp, result)?; result.append(&mut tmp); @@ -505,7 +503,7 @@ impl<'a> ServerClassesParser<'a> { excludes: &Vec<(&'a str, &'a str)>, current: &mut Vec<(i32, PropDescriptor)>, result: &mut Vec<(i32, PropDescriptor)>, - ) -> anyhow::Result<()> { + ) -> Result<()> { let mut array_elem = None; for sendprop in &table.props { // sendprop.dt_name() is only set for data tables and exclude props. @@ -524,7 +522,7 @@ impl<'a> ServerClassesParser<'a> { } } else if sendprop.type_() == DPT_ARRAY { let array_elem = array_elem.ok_or_else(|| { - anyhow::anyhow!(format!( + Error::ServerClass(format!( "array sendprop without preceding element: {}.{}", sendprop.dt_name(), sendprop.var_name() diff --git a/csgo-demo/src/error.rs b/csgo-demo/src/error.rs index f84bb44..516c0c1 100644 --- a/csgo-demo/src/error.rs +++ b/csgo-demo/src/error.rs @@ -6,11 +6,8 @@ pub type Result = std::result::Result; #[derive(thiserror::Error, Debug)] #[allow(clippy::enum_variant_names)] pub enum HeaderParsingError { - #[error("invalid demo type (expected: {expected}, found: {found})")] - InvalidDemoType { - expected: &'static str, - found: String, - }, + #[error("invalid demo type (expected: HL2DEMO, found: {0:?})")] + InvalidDemoType(Box<[u8]>), #[error("invalid demo protocol (expected: {expected}, found: {found})")] InvalidDemoProtocol { expected: u32, found: u32 }, #[error("invalid game (expected: {expected}, found: {found})")] @@ -44,4 +41,10 @@ pub enum Error { UnknownPacketCommand(u8), #[error(transparent)] DataTablesParsing(#[from] DataTablesParsingError), + #[error("StringTable error: {0}")] + StringTable(&'static str), + #[error("Entity error: {0}")] + Entity(&'static str), + #[error("ServerClass error: {0}")] + ServerClass(String), } diff --git a/csgo-demo/src/header.rs b/csgo-demo/src/header.rs index 7dc65b8..88aa710 100644 --- a/csgo-demo/src/header.rs +++ b/csgo-demo/src/header.rs @@ -2,12 +2,12 @@ use getset::Getters; use protobuf::CodedInputStream; use crate::error::{HeaderParsingError, Result}; -use demo_format::read::ReadExt; +use crate::read::ReadExt; const MAX_OS_PATH: usize = 260; /// Expected demo type. -const EXPECTED_DEMO_TYPE: &str = "HL2DEMO"; // in UPPERCASE +const EXPECTED_DEMO_TYPE: &[u8; 8] = b"HL2DEMO\0"; /// Expected demo protocol. const EXPECTED_DEMO_PROTOCOL: u32 = 4; /// Expected game name. @@ -17,8 +17,6 @@ const EXPECTED_GAME: &str = "csgo"; // in lowercase #[derive(Getters, Debug)] #[getset(get = "pub")] pub struct DemoHeader { - /// Demo type. Should always be `HL2DEMO`. - demo_type: String, /// Demo protocol version. Should always be `4`. demo_protocol: u32, /// Network protocol version. @@ -43,14 +41,19 @@ pub struct DemoHeader { impl DemoHeader { /// Assumes the demo type has already been read and is valid. - pub(crate) fn try_new_after_demo_type(reader: &mut CodedInputStream) -> Result { + pub(crate) fn try_new(reader: &mut CodedInputStream) -> Result { + let mut demo_type = [std::mem::MaybeUninit::::uninit(); 8]; + reader.read_exact(&mut demo_type)?; + let demo_type = unsafe { std::mem::transmute::<_, [u8; 8]>(demo_type) }; + if &demo_type != EXPECTED_DEMO_TYPE { + Err(HeaderParsingError::InvalidDemoType(Box::new(demo_type)))? + } let demo_protocol = reader.read_fixed32()?; if demo_protocol != EXPECTED_DEMO_PROTOCOL { - return Err(HeaderParsingError::InvalidDemoProtocol { + Err(HeaderParsingError::InvalidDemoProtocol { expected: EXPECTED_DEMO_PROTOCOL, found: demo_protocol, - } - .into()); + })?; } let network_protocol = reader.read_fixed32()?; @@ -68,7 +71,6 @@ impl DemoHeader { } Ok(Self { - demo_type: EXPECTED_DEMO_TYPE.into(), demo_protocol, network_protocol, server_name, diff --git a/csgo-demo/src/lib.rs b/csgo-demo/src/lib.rs index 7c911fb..1b7179b 100644 --- a/csgo-demo/src/lib.rs +++ b/csgo-demo/src/lib.rs @@ -1,19 +1,21 @@ mod command; mod console_command; mod data_table; +pub mod entity; mod error; mod header; mod message; mod packet; pub mod proto; +mod read; mod read_to_terminator; -mod string_table; +pub mod string_table; mod user_command; use crate::command::{Command, PacketHeader}; use crate::console_command::ConsoleCommand; use crate::packet::Packet; -use crate::string_table::StringTables; +use crate::string_table::parse_string_tables; use crate::user_command::UserCommandCompressed; use getset::Getters; use protobuf::CodedInputStream; @@ -25,7 +27,10 @@ pub use data_table::DataTables; pub use error::{Error, Result}; pub use header::DemoHeader; pub use message::Message; -pub use string_table::StringTable; + +pub type Tick = i32; + +type BitReader<'a> = bitstream_io::BitReader<&'a [u8], bitstream_io::LittleEndian>; #[derive(Getters, Debug)] #[getset(get = "pub")] @@ -36,9 +41,9 @@ pub struct DemoParser<'a> { } impl<'a> DemoParser<'a> { - pub fn try_new_after_demo_type(read: &'a mut dyn io::Read) -> Result { + pub fn try_new(read: &'a mut dyn io::Read) -> Result { let mut reader = CodedInputStream::new(read); - let header = DemoHeader::try_new_after_demo_type(&mut reader)?; + let header = DemoHeader::try_new(&mut reader)?; trace!(?header); Ok(Self { header, reader }) @@ -73,8 +78,8 @@ impl<'a> DemoParser<'a> { Some((header, PacketContent::Packet(packet.messages))) } Command::StringTables => { - let string_tables = StringTables::try_new(&mut self.reader)?; - Some((header, PacketContent::StringTables(string_tables.tables))) + let string_tables = parse_string_tables(&mut self.reader)?; + Some((header, PacketContent::StringTables(string_tables))) } Command::DataTables => { let data_tables = DataTables::try_new(&mut self.reader)?; @@ -84,3 +89,12 @@ impl<'a> DemoParser<'a> { }) } } + +// Number of bits needed to represent values in the 0..=n interval. +fn num_bits(n: u32) -> u32 { + if n == 0 { + 1 + } else { + u32::BITS - n.leading_zeros() + } +} diff --git a/demo-format/src/read.rs b/csgo-demo/src/read.rs similarity index 100% rename from demo-format/src/read.rs rename to csgo-demo/src/read.rs diff --git a/csgo-demo/src/read_to_terminator.rs b/csgo-demo/src/read_to_terminator.rs index 7d158ea..ead62dd 100644 --- a/csgo-demo/src/read_to_terminator.rs +++ b/csgo-demo/src/read_to_terminator.rs @@ -1,8 +1,9 @@ use bitstream_io::BitRead; -use demo_format::BitReader; use protobuf::CodedInputStream; use std::io; +use crate::BitReader; + trait ReadByte { fn read_byte(&mut self) -> io::Result; } diff --git a/csgo-demo/src/string_table.rs b/csgo-demo/src/string_table.rs index 075e98a..3de4db8 100644 --- a/csgo-demo/src/string_table.rs +++ b/csgo-demo/src/string_table.rs @@ -1,11 +1,14 @@ use bitstream_io::BitRead; -use demo_format::BitReader; +use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; use getset::Getters; use protobuf::CodedInputStream; +use std::ffi::CStr; +use std::io::{BufRead, Cursor}; use tracing::{instrument, trace}; +use crate::proto::netmessages::{CSVCMsg_CreateStringTable, CSVCMsg_UpdateStringTable}; use crate::read_to_terminator::ReadToTerminator; -use crate::Result; +use crate::{num_bits, BitReader, Error, Result}; /// Strings from [`StringTable`]. #[derive(Getters, Debug)] @@ -90,27 +93,249 @@ impl StringTable { } } -#[derive(Debug)] -pub(crate) struct StringTables { - pub tables: Vec, +pub(crate) fn parse_string_tables(reader: &mut CodedInputStream) -> Result> { + let size = reader.read_fixed32()?; + let data = reader.read_raw_bytes(size)?; + + let mut reader = BitReader::new(data.as_slice()); + let tables_number = reader.read::(8)?; + let mut tables: Vec = Vec::with_capacity(tables_number as usize); + + for _ in 0..tables_number { + let string_table = StringTable::try_new(&mut reader)?; + trace!(?string_table); + tables.push(string_table); + } + + Ok(tables) +} + +#[derive(Debug, Clone)] +struct StringTableDescriptor { + name: String, + max_entries: u32, + user_data_fixed_size: bool, +} + +pub struct StringTables { + string_tables: Vec, } impl StringTables { - #[instrument(level = "trace", skip(reader))] - pub(crate) fn try_new(reader: &mut CodedInputStream) -> Result { - let size = reader.read_fixed32()?; - let data = reader.read_raw_bytes(size)?; - - let mut reader = BitReader::new(data.as_slice()); - let tables_number = reader.read(8)?; - let mut tables: Vec = Vec::with_capacity(tables_number as usize); - - for _ in 0..tables_number { - let string_table = StringTable::try_new(&mut reader)?; - trace!(?string_table); - tables.push(string_table); + pub fn new() -> Self { + Self { + string_tables: Vec::new(), + } + } + + pub fn create_string_table<'a, 's: 'a>( + &'s mut self, + table: &'a CSVCMsg_CreateStringTable, + ) -> StringTableUpdates<'a> { + self.string_tables.push(StringTableDescriptor { + name: table.name().to_string(), + max_entries: table.max_entries() as u32, + user_data_fixed_size: table.user_data_fixed_size(), + }); + StringTableUpdates::new( + self.string_tables.last().unwrap(), + table.num_entries(), + table.string_data(), + ) + } + + pub fn update_string_table<'a, 's: 'a>( + &'s mut self, + table: &'a CSVCMsg_UpdateStringTable, + ) -> Result> { + if let Some(table_descriptor) = self.string_tables.get(table.table_id() as usize) { + Ok(StringTableUpdates::new( + table_descriptor, + table.num_changed_entries(), + table.string_data(), + )) + } else { + Err(Error::StringTable("got bad index for UpdateStringTable")) + } + } +} + +impl Default for StringTables { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Default)] +pub struct PlayerInfo { + pub version: u64, + pub xuid: u64, + pub name: String, + pub user_id: i32, + pub guid: String, + pub friends_id: i32, + pub friends_name: String, + pub fakeplayer: bool, + pub is_hltv: bool, + pub files_downloaded: u8, + pub entity_id: i32, +} + +pub struct StringTableUpdates<'a> { + table_descriptor: &'a StringTableDescriptor, + entries: i32, + reader: BitReader<'a>, + entry: i32, + next_entity_id: i32, + entry_bits: u32, +} + +impl<'a> StringTableUpdates<'a> { + fn new(table_descriptor: &'a StringTableDescriptor, entries: i32, data: &'a [u8]) -> Self { + let entry_bits = num_bits(table_descriptor.max_entries - 1); + Self { + table_descriptor, + entries, + reader: BitReader::new(data), + entry: 0, + next_entity_id: 0, + entry_bits, + } + } + + pub fn next_player_info(&mut self) -> Result> { + if self.entry >= self.entries { + return Ok(None); + } + if self.entry == 0 { + if self.table_descriptor.name != "userinfo" { + return Ok(None); + } + if self.table_descriptor.user_data_fixed_size { + Err(Error::StringTable("userinfo should not be fixed data"))?; + } + if self.reader.read_bit()? { + Err(Error::StringTable( + "cannot decode string table encoded with dictionaries", + ))?; + } + } + let max_entries = self.table_descriptor.max_entries; + while self.entry < self.entries { + let entity_id = if !self.reader.read_bit()? { + self.reader.read::(self.entry_bits)? as i32 + } else { + self.next_entity_id + }; + self.next_entity_id = entity_id + 1; + if entity_id >= max_entries as i32 { + Err(Error::StringTable("update_string_table got a bad index"))?; + } + if self.reader.read_bit()? { + if self.reader.read_bit()? { + Err(Error::StringTable("substrings not implemented"))?; + } else { + // I don't know what this is, ignore the string. + while self.reader.read::(8)? != 0 {} + } + } + if self.reader.read_bit()? { + let num_bytes = self.reader.read::(14)? as usize; + let mut buf = vec![0; num_bytes]; + self.reader.read_bytes(buf.as_mut_slice())?; + let player_info = parse_player_info(&buf, entity_id)?; + self.entry += 1; + return Ok(Some(player_info)); + } else { + self.entry += 1 + } } + Ok(None) + } +} + +fn parse_player_info(buf: &[u8], entity_id: i32) -> Result { + const PLAYER_NAME_LENGTH: usize = 128; + const GUID_LENGTH: usize = 33; + let mut reader = Cursor::new(buf); + let version = reader.read_u64::()?; + let xuid = reader.read_u64::()?; + let name = read_cstring_buffer(&mut reader, PLAYER_NAME_LENGTH)?; + let user_id = reader.read_i32::()?; + let guid = read_cstring_buffer(&mut reader, GUID_LENGTH)?; + // Skip padding. + reader.consume(3); + let friends_id = reader.read_i32::()?; + let friends_name = read_cstring_buffer(&mut reader, PLAYER_NAME_LENGTH)?; + let fakeplayer = reader.read_u8()? != 0; + let is_hltv = reader.read_u8()? != 0; + // Skip padding. + reader.consume(2); + // Ignore custom_files (4 CRC32 values). + reader.consume(4 * std::mem::size_of::()); + let files_downloaded = reader.read_u8()?; + let player_info = PlayerInfo { + version, + xuid, + name, + user_id, + guid, + friends_id, + friends_name, + fakeplayer, + is_hltv, + files_downloaded, + entity_id, + }; + Ok(player_info) +} + +pub fn parse_player_infos(st: Vec) -> Result> { + let mut result = Vec::new(); + for st in st.iter().filter(|st| st.name() == "userinfo") { + for (entity_id, string) in st.strings().iter().enumerate() { + if let Some(data) = string.data() { + let player_info = parse_player_info(data, entity_id as i32)?; + result.push(player_info); + } + } + } + Ok(result) +} + +fn read_cstring_buffer(cursor: &mut Cursor<&[u8]>, size: usize) -> std::io::Result { + let cstr = CStr::from_bytes_until_nul(&cursor.get_ref()[cursor.position() as usize..]) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + cursor.consume(size); + Ok(cstr.to_string_lossy().into_owned()) +} + +#[cfg(test)] +mod tests { + use super::StringTables; + use crate::Result; + use protobuf::text_format::parse_from_str; - Ok(Self { tables }) + #[test] + fn userinfo_empty_update() -> Result<()> { + let mut st = StringTables::default(); + let create = parse_from_str( + r#"name: "userinfo" + max_entries: 256 + user_data_fixed_size: false + string_data: "\0""#, + ) + .unwrap(); + let update = parse_from_str( + r#"table_id: 0 + num_changed_entries: 1 + string_data: "\020\260""#, + ) + .unwrap(); + let mut updates = st.create_string_table(&create); + while (updates.next_player_info()?).is_some() {} + let mut updates = st.update_string_table(&update)?; + while (updates.next_player_info()?).is_some() {} + Ok(()) } } diff --git a/demo-format/Cargo.toml b/demo-format/Cargo.toml deleted file mode 100644 index 118439e..0000000 --- a/demo-format/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "demo-format" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -bitstream-io.workspace = true diff --git a/demo-format/src/lib.rs b/demo-format/src/lib.rs deleted file mode 100644 index 03acf9a..0000000 --- a/demo-format/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod read; - -pub type Tick = i32; -pub type BitReader<'a> = bitstream_io::BitReader<&'a [u8], bitstream_io::LittleEndian>; diff --git a/parsetest/Cargo.toml b/parsetest/Cargo.toml index 36b8ed7..2fb75c2 100644 --- a/parsetest/Cargo.toml +++ b/parsetest/Cargo.toml @@ -11,7 +11,6 @@ clap = { version = "4.3.0", features = ["derive"] } console = "0.15.7" glob = "0.3.1" indicatif = { version = "0.17.3", features = ["rayon"] } -once_cell = "1.17.2" rayon = "1.7.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"