Skip to content

Commit

Permalink
cs2: Parse game events.
Browse files Browse the repository at this point in the history
Also refactor the code to expose the DemoInfo format written by the parser.

The new incomplete CS2 parser is behind an environment variable for now to
prevent accidentally adding incomplete data to the HeadhshotBox server.
  • Loading branch information
abenea committed Sep 21, 2023
1 parent 4e8d10d commit 53ed085
Show file tree
Hide file tree
Showing 26 changed files with 2,441 additions and 1,067 deletions.
452 changes: 286 additions & 166 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

members = [
"csgo-demo-parser",
"cs2-demo",
"csdemoparser",
"demo-format",
"parsetest",
]

[workspace.dependencies]
bitstream-io = "1.7"
18 changes: 18 additions & 0 deletions cs2-demo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "cs2-demo"
version = "0.0.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bitstream-io.workspace = true
demo-format = { path = "../demo-format" }
protobuf = { version = "3.2.0", features = ["with-bytes"] }
snap = "1.1"
thiserror = "1.0"
tracing = "0.1"
paste = "1.0"

[build-dependencies]
protobuf-codegen = "3.2"
17 changes: 17 additions & 0 deletions cs2-demo/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use std::{ffi::OsStr, fs};

fn main() {
let proto_dir = "proto";
println!("cargo:rerun-if-changed={proto_dir}");

let proto_files = fs::read_dir(proto_dir)
.unwrap()
.filter_map(|res| res.map(|e| e.path()).ok())
.filter(|p| p.extension() == Some(OsStr::new("proto")))
.collect::<Vec<_>>();
protobuf_codegen::Codegen::new()
.include(proto_dir)
.inputs(proto_files)
.cargo_out_dir("proto")
.run_from_script();
}
1 change: 1 addition & 0 deletions cs2-demo/proto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
These proto files are copied from [SteamDatabase/GameTracking-CS2](https://github.com/SteamDatabase/GameTracking-CS2/tree/master/Protobufs).
160 changes: 160 additions & 0 deletions cs2-demo/proto/demo.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
enum EDemoCommands {
DEM_Error = -1;
DEM_Stop = 0;
DEM_FileHeader = 1;
DEM_FileInfo = 2;
DEM_SyncTick = 3;
DEM_SendTables = 4;
DEM_ClassInfo = 5;
DEM_StringTables = 6;
DEM_Packet = 7;
DEM_SignonPacket = 8;
DEM_ConsoleCmd = 9;
DEM_CustomData = 10;
DEM_CustomDataCallbacks = 11;
DEM_UserCmd = 12;
DEM_FullPacket = 13;
DEM_SaveGame = 14;
DEM_SpawnGroups = 15;
DEM_AnimationData = 16;
DEM_Max = 17;
DEM_IsCompressed = 64;
}

message CDemoFileHeader {
required string demo_file_stamp = 1;
optional int32 network_protocol = 2;
optional string server_name = 3;
optional string client_name = 4;
optional string map_name = 5;
optional string game_directory = 6;
optional int32 fullpackets_version = 7;
optional bool allow_clientside_entities = 8;
optional bool allow_clientside_particles = 9;
optional string addons = 10;
optional string demo_version_name = 11;
optional string demo_version_guid = 12;
optional int32 build_num = 13;
optional string game = 14;
}

message CGameInfo {
message CDotaGameInfo {
message CPlayerInfo {
optional string hero_name = 1;
optional string player_name = 2;
optional bool is_fake_client = 3;
optional uint64 steamid = 4;
optional int32 game_team = 5;
}

message CHeroSelectEvent {
optional bool is_pick = 1;
optional uint32 team = 2;
optional uint32 hero_id = 3;
}

optional uint64 match_id = 1;
optional int32 game_mode = 2;
optional int32 game_winner = 3;
repeated .CGameInfo.CDotaGameInfo.CPlayerInfo player_info = 4;
optional uint32 leagueid = 5;
repeated .CGameInfo.CDotaGameInfo.CHeroSelectEvent picks_bans = 6;
optional uint32 radiant_team_id = 7;
optional uint32 dire_team_id = 8;
optional string radiant_team_tag = 9;
optional string dire_team_tag = 10;
optional uint32 end_time = 11;
}

optional .CGameInfo.CDotaGameInfo dota = 4;
}

message CDemoFileInfo {
optional float playback_time = 1;
optional int32 playback_ticks = 2;
optional int32 playback_frames = 3;
optional .CGameInfo game_info = 4;
}

message CDemoPacket {
optional bytes data = 3;
}

message CDemoFullPacket {
optional .CDemoStringTables string_table = 1;
optional .CDemoPacket packet = 2;
}

message CDemoSaveGame {
optional bytes data = 1;
optional fixed64 steam_id = 2;
optional fixed64 signature = 3;
optional int32 version = 4;
}

message CDemoSyncTick {
}

message CDemoConsoleCmd {
optional string cmdstring = 1;
}

message CDemoSendTables {
optional bytes data = 1;
}

message CDemoClassInfo {
message class_t {
optional int32 class_id = 1;
optional string network_name = 2;
optional string table_name = 3;
}

repeated .CDemoClassInfo.class_t classes = 1;
}

message CDemoCustomData {
optional int32 callback_index = 1;
optional bytes data = 2;
}

message CDemoCustomDataCallbacks {
repeated string save_id = 1;
}

message CDemoAnimationData {
optional sint32 entity_id = 1;
optional int32 start_tick = 2;
optional int32 end_tick = 3;
optional bytes data = 4;
optional int64 data_checksum = 5;
}

message CDemoStringTables {
message items_t {
optional string str = 1;
optional bytes data = 2;
}

message table_t {
optional string table_name = 1;
repeated .CDemoStringTables.items_t items = 2;
repeated .CDemoStringTables.items_t items_clientside = 3;
optional int32 table_flags = 4;
}

repeated .CDemoStringTables.table_t tables = 1;
}

message CDemoStop {
}

message CDemoUserCmd {
optional int32 cmd_number = 1;
optional bytes data = 2;
}

message CDemoSpawnGroups {
repeated bytes msgs = 3;
}
49 changes: 49 additions & 0 deletions cs2-demo/proto/gameevents.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
enum EBaseGameEvents {
GE_VDebugGameSessionIDEvent = 200;
GE_PlaceDecalEvent = 201;
GE_ClearWorldDecalsEvent = 202;
GE_ClearEntityDecalsEvent = 203;
GE_ClearDecalsForSkeletonInstanceEvent = 204;
GE_Source1LegacyGameEventList = 205;
GE_Source1LegacyListenEvents = 206;
GE_Source1LegacyGameEvent = 207;
GE_SosStartSoundEvent = 208;
GE_SosStopSoundEvent = 209;
GE_SosSetSoundEventParams = 210;
GE_SosSetLibraryStackFields = 211;
GE_SosStopSoundEventHash = 212;
}

message CMsgSource1LegacyGameEventList {
message key_t {
optional int32 type = 1;
optional string name = 2;
}

message descriptor_t {
optional int32 eventid = 1;
optional string name = 2;
repeated .CMsgSource1LegacyGameEventList.key_t keys = 3;
}

repeated .CMsgSource1LegacyGameEventList.descriptor_t descriptors = 1;
}

message CMsgSource1LegacyGameEvent {
message key_t {
optional int32 type = 1;
optional string val_string = 2;
optional float val_float = 3;
optional int32 val_long = 4;
optional int32 val_short = 5;
optional int32 val_byte = 6;
optional bool val_bool = 7;
optional uint64 val_uint64 = 8;
}

optional string event_name = 1;
optional int32 eventid = 2;
repeated .CMsgSource1LegacyGameEvent.key_t keys = 3;
optional int32 server_tick = 4;
optional int32 passthrough = 5;
}
67 changes: 67 additions & 0 deletions cs2-demo/src/demo_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::fmt;

use super::packet::Packet;
use super::proto::demo::{CDemoFileHeader, CDemoPacket, CDemoSendTables};
use super::{Error, Result};
use protobuf::Message;

#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum DemoCommand {
/// The last packet dispatched. It means there are no more packet left to
/// parse.
Stop,
/// The first packet.
FileHeader(CDemoFileHeader),
FileInfo,
/// A sync tick. It contains no data.
SyncTick,
SendTables(CDemoSendTables),
ClassInfo,
StringTables,
Packet(Packet),
ConsoleCmd,
CustomData,
CustomDataCallbacks,
UserCmd,
FullPacket,
SaveGame,
SpawnGroups,
AnimationData,
}

