mirror of
https://github.com/eigenein/wotbreplay-parser.git
synced 2026-10-21 10:27:01 +02:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72165ae52c | ||
|
|
e48fe1dd0d | ||
|
|
9d601082d6 | ||
|
|
632f1781e7 | ||
|
|
fa6ac70d21 | ||
|
|
1fb4bea267 | ||
|
|
705280fc19 | ||
|
|
3f557e33bf | ||
|
|
fb5f52038d | ||
|
|
615537ce45 | ||
|
|
8e96f4847b |
No files matched your search
@@ -20,37 +20,43 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: 📥 Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up the toolchain
|
||||
- name: ✨ Set up the toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Rust Cache
|
||||
- name: ⚡️ Rust Cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
with:
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Test
|
||||
- name: 🧪 Test without optional features
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --doc
|
||||
args: --all-targets
|
||||
|
||||
- name: Clippy
|
||||
- name: 🧪 Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features
|
||||
|
||||
- name: 🪄 Clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-targets --all-features --tests -- -D warnings
|
||||
|
||||
- name: Publish the crates (dry run)
|
||||
- name: 📤 Publish the crates (dry run)
|
||||
run: |
|
||||
cargo publish --allow-dirty --dry-run
|
||||
env:
|
||||
|
||||
@@ -15,12 +15,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set the versions
|
||||
- name: 🪄 Set the versions
|
||||
run: |
|
||||
sed -i -e 's/version = "0\.0\.0"/version = "${{ github.ref_name }}"/g' Cargo.toml
|
||||
- name: Rustup
|
||||
@@ -29,13 +29,13 @@ jobs:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
|
||||
- name: Build the changelog
|
||||
- name: 📝 Build the changelog
|
||||
id: changelog
|
||||
uses: Bullrich/generate-release-changelog@master
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
|
||||
- name: Update the release
|
||||
- name: 🪄 Update the release
|
||||
uses: tubone24/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
release_name: ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
|
||||
- name: Publish the crates
|
||||
- name: 📤 Publish the crates
|
||||
run: |
|
||||
cargo publish --allow-dirty
|
||||
env:
|
||||
|
||||
+5
-1
@@ -12,13 +12,17 @@ categories = ["parsing"]
|
||||
|
||||
[features]
|
||||
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 = "1.0.96"
|
||||
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"] }
|
||||
|
||||
|
||||
@@ -14,23 +14,24 @@ World of Tanks Blitz replay parser in Rust.
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use wotbreplay_parser::prelude::*;
|
||||
use wotbreplay_parser::models::battle_results::TeamNumber;
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/battle_results.wotbreplay")?)?
|
||||
let battle_results = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
||||
.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[0].account_id, 520886428);
|
||||
assert_eq!(battle_results.players[0].info.nickname, "77mmmr");
|
||||
assert_eq!(battle_results.players[0].info.team(), TeamNumber::Two);
|
||||
assert_eq!(battle_results.players[0].info.platoon_id, None);
|
||||
assert_eq!(battle_results.players[0].account_id, 595693744);
|
||||
assert_eq!(battle_results.players[0].info.nickname, "yuranhik_hustriy26");
|
||||
assert_eq!(battle_results.players[0].info.team(), TeamNumber::One);
|
||||
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.team(), TeamNumber::One);
|
||||
assert_eq!(battle_results.players[1].info.platoon_id, Some(547466834));
|
||||
assert_eq!(battle_results.players[1].info.team(), TeamNumber::Two);
|
||||
assert_eq!(battle_results.players[1].info.platoon_id, Some(273692628));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,3 +8,4 @@ reorder_impl_items = true
|
||||
unstable_features = true
|
||||
use_field_init_shorthand = true
|
||||
version = "Two"
|
||||
struct_lit_width = 35
|
||||
+20
-10
@@ -2,18 +2,28 @@ use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("failed to open the replay as a ZIP-archive")]
|
||||
OpenArchiveFailed(#[source] zip::result::ZipError),
|
||||
#[error("ZIP error")]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
|
||||
#[error("failed to open the entry in the replay archive")]
|
||||
OpenEntryFailed(#[source] zip::result::ZipError),
|
||||
#[error("Protocol Buffers error")]
|
||||
ProtobufError(#[from] prost::DecodeError),
|
||||
|
||||
#[error("failed to decode the Protocol Buffers encoded data")]
|
||||
DecodeFailed(#[source] prost::DecodeError),
|
||||
#[error("Pickle error")]
|
||||
PickleError(#[from] serde_pickle::Error),
|
||||
|
||||
#[error("failed to un-pickle")]
|
||||
UnpickleFailed(#[source] serde_pickle::Error),
|
||||
#[error("I/O error")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("failed to decode JSON")]
|
||||
JsonDecodeFailed(#[source] serde_json::Error),
|
||||
#[error("UTF-8 error")]
|
||||
StringDecodeError(#[from] std::string::FromUtf8Error),
|
||||
|
||||
#[cfg(feature = "meta")]
|
||||
#[error("JSON error")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
|
||||
#[error("invalid magic: {0:#X}, expected: {1:#X}")]
|
||||
InvalidMagic(u32, u32),
|
||||
|
||||
#[error("failed to read a packet length, got {0} bytes")]
|
||||
PacketLengthError(usize),
|
||||
}
|
||||
+2
-3
@@ -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;
|
||||
|
||||
+4
-7
@@ -1,7 +1,4 @@
|
||||
mod battle_results;
|
||||
mod battle_results_dat;
|
||||
mod meta;
|
||||
|
||||
pub use self::battle_results::*;
|
||||
pub use self::battle_results_dat::*;
|
||||
pub use self::meta::*;
|
||||
pub mod battle_results;
|
||||
pub mod battle_results_dat;
|
||||
pub mod data;
|
||||
pub mod meta;
|
||||
@@ -2,7 +2,6 @@ use prost::bytes::Buf;
|
||||
use prost::Message;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::result::Result;
|
||||
|
||||
impl BattleResults {
|
||||
@@ -10,7 +9,7 @@ impl BattleResults {
|
||||
///
|
||||
/// Note, that this does NOT parse `battle_results.dat` itself, but the un-pickled tuple element.
|
||||
pub fn from_buffer(buffer: impl Buf) -> Result<Self> {
|
||||
Self::decode(buffer).map_err(Error::DecodeFailed)
|
||||
Ok(Self::decode(buffer)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +23,7 @@ pub struct BattleResults {
|
||||
pub timestamp: i64,
|
||||
|
||||
#[prost(enumeration = "TeamNumber", tag = "3")]
|
||||
pub winning_team: i32,
|
||||
pub winner_team_number: i32,
|
||||
|
||||
/// Replay's author results.
|
||||
#[prost(message, required, tag = "8")]
|
||||
|
||||
@@ -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`.
|
||||
@@ -33,7 +33,7 @@ impl BattleResultsDat {
|
||||
/// It can be used either on the `battle_results.dat` for a replay archive,
|
||||
/// or directly on `**/DAVAProject/battle_results/*/*_full.dat`.
|
||||
pub fn from_reader(reader: impl Read) -> Result<Self> {
|
||||
serde_pickle::from_reader(reader, Default::default()).map_err(Error::UnpickleFailed)
|
||||
Ok(serde_pickle::from_reader(reader, Default::default())?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,6 @@ impl TryInto<BattleResults> for BattleResultsDat {
|
||||
|
||||
/// Decodes the battle results from the internal buffer.
|
||||
fn try_into(self) -> Result<BattleResults> {
|
||||
BattleResults::decode(self.buffer.as_ref()).map_err(Error::DecodeFailed)
|
||||
Ok(BattleResults::decode(self.buffer.as_ref())?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! `data.wotreplay` models.
|
||||
|
||||
pub mod entity_method;
|
||||
pub mod packet;
|
||||
pub mod payload;
|
||||
pub mod subtype_2f;
|
||||
pub mod type_0;
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::models::data::packet::Packet;
|
||||
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 packets: Vec<Packet>,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn from_reader(mut reader: impl Read) -> Result<Self> {
|
||||
assert_magic(reader.read_u32::<LittleEndian>()?, 0x12345678)?;
|
||||
|
||||
// No idea what it is:
|
||||
reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
// Some sort of client hash, e.g.: `6CF2A9EFA5C52D6F6CE43A6D4A699C05`:
|
||||
read_length_delimited(&mut reader)?;
|
||||
|
||||
let client_version = read_string(&mut reader)?;
|
||||
|
||||
// Some extra byte, no idea:
|
||||
reader.read_u8()?;
|
||||
|
||||
let mut packets = Vec::new();
|
||||
while let Some(packet) = Packet::from_reader(&mut reader)? {
|
||||
packets.push(packet);
|
||||
}
|
||||
|
||||
Ok(Self { client_version, packets })
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn assert_magic<T: Into<u32> + PartialEq>(actual: T, expected: T) -> Result {
|
||||
if actual == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::InvalidMagic(actual.into(), expected.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_length_delimited(reader: &mut impl Read) -> Result<Vec<u8>> {
|
||||
let length = reader.read_u8()? as usize;
|
||||
let mut buffer = Vec::new();
|
||||
buffer.resize(length, 0);
|
||||
reader.read_exact(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_string(reader: &mut impl Read) -> Result<String> {
|
||||
Ok(String::from_utf8(read_length_delimited(reader)?)?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_pickled<T: DeserializeOwned>(reader: &mut impl Read, length: usize) -> Result<T> {
|
||||
let mut buffer = Vec::new();
|
||||
buffer.resize(length, 0);
|
||||
reader.read_exact(&mut buffer)?;
|
||||
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,57 @@
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use prost::encoding::decode_varint;
|
||||
use prost::Message;
|
||||
use serde::Serialize;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::models::data::read_quirky_length;
|
||||
use crate::models::data::subtype_2f::Subtype2F;
|
||||
use crate::result::Result;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum EntityMethod {
|
||||
Subtype2F {
|
||||
field_number: u64,
|
||||
sub_type_2f: Subtype2F,
|
||||
|
||||
/// 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>,
|
||||
},
|
||||
}
|
||||
|
||||
impl EntityMethod {
|
||||
pub fn new(payload: Vec<u8>) -> Result<Self> {
|
||||
let mut reader = payload.as_slice();
|
||||
|
||||
reader.read_u32::<LittleEndian>()?;
|
||||
let sub_type = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let this = match sub_type {
|
||||
0x2F => {
|
||||
let _inner_length = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let field_number = decode_varint(&mut reader)?;
|
||||
let message_length = read_quirky_length(&mut reader)?;
|
||||
Self::Subtype2F {
|
||||
field_number,
|
||||
sub_type_2f: Subtype2F::decode(&reader[..message_length])?,
|
||||
payload,
|
||||
}
|
||||
}
|
||||
|
||||
_ => Self::Unknown { sub_type, payload },
|
||||
};
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
@@ -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<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 };
|
||||
Ok(Some(this))
|
||||
}
|
||||
|
||||
fn read_payload(reader: &mut impl Read, length: usize) -> Result<Vec<u8>> {
|
||||
let mut buffer = Vec::new();
|
||||
buffer.resize(length, 0);
|
||||
reader.read_exact(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use std::io::Read;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use serde::Serialize;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::models::data::entity_method::EntityMethod;
|
||||
use crate::models::data::type_0::Type0;
|
||||
use crate::models::data::{read_pickled, read_quirky_length, read_string};
|
||||
use crate::result::Result;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum Payload {
|
||||
/// Looks like some sort of replay summary.
|
||||
Type0 {
|
||||
author_nickname: String,
|
||||
arena_unique_id: u64,
|
||||
arena_type_id: u32,
|
||||
type_0: Box<Type0>,
|
||||
},
|
||||
|
||||
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>,
|
||||
},
|
||||
}
|
||||
|
||||
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])?;
|
||||
|
||||
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)?
|
||||
};
|
||||
Self::Type0 {
|
||||
type_0,
|
||||
author_nickname,
|
||||
arena_unique_id,
|
||||
arena_type_id,
|
||||
}
|
||||
}
|
||||
|
||||
8 => Self::EntityMethod(EntityMethod::new(payload)?),
|
||||
|
||||
_ => Self::Unknown { raw: payload, type_ },
|
||||
};
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use prost::Message;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct Subtype2F {
|
||||
#[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>,
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Looks like some sort of replay summary.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Type0 {
|
||||
#[serde(rename(deserialize = "playersBattleCategoriesIds"))]
|
||||
pub players_battle_categories_ids: 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(rename(deserialize = "avgMmr"))]
|
||||
pub average_mmr: Vec<Option<f64>>,
|
||||
|
||||
#[serde(rename(deserialize = "playerWaitTimes"))]
|
||||
pub player_wait_times: HashMap<u32, f64>,
|
||||
|
||||
#[serde(rename(deserialize = "accountDatabaseIds"))]
|
||||
pub account_database_ids: Vec<u32>,
|
||||
|
||||
#[serde(rename(deserialize = "turboBattlesStats"))]
|
||||
pub turbo_battles_statistics: 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,
|
||||
}
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
#![cfg(feature = "meta")]
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::result::Result;
|
||||
|
||||
/// The model for `meta.json`.
|
||||
@@ -23,6 +24,6 @@ pub struct Meta {
|
||||
|
||||
impl Meta {
|
||||
pub fn from_reader(reader: impl Read) -> Result<Self> {
|
||||
serde_json::from_reader(reader).map_err(Error::JsonDecodeFailed)
|
||||
Ok(serde_json::from_reader(reader)?)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub use crate::models::*;
|
||||
pub use crate::replay::Replay;
|
||||
+13
-14
@@ -2,8 +2,11 @@ use std::io::{Read, Seek};
|
||||
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::models::{BattleResults, BattleResultsDat, Meta};
|
||||
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::Meta;
|
||||
use crate::result::Result;
|
||||
|
||||
/// Opened (but not yet parsed) replay.
|
||||
@@ -12,9 +15,7 @@ pub struct Replay<R>(pub(crate) ZipArchive<R>);
|
||||
impl<R: Read + Seek> Replay<R> {
|
||||
/// Opens a replay from the reader which contains a `*.wotbreplay`.
|
||||
pub fn open(reader: R) -> Result<Self> {
|
||||
ZipArchive::new(reader)
|
||||
.map_err(Error::OpenArchiveFailed)
|
||||
.map(Self)
|
||||
Ok(Self(ZipArchive::new(reader)?))
|
||||
}
|
||||
|
||||
/// Reads and parses the battle results from the replay.
|
||||
@@ -24,18 +25,16 @@ impl<R: Read + Seek> Replay<R> {
|
||||
|
||||
/// Reads and parses the included `battle_results.dat`.
|
||||
pub fn read_battle_results_dat(&mut self) -> Result<BattleResultsDat> {
|
||||
let pickled_battle_results = self
|
||||
.0
|
||||
.by_name("battle_results.dat")
|
||||
.map_err(Error::OpenEntryFailed)?;
|
||||
let pickled_battle_results = self.0.by_name("battle_results.dat")?;
|
||||
BattleResultsDat::from_reader(pickled_battle_results)
|
||||
}
|
||||
|
||||
#[cfg(feature = "meta")]
|
||||
pub fn read_meta(&mut self) -> Result<Meta> {
|
||||
Meta::from_reader(
|
||||
self.0
|
||||
.by_name("meta.json")
|
||||
.map_err(Error::OpenEntryFailed)?,
|
||||
)
|
||||
Meta::from_reader(self.0.by_name("meta.json")?)
|
||||
}
|
||||
|
||||
pub fn read_data(&mut self) -> Result<Data> {
|
||||
Data::from_reader(self.0.by_name("data.wotreplay")?)
|
||||
}
|
||||
}
|
||||
+11
-25
@@ -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<()> {
|
||||
@@ -10,6 +11,15 @@ fn player_results_ok() -> Result<()> {
|
||||
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, 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;
|
||||
assert_eq!(snak_the_ripper_info.base_xp, 929);
|
||||
assert_eq!(snak_the_ripper_info.damage_dealt, 2584);
|
||||
@@ -107,27 +117,3 @@ fn victory_points_ok() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn winning_team_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/winning_team.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
|
||||
assert_eq!(battle_results.winning_team(), TeamNumber::One);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn author_results_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/author_results.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
|
||||
assert_eq!(battle_results.author.credits_earned, 53623);
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use wotbreplay_parser::models::data::entity_method::EntityMethod;
|
||||
use wotbreplay_parser::models::data::payload::Payload;
|
||||
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 data = replay.read_data()?;
|
||||
|
||||
assert_eq!(data.client_version, "9.8.5_apple");
|
||||
assert_eq!(data.packets.len(), 60139);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type0_ok() -> Result<()> {
|
||||
let mut replay = Replay::open(File::open(
|
||||
"tests/replays/20230429_0126__helaas_pindakaas_A140_ASTRON_REX_105_16114615898101270.wotbreplay",
|
||||
)?)?;
|
||||
let data = replay.read_data()?;
|
||||
|
||||
let (author_nickname, arena_unique_id, arena_type_id, type_0) = data
|
||||
.packets
|
||||
.iter()
|
||||
.filter_map(|packet| {
|
||||
if let Payload::Type0 {
|
||||
author_nickname,
|
||||
arena_unique_id,
|
||||
arena_type_id,
|
||||
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!(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!(
|
||||
type_0.account_database_ids,
|
||||
[
|
||||
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",
|
||||
)?)?;
|
||||
let data = replay.read_data()?;
|
||||
|
||||
let players = data
|
||||
.packets
|
||||
.iter()
|
||||
.filter_map(|packet| match &packet.payload {
|
||||
Payload::EntityMethod(EntityMethod::Subtype2F { sub_type_2f, .. }) => {
|
||||
sub_type_2f.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(())
|
||||
}
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
#![cfg(feature = "meta")]
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use wotbreplay_parser::prelude::*;
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
#[test]
|
||||
fn meta_ok() -> Result<()> {
|
||||
|
||||
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Reference in new issue
Block a user