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 pub backend_receiver: Option<std::sync::mpsc::Receiver<(PathBuf, AnalyzeResult)>>,
24
25 pub snapshot_status: Option<String>,
27 pub snapshot_task: Option<TaskHandle<String>>, 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 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 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 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 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 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 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 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 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 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()) .column(Column::auto()) .column(Column::auto()) .column(Column::auto()) .column(Column::auto().at_least(108.0)) .column(Column::auto().at_least(108.0)); if state.is_advanced_mode {
215 builder = builder.column(Column::auto()); }
217
218 builder = builder.column(Column::auto().at_least(108.0)); builder = builder.column(Column::auto().at_least(108.0)); if state.is_advanced_mode {
223 builder = builder
224 .column(Column::auto()) .column(Column::auto()) .column(Column::auto()); }
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
340fn 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 supreme_replay_analyzer::scanner::write_performance_log(storage, &performance);
354}
355
356fn 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
392fn 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}