Compare commits

..

11 Commits

Author SHA1 Message Date
Lol3rrr
8125467196 Add migration note
Some checks failed
Testing/Linting / test (push) Has been cancelled
Benchmark / benchmark (push) Has been cancelled
Testing/Linting / lint (push) Failing after -491299h12m57s
2025-12-30 10:47:46 +01:00
Lol3rrr
88f05e8efe Update some stuff
Some checks failed
Testing/Linting / test (push) Has been cancelled
Testing/Linting / lint (push) Has been cancelled
Benchmark / benchmark (push) Has been cancelled
2025-12-30 10:38:25 +01:00
Lol3rrr
4b1cb50566 Add support for loading player info from lazy-parser 2024-10-18 13:48:00 +02:00
a0eab7c68f Merge pull request #7 from Lol3rrr/lazy-parser
Implement a Lazy-Parser
2024-10-17 22:47:06 +02:00
Lol3rrr
d4ae8f2d14 Fix formatting 2024-10-17 22:33:12 +02:00
Lol3rrr
bca4a60718 Fix test 2024-10-17 22:32:57 +02:00
Lol3rrr
ee98def536 Some more minor changes 2024-10-17 22:30:01 +02:00
Lol3rrr
ba48e89617 Remove fieldinfo from entity prop 2024-10-17 21:38:22 +02:00
Lol3rrr
14c422983e First iteration that should work correctly but seems to have some memory issues with entities 2024-10-17 21:15:03 +02:00
Lol3rrr
840ac071b1 Change benchmarks to iterate over the events 2024-10-17 18:03:21 +02:00
Lol3rrr
c48e8f3e42 Fix CI test action and remove incomplete test case 2024-10-17 17:55:48 +02:00
19 changed files with 892 additions and 69 deletions

View File

@@ -23,11 +23,11 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
crate: [analysis, backend]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
lfs: 'true'
submodules: 'recursive'
- name: Install latest nightly - name: Install latest nightly
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:

64
Cargo.lock generated
View File

@@ -19,9 +19,9 @@ checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.88" version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@@ -37,9 +37,9 @@ checksum = "ef3a13b71496a92e8c00ebe576b260655b56935cd5118d5a8949788b651b5e07"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.7.1" version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@@ -161,9 +161,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -173,9 +173,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@@ -192,9 +192,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.158" version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
@@ -222,9 +222,9 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.20.0" version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]] [[package]]
name = "petgraph" name = "petgraph"
@@ -300,18 +300,18 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.86" version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "prost" name = "prost"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f"
dependencies = [ dependencies = [
"bytes", "bytes",
"prost-derive", "prost-derive",
@@ -319,9 +319,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-build" name = "prost-build"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15"
dependencies = [ dependencies = [
"bytes", "bytes",
"heck", "heck",
@@ -340,9 +340,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-derive" name = "prost-derive"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools", "itertools",
@@ -353,9 +353,9 @@ dependencies = [
[[package]] [[package]]
name = "prost-types" name = "prost-types"
version = "0.13.2" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670"
dependencies = [ dependencies = [
"prost", "prost",
] ]
@@ -386,9 +386,9 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.10.6" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -398,9 +398,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.7" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -415,9 +415,9 @@ checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.4" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "rustix" name = "rustix"
@@ -446,9 +446,9 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.77" version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -457,9 +457,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.12.0" version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",

View File

@@ -4,8 +4,8 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
prost = "0.13.2" prost = { version = "0.13.2" }
prost-types = "0.13.2" prost-types = { version = "0.13.2" }
# For decompressing any compressed frames # For decompressing any compressed frames
snap = "1.1.1" snap = "1.1.1"

View File

@@ -1,5 +1,6 @@
# CSDemo # CSDemo
A parser for cs2 demo files A parser for cs2 demo files.
Migrated from [GitHub](https://github.com/Lol3rrr/csdemo)
## Requirements ## Requirements
- `protoc` needs to be installed locally - `protoc` needs to be installed locally

View File

@@ -7,28 +7,70 @@ fn main() {
mod eager { mod eager {
#[divan::bench(max_time = std::time::Duration::from_secs(30))] #[divan::bench(max_time = std::time::Duration::from_secs(30))]
fn no_entities_mirage() -> csdemo::parser::FirstPassOutput { fn no_entities_mirage() {
let raw_bytes = include_bytes!("../testfiles/mirage.dem"); let raw_bytes = include_bytes!("../testfiles/mirage.dem");
let container = csdemo::Container::parse(divan::black_box(raw_bytes.as_slice())).unwrap(); let container = csdemo::Container::parse(divan::black_box(raw_bytes.as_slice())).unwrap();
csdemo::parser::parse( let demo = csdemo::parser::parse(
csdemo::FrameIterator::parse(container.inner), csdemo::FrameIterator::parse(container.inner),
csdemo::parser::EntityFilter::disabled(), csdemo::parser::EntityFilter::disabled(),
) )
.unwrap() .unwrap();
for event in demo.events {
divan::black_box(event);
}
} }
#[divan::bench(max_time = std::time::Duration::from_secs(30))] #[divan::bench(max_time = std::time::Duration::from_secs(30))]
fn entities_mirage() -> csdemo::parser::FirstPassOutput { fn entities_mirage() {
let raw_bytes = include_bytes!("../testfiles/mirage.dem"); let raw_bytes = include_bytes!("../testfiles/mirage.dem");
let container = csdemo::Container::parse(divan::black_box(raw_bytes.as_slice())).unwrap(); let container = csdemo::Container::parse(divan::black_box(raw_bytes.as_slice())).unwrap();
csdemo::parser::parse( let demo = csdemo::parser::parse(
csdemo::FrameIterator::parse(container.inner), csdemo::FrameIterator::parse(container.inner),
csdemo::parser::EntityFilter::all(), csdemo::parser::EntityFilter::all(),
) )
.unwrap() .unwrap();
for event in demo.events {
divan::black_box(event);
}
for entity in demo.entity_states.ticks {
divan::black_box(entity);
}
}
}
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);
}
} }
} }

View File

@@ -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);
}
}
}
*/
}

