mirror of
https://github.com/eigenein/wotbreplay-parser.git
synced 2026-10-21 18:37:00 +02:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
632f1781e7 | ||
|
|
fa6ac70d21 | ||
|
|
1fb4bea267 | ||
|
|
705280fc19 | ||
|
|
3f557e33bf | ||
|
|
fb5f52038d | ||
|
|
615537ce45 | ||
|
|
8e96f4847b | ||
|
|
14148c7410 | ||
|
|
903cd00f32 | ||
|
|
e0ce6a22b1 |
No files matched your search
@@ -20,37 +20,43 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: 📥 Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up the toolchain
|
||||
- name: ✨ Set up the toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Rust Cache
|
||||
- name: ⚡️ Rust Cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
with:
|
||||
cache-on-failure: true
|
||||
|
||||
- name: Test
|
||||
- name: 🧪 Test without optional features
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --doc
|
||||
args: --all-targets
|
||||
|
||||
- name: Clippy
|
||||
- name: 🧪 Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features
|
||||
|
||||
- name: 🪄 Clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-targets --all-features --tests -- -D warnings
|
||||
|
||||
- name: Publish the crates (dry run)
|
||||
- name: 📤 Publish the crates (dry run)
|
||||
run: |
|
||||
cargo publish --allow-dirty --dry-run
|
||||
env:
|
||||
|
||||
@@ -15,12 +15,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
- name: 📥 Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set the versions
|
||||
- name: 🪄 Set the versions
|
||||
run: |
|
||||
sed -i -e 's/version = "0\.0\.0"/version = "${{ github.ref_name }}"/g' Cargo.toml
|
||||
- name: Rustup
|
||||
@@ -29,13 +29,13 @@ jobs:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
|
||||
- name: Build the changelog
|
||||
- name: 📝 Build the changelog
|
||||
id: changelog
|
||||
uses: Bullrich/generate-release-changelog@master
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
|
||||
- name: Update the release
|
||||
- name: 🪄 Update the release
|
||||
uses: tubone24/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
release_name: ${{ github.ref_name }}
|
||||
body: ${{ steps.changelog.outputs.changelog }}
|
||||
|
||||
- name: Publish the crates
|
||||
- name: 📤 Publish the crates
|
||||
run: |
|
||||
cargo publish --allow-dirty
|
||||
env:
|
||||
|
||||
@@ -12,12 +12,17 @@ categories = ["parsing"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
meta = ["dep:serde_json"]
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.4.0"
|
||||
byteorder = "1.4.3"
|
||||
prost = "0.11.3"
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_bytes = "0.11.7"
|
||||
serde-pickle = "1.1.1"
|
||||
serde_json = { version = "1.0.96", optional = true }
|
||||
serde_with = { version = "2.3.3", features = ["hex"] }
|
||||
thiserror = "1.0.37"
|
||||
zip = { version = "0.6.3", default-features = false, features = ["deflate"] }
|
||||
|
||||
|
||||
@@ -14,23 +14,24 @@ World of Tanks Blitz replay parser in Rust.
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use wotbreplay_parser::prelude::*;
|
||||
use wotbreplay_parser::models::battle_results::TeamNumber;
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/battle_results.wotbreplay")?)?
|
||||
let battle_results = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
|
||||
assert_eq!(battle_results.timestamp, 1670018359);
|
||||
assert_eq!(battle_results.timestamp, 1670083956);
|
||||
assert_eq!(battle_results.players.len(), 14);
|
||||
|
||||
assert_eq!(battle_results.players[0].account_id, 520886428);
|
||||
assert_eq!(battle_results.players[0].info.nickname, "77mmmr");
|
||||
assert_eq!(battle_results.players[0].info.team(), TeamNumber::Two);
|
||||
assert_eq!(battle_results.players[0].info.platoon_id, None);
|
||||
assert_eq!(battle_results.players[0].account_id, 595693744);
|
||||
assert_eq!(battle_results.players[0].info.nickname, "yuranhik_hustriy26");
|
||||
assert_eq!(battle_results.players[0].info.team(), TeamNumber::One);
|
||||
assert_eq!(battle_results.players[0].info.platoon_id, Some(545104609));
|
||||
|
||||
assert_eq!(battle_results.players[1].info.nickname, "SNAK_THE_RIPPER");
|
||||
assert_eq!(battle_results.players[1].info.team(), TeamNumber::One);
|
||||
assert_eq!(battle_results.players[1].info.platoon_id, Some(547466834));
|
||||
assert_eq!(battle_results.players[1].info.team(), TeamNumber::Two);
|
||||
assert_eq!(battle_results.players[1].info.platoon_id, Some(273692628));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+20
-10
@@ -2,18 +2,28 @@ use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("failed to open the ZIP-archive")]
|
||||
OpenArchiveFailed(#[source] zip::result::ZipError),
|
||||
#[error("ZIP error")]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
|
||||
#[error("failed to open the battle results entry in the replay archive")]
|
||||
OpenBattleResultsFailed(#[source] zip::result::ZipError),
|
||||
#[error("Protocol Buffers error")]
|
||||
ProtobufError(#[from] prost::DecodeError),
|
||||
|
||||
#[error("failed to read the archive entry")]
|
||||
ReadEntryFailed(#[source] std::io::Error),
|
||||
#[error("Pickle error")]
|
||||
PickleError(#[from] serde_pickle::Error),
|
||||
|
||||
#[error("failed to decode")]
|
||||
DecodeFailed(#[source] prost::DecodeError),
|
||||
#[error("I/O error")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("failed to un-pickle")]
|
||||
UnpickleFailed(#[source] serde_pickle::Error),
|
||||
#[error("UTF-8 error")]
|
||||
StringDecodeError(#[from] std::string::FromUtf8Error),
|
||||
|
||||
#[cfg(feature = "meta")]
|
||||
#[error("JSON error")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
|
||||
#[error("invalid magic: {0:#X}, expected: {1:#X}")]
|
||||
InvalidMagic(u32, u32),
|
||||
|
||||
#[error("failed to read a packet length, got {0} bytes")]
|
||||
PacketLengthError(usize),
|
||||
}
|
||||
+5
-3
@@ -12,11 +12,13 @@
|
||||
)]
|
||||
|
||||
mod error;
|
||||
mod models;
|
||||
pub mod prelude;
|
||||
mod replay;
|
||||
pub mod models;
|
||||
pub mod replay;
|
||||
mod result;
|
||||
|
||||
pub use self::error::Error;
|
||||
pub use self::result::Result;
|
||||
|
||||
#[cfg(doctest)]
|
||||
mod test_readme {
|
||||
macro_rules! external_doc_test {
|
||||
|
||||
+4
-5
@@ -1,5 +1,4 @@
|
||||
mod battle_results;
|
||||
mod battle_results_dat;
|
||||
|
||||
pub use self::battle_results::*;
|
||||
pub use self::battle_results_dat::*;
|
||||
pub mod battle_results;
|
||||
pub mod battle_results_dat;
|
||||
pub mod data;
|
||||
pub mod meta;
|
||||
@@ -2,7 +2,6 @@ use prost::bytes::Buf;
|
||||
use prost::Message;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::result::Result;
|
||||
|
||||
impl BattleResults {
|
||||
@@ -10,11 +9,13 @@ impl BattleResults {
|
||||
///
|
||||
/// Note, that this does NOT parse `battle_results.dat` itself, but the un-pickled tuple element.
|
||||
pub fn from_buffer(buffer: impl Buf) -> Result<Self> {
|
||||
Self::decode(buffer).map_err(Error::DecodeFailed)
|
||||
Ok(Self::decode(buffer)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed battle results.
|
||||
///
|
||||
/// TODO: 150[0].21[0].8[0].801[0].unsigned is arena end time.
|
||||
#[derive(Message, Serialize)]
|
||||
pub struct BattleResults {
|
||||
/// Battle timestamp.
|
||||
@@ -22,7 +23,7 @@ pub struct BattleResults {
|
||||
pub timestamp: i64,
|
||||
|
||||
#[prost(enumeration = "TeamNumber", tag = "3")]
|
||||
pub winning_team: i32,
|
||||
pub winner_team_number: i32,
|
||||
|
||||
/// Replay's author results.
|
||||
#[prost(message, required, tag = "8")]
|
||||
|
||||
@@ -5,36 +5,43 @@ use serde::Deserialize;
|
||||
use serde_bytes::ByteBuf;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::models::BattleResults;
|
||||
use crate::models::battle_results::BattleResults;
|
||||
use crate::result::Result;
|
||||
|
||||
/// Represents un-pickled `battle_results.dat`.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BattleResultsDat(
|
||||
/// No idea what this is.
|
||||
/// My best guess – some kind of checksum,
|
||||
/// but none of MD5, SHA1, SHA224, SHA256, SHA384, SHA512, nor CRC64 over the buffer have matched.
|
||||
pub u64,
|
||||
#[serde(from = "(u64, ByteBuf)")]
|
||||
pub struct BattleResultsDat {
|
||||
pub arena_unique_id: u64,
|
||||
|
||||
/// Protobuf-serialized battle results.
|
||||
pub 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 {
|
||||
/// 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> {
|
||||
serde_pickle::from_reader(reader, Default::default()).map_err(Error::UnpickleFailed)
|
||||
}
|
||||
|
||||
/// Decodes the battle results from the internal buffer.
|
||||
pub fn decode_battle_results(&self) -> Result<BattleResults> {
|
||||
BattleResults::decode(self.1.as_ref()).map_err(Error::DecodeFailed)
|
||||
Ok(serde_pickle::from_reader(reader, Default::default())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<BattleResults> for BattleResultsDat {
|
||||
type Error = Error;
|
||||
|
||||
/// Decodes the battle results from the internal buffer.
|
||||
fn try_into(self) -> Result<BattleResults> {
|
||||
self.decode_battle_results()
|
||||
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,36 @@
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use serde::Serialize;
|
||||
use serde_with::serde_as;
|
||||
|
||||
use crate::result::Result;
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum EntityMethod {
|
||||
/// TODO: team composition.
|
||||
Subtype2F,
|
||||
|
||||
/// Default variant when subtype is not known.
|
||||
Unknown {
|
||||
sub_type: u32,
|
||||
|
||||
/// Whole packet payload (including the sub-type).
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl EntityMethod {
|
||||
pub fn new(payload: Vec<u8>) -> Result<Self> {
|
||||
let mut reader = payload.as_slice();
|
||||
|
||||
reader.read_u32::<LittleEndian>()?;
|
||||
let sub_type = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let this = match sub_type {
|
||||
0x2F => 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,60 @@
|
||||
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(Box<ReplayHeader>),
|
||||
EntityMethod(EntityMethod),
|
||||
|
||||
/// Default payload when type is not known.
|
||||
Unknown {
|
||||
type_: u32,
|
||||
|
||||
/// Whole packet payload.
|
||||
#[serde_as(as = "serde_with::hex::Hex")]
|
||||
raw: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Payload {
|
||||
pub fn new(type_: u32, payload: Vec<u8>) -> Result<Self> {
|
||||
let this = match type_ {
|
||||
0 => {
|
||||
let mut reader = payload.as_slice();
|
||||
reader.read_exact(&mut [0; 10])?;
|
||||
|
||||
let author_nickname = read_string(&mut reader)?;
|
||||
let arena_unique_id = reader.read_u64::<LittleEndian>()?;
|
||||
let arena_type_id = reader.read_u32::<LittleEndian>()?;
|
||||
let 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(Box::new(ReplayHeader {
|
||||
summary,
|
||||
author_nickname,
|
||||
arena_unique_id,
|
||||
arena_type_id,
|
||||
}))
|
||||
}
|
||||
|
||||
8 => Self::EntityMethod(EntityMethod::new(payload)?),
|
||||
|
||||
_ => Self::Unknown {
|
||||
raw: payload,
|
||||
type_,
|
||||
},
|
||||
};
|
||||
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,72 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Summary {
|
||||
#[serde(rename(deserialize = "playersBattleCategoriesIds"))]
|
||||
pub players_battle_categories_ids: HashMap<u32, (u8, u32)>,
|
||||
|
||||
#[serde(rename(deserialize = "battleLevel"))]
|
||||
pub battle_level: u8,
|
||||
|
||||
#[serde(rename(deserialize = "battleCategoryId"))]
|
||||
pub battle_category_id: u8,
|
||||
|
||||
#[serde(rename(deserialize = "mouseEnabled"))]
|
||||
pub is_mouse_enabled: bool,
|
||||
|
||||
#[serde(rename(deserialize = "mmType"))]
|
||||
pub matchmaker_type: u8,
|
||||
|
||||
#[serde(rename(deserialize = "camouflageSlot"))]
|
||||
pub camouflage_slot: u8,
|
||||
|
||||
#[serde(rename(deserialize = "avgMmr"))]
|
||||
pub average_mmr: Vec<Option<f64>>,
|
||||
|
||||
#[serde(rename(deserialize = "playerWaitTimes"))]
|
||||
pub player_wait_times: HashMap<u32, f64>,
|
||||
|
||||
#[serde(rename(deserialize = "accountDatabaseIds"))]
|
||||
pub account_database_ids: Vec<u32>,
|
||||
|
||||
#[serde(rename(deserialize = "turboBattlesStats"))]
|
||||
pub turbo_battles_statistics: TurboBattlesStatistics,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct TurboBattlesStatistics {
|
||||
#[serde(rename(deserialize = "turboBattleChance"))]
|
||||
pub battle_chance: f64,
|
||||
|
||||
#[serde(rename(deserialize = "turboLeaveChance"))]
|
||||
pub leave_chance: f64,
|
||||
|
||||
#[serde(rename(deserialize = "possibleTurboBattlesNotForSkip"))]
|
||||
pub possible_battles_not_for_skip: u32,
|
||||
|
||||
#[serde(rename(deserialize = "possibleTurboBattlesForSkip"))]
|
||||
pub possible_battles_for_skip: u32,
|
||||
|
||||
#[serde(rename(deserialize = "turboBattlesForSkip"))]
|
||||
pub battles_for_skip: u32,
|
||||
|
||||
#[serde(rename(deserialize = "turboBattlesNotForSkip"))]
|
||||
pub battles_not_for_skip: u32,
|
||||
|
||||
#[serde(rename(deserialize = "abSkip"))]
|
||||
pub ab_skip: i32,
|
||||
|
||||
#[serde(rename(deserialize = "beta"))]
|
||||
pub beta: f64,
|
||||
|
||||
#[serde(rename(deserialize = "premiumVehDiff"))]
|
||||
pub premium_vehicle_difference: i32,
|
||||
|
||||
#[serde(rename(deserialize = "htDiff"))]
|
||||
pub ht_difference: i32,
|
||||
|
||||
#[serde(rename(deserialize = "alpha"))]
|
||||
pub alpha: f64,
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#![cfg(feature = "meta")]
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::result::Result;
|
||||
|
||||
/// The model for `meta.json`.
|
||||
#[derive(Deserialize)]
|
||||
pub struct Meta {
|
||||
#[serde(rename = "playerName")]
|
||||
pub player_name: String,
|
||||
|
||||
#[serde(rename = "arenaUniqueId")]
|
||||
pub arena_unique_id: String,
|
||||
|
||||
#[serde(rename = "battleDuration")]
|
||||
pub battle_duration_secs: f64,
|
||||
|
||||
#[serde(rename = "vehicleCompDescriptor")]
|
||||
pub tank_id: u16,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
pub fn from_reader(reader: impl Read) -> Result<Self> {
|
||||
Ok(serde_json::from_reader(reader)?)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub use crate::error::Error;
|
||||
pub use crate::models::*;
|
||||
pub use crate::replay::Replay;
|
||||
pub use crate::result::Result;
|
||||
+17
-10
@@ -2,8 +2,11 @@ use std::io::{Read, Seek};
|
||||
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::models::{BattleResults, BattleResultsDat};
|
||||
use crate::models::battle_results::BattleResults;
|
||||
use crate::models::battle_results_dat::BattleResultsDat;
|
||||
use crate::models::data::Data;
|
||||
#[cfg(feature = "meta")]
|
||||
use crate::models::meta::Meta;
|
||||
use crate::result::Result;
|
||||
|
||||
/// Opened (but not yet parsed) replay.
|
||||
@@ -12,22 +15,26 @@ pub struct Replay<R>(pub(crate) ZipArchive<R>);
|
||||
impl<R: Read + Seek> Replay<R> {
|
||||
/// Opens a replay from the reader which contains a `*.wotbreplay`.
|
||||
pub fn open(reader: R) -> Result<Self> {
|
||||
ZipArchive::new(reader)
|
||||
.map_err(Error::OpenArchiveFailed)
|
||||
.map(Self)
|
||||
Ok(Self(ZipArchive::new(reader)?))
|
||||
}
|
||||
|
||||
/// Reads and parses the battle results from the replay.
|
||||
pub fn read_battle_results(&mut self) -> Result<BattleResults> {
|
||||
self.read_battle_results_dat()?.decode_battle_results()
|
||||
self.read_battle_results_dat()?.try_into()
|
||||
}
|
||||
|
||||
/// Reads and parses the included `battle_results.dat`.
|
||||
pub fn read_battle_results_dat(&mut self) -> Result<BattleResultsDat> {
|
||||
let pickled_battle_results = self
|
||||
.0
|
||||
.by_name("battle_results.dat")
|
||||
.map_err(Error::OpenBattleResultsFailed)?;
|
||||
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")?)
|
||||
}
|
||||
}
|
||||
+15
-27
@@ -1,12 +1,24 @@
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
use wotbreplay_parser::prelude::*;
|
||||
use wotbreplay_parser::models::battle_results::BattleResults;
|
||||
use wotbreplay_parser::replay::Replay;
|
||||
|
||||
#[test]
|
||||
fn player_results_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
let battle_results_dat = Replay::open(File::open("tests/replays/player_results.wotbreplay")?)?
|
||||
.read_battle_results_dat()?;
|
||||
assert_eq!(battle_results_dat.arena_unique_id, 1661909200500084);
|
||||
let battle_results: BattleResults = battle_results_dat.try_into()?;
|
||||
|
||||
assert_eq!(battle_results.winner_team_number, 2);
|
||||
|
||||
assert_eq!(battle_results.author.team_number, 2);
|
||||
assert_eq!(battle_results.author.credits_earned, 59518);
|
||||
assert_eq!(battle_results.author.combat_xp, 5255);
|
||||
assert_eq!(battle_results.author.damage_dealt, 1438);
|
||||
assert_eq!(battle_results.author.account_id, 594778041);
|
||||
assert_eq!(battle_results.free_xp, 260);
|
||||
|
||||
let snak_the_ripper_info = &battle_results.player_results[3].info;
|
||||
assert_eq!(snak_the_ripper_info.base_xp, 929);
|
||||
@@ -105,27 +117,3 @@ fn victory_points_ok() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn winning_team_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/winning_team.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
|
||||
assert_eq!(battle_results.winning_team(), TeamNumber::One);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn author_results_ok() -> Result<()> {
|
||||
let battle_results = Replay::open(File::open("tests/replays/author_results.wotbreplay")?)?
|
||||
.read_battle_results()?;
|
||||
|
||||
assert_eq!(battle_results.author.credits_earned, 53623);
|
||||
assert_eq!(battle_results.author.combat_xp, 2370);
|
||||
assert_eq!(battle_results.author.damage_dealt, 1738);
|
||||
assert_eq!(battle_results.author.account_id, 594778041);
|
||||
assert_eq!(battle_results.free_xp, 116);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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/20230429_0126__helaas_pindakaas_A140_ASTRON_REX_105_16114615898101270.wotbreplay",
|
||||
)?)?;
|
||||
let data = replay.read_data()?;
|
||||
|
||||
assert_eq!(data.client_version, "9.8.5_apple");
|
||||
assert_eq!(data.other_packets.len(), 60138);
|
||||
|
||||
let header = data.replay_header.unwrap();
|
||||
assert_eq!(header.author_nickname, "helaas_pindakaas");
|
||||
assert_eq!(header.arena_unique_id, 16114615898101270);
|
||||
assert_eq!(header.arena_type_id, 43);
|
||||
assert_eq!(header.summary.battle_level, 9);
|
||||
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, [Some(59.29457753400008), Some(99.34182121604681)]);
|
||||
assert_eq!(
|
||||
header.summary.account_database_ids,
|
||||
[
|
||||
517033644, 537867068, 597583401, 601893865, 576810156, 531053956, 599580824, 558025185,
|
||||
526927603, 594778041, 505587490, 597422303, 531923111, 587617355
|
||||
]
|
||||
);
|
||||
|
||||
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(())
|
||||
}
|
||||
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Generated
BIN
Binary file not shown.
Reference in new issue
Block a user