mirror of
https://github.com/eigenein/wotbreplay-parser.git
synced 2026-10-23 19:29:48 +02:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a177447ca1 | ||
|
|
2d2c85c18a | ||
|
|
0f437c39de | ||
|
|
12aa208217 | ||
|
|
181da1ae96 | ||
|
|
0ac983832b | ||
|
|
1830536224 | ||
|
|
897f98c17c | ||
|
|
59353a9aca | ||
|
|
c9a2adff4c | ||
|
|
72165ae52c | ||
|
|
e48fe1dd0d | ||
|
|
9d601082d6 | ||
|
|
632f1781e7 | ||
|
|
fa6ac70d21 | ||
|
|
1fb4bea267 | ||
|
|
705280fc19 | ||
|
|
3f557e33bf |
No files matched your search
@@ -1,5 +1 @@
|
|||||||
github: [eigenein]
|
github: [eigenein]
|
||||||
custom:
|
|
||||||
- 'https://www.paypal.me/eigenein'
|
|
||||||
- 'https://bunq.me/eigenein'
|
|
||||||
- 'https://money.yandex.ru/to/410011693851843'
|
|
||||||
@@ -43,17 +43,11 @@ jobs:
|
|||||||
command: test
|
command: test
|
||||||
args: --all-targets
|
args: --all-targets
|
||||||
|
|
||||||
- name: 🧪 Test all features
|
- name: 🧪 Test
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --all-targets --all-features
|
args: --all-features
|
||||||
|
|
||||||
- name: 🧪 Test docs
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --doc --all-features
|
|
||||||
|
|
||||||
- name: 🪄 Clippy
|
- name: 🪄 Clippy
|
||||||
uses: actions-rs/clippy-check@v1
|
uses: actions-rs/clippy-check@v1
|
||||||
|
|||||||
Generated
+1
@@ -4,6 +4,7 @@
|
|||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/benches" isTestSource="true" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
|||||||
+7
-1
@@ -12,7 +12,8 @@ categories = ["parsing"]
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
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]
|
[dependencies]
|
||||||
bytes = "1.4.0"
|
bytes = "1.4.0"
|
||||||
@@ -28,3 +29,8 @@ zip = { version = "0.6.3", default-features = false, features = ["deflate"] }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = "1.0.66"
|
anyhow = "1.0.66"
|
||||||
|
criterion = "0.4.0"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "data"
|
||||||
|
harness = false
|
||||||
@@ -14,47 +14,24 @@ World of Tanks Blitz replay parser in Rust.
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use wotbreplay_parser::prelude::*;
|
use wotbreplay_parser::models::battle_results::TeamNumber;
|
||||||
|
use wotbreplay_parser::replay::Replay;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let battle_results = Replay::open(File::open("tests/replays/battle_results.wotbreplay")?)?
|
let battle_results = Replay::open(File::open("replays/20221203_player_results.wotbreplay")?)?.read_battle_results()?;
|
||||||
.read_battle_results()?;
|
|
||||||
|
|
||||||
assert_eq!(battle_results.timestamp, 1670018359);
|
assert_eq!(battle_results.timestamp, 1670083956);
|
||||||
assert_eq!(battle_results.players.len(), 14);
|
assert_eq!(battle_results.players.len(), 14);
|
||||||
|
|
||||||
assert_eq!(battle_results.players[0].account_id, 520886428);
|
assert_eq!(battle_results.players[0].account_id, 595693744);
|
||||||
assert_eq!(battle_results.players[0].info.nickname, "77mmmr");
|
assert_eq!(battle_results.players[0].info.nickname, "yuranhik_hustriy26");
|
||||||
assert_eq!(battle_results.players[0].info.team(), TeamNumber::Two);
|
assert_eq!(battle_results.players[0].info.team(), TeamNumber::One);
|
||||||
assert_eq!(battle_results.players[0].info.platoon_id, None);
|
assert_eq!(battle_results.players[0].info.platoon_id, Some(545104609));
|
||||||
|
|
||||||
assert_eq!(battle_results.players[1].info.nickname, "SNAK_THE_RIPPER");
|
assert_eq!(battle_results.players[1].info.nickname, "SNAK_THE_RIPPER");
|
||||||
assert_eq!(battle_results.players[1].info.team(), TeamNumber::One);
|
assert_eq!(battle_results.players[1].info.team(), TeamNumber::Two);
|
||||||
assert_eq!(battle_results.players[1].info.platoon_id, Some(547466834));
|
assert_eq!(battle_results.players[1].info.platoon_id, Some(273692628));
|
||||||
|
|
||||||
Ok(())
|
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.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -8,3 +8,4 @@ reorder_impl_items = true
|
|||||||
unstable_features = true
|
unstable_features = true
|
||||||
use_field_init_shorthand = true
|
use_field_init_shorthand = true
|
||||||
version = "Two"
|
version = "Two"
|
||||||
|
struct_lit_width = 35
|
||||||
+3
-3
@@ -6,10 +6,10 @@ pub enum Error {
|
|||||||
ZipError(#[from] zip::result::ZipError),
|
ZipError(#[from] zip::result::ZipError),
|
||||||
|
|
||||||
#[error("Protocol Buffers error")]
|
#[error("Protocol Buffers error")]
|
||||||
DecodeFailed(#[from] prost::DecodeError),
|
ProtobufError(#[from] prost::DecodeError),
|
||||||
|
|
||||||
#[error("Pickle error")]
|
#[error("Pickle error")]
|
||||||
UnpickleFailed(#[from] serde_pickle::Error),
|
PickleError(#[from] serde_pickle::Error),
|
||||||
|
|
||||||
#[error("I/O error")]
|
#[error("I/O error")]
|
||||||
IoError(#[from] std::io::Error),
|
IoError(#[from] std::io::Error),
|
||||||
@@ -19,7 +19,7 @@ pub enum Error {
|
|||||||
|
|
||||||
#[cfg(feature = "meta")]
|
#[cfg(feature = "meta")]
|
||||||
#[error("JSON error")]
|
#[error("JSON error")]
|
||||||
JsonDecodeFailed(#[from] serde_json::Error),
|
JsonError(#[from] serde_json::Error),
|
||||||
|
|
||||||
#[error("invalid magic: {0:#X}, expected: {1:#X}")]
|
#[error("invalid magic: {0:#X}, expected: {1:#X}")]
|
||||||
InvalidMagic(u32, u32),
|
InvalidMagic(u32, u32),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use prost::bytes::Buf;
|
use prost::bytes::Buf;
|
||||||
use prost::Message;
|
use prost::{Enumeration, Message};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::result::Result;
|
use crate::result::Result;
|
||||||
@@ -7,7 +7,10 @@ use crate::result::Result;
|
|||||||
impl BattleResults {
|
impl BattleResults {
|
||||||
/// Parse battle results from the buffer.
|
/// Parse battle results from the buffer.
|
||||||
///
|
///
|
||||||
/// Note, that this does NOT parse `battle_results.dat` itself, but the un-pickled tuple element.
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This does **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> {
|
pub fn from_buffer(buffer: impl Buf) -> Result<Self> {
|
||||||
Ok(Self::decode(buffer)?)
|
Ok(Self::decode(buffer)?)
|
||||||
}
|
}
|
||||||
@@ -22,8 +25,8 @@ pub struct BattleResults {
|
|||||||
#[prost(int64, tag = "2")]
|
#[prost(int64, tag = "2")]
|
||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
|
|
||||||
#[prost(enumeration = "TeamNumber", tag = "3")]
|
#[prost(enumeration = "TeamNumber", optional, tag = "3")]
|
||||||
pub winner_team_number: i32,
|
pub winner_team_number: Option<i32>,
|
||||||
|
|
||||||
/// Replay's author results.
|
/// Replay's author results.
|
||||||
#[prost(message, required, tag = "8")]
|
#[prost(message, required, tag = "8")]
|
||||||
@@ -78,9 +81,7 @@ pub struct PlayerInfo {
|
|||||||
pub avatar: Avatar,
|
pub avatar: Avatar,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Enumeration, Serialize)]
|
||||||
Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, prost::Enumeration, Serialize,
|
|
||||||
)]
|
|
||||||
#[repr(i32)]
|
#[repr(i32)]
|
||||||
pub enum TeamNumber {
|
pub enum TeamNumber {
|
||||||
One = 1,
|
One = 1,
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ use crate::error::Error;
|
|||||||
use crate::models::battle_results::BattleResults;
|
use crate::models::battle_results::BattleResults;
|
||||||
use crate::result::Result;
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(from = "(u64, ByteBuf)")]
|
#[serde(from = "(u64, ByteBuf)")]
|
||||||
pub struct BattleResultsDat {
|
pub struct BattleResultsDat {
|
||||||
@@ -20,15 +27,12 @@ pub struct BattleResultsDat {
|
|||||||
|
|
||||||
impl From<(u64, ByteBuf)> for BattleResultsDat {
|
impl From<(u64, ByteBuf)> for BattleResultsDat {
|
||||||
fn from((arena_unique_id, buffer): (u64, ByteBuf)) -> Self {
|
fn from((arena_unique_id, buffer): (u64, ByteBuf)) -> Self {
|
||||||
Self {
|
Self { arena_unique_id, buffer }
|
||||||
arena_unique_id,
|
|
||||||
buffer,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BattleResultsDat {
|
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,
|
/// It can be used either on the `battle_results.dat` for a replay archive,
|
||||||
/// or directly on `**/DAVAProject/battle_results/*/*_full.dat`.
|
/// or directly on `**/DAVAProject/battle_results/*/*_full.dat`.
|
||||||
@@ -40,7 +44,7 @@ impl BattleResultsDat {
|
|||||||
impl TryInto<BattleResults> for BattleResultsDat {
|
impl TryInto<BattleResults> for BattleResultsDat {
|
||||||
type Error = Error;
|
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> {
|
fn try_into(self) -> Result<BattleResults> {
|
||||||
Ok(BattleResults::decode(self.buffer.as_ref())?)
|
Ok(BattleResults::decode(self.buffer.as_ref())?)
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-34
@@ -1,10 +1,9 @@
|
|||||||
//! `data.wotreplay` models.
|
//! Models for `data.wotreplay` contents.
|
||||||
|
|
||||||
|
pub mod base_player_create;
|
||||||
pub mod entity_method;
|
pub mod entity_method;
|
||||||
pub mod packet;
|
pub mod packet;
|
||||||
pub mod payload;
|
pub mod payload;
|
||||||
pub mod replay_header;
|
|
||||||
pub mod summary;
|
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
@@ -12,11 +11,7 @@ use byteorder::{LittleEndian, ReadBytesExt};
|
|||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
pub use self::entity_method::*;
|
|
||||||
pub use self::summary::*;
|
|
||||||
use crate::models::data::packet::Packet;
|
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::result::Result;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
@@ -26,9 +21,7 @@ pub struct Data {
|
|||||||
/// For example: `9.8.5_apple`.
|
/// For example: `9.8.5_apple`.
|
||||||
pub client_version: String,
|
pub client_version: String,
|
||||||
|
|
||||||
pub replay_header: Option<ReplayHeader>,
|
pub packets: Vec<Packet>,
|
||||||
|
|
||||||
pub other_packets: Vec<Packet>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Data {
|
impl Data {
|
||||||
@@ -39,36 +32,19 @@ impl Data {
|
|||||||
reader.read_u64::<LittleEndian>()?;
|
reader.read_u64::<LittleEndian>()?;
|
||||||
|
|
||||||
// Some sort of client hash, e.g.: `6CF2A9EFA5C52D6F6CE43A6D4A699C05`:
|
// Some sort of client hash, e.g.: `6CF2A9EFA5C52D6F6CE43A6D4A699C05`:
|
||||||
read_bytes(&mut reader)?;
|
read_length_delimited(&mut reader)?;
|
||||||
|
|
||||||
let client_version = read_string(&mut reader)?;
|
let client_version = read_string(&mut reader)?;
|
||||||
|
|
||||||
// Some extra byte, no idea:
|
// Some extra byte, no idea:
|
||||||
reader.read_u8()?;
|
reader.read_u8()?;
|
||||||
|
|
||||||
let mut this = Self::new(client_version);
|
let mut packets = Vec::new();
|
||||||
|
while let Some(packet) = Packet::from_reader(&mut reader)? {
|
||||||
loop {
|
packets.push(packet);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(this)
|
Ok(Self { client_version, packets })
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn new(client_version: String) -> Self {
|
|
||||||
Self {
|
|
||||||
client_version,
|
|
||||||
replay_header: None,
|
|
||||||
other_packets: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +58,7 @@ fn assert_magic<T: Into<u32> + PartialEq>(actual: T, expected: T) -> Result {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn read_bytes(reader: &mut impl Read) -> Result<Vec<u8>> {
|
fn read_length_delimited(reader: &mut impl Read) -> Result<Vec<u8>> {
|
||||||
let length = reader.read_u8()? as usize;
|
let length = reader.read_u8()? as usize;
|
||||||
let mut buffer = Vec::new();
|
let mut buffer = Vec::new();
|
||||||
buffer.resize(length, 0);
|
buffer.resize(length, 0);
|
||||||
@@ -92,7 +68,7 @@ fn read_bytes(reader: &mut impl Read) -> Result<Vec<u8>> {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn read_string(reader: &mut impl Read) -> Result<String> {
|
fn read_string(reader: &mut impl Read) -> Result<String> {
|
||||||
Ok(String::from_utf8(read_bytes(reader)?)?)
|
Ok(String::from_utf8(read_length_delimited(reader)?)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -102,3 +78,19 @@ fn read_pickled<T: DeserializeOwned>(reader: &mut impl Read, length: usize) -> R
|
|||||||
reader.read_exact(&mut buffer)?;
|
reader.read_exact(&mut buffer)?;
|
||||||
Ok(serde_pickle::from_slice(&buffer, Default::default())?)
|
Ok(serde_pickle::from_slice(&buffer, Default::default())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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!
|
||||||
|
#[inline]
|
||||||
|
fn read_quirky_length(reader: &mut impl Read) -> Result<usize> {
|
||||||
|
match reader.read_u8()? {
|
||||||
|
0xFF => {
|
||||||
|
let length = reader.read_u16::<LittleEndian>()?;
|
||||||
|
assert_magic(reader.read_u8()?, 0x00)?;
|
||||||
|
Ok(length as usize)
|
||||||
|
}
|
||||||
|
length => Ok(length as usize),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct BasePlayerCreate {
|
||||||
|
#[serde(default, rename(deserialize = "playersBattleCategoriesIds"))]
|
||||||
|
pub players_battle_categories_ids: Option<HashMap<u32, (u8, u32)>>,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "battleLevel"))]
|
||||||
|
pub battle_level: u8,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "battleCategoryId"))]
|
||||||
|
pub battle_category_id: u8,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "mouseEnabled"))]
|
||||||
|
pub is_mouse_enabled: bool,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "mmType"))]
|
||||||
|
pub matchmaker_type: u8,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "camouflageSlot"))]
|
||||||
|
pub camouflage_slot: u8,
|
||||||
|
|
||||||
|
#[serde(default, rename(deserialize = "avgMmr"))]
|
||||||
|
pub average_mmr: Option<Vec<Option<f64>>>,
|
||||||
|
|
||||||
|
#[serde(default, rename(deserialize = "playerWaitTimes"))]
|
||||||
|
pub player_wait_times: Option<HashMap<u32, f64>>,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "accountDatabaseIds"))]
|
||||||
|
pub account_database_ids: Vec<u32>,
|
||||||
|
|
||||||
|
#[serde(default, rename(deserialize = "turboBattlesStats"))]
|
||||||
|
pub turbo_battles_statistics: Option<TurboBattlesStatistics>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TurboBattlesStatistics {
|
||||||
|
#[serde(rename(deserialize = "turboBattleChance"))]
|
||||||
|
pub battle_chance: f64,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "turboLeaveChance"))]
|
||||||
|
pub leave_chance: f64,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "possibleTurboBattlesNotForSkip"))]
|
||||||
|
pub possible_battles_not_for_skip: u32,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "possibleTurboBattlesForSkip"))]
|
||||||
|
pub possible_battles_for_skip: u32,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "turboBattlesForSkip"))]
|
||||||
|
pub battles_for_skip: u32,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "turboBattlesNotForSkip"))]
|
||||||
|
pub battles_not_for_skip: u32,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "abSkip"))]
|
||||||
|
pub ab_skip: i32,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "beta"))]
|
||||||
|
pub beta: f64,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "premiumVehDiff"))]
|
||||||
|
pub premium_vehicle_difference: i32,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "htDiff"))]
|
||||||
|
pub ht_difference: i32,
|
||||||
|
|
||||||
|
#[serde(rename(deserialize = "alpha"))]
|
||||||
|
pub alpha: f64,
|
||||||
|
}
|
||||||
@@ -1,27 +1,51 @@
|
|||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
//! Structures for «entity method» call packets.
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use prost::encoding::decode_varint;
|
||||||
|
use prost::Message;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_with::serde_as;
|
||||||
|
|
||||||
|
use self::update_arena::UpdateArena;
|
||||||
|
use crate::models::data::read_quirky_length;
|
||||||
use crate::result::Result;
|
use crate::result::Result;
|
||||||
|
|
||||||
|
pub mod update_arena;
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub enum EntityMethod {
|
pub enum EntityMethod {
|
||||||
/// TODO: team composition.
|
/// Subtype 47.
|
||||||
Subtype2F,
|
UpdateArena {
|
||||||
|
field_number: u64,
|
||||||
|
arguments: UpdateArena,
|
||||||
|
},
|
||||||
|
|
||||||
/// Default variant when subtype is not known.
|
/// Default variant when subtype is not known by the parser.
|
||||||
Unknown { sub_type: u32, payload: Vec<u8> },
|
Unknown { sub_type: u32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EntityMethod {
|
impl EntityMethod {
|
||||||
pub fn new(payload: Vec<u8>) -> Result<Self> {
|
/// Parse the entity method payload.
|
||||||
let mut reader = payload.as_slice();
|
pub fn new(payload: &[u8]) -> Result<Self> {
|
||||||
|
let mut payload = payload;
|
||||||
|
|
||||||
reader.read_u32::<LittleEndian>()?;
|
payload.read_u32::<LittleEndian>()?;
|
||||||
let sub_type = reader.read_u32::<LittleEndian>()?;
|
let sub_type = payload.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
let this = match sub_type {
|
let this = match sub_type {
|
||||||
0x2F => Self::Subtype2F,
|
47 => {
|
||||||
_ => Self::Unknown { sub_type, payload },
|
let _inner_length = payload.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let field_number = decode_varint(&mut payload)?;
|
||||||
|
let message_length = read_quirky_length(&mut payload)?;
|
||||||
|
Self::UpdateArena {
|
||||||
|
field_number,
|
||||||
|
arguments: UpdateArena::decode(&payload[..message_length])?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => Self::Unknown { sub_type },
|
||||||
};
|
};
|
||||||
Ok(this)
|
Ok(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use prost::Message;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Message, Serialize)]
|
||||||
|
pub struct UpdateArena {
|
||||||
|
#[prost(message, tag = "1")]
|
||||||
|
pub players: Option<Players>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message, Serialize)]
|
||||||
|
pub struct Players {
|
||||||
|
#[prost(message, repeated, tag = "1")]
|
||||||
|
pub players: Vec<Player>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message, Serialize)]
|
||||||
|
pub struct Player {
|
||||||
|
#[prost(string, required, tag = "3")]
|
||||||
|
pub nickname: String,
|
||||||
|
|
||||||
|
#[prost(uint32, required, tag = "4")]
|
||||||
|
pub team_number: u32,
|
||||||
|
|
||||||
|
#[prost(uint32, required, tag = "7")]
|
||||||
|
pub account_id: u32,
|
||||||
|
|
||||||
|
#[prost(string, optional, tag = "8")]
|
||||||
|
pub clan_tag: Option<String>,
|
||||||
|
|
||||||
|
#[prost(uint32, optional, tag = "11")]
|
||||||
|
pub platoon_number: Option<u32>,
|
||||||
|
}
|
||||||
@@ -2,14 +2,20 @@ use std::io::Read;
|
|||||||
|
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use serde_with::serde_as;
|
||||||
|
|
||||||
use crate::models::data::payload::Payload;
|
use crate::models::data::payload::Payload;
|
||||||
use crate::result::Result;
|
use crate::result::Result;
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct Packet {
|
pub struct Packet {
|
||||||
pub clock: f32,
|
pub clock: f32,
|
||||||
pub payload: Payload,
|
pub payload: Payload,
|
||||||
|
|
||||||
|
#[cfg(feature = "raw-payload")]
|
||||||
|
#[serde_as(as = "serde_with::hex::Hex")]
|
||||||
|
pub raw_payload: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Packet {
|
impl Packet {
|
||||||
@@ -18,8 +24,13 @@ impl Packet {
|
|||||||
let type_ = reader.read_u32::<LittleEndian>()?;
|
let type_ = reader.read_u32::<LittleEndian>()?;
|
||||||
let clock = reader.read_f32::<LittleEndian>()?;
|
let clock = reader.read_f32::<LittleEndian>()?;
|
||||||
let payload = Self::read_payload(reader, length as usize)?;
|
let payload = Self::read_payload(reader, length as usize)?;
|
||||||
let payload = Payload::new(type_, payload)?;
|
let this = Self {
|
||||||
let this = Self { clock, payload };
|
clock,
|
||||||
|
payload: Payload::new(type_, &payload)?,
|
||||||
|
|
||||||
|
#[cfg(feature = "raw-payload")]
|
||||||
|
raw_payload: payload,
|
||||||
|
};
|
||||||
Ok(Some(this))
|
Ok(Some(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-22
@@ -4,47 +4,57 @@ use byteorder::{LittleEndian, ReadBytesExt};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
|
|
||||||
use crate::models::data::replay_header::ReplayHeader;
|
use crate::models::data::base_player_create::BasePlayerCreate;
|
||||||
use crate::models::data::{assert_magic, read_pickled, read_string, EntityMethod};
|
use crate::models::data::entity_method::EntityMethod;
|
||||||
|
use crate::models::data::{read_pickled, read_quirky_length, read_string};
|
||||||
use crate::result::Result;
|
use crate::result::Result;
|
||||||
|
|
||||||
|
/// Packet payload.
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub enum Payload {
|
pub enum Payload {
|
||||||
ReplayHeader(ReplayHeader),
|
/// Type 0.
|
||||||
|
BasePlayerCreate {
|
||||||
|
author_nickname: String,
|
||||||
|
arena_unique_id: u64,
|
||||||
|
arena_type_id: u32,
|
||||||
|
arguments: Box<BasePlayerCreate>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Type 8.
|
||||||
EntityMethod(EntityMethod),
|
EntityMethod(EntityMethod),
|
||||||
|
|
||||||
/// Default payload when type is not known.
|
/// Default payload when type is not known by the parser.
|
||||||
Unknown(#[serde_as(as = "serde_with::hex::Hex")] Vec<u8>),
|
Unknown { packet_type: u32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Payload {
|
impl Payload {
|
||||||
pub fn new(type_: u32, payload: Vec<u8>) -> Result<Self> {
|
/// Parse the packet payload.
|
||||||
let this = match type_ {
|
pub fn new(packet_type: u32, payload: &[u8]) -> Result<Self> {
|
||||||
0 => {
|
let mut payload = payload;
|
||||||
let mut reader = payload.as_slice();
|
|
||||||
reader.read_exact(&mut [0; 10])?;
|
|
||||||
|
|
||||||
let author_nickname = read_string(&mut reader)?;
|
let this = match packet_type {
|
||||||
let arena_unique_id = reader.read_u64::<LittleEndian>()?;
|
0 => {
|
||||||
let arena_type_id = reader.read_u32::<LittleEndian>()?;
|
payload.read_exact(&mut [0; 10])?;
|
||||||
let summary = {
|
|
||||||
assert_magic(reader.read_u8()?, 0xFF)?;
|
let author_nickname = read_string(&mut payload)?;
|
||||||
let pickled_length = reader.read_u16::<LittleEndian>()?;
|
let arena_unique_id = payload.read_u64::<LittleEndian>()?;
|
||||||
assert_magic(reader.read_u8()?, 0x00)?;
|
let arena_type_id = payload.read_u32::<LittleEndian>()?;
|
||||||
read_pickled(&mut reader, pickled_length as usize)?
|
let arguments = {
|
||||||
|
let pickled_length = read_quirky_length(&mut payload)?;
|
||||||
|
read_pickled(&mut payload, pickled_length)?
|
||||||
};
|
};
|
||||||
Self::ReplayHeader(ReplayHeader {
|
Self::BasePlayerCreate {
|
||||||
summary,
|
arguments,
|
||||||
author_nickname,
|
author_nickname,
|
||||||
arena_unique_id,
|
arena_unique_id,
|
||||||
arena_type_id,
|
arena_type_id,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
8 => Self::EntityMethod(EntityMethod::new(payload)?),
|
8 => Self::EntityMethod(EntityMethod::new(payload)?),
|
||||||
|
|
||||||
_ => Self::Unknown(payload),
|
_ => Self::Unknown { packet_type },
|
||||||
};
|
};
|
||||||
Ok(this)
|
Ok(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Summary {
|
|
||||||
#[serde(rename = "playersBattleCategoriesIds")]
|
|
||||||
pub players_battle_categories_ids: HashMap<u32, (u8, u32)>,
|
|
||||||
|
|
||||||
#[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<f64>,
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//! Models for `meta.json` contents.
|
||||||
#![cfg(feature = "meta")]
|
#![cfg(feature = "meta")]
|
||||||
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|||||||
+13
-3
@@ -9,7 +9,15 @@ use crate::models::data::Data;
|
|||||||
use crate::models::meta::Meta;
|
use crate::models::meta::Meta;
|
||||||
use crate::result::Result;
|
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>);
|
pub struct Replay<R>(pub(crate) ZipArchive<R>);
|
||||||
|
|
||||||
impl<R: Read + Seek> Replay<R> {
|
impl<R: Read + Seek> Replay<R> {
|
||||||
@@ -18,22 +26,24 @@ impl<R: Read + Seek> Replay<R> {
|
|||||||
Ok(Self(ZipArchive::new(reader)?))
|
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> {
|
pub fn read_battle_results(&mut self) -> Result<BattleResults> {
|
||||||
self.read_battle_results_dat()?.try_into()
|
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> {
|
pub fn read_battle_results_dat(&mut self) -> Result<BattleResultsDat> {
|
||||||
let pickled_battle_results = self.0.by_name("battle_results.dat")?;
|
let pickled_battle_results = self.0.by_name("battle_results.dat")?;
|
||||||
BattleResultsDat::from_reader(pickled_battle_results)
|
BattleResultsDat::from_reader(pickled_battle_results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read and parse the included `meta.json`.
|
||||||
#[cfg(feature = "meta")]
|
#[cfg(feature = "meta")]
|
||||||
pub fn read_meta(&mut self) -> Result<Meta> {
|
pub fn read_meta(&mut self) -> Result<Meta> {
|
||||||
Meta::from_reader(self.0.by_name("meta.json")?)
|
Meta::from_reader(self.0.by_name("meta.json")?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read and parse the included `data.wotreplay`.
|
||||||
pub fn read_data(&mut self) -> Result<Data> {
|
pub fn read_data(&mut self) -> Result<Data> {
|
||||||
Data::from_reader(self.0.by_name("data.wotreplay")?)
|
Data::from_reader(self.0.by_name("data.wotreplay")?)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-15
@@ -6,13 +6,20 @@ use wotbreplay_parser::replay::Replay;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn player_results_ok() -> Result<()> {
|
fn player_results_ok() -> Result<()> {
|
||||||
let battle_results_dat = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
let battle_results_dat =
|
||||||
.read_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);
|
assert_eq!(battle_results_dat.arena_unique_id, 1661909200500084);
|
||||||
let battle_results: BattleResults = battle_results_dat.try_into()?;
|
let battle_results: BattleResults = battle_results_dat.try_into()?;
|
||||||
|
|
||||||
assert_eq!(battle_results.winner_team_number, 2);
|
assert_eq!(battle_results.winner_team_number, Some(2));
|
||||||
|
|
||||||
assert_eq!(battle_results.author.team_number, 2);
|
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.damage_dealt, 1438);
|
||||||
|
assert_eq!(battle_results.author.account_id, 594778041);
|
||||||
|
assert_eq!(battle_results.free_xp, 260);
|
||||||
|
|
||||||
let snak_the_ripper_info = &battle_results.player_results[3].info;
|
let snak_the_ripper_info = &battle_results.player_results[3].info;
|
||||||
assert_eq!(snak_the_ripper_info.base_xp, 929);
|
assert_eq!(snak_the_ripper_info.base_xp, 929);
|
||||||
@@ -74,7 +81,7 @@ fn player_results_ok() -> Result<()> {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn player_info_ok() -> Result<()> {
|
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()?;
|
.read_battle_results()?;
|
||||||
|
|
||||||
assert_eq!(battle_results.players[1].info.clan_tag.as_deref(), Some("AN0NY"));
|
assert_eq!(battle_results.players[1].info.clan_tag.as_deref(), Some("AN0NY"));
|
||||||
@@ -97,7 +104,7 @@ fn player_info_ok() -> Result<()> {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn victory_points_ok() -> Result<()> {
|
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()?;
|
.read_battle_results()?;
|
||||||
|
|
||||||
assert_eq!(battle_results.player_results[1].info.victory_points_seized, 40);
|
assert_eq!(battle_results.player_results[1].info.victory_points_seized, 40);
|
||||||
@@ -113,15 +120,10 @@ fn victory_points_ok() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn author_results_ok() -> Result<()> {
|
fn draw_ok() -> Result<()> {
|
||||||
let battle_results = Replay::open(File::open("tests/replays/author_results.wotbreplay")?)?
|
let battle_results =
|
||||||
.read_battle_results()?;
|
Replay::open(File::open("replays/20230503_0027__helaas_pindakaas.wotbreplay")?)?
|
||||||
|
.read_battle_results()?;
|
||||||
assert_eq!(battle_results.author.credits_earned, 53623);
|
assert_eq!(battle_results.winner_team_number, None);
|
||||||
assert_eq!(battle_results.author.combat_xp, 2370);
|
|
||||||
assert_eq!(battle_results.author.damage_dealt, 1738);
|
|
||||||
assert_eq!(battle_results.author.account_id, 594778041);
|
|
||||||
assert_eq!(battle_results.free_xp, 116);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
+89
-15
@@ -1,26 +1,100 @@
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use wotbreplay_parser::models::data::entity_method::EntityMethod;
|
||||||
|
use wotbreplay_parser::models::data::payload::Payload;
|
||||||
use wotbreplay_parser::replay::Replay;
|
use wotbreplay_parser::replay::Replay;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn data_ok() -> Result<()> {
|
fn parse_ok() -> Result<()> {
|
||||||
let mut replay = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?;
|
let mut replay =
|
||||||
|
Replay::open(File::open("replays/20230429_0126__helaas_pindakaas.wotbreplay")?)?;
|
||||||
let data = replay.read_data()?;
|
let data = replay.read_data()?;
|
||||||
|
|
||||||
assert_eq!(data.client_version, "9.4.0_apple");
|
assert_eq!(data.client_version, "9.8.5_apple");
|
||||||
assert_eq!(data.other_packets.len(), 44720);
|
assert_eq!(data.packets.len(), 60139);
|
||||||
|
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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, arguments) = data
|
||||||
|
.packets
|
||||||
|
.iter()
|
||||||
|
.filter_map(|packet| {
|
||||||
|
if let Payload::BasePlayerCreate {
|
||||||
|
author_nickname,
|
||||||
|
arena_unique_id,
|
||||||
|
arena_type_id,
|
||||||
|
arguments: type_0,
|
||||||
|
} = &packet.payload
|
||||||
|
{
|
||||||
|
Some((author_nickname, arena_unique_id, arena_type_id, type_0))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(author_nickname, "helaas_pindakaas");
|
||||||
|
assert_eq!(*arena_unique_id, 16114615898101270);
|
||||||
|
assert_eq!(*arena_type_id, 43);
|
||||||
|
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!(
|
||||||
|
arguments.average_mmr,
|
||||||
|
Some(vec![Some(59.29457753400008), Some(99.34182121604681)])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
arguments.account_database_ids,
|
||||||
|
[
|
||||||
|
517033644, 537867068, 597583401, 601893865, 576810156, 531053956, 599580824, 558025185,
|
||||||
|
526927603, 594778041, 505587490, 597422303, 531923111, 587617355
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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, .. }) => {
|
||||||
|
arguments.players.as_ref()
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(players.players.len(), 14);
|
||||||
|
assert_eq!(players.players[1].team_number, 1);
|
||||||
|
assert_eq!(players.players[1].nickname, "Igrok_WoT_blitz");
|
||||||
|
assert_eq!(players.players[1].account_id, 597583401);
|
||||||
|
assert_eq!(players.players[1].platoon_number, None);
|
||||||
|
assert_eq!(players.players[1].clan_tag, Some("F_ANG".to_string()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn training_room_ok() -> Result<()> {
|
||||||
|
let mut replay = Replay::open(File::open("replays/20230503_training_room.wotbreplay")?)?;
|
||||||
|
replay.read_data()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+2
-1
@@ -7,7 +7,8 @@ use wotbreplay_parser::replay::Replay;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn meta_ok() -> Result<()> {
|
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.player_name, "zeekrab");
|
||||||
assert_eq!(meta.arena_unique_id, "1661909200500084");
|
assert_eq!(meta.arena_unique_id, "1661909200500084");
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user