Compare commits

..
66 Commits
Author SHA1 Message Date
eigenein 87beaf2076 🚑️ Fix for 64-bit player IDs 2024-01-29 20:05:45 +01:00
eigenein 581192b8c7 ✨ Add PlayerInfo::rank 2023-08-02 21:38:20 +02:00
eigenein f1cddbecd5 🚑️ Make account_database_ids optional 2023-06-29 23:25:30 +02:00
eigenein bad56d0121 🚑️ Fix the map IDs 2023-05-16 21:56:25 +02:00
eigenein c2ddaf4d0e 🚑️ Fix the tag 2023-05-16 21:39:30 +02:00
eigenein 50f15b9ae9 💥 Add more maps and result fields 2023-05-16 21:37:07 +02:00
eigenein da0646f40a ✨ Add more maps, fix the enum 2023-05-13 20:37:05 +02:00
eigenein 1313bc82f7 ✨ Add MapId 2023-05-12 01:00:55 +02:00
eigenein c986abca5d 💥 Improve the docs and error handling 2023-05-08 23:39:52 +02:00
eigenein 838a49ee68 💥 Rename Packet::clock to clock_secs 2023-05-08 23:14:13 +02:00
eigenein a177447ca1 💥 Hide raw payloads behind the feature flag, and improve the performance 2023-05-08 23:00:40 +02:00
eigenein 2d2c85c18a ✅ Add the benchmark 2023-05-08 22:43:28 +02:00
eigenein 0f437c39de 🎨 Move replays to the project root 2023-05-08 22:26:03 +02:00
eigenein 12aa208217 Opt: remove the custom funding links 2023-05-08 20:55:35 +02:00
eigenein 181da1ae96 Break: improve the docs and rename type_ into packet_type 2023-05-08 20:53:48 +02:00
eigenein 0ac983832b Fix: make some fields optional 2023-05-04 17:53:52 +02:00
eigenein 1830536224 Break: restructure the code 2023-05-03 15:25:33 +02:00
eigenein 897f98c17c Break: make winner_team_number optional 2023-05-03 12:50:33 +02:00
eigenein 59353a9aca Break: fix winner_team_number for a draw 2023-05-03 00:48:10 +02:00
eigenein c9a2adff4c Break: rename subtype 2F to «update arena» 2023-05-03 00:41:45 +02:00
eigenein 72165ae52c Break: decode the players packet 2023-04-30 22:13:51 +02:00
eigenein e48fe1dd0d Break: refactor the packet payloads 2023-04-30 19:32:35 +02:00
eigenein 9d601082d6 New: temporarily dump Subtype2F payload in order to investigate it 2023-04-30 18:57:01 +02:00
eigenein 632f1781e7 Fix: average_mmr may be optional 2023-04-29 23:33:28 +02:00
eigenein fa6ac70d21 New: add TurboBattlesStatistics 2023-04-29 22:52:24 +02:00
eigenein 1fb4bea267 Fix: include raw payload in Payload::Unknown as well 2023-04-29 22:32:46 +02:00
eigenein 705280fc19 Fix: serialize EntityMethod::Unknown::payload as hex 2023-04-29 22:29:52 +02:00
eigenein 3f557e33bf Fix: doctests 2023-04-29 22:20:46 +02:00
eigenein fb5f52038d Break: read replay packets 2023-04-29 22:17:12 +02:00
eigenein 615537ce45 Break: read some of data.wotreplay 2023-04-29 19:05:40 +02:00
eigenein 8e96f4847b Break: make meta optional 2023-04-28 16:22:10 +02:00
eigenein 14148c7410 Break: add initial meta.json support 2023-04-24 22:38:23 +02:00
eigenein 903cd00f32 Break: support arena_unique_id 2023-04-24 22:13:03 +02:00
eigenein e0ce6a22b1 Opt: add the docstrings and TODO 2023-04-20 01:24:07 +02:00
eigenein fb9beb5ecc Fix: remove the explicit cargo login call 2023-04-17 00:16:23 +02:00
eigenein 06de455795 Fix: workflow status badge 2023-04-17 00:11:52 +02:00
eigenein ac256d4c9d New: add Author::team_number 2023-04-17 00:05:03 +02:00
eigenein e853224017 Break: rename parse into from_buffer 2023-04-16 23:09:05 +02:00
eigenein 04420da334 New: impl TryInto<BattleResults> for BattleResultsDat 2023-04-16 23:05:41 +02:00
eigenein cd933f6584 Opt: chores 2023-04-16 21:35:20 +02:00
eigenein ba28620be7 New: add AvatarInfo 2022-12-07 15:40:34 +01:00
eigenein 3913046be7 Fix: forgotten AccountId leftover 2022-12-07 15:22:11 +01:00
eigenein de6deb3a06 Break: extract the re-imports to prelude 2022-12-07 15:21:21 +01:00
eigenein e1d9c27bda Break: rename author_results into author and add author.account_id 2022-12-07 14:17:23 +01:00
eigenein d2fb61425a New: initial author_results 2022-12-06 19:45:01 +01:00
eigenein 93cea446f5 New: add winning_team 2022-12-06 19:08:56 +01:00
eigenein 8d45bb28ed Opt: add .gitattributes 2022-12-06 01:14:01 +01:00
eigenein 1e0ce05c18 New: derive Serialize on battle results 2022-12-05 22:26:47 +01:00
eigenein f63ba599ec Fix: improved the docstring 2022-12-05 20:11:00 +01:00
eigenein f619ffd93e New: add LICENSE 2022-12-05 19:47:14 +01:00
Pavel Perestoronin 7d99456aaa Opt: add the docstrings, make the BattleResultsDat's fields public 2022-12-05 16:03:26 +01:00
Pavel Perestoronin be4a6c21b5 New: extract Replay::read_battle_results_dat 2022-12-05 15:50:13 +01:00
Pavel Perestoronin 805a2af81b Opt: add the keywords and categories 2022-12-03 21:15:15 +01:00
Pavel Perestoronin 899456eaf7 Break: distinguish the victory points seized vs earned 2022-12-03 21:05:52 +01:00
Pavel Perestoronin 96e63f81f4 Opt: move the test to README.md 2022-12-03 20:52:51 +01:00
eigenein 5786253cd5 New: set up sponsorship 2022-12-03 20:36:28 +01:00
Pavel Perestoronin e1f38014b8 New: add clan_tag 2022-12-03 20:34:44 +01:00
Pavel Perestoronin 7376cfc97e New: add n_enemies_damaged, n_enemies_destroyed, and credits 2022-12-03 20:28:18 +01:00
Pavel Perestoronin bc3b39e764 New: add tank_id 2022-12-03 20:11:49 +01:00
Pavel Perestoronin 4edcc50a87 Fix: don't clean up the pre-release flag 2022-12-03 20:06:35 +01:00
Pavel Perestoronin bccb5b519e New: reverse-engineer PlayerResultsInfo 2022-12-03 20:03:24 +01:00
Pavel Perestoronin ac086b1eb2 Break: refactor the parsers 2022-12-03 18:21:32 +01:00
Pavel Perestoronin 68e00c5eca Break: extract TeamNumber 2022-12-03 18:13:57 +01:00
Pavel Perestoronin 6fb70da06f Opt: extract BattleResultsDat::from_reader 2022-12-03 17:59:04 +01:00
Pavel Perestoronin 679b0ddadf Opt: avoid the buffer allocation, use a reader 2022-12-03 17:49:47 +01:00
Pavel Perestoronin 21206737c0 Break: refactor the models 2022-12-03 17:48:42 +01:00
38 changed files with 1262 additions and 111 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
github: [eigenein]
+18 -6
View File
@@ -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 }}
+5 -7
View File
@@ -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 }}
+1
View File
@@ -4,6 +4,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/benches" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
+15 -2
View File
@@ -7,18 +7,31 @@ 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"] # `meta.json` parsing
raw-payload = [] # save raw packet payloads, enabling increases memory usage and decreases performance
[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_repr = "0.1.12"
serde_with = { version = "2.3.3", features = ["hex"] }
thiserror = "1.0.37"
zip = { version = "0.6.3", default-features = false, features = ["deflate"] }
[dev-dependencies]
anyhow = "1.0.66"
criterion = "0.4.0"
[[bench]]
name = "data"
harness = false
+21
View File
@@ -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.
+21 -16
View File
@@ -4,29 +4,34 @@ World of Tanks Blitz replay parser in Rust.
[![Crates.io](https://img.shields.io/crates/v/wotbreplay-parser)](https://crates.io/crates/wotbreplay-parser)
[![Last commit](https://img.shields.io/github/last-commit/eigenein/wotbreplay-parser)](https://github.com/eigenein/wotbreplay-parser/commits/main)
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/eigenein/wotbreplay-parser/Check)](https://github.com/eigenein/wotbreplay-parser/actions)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/eigenein/wotbreplay-parser/check.yaml)](https://github.com/eigenein/wotbreplay-parser/actions)
![License: MIT](https://img.shields.io/crates/l/wotbreplay-parser)
[![docs.rs](https://img.shields.io/docsrs/wotbreplay-parser)](https://docs.rs/wotbreplay-parser)
## Replay structure
## Quickstart
`*.wotbreplay` is a ZIP-archive containing:
- `battle_results.dat`
- `meta.json`
- `data.wotreplay`
```rust
use std::fs::File;
### `battle_results.dat`
use anyhow::Result;
use wotbreplay_parser::models::battle_results::TeamNumber;
use wotbreplay_parser::replay::Replay;
This is a [pickled](https://docs.python.org/3/library/pickle.html) 2-tuple:
- Some big number
- Battle results serialized with [Protocol Buffers](https://developers.google.com/protocol-buffers)
fn main() -> Result<()> {
let battle_results = Replay::open(File::open("replays/20221203_player_results.wotbreplay")?)?.read_battle_results()?;
#### Useful tools
assert_eq!(battle_results.timestamp_secs, 1670083956);
assert_eq!(battle_results.players.len(), 14);
- [Protobuf Decoder](https://protobuf-decoder.netlify.app/)
- [`protobuf-inspector`](https://github.com/mildsunrise/protobuf-inspector)
- [Hex Fiend](https://hexfiend.com/)
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));
### `meta.json`
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));
### `data.wotreplay`
Ok(())
}
```
+20
View File
@@ -0,0 +1,20 @@
use std::fs::File;
use criterion::{criterion_group, criterion_main, Criterion};
use wotbreplay_parser::replay::Replay;
fn bench_read_data(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("read_data");
for path in [
"replays/20230429_0126__helaas_pindakaas.wotbreplay",
"replays/20230503_0027__helaas_pindakaas.wotbreplay",
"replays/20230429_1682194799_malinovka.wotbreplay",
] {
let mut replay = Replay::open(File::open(path).unwrap()).unwrap();
group.bench_function(path, |bencher| bencher.iter(|| replay.read_data().unwrap()));
}
group.finish();
}
criterion_group!(benches, bench_read_data);
criterion_main!(benches);
+1
View File
@@ -0,0 +1 @@
*.wotbreplay linguist-generated
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -8,3 +8,4 @@ reorder_impl_items = true
unstable_features = true
use_field_init_shorthand = true
version = "Two"
struct_lit_width = 35
+24 -10
View File
@@ -2,18 +2,32 @@ 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: {actual:#X}, expected: {expected:#X}")]
InvalidMagic { expected: u32, actual: u32 },
#[error("failed to parse type {type_} packet's payload at replay time {clock_secs}s")]
PacketPayloadParsingError {
source: Box<Error>,
type_: u32,
clock_secs: f32,
},
}
+28 -4
View File
@@ -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"));
}
+6 -31
View File
@@ -1,31 +1,6 @@
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 map_id;
pub mod meta;
pub mod room_type;
+255
View File
@@ -0,0 +1,255 @@
use prost::bytes::Buf;
use prost::{Enumeration, Message};
use serde::Serialize;
use crate::models::room_type::RoomType;
use crate::result::Result;
impl BattleResults {
/// Parse battle results from the buffer.
///
/// # Note
///
/// This can **not** parse `battle_results.dat` itself, but the un-pickled tuple element.
/// To parse `battle_results.dat`, use [`crate::models::battle_results_dat::BattleResultsDat`].
pub fn from_buffer(buffer: impl Buf) -> Result<Self> {
Ok(Self::decode(buffer)?)
}
}
/// Parsed battle results.
///
/// TODO: 150[0].21[0].8[0].801[0].unsigned is arena end time.
#[derive(Message, Serialize)]
pub struct BattleResults {
/// The two least-significant bytes is map ID.
///
/// The 3rd byte is probably a game mode:
///
/// - `0x0xxxx` is «encounter»
/// - `0x1xxxx` flag is «supremacy»
#[prost(uint32, tag = "1")]
pub mode_map_id: u32,
/// Battle timestamp.
#[prost(int64, tag = "2")]
pub timestamp_secs: i64,
#[prost(enumeration = "TeamNumber", optional, tag = "3")]
pub winner_team_number: Option<i32>,
/// Replay's author results.
#[prost(message, required, tag = "8")]
pub author: Author,
#[prost(enumeration = "RoomType", tag = "9")]
pub room_type: i32,
/// Author's free XP, including premium.
#[prost(uint32, tag = "137")]
pub free_xp: u32,
/// Players in the battle.
#[prost(message, repeated, tag = "201")]
pub players: Vec<Player>,
/// Individual 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 the 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,
#[prost(uint32, optional, tag = "9")]
pub rank: Option<u32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Enumeration, Serialize)]
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.
///
/// The display rating is calculated as: `3000.0 + mm_rating * 10.0`.
#[prost(float, optional, tag = "107")]
pub mm_rating: Option<f32>,
#[prost(uint32, tag = "117")]
pub damage_blocked: u32,
}
impl PlayerResultsInfo {
/// Calculate displayed rating according to the game client.
pub fn display_rating(&self) -> Option<u32> {
self.mm_rating
.map(|mm_rating| (mm_rating * 10.0 + 3000.0) as u32)
}
}
#[derive(Message, Serialize)]
pub struct Author {
/// # Note
///
/// The field is `-2` if the player is auto-destroyed by inactivity.
#[prost(int32, tag = "1")]
pub hitpoints_left: i32,
/// Earned credits, including premium and special awards and medals.
#[prost(uint32, tag = "2")]
pub total_credits: u32,
/// Combat XP, including premium.
#[prost(uint32, tag = "3")]
pub total_xp: u32,
#[prost(uint32, tag = "4")]
pub n_shots: u32,
#[prost(uint32, tag = "5")]
pub n_hits: u32,
#[prost(uint32, tag = "6")]
pub n_splashes: u32,
#[prost(uint32, tag = "7")]
pub n_penetrations: u32,
#[prost(uint32, tag = "8")]
pub damage_dealt: u32,
#[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,
}
+51
View File
@@ -0,0 +1,51 @@
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;
/// Un-pickled `battle_results.dat`.
///
/// # `battle_results.dat` structure
///
/// Entire file is a [pickled](https://docs.python.org/3/library/pickle.html) 2-tuple:
///
/// - Arena unique ID
/// - Battle results serialized with [Protocol Buffers](https://developers.google.com/protocol-buffers)
#[derive(Debug, Deserialize)]
#[serde(from = "(u64, ByteBuf)")]
pub struct BattleResultsDat {
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 {
/// Parse the pickled structure from the reader which contains `battle_results.dat`.
///
/// It can be used either on the `battle_results.dat` for a replay archive,
/// or directly on `**/DAVAProject/battle_results/*/*_full.dat`.
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;
/// Decode the battle results from the internal buffer.
fn try_into(self) -> Result<BattleResults> {
Ok(BattleResults::decode(self.buffer.as_ref())?)
}
}
+101
View File
@@ -0,0 +1,101 @@
//! Models for `data.wotreplay` contents.
pub mod base_player_create;
pub mod entity_method;
pub mod packet;
pub mod payload;
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: actual.into(),
expected: 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![0; length];
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![0; length];
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.
///
/// # Context
///
/// It seems that for payloads larger than 254 bytes, Wargaming writes `00 <XX XX> FF`,
/// where the word in the middle is the actual length of a payload, and the `00` and `FF`
/// are magic values.
#[inline]
fn read_quirky_length(reader: &mut impl Read) -> Result<usize> {
match reader.read_u8()? {
0xFF => {
let length = reader.read_u16::<LittleEndian>()?;
assert_magic(reader.read_u8()?, 0x00)?;
Ok(length as usize)
}
length => Ok(length as usize),
}
}
+84
View File
@@ -0,0 +1,84 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct BasePlayerCreate {
#[serde(default, rename(deserialize = "playersBattleCategoriesIds"))]
pub players_battle_categories_ids: Option<HashMap<u64, (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,
/// Average team ratings for rating battles.
///
/// # Contains
///
/// - [`Some`] 2-vector, if the attribute is present in the replay. Each element represents one team:
/// - [`Some`] rating, if known
/// - [`None`], if the average rating is unknown (all the players are calibrating)
/// - [`None`], if the attribute is missing in the replay
#[serde(default, rename(deserialize = "avgMmr"))]
pub average_mm_ratings: Option<Vec<Option<f64>>>,
#[serde(default, rename(deserialize = "playerWaitTimes"))]
pub player_wait_times: Option<HashMap<u64, f64>>,
#[serde(rename(deserialize = "accountDatabaseIds"))]
pub account_database_ids: Option<Vec<u64>>,
#[serde(default, rename(deserialize = "turboBattlesStats"))]
pub turbo_battles_statistics: Option<TurboBattlesStatistics>,
}
/// I don't know what that is:
///
/// - On EU, Asia, and NA the values are just constants
/// - On Russia, the `alpha` and `beta` are different constants, and the chances actually differ for different replays
#[derive(Debug, Serialize, Deserialize)]
pub struct TurboBattlesStatistics {
#[serde(rename(deserialize = "turboBattleChance"))]
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,
}
+52
View File
@@ -0,0 +1,52 @@
//! Structures for «entity method» call packets.
use byteorder::{LittleEndian, ReadBytesExt};
use prost::encoding::decode_varint;
use prost::Message;
use serde::Serialize;
use serde_with::serde_as;
use self::update_arena::UpdateArena;
use crate::models::data::read_quirky_length;
use crate::result::Result;
pub mod update_arena;
#[serde_as]
#[derive(Debug, Serialize)]
pub enum EntityMethod {
/// Subtype 47.
UpdateArena {
field_number: u64,
arguments: UpdateArena,
},
/// Default variant when subtype is not known by the parser.
Unknown { sub_type: u32 },
}
impl EntityMethod {
/// Parse the entity method raw payload.
pub fn new(raw_payload: &[u8]) -> Result<Self> {
let mut raw_payload = raw_payload;
raw_payload.read_u32::<LittleEndian>()?;
let sub_type = raw_payload.read_u32::<LittleEndian>()?;
let this = match sub_type {
47 => {
let _inner_length = raw_payload.read_u32::<LittleEndian>()?;
let field_number = decode_varint(&mut raw_payload)?;
let message_length = read_quirky_length(&mut raw_payload)?;
Self::UpdateArena {
field_number,
arguments: UpdateArena::decode(&raw_payload[..message_length])?,
}
}
_ => Self::Unknown { sub_type },
};
Ok(this)
}
}
@@ -0,0 +1,32 @@
use prost::Message;
use serde::Serialize;
#[derive(Message, Serialize)]
pub struct UpdateArena {
#[prost(message, tag = "1")]
pub players: Option<Players>,
}
#[derive(Message, Serialize)]
pub struct Players {
#[prost(message, repeated, tag = "1")]
pub players: Vec<Player>,
}
#[derive(Message, Serialize)]
pub struct Player {
#[prost(string, required, tag = "3")]
pub nickname: String,
#[prost(uint32, required, tag = "4")]
pub team_number: u32,
#[prost(uint32, required, tag = "7")]
pub account_id: u32,
#[prost(string, optional, tag = "8")]
pub clan_tag: Option<String>,
#[prost(uint32, optional, tag = "11")]
pub platoon_number: Option<u32>,
}
+66
View File
@@ -0,0 +1,66 @@
use std::io::Read;
use byteorder::{LittleEndian, ReadBytesExt};
use serde::Serialize;
use serde_with::serde_as;
use crate::models::data::payload::Payload;
use crate::result::Result;
use crate::Error;
/// «Packet» is a single traffic message in a replay,
/// packets tell us what is going on during the battle.
#[serde_as]
#[derive(Debug, Serialize)]
pub struct Packet {
/// Replay time, starting from zero.
#[serde(rename = "clock")]
pub clock_secs: f32,
/// Parsed packet payload.
pub payload: Payload,
/// Raw packet payload, excluding the clock and packet type, and including optional subtype.
///
/// This attribute requires the `raw-payload` feature.
#[cfg(feature = "raw-payload")]
#[serde_as(as = "serde_with::hex::Hex")]
pub raw_payload: Vec<u8>,
}
impl Packet {
/// Parse next packet from the reader.
///
/// # Returns
///
/// Parsed packet, or [`None`], when no packets are left.
pub fn from_reader(reader: &mut impl Read) -> Result<Option<Self>> {
let Ok(length) = reader.read_u32::<LittleEndian>() else {
return Ok(None);
};
let type_ = reader.read_u32::<LittleEndian>()?;
let clock_secs = reader.read_f32::<LittleEndian>()?;
let raw_payload = Self::read_raw_payload(reader, length as usize)?;
let payload = Payload::new(type_, &raw_payload).map_err(|source| {
Error::PacketPayloadParsingError {
source: Box::new(source),
type_,
clock_secs,
}
})?;
let this = Self {
clock_secs,
payload,
#[cfg(feature = "raw-payload")]
raw_payload,
};
Ok(Some(this))
}
fn read_raw_payload(reader: &mut impl Read, length: usize) -> Result<Vec<u8>> {
let mut buffer = vec![0; length];
reader.read_exact(&mut buffer)?;
Ok(buffer)
}
}
+61
View File
@@ -0,0 +1,61 @@
use std::io::Read;
use byteorder::{LittleEndian, ReadBytesExt};
use serde::Serialize;
use serde_with::serde_as;
use crate::models::data::base_player_create::BasePlayerCreate;
use crate::models::data::entity_method::EntityMethod;
use crate::models::data::{read_pickled, read_quirky_length, read_string};
use crate::result::Result;
/// Packet payload.
#[serde_as]
#[derive(Debug, Serialize)]
pub enum Payload {
/// Type 0.
BasePlayerCreate {
author_nickname: String,
arena_unique_id: u64,
arena_type_id: u32,
arguments: Box<BasePlayerCreate>,
},
/// Type 8.
EntityMethod(EntityMethod),
/// Default payload when type is not known by the parser.
Unknown { packet_type: u32 },
}
impl Payload {
/// Parse the raw packet payload.
pub fn new(packet_type: u32, raw_payload: &[u8]) -> Result<Self> {
let mut raw_payload = raw_payload;
let this = match packet_type {
0 => {
raw_payload.read_exact(&mut [0; 10])?;
let author_nickname = read_string(&mut raw_payload)?;
let arena_unique_id = raw_payload.read_u64::<LittleEndian>()?;
let arena_type_id = raw_payload.read_u32::<LittleEndian>()?;
let arguments = {
let pickled_length = read_quirky_length(&mut raw_payload)?;
read_pickled(&mut raw_payload, pickled_length)?
};
Self::BasePlayerCreate {
arguments,
author_nickname,
arena_unique_id,
arena_type_id,
}
}
8 => Self::EntityMethod(EntityMethod::new(raw_payload)?),
_ => Self::Unknown { packet_type },
};
Ok(this)
}
}
+38
View File
@@ -0,0 +1,38 @@
use prost::Enumeration;
use serde_repr::{Deserialize_repr, Serialize_repr};
#[derive(Debug, Serialize_repr, Deserialize_repr, Enumeration, Ord, PartialOrd, Eq, PartialEq)]
#[serde(untagged)]
#[repr(u16)]
pub enum MapId {
DesertSands = 2,
Middleburg = 3,
Copperfield = 4,
Alpenstadt = 5,
Mines = 6,
DeadRail = 7,
FortDespair = 8,
Himmelsdorf = 9,
BlackGoldville = 10,
OasisPalms = 11,
GhostFactory = 12,
Molendijk = 14,
PortBay = 15,
WinterMalinovka = 19,
Castilla = 20,
Canal = 21,
Vineyards = 23,
YamatoHarbor = 25,
Canyon = 27,
MayanRuins = 30,
DynastyPearl = 31,
NavalFrontier = 35,
FallsCreek = 38,
NewBay = 40,
Normandy = 42,
Wasteland = 71,
/// Default value for Protocol Buffers and fallback value for Serde.
#[serde(deserialize_with = "serde_with::rust::deserialize_ignore_any")]
Undefined = 0,
}
+34
View File
@@ -0,0 +1,34 @@
//! Models for `meta.json` contents.
#![cfg(feature = "meta")]
use std::io::Read;
use serde::Deserialize;
use crate::models::map_id::MapId;
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,
#[serde(rename = "mapId")]
pub map_id: MapId,
}
impl Meta {
pub fn from_reader(reader: impl Read) -> Result<Self> {
Ok(serde_json::from_reader(reader)?)
}
}
+18
View File
@@ -0,0 +1,18 @@
use prost::Enumeration;
use serde::Serialize;
#[derive(Debug, Enumeration, Serialize, Ord, PartialOrd, Eq, PartialEq)]
pub enum RoomType {
Any = 0,
Regular = 1,
TrainingRoom = 2,
Tournament = 4,
QuickTournament = 5,
Rating = 7,
MadGames = 8,
RealisticBattles = 22,
Uprising = 23,
GravityMode = 24,
Skirmish = 25,
BurningGames = 26,
}
+35 -20
View File
@@ -1,35 +1,50 @@
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;
/// World of Tanks Blitz replay.
///
/// # Replay structure
///
/// `*.wotbreplay` is a ZIP-archive containing:
///
/// - `battle_results.dat`
/// - `meta.json`
/// - `data.wotreplay`
pub struct Replay<R>(pub(crate) ZipArchive<R>);
impl<R: Read + Seek> Replay<R> {
/// Opens a replay from the reader.
/// Open 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)?))
}
/// Read and parse the full battle results structure from `battle_results.dat`.
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()
}
/// Read and parse the root 2-tuple from `battle_results.dat`.
pub fn read_battle_results_dat(&mut self) -> Result<BattleResultsDat> {
let pickled_battle_results = self.0.by_name("battle_results.dat")?;
BattleResultsDat::from_reader(pickled_battle_results)
}
/// Read and parse the included `meta.json`.
#[cfg(feature = "meta")]
pub fn read_meta(&mut self) -> Result<Meta> {
Meta::from_reader(self.0.by_name("meta.json")?)
}
/// Read and parse the included `data.wotreplay`.
pub fn read_data(&mut self) -> Result<Data> {
Data::from_reader(self.0.by_name("data.wotreplay")?)
}
}
+152 -15
View File
@@ -1,26 +1,163 @@
use std::fs::File;
use anyhow::Result;
use wotbreplay_parser::Replay;
use wotbreplay_parser::models::battle_results::BattleResults;
use wotbreplay_parser::models::room_type::RoomType;
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("replays/20221203_player_results.wotbreplay")?)?
.read_battle_results_dat()?;
assert_eq!(battle_results_dat.arena_unique_id, 1661909200500084);
let battle_results: BattleResults = battle_results_dat.try_into()?;
assert_eq!(battle_results.timestamp, 1670018359);
assert_eq!(battle_results.players.len(), 14);
assert_eq!(battle_results.winner_team_number, Some(2));
assert_eq!(battle_results.room_type(), RoomType::Regular);
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.total_credits, 59518);
assert_eq!(battle_results.author.total_xp, 5255);
assert_eq!(battle_results.author.damage_dealt, 1438);
assert_eq!(battle_results.author.account_id, 594778041);
assert_eq!(battle_results.free_xp, 260);
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("replays/20221203_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("replays/20221203_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 draw_ok() -> Result<()> {
let battle_results =
Replay::open(File::open("replays/20230503_0027__helaas_pindakaas.wotbreplay")?)?
.read_battle_results()?;
assert_eq!(battle_results.winner_team_number, None);
Ok(())
}
#[test]
fn room_type_ok() -> Result<()> {
let battle_results = Replay::open(File::open(
"replays/20230512_2150__helaas_pindakaas_R159_SU_130PM_15146680594634860.wotbreplay",
)?)?
.read_battle_results()?;
assert_eq!(battle_results.room_type(), RoomType::MadGames);
Ok(())
}
/// Training room on Strv K with a rank.
#[test]
fn rank_ok() -> Result<()> {
let battle_results = Replay::open(File::open(
"replays/20230802_2133__helaas_pindakaas_S31_Strv_K_15193856522497923.wotbreplay",
)?)?
.read_battle_results()?;
assert_eq!(battle_results.players[0].info.rank, Some(12));
Ok(())
}
/// Training room on Regressor with no rank.
#[test]
fn no_rank_ok() -> Result<()> {
let battle_results = Replay::open(File::open(
"replays/20230802_2136__helaas_pindakaas_Oth38_50TP_Tyszkiewicza_S1_15188303129784363.wotbreplay",
)?)?
.read_battle_results()?;
assert_eq!(battle_results.players[0].info.rank, None);
Ok(())
}
+100
View File
@@ -0,0 +1,100 @@
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("replays/20230429_0126__helaas_pindakaas.wotbreplay")?)?;
let data = replay.read_data()?;
assert_eq!(data.client_version, "9.8.5_apple");
assert_eq!(data.packets.len(), 60139);
Ok(())
}
#[test]
fn base_player_create_ok() -> Result<()> {
let mut replay =
Replay::open(File::open("replays/20230429_0126__helaas_pindakaas.wotbreplay")?)?;
let data = replay.read_data()?;
let (author_nickname, arena_unique_id, arena_type_id, arguments) = data
.packets
.iter()
.filter_map(|packet| {
if let Payload::BasePlayerCreate {
author_nickname,
arena_unique_id,
arena_type_id,
arguments: type_0,
} = &packet.payload
{
Some((author_nickname, arena_unique_id, arena_type_id, type_0))
} else {
None
}
})
.next()
.unwrap();
assert_eq!(author_nickname, "helaas_pindakaas");
assert_eq!(*arena_unique_id, 16114615898101270);
assert_eq!(*arena_type_id, 43);
assert_eq!(arguments.battle_level, 9);
assert_eq!(arguments.battle_category_id, 13);
assert!(arguments.is_mouse_enabled);
assert_eq!(arguments.matchmaker_type, 2);
assert_eq!(arguments.camouflage_slot, 1);
assert_eq!(
arguments.average_mm_ratings,
Some(vec![Some(59.29457753400008), Some(99.34182121604681)])
);
assert_eq!(
arguments.account_database_ids,
Some(vec![
517033644, 537867068, 597583401, 601893865, 576810156, 531053956, 599580824, 558025185,
526927603, 594778041, 505587490, 597422303, 531923111, 587617355
])
);
Ok(())
}
#[test]
fn update_arena_ok() -> Result<()> {
let mut replay =
Replay::open(File::open("replays/20230429_0126__helaas_pindakaas.wotbreplay")?)?;
let data = replay.read_data()?;
let players = data
.packets
.iter()
.filter_map(|packet| match &packet.payload {
Payload::EntityMethod(EntityMethod::UpdateArena { arguments, .. }) => {
arguments.players.as_ref()
}
_ => None,
})
.next()
.unwrap();
assert_eq!(players.players.len(), 14);
assert_eq!(players.players[1].team_number, 1);
assert_eq!(players.players[1].nickname, "Igrok_WoT_blitz");
assert_eq!(players.players[1].account_id, 597583401);
assert_eq!(players.players[1].platoon_number, None);
assert_eq!(players.players[1].clan_tag, Some("F_ANG".to_string()));
Ok(())
}
#[test]
fn training_room_ok() -> Result<()> {
let mut replay = Replay::open(File::open("replays/20230503_training_room.wotbreplay")?)?;
replay.read_data()?;
Ok(())
}
+21
View File
@@ -0,0 +1,21 @@
#![cfg(feature = "meta")]
use std::fs::File;
use anyhow::Result;
use wotbreplay_parser::models::map_id::MapId;
use wotbreplay_parser::replay::Replay;
#[test]
fn meta_ok() -> Result<()> {
let meta =
Replay::open(File::open("replays/20221203_player_results.wotbreplay")?)?.read_meta()?;
assert_eq!(meta.player_name, "zeekrab");
assert_eq!(meta.arena_unique_id, "1661909200500084");
assert_eq!(meta.battle_duration_secs, 164.08485);
assert_eq!(meta.tank_id, 26657);
assert_eq!(meta.map_id, MapId::DynastyPearl);
Ok(())
}