From 087aac91d8954617e749cfc647125cc05864586c Mon Sep 17 00:00:00 2001 From: Haerbin23456 <60066765+Haerbin23456@users.noreply.github.com> Date: Tue, 12 May 2026 00:29:18 +0800 Subject: [PATCH 1/3] fix(everest): improve installation reliability and add cross-platform support --- src/everest.rs | 130 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 93 insertions(+), 37 deletions(-) diff --git a/src/everest.rs b/src/everest.rs index 528fd25..bdc9077 100644 --- a/src/everest.rs +++ b/src/everest.rs @@ -1,15 +1,15 @@ use crate::{ureq, wegfan}; -use anyhow::bail; +use ::ureq::get; +use anyhow::{Context, bail}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; -use ::ureq::get; use std::{ collections::HashMap, - io::{BufRead, BufReader}, + io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, - sync::{atomic::AtomicBool, Arc}, + sync::{Arc, atomic::AtomicBool}, }; #[derive(Serialize, Deserialize)] @@ -101,8 +101,8 @@ static MAGIC_STR: &str = "EverestBuild"; static MAGIC_STR_ONLY_ORIGIN_EXE: &str = "_StarJumpEnd+"; pub fn get_everest_version(game_path: &str) -> Option { - fn check_file(path: String) -> Option { - println!("Checking {path}"); + fn check_file(path: PathBuf) -> Option { + println!("Checking {}", path.display()); let buf = std::fs::read(path).ok()?; let str = unsafe { std::str::from_utf8_unchecked(&buf) }; let pos = str.find(MAGIC_STR); @@ -116,16 +116,18 @@ pub fn get_everest_version(game_path: &str) -> Option { Some(str) } - check_file(game_path.to_owned() + "/Celeste.exe") + let game_path = resolve_everest_install_path(Path::new(game_path)); + + check_file(game_path.join("Celeste.exe")) .or_else(|| { - if let Ok(data) = std::fs::read(game_path.to_owned() + "/Celeste.exe") + if let Ok(data) = std::fs::read(game_path.join("Celeste.exe")) && data .windows(MAGIC_STR_ONLY_ORIGIN_EXE.as_bytes().len()) .any(|window| window == MAGIC_STR_ONLY_ORIGIN_EXE.as_bytes()) { None } else { - check_file(game_path.to_owned() + "/Celeste.dll") + check_file(game_path.join("Celeste.dll")) } }) .or(None) @@ -139,7 +141,6 @@ fn run_command( cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); const CREATE_NO_WINDOW: u32 = 0x08000000; - const DETACHED_PROCESS: u32 = 0x00000008; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; #[cfg(target_os = "windows")] @@ -151,9 +152,38 @@ fn run_command( .ok_or_else(|| anyhow::anyhow!("Invalid installer path"))?, ); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let metadata = std::fs::metadata(&installer_path)?; + let mut permissions = metadata.permissions(); + permissions.set_mode(permissions.mode() | 0o755); + std::fs::set_permissions(&installer_path, permissions)?; + } + let mut child = cmd.spawn()?; - let stdout = child.stdout.take().unwrap(); + let stdout = child + .stdout + .take() + .context("Failed to capture installer stdout")?; + let stderr = child + .stderr + .take() + .context("Failed to capture installer stderr")?; let reader = BufReader::new(stdout); + let stderr_handle = std::thread::spawn(move || { + let mut lines = Vec::new(); + for line in BufReader::new(stderr).lines() { + match line { + Ok(line) => lines.push(line), + Err(err) => { + lines.push(format!("Failed to read installer stderr: {err}")); + break; + } + } + } + lines + }); let mut line_count = 50f32; for line in reader.lines() { @@ -162,17 +192,58 @@ fn run_command( progress_callback(line, line_count); } - let output = child.wait_with_output()?; - - let stderr = String::from_utf8(output.stderr)?; + let status = child.wait()?; + let stderr = stderr_handle + .join() + .unwrap_or_else(|_| vec!["Failed to join installer stderr reader".to_string()]) + .join("\n"); - if !output.status.success() { + if !status.success() { bail!("Command failed with error: {}", stderr); } Ok(()) } +fn resolve_everest_install_path(game_path: &Path) -> PathBuf { + #[cfg(target_os = "macos")] + { + let bundled_resources = game_path + .join("Celeste.app") + .join("Contents") + .join("Resources"); + if bundled_resources.exists() { + return bundled_resources; + } + + let resources = game_path.join("Contents").join("Resources"); + if resources.exists() { + return resources; + } + } + + game_path.to_path_buf() +} + +#[cfg(target_os = "windows")] +fn installer_name() -> anyhow::Result<&'static str> { + match std::env::consts::ARCH { + "x86_64" => Ok("MiniInstaller-win64.exe"), + "x86" => Ok("MiniInstaller-win.exe"), + arch => bail!("Unsupported Windows architecture: {arch}"), + } +} + +#[cfg(target_os = "macos")] +fn installer_name() -> anyhow::Result<&'static str> { + Ok("MiniInstaller-osx") +} + +#[cfg(target_os = "linux")] +fn installer_name() -> anyhow::Result<&'static str> { + Ok("MiniInstaller-linux") +} + pub fn download_and_install_everest( game_path: &str, url: &str, @@ -181,10 +252,8 @@ pub fn download_and_install_everest( let generate_backup = false; let temp_path = std::env::temp_dir().join("everest.zip"); - let game_path = std::path::Path::new(game_path); - let temp_path = temp_path.to_str().unwrap(); - let game_path = game_path.to_str().unwrap(); + let game_path = resolve_everest_install_path(std::path::Path::new(game_path)); let cancel_flag = Arc::new(AtomicBool::new(false)); ureq::download_file_with_progress( @@ -203,19 +272,16 @@ pub fn download_and_install_everest( let mut archive = zip::ZipArchive::new(std::fs::File::open(temp_path)?)?; let archive_len = archive.len(); - let backup_dir = std::path::Path::new(game_path).join("backup"); + let backup_dir = game_path.join("backup"); for i in 0..archive_len { let mut file = archive.by_index(i)?; let dist_name = file.mangled_name(); // strip /main/ from the name let dist_name = dist_name.strip_prefix("main/")?; - let outpath = std::path::Path::new(game_path).join(dist_name); + let outpath = game_path.join(dist_name); let status_str = format!("Extracting {}", outpath.display()); - progress_callback( - status_str, - (i as f32) / (archive_len as f32) / 2f32 * 100f32, - ); + progress_callback(status_str, 50.0 + (i as f32) / (archive_len as f32) * 40.0); if file.name().ends_with('/') { std::fs::create_dir_all(&outpath)?; } else { @@ -235,22 +301,12 @@ pub fn download_and_install_everest( let mut outfile = std::fs::File::create(&outpath)?; std::io::copy(&mut file, &mut outfile)?; + outfile.flush()?; } } - let target = match std::env::consts::ARCH { - "x86_64" => "win-x64", - "x86" => "win-x86", - _ => unimplemented!("Unsupported target"), - }; - - let installer_name = match target { - "win-x64" => "MiniInstaller-win64.exe", - "win-x86" => "MiniInstaller-win.exe", - _ => unimplemented!("Unsupported target"), - }; - - let installer_path = std::path::Path::new(game_path).join(installer_name); + progress_callback("Running Everest installer".to_string(), 90.0); + let installer_path = game_path.join(installer_name()?); run_command(installer_path, progress_callback) } From c5ab9239ecb6f61418d2ddfa28e01d50a8924732 Mon Sep 17 00:00:00 2001 From: Haerbin23456 <60066765+Haerbin23456@users.noreply.github.com> Date: Tue, 12 May 2026 18:09:07 +0800 Subject: [PATCH 2/3] fix: normalize game path handling for macOS installation and improve path verification --- src/celemod-ui/src/states.ts | 4 +- src/celemod-ui/src/utils.ts | 6 +- src/everest.rs | 24 +--- src/main.rs | 245 ++++++++++++++++++++++++++++++----- 4 files changed, 223 insertions(+), 56 deletions(-) diff --git a/src/celemod-ui/src/states.ts b/src/celemod-ui/src/states.ts index 141d220..79f4434 100644 --- a/src/celemod-ui/src/states.ts +++ b/src/celemod-ui/src/states.ts @@ -141,7 +141,7 @@ const createPersistedStateByKey = (key: string, defaultValue: T) => createPer export const [initMirror, useMirror, currentMirror] = createPersistedStateByKey('mirror', 'wegfan') export const [initGamePath, useGamePath] = createPersistedState('', storage => { if (storage?.root?.lastGamePath) - return storage.root.lastGamePath + return callRemote("normalize_game_path", storage.root.lastGamePath) const paths = callRemote("get_celeste_dirs").split("\n").filter((v: string | null) => v); return paths[0] }, (storage, data, save) => { @@ -157,4 +157,4 @@ export const [initSearchSort, useSearchSort] = createPersistedStateByKey<'new' | export const [initAutoDisableNewMods, useAutoDisableNewMods] = createPersistedStateByKey('autoDisableNewMods', false) -export const [initModComments, useModComments] = createPersistedStateByKey('modComments', {}) \ No newline at end of file +export const [initModComments, useModComments] = createPersistedStateByKey('modComments', {}) diff --git a/src/celemod-ui/src/utils.ts b/src/celemod-ui/src/utils.ts index 6b59d5c..b3749a1 100644 --- a/src/celemod-ui/src/utils.ts +++ b/src/celemod-ui/src/utils.ts @@ -134,7 +134,11 @@ export const selectGamePath = (successCallback) => { // strip file:// and Celeste.exe const prefix = "file://".length; const decoded = decodeURI(res); - const path = dirname(decoded.slice(prefix)); + const path = callRemote("normalize_game_path", dirname(decoded.slice(prefix))); + if (!callRemote("verify_celeste_install", path)) { + alert("Invalid Celeste install path."); + return; + } console.log("Selected", path); successCallback(path); return path; diff --git a/src/everest.rs b/src/everest.rs index bdc9077..771b266 100644 --- a/src/everest.rs +++ b/src/everest.rs @@ -116,7 +116,7 @@ pub fn get_everest_version(game_path: &str) -> Option { Some(str) } - let game_path = resolve_everest_install_path(Path::new(game_path)); + let game_path = Path::new(game_path); check_file(game_path.join("Celeste.exe")) .or_else(|| { @@ -205,26 +205,6 @@ fn run_command( Ok(()) } -fn resolve_everest_install_path(game_path: &Path) -> PathBuf { - #[cfg(target_os = "macos")] - { - let bundled_resources = game_path - .join("Celeste.app") - .join("Contents") - .join("Resources"); - if bundled_resources.exists() { - return bundled_resources; - } - - let resources = game_path.join("Contents").join("Resources"); - if resources.exists() { - return resources; - } - } - - game_path.to_path_buf() -} - #[cfg(target_os = "windows")] fn installer_name() -> anyhow::Result<&'static str> { match std::env::consts::ARCH { @@ -253,7 +233,7 @@ pub fn download_and_install_everest( let temp_path = std::env::temp_dir().join("everest.zip"); let temp_path = temp_path.to_str().unwrap(); - let game_path = resolve_everest_install_path(std::path::Path::new(game_path)); + let game_path = std::path::Path::new(game_path); let cancel_flag = Arc::new(AtomicBool::new(false)); ureq::download_file_with_progress( diff --git a/src/main.rs b/src/main.rs index 8d3b98d..e7512e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize}; use anyhow::{Context, bail}; -use ureq::DownloadCallbackInfo; use dirs; use everest::get_mod_cached_new; use game_scanner::prelude::Game; @@ -14,8 +13,12 @@ use std::{ fs, io::Read, path::{Path, PathBuf}, - sync::{Arc, Mutex, atomic::{AtomicBool, AtomicUsize, Ordering}}, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, AtomicUsize, Ordering}, + }, }; +use ureq::DownloadCallbackInfo; static TEST_MODE: AtomicBool = AtomicBool::new(false); @@ -44,9 +47,9 @@ lazy_static::lazy_static! { static ref DOWNLOAD_CANCEL_FLAGS: Mutex>> = Mutex::new(HashMap::new()); } -mod ureq; mod blacklist; mod everest; +mod ureq; mod wegfan; #[macro_use] @@ -123,7 +126,13 @@ fn get_invalid_zip_mod_files(mods_folder_path: &str) -> Vec { entries .filter_map(|entry| entry.ok()) .filter(|entry| entry.file_type().map(|v| v.is_file()).unwrap_or(false)) - .filter(|entry| entry.path().extension().map(|v| v == "zip").unwrap_or(false)) + .filter(|entry| { + entry + .path() + .extension() + .map(|v| v == "zip") + .unwrap_or(false) + }) .filter(|entry| !is_valid_zip_archive(&entry.path())) .filter_map(|entry| entry.file_name().into_string().ok()) .collect() @@ -237,12 +246,23 @@ fn delete_mod_files(mods_folder_path: &str, file_names: &[String]) -> anyhow::Re Ok(()) } -fn download_mod_archive(url: &str, dest: &str, progress_callback: &mut dyn FnMut(DownloadCallbackInfo), multi_thread: bool) -> anyhow::Result<()> { +fn download_mod_archive( + url: &str, + dest: &str, + progress_callback: &mut dyn FnMut(DownloadCallbackInfo), + multi_thread: bool, +) -> anyhow::Result<()> { let cancel_flag = Arc::new(AtomicBool::new(false)); download_mod_archive_with_cancel(url, dest, progress_callback, multi_thread, &cancel_flag) } -fn download_mod_archive_with_cancel(url: &str, dest: &str, progress_callback: &mut dyn FnMut(DownloadCallbackInfo), multi_thread: bool, cancel_flag: &Arc) -> anyhow::Result<()> { +fn download_mod_archive_with_cancel( + url: &str, + dest: &str, + progress_callback: &mut dyn FnMut(DownloadCallbackInfo), + multi_thread: bool, + cancel_flag: &Arc, +) -> anyhow::Result<()> { let tmp_dir = std::env::temp_dir().join("CelemodTemp").join("mods"); std::fs::create_dir_all(&tmp_dir)?; @@ -326,8 +346,14 @@ fn get_installed_mods_sync(mods_folder_path: String) -> Vec { let mut mods = Vec::new(); let mod_data = get_mod_cached_new().unwrap(); - for entry in fs::read_dir(mods_folder_path).unwrap() { - let entry = entry.unwrap(); + let Ok(entries) = fs::read_dir(mods_folder_path) else { + return mods; + }; + + for entry in entries { + let Ok(entry) = entry else { + continue; + }; println!("Checking mod entry: {:?}", entry.file_name()); let res: anyhow::Result<_> = try { if false { @@ -508,6 +534,91 @@ fn get_celestes() -> Vec { games } +fn normalize_game_path(path: &str) -> String { + normalize_game_path_buf(Path::new(path)) + .to_string_lossy() + .to_string() +} + +fn normalize_game_path_buf(path: &Path) -> PathBuf { + #[cfg(target_os = "macos")] + { + fn has_game_artifact(path: &Path) -> bool { + path.join("Celeste.exe").is_file() + || path.join("Celeste.dll").is_file() + || path.join("Celeste").is_file() + } + + fn is_named(path: &Path, name: &str) -> bool { + path.file_name().and_then(|v| v.to_str()) == Some(name) + } + + fn resources_if_valid(path: PathBuf) -> Option { + if path.is_dir() + && (has_game_artifact(&path) + || path + .parent() + .map(|contents| contents.join("MacOS").join("Celeste").is_file()) + .unwrap_or(false)) + { + Some(path) + } else { + None + } + } + + let path = if path.is_file() { + path.parent().unwrap_or(path) + } else { + path + }; + + if is_named(path, "Resources") { + if let Some(resources) = resources_if_valid(path.to_path_buf()) { + return resources; + } + } + + if is_named(path, "MacOS") { + if let Some(contents) = path.parent() { + if let Some(resources) = resources_if_valid(contents.join("Resources")) { + return resources; + } + } + } + + if is_named(path, "Contents") { + if let Some(resources) = resources_if_valid(path.join("Resources")) { + return resources; + } + } + + if path.extension().and_then(|v| v.to_str()) == Some("app") { + if let Some(resources) = resources_if_valid(path.join("Contents").join("Resources")) { + return resources; + } + } + + if let Some(resources) = + resources_if_valid(path.join("Celeste.app").join("Contents").join("Resources")) + { + return resources; + } + + if has_game_artifact(path) { + if let Some(parent) = path.parent() { + if is_named(parent, "Contents") { + if let Some(resources) = resources_if_valid(parent.join("Resources")) { + return resources; + } + } + } + } + } + + path.to_path_buf() +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] enum DownloadStatus { Waiting, @@ -543,6 +654,10 @@ impl Handler { _use_cn_proxy: bool, multi_thread: bool, ) { + if let Err(e) = std::fs::create_dir_all(&mods_dir) { + eprintln!("Failed to create mods dir {}: {}", mods_dir, e); + } + let dest = Path::new(&mods_dir) .join(make_path_compatible_name(&name) + ".zip") .to_str() @@ -585,7 +700,11 @@ impl Handler { Err(e) => { eprintln!("Failed to get mod data: {}", e); callback - .call(None, &make_args!(format!("Failed to get mod data: {}", e)), None) + .call( + None, + &make_args!(format!("Failed to get mod data: {}", e)), + None, + ) .unwrap(); return; } @@ -600,7 +719,8 @@ impl Handler { let post_callback: Arc, &str) + Send + Sync> = { let sync_cb = Arc::clone(&sync_cb); Arc::new(move |tasklist: &Vec, state: &str| { - sync_cb.0 + sync_cb + .0 .call( None, &make_args!(serde_json::to_string(tasklist).unwrap(), state), @@ -679,7 +799,8 @@ impl Handler { deps.into_iter() .filter_map(|(dep, min_ver)| { if installed_mods.iter().any(|m| { - m.name == dep && compare_version(&m.version, &min_ver) >= 0 + m.name == dep + && compare_version(&m.version, &min_ver) >= 0 }) { return None; } @@ -847,29 +968,53 @@ impl Handler { } get_celestes() .iter() - .map(|game| game.path.clone().unwrap().to_str().unwrap().to_string()) + .map(|game| normalize_game_path_buf(&game.path.clone().unwrap())) + .map(|path| path.to_string_lossy().to_string()) .collect::>() .join("\n") } fn start_game(&self, path: String) { + let path = normalize_game_path(&path); let celestes = get_celestes(); - let game = celestes - .iter() - .find(|game| game.path.clone().unwrap().to_str().unwrap() == path) - .unwrap(); - game_scanner::manager::launch_game(game).unwrap(); + if let Some(game) = celestes.iter().find(|game| { + normalize_game_path_buf(&game.path.clone().unwrap()) + .to_string_lossy() + .to_string() + == path + }) { + game_scanner::manager::launch_game(game).unwrap(); + } else { + self.start_game_directly(path, false); + } } fn start_game_directly(&self, path: String, origin: bool) { - #[cfg(windows)] - let file = "Celeste.exe"; + let path = normalize_game_path(&path); + let path = Path::new(&path); - #[cfg(unix)] - let file = "Celeste"; + #[cfg(windows)] + let game = path.join("Celeste.exe"); + + #[cfg(all(unix, not(target_os = "macos")))] + let game = path.join("Celeste"); + + #[cfg(target_os = "macos")] + let game = { + let direct = path.join("Celeste"); + if direct.exists() { + direct + } else if path.file_name().and_then(|name| name.to_str()) == Some("Resources") { + path.parent().unwrap_or(path).join("MacOS").join("Celeste") + } else { + direct + } + }; - let game = Path::new(&path).join(file); - let game_origin = Path::new(&path).join("orig").join(file); + let game_origin = path.join("orig").join( + game.file_name() + .unwrap_or_else(|| std::ffi::OsStr::new("Celeste")), + ); if origin { if game_origin.exists() { @@ -948,6 +1093,7 @@ impl Handler { fn get_blacklist_profiles(&self, game_path: String, callback: sciter::Value) { std::thread::spawn(move || { + let game_path = normalize_game_path(&game_path); let profiles = blacklist::get_mod_blacklist_profiles(&game_path); callback .call( @@ -965,6 +1111,7 @@ impl Handler { profile_name: String, always_on_mods: String, ) -> String { + let game_path = normalize_game_path(&game_path); let always_on_mods: Vec = serde_json::from_str(&always_on_mods).unwrap(); let result = blacklist::apply_mod_blacklist_profile(&game_path, &profile_name, &always_on_mods); @@ -984,6 +1131,7 @@ impl Handler { mod_files: String, enabled: bool, ) -> String { + let game_path = normalize_game_path(&game_path); let mod_names: Vec = serde_json::from_str(&mod_names).unwrap(); let mod_files: Vec = serde_json::from_str(&mod_files).unwrap(); let mods: Vec<(&String, &String)> = mod_names.iter().zip(mod_files.iter()).collect(); @@ -999,6 +1147,7 @@ impl Handler { } fn new_mod_blacklist_profile(&self, game_path: String, profile_name: String) -> String { + let game_path = normalize_game_path(&game_path); let result = blacklist::new_mod_blacklist_profile(&game_path, &profile_name); if let Err(e) = result { eprintln!("Failed to create blacklist profile: {}", e); @@ -1009,6 +1158,7 @@ impl Handler { } fn get_current_profile(&self, game_path: String) -> String { + let game_path = normalize_game_path(&game_path); let result = blacklist::get_current_profile(&game_path); if let Err(e) = result { eprintln!("Failed to get current profile: {}", e); @@ -1019,6 +1169,7 @@ impl Handler { } fn remove_mod_blacklist_profile(&self, game_path: String, profile_name: String) -> String { + let game_path = normalize_game_path(&game_path); let result = blacklist::remove_mod_blacklist_profile(&game_path, &profile_name); if let Err(e) = result { eprintln!("Failed to remove blacklist profile: {}", e); @@ -1029,6 +1180,7 @@ impl Handler { } fn get_current_blacklist_content(&self, game_path: String) -> String { + let game_path = normalize_game_path(&game_path); let result = blacklist::get_current_blacklist_content(&game_path); if let Err(e) = result { eprintln!("Failed to get current blacklist content: {}", e); @@ -1100,6 +1252,7 @@ impl Handler { } fn sync_blacklist_profile_from_file(&self, game_path: String, profile_name: String) -> String { + let game_path = normalize_game_path(&game_path); let result = blacklist::sync_blacklist_profile_from_file(&game_path, &profile_name); if let Err(e) = result { eprintln!("Failed to sync blacklist profile: {}", e); @@ -1109,7 +1262,13 @@ impl Handler { } } - fn set_mod_options_order(&self, game_path: String, profile_name: String, order_json: String) -> String { + fn set_mod_options_order( + &self, + game_path: String, + profile_name: String, + order_json: String, + ) -> String { + let game_path = normalize_game_path(&game_path); let order: Vec = match serde_json::from_str(&order_json) { Ok(v) => v, Err(e) => return format!("Failed to parse order: {}", e), @@ -1183,6 +1342,7 @@ impl Handler { fn delete_mods(&self, game_path: String, mod_names: String, callback: sciter::Value) { std::thread::spawn(move || { + let game_path = normalize_game_path(&game_path); let mods_folder_path = Path::new(&game_path) .join("Mods") .to_string_lossy() @@ -1208,7 +1368,12 @@ impl Handler { }); } - fn delete_mod_files(&self, mods_folder_path: String, file_names: String, callback: sciter::Value) { + fn delete_mod_files( + &self, + mods_folder_path: String, + file_names: String, + callback: sciter::Value, + ) { std::thread::spawn(move || { let file_names: Vec = serde_json::from_str(&file_names).unwrap_or_default(); let result = match delete_mod_files(&mods_folder_path, &file_names) { @@ -1225,6 +1390,7 @@ impl Handler { let version = if is_test_mode() { "4000".to_string() } else { + let game_path = normalize_game_path(&game_path); everest::get_everest_version(&game_path) .map(|v| v.to_string()) .unwrap_or_default() @@ -1244,6 +1410,7 @@ impl Handler { callback.call(None, &make_args!("Success"), None).unwrap(); return; } + let game_path = normalize_game_path(&game_path); let callback2 = callback.clone(); match everest::download_and_install_everest(&game_path, &url, &mut |msg, progress| { callback @@ -1312,16 +1479,32 @@ impl Handler { if is_test_mode() && path == get_test_game_path().to_string_lossy() { return true; } + let path = normalize_game_path(&path); let path = Path::new(&path); - let checklist = vec!["Celeste.exe", "Celeste"]; + let checklist = vec!["Celeste.exe", "Celeste", "Celeste.dll"]; for file in checklist { if path.join(file).exists() { return true; } } + #[cfg(target_os = "macos")] + { + if path.file_name().and_then(|name| name.to_str()) == Some("Resources") + && path + .parent() + .map(|contents| contents.join("MacOS").join("Celeste").exists()) + .unwrap_or(false) + { + return true; + } + } false } + fn normalize_game_path(&self, path: String) -> String { + normalize_game_path(&path) + } + fn show_log_window(&self) { #[cfg(windows)] { @@ -1365,6 +1548,7 @@ impl sciter::EventHandler for Handler { fn do_self_update(String, Value); fn start_game_directly(String, bool); fn verify_celeste_install(String); + fn normalize_game_path(String); fn get_mod_latest_info(Value); fn show_log_window(); fn get_current_blacklist_content(String); @@ -1489,20 +1673,19 @@ fn main() { #[cfg(not(target_os = "windows"))] const INDEX_HTML: &str = "index.html"; - #[cfg(debug_assertions)] frame.load_html( read_to_string_bom(Path::new("../../src/celemod-ui/debug_index.html")) .unwrap() - .as_bytes(), Some( - &format!("app://{}", INDEX_HTML) - )); + .as_bytes(), + Some(&format!("app://{}", INDEX_HTML)), + ); #[cfg(not(debug_assertions))] { frame .archive_handler(include_bytes!("../resources/dist.rc")) .unwrap(); - + frame.load_file(&format!("this://app/{}", INDEX_HTML)); } From 406c489d76d64d056f537c50c6d5894ad42cf708 Mon Sep 17 00:00:00 2001 From: Haerbin23456 <60066765+Haerbin23456@users.noreply.github.com> Date: Tue, 12 May 2026 19:42:38 +0800 Subject: [PATCH 3/3] fix: update installation progress messages for clarity and accuracy --- src/celemod-ui/src/routes/Everest.tsx | 4 ++-- src/everest.rs | 18 ++++++++++-------- src/main.rs | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/celemod-ui/src/routes/Everest.tsx b/src/celemod-ui/src/routes/Everest.tsx index a8e4d06..c6d34f8 100644 --- a/src/celemod-ui/src/routes/Everest.tsx +++ b/src/celemod-ui/src/routes/Everest.tsx @@ -106,7 +106,7 @@ export const Everest = () => { setInstallingUrl(url); setInstallProgress(null); setFailedReason(null); - setInstallState('Downloading Everest'); + setInstallState('[1/3] Download Everest'); callRemote( 'download_and_install_everest', gamePath, @@ -284,7 +284,7 @@ export const Everest = () => { />
- {installState === 'Downloading Everest' + {installState?.startsWith('[1/3]') ? _i18n.t('正在下载') : _i18n.t('正在安装')}
diff --git a/src/everest.rs b/src/everest.rs index 771b266..8637fcf 100644 --- a/src/everest.rs +++ b/src/everest.rs @@ -185,11 +185,11 @@ fn run_command( lines }); - let mut line_count = 50f32; + let mut line_count = 0f32; for line in reader.lines() { let line = line?; - line_count += 0.5; - progress_callback(line, line_count); + line_count = (line_count + 0.5).min(99.0); + progress_callback(format!("[3/3] Run MiniInstaller: {line}"), line_count); } let status = child.wait()?; @@ -202,6 +202,8 @@ fn run_command( bail!("Command failed with error: {}", stderr); } + progress_callback("[3/3] Run MiniInstaller".to_string(), 100.0); + Ok(()) } @@ -240,13 +242,13 @@ pub fn download_and_install_everest( url, temp_path, &mut |callback| { - progress_callback("Downloading Everest".to_string(), callback.progress); + progress_callback("[1/3] Download Everest".to_string(), callback.progress); }, false, &cancel_flag, )?; - progress_callback("Installing Everest".to_string(), 50.0); + progress_callback("[2/3] Extract Everest files".to_string(), 0.0); // unzip everest/main/* to game_path and overwrite all let mut archive = zip::ZipArchive::new(std::fs::File::open(temp_path)?)?; @@ -260,8 +262,8 @@ pub fn download_and_install_everest( // strip /main/ from the name let dist_name = dist_name.strip_prefix("main/")?; let outpath = game_path.join(dist_name); - let status_str = format!("Extracting {}", outpath.display()); - progress_callback(status_str, 50.0 + (i as f32) / (archive_len as f32) * 40.0); + let status_str = format!("[2/3] Extract Everest files: {}", outpath.display()); + progress_callback(status_str, (i as f32) / (archive_len as f32) * 100.0); if file.name().ends_with('/') { std::fs::create_dir_all(&outpath)?; } else { @@ -285,7 +287,7 @@ pub fn download_and_install_everest( } } - progress_callback("Running Everest installer".to_string(), 90.0); + progress_callback("[3/3] Run MiniInstaller".to_string(), 0.0); let installer_path = game_path.join(installer_name()?); run_command(installer_path, progress_callback) diff --git a/src/main.rs b/src/main.rs index e7512e7..d854957 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1418,7 +1418,7 @@ impl Handler { .unwrap(); }) { Ok(()) => { - callback2.call(None, &make_args!("Success"), None).unwrap(); + callback2.call(None, &make_args!("Success", 100.0), None).unwrap(); } Err(e) => { callback2