Initial version

This commit is contained in:
Lol3rrr
2024-09-15 05:09:15 +02:00
commit c7aa4dbe8c
489 changed files with 124650 additions and 0 deletions

200
src/bitreader.rs Normal file
View File

@@ -0,0 +1,200 @@
use bitter::BitReader;
use bitter::LittleEndianReader;
pub struct Bitreader<'a> {
pub reader: LittleEndianReader<'a>,
pub bits_left: u32,
pub bits: u64,
pub total_bits_left: u32,
}
#[derive(Debug)]
pub enum BitReadError {
FailedByteRead(String),
MalformedMessage,
}
impl<'a> Bitreader<'a> {
pub fn new(bytes: &'a [u8]) -> Bitreader<'a> {
let b = Bitreader {
reader: LittleEndianReader::new(bytes),
bits: 0,
bits_left: 0,
total_bits_left: 0,
};
b
}
#[inline(always)]
pub fn consume(&mut self, n: u32) {
self.bits_left -= n;
self.bits >>= n;
self.reader.consume(n);
}
#[inline(always)]
pub fn peek(&mut self, n: u32) -> u64 {
self.bits & ((1 << n) - 1)
}
#[inline(always)]
pub fn refill(&mut self) {
self.reader.refill_lookahead();
let refilled = self.reader.lookahead_bits();
if refilled > 0 {
self.bits = self.reader.peek(refilled);
}
self.bits_left = refilled;
}
#[inline(always)]
pub fn bits_remaining(&mut self) -> Option<usize> {
Some(self.reader.bits_remaining()?)
}
#[inline(always)]
pub fn read_nbits(&mut self, n: u32) -> Result<u32, BitReadError> {
if self.bits_left < n {
self.refill();
}
let b = self.peek(n);
self.consume(n);
return Ok(b as u32);
}
#[inline(always)]
pub fn read_u_bit_var(&mut self) -> Result<u32, BitReadError> {
let bits = self.read_nbits(6)?;
match bits & 0b110000 {
0b10000 => return Ok((bits & 0b1111) | (self.read_nbits(4)? << 4)),
0b100000 => return Ok((bits & 0b1111) | (self.read_nbits(8)? << 4)),
0b110000 => return Ok((bits & 0b1111) | (self.read_nbits(28)? << 4)),
_ => return Ok(bits),
}
}
#[inline(always)]
pub fn read_varint32(&mut self) -> Result<i32, BitReadError> {
let x = self.read_varint()? as i32;
let mut y = x >> 1;
if x & 1 != 0 {
y = !y;
}
Ok(y as i32)
}
#[inline(always)]
pub fn read_varint(&mut self) -> Result<u32, BitReadError> {
let mut result: u32 = 0;
let mut count: i32 = 0;
let mut b: u32;
loop {
if count >= 5 {
return Ok(result);
}
b = self.read_nbits(8)?;
result |= (b & 127) << (7 * count);
count += 1;
if b & 0x80 == 0 {
break;
}
}
Ok(result)
}
#[inline(always)]
pub fn read_varint_u_64(&mut self) -> Result<u64, BitReadError> {
let mut result: u64 = 0;
let mut count: i32 = 0;
let mut b: u32;
let mut s = 0;
loop {
b = self.read_nbits(8)?;
if b < 0x80 {
if count > 9 || count == 9 && b > 1 {
return Err(BitReadError::MalformedMessage);
}
return Ok(result | (b as u64) << s);
}
result |= ((b as u64) & 127) << s;
count += 1;
if b & 0x80 == 0 {
break;
}
s += 7;
}
Ok(result)
}
#[inline(always)]
pub fn read_boolean(&mut self) -> Result<bool, BitReadError> {
Ok(self.read_nbits(1)? != 0)
}
pub fn read_n_bytes(&mut self, n: usize) -> Result<Vec<u8>, BitReadError> {
let mut bytes = vec![0_u8; n];
match self.reader.read_bytes(&mut bytes) {
true => {
self.refill();
Ok(bytes)
}
false => Err(BitReadError::FailedByteRead(format!(
"Failed to read message/command. bytes left in stream: {}, requested bytes: {}",
self.reader
.bits_remaining()
.unwrap_or(0)
.checked_div(8)
.unwrap_or(0),
n,
))),
}
}
pub fn read_n_bytes_mut(&mut self, n: usize, buf: &mut [u8]) -> Result<(), BitReadError> {
if buf.len() < n {
return Err(BitReadError::MalformedMessage);
}
match self.reader.read_bytes(&mut buf[..n]) {
true => {
self.refill();
Ok(())
}
false => Err(BitReadError::FailedByteRead(format!(
"Failed to read message/command. bytes left in stream: {}, requested bytes: {}",
self.reader
.bits_remaining()
.unwrap_or(0)
.checked_div(8)
.unwrap_or(0),
n,
))),
}
}
pub fn read_ubit_var_fp(&mut self) -> Result<u32, BitReadError> {
if self.read_boolean()? {
return Ok(self.read_nbits(2)?);
}
if self.read_boolean()? {
return Ok(self.read_nbits(4)?);
}
if self.read_boolean()? {
return Ok(self.read_nbits(10)?);
}
if self.read_boolean()? {
return Ok(self.read_nbits(17)?);
}
return Ok(self.read_nbits(31)?);
}
#[inline(always)]
pub fn read_bit_coord(&mut self) -> Result<f32, BitReadError> {
let mut int_val = 0;
let mut frac_val = 0;
let i2 = self.read_boolean()?;
let f2 = self.read_boolean()?;
if !i2 && !f2 {
return Ok(0.0);
}
let sign = self.read_boolean()?;
if i2 {
int_val = self.read_nbits(14)? + 1;
}
if f2 {
frac_val = self.read_nbits(5)?;
}
let resol: f64 = 1.0 / (1 << 5) as f64;
let result: f32 = (int_val as f64 + (frac_val as f64 * resol) as f64) as f32;
if sign {
Ok(-result)
} else {
Ok(result)
}
}
}

44
src/container.rs Normal file
View File

@@ -0,0 +1,44 @@
#[derive(Debug)]
pub enum ParseContainerError {
MissingHeader,
InvalidMagic(core::str::Utf8Error),
MismatchedLength {
buffer_len: usize,
expected_len: usize,
},
Other(&'static str),
}
#[derive(Debug)]
pub struct Container<'b> {
pub magic: &'b str,
pub inner: &'b [u8],
}
impl<'b> Container<'b> {
pub fn parse<'ib>(input: &'ib [u8]) -> Result<Self, ParseContainerError>
where
'ib: 'b,
{
if input.len() < 16 {
return Err(ParseContainerError::MissingHeader);
}
let magic =
core::str::from_utf8(&input[..8]).map_err(|e| ParseContainerError::InvalidMagic(e))?;
let raw_len: [u8; 4] = input[8..12]
.try_into()
.expect("We know that the input buffer is at least 16 bytes large");
let len = u32::from_le_bytes(raw_len);
let inner = &input[16..];
if inner.len() != len as usize + 2 {
return Err(ParseContainerError::MismatchedLength {
buffer_len: inner.len(),
expected_len: len as usize + 2,
});
}
Ok(Self { magic, inner })
}
}

