supreme_replay_analyzer/
installer.rs1use 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#[derive(Debug)]
17pub enum InstallEvent {
18 Log(String),
20 Finished(PathBuf, AnalyzeResult),
22}
23
24pub struct InstallHandle {
26 pub receiver: Receiver<InstallEvent>,
28}
29
30#[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
59fn run_installation(storage: GameStorage, replay_name: String, tx: Sender<InstallEvent>) {
61 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 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 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 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 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 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 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
165fn 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}