diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 0000000..47f30f1
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,6 @@
+[alias]
+c = "check"
+cc = "clippy"
+u = "update"
+o = "outdated"
+r = "run"
diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml
new file mode 100644
index 0000000..4ac4afe
--- /dev/null
+++ b/.github/workflows/check.yaml
@@ -0,0 +1,51 @@
+name: Check
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+permissions:
+ checks: write
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ check:
+ name: Check
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Set up the toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ components: rustfmt, clippy
+
+ - name: Rust Cache
+ uses: Swatinem/rust-cache@v1
+ with:
+ cache-on-failure: true
+
+ - name: Test
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --doc
+
+ - 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
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
new file mode 100644
index 0000000..741227d
--- /dev/null
+++ b/.github/workflows/publish.yaml
@@ -0,0 +1,52 @@
+ame: Publish
+
+on:
+ release:
+ types: ["published"]
+
+permissions:
+ contents: write
+
+env:
+ CARGO_TERM_COLOR: always
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout the repository
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Set the versions
+ run: |
+ sed -i -e 's/version = "0\.0\.0"/version = "${{ github.ref_name }}"/g' Cargo.toml
+ - name: Rustup
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+
+ - name: Build the changelog
+ id: changelog
+ uses: Bullrich/generate-release-changelog@master
+ env:
+ REPO: ${{ github.repository }}
+
+ - name: Update the release
+ uses: tubone24/update_release@v1.3.1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ release_name: ${{ github.ref_name }}
+ body: ${{ steps.changelog.outputs.changelog }}
+ prerelease: false
+
+ - name: Publish the crates
+ run: |
+ cargo login
+ cargo publish --allow-dirty
+ env:
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..dda922e
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..d56657a
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..a6c2197
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/wotbreplay-parser.iml b/.idea/wotbreplay-parser.iml
new file mode 100644
index 0000000..227e58a
--- /dev/null
+++ b/.idea/wotbreplay-parser.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index 07a18f9..8535a9a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,4 +14,11 @@ categories = []
default = []
[dependencies]
-thiserror = "1.0.31"
+prost = "0.11.3"
+serde_bytes = "0.11.7"
+serde-pickle = "1.1.1"
+thiserror = "1.0.37"
+zip = { version = "0.6.3", default-features = false, features = ["deflate"] }
+
+[dev-dependencies]
+anyhow = "1.0.66"
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..d53ce3e
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,10 @@
+edition = "2021"
+fn_call_width = 80
+format_code_in_doc_comments = true
+format_macro_matchers = true
+group_imports = "StdExternalCrate"
+imports_granularity = "Module"
+reorder_impl_items = true
+unstable_features = true
+use_field_init_shorthand = true
+version = "Two"
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..f919695
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,19 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("failed to open the ZIP-archive")]
+ OpenArchiveFailed(#[source] zip::result::ZipError),
+
+ #[error("failed to open the battle results entry in the replay archive")]
+ OpenBattleResultsFailed(#[source] zip::result::ZipError),
+
+ #[error("failed to read the archive entry")]
+ ReadEntryFailed(#[source] std::io::Error),
+
+ #[error("failed to decode")]
+ DecodeFailed(#[source] prost::DecodeError),
+
+ #[error("failed to un-pickle")]
+ UnpickleFailed(#[source] serde_pickle::Error),
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..ce240eb
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,8 @@
+mod error;
+pub mod models;
+mod replay;
+mod result;
+
+pub use crate::error::Error;
+pub use crate::replay::Replay;
+pub use crate::result::Result;
diff --git a/src/models.rs b/src/models.rs
new file mode 100644
index 0000000..de41171
--- /dev/null
+++ b/src/models.rs
@@ -0,0 +1,31 @@
+use ::prost::Message;
+
+#[derive(Message)]
+pub struct BattleResults {
+ #[prost(int64, tag = "2")]
+ pub timestamp: i64,
+
+ #[prost(message, repeated, tag = "201")]
+ pub players: Vec,
+}
+
+#[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,
+
+ #[prost(uint32, tag = "3")]
+ pub team_number: u32,
+}
diff --git a/src/replay.rs b/src/replay.rs
new file mode 100644
index 0000000..d968ba2
--- /dev/null
+++ b/src/replay.rs
@@ -0,0 +1,35 @@
+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::result::Result;
+
+pub struct Replay(pub(crate) ZipArchive);
+
+impl Replay {
+ /// Opens a replay from the reader.
+ pub fn open(reader: R) -> Result {
+ ZipArchive::new(reader)
+ .map_err(Error::OpenArchiveFailed)
+ .map(Self)
+ }
+
+ pub fn read_battle_results(&mut self) -> Result {
+ 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)
+ }
+}
diff --git a/src/result.rs b/src/result.rs
new file mode 100644
index 0000000..97461f9
--- /dev/null
+++ b/src/result.rs
@@ -0,0 +1,3 @@
+use crate::error::Error;
+
+pub type Result = std::result::Result;
diff --git a/tests/battle_results.rs b/tests/battle_results.rs
new file mode 100644
index 0000000..3a6e419
--- /dev/null
+++ b/tests/battle_results.rs
@@ -0,0 +1,26 @@
+use std::fs::File;
+
+use anyhow::Result;
+use wotbreplay_parser::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()?;
+
+ 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, 2);
+ 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, 1);
+ assert_eq!(battle_results.players[1].info.platoon_id, Some(547466834));
+
+ Ok(())
+}
diff --git a/tests/replays/20221202_2259__zeekrab_It20_Car_Comb_45t_2308705958773814583.wotbreplay b/tests/replays/20221202_2259__zeekrab_It20_Car_Comb_45t_2308705958773814583.wotbreplay
new file mode 100644
index 0000000..fb578fc
Binary files /dev/null and b/tests/replays/20221202_2259__zeekrab_It20_Car_Comb_45t_2308705958773814583.wotbreplay differ