Skip to main content

supreme_replay_analyzer/
analyzer.rs

1//! Main replay analysis functionality
2
3use 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    // Extract coordinate data
27    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    // Score track to determine which level this replay is from
44    if let Some(level_data) = level_data {
45        result.track_name = track_scoring::score_track(&result, level_data);
46    }
47
48    // Extract checkpoint crossing times for the detected track
49    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    // Fast track identification: read positions at Start, CP1, and Finish
77    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                // Fallback to start pos only if other blocks are missing
92                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,           // Skip for fast scan
108        checkpoint_times: None, // Skip for fast scan
109    })
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    // Read little-endian u16 values and convert to milliseconds
152    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    // Check if footer exists (last block is all zeros)
245    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        // Read position (3 f32 values)
269        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}