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