Skip to main content

supreme_dependencies/
lib.rs

1use anyhow::Result;
2use sha2::{Digest, Sha256};
3use std::fs;
4use std::io::{self, Cursor, Read};
5use std::path::{Path, PathBuf};
6use std::sync::mpsc::{Sender, channel};
7use std::thread;
8use supreme_fs::GameStorage;
9use zip::ZipArchive;
10
11#[derive(Debug, Clone)]
12pub enum DependencyEvent {
13    Log(String),
14    SetStatus(String),
15    InitProgress(usize),
16    UpdateProgress(usize),
17    Finished(Option<String>), // None = success, Some(error_message) = failure
18}
19
20// Ensure that the injector and helper are present and intact
21#[must_use]
22pub fn check_dependencies_exist(storage: &GameStorage) -> bool {
23    let game_path = storage.game_path();
24    let injector_path = game_path.join("Display_Config_Resources/Injector.exe");
25    let helper_path = game_path.join("Display_Config_Resources/Display_Config_Helper.dll");
26
27    if !injector_path.exists() || !helper_path.exists() {
28        return false;
29    }
30
31    // SHA256 checks
32    let injector_hash = calculate_sha256(&injector_path).unwrap_or_default();
33    let helper_hash = calculate_sha256(&helper_path).unwrap_or_default();
34
35    // Placeholders for real SHA256 values
36    let expected_injector_hash = "ad8d3abe13e2af0084fa62a8bf6d0474dee2b266e8f7ac1a9964c773cdef3d2f";
37    let expected_helper_hash = "0df99e44d6a1f8f637c6836cb4347b9b3c2329c21137faa047e9907494b54671";
38
39    injector_hash == expected_injector_hash && helper_hash == expected_helper_hash
40}
41
42fn calculate_sha256(path: &PathBuf) -> Result<String> {
43    let mut file = fs::File::open(path)?;
44    let mut hasher = Sha256::new();
45    let mut buffer = Vec::new();
46    file.read_to_end(&mut buffer)?;
47    hasher.update(&buffer);
48    let hash = hasher.finalize();
49    Ok(hex::encode(hash))
50}
51
52#[must_use]
53pub fn check_hd_fonts_needed(storage: &GameStorage) -> bool {
54    let frnk_path = storage.game_path().join("Data/Fonts/Frnk/FRNK0033.tga");
55
56    if !frnk_path.exists() {
57        return false;
58    }
59
60    if let Ok(img) = image::open(&frnk_path) {
61        use image::GenericImageView;
62        let (width, height) = img.dimensions();
63        // If it's the original low-res font, it will be 32x32.
64        // We want to update if we find the small version.
65        return width == 32 && height == 32;
66    }
67
68    false
69}
70
71// Refactored generic function to verify and extract
72fn download_and_extract_zip<F>(
73    url: &str,
74    target_dir: &Path,
75    filter: F,
76    tx: Sender<DependencyEvent>,
77) -> Result<usize>
78where
79    F: Fn(&str) -> bool,
80{
81    let _ = tx.send(DependencyEvent::Log(format!("Downloading from {url}")));
82    let _ = tx.send(DependencyEvent::SetStatus("Downloading...".to_string()));
83
84    let response = ureq::get(url).call().map_err(|e| {
85        let err_msg = format!("Failed to download file: {e}");
86        let _ = tx.send(DependencyEvent::Log(err_msg.clone()));
87        anyhow::anyhow!(err_msg)
88    })?;
89
90    // Get the total size for progress tracking
91    let total_size = response
92        .headers()
93        .get("Content-Length")
94        .and_then(|v| v.to_str().ok())
95        .and_then(|s| s.parse::<u64>().ok())
96        .unwrap_or(0);
97    let _ = tx.send(DependencyEvent::Log(format!(
98        "Downloading {total_size} bytes...",
99    )));
100    let _ = tx.send(DependencyEvent::InitProgress(100));
101
102    let mut bytes = Vec::new();
103    let mut downloaded = 0;
104
105    // Use chunked reading to track progress
106    let mut reader = response.into_body().into_reader();
107    let mut buffer = [0; 8192]; // 8KB buffer
108    while let Ok(bytes_read) = reader.read(&mut buffer) {
109        if bytes_read == 0 {
110            break;
111        }
112
113        bytes.extend_from_slice(&buffer[..bytes_read]);
114        downloaded += bytes_read as u64;
115
116        if total_size > 0 {
117            let progress = (downloaded as f32 / total_size as f32 * 100.0) as usize;
118            let _ = tx.send(DependencyEvent::UpdateProgress(progress));
119            let _ = tx.send(DependencyEvent::SetStatus(format!(
120                "Downloading... {progress}%",
121            )));
122        }
123    }
124
125    let _ = tx.send(DependencyEvent::Log("Download completed!".to_string()));
126    let _ = tx.send(DependencyEvent::SetStatus(
127        "Extracting files...".to_string(),
128    ));
129
130    // Extract the zip file
131    let cursor = Cursor::new(bytes);
132    let mut archive = ZipArchive::new(cursor).map_err(|e| {
133        let err_msg = format!("Failed to open zip archive: {e}");
134        let _ = tx.send(DependencyEvent::Log(err_msg.clone()));
135        anyhow::anyhow!(err_msg)
136    })?;
137
138    let _ = tx.send(DependencyEvent::InitProgress(archive.len()));
139
140    // Extract only the files we need
141    let mut extracted_count = 0;
142    for i in 0..archive.len() {
143        let mut file = archive.by_index(i).map_err(|e| {
144            let err_msg = format!("Failed to read file from archive: {e}");
145            let _ = tx.send(DependencyEvent::Log(err_msg.clone()));
146            anyhow::anyhow!(err_msg)
147        })?;
148
149        let file_name = file.name().to_string();
150
151        if file.is_dir() {
152            continue;
153        }
154
155        // Filter the files we need
156        if filter(&file_name) {
157            let out_path = target_dir.join(&file_name);
158
159            if let Some(parent) = out_path.parent()
160                && let Err(e) = fs::create_dir_all(parent)
161            {
162                let err_msg = format!("Failed to create parent directory: {e}");
163                let _ = tx.send(DependencyEvent::Log(err_msg.clone()));
164                anyhow::bail!(err_msg);
165            }
166
167            let mut out_file = fs::File::create(&out_path).map_err(|e| {
168                let err_msg = format!("Failed to create file: {e}");
169                let _ = tx.send(DependencyEvent::Log(err_msg.clone()));
170                anyhow::anyhow!(err_msg)
171            })?;
172
173            if let Err(e) = io::copy(&mut file, &mut out_file) {
174                let err_msg = format!("Failed to extract file: {e}");
175                let _ = tx.send(DependencyEvent::Log(err_msg.clone()));
176                anyhow::bail!(err_msg);
177            }
178
179            let extracted_file_name = file.name().to_string();
180            let _ = tx.send(DependencyEvent::Log(format!(
181                "Extracted {extracted_file_name}",
182            )));
183            extracted_count += 1;
184        }
185
186        let _ = tx.send(DependencyEvent::UpdateProgress(i + 1));
187    }
188
189    Ok(extracted_count)
190}
191
192pub fn install_dependencies(storage: &GameStorage, tx: Sender<DependencyEvent>) -> Result<()> {
193    let game_path = storage.game_path();
194
195    // Create the target directory if it doesn't exist
196    let resources_dir = game_path.join("Display_Config_Resources");
197    if let Err(e) = fs::create_dir_all(&resources_dir) {
198        let err_msg = format!("Failed to create directory: {e}");
199        let _ = tx.send(DependencyEvent::Log(err_msg.clone()));
200        anyhow::bail!(err_msg);
201    }
202
203    // We extract directly to game_path because the zip contains "Display_Config_Resources/" structure
204    let target_dir = game_path;
205    let url =
206        "https://github.com/domsleee/SS-Dat-Info/releases/download/0.4.5/Display_Config_0.4.5.zip";
207
208    let extracted_count = download_and_extract_zip(
209        url,
210        target_dir,
211        |file_name| file_name.starts_with("Display_Config_Resources/"),
212        tx.clone(),
213    )?;
214
215    let _ = tx.send(DependencyEvent::Log(format!(
216        "Extracted {extracted_count} required files",
217    )));
218
219    Ok(())
220}
221
222pub fn install_hd_fonts(storage: &GameStorage, tx: Sender<DependencyEvent>) -> Result<()> {
223    let game_path = storage.game_path();
224    // The zip file contains the full directory structure "Data/Fonts/Frnk/..."
225    // so we extract to the game root.
226    let target_dir = game_path;
227    let url = "https://github.com/domsleee/SS-Dat-Info/releases/download/0.3.2/4xFonts.zip";
228
229    let extracted_count = download_and_extract_zip(
230        url,
231        target_dir,
232        |file_name| {
233            let path = Path::new(file_name);
234            let is_tga = path.extension().is_some_and(|e| e == "TGA");
235            let is_frnk = path
236                .file_stem()
237                .is_some_and(|n| n.as_encoded_bytes().starts_with(b"FRNK"));
238            is_tga && is_frnk
239        },
240        tx.clone(),
241    )?;
242
243    let _ = tx.send(DependencyEvent::Log(format!(
244        "Extracted {extracted_count} HD font files",
245    )));
246
247    Ok(())
248}
249
250/// Starts the dependency installation process in a separate thread
251#[must_use]
252pub fn start_install_dependencies(
253    storage: &GameStorage,
254) -> std::sync::mpsc::Receiver<DependencyEvent> {
255    let (tx, rx) = channel();
256
257    let storage_clone = storage.clone();
258    thread::spawn(move || {
259        let result = install_dependencies(&storage_clone, tx.clone());
260        let error_msg = result.err().map(|e| e.to_string());
261        let _ = tx.send(DependencyEvent::Finished(error_msg));
262    });
263
264    rx
265}
266
267/// Starts the HD fonts installation process in a separate thread
268#[must_use]
269pub fn start_install_hd_fonts(storage: &GameStorage) -> std::sync::mpsc::Receiver<DependencyEvent> {
270    let (tx, rx) = channel();
271
272    let storage_clone = storage.clone();
273    thread::spawn(move || {
274        let result = install_hd_fonts(&storage_clone, tx.clone());
275        let error_msg = result.err().map(|e| e.to_string());
276        let _ = tx.send(DependencyEvent::Finished(error_msg));
277    });
278
279    rx
280}