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/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 528fd25..8637fcf 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 = 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,28 +152,80 @@ 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;
+ 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 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);
}
+ progress_callback("[3/3] Run MiniInstaller".to_string(), 100.0);
+
Ok(())
}
+#[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,41 +234,36 @@ 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 = std::path::Path::new(game_path);
let cancel_flag = Arc::new(AtomicBool::new(false));
ureq::download_file_with_progress(
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)?)?;
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 status_str = format!("Extracting {}", outpath.display());
- progress_callback(
- status_str,
- (i as f32) / (archive_len as f32) / 2f32 * 100f32,
- );
+ let outpath = game_path.join(dist_name);
+ 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 {
@@ -235,22 +283,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("[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 8d3b98d..d854957 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
@@ -1251,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
@@ -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));
}