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,
}
}
}