diff --git a/replays/ai_takeover.rec b/replays/ai_takeover.rec new file mode 100644 index 0000000..6e20a0b Binary files /dev/null and b/replays/ai_takeover.rec differ diff --git a/src/command.rs b/src/command.rs index ee9a69e..1b753a9 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,7 +1,7 @@ //! Wrapper for Company of Heroes 3 player commands. use crate::{ - command_data::{Pbgid, Sourced, SourcedIndex, SourcedPbgid, Unknown}, + command_data::{Empty, Pbgid, Sourced, SourcedIndex, SourcedPbgid, Unknown}, command_type::CommandType, data::ticks, }; @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "magnus", magnus::wrap(class = "VaultCoh::Command"))] pub enum Command { + AITakeover(Empty), BuildGlobalUpgrade(SourcedPbgid), BuildSquad(SourcedPbgid), CancelConstruction(Sourced), @@ -34,6 +35,13 @@ pub enum Command { impl Command { pub(crate) fn from_data_command_at_tick(command: ticks::Command, tick: u32) -> Self { match command.data { + ticks::CommandData::Empty => match command.action_type { + CommandType::PCMD_AIPlayer => Self::AITakeover(Empty::new(tick)), + _ => panic!( + "an empty command isn't being handled here! command type {:?}", + command.action_type + ), + }, ticks::CommandData::Pbgid(pbgid) => match command.action_type { CommandType::PCMD_Ability => { Self::UseBattlegroupAbility(Pbgid::new(tick, command.index, pbgid)) diff --git a/src/command_data/empty.rs b/src/command_data/empty.rs new file mode 100644 index 0000000..4985691 --- /dev/null +++ b/src/command_data/empty.rs @@ -0,0 +1,24 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// An empty command format with no additional context. + +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Empty { + tick: u32, +} + +impl Empty { + pub(crate) fn new(tick: u32) -> Self { + Self { tick } + } + + /// This value is the tick at which the command was found while parsing the replay, which + /// represents the time in the replay at which it was executed. Because CoH3's engine runs at 8 + /// ticks per second, you can divide this value by 8 to get the number of seconds since the + /// replay began, which will tell you when this command was executed. + pub fn tick(&self) -> u32 { + self.tick + } +} diff --git a/src/command_data/mod.rs b/src/command_data/mod.rs index 591bb90..b9c3261 100644 --- a/src/command_data/mod.rs +++ b/src/command_data/mod.rs @@ -1,11 +1,13 @@ //! Representations of replay command data formats. +mod empty; mod pbgid; mod sourced; mod sourced_index; mod sourced_pbgid; mod unknown; +pub use crate::command_data::empty::Empty; pub use crate::command_data::pbgid::Pbgid; pub use crate::command_data::sourced::Sourced; pub use crate::command_data::sourced_index::SourcedIndex; diff --git a/src/data/ticks/command.rs b/src/data/ticks/command.rs index 2e15c5b..ef8d00e 100644 --- a/src/data/ticks/command.rs +++ b/src/data/ticks/command.rs @@ -12,6 +12,7 @@ use nom::{ #[derive(Debug, Copy, Clone)] pub enum CommandData { + Empty, Pbgid(u32), SourcedPbgid(u32, u16), Sourced(u16), @@ -20,6 +21,10 @@ pub enum CommandData { } impl CommandData { + pub fn parse_empty(input: Span) -> ParserResult { + map(rest, |_| CommandData::Empty)(input) + } + pub fn parse_pbgid(input: Span) -> ParserResult { map(tuple((take(27u32), le_u32)), |(_, pbgid)| { CommandData::Pbgid(pbgid) @@ -56,6 +61,7 @@ impl CommandData { command_type: CommandType, ) -> impl FnMut(Span) -> ParserResult { match command_type { + CommandType::PCMD_AIPlayer => Self::parse_empty, CommandType::PCMD_Ability | CommandType::PCMD_InstantUpgrade | CommandType::PCMD_TentativeUpgrade => Self::parse_pbgid, diff --git a/tests/lib.rs b/tests/lib.rs index 13e5ae5..33091b3 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -123,6 +123,13 @@ fn parse_new_map_chunk() { assert_eq!(replay.map_localized_description_id(), "$11233955"); } +#[test] +fn parse_ai_takeover() { + let data = include_bytes!("../replays/ai_takeover.rec"); + let replay = Replay::from_bytes(data); + assert!(replay.is_ok()); +} + #[test] #[cfg_attr(not(feature = "regression"), ignore)] fn regression() {