mirror of
https://github.com/eigenein/wotbreplay-parser.git
synced 2026-10-21 18:37:00 +02:00
Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72165ae52c | ||
|
|
e48fe1dd0d | ||
|
|
9d601082d6 | ||
|
|
632f1781e7 | ||
|
|
fa6ac70d21 | ||
|
|
1fb4bea267 | ||
|
|
705280fc19 | ||
|
|
3f557e33bf | ||
|
|
fb5f52038d | ||
|
|
615537ce45 | ||
|
|
8e96f4847b | ||
|
|
14148c7410 | ||
|
|
903cd00f32 | ||
|
|
e0ce6a22b1 | ||
|
|
fb9beb5ecc | ||
|
|
06de455795 | ||
|
|
ac256d4c9d | ||
|
|
e853224017 | ||
|
|
04420da334 | ||
|
|
cd933f6584 | ||
|
|
ba28620be7 | ||
|
|
3913046be7 | ||
|
|
de6deb3a06 | ||
|
|
e1d9c27bda | ||
|
|
d2fb61425a | ||
|
|
93cea446f5 | ||
|
|
8d45bb28ed | ||
|
|
1e0ce05c18 | ||
|
|
f63ba599ec | ||
|
|
f619ffd93e | ||
|
|
7d99456aaa | ||
|
|
be4a6c21b5 | ||
|
|
805a2af81b | ||
|
|
899456eaf7 | ||
|
|
96e63f81f4 | ||
|
|
5786253cd5 | ||
|
|
e1f38014b8 | ||
|
|
7376cfc97e | ||
|
|
bc3b39e764 | ||
|
|
4edcc50a87 | ||
|
|
bccb5b519e | ||
|
|
ac086b1eb2 | ||
|
|
68e00c5eca | ||
|
|
6fb70da06f | ||
|
|
679b0ddadf | ||
|
|
21206737c0 |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
github: [eigenein]
|
||||
custom:
|
||||
- 'https://www.paypal.me/eigenein'
|
||||
- 'https://bunq.me/eigenein'
|
||||
- 'https://money.yandex.ru/to/410011693851843'
|
||||
@@ -20,32 +20,44 @@ 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)
|
||||
run: |
|
||||
cargo publish --allow-dirty --dry-run
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
@@ -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,24 +29,22 @@ 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 }}
|
||||
with:
|
||||
release_name: ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
prerelease: false
|
||||
|
||||
- name: Publish the crates
|
||||
- name: 📤 Publish the crates
|
||||
run: |
|
||||
cargo login
|
||||
cargo publish --allow-dirty
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
+8
-2
@@ -7,16 +7,22 @@ authors = ["Pavel Perestoronin <[email protected]>"]
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/eigenein/wotbreplay-parser"
|
||||
license = "MIT"
|
||||
keywords = []
|
||||
categories = []
|
||||
keywords = ["world-of-tanks-blitz", "replay", "parser", "wargaming", "results"]
|
||||
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 = { 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"] }
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Pavel Perestoronin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -4,10 +4,39 @@ World of Tanks Blitz replay parser in Rust.
|
||||
|
||||
[](https://crates.io/crates/wotbreplay-parser)
|
||||
[](https://github.com/eigenein/wotbreplay-parser/commits/main)
|
||||
[](https://github.com/eigenein/wotbreplay-parser/actions)
|
||||
[](https://github.com/eigenein/wotbreplay-parser/actions)
|
||||

|
||||
[](https://docs.rs/wotbreplay-parser)
|
||||
|
||||
## Quickstart
|
||||
|
||||
```rust
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
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()?;
|
||||
|
||||
assert_eq!(battle_results.timestamp, 1670083956);
|
||||
assert_eq!(battle_results.players.len(), 14);
|
||||
|
||||
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::Two);
|
||||
assert_eq!(battle_results.players[1].info.platoon_id, Some(273692628));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Replay structure
|
||||
|
||||
`*.wotbreplay` is a ZIP-archive containing:
|
||||
|
||||
@@ -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 ZIP-archive")]
|
||||
OpenArchiveFailed(#[source] zip::result::ZipError),
|
||||
#[error("ZIP error")]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
|
||||
#[error("failed to open the battle results entry in the replay archive")]
|
||||
OpenBattleResultsFailed(#[source] zip::result::ZipError),
|
||||
#[error("Protocol Buffers error")]
|
||||
ProtobufError(#[from] prost::DecodeError),
|
||||
|
||||
#[error("failed to read the archive entry")]
|
||||
ReadEntryFailed(#[source] std::io::Error),
|
||||
#[error("Pickle error")]
|
||||
PickleError(#[from] serde_pickle::Error),
|
||||
|
||||
#[error("failed to decode")]
|
||||
DecodeFailed(#[source] prost::DecodeError),
|
||||
#[error("I/O error")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("failed to un-pickle")]
|
||||
UnpickleFailed(#[source] serde_pickle::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),
|
||||
}
|
||||
+28
-4
@@ -1,8 +1,32 @@
|
||||
//! World of Tanks Blitz replay parser.
|
||||
|
||||
#![warn(
|
||||
clippy::all,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::manual_let_else,
|
||||
clippy::map_unwrap_or,
|
||||
clippy::missing_const_for_fn,
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::trivially_copy_pass_by_ref,
|
||||
clippy::unused_self
|
||||
)]
|
||||
|
||||
mod error;
|
||||
pub mod models;
|
||||
mod replay;
|
||||
pub mod replay;
|
||||
mod result;
|
||||
|
||||
pub use crate::error::Error;
|
||||
pub use crate::replay::Replay;
|
||||
pub use crate::result::Result;
|
||||
pub use self::error::Error;
|
||||
pub use self::result::Result;
|
||||
|
||||
#[cfg(doctest)]
|
||||
mod test_readme {
|
||||
macro_rules! external_doc_test {
|
||||
($x:expr) => {
|
||||
#[doc = $x]
|
||||
extern "C" {}
|
||||
};
|
||||
}
|
||||
|
||||
external_doc_test!(include_str!("../README.md"));
|
||||
}
|
||||
+4
-31
@@ -1,31 +1,4 @@
|
||||
use ::prost::Message;
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct BattleResults {
|
||||
#[prost(int64, tag = "2")]
|
||||
pub timestamp: i64,
|
||||
|
||||
#[prost(message, repeated, tag = "201")]
|
||||
pub players: Vec<Player>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct Player {
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub account_id: u32,
|
||||
|
||||
#[prost(message, required, tag = "2")]
|
||||
pub info: PlayerInfo,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct PlayerInfo {
|
||||
#[prost(string, tag = "1")]
|
||||
pub nickname: String,
|
||||
|
||||
#[prost(uint32, optional, tag = "2")]
|
||||
pub platoon_id: Option<u32>,
|
||||
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub team_number: u32,
|
||||
}
|
||||
pub mod battle_results;
|
||||
pub mod battle_results_dat;
|
||||
pub mod data;
|
||||
pub mod meta;
|
||||
@@ -0,0 +1,218 @@
|
||||
use prost::bytes::Buf;
|
||||
use prost::Message;
|
||||
use serde::Serialize;
|
||||
|
||||
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.
|
||||
pub fn from_buffer(buffer: impl Buf) -> Result<Self> {
|
||||
Ok(Self::decode(buffer)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed battle results.
|
||||
///
|
||||
/// TODO: 150[0].21[0].8[0].801[0].unsigned is arena end time.
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct BattleResults {
|
||||
/// Battle timestamp.
|
||||
#[prost(int64, tag = "2")]
|
||||
pub timestamp: i64,
|
||||
|
||||
#[prost(enumeration = "TeamNumber", tag = "3")]
|
||||
pub winner_team_number: i32,
|
||||
|
||||
/// Replay's author results.
|
||||
#[prost(message, required, tag = "8")]
|
||||
pub author: Author,
|
||||
|
||||
/// Author's free XP, including premium.
|
||||
#[prost(uint32, tag = "137")]
|
||||
pub free_xp: u32,
|
||||
|
||||
/// Players in the battle.
|
||||
#[prost(message, repeated, tag = "201")]
|
||||
pub players: Vec<Player>,
|
||||
|
||||
/// Player's results.
|
||||
#[prost(message, repeated, tag = "301")]
|
||||
pub player_results: Vec<PlayerResults>,
|
||||
}
|
||||
|
||||
/// Battle player.
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct Player {
|
||||
/// Player's account ID.
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub account_id: u32,
|
||||
|
||||
/// Player's extended information.
|
||||
#[prost(message, required, tag = "2")]
|
||||
pub info: PlayerInfo,
|
||||
}
|
||||
|
||||
/// Player's extended information.
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct PlayerInfo {
|
||||
/// Player's nickname.
|
||||
#[prost(string, tag = "1")]
|
||||
pub nickname: String,
|
||||
|
||||
/// Some sort of platoon ID:
|
||||
/// - contains same ID for a platoon members, or
|
||||
/// - `None` for non-platoon players
|
||||
#[prost(uint32, optional, tag = "2")]
|
||||
pub platoon_id: Option<u32>,
|
||||
|
||||
/// Player's team assignment.
|
||||
#[prost(enumeration = "TeamNumber", tag = "3")]
|
||||
pub team: i32,
|
||||
|
||||
#[prost(string, optional, tag = "5")]
|
||||
pub clan_tag: Option<String>,
|
||||
|
||||
#[prost(message, required, tag = "7")]
|
||||
pub avatar: Avatar,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, prost::Enumeration, Serialize,
|
||||
)]
|
||||
#[repr(i32)]
|
||||
pub enum TeamNumber {
|
||||
One = 1,
|
||||
Two = 2,
|
||||
}
|
||||
|
||||
/// Player's battle results.
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct PlayerResults {
|
||||
/// Looks like some sort of sequential result ID. They reference each other,
|
||||
/// but I haven't figured this out yet.
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub result_id: u32,
|
||||
|
||||
/// Extended player's results information.
|
||||
#[prost(message, required, tag = "2")]
|
||||
pub info: PlayerResultsInfo,
|
||||
}
|
||||
|
||||
/// Extended player's results information.
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct PlayerResultsInfo {
|
||||
/// Credits earned – without special awards and medals and premium account excluded.
|
||||
#[prost(uint32, tag = "2")]
|
||||
pub credits_earned: u32,
|
||||
|
||||
/// Base XP (the total without multipliers).
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub base_xp: u32,
|
||||
|
||||
#[prost(uint32, tag = "4")]
|
||||
pub n_shots: u32,
|
||||
|
||||
#[prost(uint32, tag = "5")]
|
||||
pub n_hits_dealt: u32,
|
||||
|
||||
#[prost(uint32, tag = "7")]
|
||||
pub n_penetrations_dealt: u32,
|
||||
|
||||
#[prost(uint32, tag = "8")]
|
||||
pub damage_dealt: u32,
|
||||
|
||||
/// TODO: distinguish the kinds of assisted damage.
|
||||
#[prost(uint32, tag = "9")]
|
||||
pub damage_assisted_1: u32,
|
||||
|
||||
/// TODO: distinguish the kinds of assisted damage.
|
||||
#[prost(uint32, tag = "10")]
|
||||
pub damage_assisted_2: u32,
|
||||
|
||||
#[prost(uint32, tag = "12")]
|
||||
pub n_hits_received: u32,
|
||||
|
||||
#[prost(uint32, tag = "13")]
|
||||
pub n_non_penetrating_hits_received: u32,
|
||||
|
||||
#[prost(uint32, tag = "15")]
|
||||
pub n_penetrations_received: u32,
|
||||
|
||||
#[prost(uint32, tag = "17")]
|
||||
pub n_enemies_damaged: u32,
|
||||
|
||||
#[prost(uint32, tag = "18")]
|
||||
pub n_enemies_destroyed: u32,
|
||||
|
||||
#[prost(uint32, tag = "32")]
|
||||
pub victory_points_earned: u32,
|
||||
|
||||
#[prost(uint32, tag = "33")]
|
||||
pub victory_points_seized: u32,
|
||||
|
||||
/// Player's account ID.
|
||||
#[prost(uint32, tag = "101")]
|
||||
pub account_id: u32,
|
||||
|
||||
/// Player's tank ID as per Wargaming.net API.
|
||||
#[prost(uint32, tag = "103")]
|
||||
pub tank_id: u32,
|
||||
|
||||
/// Rating for the Rating Battles.
|
||||
///
|
||||
/// Note, that this is **not** the game client's displayed rating.
|
||||
/// This field matches the `mm_rating` as returned by the Wargaming.net API.
|
||||
#[prost(float, optional, tag = "107")]
|
||||
pub mm_rating: Option<f32>,
|
||||
|
||||
#[prost(uint32, tag = "117")]
|
||||
pub damage_blocked: u32,
|
||||
}
|
||||
|
||||
impl PlayerResultsInfo {
|
||||
/// Returns displayed rating as per the game client.
|
||||
pub fn display_rating(&self) -> Option<u32> {
|
||||
self.mm_rating
|
||||
.map(|mm_rating| (mm_rating * 10.0 + 3000.0) as u32)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct Author {
|
||||
/// Earned credits, including premium and special awards and medals.
|
||||
#[prost(uint32, tag = "2")]
|
||||
pub credits_earned: u32,
|
||||
|
||||
/// Combat XP, including premium.
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub combat_xp: u32,
|
||||
|
||||
#[prost(uint32, tag = "8")]
|
||||
pub damage_dealt: u32,
|
||||
|
||||
#[prost(uint32, tag = "101")]
|
||||
pub account_id: u32,
|
||||
|
||||
#[prost(enumeration = "TeamNumber", tag = "102")]
|
||||
pub team_number: i32,
|
||||
}
|
||||
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct Avatar {
|
||||
#[prost(message, required, tag = "2")]
|
||||
pub info: AvatarInfo,
|
||||
}
|
||||
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct AvatarInfo {
|
||||
#[prost(string, tag = "2")]
|
||||
pub gfx_url: String,
|
||||
|
||||
#[prost(string, tag = "3")]
|
||||
pub gfx2_url: String,
|
||||
|
||||
#[prost(string, tag = "4")]
|
||||
pub kind: String,
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::io::Read;
|
||||
|
||||
use prost::Message;
|
||||
use serde::Deserialize;
|
||||
use serde_bytes::ByteBuf;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::models::battle_results::BattleResults;
|
||||
use crate::result::Result;
|
||||
|
||||
/// Represents un-pickled `battle_results.dat`.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(from = "(u64, ByteBuf)")]
|
||||
pub struct BattleResultsDat {
|
||||
pub arena_unique_id: u64,
|
||||
|
||||
/// Protobuf-serialized battle results.
|
||||
pub buffer: ByteBuf,
|
||||
}
|
||||
|
||||
impl From<(u64, ByteBuf)> for BattleResultsDat {
|
||||
fn from((arena_unique_id, buffer): (u64, ByteBuf)) -> Self {
|
||||
Self {
|
||||
arena_unique_id,
|
||||
buffer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BattleResultsDat {
|
||||
/// Parses 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`.
|
||||
pub fn from_reader(reader: impl Read) -> Result<Self> {
|
||||
Ok(serde_pickle::from_reader(reader, Default::default())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<BattleResults> for BattleResultsDat {
|
||||
type Error = Error;
|
||||
|
||||
/// Decodes the battle results from the internal buffer.
|
||||
fn try_into(self) -> Result<BattleResults> {
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#![cfg(feature = "meta")]
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::result::Result;
|
||||
|
||||
/// The model for `meta.json`.
|
||||
#[derive(Deserialize)]
|
||||
pub struct Meta {
|
||||
#[serde(rename = "playerName")]
|
||||
pub player_name: String,
|
||||
|
||||
#[serde(rename = "arenaUniqueId")]
|
||||
pub arena_unique_id: String,
|
||||
|
||||
#[serde(rename = "battleDuration")]
|
||||
pub battle_duration_secs: f64,
|
||||
|
||||
#[serde(rename = "vehicleCompDescriptor")]
|
||||
pub tank_id: u16,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
pub fn from_reader(reader: impl Read) -> Result<Self> {
|
||||
Ok(serde_json::from_reader(reader)?)
|
||||
}
|
||||
}
|
||||
+25
-20
@@ -1,35 +1,40 @@
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use prost::Message;
|
||||
use serde_bytes::ByteBuf;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::models::BattleResults;
|
||||
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.
|
||||
pub struct Replay<R>(pub(crate) ZipArchive<R>);
|
||||
|
||||
impl<R: Read + Seek> Replay<R> {
|
||||
/// Opens a replay from the reader.
|
||||
/// 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.
|
||||
pub fn read_battle_results(&mut self) -> Result<BattleResults> {
|
||||
let mut pickled_battle_results = self
|
||||
.0
|
||||
.by_name("battle_results.dat")
|
||||
.map_err(Error::OpenBattleResultsFailed)?;
|
||||
let mut serialized_battle_results = Vec::new();
|
||||
pickled_battle_results
|
||||
.read_to_end(&mut serialized_battle_results)
|
||||
.map_err(Error::ReadEntryFailed)?;
|
||||
let (_, battle_results): (u64, ByteBuf) =
|
||||
serde_pickle::from_slice(&serialized_battle_results, Default::default())
|
||||
.map_err(Error::UnpickleFailed)?;
|
||||
BattleResults::decode(battle_results.as_ref()).map_err(Error::DecodeFailed)
|
||||
self.read_battle_results_dat()?.try_into()
|
||||
}
|
||||
|
||||
/// 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")?;
|
||||
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")?)
|
||||
}
|
||||
|
||||
pub fn read_data(&mut self) -> Result<Data> {
|
||||
Data::from_reader(self.0.by_name("data.wotreplay")?)
|
||||
}
|
||||
}
|
||||
+108
-15
@@ -1,26 +1,119 @@
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use wotbreplay_parser::Replay;
|
||||
use wotbreplay_parser::models::battle_results::BattleResults;
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
#[test]
|
||||
fn battle_results_ok() -> Result<()> {
|
||||
let mut replay = Replay::open(File::open(
|
||||
"tests/replays/20221202_2259__zeekrab_It20_Car_Comb_45t_2308705958773814583.wotbreplay",
|
||||
)?)?;
|
||||
let battle_results = replay.read_battle_results()?;
|
||||
fn player_results_ok() -> Result<()> {
|
||||
let battle_results_dat = Replay::open(File::open("tests/replays/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.timestamp, 1670018359);
|
||||
assert_eq!(battle_results.players.len(), 14);
|
||||
assert_eq!(battle_results.winner_team_number, 2);
|
||||
|
||||
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_number, 2);
|
||||
assert_eq!(battle_results.players[0].info.platoon_id, None);
|
||||
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);
|
||||
|
||||
assert_eq!(battle_results.players[1].info.nickname, "SNAK_THE_RIPPER");
|
||||
assert_eq!(battle_results.players[1].info.team_number, 1);
|
||||
assert_eq!(battle_results.players[1].info.platoon_id, Some(547466834));
|
||||
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);
|
||||
assert_eq!(snak_the_ripper_info.damage_assisted_1, 37);
|
||||
assert_eq!(snak_the_ripper_info.account_id, 566225799);
|
||||
assert_eq!(snak_the_ripper_info.display_rating(), Some(4070));
|
||||
assert_eq!(snak_the_ripper_info.n_shots, 6);
|
||||
assert_eq!(snak_the_ripper_info.n_hits_dealt, 5);
|
||||
assert_eq!(snak_the_ripper_info.n_penetrations_dealt, 5);
|
||||
assert_eq!(snak_the_ripper_info.tank_id, 5233);
|
||||
assert_eq!(snak_the_ripper_info.n_enemies_damaged, 3);
|
||||
assert_eq!(snak_the_ripper_info.n_enemies_destroyed, 2);
|
||||
|
||||
let zeekrab_info = &battle_results.player_results[10].info;
|
||||
assert_eq!(zeekrab_info.base_xp, 701);
|
||||
assert_eq!(zeekrab_info.damage_dealt, 1438);
|
||||
assert_eq!(zeekrab_info.damage_assisted_2, 31);
|
||||
assert_eq!(zeekrab_info.damage_blocked, 190);
|
||||
assert_eq!(zeekrab_info.account_id, 594778041);
|
||||
assert_eq!(zeekrab_info.display_rating(), Some(4277));
|
||||
assert_eq!(zeekrab_info.n_shots, 5);
|
||||
assert_eq!(zeekrab_info.n_hits_dealt, 5);
|
||||
assert_eq!(zeekrab_info.n_penetrations_dealt, 5);
|
||||
assert_eq!(zeekrab_info.n_hits_received, 3);
|
||||
assert_eq!(zeekrab_info.n_penetrations_received, 3);
|
||||
assert_eq!(zeekrab_info.n_non_penetrating_hits_received, 0);
|
||||
assert_eq!(zeekrab_info.victory_points_earned, 40);
|
||||
assert_eq!(zeekrab_info.victory_points_seized, 40);
|
||||
assert_eq!(zeekrab_info.tank_id, 26657);
|
||||
assert_eq!(zeekrab_info.credits_earned, 37679);
|
||||
assert_eq!(zeekrab_info.n_enemies_damaged, 2);
|
||||
assert_eq!(zeekrab_info.n_enemies_destroyed, 1);
|
||||
|
||||
let balls_soup_info = &battle_results.player_results[8].info;
|
||||
assert_eq!(balls_soup_info.damage_dealt, 2064);
|
||||
assert_eq!(balls_soup_info.display_rating(), None);
|
||||
assert_eq!(balls_soup_info.n_shots, 9);
|
||||
assert_eq!(balls_soup_info.n_hits_dealt, 8);
|
||||
assert_eq!(balls_soup_info.n_penetrations_dealt, 8);
|
||||
assert_eq!(balls_soup_info.n_hits_received, 6);
|
||||
assert_eq!(balls_soup_info.n_penetrations_received, 4);
|
||||
assert_eq!(balls_soup_info.n_non_penetrating_hits_received, 2);
|
||||
assert_eq!(balls_soup_info.tank_id, 4737);
|
||||
assert_eq!(balls_soup_info.n_enemies_damaged, 4);
|
||||
assert_eq!(balls_soup_info.n_enemies_destroyed, 1);
|
||||
|
||||
let ceaser_info = &battle_results.player_results[0].info;
|
||||
assert_eq!(ceaser_info.damage_dealt, 919);
|
||||
assert_eq!(ceaser_info.display_rating(), Some(4439));
|
||||
assert_eq!(ceaser_info.n_shots, 9);
|
||||
assert_eq!(ceaser_info.n_hits_dealt, 6);
|
||||
assert_eq!(ceaser_info.n_penetrations_dealt, 5);
|
||||
assert_eq!(ceaser_info.n_enemies_damaged, 3);
|
||||
assert_eq!(ceaser_info.n_enemies_destroyed, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_info_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
|
||||
assert_eq!(battle_results.players[1].info.clan_tag.as_deref(), Some("AN0NY"));
|
||||
|
||||
assert_eq!(battle_results.players[10].info.clan_tag.as_deref(), Some("BBS"));
|
||||
assert_eq!(battle_results.players[10].info.avatar.info.kind, "legendary");
|
||||
assert_eq!(
|
||||
battle_results.players[10].info.avatar.info.gfx_url,
|
||||
"https://stufficons.wgcdn.co/Gfx/avatar_UFO_legendary_anim/220523_104739_752791.webp"
|
||||
);
|
||||
assert_eq!(
|
||||
battle_results.players[10].info.avatar.info.gfx2_url,
|
||||
"https://stufficons.wgcdn.co/Gfx2/avatar_UFO_legendary_anim/220523_104755_251331.webp"
|
||||
);
|
||||
|
||||
assert_eq!(battle_results.players[11].info.clan_tag, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn victory_points_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/victory_points.wotbreplay")?)?
|
||||
.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_earned, 40);
|
||||
|
||||
assert_eq!(battle_results.player_results[3].info.victory_points_seized, 0);
|
||||
assert_eq!(battle_results.player_results[3].info.victory_points_earned, 112);
|
||||
|
||||
assert_eq!(battle_results.player_results[5].info.victory_points_earned, 280);
|
||||
assert_eq!(battle_results.player_results[5].info.victory_points_seized, 0);
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#![cfg(feature = "meta")]
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
#[test]
|
||||
fn meta_ok() -> Result<()> {
|
||||
let meta = Replay::open(File::open("tests/replays/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);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
*.wotbreplay linguist-generated
|
||||
BIN
Binary file not shown.
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