1use std::io;
4
5use crate::{AnalyzeResult, CoordinateData, Level, RowData, checkpoint_times, track_scoring};
6
7const BLOCK_SIZE_BYTES: usize = 109;
8const TIME_INCREMENT_MS: f32 = 10.0;
9
10pub fn analyze_replay(
11 data: &[u8],
12 level_data: Option<&[Level]>,
13) -> Result<AnalyzeResult, io::Error> {
14 let (player_name, end_name_addr) = read_name(data)?;
15 let timing_data = get_timing_data_from_header(data, end_name_addr)?;
16
17 let displayed_ms =
18 timing_data.total_time_to_finish_ms - timing_data.cross_start_plus_start_delay_ms;
19 let lag_before_start_ms = count_same(data)? * TIME_INCREMENT_MS;
20 let start_ms = timing_data.cross_start_plus_start_delay_ms - lag_before_start_ms;
21 let lag_after_finish_ms =
22 timing_data.total_recording_time_ms - timing_data.total_time_to_finish_ms;
23 let checkpoint1_ms =
24 timing_data.checkpoint1_total_ms - timing_data.cross_start_plus_start_delay_ms;
25
26 let coords = get_coordinate_data(data)?;
28
29 let mut result = AnalyzeResult {
30 player_name,
31 track_name: None,
32 displayed_ms,
33 start_ms,
34 total_ms: displayed_ms + start_ms,
35 lag_start_ms: lag_before_start_ms,
36 lag_finish_ms: lag_after_finish_ms,
37 recording_ms: timing_data.total_recording_time_ms,
38 checkpoint1_ms,
39 coords: Some(coords),
40 checkpoint_times: None,
41 };
42
43 if let Some(level_data) = level_data {
45 result.track_name = track_scoring::score_track(&result, level_data);
46 }
47
48 if let Some(ref track_name) = result.track_name
50 && let Some(level_data) = level_data
51 {
52 result.checkpoint_times = Some(checkpoint_times::extract_checkpoint_times(
53 &result, track_name, level_data,
54 ));
55 }
56
57 Ok(result)
58}
59
60pub fn analyze_replay_fast(
61 data: &[u8],
62 level_data: Option<&[Level]>,
63) -> Result<AnalyzeResult, io::Error> {
64 let (player_name, end_name_addr) = read_name(data)?;
65 let timing_data = get_timing_data_from_header(data, end_name_addr)?;
66
67 let displayed_ms =
68 timing_data.total_time_to_finish_ms - timing_data.cross_start_plus_start_delay_ms;
69 let lag_before_start_ms = count_same(data)? * TIME_INCREMENT_MS;
70 let start_ms = timing_data.cross_start_plus_start_delay_ms - lag_before_start_ms;
71 let lag_after_finish_ms =
72 timing_data.total_recording_time_ms - timing_data.total_time_to_finish_ms;
73 let checkpoint1_ms =
74 timing_data.checkpoint1_total_ms - timing_data.cross_start_plus_start_delay_ms;
75
76 let mut track_name = None;
78 if let Some(levels) = level_data {
79 let first_block_offset = get_first_block_offset(data)?;
80
81 let start_pos = get_row_at_block_index(data, first_block_offset, 0);
82 let cp1_index = (timing_data.checkpoint1_total_ms / TIME_INCREMENT_MS) as usize;
83 let cp1_pos = get_row_at_block_index(data, first_block_offset, cp1_index);
84 let finish_index = (timing_data.total_time_to_finish_ms / TIME_INCREMENT_MS) as usize;
85 let finish_pos = get_row_at_block_index(data, first_block_offset, finish_index);
86
87 if let Some(start) = start_pos {
88 if let (Some(cp1), Some(finish)) = (cp1_pos, finish_pos) {
89 track_name = track_scoring::identify_track_fast(&start, &cp1, &finish, levels);
90 } else {
91 track_name = track_scoring::identify_track_by_start_pos(&start, levels);
93 }
94 }
95 }
96
97 Ok(AnalyzeResult {
98 player_name,
99 track_name,
100 displayed_ms,
101 start_ms,
102 total_ms: displayed_ms + start_ms,
103 lag_start_ms: lag_before_start_ms,
104 lag_finish_ms: lag_after_finish_ms,
105 recording_ms: timing_data.total_recording_time_ms,
106 checkpoint1_ms,
107 coords: None, checkpoint_times: None, })
110}
111
112fn read_name(data: &[u8]) -> Result<(String, usize), io::Error> {
113 for i in 24..data.len() {
114 if data[i] == 0x00 {
115 let name_bytes = &data[24..i];
116 let player_name = String::from_utf8_lossy(name_bytes).to_string();
117 return Ok((player_name, i));
118 }
119 }
120 Err(io::Error::new(
121 io::ErrorKind::InvalidData,
122 "Player name not found in replay data",
123 ))
124}
125
126struct TimingDataFromHeader {
127 total_recording_time_ms: f32,
128 cross_start_plus_start_delay_ms: f32,
129 checkpoint1_total_ms: f32,
130 total_time_to_finish_ms: f32,
131}
132
133fn get_timing_data_from_header(
134 data: &[u8],
135 end_name_addr: usize,
136) -> Result<TimingDataFromHeader, io::Error> {
137 if data.len() < 6 {
138 return Err(io::Error::new(
139 io::ErrorKind::InvalidData,
140 "Data too short for timing header",
141 ));
142 }
143
144 if end_name_addr + 11 >= data.len() {
145 return Err(io::Error::new(
146 io::ErrorKind::InvalidData,
147 "Data too short for timing data after name",
148 ));
149 }
150
151 let total_recording_time_ms =
153 TIME_INCREMENT_MS * f32::from(u16::from_le_bytes([data[4], data[5]]));
154 let cross_start_plus_start_delay_ms = TIME_INCREMENT_MS
155 * f32::from(u16::from_le_bytes([
156 data[end_name_addr + 1],
157 data[end_name_addr + 2],
158 ]));
159 let checkpoint1_total_ms = TIME_INCREMENT_MS
160 * f32::from(u16::from_le_bytes([
161 data[end_name_addr + 5],
162 data[end_name_addr + 6],
163 ]));
164 let total_time_to_finish_ms = TIME_INCREMENT_MS
165 * f32::from(u16::from_le_bytes([
166 data[end_name_addr + 9],
167 data[end_name_addr + 10],
168 ]));
169
170 Ok(TimingDataFromHeader {
171 total_recording_time_ms,
172 cross_start_plus_start_delay_ms,
173 checkpoint1_total_ms,
174 total_time_to_finish_ms,
175 })
176}
177
178fn count_same(data: &[u8]) -> Result<f32, io::Error> {
179 let first_block_offset = get_first_block_offset(data)?;
180 let mut count = 1.0;
181 let mut i = first_block_offset;
182
183 while i + BLOCK_SIZE_BYTES < data.len() {
184 if !block_is_same(data, i, i + BLOCK_SIZE_BYTES) {
185 return Ok(count);
186 }
187 i += BLOCK_SIZE_BYTES;
188 count += 1.0;
189 }
190
191 Ok(count)
192}
193
194fn block_is_same(data: &[u8], block1: usize, block2: usize) -> bool {
195 for i in 0..BLOCK_SIZE_BYTES {
196 if block1 + i >= data.len() || block2 + i >= data.len() {
197 return false;
198 }
199 if data[block1 + i] != data[block2 + i] {
200 return false;
201 }
202 }
203 true
204}
205
206fn get_first_block_offset(data: &[u8]) -> Result<usize, io::Error> {
207 let (_, end_name_addr) = read_name(data)?;
208 Ok(end_name_addr + 13)
209}
210
211fn get_row_at_block_index(data: &[u8], offset: usize, index: usize) -> Option<RowData> {
212 let block_start = offset + index * BLOCK_SIZE_BYTES;
213 let pos_offset = block_start + 44;
214
215 if pos_offset + 12 > data.len() {
216 return None;
217 }
218
219 let x = f32::from_le_bytes([
220 data[pos_offset],
221 data[pos_offset + 1],
222 data[pos_offset + 2],
223 data[pos_offset + 3],
224 ]);
225 let y = f32::from_le_bytes([
226 data[pos_offset + 4],
227 data[pos_offset + 5],
228 data[pos_offset + 6],
229 data[pos_offset + 7],
230 ]);
231 let z = f32::from_le_bytes([
232 data[pos_offset + 8],
233 data[pos_offset + 9],
234 data[pos_offset + 10],
235 data[pos_offset + 11],
236 ]);
237
238 Some(RowData { x, y, z })
239}
240
241fn get_coordinate_data(block_data: &[u8]) -> Result<CoordinateData, io::Error> {
242 let first_block_offset = get_first_block_offset(block_data)?;
243
244 let footer_exists = if block_data.len() >= BLOCK_SIZE_BYTES {
246 let last_block_start = block_data.len() - BLOCK_SIZE_BYTES;
247 block_data[last_block_start..].iter().all(|&b| b == 0x00)
248 } else {
249 false
250 };
251
252 let mut rows = Vec::new();
253 let last_block = if footer_exists {
254 block_data.len() - BLOCK_SIZE_BYTES
255 } else {
256 block_data.len()
257 };
258
259 let mut i = first_block_offset;
260 while i + BLOCK_SIZE_BYTES <= last_block {
261 let pos_offset = i + 44;
262 let matrix_offset = pos_offset + 3 * 4;
263
264 if matrix_offset + 9 * 4 > block_data.len() {
265 break;
266 }
267
268 let x = f32::from_le_bytes([
270 block_data[pos_offset],
271 block_data[pos_offset + 1],
272 block_data[pos_offset + 2],
273 block_data[pos_offset + 3],
274 ]);
275 let y = f32::from_le_bytes([
276 block_data[pos_offset + 4],
277 block_data[pos_offset + 5],
278 block_data[pos_offset + 6],
279 block_data[pos_offset + 7],
280 ]);
281 let z = f32::from_le_bytes([
282 block_data[pos_offset + 8],
283 block_data[pos_offset + 9],
284 block_data[pos_offset + 10],
285 block_data[pos_offset + 11],
286 ]);
287
288 rows.push(RowData { x, y, z });
289
290 i += BLOCK_SIZE_BYTES;
291 }
292
293 Ok(CoordinateData { rows })
294}