Skip to main content

launcher/pages/
replays.rs

1use crate::tasks::{TaskEvent, TaskHandle, TaskSender, spawn_task};
2use crate::widgets::replay_row;
3use crate::widgets::replay_row::ReplayRowAction;
4use eframe::egui;
5use egui_extras::{Column, TableBuilder};
6use std::path::PathBuf;
7use std::thread;
8use supreme_fs::GameStorage;
9use supreme_replay_analyzer::AnalyzeResult;
10use supreme_replay_analyzer::installer::InstallEvent;
11use supreme_replay_analyzer::scanner::ScanResult;
12use supreme_snapshot::SnapshotEvent;
13
14#[derive(Default)]
15pub struct ReplaysPageState {
16    pub analysis_results: Vec<(PathBuf, Result<AnalyzeResult, String>)>,
17    pub is_scanning: bool,
18    pub is_scanned: bool,
19    pub is_advanced_mode: bool,
20    pub selected_level: Option<String>,
21
22    // Background processing
23    pub backend_receiver: Option<std::sync::mpsc::Receiver<(PathBuf, AnalyzeResult)>>,
24
25    // Snapshot state
26    pub snapshot_status: Option<String>,
27    pub snapshot_task: Option<TaskHandle<String>>, // Returns snapshot name
28
29    // Install state
30    pub replay_name_input: String,
31    pub install_status: Option<String>,
32    pub install_task: Option<TaskHandle<(PathBuf, AnalyzeResult)>>,
33}
34
35pub fn replays_page(ui: &mut egui::Ui, state: &mut ReplaysPageState, storage: &GameStorage) {
36    // Extract unique levels for the filter
37    let mut levels: Vec<String> = state
38        .analysis_results
39        .iter()
40        .filter_map(|(_, res)| res.as_ref().ok())
41        .filter_map(|analysis| analysis.track_name.clone())
42        .collect();
43    levels.sort();
44    levels.dedup();
45
46    ui.horizontal(|ui| {
47        ui.heading("Replays");
48
49        ui.add_space(10.0);
50
51        egui::ComboBox::from_id_salt("level_filter")
52            .selected_text(state.selected_level.as_deref().unwrap_or("All Levels"))
53            .show_ui(ui, |ui| {
54                ui.selectable_value(&mut state.selected_level, None, "All Levels");
55                for level in levels {
56                    ui.selectable_value(&mut state.selected_level, Some(level.clone()), level);
57                }
58            });
59
60        ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| {
61            ui.checkbox(&mut state.is_advanced_mode, "Advanced");
62        });
63    });
64
65    ui.add_space(10.0);
66
67    // Install Replay UI
68    ui.group(|ui| {
69        ui.horizontal(|ui| {
70            ui.label("Install Replay:");
71            let response = ui.text_edit_singleline(&mut state.replay_name_input);
72            if (ui.button("Install / Fetch").clicked()
73                || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))))
74                && !state.replay_name_input.is_empty()
75            {
76                start_replay_installation(
77                    ui.ctx().clone(),
78                    state,
79                    storage,
80                    state.replay_name_input.clone(),
81                );
82            }
83        });
84        if let Some(status) = &state.install_status {
85            ui.add_space(5.0);
86            ui.label(egui::RichText::new(status).italics());
87        }
88    });
89
90    // Check for install updates
91    let mut install_finished = false;
92    if let Some(handle) = &state.install_task {
93        while let Ok(event) = handle.try_recv() {
94            match event {
95                TaskEvent::Log(msg) | TaskEvent::SetStatus(msg) => {
96                    if msg.is_empty() {
97                        state.install_status = None;
98                    } else {
99                        state.install_status = Some(msg);
100                    }
101                }
102                TaskEvent::PageData((path, analysis)) => {
103                    state.install_status = Some(format!(
104                        "Successfully installed and analyzed!\nFile: {}",
105                        path.file_name().unwrap_or_default().to_string_lossy()
106                    ));
107                    // Update cache
108                    if let Some(entry) = state.analysis_results.iter_mut().find(|(p, _)| *p == path)
109                    {
110                        entry.1 = Ok(analysis);
111                    } else {
112                        state.analysis_results.push((path, Ok(analysis)));
113                        state.analysis_results.sort_by(|a, b| a.0.cmp(&b.0));
114                    }
115                }
116                TaskEvent::Finished(_) => {
117                    install_finished = true;
118                }
119                _ => {}
120            }
121        }
122    }
123    if install_finished {
124        state.install_task = None;
125    }
126
127    // Check for background results
128    if let Some(rx) = &state.backend_receiver {
129        while let Ok((path, result)) = rx.try_recv() {
130            if let Some(entry) = state.analysis_results.iter_mut().find(|(p, _)| *p == path) {
131                entry.1 = Ok(result);
132            }
133        }
134    }
135
136    // Check for snapshot updates
137    let mut snapshot_finished = false;
138    if let Some(handle) = &state.snapshot_task {
139        while let Ok(event) = handle.try_recv() {
140            match event {
141                TaskEvent::Log(_) => {}
142                TaskEvent::SetStatus(msg) => state.snapshot_status = Some(msg),
143                TaskEvent::InitProgress(_) => {}
144                TaskEvent::IncrementProgress => {}
145                TaskEvent::UpdateProgress(_) => {}
146                TaskEvent::Perf(_, _, _) => {}
147                TaskEvent::PageData(_) => {}
148                TaskEvent::Finished(res) => {
149                    snapshot_finished = true;
150                    match res {
151                        Ok(snapshot_name) => {
152                            ui.ctx().copy_text(snapshot_name);
153                            state.snapshot_status =
154                                Some("Snapshot created and name copied to clipboard!".to_string());
155                        }
156                        Err(e) => {
157                            state.snapshot_status = Some(format!("Error: {e}"));
158                        }
159                    }
160                }
161            }
162        }
163    }
164    if snapshot_finished {
165        state.snapshot_task = None;
166    }
167
168    if let Some(status) = &state.snapshot_status
169        && !status.is_empty()
170    {
171        ui.label(status);
172        ui.add_space(10.0);
173    }
174
175    ui.add_space(10.0);
176
177    // Scan for .dat files if not already done
178    if !state.is_scanned && !state.is_scanning {
179        state.is_scanning = true;
180        ui.label("Analyzing all .dat replay files in ./data directory...");
181        ui.add_space(10.0);
182
183        // Perform the analysis and cache results
184        scan_and_analyze_replays(state, storage);
185
186        state.is_scanning = false;
187        state.is_scanned = true;
188    } else {
189        ui.label("The replay analyzer is experimental, please report any computation mistakes.");
190        ui.add_space(10.0);
191    }
192
193    if state.analysis_results.is_empty() && !state.is_scanning {
194        ui.label("No .dat files found in ./data directory.");
195        return;
196    }
197
198    // Display results in a table
199    ui.add_space(10.0);
200
201    let mut snapshot_request = None;
202
203    let mut builder = TableBuilder::new(ui)
204        .striped(true)
205        .resizable(true)
206        .cell_layout(egui::Layout::left_to_right(egui::Align::Center))
207        .column(Column::auto()) // Snapshot
208        .column(Column::auto()) // File
209        .column(Column::auto()) // Player
210        .column(Column::auto()) // Track
211        .column(Column::auto().at_least(108.0)) // Start
212        .column(Column::auto().at_least(108.0)); // CP1
213
214    if state.is_advanced_mode {
215        builder = builder.column(Column::auto()); // CP2
216    }
217
218    builder = builder.column(Column::auto().at_least(108.0)); // Finish
219
220    builder = builder.column(Column::auto().at_least(108.0)); // Total
221
222    if state.is_advanced_mode {
223        builder = builder
224            .column(Column::auto()) // Lag Before
225            .column(Column::auto()) // Lag After
226            .column(Column::auto()); // Recording length
227    }
228
229    builder
230        .header(20.0, |mut header| {
231            header.col(|ui| {
232                ui.strong("📷");
233            });
234            header.col(|ui| {
235                ui.strong("File");
236            });
237            header.col(|ui| {
238                ui.strong("Player");
239            });
240            header.col(|ui| {
241                ui.strong("Track");
242            });
243            header.col(|ui| {
244                ui.horizontal(|ui| {
245                    ui.strong("Start");
246                    ui.label("ℹ").on_hover_text(
247                        "Duration between the end of the countdown and the crossing of the start line.",
248                    );
249                });
250            });
251            header.col(|ui| {
252                ui.horizontal(|ui| {
253                    ui.strong("CP1");
254                    ui.label("ℹ").on_hover_text(
255                        "Duration from the start line and checkpoint 1.",
256                    );
257                });
258            });
259            if state.is_advanced_mode {
260                header.col(|ui| {
261                    ui.horizontal(|ui| {
262                        ui.strong("CP2");
263                        ui.label("ℹ").on_hover_text(
264                            "Duration from the start line and checkpoint 2.",
265                        );
266                    });
267                });
268            }
269            header.col(|ui| {
270                ui.horizontal(|ui| {
271                    ui.strong("Finish");
272                    ui.label("ℹ").on_hover_text(
273                        "Duration from the start line and finish line.",
274                    );
275                });
276            });
277            header.col(|ui| {
278                ui.horizontal(|ui| {
279                    ui.strong("Total");
280                    ui.label("ℹ").on_hover_text(
281                        "Total time the character was controlled by the player, from the end of the countdown to the finish line",
282                    );
283                });
284            });
285            if state.is_advanced_mode {
286                header.col(|ui| {
287                    ui.horizontal(|ui| {
288                    ui.strong("Lag before");
289                    ui.label("ℹ").on_hover_text(
290                        "Duration of the countdown, before the player controls the character.",
291                    );
292                });
293                });
294                header.col(|ui| {
295                    ui.horizontal(|ui| {
296                        ui.strong("Lag after");
297                        ui.label("ℹ").on_hover_text(
298                            "Duration of the part after crossing the finish line.",
299                        );
300                    });
301                });
302                header.col(|ui| {
303                    ui.horizontal(|ui| {
304                        ui.strong("Recording duration");
305                        ui.label("ℹ").on_hover_text(
306                            "Total recording duration, which is the total time plus the lags.",
307                        );
308                    });
309                });
310            }
311        })
312        .body(|mut body| {
313            for (path, result) in &state.analysis_results {
314                if let Some(selected_level) = &state.selected_level {
315                    match result {
316                        Ok(analysis) => {
317                            if analysis.track_name.as_deref() != Some(selected_level) {
318                                continue;
319                            }
320                        }
321                        Err(_) => continue,
322                    }
323                }
324
325                body.row(18.0, |mut row| {
326                    if let Some(ReplayRowAction::CreateSnapshot) =
327                        replay_row::render_replay_row(&mut row, path, result, state.is_advanced_mode)
328                        && let Ok(analysis) = result {
329                            snapshot_request = Some((path.clone(), analysis.clone()));
330                        }
331                });
332            }
333        });
334
335    if let Some((path, analysis)) = snapshot_request {
336        start_snapshot_creation(ui.ctx().clone(), state, storage, path, analysis);
337    }
338}
339
340/// Scans and analyzes replay files using the scanner module
341fn scan_and_analyze_replays(state: &mut ReplaysPageState, storage: &GameStorage) {
342    let ScanResult {
343        analysis_results,
344        background_receiver,
345        performance,
346    } = supreme_replay_analyzer::scanner::scan_and_analyze_replays(storage);
347
348    state.analysis_results = analysis_results;
349    state.backend_receiver = Some(background_receiver);
350    state.is_scanned = true;
351
352    // Write performance log
353    supreme_replay_analyzer::scanner::write_performance_log(storage, &performance);
354}
355
356/// Starts snapshot creation using the `supreme_snapshot` module
357fn start_snapshot_creation(
358    ctx: eframe::egui::Context,
359    state: &mut ReplaysPageState,
360    storage: &GameStorage,
361    replay_path: PathBuf,
362    analysis: AnalyzeResult,
363) {
364    state.snapshot_status = Some("Initializing snapshot...".to_string());
365    let storage_clone = storage.clone();
366
367    state.snapshot_task = Some(spawn_task(ctx, move |tx: TaskSender<String>| {
368        let (lib_tx, lib_rx) = std::sync::mpsc::channel();
369        let storage_clone = storage_clone.clone();
370        let handle = thread::spawn(move || {
371            supreme_snapshot::create_replay_snapshot(&storage_clone, replay_path, &analysis, lib_tx)
372        });
373
374        while let Ok(event) = lib_rx.recv() {
375            let mapped = match event {
376                SnapshotEvent::Log(msg) => TaskEvent::Log(msg),
377                SnapshotEvent::SetStatus(msg) => TaskEvent::SetStatus(msg),
378                SnapshotEvent::InitProgress(total) => TaskEvent::InitProgress(total),
379                SnapshotEvent::IncrementProgress => TaskEvent::IncrementProgress,
380                SnapshotEvent::Perf(op, target, ms) => TaskEvent::Perf(op, target, ms),
381            };
382            let _ = tx.send(mapped);
383        }
384
385        handle
386            .join()
387            .unwrap_or(Err(std::io::Error::other("Thread panicked")))
388            .map_err(|e| e.to_string())
389    }));
390}
391
392/// Starts replay installation using the installer module
393fn start_replay_installation(
394    ctx: eframe::egui::Context,
395    state: &mut ReplaysPageState,
396    storage: &GameStorage,
397    replay_name: String,
398) {
399    state.install_status = Some(format!("Fetching '{replay_name}'..."));
400    let storage = storage.clone();
401
402    state.install_task = Some(spawn_task(ctx, move |tx| {
403        let handle = supreme_replay_analyzer::installer::install_replay(&storage, replay_name);
404
405        for event in handle.receiver {
406            match event {
407                InstallEvent::Log(msg) => {
408                    let _ = tx.send(TaskEvent::Log(msg));
409                }
410                InstallEvent::Finished(path, analysis) => {
411                    let _ = tx.send(TaskEvent::PageData((path.clone(), analysis.clone())));
412                    return Ok((path, analysis));
413                }
414            }
415        }
416
417        Err("Installation ended without a result".to_string())
418    }));
419}