55
src/democmd.rs Normal file
View File

@@ -0,0 +1,55 @@
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum DemoCommand {
Error,
Stop,
FileHeader,
FileInfo,
SyncTick,
SendTables,
ClassInfo,
StringTables,
Packet,
SignonPacket,
ConsoleCmd,
CustomData,
CustomDataCallbacks,
UserCmd,
FullPacket,
SaveGame,
SpawnGroups,
AnimationData,
AnimationHeader,
Max,
IsCompressed,
}
impl TryFrom<i32> for DemoCommand {
type Error = i32;
fn try_from(value: i32) -> Result<Self, i32> {
match value {
-1 => Ok(Self::Error),
0 => Ok(Self::Stop),
1 => Ok(Self::FileHeader),
2 => Ok(Self::FileInfo),
3 => Ok(Self::SyncTick),
4 => Ok(Self::SendTables),
5 => Ok(Self::ClassInfo),
6 => Ok(Self::StringTables),
7 => Ok(Self::Packet),
8 => Ok(Self::SignonPacket),
9 => Ok(Self::ConsoleCmd),
10 => Ok(Self::CustomData),
11 => Ok(Self::CustomDataCallbacks),
12 => Ok(Self::UserCmd),
13 => Ok(Self::FullPacket),
14 => Ok(Self::SaveGame),
15 => Ok(Self::SpawnGroups),
16 => Ok(Self::AnimationData),
17 => Ok(Self::AnimationHeader),
18 => Ok(Self::Max),
64 => Ok(Self::IsCompressed),
unknown => Err(unknown),
}
}
}

91
src/frame.rs Normal file
View File

@@ -0,0 +1,91 @@
pub struct Frame<'b> {
pub cmd: crate::DemoCommand,
pub tick: i32,
pub compressed: bool,
pub inner: std::borrow::Cow<'b, [u8]>,
}
impl<'b> Frame<'b> {
pub fn parse<'ib>(input: &'ib [u8]) -> Result<(&'ib [u8], Self), ()>
where
'ib: 'b,
{
let (input, raw_cmd) = crate::varint::parse_varint(input)?;
let (input, tick) = crate::varint::parse_varint(input)?;
let (input, size) = crate::varint::parse_varint(input)?;
if input.len() < size as usize {
return Err(());
}
let demo_cmd = crate::DemoCommand::try_from((raw_cmd & !64) as i32).map_err(|e| ())?;
Ok((
&input[size as usize..],
Self {
tick: tick as i32,
cmd: demo_cmd,
compressed: (raw_cmd & 64) == 64,
inner: std::borrow::Cow::Borrowed(&input[..size as usize]),
},
))
}
pub fn data(&self) -> Option<&[u8]> {
if self.compressed {
return None;
}
Some(self.inner.as_ref())
}
pub fn decompress(&mut self) -> Result<(), ()> {
if !self.compressed {
return Ok(());
}
let decompressed = snap::raw::Decoder::new()
.decompress_vec(&self.inner.as_ref())
.map_err(|e| ())?;
self.compressed = false;
self.inner = std::borrow::Cow::Owned(decompressed);
Ok(())
}
}
pub struct FrameIterator<'b> {
remaining: &'b [u8],
}
impl<'b> FrameIterator<'b> {
pub fn parse<'ib>(input: &'ib [u8]) -> Self
where
'ib: 'b,
{
Self { remaining: input }
}
}
impl<'b> Iterator for FrameIterator<'b> {
type Item = Frame<'b>;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining.is_empty() {
return None;
}
match Frame::parse(self.remaining) {
Ok((rem, frame)) => {
self.remaining = rem;
Some(frame)
}
Err(_e) => {
// TODO
// How do we handle errors?
self.remaining = &[];
None
}
}
}
}

293
src/game_event.rs Normal file
View File