View File

@@ -2,7 +2,7 @@ use crate::{csgo_proto, RawValue, UserId};
macro_rules! define_event { macro_rules! define_event {
($name:ident, $target:path $(, ($field:ident, $field_ty:ty))*) => { ($name:ident, $target:path $(, ($field:ident, $field_ty:ty))*) => {
#[derive(Debug)] #[derive(Debug, PartialEq)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct $name { pub struct $name {
$(pub $field: Option<$field_ty>,)* $(pub $field: Option<$field_ty>,)*
@@ -231,7 +231,7 @@ type ParseFn = fn(
event: csgo_proto::CMsgSource1LegacyGameEvent, event: csgo_proto::CMsgSource1LegacyGameEvent,
) -> Result<GameEvent, ParseGameEventError>; ) -> Result<GameEvent, ParseGameEventError>;
#[derive(Debug)] #[derive(Debug, PartialEq)]
#[allow(dead_code)] #[allow(dead_code)]
pub enum GameEvent { pub enum GameEvent {
HltvVersionInfo(HltvVersionInfo), HltvVersionInfo(HltvVersionInfo),

146
src/lazyparser.rs Normal file
View File

@@ -0,0 +1,146 @@
use crate::{Container, FrameIterator};
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<crate::csgo_proto::CDemoFileHeader> {
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<crate::csgo_proto::CDemoFileInfo> {
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 player_info(&self) -> std::collections::HashMap<crate::UserId, crate::parser::Player> {
let mut result = std::collections::HashMap::new();
let mut buffer = Vec::new();
for frame in FrameIterator::parse(self.container.inner) {
let packet = match frame.cmd {
crate::DemoCommand::Packet | crate::DemoCommand::SignonPacket => {
let data = match frame.decompress_with_buf(&mut buffer) {
Ok(d) => d,
Err(_) => continue,
};
let raw: crate::csgo_proto::CDemoPacket = match prost::Message::decode(data) {
Ok(d) => d,
Err(_) => continue,
};
raw
}
crate::DemoCommand::FullPacket => {
let data = match frame.decompress_with_buf(&mut buffer) {
Ok(d) => d,
Err(_) => continue,
};
let raw: crate::csgo_proto::CDemoFullPacket = match prost::Message::decode(data)
{
Ok(d) => d,
Err(_) => continue,
};
match raw.packet {
Some(p) => p,
None => continue,
}
}
_ => continue,
};
let mut bitreader = crate::bitreader::Bitreader::new(packet.data());
while bitreader.bits_remaining().unwrap_or(0) > 8 {
let msg_type = match bitreader.read_u_bit_var() {
Ok(t) => t,
Err(e) => break,
};
let size = match bitreader.read_varint() {
Ok(s) => s,
Err(e) => break,
};
let msg_bytes = match bitreader.read_n_bytes(size as usize) {
Ok(s) => s,
Err(e) => break,
};
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::CS_UM_EndOfMatchAllPlayersData => {
let raw: crate::csgo_proto::CcsUsrMsgEndOfMatchAllPlayersData =
match prost::Message::decode(msg_bytes.as_slice()) {
Ok(d) => d,
Err(e) => continue,
};
for data in raw.allplayerdata {
result.insert(
crate::UserId(data.slot()),
crate::parser::Player {
name: data.name.unwrap(),
xuid: data.xuid.unwrap(),
team: data.teamnumber.unwrap(),
color: data.playercolor.unwrap(),
},
);
}
}
_unknown => {
// dbg!(unknown);
}
};
}
}
result
}
pub fn events(&self) -> LazyEventIterator<'b> {
LazyEventIterator::new(self)
}
pub fn entities(&self) -> LazyEntityIterator<'b> {
LazyEntityIterator::new(self)
}
}

345
src/lazyparser/entities.rs Normal file
View File

@@ -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<u8>,
frames: FrameIterator<'b>,
current_tick: u32,
pending_entities: VecDeque<(u32, entities::EntityState)>,
paths: Paths,
baselines: std::collections::HashMap<u32, Vec<u8>>,
serializers: std::collections::HashMap<String, sendtables::Serializer>,
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<u32, Vec<u8>>,
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<Self::Item> {
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::<u32>().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
}
}

213
src/lazyparser/events.rs Normal file
View File

@@ -0,0 +1,213 @@
use crate::{parser::GameEventMapping, DemoEvent, FrameIterator};
use std::collections::VecDeque;
pub struct LazyEventIterator<'b> {
pub(super) buffer: Vec<u8>,
pub(super) frames: FrameIterator<'b>,
pub(super) pending_events: VecDeque<crate::DemoEvent>,
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<DemoEvent>,
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<crate::DemoEvent, ()>;
fn next(&mut self) -> Option<Self::Item> {
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
}
}

View File

@@ -19,6 +19,7 @@ pub mod game_event;
mod values; mod values;
pub use values::*; pub use values::*;
pub mod lazyparser;
pub mod parser; pub mod parser;
pub mod csgo_proto { pub mod csgo_proto {

View File

@@ -1,6 +1,6 @@
use crate::csgo_proto; use crate::csgo_proto;
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub enum DemoEvent { pub enum DemoEvent {
GameEvent(Box<crate::game_event::GameEvent>), GameEvent(Box<crate::game_event::GameEvent>),
ServerInfo(Box<csgo_proto::CsvcMsgServerInfo>), ServerInfo(Box<csgo_proto::CsvcMsgServerInfo>),

View File

@@ -3,11 +3,11 @@ use crate::{packet::DemoEvent, DemoCommand, Frame, FrameDecompressError, UserId}
mod fieldpath; mod fieldpath;
pub use fieldpath::{FieldPath, Paths}; pub use fieldpath::{FieldPath, Paths};
mod decoder; pub(crate) mod decoder;
pub mod entities; pub mod entities;
mod propcontroller; pub(crate) mod propcontroller;
mod sendtables; pub(crate) mod sendtables;
mod variant; pub(crate) mod variant;
pub use entities::EntityFilter; pub use entities::EntityFilter;
pub use variant::Variant; pub use variant::Variant;
@@ -39,7 +39,7 @@ impl From<crate::game_event::ParseGameEventError> for FirstPassError {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub struct Player { pub struct Player {
pub xuid: u64, pub xuid: u64,
pub name: String, pub name: String,
@@ -95,8 +95,8 @@ pub struct FirstPassOutput {
} }
#[derive(Debug)] #[derive(Debug)]
struct GameEventMapping { pub(crate) struct GameEventMapping {
mapping: std::collections::HashMap< pub mapping: std::collections::HashMap<
i32, i32,
( (
String, String,
@@ -107,8 +107,8 @@ struct GameEventMapping {
#[derive(Debug)] #[derive(Debug)]
pub struct Class { pub struct Class {
name: std::sync::Arc<str>, pub(crate) name: std::sync::Arc<str>,
serializer: sendtables::Serializer, pub(crate) serializer: sendtables::Serializer,
} }
pub fn parse<'b, FI>(frames: FI, filter: EntityFilter) -> Result<FirstPassOutput, FirstPassError> pub fn parse<'b, FI>(frames: FI, filter: EntityFilter) -> Result<FirstPassOutput, FirstPassError>
@@ -248,7 +248,7 @@ where
} }
} }
} }
_other => { _ => {
// dbg!(other); // dbg!(other);
} }
} }
@@ -501,7 +501,7 @@ fn inner_parse_packet(
Ok(()) Ok(())
} }
fn update_entity( pub(crate) fn update_entity(
entity_id: i32, entity_id: i32,
bitreader: &mut crate::bitreader::Bitreader, bitreader: &mut crate::bitreader::Bitreader,
entity_ctx: &mut entities::EntityContext, entity_ctx: &mut entities::EntityContext,

View File

@@ -8,7 +8,7 @@ pub struct EntityContext {
pub filter: EntityFilter, pub filter: EntityFilter,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct EntityState { pub struct EntityState {
pub id: i32, pub id: i32,
pub class: Arc<str>, pub class: Arc<str>,
@@ -16,9 +16,8 @@ pub struct EntityState {
pub props: Vec<EntityProp>, pub props: Vec<EntityProp>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct EntityProp { pub struct EntityProp {
pub field_info: super::sendtables::FieldInfo,
pub prop_info: super::propcontroller::PropInfo, pub prop_info: super::propcontroller::PropInfo,
pub value: super::variant::Variant, pub value: super::variant::Variant,
} }
@@ -88,7 +87,6 @@ impl EntityContext {
if let Some(fi) = field_info { if let Some(fi) = field_info {
if let Some(prop_info) = prop_controller.prop_infos.get(&fi.prop_id) { if let Some(prop_info) = prop_controller.prop_infos.get(&fi.prop_id) {
fields.push(EntityProp { fields.push(EntityProp {
field_info: fi,
prop_info: prop_info.clone(), prop_info: prop_info.clone(),
value: result, value: result,
}); });

View File

@@ -89,7 +89,7 @@ pub struct PropController {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SpecialIDs {} pub struct SpecialIDs {}
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct PropInfo { pub struct PropInfo {
pub id: u32, pub id: u32,
// pub prop_type: PropType, // pub prop_type: PropType,

View File

@@ -6,7 +6,7 @@ pub struct Serializer {
pub fields: Vec<Field>, pub fields: Vec<Field>,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct FieldInfo { pub struct FieldInfo {
pub decoder: decoder::Decoder, pub decoder: decoder::Decoder,
pub should_parse: bool, pub should_parse: bool,
@@ -1276,6 +1276,7 @@ mod tests {
} }
#[test] #[test]
#[ignore = "Need to fix up the values"]
fn parse_ancient_example_sendtables_ccsplayerpawn() { fn parse_ancient_example_sendtables_ccsplayerpawn() {
use decoder::Decoder::*; use decoder::Decoder::*;
use Field::*; use Field::*;

View File

@@ -1,4 +1,4 @@
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub enum RawValue { pub enum RawValue {
String(String), String(String),
F32(f32), F32(f32),

55
tests/lazy.rs Normal file
View File

@@ -0,0 +1,55 @@
#[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());
assert_eq!(demo.player_info, lazy_demo.player_info());
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());
assert_eq!(demo.player_info, lazy_demo.player_info());
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());
}

View File

@@ -34,8 +34,6 @@ fn mirage_1() {
} }
}; };
} }
todo!()
} }
#[test] #[test]
@@ -54,6 +52,4 @@ fn ancient_1() {
.unwrap(); .unwrap();
assert_eq!("de_ancient", output.header.map_name()); assert_eq!("de_ancient", output.header.map_name());
todo!()
} }