Initial version
This commit is contained in:
200
src/bitreader.rs
Normal file
200
src/bitreader.rs
Normal 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
44
src/container.rs
Normal 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
55
src/democmd.rs
Normal 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
91
src/frame.rs
Normal 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
293
src/game_event.rs
Normal 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
22
src/lib.rs
Normal 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
390
src/netmessagetypes.rs
Normal 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
10
src/packet.rs
Normal 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
229
src/parser.rs
Normal 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
65
src/varint.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user