@@ -0,0 +1,293 @@
use crate::csgo_proto;
#[derive(Debug)]
pub enum RawValue {
String(String),
F32(f32),
I32(i32),
Bool(bool),
U64(u64),
}
impl TryFrom<crate::csgo_proto::c_msg_source1_legacy_game_event::KeyT> for RawValue {
type Error = ();
fn try_from(value: crate::csgo_proto::c_msg_source1_legacy_game_event::KeyT) -> Result<Self, Self::Error> {
match value.r#type() {
1 if value.val_string.is_some() => Ok(Self::String(value.val_string.unwrap())),
2 if value.val_float.is_some() => Ok(Self::F32(value.val_float.unwrap())),
3 if value.val_long.is_some() => Ok(Self::I32(value.val_long.unwrap())),
4 if value.val_short.is_some() => Ok(Self::I32(value.val_short.unwrap() as i32)),
5 if value.val_byte.is_some() => Ok(Self::I32(value.val_byte.unwrap() as i32)),
6 if value.val_bool.is_some() => Ok(Self::Bool(value.val_bool.unwrap())),
7 if value.val_uint64.is_some() => Ok(Self::U64(value.val_uint64.unwrap())),
8 if value.val_long.is_some() => Ok(Self::I32(value.val_long.unwrap())),
9 if value.val_short.is_some() => Ok(Self::I32(value.val_short.unwrap() as i32)),
_ => Err(()),
}
}
}
impl TryFrom<RawValue> for i32 {
type Error = ();
fn try_from(value: RawValue) -> Result<Self, Self::Error> {
match value {
RawValue::I32(v) => Ok(v),
_ => Err(()),
}
}
}
impl TryFrom<RawValue> for bool {
type Error = ();
fn try_from(value: RawValue) -> Result<Self, Self::Error> {
match value {
RawValue::Bool(v) => Ok(v),
_ => Err(()),
}
}
}
impl TryFrom<RawValue> for String {
type Error = ();
fn try_from(value: RawValue) -> Result<Self, Self::Error> {
match value {
RawValue::String(v) => Ok(v),
_ => Err(()),
}
}
}
macro_rules! define_event {
($name:ident, $target:path $(, ($field:ident, $field_ty:ty))*) => {
#[derive(Debug)]
#[allow(dead_code)]
pub struct $name {
$($field: Option<$field_ty>,)*
remaining: ::std::collections::HashMap<String, crate::csgo_proto::c_msg_source1_legacy_game_event::KeyT>,
}
impl $name {
#[allow(unused_mut)]
fn parse(keys: &[crate::csgo_proto::csvc_msg_game_event_list::KeyT], event: crate::csgo_proto::CMsgSource1LegacyGameEvent) -> Result<GameEvent, ParseGameEventError> {
let mut fields: ::std::collections::HashMap<_,_> = keys.iter().zip(event.keys.into_iter()).map(|(k, f)| {
(k.name().to_owned(), f)
}).collect();
$(let $field: Option<RawValue> = fields.remove(stringify!($field)).map(|f| f.try_into().ok()).flatten();)*
let value = $name {
$($field: $field.map(|f| f.try_into().ok()).flatten(),)*
remaining: fields,
};
Ok($target(value))
}
}
};
}
define_event!(HltvVersionInfo, GameEvent::HltvVersionInfo);
define_event!(ItemEquip, GameEvent::ItemEquip, (userid, i32), (hassilencer, bool), (hastracers, bool), (item, String), (issilenced, bool), (canzoom, bool), (ispainted, bool), (weptype, i32), (defindex, i32));
define_event!(ItemPickup, GameEvent::ItemPickup, (userid, RawValue), (item, RawValue), (silent, RawValue), (defindex, RawValue));
define_event!(WeaponReload, GameEvent::WeaponReload, (userid, RawValue), (userid_pawn, RawValue));
define_event!(WeaponZoom, GameEvent::WeaponZoom, (userid, RawValue), (userid_pawn, RawValue));
define_event!(WeaponFire, GameEvent::WeaponFire, (userid, RawValue), (weapon, RawValue), (silenced, RawValue), (userid_pawn, RawValue));
define_event!(SmokeGrenadeDetonate, GameEvent::SmokeGrenadeDetonate);
define_event!(SmokeGrenadeExpired, GameEvent::SmokeGrenadeExpired);
define_event!(HEGrenadeDetonate, GameEvent::HEGrenadeDetonate);
define_event!(InfernoStartBurn, GameEvent::InfernoStartBurn);
define_event!(InfernoExpire, GameEvent::InfernoExpire);
define_event!(FlashbangDetonate, GameEvent::FlashbangDetonate);
define_event!(DecoyStarted, GameEvent::DecoyStarted);
define_event!(DecoyDetonate, GameEvent::DecoyDetonate);
define_event!(PlayerConnect, GameEvent::PlayerConnect, (address, RawValue), (bot, RawValue), (name, RawValue), (userid, RawValue), (networkid, RawValue), (xuid, RawValue));
define_event!(PlayerConnectFull, GameEvent::PlayerConnectFull, (userid, RawValue));
define_event!(PlayerDisconnect, GameEvent::PlayerDisconnect, (userid, RawValue), (reason, RawValue), (name, RawValue), (networkid, RawValue), (xuid, RawValue));
define_event!(PlayerFootstep, GameEvent::PlayerFootstep);
define_event!(PlayerJump, GameEvent::PlayerJump);
define_event!(PlayerHurt, GameEvent::PlayerHurt);
define_event!(PlayerDeath, GameEvent::PlayerDeath);
define_event!(PlayerSpawn, GameEvent::PlayerSpawn);
define_event!(PlayerBlind, GameEvent::PlayerBlind);
define_event!(PlayerTeam, GameEvent::PlayerTeam, (userid, RawValue), (team, RawValue), (oldteam, RawValue), (disconnect, RawValue), (silent, RawValue), (isbot, RawValue), (userid_pawn, RawValue));
define_event!(BulletDamage, GameEvent::BulletDamage);
define_event!(OtherDeath, GameEvent::OtherDeath);
define_event!(BombPickup, GameEvent::BombPickup);
define_event!(BombDropped, GameEvent::BombDropped);
define_event!(BombBeginPlant, GameEvent::BombBeginPlant);
define_event!(BombPlanted, GameEvent::BombPlanted);
define_event!(BombExploded, GameEvent::BombExploded);
define_event!(BombBeginDefuse, GameEvent::BombBeginDefuse);
define_event!(BombDefused, GameEvent::BombDefused);
define_event!(BeginNewMatch, GameEvent::BeginNewMatch);
define_event!(RoundAnnounceMatchStart, GameEvent::RoundAnnounceMatchStart);
define_event!(RoundFreezeEnd, GameEvent::RoundFreezeEnd);
define_event!(RoundPreStart, GameEvent::RoundPreStart);
define_event!(RoundPostStart, GameEvent::RoundPostStart);
define_event!(RoundOfficiallyEnded, GameEvent::RoundOfficiallyEnded);
define_event!(RoundStartBeep, GameEvent::RoundStartBeep);
define_event!(RoundAnnounceMatchpoint, GameEvent::RoundAnnounceMatchpoint);
define_event!(RoundPreRestart, GameEvent::RoundPreRestart);
define_event!(RoundTimeWarning, GameEvent::RoundTimeWarning);
define_event!(RoundFinalBeep, GameEvent::RoundFinalBeep);
define_event!(BuyTimeEnded, GameEvent::BuyTimeEnded);
define_event!(RoundAnnounceLastRoundHalf, GameEvent::RoundAnnounceLastRoundHalf);
define_event!(AnnouncePhaseEnd, GameEvent::AnnouncePhaseEnd);
define_event!(WinPanelMatch, GameEvent::WinPanelMatch);
type ParseFn = fn(keys: &[csgo_proto::csvc_msg_game_event_list::KeyT], event: csgo_proto::CMsgSource1LegacyGameEvent) -> Result<GameEvent, ParseGameEventError>;
#[derive(Debug)]
#[allow(dead_code)]
pub enum GameEvent {
HltvVersionInfo(HltvVersionInfo),
//
ItemEquip(ItemEquip),
ItemPickup(ItemPickup),
//
WeaponReload(WeaponReload),
WeaponZoom(WeaponZoom),
WeaponFire(WeaponFire),
//
SmokeGrenadeDetonate(SmokeGrenadeDetonate),
SmokeGrenadeExpired(SmokeGrenadeExpired),
HEGrenadeDetonate(HEGrenadeDetonate),
InfernoStartBurn(InfernoStartBurn),
InfernoExpire(InfernoExpire),
FlashbangDetonate(FlashbangDetonate),
DecoyStarted(DecoyStarted),
DecoyDetonate(DecoyDetonate),
//
PlayerConnect(PlayerConnect),
PlayerConnectFull(PlayerConnectFull),
PlayerDisconnect(PlayerDisconnect),
PlayerFootstep(PlayerFootstep),
PlayerJump(PlayerJump),
PlayerHurt(PlayerHurt),
PlayerDeath(PlayerDeath),
PlayerSpawn(PlayerSpawn),
PlayerBlind(PlayerBlind),
PlayerTeam(PlayerTeam),
//
BulletDamage(BulletDamage),
//
OtherDeath(OtherDeath),
//
BombPickup(BombPickup),
BombDropped(BombDropped),
BombBeginPlant(BombBeginPlant),
BombPlanted(BombPlanted),
BombExploded(BombExploded),
BombBeginDefuse(BombBeginDefuse),
BombDefused(BombDefused),
//
BeginNewMatch(BeginNewMatch),
RoundAnnounceMatchStart(RoundAnnounceMatchStart),
RoundFreezeEnd(RoundFreezeEnd),
RoundPreStart(RoundPreStart),
RoundPostStart(RoundPostStart),
RoundOfficiallyEnded(RoundOfficiallyEnded),
RoundStartBeep(RoundStartBeep),
RoundAnnounceMatchpoint(RoundAnnounceMatchpoint),
RoundPreRestart(RoundPreRestart),
RoundTimeWarning(RoundTimeWarning),
RoundFinalBeep(RoundFinalBeep),
BuyTimeEnded(BuyTimeEnded),
RoundAnnounceLastRoundHalf(RoundAnnounceLastRoundHalf),
AnnouncePhaseEnd(AnnouncePhaseEnd),
WinPanelMatch(WinPanelMatch),
}
#[derive(Debug)]
pub enum ParseGameEventError {
MismatchedKeysFields,
}
pub static EVENT_PARSERS: phf::Map<&'static str, GameEventParser> = phf::phf_map! {
"hltv_versioninfo" => GameEventParser::new(HltvVersionInfo::parse),
"item_equip" => GameEventParser::new(ItemEquip::parse),
"item_pickup" => GameEventParser::new(ItemPickup::parse),
"weapon_reload" => GameEventParser::new(WeaponReload::parse),
"weapon_zoom" => GameEventParser::new(WeaponZoom::parse),
"weapon_fire" => GameEventParser::new(WeaponFire::parse),
"smokegrenade_detonate" => GameEventParser::new(SmokeGrenadeDetonate::parse),
"smokegrenade_expired" => GameEventParser::new(SmokeGrenadeExpired::parse),
"hegrenade_detonate" => GameEventParser::new(HEGrenadeDetonate::parse),
"inferno_startburn" => GameEventParser::new(InfernoStartBurn::parse),
"inferno_expire" => GameEventParser::new(InfernoExpire::parse),
"flashbang_detonate" => GameEventParser::new(FlashbangDetonate::parse),
"decoy_started" => GameEventParser::new(DecoyStarted::parse),
"decoy_detonate" => GameEventParser::new(DecoyDetonate::parse),
"player_connect" => GameEventParser::new(PlayerConnect::parse),
"player_connect_full" => GameEventParser::new(PlayerConnectFull::parse),
"player_disconnect" => GameEventParser::new(PlayerDisconnect::parse),
"player_footstep" => GameEventParser::new(PlayerFootstep::parse),
"player_jump" => GameEventParser::new(PlayerJump::parse),
"player_hurt" => GameEventParser::new(PlayerHurt::parse),
"player_death" => GameEventParser::new(PlayerDeath::parse),
"player_spawn" => GameEventParser::new(PlayerSpawn::parse),
"player_blind" => GameEventParser::new(PlayerBlind::parse),
"player_team" => GameEventParser::new(PlayerTeam::parse),
"bullet_damage" => GameEventParser::new(BulletDamage::parse),
"other_death" => GameEventParser::new(OtherDeath::parse),
"bomb_pickup" => GameEventParser::new(BombPickup::parse),
"bomb_dropped" => GameEventParser::new(BombDropped::parse),
"bomb_beginplant" => GameEventParser::new(BombBeginPlant::parse),
"bomb_planted" => GameEventParser::new(BombPlanted::parse),
"bomb_exploded" => GameEventParser::new(BombExploded::parse),
"bomb_begindefuse" => GameEventParser::new(BombBeginDefuse::parse),
"bomb_defused" => GameEventParser::new(BombDefused::parse),
"begin_new_match" => GameEventParser::new(BeginNewMatch::parse),
"round_announce_match_start" => GameEventParser::new(RoundAnnounceMatchStart::parse),
"round_freeze_end" => GameEventParser::new(RoundFreezeEnd::parse),
"round_prestart" => GameEventParser::new(RoundPreStart::parse),
"round_poststart" => GameEventParser::new(RoundPostStart::parse),
"round_officially_ended" => GameEventParser::new(RoundOfficiallyEnded::parse),
"cs_round_start_beep" => GameEventParser::new(RoundStartBeep::parse),
"round_announce_match_point" => GameEventParser::new(RoundAnnounceMatchpoint::parse),
"cs_pre_restart" => GameEventParser::new(RoundPreRestart::parse),
"round_time_warning" => GameEventParser::new(RoundTimeWarning::parse),
"cs_round_final_beep" => GameEventParser::new(RoundFinalBeep::parse),
"buytime_ended" => GameEventParser::new(BuyTimeEnded::parse),
"round_announce_last_round_half" => GameEventParser::new(RoundAnnounceLastRoundHalf::parse),
"announce_phase_end" => GameEventParser::new(AnnouncePhaseEnd::parse),
"cs_win_panel_match" => GameEventParser::new(WinPanelMatch::parse),
};
pub struct GameEventParser {
inner: fn(keys: &[csgo_proto::csvc_msg_game_event_list::KeyT], event: csgo_proto::CMsgSource1LegacyGameEvent) -> Result<GameEvent, ParseGameEventError>,
}
impl GameEventParser {
pub const fn new(func: ParseFn) -> Self {
Self {
inner: func,
}
}
pub fn parse(&self, keys: &[csgo_proto::csvc_msg_game_event_list::KeyT], event: csgo_proto::CMsgSource1LegacyGameEvent) -> Result<GameEvent, ParseGameEventError> {
if keys.len() != event.keys.len() {
return Err(ParseGameEventError::MismatchedKeysFields);
}
(self.inner)(keys, event)
}
}