impl DemoCommand {
pub fn try_new(cmd: u32, data: &[u8]) -> Result<Self> {
let content = match cmd {
0 => DemoCommand::Stop,
1 => DemoCommand::FileHeader(CDemoFileHeader::parse_from_bytes(data)?),
2 => DemoCommand::FileInfo,
3 => DemoCommand::SyncTick,
4 => DemoCommand::SendTables(CDemoSendTables::parse_from_bytes(data)?),
5 => DemoCommand::ClassInfo,
6 => DemoCommand::StringTables,
// SignonPacket seems to be identical to Packet.
7 | 8 => DemoCommand::Packet(Packet::try_new(CDemoPacket::parse_from_bytes(data)?)?),
9 => DemoCommand::ConsoleCmd,
10 => DemoCommand::CustomData,
11 => DemoCommand::CustomDataCallbacks,
12 => DemoCommand::UserCmd,
13 => DemoCommand::FullPacket,
14 => DemoCommand::SaveGame,
15 => DemoCommand::SpawnGroups,
16 => DemoCommand::AnimationData,
_ => return Err(Error::UnknownPacketCommand(cmd)),
};
Ok(content)
}
}

impl fmt::Display for DemoCommand {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
DemoCommand::FileHeader(m) => write!(f, "FileHeader {}", m),
DemoCommand::SendTables(m) => write!(f, "SendTables {} bytes", m.data().len()),
_ => write!(f, "{:?}", self),
}
}
}
58 changes: 58 additions & 0 deletions cs2-demo/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
mod demo_command;
pub mod packet;
pub mod proto;

