Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse Game Type #24

Merged
merged 5 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added replays/automatch.rec
Binary file not shown.
Binary file added replays/custom.rec
Binary file not shown.
Binary file added replays/skirmish.rec
Binary file not shown.
6 changes: 5 additions & 1 deletion src/data/chunks/chunk.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::data::chunks::{DataDataChunk, DataSdscChunk, FoldChunk, Header, TrashDataChunk};
use crate::data::chunks::{
DataAutoChunk, DataDataChunk, DataSdscChunk, FoldChunk, Header, TrashDataChunk,
};
use crate::data::{ParserResult, Span};

#[derive(Debug)]
pub enum Chunk {
Fold(FoldChunk),
Data(TrashDataChunk),
DataAuto(DataAutoChunk),
DataData(DataDataChunk),
DataSdsc(DataSdscChunk),
}
Expand All @@ -16,6 +19,7 @@ impl Chunk {

return match &header.chunk_kind as &str {
"DATA" => match &header.chunk_type as &str {
"AUTO" => DataAutoChunk::parse(input, header),
"DATA" => DataDataChunk::parse(input, header, version),
"SDSC" => DataSdscChunk::parse(input, header),
_ => TrashDataChunk::parse(input, header),
Expand Down
27 changes: 27 additions & 0 deletions src/data/chunks/data_auto_chunk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::data::chunks::header::Header;
use crate::data::chunks::Chunk;
use crate::data::chunks::Chunk::DataAuto;
use crate::data::{ParserResult, Span};
use nom::bytes::complete::take;
use nom::combinator::{cut, map, map_parser};
use nom::number::complete::le_u8;
use nom_tracable::tracable_parser;

#[derive(Debug)]
pub struct DataAutoChunk {
pub automatch: bool,
}

impl DataAutoChunk {
#[tracable_parser]
pub fn parse(input: Span, header: Header) -> ParserResult<Chunk> {
cut(map_parser(
take(header.length),
map(le_u8, |automatch| {
DataAuto(DataAutoChunk {
automatch: automatch == 1,
})
}),
))(input)
}
}
11 changes: 9 additions & 2 deletions src/data/chunks/data_data_chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub struct DataDataChunk {
pub header: Header,
pub opponent_type: u32,
pub players: Vec<Player>,
pub skirmish: bool,
pub matchhistory_id: u64,
pub options: Vec<Option>,
pub mod_uuid: Uuid,
Expand All @@ -53,7 +54,7 @@ impl DataDataChunk {
take(6u32),
Self::parse_players(version),
length_data(le_u32),
length_data(le_u32),
Self::parse_skirmish_flag,
le_u64,
take(16u32),
length_count(Self::parse_options_length, Option::parse_option),
Expand All @@ -65,7 +66,7 @@ impl DataDataChunk {
_,
players,
_,
_,
skirmish,
matchhistory_id,
_,
options,
Expand All @@ -76,6 +77,7 @@ impl DataDataChunk {
header: header.clone(),
opponent_type,
players,
skirmish,
matchhistory_id,
options,
mod_uuid,
Expand All @@ -100,6 +102,11 @@ impl DataDataChunk {
fold_many_m_n(2, 2, le_u32, || -> u32 { 1 }, |acc: u32, item| acc * item)(input)
}

#[tracable_parser]
fn parse_skirmish_flag(input: Span) -> ParserResult<bool> {
map(parse_utf8_variable(le_u32), |(_, id)| !id.is_empty())(input)
}

#[tracable_parser]
fn parse_mod_info(input: Span) -> ParserResult<(Uuid, u32)> {
length_value(
Expand Down
2 changes: 2 additions & 0 deletions src/data/chunks/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
mod chunk;
mod data_auto_chunk;
mod data_data_chunk;
mod data_sdsc_chunk;
mod fold_chunk;
mod header;
mod trash_data_chunk;

pub use crate::data::chunks::chunk::Chunk;
pub use crate::data::chunks::data_auto_chunk::DataAutoChunk;
pub use crate::data::chunks::data_data_chunk::DataDataChunk;
pub use crate::data::chunks::data_sdsc_chunk::DataSdscChunk;
use crate::data::chunks::fold_chunk::FoldChunk;
Expand Down
16 changes: 14 additions & 2 deletions src/data/replay.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::data::chunks::Chunk::{DataData, DataSdsc};
use crate::data::chunks::{Chunk, DataDataChunk, DataSdscChunk};
use crate::data::chunks::Chunk::{DataAuto, DataData, DataSdsc};
use crate::data::chunks::{Chunk, DataAutoChunk, DataDataChunk, DataSdscChunk};
use crate::data::ticks::Tick::Command;
use crate::data::ticks::{CommandTick, Tick};
use crate::data::{Chunky, Header};
Expand Down Expand Up @@ -76,6 +76,18 @@ impl Replay {
}
}

pub fn automatch_data(&self) -> Option<&DataAutoChunk> {
match self
.data_chunks()
.iter()
.find(|chunk| matches!(chunk, DataAuto(_)))
{
Some(DataAuto(chunk)) => Some(chunk),
None => None,
_ => panic!(),
}
}

pub fn map_data(&self) -> &DataSdscChunk {
let chunks = self.data_chunks();

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ pub use crate::message::Message;
pub use crate::player::Faction;
pub use crate::player::Player;
pub use crate::player::Team;
pub use crate::replay::GameType;
pub use crate::replay::Replay;
67 changes: 62 additions & 5 deletions src/replay.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Representation of parsed replay information.

use crate::data::chunks::DataAutoChunk;
use crate::data::{Replay as ReplayData, Span};
use crate::map::{map_from_data, Map};
use crate::player::{player_from_data, Player};
Expand All @@ -8,6 +9,8 @@ use nom_locate::LocatedSpan;
use nom_tracable::TracableInfo;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::{Display, Formatter};
use uuid::Uuid;

/// A complete representation of all information able to be parsed from a Company of Heroes 3
Expand All @@ -20,7 +23,8 @@ use uuid::Uuid;
pub struct Replay {
version: u16,
timestamp: String,
matchhistory_id: u64,
game_type: GameType,
matchhistory_id: Option<u64>,
mod_uuid: Uuid,
map: Map,
players: Vec<Player>,
Expand Down Expand Up @@ -59,10 +63,21 @@ impl Replay {
pub fn timestamp(&self) -> &str {
&self.timestamp
}
/// The type of game this replay represents. Note that this information is parsed on a best-
/// effort basis and therefore may not always be correct. Also note that it's currently not
/// known if there's a way to differentiate between automatch and custom games for replays
/// recorded before the replay system release in patch 1.4.0. Games played before that patch
/// will be marked as either `Skirmish` (for local AI games) or `Multiplayer` (for networked
/// custom or automatch games). Games recorded on or after patch 1.4.0 will properly
/// differentiate between `Custom` and `Automatch` games.
pub fn game_type(&self) -> GameType {
self.game_type
}
/// The ID used by Relic to track this match on their internal servers. This ID can be matched
/// with an ID of the same name returned by Relic's CoH3 stats API, enabling linkage between
/// replay files and statistical information for a match.
pub fn matchhistory_id(&self) -> u64 {
/// replay files and statistical information for a match. When the game type is `Skirmish`,
/// there is no ID assigned by Relic, so this will be `None`.
pub fn matchhistory_id(&self) -> Option<u64> {
self.matchhistory_id
}
/// The UUID of the base game mod this replay ran on. If no mod was used, this will be a nil
Expand Down Expand Up @@ -99,11 +114,12 @@ impl Replay {
}
}

pub(crate) fn replay_from_data(data: &ReplayData) -> Replay {
fn replay_from_data(data: &ReplayData) -> Replay {
Replay {
version: data.header.version,
timestamp: data.header.timestamp.clone(),
matchhistory_id: data.game_data().matchhistory_id,
game_type: game_type_from_data(data),
matchhistory_id: matchhistory_id_from_data(data),
mod_uuid: data.game_data().mod_uuid,
map: map_from_data(data.map_data()),
length: data.commands().count(),
Expand All @@ -115,3 +131,44 @@ pub(crate) fn replay_from_data(data: &ReplayData) -> Replay {
.collect(),
}
}

fn matchhistory_id_from_data(data: &ReplayData) -> Option<u64> {
if game_type_from_data(data) == GameType::Skirmish {
None
} else {
Some(data.game_data().matchhistory_id)
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "magnus", magnus::wrap(class = "VaultCoh::GameType"))]
pub enum GameType {
Skirmish,
Multiplayer,
Automatch,
Custom,
}

impl Display for GameType {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
GameType::Skirmish => write!(f, "skirmish"),
GameType::Multiplayer => write!(f, "multiplayer"),
GameType::Automatch => write!(f, "automatch"),
GameType::Custom => write!(f, "custom"),
}
}
}

fn game_type_from_data(data: &ReplayData) -> GameType {
if data.game_data().skirmish {
GameType::Skirmish
} else {
match data.automatch_data() {
Some(DataAutoChunk { automatch: true }) => GameType::Automatch,
Some(DataAutoChunk { automatch: false }) => GameType::Custom,
None => GameType::Multiplayer,
}
}
}
32 changes: 31 additions & 1 deletion tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
extern crate vault;

use uuid::{uuid, Uuid};
use vault::Replay;
use vault::{GameType, Replay};

#[test]
fn parse_success() {
Expand All @@ -21,6 +21,8 @@ fn parse_success() {
vec!["madhax", "Quixalotl"]
);
assert_eq!(unwrapped.mod_uuid(), Uuid::nil());
assert_eq!(unwrapped.game_type(), GameType::Multiplayer);
assert_eq!(unwrapped.matchhistory_id(), Some(5569487));
}

#[test]
Expand Down Expand Up @@ -49,6 +51,8 @@ fn parse_success_ai() {
unwrapped.mod_uuid(),
uuid!("385d9810-96ba-4ece-9040-8281db65174e")
);
assert_eq!(unwrapped.game_type(), GameType::Skirmish);
assert_eq!(unwrapped.matchhistory_id(), None);
}

#[test]
Expand All @@ -59,6 +63,8 @@ fn parse_weird_description() {
let unwrapped = replay.unwrap();
assert_eq!(unwrapped.map().localized_name_id(), "Twin Beaches ML");
assert_eq!(unwrapped.map().localized_description_id(), "TB ML");
assert_eq!(unwrapped.game_type(), GameType::Multiplayer);
assert_eq!(unwrapped.matchhistory_id(), Some(11782009));
}

#[test]
Expand All @@ -74,3 +80,27 @@ fn parse_battlegroup() {
vec![Some(2072430), Some(196934)]
);
}

#[test]
fn parse_automatch() {
let data = include_bytes!("../replays/automatch.rec");
let replay = Replay::from_bytes(data).unwrap();
assert_eq!(replay.game_type(), GameType::Automatch);
assert_eq!(replay.matchhistory_id(), Some(18837622));
}

#[test]
fn parse_custom() {
let data = include_bytes!("../replays/custom.rec");
let replay = Replay::from_bytes(data).unwrap();
assert_eq!(replay.game_type(), GameType::Custom);
assert_eq!(replay.matchhistory_id(), Some(18838931));
}

#[test]
fn parse_skirmish() {
let data = include_bytes!("../replays/skirmish.rec");
let replay = Replay::from_bytes(data).unwrap();
assert_eq!(replay.game_type(), GameType::Skirmish);
assert_eq!(replay.matchhistory_id(), None);
}
Loading