22
src/lib.rs Normal file
View File

@@ -0,0 +1,22 @@
mod container;
pub use container::{Container, ParseContainerError};
mod frame;
pub use frame::{Frame, FrameIterator};
mod democmd;
pub use democmd::DemoCommand;
mod netmessagetypes;
mod bitreader;
mod varint;
mod packet;
pub mod game_event;
pub mod parser;
pub mod csgo_proto {
include!(concat!(env!("OUT_DIR"), "/_.rs"));
}

390
src/netmessagetypes.rs Normal file
View File

@@ -0,0 +1,390 @@
impl TryFrom<i32> for NetmessageType {
type Error = i32;
fn try_from(value: i32) -> Result<Self, Self::Error> {
use NetmessageType::*;
match value {
0 => Ok(net_NOP),
1 => Ok(net_Disconnect),
3 => Ok(net_SplitScreenUser),
4 => Ok(net_Tick),
5 => Ok(net_StringCmd),
6 => Ok(net_SetConVar),
7 => Ok(net_SignonState),
8 => Ok(net_SpawnGroup_Load),
9 => Ok(net_SpawnGroup_ManifestUpdate),
11 => Ok(net_SpawnGroup_SetCreationTick),
12 => Ok(net_SpawnGroup_Unload),
13 => Ok(net_SpawnGroup_LoadCompleted),
15 => Ok(net_DebugOverlay),
40 => Ok(svc_ServerInfo),
41 => Ok(svc_FlattenedSerializer),
42 => Ok(svc_ClassInfo),
43 => Ok(svc_SetPause),
44 => Ok(svc_CreateStringTable),
45 => Ok(svc_UpdateStringTable),
46 => Ok(svc_VoiceInit),
47 => Ok(svc_VoiceData),
48 => Ok(svc_Print),
49 => Ok(svc_Sounds),
50 => Ok(svc_SetView),
51 => Ok(svc_ClearAllStringTables),
52 => Ok(svc_CmdKeyValues),
53 => Ok(svc_BSPDecal),
54 => Ok(svc_SplitScreen),
55 => Ok(svc_PacketEntities),
56 => Ok(svc_Prefetch),
57 => Ok(svc_Menu),
58 => Ok(svc_GetCvarValue),
59 => Ok(svc_StopSound),
60 => Ok(svc_PeerList),
61 => Ok(svc_PacketReliable),
62 => Ok(svc_HLTVStatus),
63 => Ok(svc_ServerSteamID),
70 => Ok(svc_FullFrameSplit),
71 => Ok(svc_RconServerDetails),
72 => Ok(svc_UserMessage),
73 => Ok(svc_HltvReplay),
74 => Ok(svc_Broadcast_Command),
75 => Ok(svc_HltvFixupOperatorStatus),
76 => Ok(svc_UserCmds),
101 => Ok(UM_AchievementEvent),
102 => Ok(UM_CloseCaption),
103 => Ok(UM_CloseCaptionDirect),
104 => Ok(UM_CurrentTimescale),
105 => Ok(UM_DesiredTimescale),
106 => Ok(UM_Fade),
107 => Ok(UM_GameTitle),
110 => Ok(UM_HudMsg),
111 => Ok(UM_HudText),
113 => Ok(UM_ColoredText),
114 => Ok(UM_RequestState),
115 => Ok(UM_ResetHUD),
116 => Ok(UM_Rumble),
117 => Ok(UM_SayText),
118 => Ok(UM_SayText2),
119 => Ok(UM_SayTextChannel),
120 => Ok(UM_Shake),
121 => Ok(UM_ShakeDir),
124 => Ok(UM_TextMsg),
125 => Ok(UM_ScreenTilt),
128 => Ok(UM_VoiceMask),
130 => Ok(UM_SendAudio),
131 => Ok(UM_ItemPickup),
132 => Ok(UM_AmmoDenied),
134 => Ok(UM_ShowMenu),
135 => Ok(UM_CreditsMsg),
142 => Ok(UM_CloseCaptionPlaceholder),
143 => Ok(UM_CameraTransition),
144 => Ok(UM_AudioParameter),
145 => Ok(UM_ParticleManager),
146 => Ok(UM_HudError),
148 => Ok(UM_CustomGameEvent),
149 => Ok(UM_AnimGraphUpdate),
150 => Ok(UM_HapticsManagerPulse),
151 => Ok(UM_HapticsManagerEffect),
152 => Ok(UM_CommandQueueState),
153 => Ok(UM_UpdateCssClasses),
154 => Ok(UM_ServerFrameTime),
155 => Ok(UM_LagCompensationError),
156 => Ok(UM_RequestDllStatus),
157 => Ok(UM_RequestUtilAction),
158 => Ok(UM_UtilActionResponse),
159 => Ok(UM_DllStatusResponse),
160 => Ok(UM_RequestInventory),
161 => Ok(UM_InventoryResponse),
200 => Ok(GE_VDebugGameSessionIDEvent),
201 => Ok(GE_PlaceDecalEvent),
202 => Ok(GE_ClearWorldDecalsEvent),
203 => Ok(GE_ClearEntityDecalsEvent),
204 => Ok(GE_ClearDecalsForSkeletonInstanceEvent),
205 => Ok(GE_Source1LegacyGameEventList),
206 => Ok(GE_Source1LegacyListenEvents),
207 => Ok(GE_Source1LegacyGameEvent),
208 => Ok(GE_SosStartSoundEvent),
209 => Ok(GE_SosStopSoundEvent),
210 => Ok(GE_SosSetSoundEventParams),
211 => Ok(GE_SosSetLibraryStackFields),
212 => Ok(GE_SosStopSoundEventHash),
301 => Ok(CS_UM_VGUIMenu),
302 => Ok(CS_UM_Geiger),
303 => Ok(CS_UM_Train),
304 => Ok(CS_UM_HudText),
305 => Ok(CS_UM_SayText),
306 => Ok(CS_UM_SayText2),
307 => Ok(CS_UM_TextMsg),
308 => Ok(CS_UM_HudMsg),
309 => Ok(CS_UM_ResetHud),
310 => Ok(CS_UM_GameTitle),
312 => Ok(CS_UM_Shake),
313 => Ok(CS_UM_Fade),
314 => Ok(CS_UM_Rumble),
315 => Ok(CS_UM_CloseCaption),
316 => Ok(CS_UM_CloseCaptionDirect),
317 => Ok(CS_UM_SendAudio),
318 => Ok(CS_UM_RawAudio),
319 => Ok(CS_UM_VoiceMask),
320 => Ok(CS_UM_RequestState),
321 => Ok(CS_UM_Damage),
322 => Ok(CS_UM_RadioText),
323 => Ok(CS_UM_HintText),
324 => Ok(CS_UM_KeyHintText),
325 => Ok(CS_UM_ProcessSpottedEntityUpdate),
326 => Ok(CS_UM_ReloadEffect),
327 => Ok(CS_UM_AdjustMoney),
328 => Ok(CS_UM_UpdateTeamMoney),
329 => Ok(CS_UM_StopSpectatorMode),
330 => Ok(CS_UM_KillCam),
331 => Ok(CS_UM_DesiredTimescale),
332 => Ok(CS_UM_CurrentTimescale),
333 => Ok(CS_UM_AchievementEvent),
334 => Ok(CS_UM_MatchEndConditions),
335 => Ok(CS_UM_DisconnectToLobby),
336 => Ok(CS_UM_PlayerStatsUpdate),
338 => Ok(CS_UM_WarmupHasEnded),
339 => Ok(CS_UM_ClientInfo),
340 => Ok(CS_UM_XRankGet),
341 => Ok(CS_UM_XRankUpd),
345 => Ok(CS_UM_CallVoteFailed),
346 => Ok(CS_UM_VoteStart),
347 => Ok(CS_UM_VotePass),
348 => Ok(CS_UM_VoteFailed),
349 => Ok(CS_UM_VoteSetup),
350 => Ok(CS_UM_ServerRankRevealAll),
351 => Ok(CS_UM_SendLastKillerDamageToClient),
352 => Ok(CS_UM_ServerRankUpdate),
353 => Ok(CS_UM_ItemPickup),
354 => Ok(CS_UM_ShowMenu),
355 => Ok(CS_UM_BarTime),
356 => Ok(CS_UM_AmmoDenied),
357 => Ok(CS_UM_MarkAchievement),
358 => Ok(CS_UM_MatchStatsUpdate),
359 => Ok(CS_UM_ItemDrop),
360 => Ok(CS_UM_GlowPropTurnOff),
361 => Ok(CS_UM_SendPlayerItemDrops),
362 => Ok(CS_UM_RoundBackupFilenames),
363 => Ok(CS_UM_SendPlayerItemFound),
364 => Ok(CS_UM_ReportHit),
365 => Ok(CS_UM_XpUpdate),
366 => Ok(CS_UM_QuestProgress),
367 => Ok(CS_UM_ScoreLeaderboardData),
368 => Ok(CS_UM_PlayerDecalDigitalSignature),
369 => Ok(CS_UM_WeaponSound),
370 => Ok(CS_UM_UpdateScreenHealthBar),
371 => Ok(CS_UM_EntityOutlineHighlight),
372 => Ok(CS_UM_SSUI),
373 => Ok(CS_UM_SurvivalStats),
374 => Ok(CS_UM_DisconnectToLobby2),
375 => Ok(CS_UM_EndOfMatchAllPlayersData),
376 => Ok(CS_UM_PostRoundDamageReport),
379 => Ok(CS_UM_RoundEndReportData),
380 => Ok(CS_UM_CurrentRoundOdds),
381 => Ok(CS_UM_DeepStats),
382 => Ok(CS_UM_UtilMsg),
383 => Ok(CS_UM_ShootInfo),
//
400 => Ok(TE_EffectDispatch),
411 => Ok(TE_WorldDecal),
419 => Ok(TE_Explosion),
423 => Ok(TE_PhysicsProp),
//
450 => Ok(CS_GE_PlayerAnimationEvent),
451 => Ok(CS_GE_RadioIconEvent),
452 => Ok(CS_GE_FireBullets),
other => Err(other),
}
}
}
#[derive(Debug, PartialEq)]
#[allow(non_camel_case_types)]
pub enum NetmessageType {
net_NOP,
net_Disconnect,
net_SplitScreenUser,
net_Tick,
net_StringCmd,
net_SetConVar,
net_SignonState,
net_SpawnGroup_Load,
net_SpawnGroup_ManifestUpdate,
net_SpawnGroup_SetCreationTick,
net_SpawnGroup_Unload,
net_SpawnGroup_LoadCompleted,
net_DebugOverlay,
svc_ServerInfo,
svc_FlattenedSerializer,
svc_ClassInfo,
svc_SetPause,
svc_CreateStringTable,
svc_UpdateStringTable,
svc_VoiceInit,
svc_VoiceData,
svc_Print,
svc_Sounds,
svc_SetView,
svc_ClearAllStringTables,
svc_CmdKeyValues,
svc_BSPDecal,
svc_SplitScreen,
svc_PacketEntities,
svc_Prefetch,
svc_Menu,
svc_GetCvarValue,
svc_StopSound,
svc_PeerList,
svc_PacketReliable,
svc_HLTVStatus,
svc_ServerSteamID,
svc_FullFrameSplit,
svc_RconServerDetails,
svc_UserMessage,
svc_HltvReplay,
svc_Broadcast_Command,
svc_HltvFixupOperatorStatus,
svc_UserCmds,
GE_VDebugGameSessionIDEvent,
GE_PlaceDecalEvent,
GE_ClearWorldDecalsEvent,
GE_ClearEntityDecalsEvent,
GE_ClearDecalsForSkeletonInstanceEvent,
GE_Source1LegacyGameEventList,
GE_Source1LegacyListenEvents,
GE_Source1LegacyGameEvent,
GE_SosStartSoundEvent,
GE_SosStopSoundEvent,
GE_SosSetSoundEventParams,
GE_SosSetLibraryStackFields,
GE_SosStopSoundEventHash,
CS_UM_VGUIMenu,
CS_UM_Geiger,
CS_UM_Train,
CS_UM_HudText,
CS_UM_SayText,
CS_UM_SayText2,
CS_UM_TextMsg,
CS_UM_HudMsg,
CS_UM_ResetHud,
CS_UM_GameTitle,
CS_UM_Shake,
CS_UM_Fade,
CS_UM_Rumble,
CS_UM_CloseCaption,
CS_UM_CloseCaptionDirect,
CS_UM_SendAudio,
CS_UM_RawAudio,
CS_UM_VoiceMask,
CS_UM_RequestState,
CS_UM_Damage,
CS_UM_RadioText,
CS_UM_HintText,
CS_UM_KeyHintText,
CS_UM_ProcessSpottedEntityUpdate,
CS_UM_ReloadEffect,
CS_UM_AdjustMoney,
CS_UM_UpdateTeamMoney,
CS_UM_StopSpectatorMode,
CS_UM_KillCam,
CS_UM_DesiredTimescale,
CS_UM_CurrentTimescale,
CS_UM_AchievementEvent,
CS_UM_MatchEndConditions,
CS_UM_DisconnectToLobby,
CS_UM_PlayerStatsUpdate,
CS_UM_WarmupHasEnded,
CS_UM_ClientInfo,
CS_UM_XRankGet,
CS_UM_XRankUpd,
CS_UM_CallVoteFailed,
CS_UM_VoteStart,
CS_UM_VotePass,
CS_UM_VoteFailed,
CS_UM_VoteSetup,
CS_UM_ServerRankRevealAll,
CS_UM_SendLastKillerDamageToClient,
CS_UM_ServerRankUpdate,
CS_UM_ItemPickup,
CS_UM_ShowMenu,
CS_UM_BarTime,
CS_UM_AmmoDenied,
CS_UM_MarkAchievement,
CS_UM_MatchStatsUpdate,
CS_UM_ItemDrop,
CS_UM_GlowPropTurnOff,
CS_UM_SendPlayerItemDrops,
CS_UM_RoundBackupFilenames,
CS_UM_SendPlayerItemFound,
CS_UM_ReportHit,
CS_UM_XpUpdate,
CS_UM_QuestProgress,
CS_UM_ScoreLeaderboardData,
CS_UM_PlayerDecalDigitalSignature,
CS_UM_WeaponSound,
CS_UM_UpdateScreenHealthBar,
CS_UM_EntityOutlineHighlight,
CS_UM_SSUI,
CS_UM_SurvivalStats,
CS_UM_DisconnectToLobby2,
CS_UM_EndOfMatchAllPlayersData,
CS_UM_PostRoundDamageReport,
CS_UM_RoundEndReportData,
CS_UM_CurrentRoundOdds,
CS_UM_DeepStats,
CS_UM_UtilMsg,
CS_UM_ShootInfo,
UM_AchievementEvent,
UM_CloseCaption,
UM_CloseCaptionDirect,
UM_CurrentTimescale,
UM_DesiredTimescale,
UM_Fade,
UM_GameTitle,
UM_HudMsg,
UM_HudText,
UM_ColoredText,
UM_RequestState,
UM_ResetHUD,
UM_Rumble,
UM_SayText,
UM_SayText2,
UM_SayTextChannel,
UM_Shake,
UM_ShakeDir,
UM_TextMsg,
UM_ScreenTilt,
UM_VoiceMask,
UM_SendAudio,
UM_ItemPickup,
UM_AmmoDenied,
UM_ShowMenu,
UM_CreditsMsg,
UM_CloseCaptionPlaceholder,
UM_CameraTransition,
UM_AudioParameter,
UM_ParticleManager,
UM_HudError,
UM_CustomGameEvent,
UM_AnimGraphUpdate,
UM_HapticsManagerPulse,
UM_HapticsManagerEffect,
UM_CommandQueueState,
UM_UpdateCssClasses,
UM_ServerFrameTime,
UM_LagCompensationError,
UM_RequestDllStatus,
UM_RequestUtilAction,
UM_UtilActionResponse,
UM_DllStatusResponse,
UM_RequestInventory,
UM_InventoryResponse,
//
TE_EffectDispatch,
TE_WorldDecal,
TE_Explosion,
TE_PhysicsProp,
//
CS_GE_PlayerAnimationEvent,
CS_GE_RadioIconEvent,
CS_GE_FireBullets,
}

