mirror of
https://github.com/eigenein/wotbreplay-parser.git
synced 2026-10-21 10:27:01 +02:00
Compare commits
14
Commits
0.3.0-beta.2
...
0.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1cddbecd5 | ||
|
|
bad56d0121 | ||
|
|
c2ddaf4d0e | ||
|
|
50f15b9ae9 | ||
|
|
da0646f40a | ||
|
|
1313bc82f7 | ||
|
|
c986abca5d | ||
|
|
838a49ee68 | ||
|
|
a177447ca1 | ||
|
|
2d2c85c18a | ||
|
|
0f437c39de | ||
|
|
12aa208217 | ||
|
|
181da1ae96 | ||
|
|
0ac983832b |
No files matched your search
@@ -1,5 +1 @@
|
||||
github: [eigenein]
|
||||
custom:
|
||||
- 'https://www.paypal.me/eigenein'
|
||||
- 'https://bunq.me/eigenein'
|
||||
- 'https://money.yandex.ru/to/410011693851843'
|
||||
Generated
+1
@@ -4,6 +4,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/benches" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
+8
-1
@@ -12,7 +12,8 @@ categories = ["parsing"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
meta = ["dep:serde_json"]
|
||||
meta = ["dep:serde_json"] # `meta.json` parsing
|
||||
raw-payload = [] # save raw packet payloads, enabling increases memory usage and decreases performance
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.4.0"
|
||||
@@ -22,9 +23,15 @@ 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_repr = "0.1.12"
|
||||
serde_with = { version = "2.3.3", features = ["hex"] }
|
||||
thiserror = "1.0.37"
|
||||
zip = { version = "0.6.3", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.66"
|
||||
criterion = "0.4.0"
|
||||
|
||||
[[bench]]
|
||||
name = "data"
|
||||
harness = false
|
||||
@@ -18,10 +18,9 @@ use wotbreplay_parser::models::battle_results::TeamNumber;
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
let battle_results = Replay::open(File::open("replays/20221203_player_results.wotbreplay")?)?.read_battle_results()?;
|
||||
|
||||
assert_eq!(battle_results.timestamp, 1670083956);
|
||||
assert_eq!(battle_results.timestamp_secs, 1670083956);
|
||||
assert_eq!(battle_results.players.len(), 14);
|
||||
|
||||
assert_eq!(battle_results.players[0].account_id, 595693744);
|
||||
@@ -36,26 +35,3 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Replay structure
|
||||
|
||||
`*.wotbreplay` is a ZIP-archive containing:
|
||||
- `battle_results.dat`
|
||||
- `meta.json`
|
||||
- `data.wotreplay`
|
||||
|
||||
### `battle_results.dat`
|
||||
|
||||
This is a [pickled](https://docs.python.org/3/library/pickle.html) 2-tuple:
|
||||
- Some big number
|
||||
- Battle results serialized with [Protocol Buffers](https://developers.google.com/protocol-buffers)
|
||||
|
||||
#### Useful tools
|
||||
|
||||
- [Protobuf Decoder](https://protobuf-decoder.netlify.app/)
|
||||
- [`protobuf-inspector`](https://github.com/mildsunrise/protobuf-inspector)
|
||||
- [Hex Fiend](https://hexfiend.com/)
|
||||
|
||||
### `meta.json`
|
||||
|
||||
### `data.wotreplay`
|
||||
@@ -0,0 +1,20 @@
|
||||
use std::fs::File;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
fn bench_read_data(criterion: &mut Criterion) {
|
||||
let mut group = criterion.benchmark_group("read_data");
|
||||
for path in [
|
||||
"replays/20230429_0126__helaas_pindakaas.wotbreplay",
|
||||
"replays/20230503_0027__helaas_pindakaas.wotbreplay",
|
||||
"replays/20230429_1682194799_malinovka.wotbreplay",
|
||||
] {
|
||||
let mut replay = Replay::open(File::open(path).unwrap()).unwrap();
|
||||
group.bench_function(path, |bencher| bencher.iter(|| replay.read_data().unwrap()));
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_read_data);
|
||||
criterion_main!(benches);
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
BIN
Binary file not shown.
BIN
Binary file not shown.
+8
-4
@@ -21,9 +21,13 @@ pub enum Error {
|
||||
#[error("JSON error")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
|
||||
#[error("invalid magic: {0:#X}, expected: {1:#X}")]
|
||||
InvalidMagic(u32, u32),
|
||||
#[error("invalid magic: {actual:#X}, expected: {expected:#X}")]
|
||||
InvalidMagic { expected: u32, actual: u32 },
|
||||
|
||||
#[error("failed to read a packet length, got {0} bytes")]
|
||||
PacketLengthError(usize),
|
||||
#[error("failed to parse type {type_} packet's payload at replay time {clock_secs}s")]
|
||||
PacketPayloadParsingError {
|
||||
source: Box<Error>,
|
||||
type_: u32,
|
||||
clock_secs: f32,
|
||||
},
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod battle_results;
|
||||
pub mod battle_results_dat;
|
||||
pub mod data;
|
||||
pub mod map_id;
|
||||
pub mod meta;
|
||||
pub mod room_type;
|
||||
@@ -2,12 +2,16 @@ use prost::bytes::Buf;
|
||||
use prost::{Enumeration, Message};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::models::room_type::RoomType;
|
||||
use crate::result::Result;
|
||||
|
||||
impl BattleResults {
|
||||
/// Parse battle results from the buffer.
|
||||
///
|
||||
/// Note, that this does NOT parse `battle_results.dat` itself, but the un-pickled tuple element.
|
||||
/// # Note
|
||||
///
|
||||
/// This can **not** parse `battle_results.dat` itself, but the un-pickled tuple element.
|
||||
/// To parse `battle_results.dat`, use [`crate::models::battle_results_dat::BattleResultsDat`].
|
||||
pub fn from_buffer(buffer: impl Buf) -> Result<Self> {
|
||||
Ok(Self::decode(buffer)?)
|
||||
}
|
||||
@@ -18,9 +22,18 @@ impl BattleResults {
|
||||
/// TODO: 150[0].21[0].8[0].801[0].unsigned is arena end time.
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct BattleResults {
|
||||
/// The two least-significant bytes is map ID.
|
||||
///
|
||||
/// The 3rd byte is probably a game mode:
|
||||
///
|
||||
/// - `0x0xxxx` is «encounter»
|
||||
/// - `0x1xxxx` flag is «supremacy»
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub mode_map_id: u32,
|
||||
|
||||
/// Battle timestamp.
|
||||
#[prost(int64, tag = "2")]
|
||||
pub timestamp: i64,
|
||||
pub timestamp_secs: i64,
|
||||
|
||||
#[prost(enumeration = "TeamNumber", optional, tag = "3")]
|
||||
pub winner_team_number: Option<i32>,
|
||||
@@ -29,6 +42,9 @@ pub struct BattleResults {
|
||||
#[prost(message, required, tag = "8")]
|
||||
pub author: Author,
|
||||
|
||||
#[prost(enumeration = "RoomType", tag = "9")]
|
||||
pub room_type: i32,
|
||||
|
||||
/// Author's free XP, including premium.
|
||||
#[prost(uint32, tag = "137")]
|
||||
pub free_xp: u32,
|
||||
@@ -37,7 +53,7 @@ pub struct BattleResults {
|
||||
#[prost(message, repeated, tag = "201")]
|
||||
pub players: Vec<Player>,
|
||||
|
||||
/// Player's results.
|
||||
/// Individual player's results.
|
||||
#[prost(message, repeated, tag = "301")]
|
||||
pub player_results: Vec<PlayerResults>,
|
||||
}
|
||||
@@ -62,8 +78,9 @@ pub struct PlayerInfo {
|
||||
pub nickname: String,
|
||||
|
||||
/// Some sort of platoon ID:
|
||||
/// - contains same ID for a platoon members, or
|
||||
/// - `None` for non-platoon players
|
||||
///
|
||||
/// - contains same ID for the platoon members, or
|
||||
/// - [`None`] for non-platoon players
|
||||
#[prost(uint32, optional, tag = "2")]
|
||||
pub platoon_id: Option<u32>,
|
||||
|
||||
@@ -79,7 +96,6 @@ pub struct PlayerInfo {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Enumeration, Serialize)]
|
||||
#[repr(i32)]
|
||||
pub enum TeamNumber {
|
||||
One = 1,
|
||||
Two = 2,
|
||||
@@ -162,6 +178,8 @@ pub struct PlayerResultsInfo {
|
||||
///
|
||||
/// Note, that this is **not** the game client's displayed rating.
|
||||
/// This field matches the `mm_rating` as returned by the Wargaming.net API.
|
||||
///
|
||||
/// The display rating is calculated as: `3000.0 + mm_rating * 10.0`.
|
||||
#[prost(float, optional, tag = "107")]
|
||||
pub mm_rating: Option<f32>,
|
||||
|
||||
@@ -170,7 +188,7 @@ pub struct PlayerResultsInfo {
|
||||
}
|
||||
|
||||
impl PlayerResultsInfo {
|
||||
/// Returns displayed rating as per the game client.
|
||||
/// Calculate displayed rating according to the game client.
|
||||
pub fn display_rating(&self) -> Option<u32> {
|
||||
self.mm_rating
|
||||
.map(|mm_rating| (mm_rating * 10.0 + 3000.0) as u32)
|
||||
@@ -179,13 +197,31 @@ impl PlayerResultsInfo {
|
||||
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct Author {
|
||||
/// # Note
|
||||
///
|
||||
/// The field is `-2` if the player is auto-destroyed by inactivity.
|
||||
#[prost(int32, tag = "1")]
|
||||
pub hitpoints_left: i32,
|
||||
|
||||
/// Earned credits, including premium and special awards and medals.
|
||||
#[prost(uint32, tag = "2")]
|
||||
pub credits_earned: u32,
|
||||
pub total_credits: u32,
|
||||
|
||||
/// Combat XP, including premium.
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub combat_xp: u32,
|
||||
pub total_xp: u32,
|
||||
|
||||
#[prost(uint32, tag = "4")]
|
||||
pub n_shots: u32,
|
||||
|
||||
#[prost(uint32, tag = "5")]
|
||||
pub n_hits: u32,
|
||||
|
||||
#[prost(uint32, tag = "6")]
|
||||
pub n_splashes: u32,
|
||||
|
||||
#[prost(uint32, tag = "7")]
|
||||
pub n_penetrations: u32,
|
||||
|
||||
#[prost(uint32, tag = "8")]
|
||||
pub damage_dealt: u32,
|
||||
|
||||
@@ -8,7 +8,14 @@ use crate::error::Error;
|
||||
use crate::models::battle_results::BattleResults;
|
||||
use crate::result::Result;
|
||||
|
||||
/// Represents un-pickled `battle_results.dat`.
|
||||
/// Un-pickled `battle_results.dat`.
|
||||
///
|
||||
/// # `battle_results.dat` structure
|
||||
///
|
||||
/// Entire file is a [pickled](https://docs.python.org/3/library/pickle.html) 2-tuple:
|
||||
///
|
||||
/// - Arena unique ID
|
||||
/// - Battle results serialized with [Protocol Buffers](https://developers.google.com/protocol-buffers)
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(from = "(u64, ByteBuf)")]
|
||||
pub struct BattleResultsDat {
|
||||
@@ -20,15 +27,12 @@ pub struct BattleResultsDat {
|
||||
|
||||
impl From<(u64, ByteBuf)> for BattleResultsDat {
|
||||
fn from((arena_unique_id, buffer): (u64, ByteBuf)) -> Self {
|
||||
Self {
|
||||
arena_unique_id,
|
||||
buffer,
|
||||
}
|
||||
Self { arena_unique_id, buffer }
|
||||
}
|
||||
}
|
||||
|
||||
impl BattleResultsDat {
|
||||
/// Parses the pickled structure from the reader which contains `battle_results.dat`.
|
||||
/// Parse the pickled structure from the reader which contains `battle_results.dat`.
|
||||
///
|
||||
/// It can be used either on the `battle_results.dat` for a replay archive,
|
||||
/// or directly on `**/DAVAProject/battle_results/*/*_full.dat`.
|
||||
@@ -40,7 +44,7 @@ impl BattleResultsDat {
|
||||
impl TryInto<BattleResults> for BattleResultsDat {
|
||||
type Error = Error;
|
||||
|
||||
/// Decodes the battle results from the internal buffer.
|
||||
/// Decode the battle results from the internal buffer.
|
||||
fn try_into(self) -> Result<BattleResults> {
|
||||
Ok(BattleResults::decode(self.buffer.as_ref())?)
|
||||
}
|
||||
|
||||
+9
-2
@@ -53,7 +53,10 @@ fn assert_magic<T: Into<u32> + PartialEq>(actual: T, expected: T) -> Result {
|
||||
if actual == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::InvalidMagic(actual.into(), expected.into()))
|
||||
Err(Error::InvalidMagic {
|
||||
actual: actual.into(),
|
||||
expected: expected.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +85,11 @@ fn read_pickled<T: DeserializeOwned>(reader: &mut impl Read, length: usize) -> R
|
||||
/// Read 2-byte length surrounded by the magical `0xFF` and `0x00`,
|
||||
/// unless it's a simple 1-byte length.
|
||||
///
|
||||
/// P.S. Okay, Wargaming, I've no idea what you smoke, but I want some!
|
||||
/// # Context
|
||||
///
|
||||
/// It seems that for payloads larger than 254 bytes, Wargaming writes `00 <XX XX> FF`,
|
||||
/// where the word in the middle is the actual length of a payload, and the `00` and `FF`
|
||||
/// are magic values.
|
||||
#[inline]
|
||||
fn read_quirky_length(reader: &mut impl Read) -> Result<usize> {
|
||||
match reader.read_u8()? {
|
||||
|
||||
@@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BasePlayerCreate {
|
||||
#[serde(rename(deserialize = "playersBattleCategoriesIds"))]
|
||||
pub players_battle_categories_ids: HashMap<u32, (u8, u32)>,
|
||||
#[serde(default, rename(deserialize = "playersBattleCategoriesIds"))]
|
||||
pub players_battle_categories_ids: Option<HashMap<u32, (u8, u32)>>,
|
||||
|
||||
#[serde(rename(deserialize = "battleLevel"))]
|
||||
pub battle_level: u8,
|
||||
@@ -22,19 +22,31 @@ pub struct BasePlayerCreate {
|
||||
#[serde(rename(deserialize = "camouflageSlot"))]
|
||||
pub camouflage_slot: u8,
|
||||
|
||||
#[serde(rename(deserialize = "avgMmr"))]
|
||||
pub average_mmr: Vec<Option<f64>>,
|
||||
/// Average team ratings for rating battles.
|
||||
///
|
||||
/// # Contains
|
||||
///
|
||||
/// - [`Some`] 2-vector, if the attribute is present in the replay. Each element represents one team:
|
||||
/// - [`Some`] rating, if known
|
||||
/// - [`None`], if the average rating is unknown (all the players are calibrating)
|
||||
/// - [`None`], if the attribute is missing in the replay
|
||||
#[serde(default, rename(deserialize = "avgMmr"))]
|
||||
pub average_mm_ratings: Option<Vec<Option<f64>>>,
|
||||
|
||||
#[serde(rename(deserialize = "playerWaitTimes"))]
|
||||
pub player_wait_times: HashMap<u32, f64>,
|
||||
#[serde(default, rename(deserialize = "playerWaitTimes"))]
|
||||
pub player_wait_times: Option<HashMap<u32, f64>>,
|
||||
|
||||
#[serde(rename(deserialize = "accountDatabaseIds"))]
|
||||
pub account_database_ids: Vec<u32>,
|
||||
pub account_database_ids: Option<Vec<u32>>,
|
||||
|
||||
#[serde(rename(deserialize = "turboBattlesStats"))]
|
||||
pub turbo_battles_statistics: TurboBattlesStatistics,
|
||||
#[serde(default, rename(deserialize = "turboBattlesStats"))]
|
||||
pub turbo_battles_statistics: Option<TurboBattlesStatistics>,
|
||||
}
|
||||
|
||||
/// I don't know what that is:
|
||||
///
|
||||
/// - On EU, Asia, and NA the values are just constants
|
||||
/// - On Russia, the `alpha` and `beta` are different constants, and the chances actually differ for different replays
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TurboBattlesStatistics {
|
||||
#[serde(rename(deserialize = "turboBattleChance"))]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Structures for «entity method» call packets.
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use prost::encoding::decode_varint;
|
||||
use prost::Message;
|
||||
@@ -13,48 +15,37 @@ pub mod update_arena;
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum EntityMethod {
|
||||
/// Subtype `0x2F`.
|
||||
/// Subtype 47.
|
||||
UpdateArena {
|
||||
field_number: u64,
|
||||
arguments: UpdateArena,
|
||||
|
||||
/// Original payload for investigation.
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Default variant when subtype is not known by the parser.
|
||||
Unknown {
|
||||
sub_type: u32,
|
||||
|
||||
/// Whole packet payload (including the sub-type).
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
Unknown { sub_type: u32 },
|
||||
}
|
||||
|
||||
impl EntityMethod {
|
||||
/// Parse the entity method payload.
|
||||
pub fn new(payload: Vec<u8>) -> Result<Self> {
|
||||
let mut reader = payload.as_slice();
|
||||
/// Parse the entity method raw payload.
|
||||
pub fn new(raw_payload: &[u8]) -> Result<Self> {
|
||||
let mut raw_payload = raw_payload;
|
||||
|
||||
reader.read_u32::<LittleEndian>()?;
|
||||
let sub_type = reader.read_u32::<LittleEndian>()?;
|
||||
raw_payload.read_u32::<LittleEndian>()?;
|
||||
let sub_type = raw_payload.read_u32::<LittleEndian>()?;
|
||||
|
||||
let this = match sub_type {
|
||||
0x2F => {
|
||||
let _inner_length = reader.read_u32::<LittleEndian>()?;
|
||||
47 => {
|
||||
let _inner_length = raw_payload.read_u32::<LittleEndian>()?;
|
||||
|
||||
let field_number = decode_varint(&mut reader)?;
|
||||
let message_length = read_quirky_length(&mut reader)?;
|
||||
let field_number = decode_varint(&mut raw_payload)?;
|
||||
let message_length = read_quirky_length(&mut raw_payload)?;
|
||||
Self::UpdateArena {
|
||||
field_number,
|
||||
arguments: UpdateArena::decode(&reader[..message_length])?,
|
||||
payload,
|
||||
arguments: UpdateArena::decode(&raw_payload[..message_length])?,
|
||||
}
|
||||
}
|
||||
|
||||
_ => Self::Unknown { sub_type, payload },
|
||||
_ => Self::Unknown { sub_type },
|
||||
};
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
@@ -2,28 +2,61 @@ use std::io::Read;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use serde::Serialize;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::models::data::payload::Payload;
|
||||
use crate::result::Result;
|
||||
use crate::Error;
|
||||
|
||||
/// «Packet» is a single traffic message in a replay,
|
||||
/// packets tell us what is going on during the battle.
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Packet {
|
||||
pub clock: f32,
|
||||
/// Replay time, starting from zero.
|
||||
#[serde(rename = "clock")]
|
||||
pub clock_secs: f32,
|
||||
|
||||
/// Parsed packet payload.
|
||||
pub payload: Payload,
|
||||
|
||||
/// Raw packet payload, excluding the clock and packet type, and including optional subtype.
|
||||
///
|
||||
/// This attribute requires the `raw-payload` feature.
|
||||
#[cfg(feature = "raw-payload")]
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
pub raw_payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
/// Parse next packet from the reader.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Parsed packet, or [`None`], when no packets are left.
|
||||
pub fn from_reader(reader: &mut impl Read) -> Result<Option<Self>> {
|
||||
let Ok(length) = reader.read_u32::<LittleEndian>() else { return Ok(None) };
|
||||
let type_ = reader.read_u32::<LittleEndian>()?;
|
||||
let clock = reader.read_f32::<LittleEndian>()?;
|
||||
let payload = Self::read_payload(reader, length as usize)?;
|
||||
let payload = Payload::new(type_, payload)?;
|
||||
let this = Self { clock, payload };
|
||||
let clock_secs = reader.read_f32::<LittleEndian>()?;
|
||||
let raw_payload = Self::read_raw_payload(reader, length as usize)?;
|
||||
let payload = Payload::new(type_, &raw_payload).map_err(|source| {
|
||||
Error::PacketPayloadParsingError {
|
||||
source: Box::new(source),
|
||||
type_,
|
||||
clock_secs,
|
||||
}
|
||||
})?;
|
||||
let this = Self {
|
||||
clock_secs,
|
||||
payload,
|
||||
|
||||
#[cfg(feature = "raw-payload")]
|
||||
raw_payload,
|
||||
};
|
||||
Ok(Some(this))
|
||||
}
|
||||
|
||||
fn read_payload(reader: &mut impl Read, length: usize) -> Result<Vec<u8>> {
|
||||
fn read_raw_payload(reader: &mut impl Read, length: usize) -> Result<Vec<u8>> {
|
||||
let mut buffer = Vec::new();
|
||||
buffer.resize(length, 0);
|
||||
reader.read_exact(&mut buffer)?;
|
||||
|
||||
+18
-21
@@ -9,6 +9,7 @@ use crate::models::data::entity_method::EntityMethod;
|
||||
use crate::models::data::{read_pickled, read_quirky_length, read_string};
|
||||
use crate::result::Result;
|
||||
|
||||
/// Packet payload.
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum Payload {
|
||||
@@ -24,40 +25,36 @@ pub enum Payload {
|
||||
EntityMethod(EntityMethod),
|
||||
|
||||
/// Default payload when type is not known by the parser.
|
||||
Unknown {
|
||||
type_: u32,
|
||||
|
||||
/// Whole packet payload.
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
raw: Vec<u8>,
|
||||
},
|
||||
Unknown { packet_type: u32 },
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
pub fn new(type_: u32, payload: Vec<u8>) -> Result<Self> {
|
||||
let this = match type_ {
|
||||
0 => {
|
||||
let mut reader = payload.as_slice();
|
||||
reader.read_exact(&mut [0; 10])?;
|
||||
/// Parse the raw packet payload.
|
||||
pub fn new(packet_type: u32, raw_payload: &[u8]) -> Result<Self> {
|
||||
let mut raw_payload = raw_payload;
|
||||
|
||||
let author_nickname = read_string(&mut reader)?;
|
||||
let arena_unique_id = reader.read_u64::<LittleEndian>()?;
|
||||
let arena_type_id = reader.read_u32::<LittleEndian>()?;
|
||||
let type_0 = {
|
||||
let pickled_length = read_quirky_length(&mut reader)?;
|
||||
read_pickled(&mut reader, pickled_length)?
|
||||
let this = match packet_type {
|
||||
0 => {
|
||||
raw_payload.read_exact(&mut [0; 10])?;
|
||||
|
||||
let author_nickname = read_string(&mut raw_payload)?;
|
||||
let arena_unique_id = raw_payload.read_u64::<LittleEndian>()?;
|
||||
let arena_type_id = raw_payload.read_u32::<LittleEndian>()?;
|
||||
let arguments = {
|
||||
let pickled_length = read_quirky_length(&mut raw_payload)?;
|
||||
read_pickled(&mut raw_payload, pickled_length)?
|
||||
};
|
||||
Self::BasePlayerCreate {
|
||||
arguments: type_0,
|
||||
arguments,
|
||||
author_nickname,
|
||||
arena_unique_id,
|
||||
arena_type_id,
|
||||
}
|
||||
}
|
||||
|
||||
8 => Self::EntityMethod(EntityMethod::new(payload)?),
|
||||
8 => Self::EntityMethod(EntityMethod::new(raw_payload)?),
|
||||
|
||||
_ => Self::Unknown { raw: payload, type_ },
|
||||
_ => Self::Unknown { packet_type },
|
||||
};
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use prost::Enumeration;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
#[derive(Debug, Serialize_repr, Deserialize_repr, Enumeration, Ord, PartialOrd, Eq, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
#[repr(u16)]
|
||||
pub enum MapId {
|
||||
DesertSands = 2,
|
||||
Middleburg = 3,
|
||||
Copperfield = 4,
|
||||
Alpenstadt = 5,
|
||||
Mines = 6,
|
||||
DeadRail = 7,
|
||||
FortDespair = 8,
|
||||
Himmelsdorf = 9,
|
||||
BlackGoldville = 10,
|
||||
OasisPalms = 11,
|
||||
GhostFactory = 12,
|
||||
Molendijk = 14,
|
||||
PortBay = 15,
|
||||
WinterMalinovka = 19,
|
||||
Castilla = 20,
|
||||
Canal = 21,
|
||||
Vineyards = 23,
|
||||
YamatoHarbor = 25,
|
||||
Canyon = 27,
|
||||
MayanRuins = 30,
|
||||
DynastyPearl = 31,
|
||||
NavalFrontier = 35,
|
||||
FallsCreek = 38,
|
||||
NewBay = 40,
|
||||
Normandy = 42,
|
||||
Wasteland = 71,
|
||||
|
||||
/// Default value for Protocol Buffers and fallback value for Serde.
|
||||
#[serde(deserialize_with = "serde_with::rust::deserialize_ignore_any")]
|
||||
Undefined = 0,
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use std::io::Read;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::models::map_id::MapId;
|
||||
use crate::result::Result;
|
||||
|
||||
/// The model for `meta.json`.
|
||||
@@ -21,6 +22,9 @@ pub struct Meta {
|
||||
|
||||
#[serde(rename = "vehicleCompDescriptor")]
|
||||
pub tank_id: u16,
|
||||
|
||||
#[serde(rename = "mapId")]
|
||||
pub map_id: MapId,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
use prost::Enumeration;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Enumeration, Serialize, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum RoomType {
|
||||
Any = 0,
|
||||
Regular = 1,
|
||||
TrainingRoom = 2,
|
||||
Tournament = 4,
|
||||
QuickTournament = 5,
|
||||
Rating = 7,
|
||||
MadGames = 8,
|
||||
RealisticBattles = 22,
|
||||
Uprising = 23,
|
||||
GravityMode = 24,
|
||||
Skirmish = 25,
|
||||
BurningGames = 26,
|
||||
}
|
||||
+14
-4
@@ -9,31 +9,41 @@ use crate::models::data::Data;
|
||||
use crate::models::meta::Meta;
|
||||
use crate::result::Result;
|
||||
|
||||
/// Opened (but not yet parsed) replay.
|
||||
/// World of Tanks Blitz replay.
|
||||
///
|
||||
/// # Replay structure
|
||||
///
|
||||
/// `*.wotbreplay` is a ZIP-archive containing:
|
||||
///
|
||||
/// - `battle_results.dat`
|
||||
/// - `meta.json`
|
||||
/// - `data.wotreplay`
|
||||
pub struct Replay<R>(pub(crate) ZipArchive<R>);
|
||||
|
||||
impl<R: Read + Seek> Replay<R> {
|
||||
/// Opens a replay from the reader which contains a `*.wotbreplay`.
|
||||
/// Open a replay from the reader which contains a `*.wotbreplay`.
|
||||
pub fn open(reader: R) -> Result<Self> {
|
||||
Ok(Self(ZipArchive::new(reader)?))
|
||||
}
|
||||
|
||||
/// Reads and parses the battle results from the replay.
|
||||
/// Read and parse the full battle results structure from `battle_results.dat`.
|
||||
pub fn read_battle_results(&mut self) -> Result<BattleResults> {
|
||||
self.read_battle_results_dat()?.try_into()
|
||||
}
|
||||
|
||||
/// Reads and parses the included `battle_results.dat`.
|
||||
/// Read and parse the root 2-tuple from `battle_results.dat`.
|
||||
pub fn read_battle_results_dat(&mut self) -> Result<BattleResultsDat> {
|
||||
let pickled_battle_results = self.0.by_name("battle_results.dat")?;
|
||||
BattleResultsDat::from_reader(pickled_battle_results)
|
||||
}
|
||||
|
||||
/// Read and parse the included `meta.json`.
|
||||
#[cfg(feature = "meta")]
|
||||
pub fn read_meta(&mut self) -> Result<Meta> {
|
||||
Meta::from_reader(self.0.by_name("meta.json")?)
|
||||
}
|
||||
|
||||
/// Read and parse the included `data.wotreplay`.
|
||||
pub fn read_data(&mut self) -> Result<Data> {
|
||||
Data::from_reader(self.0.by_name("data.wotreplay")?)
|
||||
}
|
||||
|
||||
+22
-8
@@ -2,20 +2,23 @@ use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use wotbreplay_parser::models::battle_results::BattleResults;
|
||||
use wotbreplay_parser::models::room_type::RoomType;
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
#[test]
|
||||
fn player_results_ok() -> Result<()> {
|
||||
let battle_results_dat = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
||||
.read_battle_results_dat()?;
|
||||
let battle_results_dat =
|
||||
Replay::open(File::open("replays/20221203_player_results.wotbreplay")?)?
|
||||
.read_battle_results_dat()?;
|
||||
assert_eq!(battle_results_dat.arena_unique_id, 1661909200500084);
|
||||
let battle_results: BattleResults = battle_results_dat.try_into()?;
|
||||
|
||||
assert_eq!(battle_results.winner_team_number, Some(2));
|
||||
assert_eq!(battle_results.room_type(), RoomType::Regular);
|
||||
|
||||
assert_eq!(battle_results.author.team_number, 2);
|
||||
assert_eq!(battle_results.author.credits_earned, 59518);
|
||||
assert_eq!(battle_results.author.combat_xp, 5255);
|
||||
assert_eq!(battle_results.author.total_credits, 59518);
|
||||
assert_eq!(battle_results.author.total_xp, 5255);
|
||||
assert_eq!(battle_results.author.damage_dealt, 1438);
|
||||
assert_eq!(battle_results.author.account_id, 594778041);
|
||||
assert_eq!(battle_results.free_xp, 260);
|
||||
@@ -80,7 +83,7 @@ fn player_results_ok() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
fn player_info_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
||||
let battle_results = Replay::open(File::open("replays/20221203_player_results.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
|
||||
assert_eq!(battle_results.players[1].info.clan_tag.as_deref(), Some("AN0NY"));
|
||||
@@ -103,7 +106,7 @@ fn player_info_ok() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
fn victory_points_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/victory_points.wotbreplay")?)?
|
||||
let battle_results = Replay::open(File::open("replays/20221203_victory_points.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
|
||||
assert_eq!(battle_results.player_results[1].info.victory_points_seized, 40);
|
||||
@@ -120,8 +123,19 @@ fn victory_points_ok() -> Result<()> {
|
||||
|
||||
#[test]
|
||||
fn draw_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/20230503_0027__helaas_pindakaas_A140_ASTRON_REX_105_2319058483532631656.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
let battle_results =
|
||||
Replay::open(File::open("replays/20230503_0027__helaas_pindakaas.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
assert_eq!(battle_results.winner_team_number, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn room_type_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open(
|
||||
"replays/20230512_2150__helaas_pindakaas_R159_SU_130PM_15146680594634860.wotbreplay",
|
||||
)?)?
|
||||
.read_battle_results()?;
|
||||
assert_eq!(battle_results.room_type(), RoomType::MadGames);
|
||||
Ok(())
|
||||
}
|
||||
+30
-23
@@ -7,9 +7,8 @@ use wotbreplay_parser::replay::Replay;
|
||||
|
||||
#[test]
|
||||
fn parse_ok() -> Result<()> {
|
||||
let mut replay = Replay::open(File::open(
|
||||
"tests/replays/20230429_0126__helaas_pindakaas_A140_ASTRON_REX_105_16114615898101270.wotbreplay",
|
||||
)?)?;
|
||||
let mut replay =
|
||||
Replay::open(File::open("replays/20230429_0126__helaas_pindakaas.wotbreplay")?)?;
|
||||
let data = replay.read_data()?;
|
||||
|
||||
assert_eq!(data.client_version, "9.8.5_apple");
|
||||
@@ -19,13 +18,12 @@ fn parse_ok() -> Result<()> {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type0_ok() -> Result<()> {
|
||||
let mut replay = Replay::open(File::open(
|
||||
"tests/replays/20230429_0126__helaas_pindakaas_A140_ASTRON_REX_105_16114615898101270.wotbreplay",
|
||||
)?)?;
|
||||
fn base_player_create_ok() -> Result<()> {
|
||||
let mut replay =
|
||||
Replay::open(File::open("replays/20230429_0126__helaas_pindakaas.wotbreplay")?)?;
|
||||
let data = replay.read_data()?;
|
||||
|
||||
let (author_nickname, arena_unique_id, arena_type_id, type_0) = data
|
||||
let (author_nickname, arena_unique_id, arena_type_id, arguments) = data
|
||||
.packets
|
||||
.iter()
|
||||
.filter_map(|packet| {
|
||||
@@ -46,36 +44,38 @@ fn type0_ok() -> Result<()> {
|
||||
assert_eq!(author_nickname, "helaas_pindakaas");
|
||||
assert_eq!(*arena_unique_id, 16114615898101270);
|
||||
assert_eq!(*arena_type_id, 43);
|
||||
assert_eq!(type_0.battle_level, 9);
|
||||
assert_eq!(type_0.battle_category_id, 13);
|
||||
assert!(type_0.is_mouse_enabled);
|
||||
assert_eq!(type_0.matchmaker_type, 2);
|
||||
assert_eq!(type_0.camouflage_slot, 1);
|
||||
assert_eq!(type_0.average_mmr, [Some(59.29457753400008), Some(99.34182121604681)]);
|
||||
assert_eq!(arguments.battle_level, 9);
|
||||
assert_eq!(arguments.battle_category_id, 13);
|
||||
assert!(arguments.is_mouse_enabled);
|
||||
assert_eq!(arguments.matchmaker_type, 2);
|
||||
assert_eq!(arguments.camouflage_slot, 1);
|
||||
assert_eq!(
|
||||
type_0.account_database_ids,
|
||||
[
|
||||
arguments.average_mm_ratings,
|
||||
Some(vec![Some(59.29457753400008), Some(99.34182121604681)])
|
||||
);
|
||||
assert_eq!(
|
||||
arguments.account_database_ids,
|
||||
Some(vec![
|
||||
517033644, 537867068, 597583401, 601893865, 576810156, 531053956, 599580824, 558025185,
|
||||
526927603, 594778041, 505587490, 597422303, 531923111, 587617355
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtype_2f_players_ok() -> Result<()> {
|
||||
let mut replay = Replay::open(File::open(
|
||||
"tests/replays/20230429_0126__helaas_pindakaas_A140_ASTRON_REX_105_16114615898101270.wotbreplay",
|
||||
)?)?;
|
||||
fn update_arena_ok() -> Result<()> {
|
||||
let mut replay =
|
||||
Replay::open(File::open("replays/20230429_0126__helaas_pindakaas.wotbreplay")?)?;
|
||||
let data = replay.read_data()?;
|
||||
|
||||
let players = data
|
||||
.packets
|
||||
.iter()
|
||||
.filter_map(|packet| match &packet.payload {
|
||||
Payload::EntityMethod(EntityMethod::UpdateArena { arguments: sub_type_2f, .. }) => {
|
||||
sub_type_2f.players.as_ref()
|
||||
Payload::EntityMethod(EntityMethod::UpdateArena { arguments, .. }) => {
|
||||
arguments.players.as_ref()
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
@@ -91,3 +91,10 @@ fn subtype_2f_players_ok() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn training_room_ok() -> Result<()> {
|
||||
let mut replay = Replay::open(File::open("replays/20230503_training_room.wotbreplay")?)?;
|
||||
replay.read_data()?;
|
||||
Ok(())
|
||||
}
|
||||
+4
-1
@@ -3,16 +3,19 @@
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use wotbreplay_parser::models::map_id::MapId;
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
#[test]
|
||||
fn meta_ok() -> Result<()> {
|
||||
let meta = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?.read_meta()?;
|
||||
let meta =
|
||||
Replay::open(File::open("replays/20221203_player_results.wotbreplay")?)?.read_meta()?;
|
||||
|
||||
assert_eq!(meta.player_name, "zeekrab");
|
||||
assert_eq!(meta.arena_unique_id, "1661909200500084");
|
||||
assert_eq!(meta.battle_duration_secs, 164.08485);
|
||||
assert_eq!(meta.tank_id, 26657);
|
||||
assert_eq!(meta.map_id, MapId::DynastyPearl);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user