Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/celemod-ui/src/routes/Everest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -284,7 +284,7 @@ export const Everest = () => {
/>
</div>
<div className="tip">
{installState === 'Downloading Everest'
{installState?.startsWith('[1/3]')
? _i18n.t('正在下载')
: _i18n.t('正在安装')}
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/celemod-ui/src/states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ const createPersistedStateByKey = <T>(key: string, defaultValue: T) => createPer
export const [initMirror, useMirror, currentMirror] = createPersistedStateByKey('mirror', 'wegfan')
export const [initGamePath, useGamePath] = createPersistedState<string>('', 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) => {
Expand All @@ -157,4 +157,4 @@ export const [initSearchSort, useSearchSort] = createPersistedStateByKey<'new' |

export const [initAutoDisableNewMods, useAutoDisableNewMods] = createPersistedStateByKey('autoDisableNewMods', false)

export const [initModComments, useModComments] = createPersistedStateByKey('modComments', {})
export const [initModComments, useModComments] = createPersistedStateByKey('modComments', {})
6 changes: 5 additions & 1 deletion src/celemod-ui/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
124 changes: 81 additions & 43 deletions src/everest.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -101,8 +101,8 @@ static MAGIC_STR: &str = "EverestBuild";
static MAGIC_STR_ONLY_ORIGIN_EXE: &str = "_StarJumpEnd+<StartCirclingPlayer>";

pub fn get_everest_version(game_path: &str) -> Option<i32> {
fn check_file(path: String) -> Option<i32> {
println!("Checking {path}");
fn check_file(path: PathBuf) -> Option<i32> {
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);
Expand All @@ -116,16 +116,18 @@ pub fn get_everest_version(game_path: &str) -> Option<i32> {
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)
Expand All @@ -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")]
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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)
}
Loading
Loading