Created
November 19, 2022 12:18
-
-
Save ZephyrBlu/e6d6ad4cb3e28d1fc7adab86e6b8848c to your computer and use it in GitHub Desktop.
Rust SC2 Parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::protocol::Int; | |
use crate::protocol::ProtocolTypeInfo; | |
use crate::protocol::Struct; | |
use std::cmp::min; | |
use std::collections::HashMap; | |
use std::str; | |
use serde::Serialize; | |
pub struct BitPackedBuffer { | |
data: Vec<u8>, | |
used: usize, | |
next: u8, | |
nextbits: usize, | |
bigendian: bool, | |
} | |
pub struct BitPackedDecoder<'a> { | |
pub buffer: BitPackedBuffer, | |
typeinfos: &'a [ProtocolTypeInfo<'a>], | |
} | |
pub struct VersionedDecoder<'a> { | |
pub buffer: BitPackedBuffer, | |
typeinfos: &'a [ProtocolTypeInfo<'a>], | |
} | |
impl BitPackedBuffer { | |
fn new(contents: Vec<u8>) -> BitPackedBuffer { | |
BitPackedBuffer { | |
data: contents, | |
used: 0, | |
next: 0, | |
nextbits: 0, | |
bigendian: true, | |
} | |
} | |
fn done(&self) -> bool { | |
self.used >= self.data.len() | |
} | |
fn used_bits(&self) -> usize { | |
(self.used * 8) - self.nextbits | |
} | |
fn byte_align(&mut self) { | |
self.nextbits = 0; | |
} | |
fn read_aligned_bytes(&mut self, bytes: usize) -> &[u8] { | |
self.byte_align(); | |
let data = &self.data[self.used..self.used + bytes]; | |
self.used += bytes; | |
if data.len() != bytes as usize { | |
panic!("TruncatedError"); | |
} | |
data | |
} | |
fn read_bits(&mut self, bits: u8) -> u128 { | |
// usually much smaller than u128, but can be in rare cases | |
let mut result: u128 = 0; | |
let mut resultbits: u8 = 0; | |
while resultbits != bits { | |
if self.nextbits == 0 { | |
if self.done() { | |
panic!("TruncatedError"); | |
} | |
self.next = self.data[self.used]; | |
self.used += 1; | |
self.nextbits = 8; | |
} | |
let copybits: u8 = min((bits - resultbits) as usize, self.nextbits) as u8; | |
let shifted_copybits: u8 = ((1 << copybits) - 1) as u8; | |
let copy: u128 = (self.next & shifted_copybits) as u128; | |
if self.bigendian { | |
result |= copy << (bits - resultbits - copybits); | |
} else { | |
result |= copy << resultbits; | |
} | |
let shifted_next: u8 = (self.next as u16 >> copybits) as u8; | |
self.next = shifted_next; | |
self.nextbits -= copybits as usize; | |
resultbits += copybits as u8; | |
} | |
result | |
} | |
fn read_unaligned_bytes(&mut self, bytes: u8) -> String { | |
let mut read_bytes = String::new(); | |
for _ in 0..bytes { | |
read_bytes.push_str(&self.read_bits(8).to_string()); | |
} | |
read_bytes | |
} | |
} | |
pub type EventEntry = (String, DecoderResult); | |
#[derive(Clone, Debug, Serialize)] | |
pub enum DecoderResult { | |
Name(String), | |
Value(i64), | |
Blob(String), | |
Array(Vec<DecoderResult>), | |
DataFragment(u32), | |
Pair((i64, i16)), | |
Gameloop((String, i64)), | |
Bool(bool), | |
Struct(Vec<EventEntry>), | |
Null, | |
} | |
pub trait Decoder { | |
fn instance<'a>( | |
&'a mut self, | |
typeinfos: &[ProtocolTypeInfo], | |
typeid: &u8, | |
) -> DecoderResult { | |
let typeid_size = *typeid as usize; | |
if typeid_size >= typeinfos.len() { | |
panic!("CorruptedError"); | |
} | |
let typeinfo = &typeinfos[typeid_size]; | |
// println!("current typeinfo {:?} {:?}", typeinfo, typeid); | |
match typeinfo { | |
ProtocolTypeInfo::Int(bounds) => self._int(bounds), | |
ProtocolTypeInfo::Blob(bounds) => self._blob(bounds), | |
ProtocolTypeInfo::Bool => self._bool(), | |
ProtocolTypeInfo::Array(bounds, typeid) => self._array(bounds, typeid), | |
ProtocolTypeInfo::Null => DecoderResult::Null, | |
ProtocolTypeInfo::BitArray(bounds) => self._bitarray(bounds), | |
ProtocolTypeInfo::Optional(typeid) => self._optional(typeid), | |
ProtocolTypeInfo::FourCC => self._fourcc(), | |
ProtocolTypeInfo::Choice(bounds, fields) => self._choice(bounds, fields), | |
ProtocolTypeInfo::Struct(fields) => self._struct(fields), | |
} | |
} | |
fn byte_align(buffer: &mut BitPackedBuffer) { | |
buffer.byte_align() | |
} | |
fn done(buffer: &BitPackedBuffer) -> bool { | |
buffer.done() | |
} | |
fn used_bits(buffer: &BitPackedBuffer) -> usize { | |
buffer.used_bits() | |
} | |
fn _int(&mut self, bounds: &Int) -> DecoderResult; | |
fn _blob(&mut self, bounds: &Int) -> DecoderResult; | |
fn _bool(&mut self) -> DecoderResult; | |
fn _array(&mut self, bounds: &Int, typeid: &u8) -> DecoderResult; | |
fn _bitarray(&mut self, bounds: &Int) -> DecoderResult; | |
fn _optional(&mut self, typeid: &u8) -> DecoderResult; | |
fn _fourcc(&mut self) -> DecoderResult; | |
fn _choice( | |
&mut self, | |
bounds: &Int, | |
fields: &HashMap<i64, (&str, u8)>, | |
) -> DecoderResult; | |
fn _struct<'a>(&'a mut self, fields: &[Struct]) -> DecoderResult; | |
} | |
impl<'a> BitPackedDecoder<'a> { | |
pub fn new( | |
contents: Vec<u8>, | |
typeinfos: &'a [ProtocolTypeInfo<'a>], | |
) -> BitPackedDecoder<'a> { | |
let buffer = BitPackedBuffer::new(contents); | |
BitPackedDecoder { buffer, typeinfos } | |
} | |
} | |
impl Decoder for BitPackedDecoder<'_> { | |
fn _int(&mut self, bounds: &Int) -> DecoderResult { | |
let read = self.buffer.read_bits(bounds.1); | |
DecoderResult::Value(bounds.0 + read as i64) | |
} | |
fn _blob(&mut self, bounds: &Int) -> DecoderResult { | |
match self._int(bounds) { | |
DecoderResult::Value(value) => DecoderResult::Blob( | |
str::from_utf8(self.buffer.read_aligned_bytes(value as usize)) | |
.unwrap_or("") | |
.to_string() | |
), | |
_other => panic!("_int didn't return DecoderResult::Value {:?}", _other), | |
} | |
} | |
fn _bool(&mut self) -> DecoderResult { | |
match self._int(&Int(0, 1)) { | |
DecoderResult::Value(value) => DecoderResult::Bool(value != 0), | |
_other => panic!("_int didn't return DecoderResult::Value {:?}", _other), | |
} | |
} | |
fn _array(&mut self, bounds: &Int, typeid: &u8) -> DecoderResult { | |
match self._int(bounds) { | |
DecoderResult::Value(value) => { | |
let mut array = Vec::with_capacity(value as usize); | |
for _i in 0..value { | |
let data = match self.instance(self.typeinfos, typeid) { | |
DecoderResult::Value(value) => DecoderResult::DataFragment(value as u32), | |
DecoderResult::Struct(values) => DecoderResult::Struct(values), | |
_other => panic!("instance returned DecoderResult::{:?}", _other), | |
}; | |
array.push(data); | |
} | |
DecoderResult::Array(array) | |
} | |
_other => panic!("_int didn't return DecoderResult::Value {:?}", _other), | |
} | |
} | |
fn _bitarray(&mut self, bounds: &Int) -> DecoderResult { | |
match self._int(bounds) { | |
DecoderResult::Value(value) => { | |
let bytes = self.buffer.read_bits(value as u8); | |
// DecoderResult::Pair((value, bytes as i16)) | |
DecoderResult::Pair((0, 0)) | |
} | |
_other => panic!("instance didn't return DecoderResult::Value {:?}", _other), | |
} | |
} | |
fn _optional(&mut self, typeid: &u8) -> DecoderResult { | |
match self._bool() { | |
DecoderResult::Bool(value) => { | |
if value { | |
self.instance(self.typeinfos, typeid) | |
} else { | |
DecoderResult::Null | |
} | |
} | |
_other => panic!("_bool didn't return DecoderResult::Bool {:?}", _other), | |
} | |
} | |
fn _fourcc(&mut self) -> DecoderResult { | |
DecoderResult::Blob(self.buffer.read_unaligned_bytes(4)) | |
} | |
fn _choice( | |
&mut self, | |
bounds: &Int, | |
fields: &HashMap<i64, (&str, u8)>, | |
) -> DecoderResult { | |
let tag = match self._int(bounds) { | |
DecoderResult::Value(value) => value, | |
_other => panic!("_int didn't return DecoderResult::Value {:?}", _other), | |
}; | |
if !fields.contains_key(&tag) { | |
panic!("CorruptedError"); | |
} | |
let field = &fields[&tag]; | |
let choice_res = match self.instance(self.typeinfos, &field.1) { | |
DecoderResult::Value(value) => value, | |
_other => panic!("didn't find DecoderResult::Value"), | |
}; | |
// println!("_choice instance returned {:?} {:?}", field.0, choice_res); | |
DecoderResult::Gameloop((field.0.to_owned(), choice_res)) | |
} | |
fn _struct<'a>(&mut self, fields: &[Struct]) -> DecoderResult { | |
let mut result = Vec::with_capacity(fields.len()); | |
for field in fields { | |
// appears that this isn't needed since field is never parent | |
// match fields.into_iter().find(|f| f.2 as i64 == tag) { | |
// Some(field) => { | |
// if field.0 == "__parent" { | |
// let parent = self.instance(self.typeinfos, field.1); | |
// } else { | |
// let field_value = match self.instance(self.typeinfos, field.1) { | |
// DecoderResult::Value(value) => value, | |
// _other => panic!("field.1 is not a value: {:?}", field), | |
// }; | |
// result.insert(field.0.as_str(), field_value as u8); | |
// } | |
// }, | |
// None => self._skip_instance(), | |
// }; | |
// field always seems to exist? | |
let field_value = self.instance(self.typeinfos, &field.1); | |
// result.insert(field.0, field_value); | |
result.push((field.0.to_string(), field_value)); | |
} | |
DecoderResult::Struct(result) | |
} | |
} | |
impl<'a> VersionedDecoder<'a> { | |
pub fn new( | |
contents: Vec<u8>, | |
typeinfos: &'a [ProtocolTypeInfo<'a>], | |
) -> VersionedDecoder<'a> { | |
let buffer = BitPackedBuffer::new(contents); | |
VersionedDecoder { buffer, typeinfos } | |
} | |
fn expect_skip(&mut self, expected: u8) { | |
let bits_read = self.buffer.read_bits(8); | |
if bits_read as u8 != expected { | |
panic!("CorruptedError"); | |
} | |
} | |
fn _vint(&mut self) -> i64 { | |
let mut buf = self.buffer.read_bits(8) as i64; | |
let negative = buf & 1; | |
let mut result: i64 = (buf >> 1) & 0x3f; | |
let mut bits = 6; | |
while (buf & 0x80) != 0 { | |
buf = self.buffer.read_bits(8) as i64; | |
result |= (buf & 0x7f) << bits; | |
bits += 7; | |
} | |
if negative != 0 { | |
-result | |
} else { | |
result | |
} | |
} | |
fn _skip_instance(&mut self) { | |
let skip = self.buffer.read_bits(8); | |
if skip == 0 { | |
// array | |
let length = self._vint(); | |
for _ in 0..length { | |
self._skip_instance(); | |
} | |
} else if skip == 1 { | |
// bitblob | |
let length = self._vint(); | |
self.buffer.read_aligned_bytes(((length + 7) / 8) as usize); | |
} else if skip == 2 { | |
// blob | |
let length = self._vint(); | |
self.buffer.read_aligned_bytes(length as usize); | |
} else if skip == 3 { | |
// choice | |
let tag = self._vint(); | |
self._skip_instance(); | |
} else if skip == 4 { | |
// optional | |
let exists = self.buffer.read_bits(8) != 0; | |
if exists { | |
self._skip_instance(); | |
} | |
} else if skip == 5 { | |
// struct | |
let length = self._vint(); | |
for _ in 0..length { | |
let tag = self._vint(); | |
self._skip_instance(); | |
} | |
} else if skip == 6 { | |
// u8 | |
self.buffer.read_aligned_bytes(1); | |
} else if skip == 7 { | |
// u32 | |
self.buffer.read_aligned_bytes(4); | |
} else if skip == 8 { | |
// u64 | |
self.buffer.read_aligned_bytes(8); | |
} else if skip == 9 { | |
// vint | |
self._vint(); | |
} | |
} | |
} | |
impl Decoder for VersionedDecoder<'_> { | |
fn _int(&mut self, bounds: &Int) -> DecoderResult { | |
self.expect_skip(9); | |
DecoderResult::Value(self._vint()) | |
} | |
fn _blob(&mut self, bounds: &Int) -> DecoderResult { | |
self.expect_skip(2); | |
let length = self._vint(); | |
DecoderResult::Blob( | |
str::from_utf8(self.buffer.read_aligned_bytes(length as usize)) | |
.unwrap_or("") | |
.to_string(), | |
) | |
} | |
fn _bool(&mut self) -> DecoderResult { | |
self.expect_skip(6); | |
DecoderResult::Bool(self.buffer.read_bits(8) != 0) | |
} | |
fn _array(&mut self, bounds: &Int, typeid: &u8) -> DecoderResult { | |
self.expect_skip(0); | |
let length = self._vint(); | |
let mut array = Vec::with_capacity(length as usize); | |
for _ in 0..length { | |
let data = match self.instance(self.typeinfos, typeid) { | |
DecoderResult::Value(value) => DecoderResult::DataFragment(value as u32), | |
DecoderResult::Struct(values) => DecoderResult::Struct(values), | |
DecoderResult::Blob(value) => DecoderResult::Blob(value), | |
_other => panic!("instance returned DecoderResult::{:?}", _other), | |
}; | |
array.push(data); | |
} | |
DecoderResult::Array(array) | |
} | |
fn _bitarray(&mut self, bounds: &Int) -> DecoderResult { | |
self.expect_skip(1); | |
let length = self._vint(); | |
let bytes = self.buffer.read_aligned_bytes((length as usize + 7) / 8); | |
let mut value: i16 = 0; | |
for v in bytes { | |
value += *v as i16; | |
} | |
// DecoderResult::Pair((length, value)) | |
DecoderResult::Pair((0, 0)) | |
} | |
fn _optional(&mut self, typeid: &u8) -> DecoderResult { | |
self.expect_skip(4); | |
if self.buffer.read_bits(8) != 0 { | |
self.instance(self.typeinfos, typeid) | |
} else { | |
DecoderResult::Null | |
} | |
} | |
fn _fourcc(&mut self) -> DecoderResult { | |
self.expect_skip(7); | |
DecoderResult::Blob( | |
str::from_utf8(self.buffer.read_aligned_bytes(4)) | |
.unwrap_or("") | |
.to_string() | |
) | |
} | |
fn _choice( | |
&mut self, | |
bounds: &Int, | |
fields: &HashMap<i64, (&str, u8)>, | |
) -> DecoderResult { | |
self.expect_skip(3); | |
let tag = self._vint(); | |
if !fields.contains_key(&tag) { | |
self._skip_instance(); | |
return DecoderResult::Pair((0, 0)); | |
} | |
let field = &fields[&tag]; | |
let choice_res = match self.instance(self.typeinfos, &field.1) { | |
DecoderResult::Value(value) => value, | |
_other => panic!("didn't find DecoderResult::Value"), | |
}; | |
// println!("_choice instance returned {:?} {:?}", field.0, choice_res); | |
DecoderResult::Gameloop((field.0.to_owned(), choice_res)) | |
} | |
fn _struct<'a>(&mut self, fields: &[Struct]) -> DecoderResult { | |
self.expect_skip(5); | |
// let mut result = HashMap::<&str, DecoderResult>::new(); | |
let mut result = Vec::with_capacity(fields.len()); | |
let length = self._vint(); | |
for _ in 0..length { | |
let tag = self._vint(); | |
// appears that this isn't needed since field is never parent | |
// match fields.into_iter().find(|f| f.2 as i64 == tag) { | |
// Some(field) => { | |
// if field.0 == "__parent" { | |
// let parent = self.instance(self.typeinfos, field.1); | |
// } else { | |
// let field_value = match self.instance(self.typeinfos, field.1) { | |
// DecoderResult::Value(value) => value, | |
// _other => panic!("field.1 is not a value: {:?}", field), | |
// }; | |
// result.insert(field.0.as_str(), field_value as u8); | |
// } | |
// }, | |
// None => self._skip_instance(), | |
// }; | |
// field always seems to exist? | |
let field = fields.iter().find(|f| f.2 as i64 == tag).unwrap(); | |
let field_value = self.instance(self.typeinfos, &field.1); | |
// result.insert(field.0, field_value); | |
result.push((field.0.to_string(), field_value)) | |
} | |
DecoderResult::Struct(result) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mod player_stats; | |
mod object_event; | |
use crate::replay::{Event, Parsed}; | |
use crate::game::Game; | |
use crate::decoders::DecoderResult; | |
use player_stats::PlayerStatsEvent; | |
use object_event::ObjectEvent; | |
pub struct EventParser<'a> { | |
replay: &'a Parsed, | |
game: &'a mut Game, | |
} | |
impl<'a> EventParser<'a> { | |
pub fn new(replay: &'a Parsed, game: &'a mut Game) -> EventParser<'a> { | |
EventParser { | |
replay, | |
game, | |
} | |
} | |
pub fn parse(&mut self, event: &Event) -> Result<(), &'static str> { | |
if let DecoderResult::Name(name) = &event.entries.last().unwrap().1 { | |
match name.as_str() { | |
"NNet.Replay.Tracker.SPlayerStatsEvent" => { | |
PlayerStatsEvent::new(self.game, event); | |
Ok(()) | |
}, | |
"NNet.Replay.Tracker.SUnitInitEvent" | | |
"NNet.Replay.Tracker.SUnitBornEvent" | | |
"NNet.Replay.Tracker.SUnitTypeChangeEvent" => { | |
ObjectEvent::new(self.game, event); | |
Ok(()) | |
}, | |
_other => Ok(()), | |
} | |
} else { | |
Err("Found event without name") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::collections::HashMap; | |
pub struct Game { | |
pub workers_active: [u8; 2], | |
pub minerals_collected: [u16; 2], | |
pub minerals_lost: [u16; 2], | |
pub gas_collected: [u16; 2], | |
pub gas_lost: [u16; 2], | |
pub collection_rate: Vec<Vec<(u16, u16)>>, | |
pub unspent_resources: Vec<Vec<(u16, u16)>>, | |
pub builds: Vec<Vec<String>>, | |
pub buildings: HashMap<u32, u8>, | |
} | |
impl Game { | |
pub fn new() -> Game { | |
let workers_active: [u8; 2] = [0, 0]; | |
let minerals_collected: [u16; 2] = [0, 0]; | |
let minerals_lost: [u16; 2] = [0, 0]; | |
let gas_collected: [u16; 2] = [0, 0]; | |
let gas_lost: [u16; 2] = [0, 0]; | |
let collection_rate: Vec<Vec<(u16, u16)>> = vec![vec![], vec![]]; | |
let unspent_resources: Vec<Vec<(u16, u16)>> = vec![vec![], vec![]]; | |
let builds: Vec<Vec<String>> = vec![vec![], vec![]]; | |
let buildings: HashMap<u32, u8> = HashMap::new(); | |
Game { | |
workers_active, | |
minerals_collected, | |
minerals_lost, | |
gas_collected, | |
gas_lost, | |
collection_rate, | |
unspent_resources, | |
builds, | |
buildings, | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::collections::HashMap; | |
use std::fs::File; | |
use std::io::copy; | |
use std::io::prelude::*; | |
use std::io::BufReader; | |
use std::io::SeekFrom; | |
use std::path::PathBuf; | |
// use bzip2::Decompress; | |
// use bzip2_rs::decoder::Decoder; | |
use bzip2_rs::DecoderReader; | |
const MPQ_FILE_IMPLODE: u32 = 0x00000100; | |
const MPQ_FILE_COMPRESS: u32 = 0x00000200; | |
const MPQ_FILE_ENCRYPTED: u32 = 0x00010000; | |
const MPQ_FILE_FIX_KEY: u32 = 0x00020000; | |
const MPQ_FILE_SINGLE_UNIT: u32 = 0x01000000; | |
const MPQ_FILE_DELETE_MARKER: u32 = 0x02000000; | |
const MPQ_FILE_SECTOR_CRC: u32 = 0x04000000; | |
const MPQ_FILE_EXISTS: u32 = 0x80000000; | |
const MPQ_MAGIC_A: [u8; 4] = [77, 80, 81, 26]; | |
const MPQ_MAGIC_B: [u8; 4] = [77, 80, 81, 27]; | |
#[derive(Copy, Clone)] | |
enum MPQHash { | |
TableOffset = 0, | |
HashA = 1, | |
HashB = 2, | |
Table = 3, | |
} | |
pub struct MPQFileHeader { | |
magic: [u8; 4], | |
offset: u32, | |
header_size: u32, | |
archive_size: u32, | |
format_version: u16, | |
sector_size_shift: u16, | |
hash_table_offset: u32, | |
block_table_offset: u32, | |
hash_table_entries: u32, | |
block_table_entries: u32, | |
pub user_data_header: Option<MPQUserDataHeader>, | |
extended: Option<MPQFileHeaderExt>, | |
} | |
// MPQFileHeader.struct_format = '< 4s 2I 2H 4I' = 4 + 8 + 4 + 16 = 32 bytes | |
struct MPQFileHeaderExt { | |
extended_block_table_offset: i64, | |
hash_table_offset_high: i16, | |
block_table_offset_high: i16, | |
} | |
// MPQFileHeaderExt.struct_format = 'q 2h' | |
pub struct MPQUserDataHeader { | |
magic: [u8; 4], | |
user_data_size: u32, | |
mpq_header_offset: u32, | |
user_data_header_size: u32, | |
pub content: Vec<u8>, | |
} | |
// MPQUserDataHeader.struct_format = '< 4s 3I' | |
#[derive(Debug, Copy, Clone)] | |
struct HashTableEntry { | |
hash_a: u32, | |
hash_b: u32, | |
locale: u16, | |
platform: u16, | |
block_table_index: u32, | |
} | |
// MPQHashTableEntry.struct_format = '2I 2H I' | |
#[derive(Debug, Copy, Clone)] | |
struct BlockTableEntry { | |
offset: u32, | |
archived_size: usize, | |
size: usize, | |
flags: u32, | |
} | |
// MPQBlockTableEntry.struct_format = '4I' | |
#[derive(Debug)] | |
enum MPQTableEntry { | |
Hash(HashTableEntry), | |
Block(BlockTableEntry), | |
} | |
pub struct MPQArchive { | |
pub file: BufReader<File>, | |
pub header: MPQFileHeader, | |
hash_table: Vec<MPQTableEntry>, | |
block_table: Vec<MPQTableEntry>, | |
encryption_table: HashMap<u64, u64>, | |
compressed: Vec<u8>, | |
decompressed_offsets: Vec<usize>, | |
compression_type: u8, | |
} | |
impl MPQArchive { | |
pub fn new(filename: &str) -> MPQArchive { | |
let file = File::open(filename).expect("Failed to read replay file"); | |
let mut reader = BufReader::new(file); | |
let header = MPQArchive::read_header(&mut reader); | |
let encryption_table = MPQArchive::prepare_encryption_table(); | |
let hash_table = MPQArchive::read_table(&mut reader, &header, &encryption_table, "hash"); | |
let block_table = MPQArchive::read_table(&mut reader, &header, &encryption_table, "block"); | |
// let block_table_entry = MPQArchive::read_block_entry( | |
// "(listfile)", | |
// &encryption_table, | |
// &hash_table, | |
// &block_table, | |
// ).expect("Couldn't find block table entry"); | |
// let contents = MPQArchive::_read_file(&mut reader, &header, &block_table_entry, false); | |
let compressed = vec![]; | |
let decompressed_offsets = vec![]; | |
let compression_type = 0; | |
MPQArchive { | |
file: reader, | |
header, | |
hash_table, | |
block_table, | |
encryption_table, | |
compressed, | |
decompressed_offsets, | |
compression_type, | |
} | |
} | |
fn read_header(file: &mut BufReader<File>) -> MPQFileHeader { | |
let mut magic = [0; 4]; | |
file.read_exact(&mut magic).unwrap(); | |
file.seek(SeekFrom::Start(0)); | |
match magic { | |
MPQ_MAGIC_A => MPQArchive::read_mpq_header(magic, file, None), | |
MPQ_MAGIC_B => { | |
let user_data_header = MPQArchive::read_mpq_user_data_header(magic, file); | |
MPQArchive::read_mpq_header(magic, file, Some(user_data_header)) | |
} | |
_other => panic!("Invalid file header"), | |
} | |
} | |
fn read_mpq_header( | |
magic: [u8; 4], | |
file: &mut BufReader<File>, | |
user_data_header: Option<MPQUserDataHeader>, | |
) -> MPQFileHeader { | |
let mut header_size = [0; 4]; | |
let mut archive_size = [0; 4]; | |
let mut format_version = [0; 2]; | |
let mut sector_size_shift = [0; 2]; | |
let mut hash_table_offset = [0; 4]; | |
let mut block_table_offset = [0; 4]; | |
let mut hash_table_entries = [0; 4]; | |
let mut block_table_entries = [0; 4]; | |
let offset = match user_data_header.as_ref() { | |
Some(header) => header.mpq_header_offset, | |
None => 0, | |
}; | |
file.seek(SeekFrom::Start(offset as u64 + 4)) | |
.expect("Failed to seek"); | |
file.read_exact(&mut header_size).unwrap(); | |
file.read_exact(&mut archive_size).unwrap(); | |
file.read_exact(&mut format_version).unwrap(); | |
file.read_exact(&mut sector_size_shift).unwrap(); | |
file.read_exact(&mut hash_table_offset).unwrap(); | |
file.read_exact(&mut block_table_offset).unwrap(); | |
file.read_exact(&mut hash_table_entries).unwrap(); | |
file.read_exact(&mut block_table_entries).unwrap(); | |
let mut header_extension = None; | |
let format_version_value = u16::from_le_bytes(format_version); | |
if format_version_value == 1 { | |
let mut extended_block_table_offset = [0; 8]; | |
let mut hash_table_offset_high = [0; 2]; | |
let mut block_table_offset_high = [0; 2]; | |
file.read_exact(&mut extended_block_table_offset).unwrap(); | |
file.read_exact(&mut hash_table_offset_high).unwrap(); | |
file.read_exact(&mut block_table_offset_high).unwrap(); | |
header_extension = Some(MPQFileHeaderExt { | |
extended_block_table_offset: i64::from_ne_bytes(extended_block_table_offset), | |
hash_table_offset_high: i16::from_ne_bytes(hash_table_offset_high), | |
block_table_offset_high: i16::from_ne_bytes(block_table_offset_high), | |
}); | |
} | |
MPQFileHeader { | |
magic, | |
offset, | |
header_size: u32::from_le_bytes(header_size), | |
archive_size: u32::from_le_bytes(archive_size), | |
format_version: format_version_value, | |
sector_size_shift: u16::from_le_bytes(sector_size_shift), | |
hash_table_offset: u32::from_le_bytes(hash_table_offset), | |
block_table_offset: u32::from_le_bytes(block_table_offset), | |
hash_table_entries: u32::from_le_bytes(hash_table_entries), | |
block_table_entries: u32::from_le_bytes(block_table_entries), | |
user_data_header, | |
extended: header_extension, | |
} | |
} | |
fn read_mpq_user_data_header(magic: [u8; 4], file: &mut BufReader<File>) -> MPQUserDataHeader { | |
let mut user_data_size = [0; 4]; | |
let mut mpq_header_offset = [0; 4]; | |
let mut user_data_header_size = [0; 4]; | |
file.seek(SeekFrom::Start(4)); | |
file.read_exact(&mut user_data_size).unwrap(); | |
file.read_exact(&mut mpq_header_offset).unwrap(); | |
file.read_exact(&mut user_data_header_size).unwrap(); | |
let user_data_header_size_value = u32::from_le_bytes(user_data_header_size); | |
let mut content = vec![0; user_data_header_size_value as usize]; | |
file.read_exact(&mut content).unwrap(); | |
MPQUserDataHeader { | |
magic, | |
user_data_size: u32::from_le_bytes(user_data_size), | |
mpq_header_offset: u32::from_le_bytes(mpq_header_offset), | |
user_data_header_size: user_data_header_size_value, | |
content, | |
} | |
} | |
fn read_table( | |
file: &mut BufReader<File>, | |
header: &MPQFileHeader, | |
table: &HashMap<u64, u64>, | |
table_entry_type: &str, | |
) -> Vec<MPQTableEntry> { | |
let (table_offset, table_entries, key) = match table_entry_type { | |
"hash" => ( | |
header.hash_table_offset, | |
header.hash_table_entries, | |
MPQArchive::hash(table, "(hash table)", MPQHash::Table), | |
), | |
"block" => ( | |
header.block_table_offset, | |
header.block_table_entries, | |
MPQArchive::hash(table, "(block table)", MPQHash::Table), | |
), | |
_other => panic!("Neither block or header"), | |
}; | |
let file_offset: u32 = table_offset + header.offset; | |
file.seek(SeekFrom::Start(file_offset as u64)); | |
let mut data = vec![0; (table_entries * 16) as usize]; | |
file.read_exact(&mut data).unwrap(); | |
let decrypted_data = MPQArchive::decrypt(table, &data, key); | |
let mut table_values = Vec::with_capacity(table_entries as usize); | |
for i in 0..table_entries { | |
let position = (i * 16) as usize; | |
let table_entry: [u8; 16] = (&decrypted_data[position..position + 16]) | |
.try_into() | |
.unwrap(); | |
let entry_value = match table_entry_type { | |
"hash" => { | |
let hash_a = u32::from_le_bytes(table_entry[0..4].try_into().unwrap()); | |
let hash_b = u32::from_le_bytes(table_entry[4..8].try_into().unwrap()); | |
let locale = u16::from_le_bytes(table_entry[8..10].try_into().unwrap()); | |
let platform = u16::from_le_bytes(table_entry[10..12].try_into().unwrap()); | |
let block_table_index = | |
u32::from_le_bytes(table_entry[12..16].try_into().unwrap()); | |
MPQTableEntry::Hash(HashTableEntry { | |
hash_a, | |
hash_b, | |
locale, | |
platform, | |
block_table_index, | |
}) | |
} | |
"block" => { | |
let offset = u32::from_le_bytes(table_entry[0..4].try_into().unwrap()); | |
let archived_size = | |
u32::from_le_bytes(table_entry[4..8].try_into().unwrap()) as usize; | |
let size = u32::from_le_bytes(table_entry[8..12].try_into().unwrap()) as usize; | |
let flags = u32::from_le_bytes(table_entry[12..16].try_into().unwrap()); | |
MPQTableEntry::Block(BlockTableEntry { | |
offset, | |
archived_size, | |
size, | |
flags, | |
}) | |
} | |
_other => panic!("Neither block or header"), | |
}; | |
table_values.push(entry_value); | |
} | |
table_values | |
} | |
fn prepare_encryption_table() -> HashMap<u64, u64> { | |
let mut seed: u64 = 0x00100001; | |
let mut encryption_table = HashMap::new(); | |
for i in 0..256 { | |
let mut index = i; | |
for _j in 0..5 { | |
seed = (seed * 125 + 3) % 0x2AAAAB; | |
let temp1 = (seed & 0xFFFF) << 0x10; | |
seed = (seed * 125 + 3) % 0x2AAAAB; | |
let temp2 = seed & 0xFFFF; | |
encryption_table.insert(index, temp1 | temp2); | |
index += 0x100; | |
} | |
} | |
encryption_table | |
} | |
fn hash(table: &HashMap<u64, u64>, string: &str, hash_type: MPQHash) -> u64 { | |
let mut seed1: u64 = 0x7FED7FED; | |
let mut seed2: u64 = 0xEEEEEEEE; | |
for byte in string.to_uppercase().bytes() { | |
let value: u64 = table[&(((hash_type as u64) << 8) + byte as u64)]; | |
seed1 = (value ^ (seed1 + seed2)) & 0xFFFFFFFF; | |
seed2 = (byte as u64 + seed1 + seed2 + (seed2 << 5) + 3) & 0xFFFFFFFF; | |
} | |
seed1 | |
} | |
fn decrypt(table: &HashMap<u64, u64>, data: &[u8], key: u64) -> Vec<u8> { | |
let mut seed1: u64 = key; | |
let mut seed2: u64 = 0xEEEEEEEE; | |
let mut result = vec![]; | |
for i in 0..(data.len() / 4) { | |
seed2 += table[&(0x400 + (seed1 & 0xFF))] as u64; | |
seed2 &= 0xFFFFFFFF; | |
let position = i * 4; | |
let value_bytes: [u8; 4] = (&data[position..position + 4]).try_into().unwrap(); | |
let mut value = u32::from_le_bytes(value_bytes) as u64; | |
value = (value ^ (seed1 + seed2)) & 0xFFFFFFFF; | |
seed1 = ((!seed1 << 0x15) + 0x11111111) | (seed1 >> 0x0B); | |
seed1 &= 0xFFFFFFFF; | |
seed2 = (value + seed2 + (seed2 << 5) + 3) & 0xFFFFFFFF; | |
let packed_value: u32 = value.try_into().unwrap(); | |
result.extend(packed_value.to_le_bytes()); | |
} | |
result | |
} | |
fn _read_file( | |
file: &mut BufReader<File>, | |
header: &MPQFileHeader, | |
block_entry: &BlockTableEntry, | |
force_decompress: bool, | |
) -> Option<Vec<u8>> { | |
if block_entry.flags & MPQ_FILE_EXISTS != 0 { | |
if block_entry.archived_size == 0 { | |
return None; | |
} | |
let offset = block_entry.offset + header.offset; | |
file.seek(SeekFrom::Start(offset as u64)); | |
let mut file_data = vec![0; block_entry.archived_size as usize]; | |
file.read_exact(&mut file_data).unwrap(); | |
if block_entry.flags & MPQ_FILE_ENCRYPTED != 0 { | |
panic!("Encrpytion not supported"); | |
} | |
// file has many sectors that need to be separately decompressed | |
if block_entry.flags & MPQ_FILE_SINGLE_UNIT == 0 { | |
panic!("Not implemented yet"); | |
// let sector_size = 512 << header.sector_size_shift; | |
// let mut sectors = block_entry.size / sector_size + 1; | |
// let mut crc = false; | |
// if block_entry.flags & MPQ_FILE_SECTOR_CRC != 0 { | |
// crc = true; | |
// sectors += 1; | |
// } | |
// let positions = file_data[..4 * (sectors + 1)]; | |
// let mut result = vec![]; | |
// let mut sector_bytes_left = block_entry.size; | |
// for i in 0..(positions.len() - (crc ? 2 : 1)) { | |
// let sector = file_data[positions[i]..positions[i + 1]] | |
// } | |
} else if (block_entry.flags & MPQ_FILE_COMPRESS != 0 | |
&& (force_decompress || block_entry.size > block_entry.archived_size)) | |
{ | |
file_data = MPQArchive::decompress(file_data); | |
} | |
return Some(file_data); | |
} | |
None | |
} | |
fn read_block_entry( | |
archive_filename: &str, | |
encryption_table: &HashMap<u64, u64>, | |
hash_table: &[MPQTableEntry], | |
block_table: &[MPQTableEntry], | |
) -> Option<BlockTableEntry> { | |
let hash_entry_wrapper = | |
MPQArchive::get_hash_table_entry(encryption_table, hash_table, archive_filename); | |
let hash_entry = match hash_entry_wrapper { | |
Some(entry) => entry, | |
None => return None, | |
}; | |
match &block_table[hash_entry.block_table_index as usize] { | |
MPQTableEntry::Block(entry) => Some(*entry), | |
_other => panic!("Not block entry"), | |
} | |
} | |
pub fn read_file(&mut self, archive_filename: &str) -> Option<Vec<u8>> { | |
// let file = File::open(self.filename).expect("Failed to read replay file"); | |
// let mut reader = BufReader::new(file); | |
let block_table_entry = MPQArchive::read_block_entry( | |
archive_filename, | |
&self.encryption_table, | |
&self.hash_table, | |
&self.block_table, | |
) | |
.expect("Couldn't find block table entry"); | |
let force_decompress = false; | |
MPQArchive::_read_file( | |
&mut self.file, | |
&self.header, | |
&block_table_entry, | |
force_decompress, | |
) | |
} | |
fn get_hash_table_entry( | |
encryption_table: &HashMap<u64, u64>, | |
hash_table: &[MPQTableEntry], | |
filename: &str, | |
) -> Option<HashTableEntry> { | |
let hash_a = MPQArchive::hash(encryption_table, filename, MPQHash::HashA); | |
let hash_b = MPQArchive::hash(encryption_table, filename, MPQHash::HashB); | |
for entry in hash_table { | |
if let MPQTableEntry::Hash(table_entry) = entry { | |
if (table_entry.hash_a as u64) == hash_a && (table_entry.hash_b as u64) == hash_b { | |
return Some(*table_entry); | |
} | |
}; | |
} | |
None | |
} | |
fn decompress(data: Vec<u8>) -> Vec<u8> { | |
let compression_type = data[0]; | |
if compression_type == 0 { | |
data | |
} else if compression_type == 2 { | |
panic!("zlib compression not implemented yet"); | |
} else if compression_type == 16 { | |
// let mut decompressor = Decompress::new(false); | |
// decompressor.decompress_vec(&mut &data[1..], &mut decompressed_data).unwrap(); | |
// let mut reader = ParallelDecoderReader::new(Cursor::new(data), RayonThreadPool, usize::max_value()); | |
// copy(&mut reader, output); | |
let mut decompressed_data = vec![]; | |
let mut reader = DecoderReader::new(&data[1..]); | |
copy(&mut reader, &mut decompressed_data); | |
decompressed_data | |
} else { | |
panic!("Unsupported compression type") | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::game::Game; | |
use crate::replay::Event; | |
use crate::decoders::DecoderResult; | |
// doesn't include supply structures, gas collectors and support structures | |
const BUILDINGS: [&str; 41] = [ | |
// Protoss | |
"Nexus", | |
"Gateway", | |
"Forge", | |
"CyberneticsCore", | |
// "PhotonCannon", // we'll see about this one | |
"RoboticsFacility", | |
"Stargate", | |
"TwilightCouncil", | |
"RoboticsBay", | |
"FleetBeacon", | |
"TemplarArchives", | |
"DarkShrine", | |
// Terran | |
"CommandCenter", | |
"OrbitalCommand", | |
"PlanetaryFortress", | |
"Barracks", | |
"EngineeringBay", | |
"GhostAcademy", | |
"Factory", | |
"Starport", | |
"Armory", | |
"FusionCore", | |
"BarracksTechLab", | |
"FactoryTechLab", | |
"StarportTechLab", | |
"BarracksReactor", | |
"FactoryReactor", | |
"StarportReactor", | |
// Zerg | |
"Hatchery", | |
"SpawningPool", | |
"EvolutionChamber", | |
"RoachWarren", | |
"BanelingNest", | |
"Lair", | |
"HydraliskDen", | |
"LurkerDenMP", | |
"Spire", | |
"GreaterSpire", | |
"NydusNetwork", | |
"InfestationPit", | |
"Hive", | |
"UltraliskCavern", | |
]; | |
pub struct ObjectEvent; | |
const MAX_BUILD_LENGTH: u8 = 15; | |
impl ObjectEvent { | |
pub fn new(game: &mut Game, event: &Event) -> Result<(), &'static str> { | |
let mut player_id: u8 = 0; | |
let mut building_name = String::new(); | |
let mut tag_index = 0; | |
let mut tag_recycle = 0; | |
let mut current_gameloop = 0; | |
// println!("event entry values {:?}", event.entries); | |
for (field, value) in &event.entries { | |
match field.as_str() { | |
"m_controlPlayerId" => player_id = if let DecoderResult::Value(v) = value { | |
*v as u8 | |
} else { | |
return Err("Player ID is not a value"); | |
}, | |
"m_unitTypeName" => if let DecoderResult::Blob(unit_name) = value { | |
if BUILDINGS.contains(&unit_name.as_str()) { | |
building_name = unit_name.to_string(); | |
} | |
}, | |
"m_unitTagIndex" => if let DecoderResult::Value(index) = value { | |
tag_index = *index as u32; | |
}, | |
"m_unitTagRecycle" => if let DecoderResult::Value(recycle) = value{ | |
tag_recycle = *recycle as u32; | |
}, | |
"_gameloop" => if let DecoderResult::Value(gameloop) = value { | |
// ~7min, 22.4 gameloops per sec | |
if *gameloop > 9408 { | |
return Err("Gameloop is past 7min"); | |
} | |
current_gameloop = *gameloop; | |
}, | |
_other => continue, | |
} | |
} | |
if building_name == "" { | |
return Err("Building not found"); | |
} | |
let tag = (tag_index << 18) + tag_recycle; | |
let player_index = match game.buildings.get(&tag) { | |
Some(building_player_id) => { | |
building_player_id - 1 | |
}, | |
None => { | |
game.buildings.insert(tag, player_id); | |
player_id - 1 | |
}, | |
}; | |
if player_index > 1 { | |
return Err("More than 2 players in replay"); | |
} | |
// if game.builds[player_index as usize].len() < 10 && current_gameloop > 0 { | |
// game.builds[player_index as usize].push(building_name); | |
// } | |
if | |
current_gameloop > 0 && | |
!(building_name.contains("Reactor") || building_name.contains("TechLab")) && | |
game.builds[player_index as usize].len() < MAX_BUILD_LENGTH as usize | |
{ | |
game.builds[player_index as usize].push(building_name); | |
} | |
Ok(()) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::{Player, ReplaySummary, ReplayEntry, SummaryStat}; | |
use crate::replay::{Parsed, Metadata}; | |
use crate::game::Game; | |
use crate::events::EventParser; | |
use crate::decoders::DecoderResult; | |
use std::collections::HashMap; | |
pub type RaceMappings<'a> = HashMap<&'a str, &'a str>; | |
pub struct ReplayParser<'a> { | |
race_mappings: RaceMappings<'a>, | |
} | |
impl<'a> ReplayParser<'a> { | |
pub fn new() -> ReplayParser<'a> { | |
let race_mappings: RaceMappings = HashMap::from([ | |
("저그", "Zerg"), | |
("异虫", "Zerg"), | |
("蟲族", "Zerg"), | |
("Zergi", "Zerg"), | |
("테란", "Terran"), | |
("人類", "Terran"), | |
("人类", "Terran"), | |
("Terraner", "Terran"), | |
("Терраны", "Terran"), | |
("프로토스", "Protoss"), | |
("神族", "Protoss"), | |
("Protosi", "Protoss"), | |
("星灵", "Protoss"), | |
("Протоссы", "Protoss"), | |
]); | |
ReplayParser { | |
race_mappings, | |
} | |
} | |
pub fn parse_replay(&'a self, replay: Parsed, builds: &mut Vec<String>) -> Result<ReplaySummary, &'static str> { | |
let tags = replay.tags.clone(); | |
let mut game = Game::new(); | |
let mut event_parser = EventParser::new(&replay, &mut game); | |
for event in &replay.tracker_events { | |
if let Err(e) = event_parser.parse(event) { | |
println!("event parsing failed: {:?}\n", e); | |
continue; | |
} | |
} | |
let resources_collected: [(u16, u16); 2] = [ | |
(game.minerals_collected[0], game.gas_collected[0]), | |
(game.minerals_collected[1], game.gas_collected[1]), | |
]; | |
let resources_lost: [(u16, u16); 2] = [ | |
(game.minerals_lost[1], game.gas_lost[1]), | |
(game.minerals_lost[0], game.gas_lost[0]), | |
]; | |
let mut avg_collection_rate: [(u16, u16); 2] = [(0, 0), (0, 0)]; | |
for (index, player_collection_rate) in game.collection_rate.iter().enumerate() { | |
let mut player_total_collection_rate: [u64; 2] = [0, 0]; | |
for (minerals, gas) in player_collection_rate { | |
player_total_collection_rate[0] += *minerals as u64; | |
player_total_collection_rate[1] += *gas as u64; | |
} | |
let num_collection_rate = player_collection_rate.len() as u64; | |
avg_collection_rate[index] = ( | |
if num_collection_rate == 0 { 0 } else { (player_total_collection_rate[0] / num_collection_rate) as u16 }, | |
if num_collection_rate == 0 { 0 } else { (player_total_collection_rate[1] / num_collection_rate) as u16 }, | |
); | |
} | |
let mut avg_unspent_resources: [(u16, u16); 2] = [(0, 0), (0, 0)]; | |
for (index, player_unspent_resources) in game.unspent_resources.iter().enumerate() { | |
let mut player_total_unspent_resources: [u64; 2] = [0, 0]; | |
for (minerals, gas) in player_unspent_resources { | |
player_total_unspent_resources[0] += *minerals as u64; | |
player_total_unspent_resources[1] += *gas as u64; | |
} | |
let num_unspent_resources = player_unspent_resources.len() as u64; | |
avg_unspent_resources[index] = ( | |
if num_unspent_resources == 0 { 0 } else { (player_total_unspent_resources[0] / num_unspent_resources) as u16 }, | |
if num_unspent_resources == 0 { 0 } else { (player_total_unspent_resources[1] / num_unspent_resources) as u16 }, | |
); | |
} | |
let mut summary_stats = HashMap::new(); | |
for player_index in 0..2 { | |
let player_summary_stats = HashMap::from([ | |
("avg_collection_rate", SummaryStat::ResourceValues(avg_collection_rate[player_index])), | |
("resources_collected", SummaryStat::ResourceValues(resources_collected[player_index])), | |
("resources_lost", SummaryStat::ResourceValues(resources_lost[player_index])), | |
("avg_unspent_resources", SummaryStat::ResourceValues(avg_unspent_resources[player_index])), | |
("workers_produced", SummaryStat::Value(game.workers_active[player_index] as u16)), | |
("workers_lost", SummaryStat::Value(0)), | |
]); | |
summary_stats.insert((player_index + 1) as u8, player_summary_stats); | |
} | |
// println!("player info {:?}", &replay.player_info); | |
let parsed_metadata: Metadata = serde_json::from_str(&replay.metadata).unwrap(); | |
let winner = match parsed_metadata.Players | |
.iter() | |
.find(|player| player.Result == "Win") { | |
Some(player) => player.PlayerID, | |
None => return Err("couldn't find winner"), | |
}; | |
let game_length = parsed_metadata.Duration; | |
let raw_map = &replay.player_info | |
.iter() | |
.find(|(field, _)| *field == "m_title") | |
.unwrap().1; | |
let mut map = String::new(); | |
if let DecoderResult::Blob(value) = raw_map { | |
map = value | |
.trim_start_matches("[M] ") | |
.trim_start_matches("[SO] ") | |
.trim_start_matches("[ESL] ") | |
.trim_start_matches("[GSL] ") | |
.trim_start_matches("[TLMC14] ") | |
.trim_start_matches("[TLMC15] ") | |
.trim_start_matches("[TLMC16] ") | |
.trim_end_matches(" LE") | |
.to_string(); | |
} | |
let raw_played_at = &replay.player_info | |
.iter() | |
.find(|(field, _)| *field == "m_timeUTC") | |
.unwrap().1; | |
let mut played_at = 0; | |
if let DecoderResult::Value(value) = raw_played_at { | |
// TODO: this truncation is not working properly | |
played_at = value.clone() as u64; | |
} | |
// game records time in window epoch for some reason | |
// https://en.wikipedia.org/wiki/Epoch_(computing) | |
played_at = (played_at / 10000000) - 11644473600; | |
let (_, player_list) = &replay.player_info | |
.iter() | |
.find(|(field, _)| *field == "m_playerList") | |
.unwrap(); | |
let mut players = vec![]; | |
match player_list { | |
DecoderResult::Array(values) => { | |
// TODO: enumerated id is incorrect for P1 and P2 in games | |
// don't support 1 player or 3+ player games | |
if values.len() != 2 { | |
return Err("Not 2 players in replay"); | |
} | |
for (id, player) in values.iter().enumerate() { | |
match player { | |
DecoderResult::Struct(player_values) => { | |
let raw_race = &player_values | |
.iter() | |
.find(|(field, _)| *field == "m_race") | |
.unwrap().1; | |
let mut race = String::new(); | |
if let DecoderResult::Blob(value) = raw_race { | |
race = value.clone(); | |
} | |
if let Some(value) = self.race_mappings.get(race.as_str()) { | |
race = value.to_string(); | |
} | |
let raw_name = &player_values | |
.iter() | |
.find(|(field, _)| *field == "m_name") | |
.unwrap().1; | |
let mut name = String::new(); | |
if let DecoderResult::Blob(value) = raw_name { | |
name = match value.find(">") { | |
Some(clan_tag_index) => value[clan_tag_index + 1..].to_string(), | |
None => value.clone(), | |
}; | |
} | |
players.push(Player { | |
id: (id + 1) as u8, | |
race, | |
name, | |
}); | |
}, | |
_other => panic!("Found DecoderResult::{:?}", _other) | |
} | |
} | |
}, | |
_other => panic!("Found DecoderResult::{:?}", _other) | |
} | |
let mut replay_build_mappings: [u16; 2] = [0, 0]; | |
let mut replay_builds: [Vec<String>; 2] = [vec![], vec![]]; | |
for (replay_build_index, build) in game.builds.iter().enumerate() { | |
replay_builds[replay_build_index] = build.clone(); | |
let joined_build = build.join(","); | |
match builds.iter().position(|seen_build| &joined_build == seen_build) { | |
Some(build_index) => replay_build_mappings[replay_build_index] = build_index as u16, | |
None => { | |
builds.push(joined_build); | |
replay_build_mappings[replay_build_index] = builds.len() as u16 - 1; | |
} | |
} | |
} | |
let replay_summary: ReplaySummary = HashMap::from([ | |
("players", ReplayEntry::Players(players)), | |
("builds", ReplayEntry::Builds(replay_builds)), | |
("build_mappings", ReplayEntry::BuildMappings(replay_build_mappings)), | |
("winner", ReplayEntry::Winner(winner)), | |
("game_length", ReplayEntry::GameLength(game_length)), | |
("map", ReplayEntry::Map(map)), | |
("played_at", ReplayEntry::PlayedAt(played_at)), | |
// ("summary_stats", ReplayEntry::SummaryStats(summary_stats)), | |
("metadata", ReplayEntry::Metadata(tags)), | |
]); | |
Ok(replay_summary) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::replay::Event; | |
use crate::decoders::DecoderResult; | |
use crate::game::Game; | |
pub struct PlayerStatsEvent; | |
impl PlayerStatsEvent { | |
pub fn new(game: &mut Game, event: &Event) -> Result<(), &'static str> { | |
let mut player_id: u8 = 0; | |
for (field, value) in &event.entries { | |
match field.as_str() { | |
"m_playerId" => player_id = if let DecoderResult::Value(v) = value { | |
*v as u8 | |
} else { | |
return Err("Player ID is not a value"); | |
}, | |
"m_stats" => if let DecoderResult::Struct(entries) = value { | |
let player_index = (player_id - 1) as usize; | |
let mut event_minerals_collected: i64 = 0; | |
let mut event_minerals_lost: i64 = 0; | |
let mut event_gas_collected: i64 = 0; | |
let mut event_gas_lost: i64 = 0; | |
let mut event_minerals_collection_rate: u16 = 0; | |
let mut event_gas_collection_rate: u16 = 0; | |
let mut event_minerals_unspent_resources: u16 = 0; | |
let mut event_gas_unspent_resources: u16 = 0; | |
// don't support more than 2 players | |
if player_index > 1 { | |
return Err("More than 1 player in replay"); | |
} | |
for (key, value) in entries { | |
match key.as_str() { | |
"m_scoreValueWorkersActiveCount" => if let DecoderResult::Value(workers) = value { | |
game.workers_active[player_index] = *workers as u8; | |
}, | |
"m_scoreValueMineralsCollectionRate" => if let DecoderResult::Value(minerals) = value { | |
event_minerals_collection_rate = *minerals as u16; | |
}, | |
"m_scoreValueVespeneCollectionRate" => if let DecoderResult::Value(gas) = value { | |
event_gas_collection_rate = *gas as u16; | |
}, | |
"m_scoreValueMineralsCurrent" => if let DecoderResult::Value(minerals) = value { | |
event_minerals_unspent_resources = *minerals as u16; | |
event_minerals_collected += minerals; | |
}, | |
"m_scoreValueVespeneCurrent" => if let DecoderResult::Value(gas) = value { | |
event_gas_unspent_resources = *gas as u16; | |
event_gas_collected += gas; | |
}, | |
"m_scoreValueMineralsLostArmy" | | |
"m_scoreValueMineralsLostEconomy" | | |
"m_scoreValueMineralsLostTechnology" => if let DecoderResult::Value(minerals) = value { | |
event_minerals_lost += minerals.abs(); | |
event_minerals_collected += minerals; | |
} | |
"m_scoreValueVespeneLostArmy" | | |
"m_scoreValueVespeneLostEconomy" | | |
"m_scoreValueVespeneLostTechnology" => if let DecoderResult::Value(gas) = value { | |
event_gas_lost += gas.abs(); | |
event_gas_collected += gas; | |
} | |
"m_scoreValueMineralsUsedInProgressArmy" | | |
"m_scoreValueMineralsUsedInProgressEconomy" | | |
"m_scoreValueMineralsUsedInProgressTechnology" | | |
"m_scoreValueMineralsUsedCurrentArmy" | | |
"m_scoreValueMineralsUsedCurrentEconomy" | | |
"m_scoreValueMineralsUsedCurrentTechnology" => if let DecoderResult::Value(minerals) = value { | |
event_minerals_collected += minerals; | |
}, | |
"m_scoreValueVespeneUsedInProgressArmy" | | |
"m_scoreValueVespeneUsedInProgressEconomy" | | |
"m_scoreValueVespeneUsedInProgressTechnology" | | |
"m_scoreValueVespeneUsedCurrentArmy" | | |
"m_scoreValueVespeneUsedCurrentEconomy" | | |
"m_scoreValueVespeneUsedCurrentTechnology" => if let DecoderResult::Value(gas) = value { | |
event_gas_collected += gas; | |
}, | |
_other => continue, | |
} | |
} | |
game.minerals_collected[player_index] = event_minerals_collected as u16; | |
game.minerals_lost[player_index] = event_minerals_lost as u16; | |
game.gas_collected[player_index] = event_gas_collected as u16; | |
game.gas_lost[player_index] = event_gas_lost as u16; | |
game.collection_rate[player_index].push((event_minerals_collection_rate, event_gas_collection_rate)); | |
game.unspent_resources[player_index].push((event_minerals_unspent_resources, event_gas_unspent_resources)); | |
} else { | |
panic!("didn't find struct {:?}", value); | |
}, | |
_other => continue, | |
} | |
} | |
Ok(()) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::decoders::{ | |
Decoder, | |
DecoderResult, | |
BitPackedDecoder, | |
VersionedDecoder, | |
EventEntry, | |
}; | |
use crate::replay::Event; | |
use std::collections::HashMap; | |
// Decoding instructions for each protocol type. | |
const RAW_TYPEINFOS: &str = "('_int',[(0,7)]), #0 | |
('_int',[(0,4)]), #1 | |
('_int',[(0,5)]), #2 | |
('_int',[(0,6)]), #3 | |
('_int',[(0,14)]), #4 | |
('_int',[(0,22)]), #5 | |
('_int',[(0,32)]), #6 | |
('_choice',[(0,2),{0:('m_uint6',3),1:('m_uint14',4),2:('m_uint22',5),3:('m_uint32',6)}]), #7 | |
('_struct',[[('m_userId',2,-1)]]), #8 | |
('_blob',[(0,8)]), #9 | |
('_int',[(0,8)]), #10 | |
('_struct',[[('m_flags',10,0),('m_major',10,1),('m_minor',10,2),('m_revision',10,3),('m_build',6,4),('m_baseBuild',6,5)]]), #11 | |
('_int',[(0,3)]), #12 | |
('_bool',[]), #13 | |
('_array',[(16,0),10]), #14 | |
('_optional',[14]), #15 | |
('_blob',[(16,0)]), #16 | |
('_struct',[[('m_dataDeprecated',15,0),('m_data',16,1)]]), #17 | |
('_struct',[[('m_signature',9,0),('m_version',11,1),('m_type',12,2),('m_elapsedGameLoops',6,3),('m_useScaledTime',13,4),('m_ngdpRootKey',17,5),('m_dataBuildNum',6,6),('m_replayCompatibilityHash',17,7),('m_ngdpRootKeyIsDevData',13,8)]]), #18 | |
('_fourcc',[]), #19 | |
('_blob',[(0,7)]), #20 | |
('_int',[(0,64)]), #21 | |
('_struct',[[('m_region',10,0),('m_programId',19,1),('m_realm',6,2),('m_name',20,3),('m_id',21,4)]]), #22 | |
('_struct',[[('m_a',10,0),('m_r',10,1),('m_g',10,2),('m_b',10,3)]]), #23 | |
('_int',[(0,2)]), #24 | |
('_optional',[10]), #25 | |
('_struct',[[('m_name',9,0),('m_toon',22,1),('m_race',9,2),('m_color',23,3),('m_control',10,4),('m_teamId',1,5),('m_handicap',6,6),('m_observe',24,7),('m_result',24,8),('m_workingSetSlotId',25,9),('m_hero',9,10)]]), #26 | |
('_array',[(0,5),26]), #27 | |
('_optional',[27]), #28 | |
('_blob',[(0,10)]), #29 | |
('_blob',[(0,11)]), #30 | |
('_struct',[[('m_file',30,0)]]), #31 | |
('_int',[(-9223372036854775808,64)]), #32 | |
('_optional',[13]), #33 | |
('_blob',[(0,12)]), #34 | |
('_blob',[(40,0)]), #35 | |
('_array',[(0,6),35]), #36 | |
('_optional',[36]), #37 | |
('_array',[(0,6),30]), #38 | |
('_optional',[38]), #39 | |
('_struct',[[('m_playerList',28,0),('m_title',29,1),('m_difficulty',9,2),('m_thumbnail',31,3),('m_isBlizzardMap',13,4),('m_timeUTC',32,5),('m_timeLocalOffset',32,6),('m_restartAsTransitionMap',33,16),('m_disableRecoverGame',13,17),('m_description',34,7),('m_imageFilePath',30,8),('m_campaignIndex',10,15),('m_mapFileName',30,9),('m_cacheHandles',37,10),('m_miniSave',13,11),('m_gameSpeed',12,12),('m_defaultDifficulty',3,13),('m_modPaths',39,14)]]), #40 | |
('_optional',[9]), #41 | |
('_optional',[35]), #42 | |
('_optional',[6]), #43 | |
('_struct',[[('m_race',25,-1)]]), #44 | |
('_struct',[[('m_team',25,-1)]]), #45 | |
('_blob',[(0,9)]), #46 | |
('_int',[(-2147483648,32)]), #47 | |
('_optional',[47]), #48 | |
('_struct',[[('m_name',9,-19),('m_clanTag',41,-18),('m_clanLogo',42,-17),('m_highestLeague',25,-16),('m_combinedRaceLevels',43,-15),('m_randomSeed',6,-14),('m_racePreference',44,-13),('m_teamPreference',45,-12),('m_testMap',13,-11),('m_testAuto',13,-10),('m_examine',13,-9),('m_customInterface',13,-8),('m_testType',6,-7),('m_observe',24,-6),('m_hero',46,-5),('m_skin',46,-4),('m_mount',46,-3),('m_toonHandle',20,-2),('m_scaledRating',48,-1)]]), #49 | |
('_array',[(0,5),49]), #50 | |
('_struct',[[('m_lockTeams',13,-16),('m_teamsTogether',13,-15),('m_advancedSharedControl',13,-14),('m_randomRaces',13,-13),('m_battleNet',13,-12),('m_amm',13,-11),('m_competitive',13,-10),('m_practice',13,-9),('m_cooperative',13,-8),('m_noVictoryOrDefeat',13,-7),('m_heroDuplicatesAllowed',13,-6),('m_fog',24,-5),('m_observers',24,-4),('m_userDifficulty',24,-3),('m_clientDebugFlags',21,-2),('m_buildCoachEnabled',13,-1)]]), #51 | |
('_int',[(1,4)]), #52 | |
('_int',[(1,8)]), #53 | |
('_bitarray',[(0,6)]), #54 | |
('_bitarray',[(0,8)]), #55 | |
('_bitarray',[(0,2)]), #56 | |
('_struct',[[('m_allowedColors',54,-6),('m_allowedRaces',55,-5),('m_allowedDifficulty',54,-4),('m_allowedControls',55,-3),('m_allowedObserveTypes',56,-2),('m_allowedAIBuilds',55,-1)]]), #57 | |
('_array',[(0,5),57]), #58 | |
('_struct',[[('m_randomValue',6,-28),('m_gameCacheName',29,-27),('m_gameOptions',51,-26),('m_gameSpeed',12,-25),('m_gameType',12,-24),('m_maxUsers',2,-23),('m_maxObservers',2,-22),('m_maxPlayers',2,-21),('m_maxTeams',52,-20),('m_maxColors',3,-19),('m_maxRaces',53,-18),('m_maxControls',10,-17),('m_mapSizeX',10,-16),('m_mapSizeY',10,-15),('m_mapFileSyncChecksum',6,-14),('m_mapFileName',30,-13),('m_mapAuthorName',9,-12),('m_modFileSyncChecksum',6,-11),('m_slotDescriptions',58,-10),('m_defaultDifficulty',3,-9),('m_defaultAIBuild',10,-8),('m_cacheHandles',36,-7),('m_hasExtensionMod',13,-6),('m_hasNonBlizzardExtensionMod',13,-5),('m_isBlizzardMap',13,-4),('m_isPremadeFFA',13,-3),('m_isCoopMode',13,-2),('m_isRealtimeMode',13,-1)]]), #59 | |
('_optional',[1]), #60 | |
('_optional',[2]), #61 | |
('_struct',[[('m_color',61,-1)]]), #62 | |
('_array',[(0,4),46]), #63 | |
('_array',[(0,17),6]), #64 | |
('_array',[(0,16),6]), #65 | |
('_array',[(0,3),6]), #66 | |
('_struct',[[('m_key',6,-2),('m_rewards',64,-1)]]), #67 | |
('_array',[(0,17),67]), #68 | |
('_struct',[[('m_control',10,-32),('m_userId',60,-31),('m_teamId',1,-30),('m_colorPref',62,-29),('m_racePref',44,-28),('m_difficulty',3,-27),('m_aiBuild',10,-26),('m_handicap',6,-25),('m_observe',24,-24),('m_logoIndex',6,-23),('m_hero',46,-22),('m_skin',46,-21),('m_mount',46,-20),('m_artifacts',63,-19),('m_workingSetSlotId',25,-18),('m_rewards',64,-17),('m_toonHandle',20,-16),('m_licenses',65,-15),('m_tandemLeaderId',60,-14),('m_commander',46,-13),('m_commanderLevel',6,-12),('m_hasSilencePenalty',13,-11),('m_tandemId',60,-10),('m_commanderMasteryLevel',6,-9),('m_commanderMasteryTalents',66,-8),('m_trophyId',6,-7),('m_rewardOverrides',68,-6),('m_brutalPlusDifficulty',6,-5),('m_retryMutationIndexes',66,-4),('m_aCEnemyRace',6,-3),('m_aCEnemyWaveType',6,-2),('m_selectedCommanderPrestige',6,-1)]]), #69 | |
('_array',[(0,5),69]), #70 | |
('_struct',[[('m_phase',12,-11),('m_maxUsers',2,-10),('m_maxObservers',2,-9),('m_slots',70,-8),('m_randomSeed',6,-7),('m_hostUserId',60,-6),('m_isSinglePlayer',13,-5),('m_pickedMapTag',10,-4),('m_gameDuration',6,-3),('m_defaultDifficulty',3,-2),('m_defaultAIBuild',10,-1)]]), #71 | |
('_struct',[[('m_userInitialData',50,-3),('m_gameDescription',59,-2),('m_lobbyState',71,-1)]]), #72 | |
('_struct',[[('m_syncLobbyState',72,-1)]]), #73 | |
('_struct',[[('m_name',20,-6)]]), #74 | |
('_blob',[(0,6)]), #75 | |
('_struct',[[('m_name',75,-6)]]), #76 | |
('_struct',[[('m_name',75,-8),('m_type',6,-7),('m_data',20,-6)]]), #77 | |
('_struct',[[('m_type',6,-8),('m_name',75,-7),('m_data',34,-6)]]), #78 | |
('_array',[(0,5),10]), #79 | |
('_struct',[[('m_signature',79,-7),('m_toonHandle',20,-6)]]), #80 | |
('_struct',[[('m_gameFullyDownloaded',13,-19),('m_developmentCheatsEnabled',13,-18),('m_testCheatsEnabled',13,-17),('m_multiplayerCheatsEnabled',13,-16),('m_syncChecksummingEnabled',13,-15),('m_isMapToMapTransition',13,-14),('m_debugPauseEnabled',13,-13),('m_useGalaxyAsserts',13,-12),('m_platformMac',13,-11),('m_cameraFollow',13,-10),('m_baseBuildNum',6,-9),('m_buildNum',6,-8),('m_versionFlags',6,-7),('m_hotkeyProfile',46,-6)]]), #81 | |
('_struct',[[]]), #82 | |
('_int',[(0,16)]), #83 | |
('_struct',[[('x',83,-2),('y',83,-1)]]), #84 | |
('_struct',[[('m_which',12,-7),('m_target',84,-6)]]), #85 | |
('_struct',[[('m_fileName',30,-10),('m_automatic',13,-9),('m_overwrite',13,-8),('m_name',9,-7),('m_description',29,-6)]]), #86 | |
('_struct',[[('m_sequence',6,-6)]]), #87 | |
('_struct',[[('x',47,-2),('y',47,-1)]]), #88 | |
('_struct',[[('m_point',88,-4),('m_time',47,-3),('m_verb',29,-2),('m_arguments',29,-1)]]), #89 | |
('_struct',[[('m_data',89,-6)]]), #90 | |
('_int',[(0,27)]), #91 | |
('_struct',[[('m_abilLink',83,-3),('m_abilCmdIndex',2,-2),('m_abilCmdData',25,-1)]]), #92 | |
('_optional',[92]), #93 | |
('_null',[]), #94 | |
('_int',[(0,20)]), #95 | |
('_struct',[[('x',95,-3),('y',95,-2),('z',47,-1)]]), #96 | |
('_struct',[[('m_targetUnitFlags',83,-7),('m_timer',10,-6),('m_tag',6,-5),('m_snapshotUnitLink',83,-4),('m_snapshotControlPlayerId',60,-3),('m_snapshotUpkeepPlayerId',60,-2),('m_snapshotPoint',96,-1)]]), #97 | |
('_choice',[(0,2),{0:('None',94),1:('TargetPoint',96),2:('TargetUnit',97),3:('Data',6)}]), #98 | |
('_int',[(1,32)]), #99 | |
('_struct',[[('m_cmdFlags',91,-11),('m_abil',93,-10),('m_data',98,-9),('m_sequence',99,-8),('m_otherUnit',43,-7),('m_unitGroup',43,-6)]]), #100 | |
('_int',[(0,9)]), #101 | |
('_bitarray',[(0,9)]), #102 | |
('_array',[(0,9),101]), #103 | |
('_choice',[(0,2),{0:('None',94),1:('Mask',102),2:('OneIndices',103),3:('ZeroIndices',103)}]), #104 | |
('_struct',[[('m_unitLink',83,-4),('m_subgroupPriority',10,-3),('m_intraSubgroupPriority',10,-2),('m_count',101,-1)]]), #105 | |
('_array',[(0,9),105]), #106 | |
('_array',[(0,9),6]), #107 | |
('_struct',[[('m_subgroupIndex',101,-4),('m_removeMask',104,-3),('m_addSubgroups',106,-2),('m_addUnitTags',107,-1)]]), #108 | |
('_struct',[[('m_controlGroupId',1,-7),('m_delta',108,-6)]]), #109 | |
('_struct',[[('m_controlGroupIndex',1,-8),('m_controlGroupUpdate',12,-7),('m_mask',104,-6)]]), #110 | |
('_struct',[[('m_count',101,-6),('m_subgroupCount',101,-5),('m_activeSubgroupIndex',101,-4),('m_unitTagsChecksum',6,-3),('m_subgroupIndicesChecksum',6,-2),('m_subgroupsChecksum',6,-1)]]), #111 | |
('_struct',[[('m_controlGroupId',1,-7),('m_selectionSyncData',111,-6)]]), #112 | |
('_array',[(0,3),47]), #113 | |
('_struct',[[('m_recipientId',1,-7),('m_resources',113,-6)]]), #114 | |
('_struct',[[('m_chatMessage',29,-6)]]), #115 | |
('_int',[(-128,8)]), #116 | |
('_struct',[[('x',47,-3),('y',47,-2),('z',47,-1)]]), #117 | |
('_struct',[[('m_beacon',116,-14),('m_ally',116,-13),('m_flags',116,-12),('m_build',116,-11),('m_targetUnitTag',6,-10),('m_targetUnitSnapshotUnitLink',83,-9),('m_targetUnitSnapshotUpkeepPlayerId',116,-8),('m_targetUnitSnapshotControlPlayerId',116,-7),('m_targetPoint',117,-6)]]), #118 | |
('_struct',[[('m_speed',12,-6)]]), #119 | |
('_struct',[[('m_delta',116,-6)]]), #120 | |
('_struct',[[('m_point',88,-14),('m_unit',6,-13),('m_unitLink',83,-12),('m_unitControlPlayerId',60,-11),('m_unitUpkeepPlayerId',60,-10),('m_unitPosition',96,-9),('m_unitIsUnderConstruction',13,-8),('m_pingedMinimap',13,-7),('m_option',47,-6)]]), #121 | |
('_struct',[[('m_verb',29,-7),('m_arguments',29,-6)]]), #122 | |
('_struct',[[('m_alliance',6,-7),('m_control',6,-6)]]), #123 | |
('_struct',[[('m_unitTag',6,-6)]]), #124 | |
('_struct',[[('m_unitTag',6,-7),('m_flags',10,-6)]]), #125 | |
('_struct',[[('m_conversationId',47,-7),('m_replyId',47,-6)]]), #126 | |
('_optional',[20]), #127 | |
('_struct',[[('m_gameUserId',1,-6),('m_observe',24,-5),('m_name',9,-4),('m_toonHandle',127,-3),('m_clanTag',41,-2),('m_clanLogo',42,-1)]]), #128 | |
('_array',[(0,5),128]), #129 | |
('_int',[(0,1)]), #130 | |
('_struct',[[('m_userInfos',129,-7),('m_method',130,-6)]]), #131 | |
('_struct',[[('m_purchaseItemId',47,-6)]]), #132 | |
('_struct',[[('m_difficultyLevel',47,-6)]]), #133 | |
('_choice',[(0,3),{0:('None',94),1:('Checked',13),2:('ValueChanged',6),3:('SelectionChanged',47),4:('TextChanged',30),5:('MouseButton',6)}]), #134 | |
('_struct',[[('m_controlId',47,-8),('m_eventType',47,-7),('m_eventData',134,-6)]]), #135 | |
('_struct',[[('m_soundHash',6,-7),('m_length',6,-6)]]), #136 | |
('_array',[(0,7),6]), #137 | |
('_struct',[[('m_soundHash',137,-2),('m_length',137,-1)]]), #138 | |
('_struct',[[('m_syncInfo',138,-6)]]), #139 | |
('_struct',[[('m_queryId',83,-8),('m_lengthMs',6,-7),('m_finishGameLoop',6,-6)]]), #140 | |
('_struct',[[('m_queryId',83,-7),('m_lengthMs',6,-6)]]), #141 | |
('_struct',[[('m_animWaitQueryId',83,-6)]]), #142 | |
('_struct',[[('m_sound',6,-6)]]), #143 | |
('_struct',[[('m_transmissionId',47,-7),('m_thread',6,-6)]]), #144 | |
('_struct',[[('m_transmissionId',47,-6)]]), #145 | |
('_optional',[84]), #146 | |
('_optional',[83]), #147 | |
('_optional',[116]), #148 | |
('_struct',[[('m_target',146,-11),('m_distance',147,-10),('m_pitch',147,-9),('m_yaw',147,-8),('m_reason',148,-7),('m_follow',13,-6)]]), #149 | |
('_struct',[[('m_skipType',130,-6)]]), #150 | |
('_int',[(0,11)]), #151 | |
('_struct',[[('x',151,-2),('y',151,-1)]]), #152 | |
('_struct',[[('m_button',6,-10),('m_down',13,-9),('m_posUI',152,-8),('m_posWorld',96,-7),('m_flags',116,-6)]]), #153 | |
('_struct',[[('m_posUI',152,-8),('m_posWorld',96,-7),('m_flags',116,-6)]]), #154 | |
('_struct',[[('m_achievementLink',83,-6)]]), #155 | |
('_struct',[[('m_hotkey',6,-7),('m_down',13,-6)]]), #156 | |
('_struct',[[('m_abilLink',83,-8),('m_abilCmdIndex',2,-7),('m_state',116,-6)]]), #157 | |
('_struct',[[('m_soundtrack',6,-6)]]), #158 | |
('_struct',[[('m_planetId',47,-6)]]), #159 | |
('_struct',[[('m_key',116,-7),('m_flags',116,-6)]]), #160 | |
('_struct',[[('m_resources',113,-6)]]), #161 | |
('_struct',[[('m_fulfillRequestId',47,-6)]]), #162 | |
('_struct',[[('m_cancelRequestId',47,-6)]]), #163 | |
('_struct',[[('m_error',47,-7),('m_abil',93,-6)]]), #164 | |
('_struct',[[('m_researchItemId',47,-6)]]), #165 | |
('_struct',[[('m_mercenaryId',47,-6)]]), #166 | |
('_struct',[[('m_battleReportId',47,-7),('m_difficultyLevel',47,-6)]]), #167 | |
('_struct',[[('m_battleReportId',47,-6)]]), #168 | |
('_struct',[[('m_decrementSeconds',47,-6)]]), #169 | |
('_struct',[[('m_portraitId',47,-6)]]), #170 | |
('_struct',[[('m_functionName',20,-6)]]), #171 | |
('_struct',[[('m_result',47,-6)]]), #172 | |
('_struct',[[('m_gameMenuItemIndex',47,-6)]]), #173 | |
('_int',[(-32768,16)]), #174 | |
('_struct',[[('m_wheelSpin',174,-7),('m_flags',116,-6)]]), #175 | |
('_struct',[[('m_purchaseCategoryId',47,-6)]]), #176 | |
('_struct',[[('m_button',83,-6)]]), #177 | |
('_struct',[[('m_cutsceneId',47,-7),('m_bookmarkName',20,-6)]]), #178 | |
('_struct',[[('m_cutsceneId',47,-6)]]), #179 | |
('_struct',[[('m_cutsceneId',47,-8),('m_conversationLine',20,-7),('m_altConversationLine',20,-6)]]), #180 | |
('_struct',[[('m_cutsceneId',47,-7),('m_conversationLine',20,-6)]]), #181 | |
('_struct',[[('m_leaveReason',1,-6)]]), #182 | |
('_struct',[[('m_observe',24,-12),('m_name',9,-11),('m_toonHandle',127,-10),('m_clanTag',41,-9),('m_clanLogo',42,-8),('m_hijack',13,-7),('m_hijackCloneGameUserId',60,-6)]]), #183 | |
('_optional',[99]), #184 | |
('_struct',[[('m_state',24,-7),('m_sequence',184,-6)]]), #185 | |
('_struct',[[('m_target',96,-6)]]), #186 | |
('_struct',[[('m_target',97,-6)]]), #187 | |
('_struct',[[('m_catalog',10,-9),('m_entry',83,-8),('m_field',9,-7),('m_value',9,-6)]]), #188 | |
('_struct',[[('m_index',6,-6)]]), #189 | |
('_struct',[[('m_shown',13,-6)]]), #190 | |
('_struct',[[('m_syncTime',6,-6)]]), #191 | |
('_struct',[[('m_recipient',12,-3),('m_string',30,-2)]]), #192 | |
('_struct',[[('m_recipient',12,-3),('m_point',88,-2)]]), #193 | |
('_struct',[[('m_progress',47,-2)]]), #194 | |
('_struct',[[('m_status',24,-2)]]), #195 | |
('_struct',[[('m_scoreValueMineralsCurrent',47,0),('m_scoreValueVespeneCurrent',47,1),('m_scoreValueMineralsCollectionRate',47,2),('m_scoreValueVespeneCollectionRate',47,3),('m_scoreValueWorkersActiveCount',47,4),('m_scoreValueMineralsUsedInProgressArmy',47,5),('m_scoreValueMineralsUsedInProgressEconomy',47,6),('m_scoreValueMineralsUsedInProgressTechnology',47,7),('m_scoreValueVespeneUsedInProgressArmy',47,8),('m_scoreValueVespeneUsedInProgressEconomy',47,9),('m_scoreValueVespeneUsedInProgressTechnology',47,10),('m_scoreValueMineralsUsedCurrentArmy',47,11),('m_scoreValueMineralsUsedCurrentEconomy',47,12),('m_scoreValueMineralsUsedCurrentTechnology',47,13),('m_scoreValueVespeneUsedCurrentArmy',47,14),('m_scoreValueVespeneUsedCurrentEconomy',47,15),('m_scoreValueVespeneUsedCurrentTechnology',47,16),('m_scoreValueMineralsLostArmy',47,17),('m_scoreValueMineralsLostEconomy',47,18),('m_scoreValueMineralsLostTechnology',47,19),('m_scoreValueVespeneLostArmy',47,20),('m_scoreValueVespeneLostEconomy',47,21),('m_scoreValueVespeneLostTechnology',47,22),('m_scoreValueMineralsKilledArmy',47,23),('m_scoreValueMineralsKilledEconomy',47,24),('m_scoreValueMineralsKilledTechnology',47,25),('m_scoreValueVespeneKilledArmy',47,26),('m_scoreValueVespeneKilledEconomy',47,27),('m_scoreValueVespeneKilledTechnology',47,28),('m_scoreValueFoodUsed',47,29),('m_scoreValueFoodMade',47,30),('m_scoreValueMineralsUsedActiveForces',47,31),('m_scoreValueVespeneUsedActiveForces',47,32),('m_scoreValueMineralsFriendlyFireArmy',47,33),('m_scoreValueMineralsFriendlyFireEconomy',47,34),('m_scoreValueMineralsFriendlyFireTechnology',47,35),('m_scoreValueVespeneFriendlyFireArmy',47,36),('m_scoreValueVespeneFriendlyFireEconomy',47,37),('m_scoreValueVespeneFriendlyFireTechnology',47,38)]]), #196 | |
('_struct',[[('m_playerId',1,0),('m_stats',196,1)]]), #197 | |
('_optional',[29]), #198 | |
('_struct',[[('m_unitTagIndex',6,0),('m_unitTagRecycle',6,1),('m_unitTypeName',29,2),('m_controlPlayerId',1,3),('m_upkeepPlayerId',1,4),('m_x',10,5),('m_y',10,6),('m_creatorUnitTagIndex',43,7),('m_creatorUnitTagRecycle',43,8),('m_creatorAbilityName',198,9)]]), #199 | |
('_struct',[[('m_unitTagIndex',6,0),('m_unitTagRecycle',6,1),('m_killerPlayerId',60,2),('m_x',10,3),('m_y',10,4),('m_killerUnitTagIndex',43,5),('m_killerUnitTagRecycle',43,6)]]), #200 | |
('_struct',[[('m_unitTagIndex',6,0),('m_unitTagRecycle',6,1),('m_controlPlayerId',1,2),('m_upkeepPlayerId',1,3)]]), #201 | |
('_struct',[[('m_unitTagIndex',6,0),('m_unitTagRecycle',6,1),('m_unitTypeName',29,2)]]), #202 | |
('_struct',[[('m_playerId',1,0),('m_upgradeTypeName',29,1),('m_count',47,2)]]), #203 | |
('_struct',[[('m_unitTagIndex',6,0),('m_unitTagRecycle',6,1),('m_unitTypeName',29,2),('m_controlPlayerId',1,3),('m_upkeepPlayerId',1,4),('m_x',10,5),('m_y',10,6)]]), #204 | |
('_struct',[[('m_unitTagIndex',6,0),('m_unitTagRecycle',6,1)]]), #205 | |
('_array',[(0,10),47]), #206 | |
('_struct',[[('m_firstUnitIndex',6,0),('m_items',206,1)]]), #207 | |
('_struct',[[('m_playerId',1,0),('m_type',6,1),('m_userId',43,2),('m_slotId',43,3)]]), #208 | |
"; | |
// The typeid of the NNet.Game.EEventId enum. | |
const GAME_EVENTID_TYPEID: u8 = 0; | |
// The typeid of the NNet.Game.EMessageId enum. | |
const MESSAGE_EVENTID_TYPEID: u8 = 1; | |
// NOTE: older builds may not support some types and the generated methods | |
// may fail to function properly, if specific backwards compatibility is | |
// needed these values should be tested against for None | |
// The typeid of the NNet.Replay.Tracker.EEventId enum. | |
const TRACKER_EVENTID_TYPEID: u8 = 2; | |
// The typeid of NNet.SVarUint32 (the type used to encode gameloop deltas). | |
const SVARUINT32_TYPEID: u8 = 7; | |
// The typeid of NNet.Replay.SGameUserId (the type used to encode player ids). | |
const REPLAY_USERID_TYPEID: u8 = 8; | |
// The typeid of NNet.Replay.SHeader (the type used to store replay game version and length). | |
const REPLAY_HEADER_TYPEID: u8 = 18; | |
// The typeid of NNet.Game.SDetails (the type used to store overall replay details). | |
const GAME_DETAILS_TYPEID: u8 = 40; | |
// The typeid of NNet.Replay.SInitData (the type used to store the inital lobby). | |
const REPLAY_INITDATA_TYPEID: u8 = 73; | |
fn instantiate_event_types<'a>() -> ( | |
HashMap<i64, (u8, &'a str)>, | |
HashMap<i64, (u8, &'a str)>, | |
HashMap<i64, (u8, &'a str)>, | |
) { | |
// Map from protocol NNet.Game.*Event eventid to (typeid, name) | |
let game_event_types: HashMap<i64, (u8, &str)> = HashMap::from([ | |
(5, (82, "NNet.s.SUserFinishedLoadingSyncEvent")), | |
(7, (81, "NNet.Game.SUserOptionsEvent")), | |
(9, (74, "NNet.Game.SBankFileEvent")), | |
(10, (76, "NNet.Game.SBankSectionEvent")), | |
(11, (77, "NNet.Game.SBankKeyEvent")), | |
(12, (78, "NNet.Game.SBankValueEvent")), | |
(13, (80, "NNet.Game.SBankSignatureEvent")), | |
(14, (85, "NNet.Game.SCameraSaveEvent")), | |
(21, (86, "NNet.Game.SSaveGameEvent")), | |
(22, (82, "NNet.Game.SSaveGameDoneEvent")), | |
(23, (82, "NNet.Game.SLoadGameDoneEvent")), | |
(25, (87, "NNet.Game.SCommandManagerResetEvent")), | |
(26, (90, "NNet.Game.SGameCheatEvent")), | |
(27, (100, "NNet.Game.SCmdEvent")), | |
(28, (109, "NNet.Game.SSelectionDeltaEvent")), | |
(29, (110, "NNet.Game.SControlGroupUpdateEvent")), | |
(30, (112, "NNet.Game.SSelectionSyncCheckEvent")), | |
(31, (114, "NNet.Game.SResourceTradeEvent")), | |
(32, (115, "NNet.Game.STriggerChatMessageEvent")), | |
(33, (118, "NNet.Game.SAICommunicateEvent")), | |
(34, (119, "NNet.Game.SSetAbsoluteGameSpeedEvent")), | |
(35, (120, "NNet.Game.SAddAbsoluteGameSpeedEvent")), | |
(36, (121, "NNet.Game.STriggerPingEvent")), | |
(37, (122, "NNet.Game.SBroadcastCheatEvent")), | |
(38, (123, "NNet.Game.SAllianceEvent")), | |
(39, (124, "NNet.Game.SUnitClickEvent")), | |
(40, (125, "NNet.Game.SUnitHighlightEvent")), | |
(41, (126, "NNet.Game.STriggerReplySelectedEvent")), | |
(43, (131, "NNet.Game.SHijackReplayGameEvent")), | |
(44, (82, "NNet.Game.STriggerSkippedEvent")), | |
(45, (136, "NNet.Game.STriggerSoundLengthQueryEvent")), | |
(46, (143, "NNet.Game.STriggerSoundOffsetEvent")), | |
(47, (144, "NNet.Game.STriggerTransmissionOffsetEvent")), | |
(48, (145, "NNet.Game.STriggerTransmissionCompleteEvent")), | |
(49, (149, "NNet.Game.SCameraUpdateEvent")), | |
(50, (82, "NNet.Game.STriggerAbortMissionEvent")), | |
(51, (132, "NNet.Game.STriggerPurchaseMadeEvent")), | |
(52, (82, "NNet.Game.STriggerPurchaseExitEvent")), | |
(53, (133, "NNet.Game.STriggerPlanetMissionLaunchedEvent")), | |
(54, (82, "NNet.Game.STriggerPlanetPanelCanceledEvent")), | |
(55, (135, "NNet.Game.STriggerDialogControlEvent")), | |
(56, (139, "NNet.Game.STriggerSoundLengthSyncEvent")), | |
(57, (150, "NNet.Game.STriggerConversationSkippedEvent")), | |
(58, (153, "NNet.Game.STriggerMouseClickedEvent")), | |
(59, (154, "NNet.Game.STriggerMouseMovedEvent")), | |
(60, (155, "NNet.Game.SAchievementAwardedEvent")), | |
(61, (156, "NNet.Game.STriggerHotkeyPressedEvent")), | |
(62, (157, "NNet.Game.STriggerTargetModeUpdateEvent")), | |
(63, (82, "NNet.Game.STriggerPlanetPanelReplayEvent")), | |
(64, (158, "NNet.Game.STriggerSoundtrackDoneEvent")), | |
(65, (159, "NNet.Game.STriggerPlanetMissionSelectedEvent")), | |
(66, (160, "NNet.Game.STriggerKeyPressedEvent")), | |
(67, (171, "NNet.Game.STriggerMovieFunctionEvent")), | |
(68, (82, "NNet.Game.STriggerPlanetPanelBirthCompleteEvent")), | |
(69, (82, "NNet.Game.STriggerPlanetPanelDeathCompleteEvent")), | |
(70, (161, "NNet.Game.SResourceRequestEvent")), | |
(71, (162, "NNet.Game.SResourceRequestFulfillEvent")), | |
(72, (163, "NNet.Game.SResourceRequestCancelEvent")), | |
(73, (82, "NNet.Game.STriggerResearchPanelExitEvent")), | |
(74, (82, "NNet.Game.STriggerResearchPanelPurchaseEvent")), | |
( | |
75, | |
(165, "NNet.Game.STriggerResearchPanelSelectionChangedEvent"), | |
), | |
(76, (164, "NNet.Game.STriggerCommandErrorEvent")), | |
(77, (82, "NNet.Game.STriggerMercenaryPanelExitEvent")), | |
(78, (82, "NNet.Game.STriggerMercenaryPanelPurchaseEvent")), | |
( | |
79, | |
(166, "NNet.Game.STriggerMercenaryPanelSelectionChangedEvent"), | |
), | |
(80, (82, "NNet.Game.STriggerVictoryPanelExitEvent")), | |
(81, (82, "NNet.Game.STriggerBattleReportPanelExitEvent")), | |
( | |
82, | |
(167, "NNet.Game.STriggerBattleReportPanelPlayMissionEvent"), | |
), | |
( | |
83, | |
(168, "NNet.Game.STriggerBattleReportPanelPlaySceneEvent"), | |
), | |
( | |
84, | |
( | |
168, | |
"NNet.Game.STriggerBattleReportPanelSelectionChangedEvent", | |
), | |
), | |
( | |
85, | |
(133, "NNet.Game.STriggerVictoryPanelPlayMissionAgainEvent"), | |
), | |
(86, (82, "NNet.Game.STriggerMovieStartedEvent")), | |
(87, (82, "NNet.Game.STriggerMovieFinishedEvent")), | |
(88, (169, "NNet.Game.SDecrementGameTimeRemainingEvent")), | |
(89, (170, "NNet.Game.STriggerPortraitLoadedEvent")), | |
(90, (172, "NNet.Game.STriggerCustomDialogDismissedEvent")), | |
(91, (173, "NNet.Game.STriggerGameMenuItemSelectedEvent")), | |
(92, (175, "NNet.Game.STriggerMouseWheelEvent")), | |
( | |
93, | |
( | |
132, | |
"NNet.Game.STriggerPurchasePanelSelectedPurchaseItemChangedEvent", | |
), | |
), | |
( | |
94, | |
( | |
176, | |
"NNet.Game.STriggerPurchasePanelSelectedPurchaseCategoryChangedEvent", | |
), | |
), | |
(95, (177, "NNet.Game.STriggerButtonPressedEvent")), | |
(96, (82, "NNet.Game.STriggerGameCreditsFinishedEvent")), | |
(97, (178, "NNet.Game.STriggerCutsceneBookmarkFiredEvent")), | |
(98, (179, "NNet.Game.STriggerCutsceneEndSceneFiredEvent")), | |
(99, (180, "NNet.Game.STriggerCutsceneConversationLineEvent")), | |
( | |
100, | |
( | |
181, | |
"NNet.Game.STriggerCutsceneConversationLineMissingEvent", | |
), | |
), | |
(101, (182, "NNet.Game.SGameUserLeaveEvent")), | |
(102, (183, "NNet.Game.SGameUserJoinEvent")), | |
(103, (185, "NNet.Game.SCommandManagerStateEvent")), | |
(104, (186, "NNet.Game.SCmdUpdateTargetPointEvent")), | |
(105, (187, "NNet.Game.SCmdUpdateTargetUnitEvent")), | |
(106, (140, "NNet.Game.STriggerAnimLengthQueryByNameEvent")), | |
(107, (141, "NNet.Game.STriggerAnimLengthQueryByPropsEvent")), | |
(108, (142, "NNet.Game.STriggerAnimOffsetEvent")), | |
(109, (188, "NNet.Game.SCatalogModifyEvent")), | |
(110, (189, "NNet.Game.SHeroTalentTreeSelectedEvent")), | |
(111, (82, "NNet.Game.STriggerProfilerLoggingFinishedEvent")), | |
( | |
112, | |
(190, "NNet.Game.SHeroTalentTreeSelectionPanelToggledEvent"), | |
), | |
(116, (191, "NNet.Game.SSetSyncLoadingTimeEvent")), | |
(117, (191, "NNet.Game.SSetSyncPlayingTimeEvent")), | |
(118, (191, "NNet.Game.SPeerSetSyncLoadingTimeEvent")), | |
(119, (191, "NNet.Game.SPeerSetSyncPlayingTimeEvent")), | |
]); | |
// Map from protocol NNet.Replay.Tracker.*Event eventid to (typeid, name) | |
let tracker_event_types: HashMap<i64, (u8, &str)> = HashMap::from([ | |
(0, (197, "NNet.Replay.Tracker.SPlayerStatsEvent")), | |
(1, (199, "NNet.Replay.Tracker.SUnitBornEvent")), | |
(2, (200, "NNet.Replay.Tracker.SUnitDiedEvent")), | |
(3, (201, "NNet.Replay.Tracker.SUnitOwnerChangeEvent")), | |
(4, (202, "NNet.Replay.Tracker.SUnitTypeChangeEvent")), | |
(5, (203, "NNet.Replay.Tracker.SUpgradeEvent")), | |
(6, (204, "NNet.Replay.Tracker.SUnitInitEvent")), | |
(7, (205, "NNet.Replay.Tracker.SUnitDoneEvent")), | |
(8, (207, "NNet.Replay.Tracker.SUnitPositionsEvent")), | |
(9, (208, "NNet.Replay.Tracker.SPlayerSetupEvent")), | |
]); | |
// Map from protocol NNet.Game.*Message eventid to (typeid, name) | |
let message_event_types: HashMap<i64, (u8, &str)> = HashMap::from([ | |
(0, (192, "NNet.Game.SChatMessage")), | |
(1, (193, "NNet.Game.SPingMessage")), | |
(2, (194, "NNet.Game.SLoadingProgressMessage")), | |
(3, (82, "NNet.Game.SServerPingMessage")), | |
(4, (195, "NNet.Game.SReconnectNotifyMessage")), | |
]); | |
(game_event_types, tracker_event_types, message_event_types) | |
} | |
#[derive(Debug, Copy, Clone)] | |
pub struct Int(pub i64, pub u8); | |
#[derive(Debug)] | |
pub struct Struct<'a>(pub &'a str, pub u8, pub i8); | |
#[derive(Debug)] | |
pub enum ProtocolTypeInfo<'a> { | |
Int(Int), | |
Blob(Int), | |
Choice(Int, HashMap<i64, (&'a str, u8)>), | |
Struct(Vec<Struct<'a>>), | |
Bool, | |
Optional(u8), | |
FourCC, | |
Array(Int, u8), | |
BitArray(Int), | |
Null, | |
} | |
const CHAR_MATCH: [char; 5] = ['[', ']', '(', ')', ',']; | |
fn match_typeinfo_structure(c: char) -> bool { | |
CHAR_MATCH.contains(&c) | |
} | |
fn handle_int(input: &str) -> Int { | |
let mut ints = input.trim_matches(match_typeinfo_structure).split(','); | |
Int( | |
ints.next().unwrap().parse::<i64>().unwrap(), | |
ints.next().unwrap().parse::<u8>().unwrap(), | |
) | |
} | |
fn handle_array(input: &str) -> ProtocolTypeInfo { | |
// structure: [(<int>, <int>), <int>], remove only square brackets first to preserve for int | |
let parts = input | |
.trim_matches(|c: char| c == '[' || c == ']') | |
.rsplit_once(',') | |
.unwrap(); | |
ProtocolTypeInfo::Array(handle_int(parts.0), parts.1.parse::<u8>().unwrap()) | |
} | |
fn handle_optional(input: &str) -> ProtocolTypeInfo { | |
let optional = input.trim_matches(match_typeinfo_structure); | |
ProtocolTypeInfo::Optional(optional.parse::<u8>().unwrap()) | |
} | |
fn parse_choice(input: &str) -> (&str, u8) { | |
let mut choice_values = input.trim_matches(match_typeinfo_structure).split(','); | |
( | |
choice_values | |
.next() | |
.unwrap() | |
.trim_matches(|c: char| c == '\''), | |
choice_values.next().unwrap().parse::<u8>().unwrap(), | |
) | |
} | |
fn handle_choice(input: &str) -> ProtocolTypeInfo { | |
let raw_choice = input | |
.trim_matches(|c: char| c == '[' || c == ']' || c == '(') | |
.split_once("),") | |
.unwrap(); | |
let int = handle_int(raw_choice.0); | |
let mut choices = HashMap::<i64, (&str, u8)>::new(); | |
let raw_choices = raw_choice | |
.1 | |
.trim_matches(|c: char| c == '{' || c == '}') | |
.split_inclusive("),"); | |
for choice in raw_choices { | |
let mut kv_pair = choice.split(':'); | |
let key = kv_pair.next().unwrap().parse::<i64>().unwrap(); | |
let value = kv_pair.next().unwrap(); | |
choices.insert(key, parse_choice(value)); | |
} | |
ProtocolTypeInfo::Choice(int, choices) | |
} | |
fn handle_struct(input: &str) -> ProtocolTypeInfo { | |
// only remove square brackets to preserve struct tuples | |
let struct_input = input | |
.trim_matches(|c: char| c == '[' || c == ']') | |
.split_inclusive("),"); | |
let mut structs = vec![]; | |
for protocol_struct in struct_input { | |
let mut struct_values = protocol_struct | |
.trim_matches(match_typeinfo_structure) | |
.split(','); | |
structs.push(Struct( | |
struct_values | |
.next() | |
.unwrap() | |
.trim_matches(|c: char| c == '\''), | |
struct_values.next().unwrap().parse::<u8>().unwrap(), | |
struct_values.next().unwrap().parse::<i8>().unwrap(), | |
)); | |
} | |
ProtocolTypeInfo::Struct(structs) | |
} | |
fn parse_typeinfos<'a>() -> Vec<ProtocolTypeInfo<'a>> { | |
let mut typeinfos: Vec<ProtocolTypeInfo> = vec![]; | |
for protocol_type in RAW_TYPEINFOS.lines() { | |
let match_outer_structure = | |
|c: char| c == '(' || c == ')' || c == ',' || c == '#' || c == ' ' || c.is_numeric(); | |
let formatted = protocol_type.trim_matches(match_outer_structure); | |
let (typename, typeinfo) = formatted.split_once(',').unwrap(); | |
// println!("typeinfo {:?}", typeinfo); | |
let parsed = match typename.trim_matches(|c: char| c == '\'') { | |
"_int" => ProtocolTypeInfo::Int(handle_int(typeinfo)), | |
"_blob" => ProtocolTypeInfo::Blob(handle_int(typeinfo)), | |
"_bool" => ProtocolTypeInfo::Bool, | |
"_array" => handle_array(typeinfo), | |
"_null" => ProtocolTypeInfo::Null, | |
"_bitarray" => ProtocolTypeInfo::BitArray(handle_int(typeinfo)), | |
"_optional" => handle_optional(typeinfo), | |
"_fourcc" => ProtocolTypeInfo::FourCC, | |
"_choice" => handle_choice(typeinfo), | |
"_struct" => handle_struct(typeinfo), | |
_other => panic!("Found unknown typeinfo {:?}", _other), | |
}; | |
// println!("parsed {:?}", parsed); | |
typeinfos.push(parsed); | |
} | |
typeinfos | |
} | |
pub struct Protocol<'a> { | |
typeinfos: Vec<ProtocolTypeInfo<'a>>, | |
game_event_types: HashMap<i64, (u8, &'a str)>, | |
tracker_event_types: HashMap<i64, (u8, &'a str)>, | |
message_event_types: HashMap<i64, (u8, &'a str)>, | |
} | |
impl<'a> Protocol<'a> { | |
pub fn new() -> Protocol<'a> { | |
let typeinfos = parse_typeinfos(); | |
let (game_event_types, tracker_event_types, message_event_types) = | |
instantiate_event_types(); | |
Protocol { | |
typeinfos, | |
game_event_types, | |
tracker_event_types, | |
message_event_types, | |
} | |
} | |
pub fn decode_replay_details(&self, contents: Vec<u8>) -> Vec<EventEntry> { | |
let mut decoder = VersionedDecoder::new(contents, &self.typeinfos); | |
let details = decoder.instance(&self.typeinfos, &GAME_DETAILS_TYPEID); | |
match details { | |
DecoderResult::Struct(values) => values, | |
_other => panic!("Found DecoderResult::{:?}", _other), | |
} | |
} | |
pub fn decode_replay_tracker_events(&self, contents: Vec<u8>) -> Vec<Event> { | |
let mut decoder = VersionedDecoder::new(contents, &self.typeinfos); | |
let mut gameloop = 0; | |
let mut events: Vec<Event> = vec![]; | |
while !VersionedDecoder::done(&decoder.buffer) { | |
let start_bits = VersionedDecoder::used_bits(&decoder.buffer); | |
let delta = decoder.instance(&self.typeinfos, &SVARUINT32_TYPEID); | |
if let DecoderResult::Gameloop((_, v)) = delta { | |
gameloop += v; | |
} else { | |
panic!("found something else {:?}", delta); | |
} | |
let event_id = match decoder.instance(&self.typeinfos, &TRACKER_EVENTID_TYPEID) { | |
DecoderResult::Value(value) => value, | |
_other => panic!("event_id is not a value: {:?}", _other), | |
}; | |
let (type_id, typename) = match self.tracker_event_types.get(&event_id) { | |
Some((type_id, typename)) => (type_id, typename), | |
None => panic!("CorruptedError: event_id({:?})", event_id), | |
}; | |
let event = match decoder.instance(&self.typeinfos, type_id) { | |
DecoderResult::Struct(mut entries) => { | |
entries.push(("_gameloop".to_string(), DecoderResult::Value(gameloop))); | |
entries.push(("_event".to_string(), DecoderResult::Name(typename.to_string()))); | |
Event::new(entries) | |
} | |
_other => panic!("Only supports Structs"), | |
}; | |
events.push(event); | |
VersionedDecoder::byte_align(&mut decoder.buffer); | |
} | |
events | |
} | |
pub fn decode_replay_game_events(&self, contents: Vec<u8>) -> Vec<Event> { | |
let mut decoder = BitPackedDecoder::new(contents, &self.typeinfos); | |
let mut gameloop = 0; | |
let mut events: Vec<Event> = vec![]; | |
while !BitPackedDecoder::done(&decoder.buffer) { | |
let start_bits = BitPackedDecoder::used_bits(&decoder.buffer); | |
let delta = decoder.instance(&self.typeinfos, &SVARUINT32_TYPEID); | |
let userid = decoder.instance(&self.typeinfos, &REPLAY_USERID_TYPEID); | |
let event_id = match decoder.instance(&self.typeinfos, &GAME_EVENTID_TYPEID) { | |
DecoderResult::Value(value) => value, | |
_other => panic!("event_id is not a value: {:?}", _other), | |
}; | |
let (type_id, typename) = match self.game_event_types.get(&event_id) { | |
Some((type_id, typename)) => (type_id, typename), | |
None => panic!("CorruptedError: event_id({:?})", event_id), | |
}; | |
let event = match decoder.instance(&self.typeinfos, type_id) { | |
DecoderResult::Struct(mut entries) => { | |
entries.push(("_event".to_string(), DecoderResult::Name(typename.to_string()))); | |
Event::new(entries) | |
} | |
_other => panic!("Only supports Structs"), | |
}; | |
events.push(event); | |
BitPackedDecoder::byte_align(&mut decoder.buffer); | |
} | |
events | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::decoders::{DecoderResult, EventEntry}; | |
use crate::mpq::MPQArchive; | |
use crate::protocol::Protocol; | |
use serde::Deserialize; | |
use std::path::PathBuf; | |
use std::time::Instant; | |
#[derive(Debug)] | |
pub struct Event { | |
pub entries: Vec<(String, DecoderResult)>, | |
} | |
impl Event { | |
pub fn new(entries: Vec<(String, DecoderResult)>) -> Event { | |
Event { | |
entries | |
} | |
} | |
} | |
#[derive(Debug, Deserialize)] | |
pub struct PlayerMetadata<'a> { | |
pub PlayerID: u8, | |
pub APM: f32, | |
pub Result: &'a str, | |
pub SelectedRace: &'a str, | |
pub AssignedRace: &'a str, | |
} | |
#[derive(Debug, Deserialize)] | |
pub struct Metadata<'a> { | |
pub Title: &'a str, | |
pub GameVersion: &'a str, | |
pub DataBuild: &'a str, | |
pub DataVersion: &'a str, | |
pub BaseBuild: &'a str, | |
pub Duration: u16, | |
// pub IsNotAvailable: bool, | |
pub Players: Vec<PlayerMetadata<'a>>, | |
} | |
#[derive(Debug)] | |
pub struct Parsed { | |
pub player_info: Vec<EventEntry>, | |
pub tracker_events: Vec<Event>, | |
pub metadata: String, | |
pub tags: String, | |
} | |
pub struct Replay { | |
pub file_path: String, | |
pub content_hash: String, | |
pub parsed: Parsed, | |
} | |
impl<'a> Replay { | |
pub fn new(file_path: PathBuf, content_hash: String, tags: Vec<&'a str>) -> Replay { | |
let path_str = file_path.to_str().unwrap(); | |
println!("parsing replay {:?}", path_str); | |
let archive = MPQArchive::new(path_str); | |
let protocol: Protocol = Protocol::new(); | |
let parsed = Replay::parse(archive, protocol, tags); | |
Replay { | |
file_path: path_str.to_string(), | |
content_hash, | |
parsed, | |
} | |
} | |
fn parse (mut archive: MPQArchive, protocol: Protocol, tags: Vec<&'a str>) -> Parsed { | |
let now = Instant::now(); | |
// let header_content = &self.archive | |
// .header | |
// .user_data_header | |
// .as_ref() | |
// .expect("No user data header") | |
// .content; | |
// // println!("read header {:.2?}", now.elapsed()); | |
let contents = archive.read_file("replay.tracker.events").unwrap(); | |
// println!("read tracker events {:.2?}", now.elapsed()); | |
// let game_info = self.archive.read_file("replay.game.events").unwrap(); | |
// // println!("read game events {:.2?}", now.elapsed()); | |
// let init_data = self.archive.read_file("replay.initData").unwrap(); | |
// // println!("read details {:.2?}", now.elapsed()); | |
let raw_metadata = archive.read_file("replay.gamemetadata.json").unwrap(); | |
let metadata = String::from_utf8(raw_metadata.clone()).unwrap(); | |
// println!("read metadata {:.2?}", now.elapsed()); | |
let details = archive.read_file("replay.details").unwrap(); | |
let player_info = protocol.decode_replay_details(details); | |
let tracker_events = protocol.decode_replay_tracker_events(contents); | |
// println!("decoded replay tracker events {:.2?}", now.elapsed()); | |
// let game_events = self.protocol.decode_replay_game_events(game_info); | |
// // println!("decoding replay game events {:.2?}", now.elapsed()); | |
println!("parsed in {:.2?}", now.elapsed()); | |
Parsed { | |
player_info, | |
tracker_events, | |
metadata, | |
tags: tags.join(","), | |
} | |
} | |
// // function that doesn't parse replay events for speed | |
// // can return high level information about game like | |
// // date, matchup, MMR, etc to decide whether to skip parsing | |
// pub fn peek() { | |
// } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment