mirror of
https://github.com/eigenein/wotbreplay-parser.git
synced 2026-10-21 10:27:01 +02:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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,50 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: 📥 Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up the toolchain
|
- name: ✨ Set up the toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: ⚡️ Rust Cache
|
||||||
uses: Swatinem/rust-cache@v1
|
uses: Swatinem/rust-cache@v1
|
||||||
with:
|
with:
|
||||||
cache-on-failure: true
|
cache-on-failure: true
|
||||||
|
|
||||||
- name: Test
|
- name: 🧪 Test without optional features
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --doc
|
args: --all-targets
|
||||||
|
|
||||||
- name: Clippy
|
- name: 🧪 Test all features
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --all-targets --all-features
|
||||||
|
|
||||||
|
- name: 🧪 Test docs
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --doc --all-features
|
||||||
|
|
||||||
|
- name: 🪄 Clippy
|
||||||
uses: actions-rs/clippy-check@v1
|
uses: actions-rs/clippy-check@v1
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
args: --all-targets --all-features --tests -- -D warnings
|
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
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: 📥 Checkout the repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set the versions
|
- name: 🪄 Set the versions
|
||||||
run: |
|
run: |
|
||||||
sed -i -e 's/version = "0\.0\.0"/version = "${{ github.ref_name }}"/g' Cargo.toml
|
sed -i -e 's/version = "0\.0\.0"/version = "${{ github.ref_name }}"/g' Cargo.toml
|
||||||
- name: Rustup
|
- name: Rustup
|
||||||
@@ -29,13 +29,13 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|
||||||
- name: Build the changelog
|
- name: 📝 Build the changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
uses: Bullrich/generate-release-changelog@master
|
uses: Bullrich/generate-release-changelog@master
|
||||||
env:
|
env:
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
|
|
||||||
- name: Update the release
|
- name: 🪄 Update the release
|
||||||
uses: tubone24/[email protected]
|
uses: tubone24/[email protected]
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -43,9 +43,8 @@ jobs:
|
|||||||
release_name: ${{ github.ref_name }}
|
release_name: ${{ github.ref_name }}
|
||||||
body: ${{ steps.changelog.outputs.changelog }}
|
body: ${{ steps.changelog.outputs.changelog }}
|
||||||
|
|
||||||
- name: Publish the crates
|
- name: 📤 Publish the crates
|
||||||
run: |
|
run: |
|
||||||
cargo login
|
|
||||||
cargo publish --allow-dirty
|
cargo publish --allow-dirty
|
||||||
env:
|
env:
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
+7
-2
@@ -7,17 +7,22 @@ authors = ["Pavel Perestoronin <[email protected]>"]
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/eigenein/wotbreplay-parser"
|
repository = "https://github.com/eigenein/wotbreplay-parser"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
keywords = []
|
keywords = ["world-of-tanks-blitz", "replay", "parser", "wargaming", "results"]
|
||||||
categories = []
|
categories = ["parsing"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
meta = ["dep:serde_json"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bytes = "1.4.0"
|
||||||
|
byteorder = "1.4.3"
|
||||||
prost = "0.11.3"
|
prost = "0.11.3"
|
||||||
serde = { version = "1.0.148", features = ["derive"] }
|
serde = { version = "1.0.148", features = ["derive"] }
|
||||||
serde_bytes = "0.11.7"
|
serde_bytes = "0.11.7"
|
||||||
serde-pickle = "1.1.1"
|
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"
|
thiserror = "1.0.37"
|
||||||
zip = { version = "0.6.3", default-features = false, features = ["deflate"] }
|
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,38 @@ World of Tanks Blitz replay parser in Rust.
|
|||||||
|
|
||||||
[](https://crates.io/crates/wotbreplay-parser)
|
[](https://crates.io/crates/wotbreplay-parser)
|
||||||
[](https://github.com/eigenein/wotbreplay-parser/commits/main)
|
[](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)
|
[](https://docs.rs/wotbreplay-parser)
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use wotbreplay_parser::prelude::*;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let battle_results = Replay::open(File::open("tests/replays/battle_results.wotbreplay")?)?
|
||||||
|
.read_battle_results()?;
|
||||||
|
|
||||||
|
assert_eq!(battle_results.timestamp, 1670018359);
|
||||||
|
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[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));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Replay structure
|
## Replay structure
|
||||||
|
|
||||||
`*.wotbreplay` is a ZIP-archive containing:
|
`*.wotbreplay` is a ZIP-archive containing:
|
||||||
|
|||||||
+20
-10
@@ -2,18 +2,28 @@ use thiserror::Error;
|
|||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("failed to open the ZIP-archive")]
|
#[error("ZIP error")]
|
||||||
OpenArchiveFailed(#[source] zip::result::ZipError),
|
ZipError(#[from] zip::result::ZipError),
|
||||||
|
|
||||||
#[error("failed to open the battle results entry in the replay archive")]
|
#[error("Protocol Buffers error")]
|
||||||
OpenBattleResultsFailed(#[source] zip::result::ZipError),
|
DecodeFailed(#[from] prost::DecodeError),
|
||||||
|
|
||||||
#[error("failed to read the archive entry")]
|
#[error("Pickle error")]
|
||||||
ReadEntryFailed(#[source] std::io::Error),
|
UnpickleFailed(#[from] serde_pickle::Error),
|
||||||
|
|
||||||
#[error("failed to decode")]
|
#[error("I/O error")]
|
||||||
DecodeFailed(#[source] prost::DecodeError),
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("failed to un-pickle")]
|
#[error("UTF-8 error")]
|
||||||
UnpickleFailed(#[source] serde_pickle::Error),
|
StringDecodeError(#[from] std::string::FromUtf8Error),
|
||||||
|
|
||||||
|
#[cfg(feature = "meta")]
|
||||||
|
#[error("JSON error")]
|
||||||
|
JsonDecodeFailed(#[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),
|
||||||
}
|
}
|
||||||
+9
-9
@@ -2,22 +2,22 @@
|
|||||||
|
|
||||||
#![warn(
|
#![warn(
|
||||||
clippy::all,
|
clippy::all,
|
||||||
clippy::missing_const_for_fn,
|
|
||||||
clippy::trivially_copy_pass_by_ref,
|
|
||||||
clippy::map_unwrap_or,
|
|
||||||
clippy::explicit_into_iter_loop,
|
clippy::explicit_into_iter_loop,
|
||||||
clippy::unused_self,
|
clippy::manual_let_else,
|
||||||
clippy::needless_pass_by_value
|
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;
|
mod error;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
mod replay;
|
pub mod replay;
|
||||||
mod result;
|
mod result;
|
||||||
|
|
||||||
pub use crate::error::Error;
|
pub use self::error::Error;
|
||||||
pub use crate::replay::Replay;
|
pub use self::result::Result;
|
||||||
pub use crate::result::Result;
|
|
||||||
|
|
||||||
#[cfg(doctest)]
|
#[cfg(doctest)]
|
||||||
mod test_readme {
|
mod test_readme {
|
||||||
|
|||||||
+4
-5
@@ -1,5 +1,4 @@
|
|||||||
mod battle_results;
|
pub mod battle_results;
|
||||||
mod battle_results_dat;
|
pub mod battle_results_dat;
|
||||||
|
pub mod data;
|
||||||
pub use self::battle_results::*;
|
pub mod meta;
|
||||||
pub use self::battle_results_dat::*;
|
|
||||||
@@ -1,25 +1,38 @@
|
|||||||
use prost::bytes::Buf;
|
use prost::bytes::Buf;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::error::Error;
|
|
||||||
use crate::result::Result;
|
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, that this does NOT parse `battle_results.dat` itself, but the un-pickled tuple element.
|
||||||
pub fn parse(buffer: impl Buf) -> Result<Self> {
|
pub fn from_buffer(buffer: impl Buf) -> Result<Self> {
|
||||||
Self::decode(buffer).map_err(Error::DecodeFailed)
|
Ok(Self::decode(buffer)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parsed battle results.
|
/// Parsed battle results.
|
||||||
#[derive(Message)]
|
///
|
||||||
|
/// TODO: 150[0].21[0].8[0].801[0].unsigned is arena end time.
|
||||||
|
#[derive(Message, Serialize)]
|
||||||
pub struct BattleResults {
|
pub struct BattleResults {
|
||||||
/// Battle timestamp.
|
/// Battle timestamp.
|
||||||
#[prost(int64, tag = "2")]
|
#[prost(int64, tag = "2")]
|
||||||
pub timestamp: i64,
|
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.
|
/// Players in the battle.
|
||||||
#[prost(message, repeated, tag = "201")]
|
#[prost(message, repeated, tag = "201")]
|
||||||
pub players: Vec<Player>,
|
pub players: Vec<Player>,
|
||||||
@@ -30,7 +43,7 @@ pub struct BattleResults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Battle player.
|
/// Battle player.
|
||||||
#[derive(Message)]
|
#[derive(Message, Serialize)]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
/// Player's account ID.
|
/// Player's account ID.
|
||||||
#[prost(uint32, tag = "1")]
|
#[prost(uint32, tag = "1")]
|
||||||
@@ -42,7 +55,7 @@ pub struct Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Player's extended information.
|
/// Player's extended information.
|
||||||
#[derive(Message)]
|
#[derive(Message, Serialize)]
|
||||||
pub struct PlayerInfo {
|
pub struct PlayerInfo {
|
||||||
/// Player's nickname.
|
/// Player's nickname.
|
||||||
#[prost(string, tag = "1")]
|
#[prost(string, tag = "1")]
|
||||||
@@ -56,10 +69,18 @@ pub struct PlayerInfo {
|
|||||||
|
|
||||||
/// Player's team assignment.
|
/// Player's team assignment.
|
||||||
#[prost(enumeration = "TeamNumber", tag = "3")]
|
#[prost(enumeration = "TeamNumber", tag = "3")]
|
||||||
pub team_number: i32,
|
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)]
|
#[derive(
|
||||||
|
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,
|
||||||
@@ -67,7 +88,7 @@ pub enum TeamNumber {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Player's battle results.
|
/// Player's battle results.
|
||||||
#[derive(Message)]
|
#[derive(Message, Serialize)]
|
||||||
pub struct PlayerResults {
|
pub struct PlayerResults {
|
||||||
/// Looks like some sort of sequential result ID. They reference each other,
|
/// Looks like some sort of sequential result ID. They reference each other,
|
||||||
/// but I haven't figured this out yet.
|
/// but I haven't figured this out yet.
|
||||||
@@ -80,8 +101,12 @@ pub struct PlayerResults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extended player's results information.
|
/// Extended player's results information.
|
||||||
#[derive(Message)]
|
#[derive(Message, Serialize)]
|
||||||
pub struct PlayerResultsInfo {
|
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).
|
/// Base XP (the total without multipliers).
|
||||||
#[prost(uint32, tag = "3")]
|
#[prost(uint32, tag = "3")]
|
||||||
pub base_xp: u32,
|
pub base_xp: u32,
|
||||||
@@ -98,9 +123,11 @@ pub struct PlayerResultsInfo {
|
|||||||
#[prost(uint32, tag = "8")]
|
#[prost(uint32, tag = "8")]
|
||||||
pub damage_dealt: u32,
|
pub damage_dealt: u32,
|
||||||
|
|
||||||
|
/// TODO: distinguish the kinds of assisted damage.
|
||||||
#[prost(uint32, tag = "9")]
|
#[prost(uint32, tag = "9")]
|
||||||
pub damage_assisted_1: u32,
|
pub damage_assisted_1: u32,
|
||||||
|
|
||||||
|
/// TODO: distinguish the kinds of assisted damage.
|
||||||
#[prost(uint32, tag = "10")]
|
#[prost(uint32, tag = "10")]
|
||||||
pub damage_assisted_2: u32,
|
pub damage_assisted_2: u32,
|
||||||
|
|
||||||
@@ -113,18 +140,26 @@ pub struct PlayerResultsInfo {
|
|||||||
#[prost(uint32, tag = "15")]
|
#[prost(uint32, tag = "15")]
|
||||||
pub n_penetrations_received: u32,
|
pub n_penetrations_received: u32,
|
||||||
|
|
||||||
/// TODO: distinguish earned vs seized.
|
#[prost(uint32, tag = "17")]
|
||||||
|
pub n_enemies_damaged: u32,
|
||||||
|
|
||||||
|
#[prost(uint32, tag = "18")]
|
||||||
|
pub n_enemies_destroyed: u32,
|
||||||
|
|
||||||
#[prost(uint32, tag = "32")]
|
#[prost(uint32, tag = "32")]
|
||||||
pub victory_points_1: u32,
|
pub victory_points_earned: u32,
|
||||||
|
|
||||||
/// TODO: distinguish earned vs seized.
|
|
||||||
#[prost(uint32, tag = "33")]
|
#[prost(uint32, tag = "33")]
|
||||||
pub victory_points_2: u32,
|
pub victory_points_seized: u32,
|
||||||
|
|
||||||
/// Player's account ID.
|
/// Player's account ID.
|
||||||
#[prost(uint32, tag = "101")]
|
#[prost(uint32, tag = "101")]
|
||||||
pub account_id: u32,
|
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.
|
/// Rating for the Rating Battles.
|
||||||
///
|
///
|
||||||
/// Note, that this is **not** the game client's displayed rating.
|
/// Note, that this is **not** the game client's displayed rating.
|
||||||
@@ -143,3 +178,41 @@ impl PlayerResultsInfo {
|
|||||||
.map(|mm_rating| (mm_rating * 10.0 + 3000.0) as u32)
|
.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,
|
||||||
|
}
|
||||||
@@ -4,26 +4,44 @@ use prost::Message;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_bytes::ByteBuf;
|
use serde_bytes::ByteBuf;
|
||||||
|
|
||||||
use crate::models::BattleResults;
|
use crate::error::Error;
|
||||||
use crate::{Error, Result};
|
use crate::models::battle_results::BattleResults;
|
||||||
|
use crate::result::Result;
|
||||||
|
|
||||||
/// Represents un-pickled `battle_results.dat`.
|
/// Represents un-pickled `battle_results.dat`.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct BattleResultsDat(
|
#[serde(from = "(u64, ByteBuf)")]
|
||||||
/// No idea what this is.
|
pub struct BattleResultsDat {
|
||||||
u64,
|
pub arena_unique_id: u64,
|
||||||
|
|
||||||
/// Protobuf-serialized battle results.
|
/// Protobuf-serialized battle results.
|
||||||
ByteBuf,
|
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 {
|
impl BattleResultsDat {
|
||||||
/// Parses the pickled structure from the reader which contains `battle_results.dat`.
|
/// 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> {
|
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())?)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<BattleResults> for BattleResultsDat {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
/// Decodes the battle results from the internal buffer.
|
/// Decodes the battle results from the internal buffer.
|
||||||
pub fn decode_battle_results(&self) -> Result<BattleResults> {
|
fn try_into(self) -> Result<BattleResults> {
|
||||||
BattleResults::decode(self.1.as_ref()).map_err(Error::DecodeFailed)
|
Ok(BattleResults::decode(self.buffer.as_ref())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
//! `data.wotreplay` models.
|
||||||
|
|
||||||
|
pub mod entity_method;
|
||||||
|
pub mod packet;
|
||||||
|
pub mod payload;
|
||||||
|
pub mod replay_header;
|
||||||
|
pub mod summary;
|
||||||
|
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub use self::entity_method::*;
|
||||||
|
pub use self::summary::*;
|
||||||
|
use crate::models::data::packet::Packet;
|
||||||
|
use crate::models::data::payload::Payload;
|
||||||
|
use crate::models::data::replay_header::ReplayHeader;
|
||||||
|
use crate::result::Result;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// `data.wotreplay` root structure.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct Data {
|
||||||
|
/// For example: `9.8.5_apple`.
|
||||||
|
pub client_version: String,
|
||||||
|
|
||||||
|
pub replay_header: Option<ReplayHeader>,
|
||||||
|
|
||||||
|
pub other_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_bytes(&mut reader)?;
|
||||||
|
|
||||||
|
let client_version = read_string(&mut reader)?;
|
||||||
|
|
||||||
|
// Some extra byte, no idea:
|
||||||
|
reader.read_u8()?;
|
||||||
|
|
||||||
|
let mut this = Self::new(client_version);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(packet) = Packet::from_reader(&mut reader)? else { break };
|
||||||
|
match packet.payload {
|
||||||
|
Payload::ReplayHeader(header) => {
|
||||||
|
this.replay_header = Some(header);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
this.other_packets.push(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn new(client_version: String) -> Self {
|
||||||
|
Self {
|
||||||
|
client_version,
|
||||||
|
replay_header: None,
|
||||||
|
other_packets: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_bytes(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_bytes(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())?)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::result::Result;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub enum EntityMethod {
|
||||||
|
/// TODO: team composition.
|
||||||
|
Subtype2F,
|
||||||
|
|
||||||
|
/// Default variant when subtype is not known.
|
||||||
|
Unknown { sub_type: u32, payload: Vec<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 => Self::Subtype2F,
|
||||||
|
_ => 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,51 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_with::serde_as;
|
||||||
|
|
||||||
|
use crate::models::data::replay_header::ReplayHeader;
|
||||||
|
use crate::models::data::{assert_magic, read_pickled, read_string, EntityMethod};
|
||||||
|
use crate::result::Result;
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub enum Payload {
|
||||||
|
ReplayHeader(ReplayHeader),
|
||||||
|
EntityMethod(EntityMethod),
|
||||||
|
|
||||||
|
/// Default payload when type is not known.
|
||||||
|
Unknown(#[serde_as(as = "serde_with::hex::Hex")] Vec<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 summary = {
|
||||||
|
assert_magic(reader.read_u8()?, 0xFF)?;
|
||||||
|
let pickled_length = reader.read_u16::<LittleEndian>()?;
|
||||||
|
assert_magic(reader.read_u8()?, 0x00)?;
|
||||||
|
read_pickled(&mut reader, pickled_length as usize)?
|
||||||
|
};
|
||||||
|
Self::ReplayHeader(ReplayHeader {
|
||||||
|
summary,
|
||||||
|
author_nickname,
|
||||||
|
arena_unique_id,
|
||||||
|
arena_type_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
8 => Self::EntityMethod(EntityMethod::new(payload)?),
|
||||||
|
|
||||||
|
_ => Self::Unknown(payload),
|
||||||
|
};
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::models::data::Summary;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ReplayHeader {
|
||||||
|
pub summary: Summary,
|
||||||
|
pub author_nickname: String,
|
||||||
|
pub arena_unique_id: u64,
|
||||||
|
pub arena_type_id: u32,
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Summary {
|
||||||
|
#[serde(rename = "playersBattleCategoriesIds")]
|
||||||
|
pub players_battle_categories_ids: HashMap<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>,
|
||||||
|
}
|
||||||
@@ -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)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
-10
@@ -2,8 +2,11 @@ use std::io::{Read, Seek};
|
|||||||
|
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::models::battle_results::BattleResults;
|
||||||
use crate::models::{BattleResults, BattleResultsDat};
|
use crate::models::battle_results_dat::BattleResultsDat;
|
||||||
|
use crate::models::data::Data;
|
||||||
|
#[cfg(feature = "meta")]
|
||||||
|
use crate::models::meta::Meta;
|
||||||
use crate::result::Result;
|
use crate::result::Result;
|
||||||
|
|
||||||
/// Opened (but not yet parsed) replay.
|
/// Opened (but not yet parsed) replay.
|
||||||
@@ -12,17 +15,26 @@ pub struct Replay<R>(pub(crate) ZipArchive<R>);
|
|||||||
impl<R: Read + Seek> Replay<R> {
|
impl<R: Read + Seek> Replay<R> {
|
||||||
/// Opens a replay from the reader which contains a `*.wotbreplay`.
|
/// Opens a replay from the reader which contains a `*.wotbreplay`.
|
||||||
pub fn open(reader: R) -> Result<Self> {
|
pub fn open(reader: R) -> Result<Self> {
|
||||||
ZipArchive::new(reader)
|
Ok(Self(ZipArchive::new(reader)?))
|
||||||
.map_err(Error::OpenArchiveFailed)
|
|
||||||
.map(Self)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads and parses the battle results from the replay.
|
/// Reads and parses the battle results from the replay.
|
||||||
pub fn read_battle_results(&mut self) -> Result<BattleResults> {
|
pub fn read_battle_results(&mut self) -> Result<BattleResults> {
|
||||||
let pickled_battle_results = self
|
self.read_battle_results_dat()?.try_into()
|
||||||
.0
|
}
|
||||||
.by_name("battle_results.dat")
|
|
||||||
.map_err(Error::OpenBattleResultsFailed)?;
|
/// Reads and parses the included `battle_results.dat`.
|
||||||
BattleResultsDat::from_reader(pickled_battle_results)?.decode_battle_results()
|
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")?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+77
-26
@@ -1,33 +1,18 @@
|
|||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use wotbreplay_parser::models::TeamNumber;
|
use wotbreplay_parser::models::battle_results::BattleResults;
|
||||||
use wotbreplay_parser::Replay;
|
use wotbreplay_parser::replay::Replay;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn battle_results_ok() -> Result<()> {
|
|
||||||
let battle_results = Replay::open(File::open("tests/replays/battle_results_ok.wotbreplay")?)?
|
|
||||||
.read_battle_results()?;
|
|
||||||
|
|
||||||
assert_eq!(battle_results.timestamp, 1670018359);
|
|
||||||
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_number(), TeamNumber::Two);
|
|
||||||
assert_eq!(battle_results.players[0].info.platoon_id, None);
|
|
||||||
|
|
||||||
assert_eq!(battle_results.players[1].info.nickname, "SNAK_THE_RIPPER");
|
|
||||||
assert_eq!(battle_results.players[1].info.team_number(), TeamNumber::One);
|
|
||||||
assert_eq!(battle_results.players[1].info.platoon_id, Some(547466834));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn player_results_ok() -> Result<()> {
|
fn player_results_ok() -> Result<()> {
|
||||||
let battle_results = Replay::open(File::open("tests/replays/player_results_ok.wotbreplay")?)?
|
let battle_results_dat = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
||||||
.read_battle_results()?;
|
.read_battle_results_dat()?;
|
||||||
|
assert_eq!(battle_results_dat.arena_unique_id, 1661909200500084);
|
||||||
|
let battle_results: BattleResults = battle_results_dat.try_into()?;
|
||||||
|
|
||||||
|
assert_eq!(battle_results.winner_team_number, 2);
|
||||||
|
assert_eq!(battle_results.author.team_number, 2);
|
||||||
|
|
||||||
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);
|
||||||
@@ -38,6 +23,9 @@ fn player_results_ok() -> Result<()> {
|
|||||||
assert_eq!(snak_the_ripper_info.n_shots, 6);
|
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_hits_dealt, 5);
|
||||||
assert_eq!(snak_the_ripper_info.n_penetrations_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;
|
let zeekrab_info = &battle_results.player_results[10].info;
|
||||||
assert_eq!(zeekrab_info.base_xp, 701);
|
assert_eq!(zeekrab_info.base_xp, 701);
|
||||||
@@ -52,8 +40,12 @@ fn player_results_ok() -> Result<()> {
|
|||||||
assert_eq!(zeekrab_info.n_hits_received, 3);
|
assert_eq!(zeekrab_info.n_hits_received, 3);
|
||||||
assert_eq!(zeekrab_info.n_penetrations_received, 3);
|
assert_eq!(zeekrab_info.n_penetrations_received, 3);
|
||||||
assert_eq!(zeekrab_info.n_non_penetrating_hits_received, 0);
|
assert_eq!(zeekrab_info.n_non_penetrating_hits_received, 0);
|
||||||
assert_eq!(zeekrab_info.victory_points_1, 40);
|
assert_eq!(zeekrab_info.victory_points_earned, 40);
|
||||||
assert_eq!(zeekrab_info.victory_points_2, 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;
|
let balls_soup_info = &battle_results.player_results[8].info;
|
||||||
assert_eq!(balls_soup_info.damage_dealt, 2064);
|
assert_eq!(balls_soup_info.damage_dealt, 2064);
|
||||||
@@ -64,6 +56,9 @@ fn player_results_ok() -> Result<()> {
|
|||||||
assert_eq!(balls_soup_info.n_hits_received, 6);
|
assert_eq!(balls_soup_info.n_hits_received, 6);
|
||||||
assert_eq!(balls_soup_info.n_penetrations_received, 4);
|
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.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;
|
let ceaser_info = &battle_results.player_results[0].info;
|
||||||
assert_eq!(ceaser_info.damage_dealt, 919);
|
assert_eq!(ceaser_info.damage_dealt, 919);
|
||||||
@@ -71,6 +66,62 @@ fn player_results_ok() -> Result<()> {
|
|||||||
assert_eq!(ceaser_info.n_shots, 9);
|
assert_eq!(ceaser_info.n_shots, 9);
|
||||||
assert_eq!(ceaser_info.n_hits_dealt, 6);
|
assert_eq!(ceaser_info.n_hits_dealt, 6);
|
||||||
assert_eq!(ceaser_info.n_penetrations_dealt, 5);
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use wotbreplay_parser::replay::Replay;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn data_ok() -> Result<()> {
|
||||||
|
let mut replay = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?;
|
||||||
|
let data = replay.read_data()?;
|
||||||
|
|
||||||
|
assert_eq!(data.client_version, "9.4.0_apple");
|
||||||
|
assert_eq!(data.other_packets.len(), 44720);
|
||||||
|
|
||||||
|
let header = data.replay_header.unwrap();
|
||||||
|
assert_eq!(header.author_nickname, "zeekrab");
|
||||||
|
assert_eq!(header.arena_unique_id, 1661909200500084);
|
||||||
|
assert_eq!(header.arena_type_id, 65567);
|
||||||
|
assert_eq!(header.summary.battle_level, 8);
|
||||||
|
assert_eq!(header.summary.battle_category_id, 13);
|
||||||
|
assert!(header.summary.is_mouse_enabled);
|
||||||
|
assert_eq!(header.summary.matchmaker_type, 2);
|
||||||
|
assert_eq!(header.summary.camouflage_slot, 1);
|
||||||
|
assert_eq!(header.summary.average_mmr, [-25.396859946846963, 77.58806455880404]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Generated
BIN
Binary file not shown.
File renamed without changes.
File renamed without changes.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Reference in new issue
Block a user