1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//! 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};
use crate::ParseError;
use nom_locate::LocatedSpan;
use nom_tracable::TracableInfo;
use std::fmt;
use std::fmt::{Display, Formatter};
use uuid::Uuid;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// A complete representation of all information able to be parsed from a Company of Heroes 3
/// replay. Note that parsing is not yet exhaustive, and iterative improvements will be made to
/// pull more information from replay files over time.

#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "magnus", magnus::wrap(class = "VaultCoh::Replay"))]
pub struct Replay {
    version: u16,
    timestamp: String,
    game_type: GameType,
    matchhistory_id: Option<u64>,
    mod_uuid: Uuid,
    map: Map,
    players: Vec<Player>,
    length: usize,
}

impl Replay {
    /// Takes a byte slice, parses it as a CoH3 replay, and returns a representation of the parsed
    /// information. Any failures during parsing or conversion will return an error.
    ///
    /// ```ignore
    /// fn main() {
    ///     let data = include_bytes!("/path/to/replay.rec");
    ///     let replay = vault::Replay::from_bytes(data);
    ///     assert!(replay.is_ok())
    /// }
    /// ```
    pub fn from_bytes(input: &[u8]) -> Result<Replay, ParseError> {
        let info = TracableInfo::new().parser_width(64).fold("term");
        let input: Span = LocatedSpan::new_extra(input, info);
        let (_, replay) = ReplayData::from_span(input)?;
        Ok(replay_from_data(&replay))
    }

    /// The Company of Heroes 3 game version this replay was recorded on. Note that this is probably
    /// more accurated described as the build version, and represents the final segment of digits
    /// you see in the game version on the game's main menu. Every time the game is patched, this
    /// version will change, and replays are generally only viewable on the same game version they
    /// were recorded on.
    pub fn version(&self) -> u16 {
        self.version
    }
    /// A UTF-16 representation of the recording user's local time when the replay was recorded.
    /// Note that value may contain non-standard characters and is not guaranteed to be parsable
    /// into an accurate date/time format.
    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. 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
    /// UUID (all zeroes).
    pub fn mod_uuid(&self) -> Uuid {
        self.mod_uuid
    }
    /// Map information for this match.
    pub fn map(&self) -> Map {
        self.map.clone()
    }
    /// Filename of the map this match was played on. See `Map::filename` for more information.
    pub fn map_filename(&self) -> &str {
        self.map.filename()
    }
    /// Localization ID of the map's name. See `Map::localized_name_id` for more information.
    pub fn map_localized_name_id(&self) -> &str {
        self.map.localized_name_id()
    }
    /// Localization ID of the map's description. See `Map::localized_description_id` for more
    /// information.
    pub fn map_localized_description_id(&self) -> &str {
        self.map.localized_description_id()
    }
    /// A list of all players who participated in this match.
    pub fn players(&self) -> Vec<Player> {
        self.players.clone()
    }
    /// A simple count of the number of ticks that were executed in this match. Because CoH3's
    /// engine runs at 8 ticks per second, you can divide this value by 8 to get the duration of
    /// the match in seconds.
    pub fn length(&self) -> usize {
        self.length
    }
}

fn replay_from_data(data: &ReplayData) -> Replay {
    let commands = data.commands();
    let messages = data.messages();
    #[cfg(feature = "raw")]
    let raw_commands = data.raw_commands();

    Replay {
        version: data.header.version,
        timestamp: data.header.timestamp.clone(),
        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.command_ticks().count(),
        players: data
            .game_data()
            .players
            .iter()
            .map(|player| {
                player_from_data(
                    player,
                    &messages,
                    &commands,
                    #[cfg(feature = "raw")]
                    &raw_commands,
                )
            })
            .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)
    }
}

/// Company of Heroes 3 game types

#[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 {
    /// Local games against AI opponents
    Skirmish,
    /// Networked games that couldn't be more specifically defined; includes both custom and
    /// automatch games from before patch 1.4.0
    Multiplayer,
    /// Ranked automatch games, detectable post patch 1.4.0
    Automatch,
    /// Custom games against human opponents, AI opponents, or a mix of both, detectable post patch
    /// 1.4.0
    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,
        }
    }
}