faf_replay_parser/
replay.rs

1//! Generic struct definitions for Supreme Commander replay files.
2
3use ordered_hash_map::OrderedHashMap;
4
5use crate::lua::LuaObject;
6use crate::version::Version;
7
8use std::collections::HashMap;
9
10/// Parsed representation of data as it appears in the replay file.
11#[derive(Debug)]
12pub struct Replay<V: Version> {
13    pub header: ReplayHeader,
14    pub body: ReplayBody<V>,
15}
16
17// Implemented by hand so that Version doesn't need Clone
18impl<V: Version> Clone for Replay<V> {
19    #[inline]
20    fn clone(&self) -> Self {
21        Self {
22            header: self.header.clone(),
23            body: self.body.clone(),
24        }
25    }
26}
27
28// Implemented by hand so that Version doesn't need PartialEq
29impl<V: Version> PartialEq for Replay<V> {
30    #[inline]
31    fn eq(&self, other: &Replay<V>) -> bool {
32        self.header == other.header && self.body == other.body
33    }
34}
35
36/// Parsed representation of the replay header section.
37#[derive(Clone, Debug, PartialEq)]
38pub struct ReplayHeader {
39    pub scfa_version: String,
40    pub replay_version: String,
41    pub map_file: String,
42    pub mods: LuaObject,
43    pub scenario: LuaObject,
44    pub players: OrderedHashMap<String, i32>,
45    pub cheats_enabled: bool,
46    pub armies: OrderedHashMap<u8, LuaObject>,
47    pub seed: u32,
48}
49
50/// Parsed representation of the command stream along with some incrementally computed simulation
51/// data.
52#[derive(Debug)]
53pub struct ReplayBody<V: Version> {
54    pub commands: Vec<V::Command>,
55    pub sim: SimData,
56}
57
58// Implemented by hand so that Version doesn't need Clone
59impl<V: Version> Clone for ReplayBody<V> {
60    fn clone(&self) -> Self {
61        Self {
62            commands: self.commands.clone(),
63            sim: self.sim.clone(),
64        }
65    }
66}
67
68// Implemented by hand so that Version doesn't need PartialEq
69impl<V: Version> PartialEq for ReplayBody<V> {
70    #[inline]
71    fn eq(&self, other: &ReplayBody<V>) -> bool {
72        self.commands == other.commands && self.sim == other.sim
73    }
74}
75
76/// Basic simulation data that is updated as the command stream is parsed.
77#[derive(Clone, Debug, PartialEq)]
78pub struct SimData {
79    /// The current tick
80    pub tick: u32,
81    /// The player id of the current command sender. Only valid if `tick > 0`
82    pub command_source: u8,
83    /// A map of player id's to the tick on which their last command was received
84    pub players_last_tick: HashMap<u8, u32>,
85    /// The current checksum value. Only valid if `checksum_tick != None`
86    pub checksum: [u8; 16],
87    /// The current tick which the checksum is verifying
88    pub checksum_tick: Option<u32>,
89    /// The first tick that was desynced
90    pub desync_tick: Option<u32>,
91    /// A list of all ticks that were desynced
92    pub desync_ticks: Option<Vec<u32>>,
93}
94impl SimData {
95    pub fn new() -> SimData {
96        SimData {
97            tick: 0,
98            command_source: 0,
99            players_last_tick: HashMap::new(),
100            checksum: [0; 16],
101            checksum_tick: None,
102            desync_tick: None,
103            desync_ticks: None,
104        }
105    }
106}
107
108/// A parsed command frame header and following raw command data.
109///
110/// This data will be parsed into a `Command` implementation.
111#[derive(Clone, Debug, PartialEq)]
112pub struct ReplayCommandFrame<V: Version> {
113    pub command_id: V::CommandId,
114    /// Size of the entire frame. This is data.len() + 3
115    pub size: u16,
116    /// Contents of the frame without the frame header.
117    pub data: Vec<u8>,
118}
119
120/// A raw command frame header and reference to body data independent of any game version.
121#[derive(Debug, PartialEq)]
122pub struct ReplayCommandFrameSpan<'a> {
123    pub cmd: u8,
124    pub size: u16,
125    pub data: &'a [u8],
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use pretty_assertions::assert_eq;
132
133    use crate::scfa::replay::*;
134    use crate::scfa::SCFA;
135
136    #[test]
137    fn test_struct_traits() {
138        let replay = Replay::<SCFA> {
139            header: ReplayHeader {
140                scfa_version: "Supreme Commander v1.50.3698".to_string(),
141                replay_version: "Replay v1.9".to_string(),
142                map_file: "/maps/map1/map1.scmap".to_string(),
143                mods: LuaObject::Nil,
144                scenario: LuaObject::Nil,
145                armies: OrderedHashMap::new(),
146                cheats_enabled: false,
147                players: OrderedHashMap::new(),
148                seed: 0,
149            },
150            body: ReplayBody {
151                sim: SimData {
152                    tick: 0,
153                    command_source: 0,
154                    players_last_tick: HashMap::new(),
155                    checksum: [0; 16],
156                    checksum_tick: None,
157                    desync_tick: None,
158                    desync_ticks: None,
159                },
160                commands: vec![],
161            },
162        };
163        let replay2 = replay.clone();
164
165        let _ = format!("{:?}", &replay);
166        assert_eq!(replay, replay2);
167    }
168
169    #[test]
170    fn test_replay_command_display() {
171        use crate::lua::LuaTable;
172        use ReplayCommand::*;
173
174        assert_eq!(
175            format!(
176                "{}",
177                VerifyChecksum {
178                    digest: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
179                    tick: 10
180                }
181            ),
182            "VerifyChecksum { digest: 000102030405060708090a0b0c0d0e0f, tick: 10 }"
183        );
184        assert_eq!(
185            format!(
186                "{}",
187                CreateProp {
188                    blueprint: "abcd".into(),
189                    position: Position {
190                        x: 0.0,
191                        y: 10.0,
192                        z: -100.0
193                    }
194                }
195            ),
196            "CreateProp { blueprint: \"abcd\", position: Position { x: 0, y: 10, z: -100 } }"
197        );
198        assert_eq!(
199            format!(
200                "{}",
201                ExecuteLuaInSim {
202                    code: "CallSomething(10 + 100)".into()
203                }
204            ),
205            "ExecuteLuaInSim { code: \"CallSomething(10 + 100)\" }"
206        );
207        assert_eq!(
208            format!("{}", SetCommandType { id: 1234, type_: 4 }),
209            "SetCommandType { id: 1234, type: FormMove }"
210        );
211        assert_eq!(
212            format!(
213                "{}",
214                SetCommandType {
215                    id: 1234,
216                    type_: 99
217                }
218            ),
219            "SetCommandType { id: 1234, type: 99 }"
220        );
221        assert_eq!(
222            format!(
223                "{}",
224                SetCommandTarget {
225                    id: 1234,
226                    target: Target::None
227                }
228            ),
229            "SetCommandTarget { id: 1234, target: None }"
230        );
231        assert_eq!(
232            format!(
233                "{}",
234                SetCommandTarget {
235                    id: 1234,
236                    target: Target::Entity { id: 1234 }
237                }
238            ),
239            "SetCommandTarget { id: 1234, target: Entity { id: 1234 } }"
240        );
241        assert_eq!(
242            format!(
243                "{}",
244                SetCommandTarget {
245                    id: 1234,
246                    target: Target::Position(Position {
247                        x: 1.,
248                        y: 2.,
249                        z: 3.
250                    })
251                }
252            ),
253            "SetCommandTarget { id: 1234, target: Position { x: 1, y: 2, z: 3 } }"
254        );
255
256        let mut upgrades_table = LuaTable::new();
257        upgrades_table.insert(
258            LuaObject::Unicode("Name".into()),
259            LuaObject::Unicode("Mazor".into()),
260        );
261        assert_eq!(
262            format!("{}", IssueCommand(GameCommand {
263                entity_ids: vec![1, 2],
264                id: 12345,
265                coordinated_attack_cmd_id: 0,
266                type_: 0,
267                arg2: -1,
268                target: Target::Position(Position {
269                    x: 1.,
270                    y: 2.,
271                    z: 3.
272                }),
273                arg3: 0,
274                formation: Some(Formation {
275                    a: 1.,
276                    b: 2.,
277                    c: 3.,
278                    d: 4.,
279                    scale: 2.,
280                }),
281                blueprint: "abcd".into(),
282                arg4: 0,
283                arg5: 1,
284                arg6: 2,
285                upgrades: LuaObject::Table(upgrades_table),
286                clear_queue: None,
287            })),
288            "IssueCommand(GameCommand { entity_ids: [1, 2], id: 12345, coordinated_attack_cmd_id: 0, type: None, arg2: -1, target: Position { x: 1, y: 2, z: 3 }, arg3: 0, formation: Formation { a: 1, b: 2, c: 3, d: 4, scale: 2 }, blueprint: \"abcd\", arg4: 0, arg5: 1, arg6: 2, upgrades: {\"Name\": \"Mazor\"}, clear_queue: None })"
289        );
290    }
291}