Skip to main content

supreme_replay_analyzer/
installer.rs

1//! Replay installation functionality
2//!
3//! This module handles fetching, parsing, and installing replay files from
4//! online storage into the correct game directories.
5
6use std::path::PathBuf;
7use std::sync::mpsc::{Receiver, Sender, channel};
8use std::thread;
9use supreme_fs::GameStorage;
10
11use crate::AnalyzeResult;
12use crate::analyzer::analyze_replay;
13use crate::level_data;
14
15/// Events sent during replay installation
16#[derive(Debug)]
17pub enum InstallEvent {
18    /// Log message or status update
19    Log(String),
20    /// Installation finished successfully with path and analysis
21    Finished(PathBuf, AnalyzeResult),
22}
23
24/// Result of starting a replay installation
25pub struct InstallHandle {
26    /// Receiver for installation events
27    pub receiver: Receiver<InstallEvent>,
28}
29
30/// Starts the installation of a replay from online storage
31///
32/// This function:
33/// 1. Fetches the replay snapshot JSON from storage
34/// 2. Parses the manifest to find the .dat file
35/// 3. Downloads the .dat file content
36/// 4. Determines the correct installation path based on replay name
37/// 5. Writes the file and analyzes it
38///
39/// # Arguments
40///
41/// * `storage` - `GameStorage` instance for fetching and writing
42/// * `replay_name` - Name of the replay to install (e.g., "`AlpineEasy_1`'23''`45_PlayerName`")
43///
44/// # Returns
45///
46/// An `InstallHandle` containing a receiver for installation events
47#[must_use]
48pub fn install_replay(storage: &GameStorage, replay_name: String) -> InstallHandle {
49    let (tx, rx) = channel();
50    let storage_clone = storage.clone();
51
52    thread::spawn(move || {
53        run_installation(storage_clone, replay_name, tx);
54    });
55
56    InstallHandle { receiver: rx }
57}
58
59/// Runs the installation process (internal implementation)
60fn run_installation(storage: GameStorage, replay_name: String, tx: Sender<InstallEvent>) {
61    // 1. Fetch Snapshot JSON
62    let _ = tx.send(InstallEvent::Log(format!("Fetching '{replay_name}'...")));
63    let json_bytes = match storage.fetch_replay(&replay_name) {
64        Ok(b) => b,
65        Err(e) => {
66            let _ = tx.send(InstallEvent::Log(format!("Error fetching replay: {e}")));
67            return;
68        }
69    };
70
71    // 2. Parse Manifest
72    let _ = tx.send(InstallEvent::Log("Parsing manifest...".to_string()));
73    let manifest: serde_json::Value = match serde_json::from_slice(&json_bytes) {
74        Ok(m) => m,
75        Err(e) => {
76            let _ = tx.send(InstallEvent::Log(
77                format!("Error parsing replay JSON: {e}",),
78            ));
79            return;
80        }
81    };
82
83    // 3. Find .dat file
84    let files = if let Some(f) = manifest.get("files").and_then(|v| v.as_object()) {
85        f
86    } else {
87        let _ = tx.send(InstallEvent::Log(
88            "Error: Invalid replay format (no files list)".to_string(),
89        ));
90        return;
91    };
92
93    let dat_entry = files.iter().find(|(path, _)| path.ends_with(".dat"));
94
95    if let Some((path, hash_val)) = dat_entry {
96        let hash = if let Some(h) = hash_val.as_str() {
97            h
98        } else {
99            let _ = tx.send(InstallEvent::Log(
100                "Error: Invalid file entry (no hash)".to_string(),
101            ));
102            return;
103        };
104
105        let _ = tx.send(InstallEvent::Log(format!(
106            "Found .dat: {path}. Downloading content...",
107        )));
108
109        // 4. Download content
110        let content = match storage.get_content(hash) {
111            Ok(c) => c,
112            Err(e) => {
113                let _ = tx.send(InstallEvent::Log(
114                    format!("Error downloading content: {e}",),
115                ));
116                return;
117            }
118        };
119
120        // 5. Determine target path and install
121        if let Some((subpath, full_target)) = determine_install_path(&storage, &replay_name) {
122            let _ = tx.send(InstallEvent::Log(format!("Installing to {subpath}...")));
123
124            if let Some(parent) = full_target.parent()
125                && let Err(e) = std::fs::create_dir_all(parent)
126            {
127                let _ = tx.send(InstallEvent::Log(format!(
128                    "Error creating directory {}: {e}",
129                    parent.display()
130                )));
131            }
132
133            if let Err(e) = std::fs::write(&full_target, &content) {
134                let _ = tx.send(InstallEvent::Log(format!("Error writing file: {e}")));
135            } else {
136                // Analyze the replay for the UI
137                let level_data = level_data::get_levels();
138                match analyze_replay(&content, Some(level_data)) {
139                    Ok(analysis) => {
140                        let _ = tx.send(InstallEvent::Finished(full_target, analysis));
141                    }
142                    Err(e) => {
143                        let _ = tx.send(InstallEvent::Log(format!(
144                            "Successfully installed but analysis failed: {e}"
145                        )));
146                    }
147                }
148
149                // Clear status after 10 seconds
150                std::thread::sleep(std::time::Duration::from_secs(10));
151                let _ = tx.send(InstallEvent::Log(String::new()));
152            }
153        } else {
154            let _ = tx.send(InstallEvent::Log(format!(
155                "Error: Unknown replay type mapping for '{replay_name}'",
156            )));
157        }
158    } else {
159        let _ = tx.send(InstallEvent::Log(
160            "Error: No .dat file found in replay profile.".to_string(),
161        ));
162    }
163}
164
165/// Determines the installation path based on replay name
166///
167/// # Arguments
168///
169/// * `storage` - `GameStorage` instance for base path
170/// * `replay_name` - Name of the replay
171///
172/// # Returns
173///
174/// A tuple of (`relative_subpath`, `full_absolute_path`) or None if the replay type is unknown
175fn determine_install_path(storage: &GameStorage, replay_name: &str) -> Option<(String, PathBuf)> {
176    let lower_name = replay_name.to_lowercase();
177
178    let (level, track) = if lower_name.starts_with("alpineeasy") {
179        (Some("alpine"), Some("easy"))
180    } else if lower_name.starts_with("alpinemedium") {
181        (Some("alpine"), Some("medium"))
182    } else if lower_name.starts_with("alpinehard") {
183        (Some("alpine"), Some("hard"))
184    } else if lower_name.starts_with("villageeasy") {
185        (Some("village"), Some("easy"))
186    } else if lower_name.starts_with("villagemedium") {
187        (Some("village"), Some("medium"))
188    } else if lower_name.starts_with("villagehard") {
189        (Some("village"), Some("hard"))
190    } else if lower_name.starts_with("foresteasy") {
191        (Some("forest"), Some("easy"))
192    } else if lower_name.starts_with("forestmediumorhard") || lower_name.starts_with("foresthard") {
193        (Some("forest"), Some("hard"))
194    } else {
195        (None, None)
196    };
197
198    if let (Some(l), Some(t)) = (level, track) {
199        let subpath = format!("levels/{l}/tracks/{t}/replays/replay_4.dat");
200        let full_target = storage.game_path().join("data").join(&subpath);
201        Some((subpath, full_target))
202    } else {
203        None
204    }
205}