Skip to main content

supreme_replay_analyzer/
scanner.rs

1//! Replay scanning and analysis functionality
2//!
3//! This module handles discovering replay files, performing fast and full analysis,
4//! and managing background processing.
5
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::sync::mpsc::{Receiver, Sender, channel};
9use std::thread;
10use supreme_fs::{DiscoverMode, GameStorage};
11
12use crate::AnalyzeResult;
13use crate::analyzer::{analyze_replay, analyze_replay_fast};
14use crate::level_data;
15
16/// Performance metrics for replay scanning
17#[derive(Debug, serde::Serialize)]
18pub struct ScanPerformance {
19    pub total_ms: f64,
20    pub discovery_ms: f64,
21    pub analysis_ms: f64,
22    pub file_count: usize,
23    pub timestamp: u64,
24    #[serde(rename = "_note")]
25    pub note: String,
26}
27
28/// Result of a replay scan operation
29pub struct ScanResult {
30    /// Analysis results for each discovered replay file
31    pub analysis_results: Vec<(PathBuf, Result<AnalyzeResult, String>)>,
32    /// Receiver for background full analysis results
33    pub background_receiver: Receiver<(PathBuf, AnalyzeResult)>,
34    /// Performance metrics
35    pub performance: ScanPerformance,
36}
37
38/// Scans for replay files and performs analysis
39///
40/// This function:
41/// 1. Discovers all .dat files in data/levels/*/replays directories
42/// 2. Performs fast analysis on each file for immediate UI display
43/// 3. Spawns a background thread for full analysis
44/// 4. Returns results and a receiver for background updates
45///
46/// # Arguments
47///
48/// * `storage` - `GameStorage` instance for file operations
49///
50/// # Returns
51///
52/// A `ScanResult` containing initial fast analysis results, a receiver for
53/// background full analysis updates, and performance metrics.
54#[must_use]
55pub fn scan_and_analyze_replays(storage: &GameStorage) -> ScanResult {
56    let start = std::time::Instant::now();
57    let data_dir = storage.game_path().join("data");
58
59    // Early return if data directory doesn't exist
60    if !data_dir.exists() {
61        return ScanResult {
62            analysis_results: Vec::new(),
63            background_receiver: channel().1, // Empty receiver
64            performance: ScanPerformance {
65                total_ms: 0.0,
66                discovery_ms: 0.0,
67                analysis_ms: 0.0,
68                file_count: 0,
69                timestamp: current_timestamp(),
70                note: "Data directory not found".to_string(),
71            },
72        };
73    }
74
75    // Get level data for analysis
76    let level_data = level_data::get_levels();
77    let mut results = Vec::new();
78
79    // 1. Discovery Phase
80    let discovery_start = std::time::Instant::now();
81    let entries = storage
82        .discover_relative(Path::new("data/levels"), DiscoverMode::Files)
83        .unwrap_or_default()
84        .into_iter()
85        .filter(|path| path.extension().and_then(|s| s.to_str()) == Some("dat"))
86        .filter(|path| path.components().any(|c| c.as_os_str() == "replays"))
87        .collect::<Vec<_>>();
88    let discovery_ms = discovery_start.elapsed().as_secs_f64() * 1000.0;
89
90    // 2. Fast Analysis Phase
91    let analysis_start = std::time::Instant::now();
92    let mut files_to_process = Vec::new();
93
94    for path in entries {
95        let result = match fs::read(&path) {
96            Ok(data) => {
97                // Fast analysis for immediate UI
98                let fast_res = analyze_replay_fast(&data, Some(level_data));
99
100                // Queue for full background analysis
101                files_to_process.push(path.clone());
102
103                match fast_res {
104                    Ok(analysis) => Ok(analysis),
105                    Err(e) => Err(format!("Analysis error: {e}")),
106                }
107            }
108            Err(e) => Err(format!("Read error: {e}")),
109        };
110
111        results.push((path, result));
112    }
113    let analysis_ms = analysis_start.elapsed().as_secs_f64() * 1000.0;
114    let total_ms = start.elapsed().as_secs_f64() * 1000.0;
115
116    // 3. Start background thread for full analysis
117    let (tx, rx) = channel();
118    spawn_background_analysis(files_to_process, tx);
119
120    // Sort results by file path
121    results.sort_by(|a, b| a.0.cmp(&b.0));
122
123    let performance = ScanPerformance {
124        total_ms,
125        discovery_ms,
126        analysis_ms,
127        file_count: results.len(),
128        timestamp: current_timestamp(),
129        note: "Fast scan time only. Full analysis running in background.".to_string(),
130    };
131
132    ScanResult {
133        analysis_results: results,
134        background_receiver: rx,
135        performance,
136    }
137}
138
139/// Writes performance metrics to a JSON file
140///
141/// # Arguments
142///
143/// * `storage` - `GameStorage` instance for determining output path
144/// * `performance` - Performance metrics to write
145pub fn write_performance_log(storage: &GameStorage, performance: &ScanPerformance) {
146    if let Err(e) = storage.write_report("replays_scan_perf", performance) {
147        eprintln!("Failed to write replay scan performance report: {e}");
148    }
149}
150
151/// Spawns a background thread to perform full analysis on replay files
152fn spawn_background_analysis(files: Vec<PathBuf>, tx: Sender<(PathBuf, AnalyzeResult)>) {
153    thread::spawn(move || {
154        let level_data = level_data::get_levels();
155        for path in files {
156            if let Ok(data) = fs::read(&path)
157                && let Ok(full_analysis) = analyze_replay(&data, Some(level_data))
158            {
159                let _ = tx.send((path, full_analysis));
160            }
161        }
162    });
163}
164
165/// Gets the current Unix timestamp in seconds
166fn current_timestamp() -> u64 {
167    std::time::SystemTime::now()
168        .duration_since(std::time::UNIX_EPOCH)
169        .unwrap_or_default()
170        .as_secs()
171}