10
src/packet.rs Normal file
View File

@@ -0,0 +1,10 @@
use crate::csgo_proto;
#[derive(Debug)]
pub enum DemoEvent {
GameEvent(crate::game_event::GameEvent),
ServerInfo(csgo_proto::CsvcMsgServerInfo),
Tick(csgo_proto::CnetMsgTick),
RankUpdate(csgo_proto::CcsUsrMsgServerRankUpdate),
RankReveal(csgo_proto::CcsUsrMsgServerRankRevealAll),
}

229
src/parser.rs Normal file
View File

@@ -0,0 +1,229 @@
use crate::{packet::DemoEvent, DemoCommand, Frame};
#[derive(Debug)]
pub enum FirstPassError {
DecompressFrame,
NoDataFrame,
DecodeProtobuf(prost::DecodeError),
MissingFileHeader,
MissingFileInfo,
Bitreader(crate::bitreader::BitReadError),
ParseGameEventError(crate::game_event::ParseGameEventError)
}
impl From<prost::DecodeError> for FirstPassError {
fn from(value: prost::DecodeError) -> Self {
Self::DecodeProtobuf(value)
}
}
impl From<crate::bitreader::BitReadError> for FirstPassError {
fn from(value: crate::bitreader::BitReadError) -> Self {
Self::Bitreader(value)
}
}
impl From<crate::game_event::ParseGameEventError> for FirstPassError {
fn from(value: crate::game_event::ParseGameEventError) -> Self {
Self::ParseGameEventError(value)
}
}
#[derive(Debug)]
pub struct FirstPassOutput {
pub header: crate::csgo_proto::CDemoFileHeader,
pub info: crate::csgo_proto::CDemoFileInfo,
pub events: Vec<DemoEvent>,
}
#[derive(Debug)]
struct GameEventMapping {
mapping: std::collections::HashMap<i32, (String, Vec<crate::csgo_proto::csvc_msg_game_event_list::KeyT>)>,
}
pub fn parse<'b, FI>(frames: FI) -> Result<FirstPassOutput, FirstPassError>
where
FI: IntoIterator<Item = Frame<'b>>,
{
let mut header = None;
let mut file_info = None;
let mut events = Vec::new();
let mut event_mapping = GameEventMapping {
mapping: std::collections::HashMap::new(),
};
for mut frame in frames.into_iter() {
frame
.decompress()
.map_err(|e| FirstPassError::DecompressFrame)?;
let data = frame.data().ok_or(FirstPassError::NoDataFrame)?;
match frame.cmd {
DemoCommand::FileHeader => {
let raw: crate::csgo_proto::CDemoFileHeader = prost::Message::decode(data)?;
header = Some(raw);
}
DemoCommand::FileInfo => {
let raw: crate::csgo_proto::CDemoFileInfo = prost::Message::decode(data)?;
file_info = Some(raw);
}
DemoCommand::SignonPacket | DemoCommand::Packet => {
parse_packet(data, &mut events, &mut event_mapping)?;
}
DemoCommand::FullPacket => {
parse_fullpacket(data, &mut events, &mut event_mapping)?;
}
_ => {}
}
}
let header = header.ok_or(FirstPassError::MissingFileHeader)?;
let info = file_info.ok_or(FirstPassError::MissingFileInfo)?;
Ok(FirstPassOutput { header, info, events })
}
fn parse_fullpacket(
data: &[u8],
events: &mut Vec<DemoEvent>,
event_mapper: &mut GameEventMapping,
) -> Result<(), FirstPassError> {
let raw: crate::csgo_proto::CDemoFullPacket = prost::Message::decode(data)?;
// TODO
// Handle string table stuff
match raw.packet {
Some(packet) => {
inner_parse_packet(&packet, events, event_mapper)?;
Ok(())
}
None => Ok(()),
}
}
fn parse_packet(
data: &[u8],
events: &mut Vec<DemoEvent>,
event_mapper: &mut GameEventMapping,
) -> Result<(), FirstPassError> {
let raw: crate::csgo_proto::CDemoPacket = prost::Message::decode(data)?;
inner_parse_packet(&raw, events, event_mapper)?;
Ok(())
}
fn inner_parse_packet(
raw: &crate::csgo_proto::CDemoPacket,
events: &mut Vec<DemoEvent>,
event_mapper: &mut GameEventMapping,
) -> Result<(), FirstPassError> {
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()?;
let size = bitreader.read_varint()?;
let msg_bytes = bitreader.read_n_bytes(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::svc_ClearAllStringTables => {}
crate::netmessagetypes::NetmessageType::svc_CreateStringTable => {}
crate::netmessagetypes::NetmessageType::svc_UpdateStringTable => {}
crate::netmessagetypes::NetmessageType::GE_Source1LegacyGameEventList => {
let event_list: crate::csgo_proto::CsvcMsgGameEventList =
prost::Message::decode(msg_bytes.as_slice())?;
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())?;
events.push(DemoEvent::ServerInfo(raw));
}
crate::netmessagetypes::NetmessageType::net_SignonState => {}
crate::netmessagetypes::NetmessageType::net_Tick => {
let raw: crate::csgo_proto::CnetMsgTick =
prost::Message::decode(msg_bytes.as_slice())?;
events.push(DemoEvent::Tick(raw));
}
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::GE_Source1LegacyGameEvent => {
let raw: crate::csgo_proto::CMsgSource1LegacyGameEvent =
prost::Message::decode(msg_bytes.as_slice())?;
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())?;
events.push(DemoEvent::GameEvent(parsed));
}
None => {
println!("No parser for {:?}", name);
}
};
}
None => {
println!("Unknown Event - ID: {}", raw.eventid());
}
};
}
crate::netmessagetypes::NetmessageType::UM_SayText2 => {}
crate::netmessagetypes::NetmessageType::CS_UM_XpUpdate => {}
crate::netmessagetypes::NetmessageType::CS_UM_ServerRankUpdate => {
let raw: crate::csgo_proto::CcsUsrMsgServerRankUpdate =
prost::Message::decode(msg_bytes.as_slice())?;
events.push(DemoEvent::RankUpdate(raw));
}
crate::netmessagetypes::NetmessageType::CS_UM_ServerRankRevealAll => {
let raw: crate::csgo_proto::CcsUsrMsgServerRankRevealAll =
prost::Message::decode(msg_bytes.as_slice())?;
events.push(DemoEvent::RankReveal(raw));
}
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_PlayerStatsUpdate => {}
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(())
}

