Skip to content

Instantly share code, notes, and snippets.

@ZephyrBlu
Created November 19, 2022 12:18
Show Gist options
  • Save ZephyrBlu/e6d6ad4cb3e28d1fc7adab86e6b8848c to your computer and use it in GitHub Desktop.
Save ZephyrBlu/e6d6ad4cb3e28d1fc7adab86e6b8848c to your computer and use it in GitHub Desktop.
Rust SC2 Parser
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)
}
}
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")
}
}
}
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,
}
}
}
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")
}
}
}
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(())
}
}
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)
}
}
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(())
}
}
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
}
}
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