From fb5f52038d4e2e45713d708e64964b81dce70868 Mon Sep 17 00:00:00 2001 From: Pavel Perestoronin Date: Sat, 29 Apr 2023 22:17:12 +0200 Subject: [PATCH] Break: read replay packets --- Cargo.toml | 2 + src/error.rs | 3 + src/lib.rs | 5 +- src/models.rs | 14 +--- src/models/battle_results_dat.rs | 2 +- src/models/data.rs | 140 +++++++++++-------------------- src/models/data/entity_method.rs | 28 +++++++ src/models/data/packet.rs | 32 +++++++ src/models/data/payload.rs | 51 +++++++++++ src/models/data/replay_header.rs | 11 +++ src/models/data/summary.rs | 27 ++++++ src/prelude.rs | 2 - src/replay.rs | 6 +- tests/battle_results.rs | 3 +- tests/data.rs | 23 ++--- tests/meta.rs | 2 +- 16 files changed, 232 insertions(+), 119 deletions(-) create mode 100644 src/models/data/entity_method.rs create mode 100644 src/models/data/packet.rs create mode 100644 src/models/data/payload.rs create mode 100644 src/models/data/replay_header.rs create mode 100644 src/models/data/summary.rs delete mode 100644 src/prelude.rs diff --git a/Cargo.toml b/Cargo.toml index d86061a..b834ca1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,12 +15,14 @@ default = [] meta = ["dep:serde_json"] [dependencies] +bytes = "1.4.0" byteorder = "1.4.3" prost = "0.11.3" serde = { version = "1.0.148", features = ["derive"] } serde_bytes = "0.11.7" serde-pickle = "1.1.1" serde_json = { version = "1.0.96", optional = true } +serde_with = { version = "2.3.3", features = ["hex"] } thiserror = "1.0.37" zip = { version = "0.6.3", default-features = false, features = ["deflate"] } diff --git a/src/error.rs b/src/error.rs index 05a7696..b5f7e6a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,4 +23,7 @@ pub enum Error { #[error("invalid magic: {0:#X}, expected: {1:#X}")] InvalidMagic(u32, u32), + + #[error("failed to read a packet length, got {0} bytes")] + PacketLengthError(usize), } diff --git a/src/lib.rs b/src/lib.rs index 01ba95f..fffc6a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,9 +12,8 @@ )] mod error; -mod models; -pub mod prelude; -mod replay; +pub mod models; +pub mod replay; mod result; pub use self::error::Error; diff --git a/src/models.rs b/src/models.rs index fcd45f3..3858d7f 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,10 +1,4 @@ -mod battle_results; -mod battle_results_dat; -mod data; -mod meta; - -pub use self::battle_results::*; -pub use self::battle_results_dat::*; -pub use self::data::*; -#[cfg(feature = "meta")] -pub use self::meta::*; +pub mod battle_results; +pub mod battle_results_dat; +pub mod data; +pub mod meta; diff --git a/src/models/battle_results_dat.rs b/src/models/battle_results_dat.rs index 4801759..7ac0f95 100644 --- a/src/models/battle_results_dat.rs +++ b/src/models/battle_results_dat.rs @@ -5,7 +5,7 @@ use serde::Deserialize; use serde_bytes::ByteBuf; use crate::error::Error; -use crate::models::BattleResults; +use crate::models::battle_results::BattleResults; use crate::result::Result; /// Represents un-pickled `battle_results.dat`. diff --git a/src/models/data.rs b/src/models/data.rs index 62d5f84..89d7a09 100644 --- a/src/models/data.rs +++ b/src/models/data.rs @@ -1,121 +1,83 @@ //! `data.wotreplay` models. -use std::collections::HashMap; +pub mod entity_method; +pub mod packet; +pub mod payload; +pub mod replay_header; +pub mod summary; + use std::io::Read; use byteorder::{LittleEndian, ReadBytesExt}; use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; +use serde::Serialize; +pub use self::entity_method::*; +pub use self::summary::*; +use crate::models::data::packet::Packet; +use crate::models::data::payload::Payload; +use crate::models::data::replay_header::ReplayHeader; use crate::result::Result; use crate::Error; /// `data.wotreplay` root structure. #[derive(Debug, Serialize)] pub struct Data { + /// For example: `9.8.5_apple`. pub client_version: String, - pub author_nickname: String, - pub arena_unique_id: u64, - pub arena_type_id: u32, - pub summary: Summary, -} -#[derive(Debug, Serialize, Deserialize)] -pub struct Summary { - #[serde(rename = "playersBattleCategoriesIds")] - pub players_battle_categories_ids: HashMap, + pub replay_header: Option, - #[serde(rename = "battleLevel")] - pub battle_level: u8, - - #[serde(rename = "battleCategoryId")] - pub battle_category_id: u8, - - #[serde(rename = "mouseEnabled")] - pub is_mouse_enabled: bool, - - #[serde(rename = "mmType")] - pub matchmaker_type: u8, - - #[serde(rename = "camouflageSlot")] - pub camouflage_slot: u8, - - #[serde(rename = "avgMmr")] - pub average_mmr: Vec, + pub other_packets: Vec, } impl Data { pub fn from_reader(mut reader: impl Read) -> Result { - Self::assert_magic(reader.read_u32::()?, 0x12345678)?; - reader.read_u64::()?; // TODO - read_bytes(&mut reader)?; // TODO + assert_magic(reader.read_u32::()?, 0x12345678)?; + + // No idea what it is: + reader.read_u64::()?; + + // Some sort of client hash, e.g.: `6CF2A9EFA5C52D6F6CE43A6D4A699C05`: + read_bytes(&mut reader)?; + let client_version = read_string(&mut reader)?; - reader.read_exact(&mut [0; 49])?; // TODO - let author_nickname = read_string(&mut reader)?; - let arena_unique_id = reader.read_u64::()?; - let arena_type_id = reader.read_u32::()?; - let summary = { - Self::assert_magic(reader.read_u8()?, 0xFF)?; - let pickled_length = reader.read_u16::()?; - Self::assert_magic(reader.read_u8()?, 0x00)?; - read_pickled(&mut reader, pickled_length as usize)? - }; + + // Some extra byte, no idea: + reader.read_u8()?; + + let mut this = Self::new(client_version); + + loop { + let Some(packet) = Packet::from_reader(&mut reader)? else { break }; + match packet.payload { + Payload::ReplayHeader(header) => { + this.replay_header = Some(header); + } + _ => { + this.other_packets.push(packet); + } + } + } - let this = Self { - client_version, - author_nickname, - arena_unique_id, - arena_type_id, - summary, - }; Ok(this) } - #[inline] - fn assert_magic + PartialEq>(actual: T, expected: T) -> Result { - if actual == expected { - Ok(()) - } else { - Err(Error::InvalidMagic(actual.into(), expected.into())) + pub const fn new(client_version: String) -> Self { + Self { + client_version, + replay_header: None, + other_packets: Vec::new(), } } } -#[derive(Debug)] -struct Packet { - clock: f32, - payload: Payload, -} - -impl Packet { - fn from_reader(reader: &mut impl Read) -> Result { - let length = reader.read_u32::()?; - let type_ = reader.read_u32::()?; - let clock = reader.read_f32::()?; - let payload = Payload::from_reader(reader, type_, length)?; - let this = Self { clock, payload }; - Ok(this) - } -} - -#[derive(Debug)] -pub enum Payload { - EntityProperty, - EntityMethod, - Other, -} - -impl Payload { - fn from_reader(reader: &mut impl Read, type_: u32, length: u32) -> Result { - let this = match type_ { - _ => { - let mut buffer = Vec::new(); - buffer.resize(length as usize, 0); - reader.read_exact(&mut buffer)?; - Self::Other - } - }; - Ok(this) +#[inline] +fn assert_magic + PartialEq>(actual: T, expected: T) -> Result { + if actual == expected { + Ok(()) + } else { + Err(Error::InvalidMagic(actual.into(), expected.into())) } } diff --git a/src/models/data/entity_method.rs b/src/models/data/entity_method.rs new file mode 100644 index 0000000..7d7f68c --- /dev/null +++ b/src/models/data/entity_method.rs @@ -0,0 +1,28 @@ +use byteorder::{LittleEndian, ReadBytesExt}; +use serde::Serialize; + +use crate::result::Result; + +#[derive(Debug, Serialize)] +pub enum EntityMethod { + /// TODO: team composition. + Subtype2F, + + /// Default variant when subtype is not known. + Unknown { sub_type: u32, payload: Vec }, +} + +impl EntityMethod { + pub fn new(payload: Vec) -> Result { + let mut reader = payload.as_slice(); + + reader.read_u32::()?; + let sub_type = reader.read_u32::()?; + + let this = match sub_type { + 0x2F => Self::Subtype2F, + _ => Self::Unknown { sub_type, payload }, + }; + Ok(this) + } +} diff --git a/src/models/data/packet.rs b/src/models/data/packet.rs new file mode 100644 index 0000000..18489d6 --- /dev/null +++ b/src/models/data/packet.rs @@ -0,0 +1,32 @@ +use std::io::Read; + +use byteorder::{LittleEndian, ReadBytesExt}; +use serde::Serialize; + +use crate::models::data::payload::Payload; +use crate::result::Result; + +#[derive(Debug, Serialize)] +pub struct Packet { + pub clock: f32, + pub payload: Payload, +} + +impl Packet { + pub fn from_reader(reader: &mut impl Read) -> Result> { + let Ok(length) = reader.read_u32::() else { return Ok(None) }; + let type_ = reader.read_u32::()?; + let clock = reader.read_f32::()?; + let payload = Self::read_payload(reader, length as usize)?; + let payload = Payload::new(type_, payload)?; + let this = Self { clock, payload }; + Ok(Some(this)) + } + + fn read_payload(reader: &mut impl Read, length: usize) -> Result> { + let mut buffer = Vec::new(); + buffer.resize(length, 0); + reader.read_exact(&mut buffer)?; + Ok(buffer) + } +} diff --git a/src/models/data/payload.rs b/src/models/data/payload.rs new file mode 100644 index 0000000..8aab0e2 --- /dev/null +++ b/src/models/data/payload.rs @@ -0,0 +1,51 @@ +use std::io::Read; + +use byteorder::{LittleEndian, ReadBytesExt}; +use serde::Serialize; +use serde_with::serde_as; + +use crate::models::data::replay_header::ReplayHeader; +use crate::models::data::{assert_magic, read_pickled, read_string, EntityMethod}; +use crate::result::Result; + +#[serde_as] +#[derive(Debug, Serialize)] +pub enum Payload { + ReplayHeader(ReplayHeader), + EntityMethod(EntityMethod), + + /// Default payload when type is not known. + Unknown(#[serde_as(as = "serde_with::hex::Hex")] Vec), +} + +impl Payload { + pub fn new(type_: u32, payload: Vec) -> Result { + let this = match type_ { + 0 => { + let mut reader = payload.as_slice(); + reader.read_exact(&mut [0; 10])?; + + let author_nickname = read_string(&mut reader)?; + let arena_unique_id = reader.read_u64::()?; + let arena_type_id = reader.read_u32::()?; + let summary = { + assert_magic(reader.read_u8()?, 0xFF)?; + let pickled_length = reader.read_u16::()?; + assert_magic(reader.read_u8()?, 0x00)?; + read_pickled(&mut reader, pickled_length as usize)? + }; + Self::ReplayHeader(ReplayHeader { + summary, + author_nickname, + arena_unique_id, + arena_type_id, + }) + } + + 8 => Self::EntityMethod(EntityMethod::new(payload)?), + + _ => Self::Unknown(payload), + }; + Ok(this) + } +} diff --git a/src/models/data/replay_header.rs b/src/models/data/replay_header.rs new file mode 100644 index 0000000..ce122e1 --- /dev/null +++ b/src/models/data/replay_header.rs @@ -0,0 +1,11 @@ +use serde::Serialize; + +use crate::models::data::Summary; + +#[derive(Debug, Serialize)] +pub struct ReplayHeader { + pub summary: Summary, + pub author_nickname: String, + pub arena_unique_id: u64, + pub arena_type_id: u32, +} diff --git a/src/models/data/summary.rs b/src/models/data/summary.rs new file mode 100644 index 0000000..20e9fdf --- /dev/null +++ b/src/models/data/summary.rs @@ -0,0 +1,27 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Summary { + #[serde(rename = "playersBattleCategoriesIds")] + pub players_battle_categories_ids: HashMap, + + #[serde(rename = "battleLevel")] + pub battle_level: u8, + + #[serde(rename = "battleCategoryId")] + pub battle_category_id: u8, + + #[serde(rename = "mouseEnabled")] + pub is_mouse_enabled: bool, + + #[serde(rename = "mmType")] + pub matchmaker_type: u8, + + #[serde(rename = "camouflageSlot")] + pub camouflage_slot: u8, + + #[serde(rename = "avgMmr")] + pub average_mmr: Vec, +} diff --git a/src/prelude.rs b/src/prelude.rs deleted file mode 100644 index e489608..0000000 --- a/src/prelude.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub use crate::models::*; -pub use crate::replay::Replay; diff --git a/src/replay.rs b/src/replay.rs index 5b0cc0a..1b3d215 100644 --- a/src/replay.rs +++ b/src/replay.rs @@ -2,9 +2,11 @@ use std::io::{Read, Seek}; use zip::ZipArchive; +use crate::models::battle_results::BattleResults; +use crate::models::battle_results_dat::BattleResultsDat; +use crate::models::data::Data; #[cfg(feature = "meta")] -use crate::models::Meta; -use crate::models::{BattleResults, BattleResultsDat, Data}; +use crate::models::meta::Meta; use crate::result::Result; /// Opened (but not yet parsed) replay. diff --git a/tests/battle_results.rs b/tests/battle_results.rs index 067e366..34a0667 100644 --- a/tests/battle_results.rs +++ b/tests/battle_results.rs @@ -1,7 +1,8 @@ use std::fs::File; use anyhow::Result; -use wotbreplay_parser::prelude::*; +use wotbreplay_parser::models::battle_results::BattleResults; +use wotbreplay_parser::replay::Replay; #[test] fn player_results_ok() -> Result<()> { diff --git a/tests/data.rs b/tests/data.rs index 88280de..c0e6d69 100644 --- a/tests/data.rs +++ b/tests/data.rs @@ -1,7 +1,7 @@ use std::fs::File; use anyhow::Result; -use wotbreplay_parser::prelude::*; +use wotbreplay_parser::replay::Replay; #[test] fn data_ok() -> Result<()> { @@ -9,15 +9,18 @@ fn data_ok() -> Result<()> { let data = replay.read_data()?; assert_eq!(data.client_version, "9.4.0_apple"); - assert_eq!(data.author_nickname, "zeekrab"); - assert_eq!(data.arena_unique_id, 1661909200500084); - assert_eq!(data.arena_type_id, 65567); - assert_eq!(data.summary.battle_level, 8); - assert_eq!(data.summary.battle_category_id, 13); - assert!(data.summary.is_mouse_enabled); - assert_eq!(data.summary.matchmaker_type, 2); - assert_eq!(data.summary.camouflage_slot, 1); - assert_eq!(data.summary.average_mmr, [-25.396859946846963, 77.58806455880404]); + assert_eq!(data.other_packets.len(), 44720); + + let header = data.replay_header.unwrap(); + assert_eq!(header.author_nickname, "zeekrab"); + assert_eq!(header.arena_unique_id, 1661909200500084); + assert_eq!(header.arena_type_id, 65567); + assert_eq!(header.summary.battle_level, 8); + assert_eq!(header.summary.battle_category_id, 13); + assert!(header.summary.is_mouse_enabled); + assert_eq!(header.summary.matchmaker_type, 2); + assert_eq!(header.summary.camouflage_slot, 1); + assert_eq!(header.summary.average_mmr, [-25.396859946846963, 77.58806455880404]); Ok(()) } diff --git a/tests/meta.rs b/tests/meta.rs index f33eab7..ee82e60 100644 --- a/tests/meta.rs +++ b/tests/meta.rs @@ -3,7 +3,7 @@ use std::fs::File; use anyhow::Result; -use wotbreplay_parser::prelude::*; +use wotbreplay_parser::replay::Replay; #[test] fn meta_ok() -> Result<()> {