65
src/varint.rs Normal file
View File

@@ -0,0 +1,65 @@
pub fn parse_varint(input: &[u8]) -> Result<(&[u8], u32), ()> {
let mut result: u32 = 0;
for count in 0..5 {
let b = input.get(count).map(|c| *c as u32).ok_or(())?;
result |= (b & 127) << (7 * count);
if b & 0x80 == 0 {
return Ok((&input[count + 1..], result));
}
}
Ok((&input[5..], result))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_byte() {
let input = &[40, 0xff];
let (remaining, value) = parse_varint(input).unwrap();
assert_eq!(&[0xff], remaining);
assert_eq!(40, value);
}
#[test]
fn double_byte() {
let input = &[0x87, 0x60, 0xff];
let (remaining, value) = parse_varint(input).unwrap();
assert_eq!(&[0xff], remaining);
}
#[test]
fn three_byte() {
let input = &[0x87, 0x80, 0x61, 0xff];
let (remaining, value) = parse_varint(input).unwrap();
assert_eq!(&[0xff], remaining);
}
#[test]
fn four_byte() {
let input = &[0x87, 0x80, 0x88, 0x61, 0xff];
let (remaining, value) = parse_varint(input).unwrap();
assert_eq!(&[0xff], remaining);
}
#[test]
fn five_byte() {
let input = &[0x87, 0x80, 0x88, 0x89, 0x81, 0xff];
let (remaining, value) = parse_varint(input).unwrap();
assert_eq!(&[0xff], remaining);
}
}