ferravex_engine/
replay.rs1use ferravex_net::InputFrame;
4const MAGIC: &[u8; 8] = b"FXRP0001";
5pub const MAX_REPLAY_FRAMES: usize = 1_000_000;
6const ROW_BYTES: usize = 37;
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct Frame {
9 pub tick: u64,
10 pub input: InputFrame,
11 pub state_hash: u64,
12}
13#[derive(Default)]
14pub struct Replay {
15 frames: Vec<Frame>,
16}
17impl Replay {
18 pub fn record(&mut self, frame: Frame) -> bool {
19 if self.frames.len() >= MAX_REPLAY_FRAMES
20 || !frame.input.is_valid()
21 || self.frames.last().is_some_and(|f| f.tick >= frame.tick)
22 {
23 return false;
24 }
25 if self.frames.len() == self.frames.capacity() {
27 let growth = self
28 .frames
29 .len()
30 .max(1024)
31 .min(MAX_REPLAY_FRAMES - self.frames.len());
32 self.frames.reserve_exact(growth);
33 }
34 self.frames.push(frame);
35 true
36 }
37 pub fn frames(&self) -> &[Frame] {
38 &self.frames
39 }
40 pub fn encode(&self) -> Vec<u8> {
41 let mut out = Vec::with_capacity(20 + self.frames.len() * ROW_BYTES);
42 out.extend_from_slice(MAGIC);
43 out.extend_from_slice(&(self.frames.len() as u32).to_le_bytes());
44 for frame in &self.frames {
45 out.extend(frame.tick.to_le_bytes());
46 out.extend(frame.input.sequence.to_le_bytes());
47 for value in [
48 frame.input.move_x,
49 frame.input.move_z,
50 frame.input.yaw,
51 frame.input.pitch,
52 ] {
53 out.extend(value.to_le_bytes());
54 }
55 out.push(frame.input.buttons);
56 out.extend(frame.state_hash.to_le_bytes());
57 }
58 let hash = checksum(&out);
59 out.extend(hash.to_le_bytes());
60 out
61 }
62 pub fn decode(data: &[u8]) -> Option<Self> {
63 if data.len() < 20 || &data[..8] != MAGIC {
64 return None;
65 }
66 let count = u32::from_le_bytes(data[8..12].try_into().ok()?) as usize;
67 let expected = count.checked_mul(ROW_BYTES)?.checked_add(20)?;
68 if count > MAX_REPLAY_FRAMES || data.len() != expected {
69 return None;
70 }
71 let end = data.len() - 8;
72 if checksum(&data[..end]) != u64::from_le_bytes(data[end..].try_into().ok()?) {
73 return None;
74 }
75 let rows = &data[12..end];
76 let mut previous = None;
77 for row in rows.as_chunks::<ROW_BYTES>().0 {
79 let frame = decode_row(row)?;
80 if previous.is_some_and(|tick| tick >= frame.tick) {
81 return None;
82 }
83 previous = Some(frame.tick);
84 }
85 let mut frames = Vec::with_capacity(count);
86 for row in rows.as_chunks::<ROW_BYTES>().0 {
87 frames.push(decode_row(row)?);
88 }
89 Some(Self { frames })
90 }
91}
92fn decode_row(row: &[u8]) -> Option<Frame> {
93 let u32at = |i| u32::from_le_bytes(row[i..i + 4].try_into().unwrap());
94 let f32at = |i| f32::from_bits(u32at(i));
95 let frame = Frame {
96 tick: u64::from_le_bytes(row[..8].try_into().ok()?),
97 input: InputFrame {
98 sequence: u32at(8),
99 move_x: f32at(12),
100 move_z: f32at(16),
101 yaw: f32at(20),
102 pitch: f32at(24),
103 buttons: row[28],
104 },
105 state_hash: u64::from_le_bytes(row[29..37].try_into().ok()?),
106 };
107 frame.input.is_valid().then_some(frame)
108}
109pub fn checksum(bytes: &[u8]) -> u64 {
110 bytes.iter().fold(0xcbf29ce484222325, |hash, &byte| {
111 (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
112 })
113}
114#[cfg(test)]
115mod tests {
116 use super::*;
117 fn seal(bytes: &mut [u8]) {
118 let end = bytes.len() - 8;
119 let hash = checksum(&bytes[..end]);
120 bytes[end..].copy_from_slice(&hash.to_le_bytes());
121 }
122 fn recording() -> Replay {
123 let mut replay = Replay::default();
124 for tick in 0..3 {
125 assert!(replay.record(Frame {
126 tick,
127 input: InputFrame::default(),
128 state_hash: tick * 7
129 }));
130 }
131 replay
132 }
133 #[test]
134 fn replay_roundtrip_rejects_corruption_and_nonmonotonic_ticks() {
135 let mut r = recording();
136 assert!(!r.record(r.frames[0]));
137 let bytes = r.encode();
138 assert_eq!(Replay::decode(&bytes).unwrap().frames(), r.frames());
139 for len in 0..bytes.len() {
140 assert!(Replay::decode(&bytes[..len]).is_none());
141 }
142 let mut bad = bytes;
143 bad[24] ^= 1;
144 assert!(Replay::decode(&bad).is_none());
145 }
146 #[test]
147 fn checked_decoder_rejects_checksum_valid_invalid_inputs() {
148 let original = recording().encode();
149 for (offset, value) in [
150 (24, f32::NAN.to_le_bytes()),
151 (24, 2.0_f32.to_le_bytes()),
152 (32, 10.0_f32.to_le_bytes()),
153 ] {
154 let mut bytes = original.clone();
155 bytes[offset..offset + 4].copy_from_slice(&value);
156 seal(&mut bytes);
157 assert!(Replay::decode(&bytes).is_none());
158 }
159 let mut buttons = original.clone();
160 buttons[40] = 255;
161 seal(&mut buttons);
162 assert!(Replay::decode(&buttons).is_none());
163 let mut duplicate = original;
164 duplicate[49..57].copy_from_slice(&0_u64.to_le_bytes());
165 seal(&mut duplicate);
166 assert!(Replay::decode(&duplicate).is_none());
167 }
168 #[test]
169 fn rejects_forged_counts_and_trailing_bytes() {
170 let original = recording().encode();
171 let mut count = original.clone();
172 count[8..12].copy_from_slice(&u32::MAX.to_le_bytes());
173 seal(&mut count);
174 assert!(Replay::decode(&count).is_none());
175 let mut trailing = original;
176 trailing.push(0);
177 assert!(Replay::decode(&trailing).is_none());
178 assert_eq!(
179 Replay::decode(&Replay::default().encode())
180 .unwrap()
181 .frames()
182 .len(),
183 0
184 );
185 }
186}