use self::proto::demo::EDemoCommands;
use protobuf::CodedInputStream;
use std::io;

pub use self::demo_command::DemoCommand;

/// Error type for this library.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Protobuf(#[from] protobuf::Error),
#[error("invalid demo type (expected: PBDEMS2, found: {found})")]
InvalidDemoType { found: String },
#[error("unknown packet command found: {0}")]
UnknownPacketCommand(u32),
#[error(transparent)]
Decompression(#[from] snap::Error),
}

pub type Result<T> = std::result::Result<T, Error>;

pub type Tick = i32;

pub struct DemoParser<'a> {
reader: CodedInputStream<'a>,
}

impl<'a> DemoParser<'a> {
pub fn try_new_after_demo_type(read: &'a mut dyn io::Read) -> Result<Self> {
let mut reader = CodedInputStream::new(read);
reader.skip_raw_bytes(8)?;
Ok(Self { reader })
}

pub fn parse_next_demo_command(&mut self) -> Result<Option<(Tick, DemoCommand)>> {
if self.reader.eof()? {
return Ok(None);
}
let cmd_flags = self.reader.read_raw_varint32()?;
let cmd = cmd_flags & !(EDemoCommands::DEM_IsCompressed as u32);
let compressed = (cmd_flags & (EDemoCommands::DEM_IsCompressed as u32)) != 0;
let tick = self.reader.read_raw_varint32()? as i32;
let size = self.reader.read_raw_varint32()?;
let data = self.reader.read_raw_bytes(size)?;
let data = if compressed {
snap::raw::Decoder::new().decompress_vec(data.as_slice())?
} else {
data
};
Ok(Some((tick, DemoCommand::try_new(cmd, &data)?)))
}
}
Loading

0 comments on commit 53ed085

Please sign in to comment.