diff --git a/bun.lockb b/bun.lockb index 38a8f27..5341a6b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b396814..59a451a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2359,6 +2359,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "junction" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72bbdfd737a243da3dfc1f99ee8d6e166480f17ab4ac84d7c34aacd73fc7bd16" +dependencies = [ + "scopeguard", + "windows-sys 0.52.0", +] + [[package]] name = "keyboard-types" version = "0.7.0" @@ -4317,6 +4327,7 @@ dependencies = [ "directories", "flate2", "futures", + "junction", "log", "regex", "reqwest", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index bbecd4c..eefb433 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -38,6 +38,7 @@ tauri-plugin-deep-link = "2" async-std = "1.13.0" closure = "0.3.0" derive_more = { version = "2.0.1", features = ["display"] } +junction = "1.2.0" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index 1aa3f6c..c2f2df7 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -20,7 +20,7 @@ pub async fn startline(state: State<'_, Mutex>) -> Result<(), String> { if let Some(p) = &appd.profile { // TODO if p.needsUpdate liner::line_up(p).await.expect("Line-up failed"); - start::start(p).map_err(|e| e.to_string()).map(|_| ()) + start::start(p).map_err(|e| { log::error!("Error launching: {}", e.to_string()); e.to_string() }).map(|_| ()) //Ok(()) } else { Err("No profile".to_owned()) @@ -126,12 +126,12 @@ pub async fn save_profile(state: State<'_, Mutex>) -> Result<(), ()> { #[tauri::command] pub async fn init_profile( state: State<'_, Mutex>, - path: PathBuf, + exe_path: PathBuf ) -> Result { - log::debug!("invoke: init_profile"); + log::debug!("invoke: init_profile({})", exe_path.to_string_lossy()); let mut appd = state.lock().await; - let new_profile = Profile::new(path); + let new_profile = Profile::new(exe_path); new_profile.save().await; appd.profile = Some(new_profile.clone()); diff --git a/rust/src/download_handler.rs b/rust/src/download_handler.rs index b3e186d..a730e75 100644 --- a/rust/src/download_handler.rs +++ b/rust/src/download_handler.rs @@ -50,6 +50,8 @@ impl DownloadHandler { cache_file_w.sync_all().await?; tokio::fs::rename(&zip_path_part, &zip_path).await?; + log::debug!("Downloaded to {}", zip_path.to_string_lossy()); + app.emit("download-end", pkg_key)?; Ok(()) diff --git a/rust/src/liner.rs b/rust/src/liner.rs index c36cf55..c3d4757 100644 --- a/rust/src/liner.rs +++ b/rust/src/liner.rs @@ -1,9 +1,22 @@ use tokio::task::JoinSet; -use anyhow::Result; +use anyhow::{Result, anyhow}; use tokio::fs; +use std::path::{Path, PathBuf}; +use ini::Ini; use crate::util; use crate::profile::Profile; +#[cfg(target_os = "linux")] +async fn symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { + fs::symlink(src, dst).await +} + +#[cfg(target_os = "windows")] +async fn symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { + //std::os::windows::fs::junction_point(src, dst) + junction::create(src, dst) +} + pub async fn line_up(p: &Profile) -> Result<()> { let dir_out = util::profile_dir(&p); log::info!("Preparing {}", dir_out.to_string_lossy()); @@ -19,9 +32,8 @@ pub async fn line_up(p: &Profile) -> Result<()> { fs::create_dir_all(dir_out.join("option")).await?; - log::debug!("--"); for m in &p.mods { - log::debug!("{}", m.0); + log::debug!("Preparing {}", m); let dir_out = util::profile_dir(&p); let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition")); let bpx = util::pkg_dir_of(namespace, &name[1..]) @@ -35,15 +47,42 @@ pub async fn line_up(p: &Profile) -> Result<()> { if opt.exists() { let x = opt.read_dir().unwrap().next().unwrap()?; if x.metadata()?.is_dir() { - fs::symlink(&x.path(), &dir_out.join("option").join(x.file_name())).await?; + symlink(&x.path(), &dir_out.join("option").join(x.file_name())).await?; } } } - log::debug!("--"); - for opt in p.path.join("option").read_dir()? { + // Todo temporary + let ini_in_raw = fs::read_to_string(p.exe_dir.join("segatools.ini")).await?; + let ini_in = Ini::load_from_str(&ini_in_raw)?; + let mut opt_dir_in = PathBuf::from( + ini_in.section(Some("vfs")) + .ok_or_else(|| anyhow!("No VFS section in segatools.ini"))? + .get("option") + .ok_or_else(|| anyhow!("No option specified in segatools.ini"))? + ); + if opt_dir_in.is_relative() { + opt_dir_in = p.exe_dir.join(opt_dir_in); + } + let opt_dir_out = &dir_out.join("option"); + + let mut ini_out = ini_in.clone(); + ini_out.with_section(Some("vfs")).set( + "option", + util::path_to_str(opt_dir_out)? + ); + ini_out.with_section(Some("unity")) + .set("enable", "1") + .set( + "targetAssembly", + util::path_to_str(dir_out.join("BepInEx").join("core").join("BepInEx.Preloader.dll"))? + ); + ini_out.write_to_file(dir_out.join("segatools.ini"))?; + + log::debug!("Option dir: {} -> {}", opt_dir_in.to_string_lossy(), opt_dir_out.to_string_lossy()); + for opt in opt_dir_in.read_dir()? { let opt = opt?; - fs::symlink(&opt.path(), &dir_out.join("option").join(opt.file_name())).await?; + symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?; } Ok(()) diff --git a/rust/src/profile.rs b/rust/src/profile.rs index d35b4ca..359afa2 100644 --- a/rust/src/profile.rs +++ b/rust/src/profile.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, path::{Path, PathBuf}}; +use std::{collections::HashSet, path::PathBuf}; use crate::{model::misc, pkg::PkgKey, util}; use serde::{Deserialize, Serialize}; @@ -10,7 +10,7 @@ use tokio::fs; #[allow(dead_code)] pub struct Profile { pub game: misc::Game, - pub path: PathBuf, + pub exe_dir: PathBuf, pub name: String, pub mods: HashSet, pub wine_runtime: Option, @@ -18,10 +18,10 @@ pub struct Profile { } impl Profile { - pub fn new(path: PathBuf) -> Profile { + pub fn new(exe_path: PathBuf) -> Profile { Profile { game: misc::Game::Ongeki, - path: path.parent().unwrap().to_owned(), + exe_dir: exe_path.parent().unwrap().to_owned(), name: "ongeki-default".to_owned(), mods: HashSet::new(), diff --git a/rust/src/start.rs b/rust/src/start.rs index eac41db..b7626a4 100644 --- a/rust/src/start.rs +++ b/rust/src/start.rs @@ -1,5 +1,7 @@ use anyhow::Result; -use tokio::process::{Child, Command}; +use std::process::Stdio; +use tokio::task::JoinSet; +use tokio::process::Command; use crate::profile::Profile; use crate::util; @@ -11,6 +13,64 @@ pub fn start(p: &Profile) -> Result { util::profile_dir(&p).join("segatools.ini"), ) .env("WINEPREFIX", p.wine_prefix.as_ref().unwrap()) - .arg(p.path.join("start.bat")) + .arg(p.exe_dir.join("start.bat")) .spawn()?) +} + +#[cfg(target_os = "windows")] +pub fn start(p: &Profile) -> Result<()> { + let ini_path = util::profile_dir(&p).join("segatools.ini"); + + log::debug!("With path {}", ini_path.to_string_lossy()); + log::info!("Launching amdaemon"); + let mut amd = Command::new("cmd.exe") + .env( + "SEGATOOLS_CONFIG_PATH", + &ini_path, + ) + .env("OPENSSL_ia32cap", ":~0x20000000") + .current_dir(&p.exe_dir) + .args(["/C", &util::path_to_str(p.exe_dir.join( "inject.exe"))?, "-d", "-k", "mu3hook.dll", "amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"]) + // Obviously this is a meme + // Output will be handled properly at a later time + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + + log::info!("Launching mu3"); + let mut game = Command::new(p.exe_dir.join( "inject.exe")) + .env( + "SEGATOOLS_CONFIG_PATH", + ini_path, + ) + .current_dir(&p.exe_dir) + .args(["-d", "-k", "mu3hook.dll", "mu3.exe", "-monitor 1", "-screen-fullscreen", "0", "-popupwindow", "-screen-width", "1080", "-screen-height", "1920"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + + tauri::async_runtime::spawn(async move { + let mut set = JoinSet::new(); + set.spawn(async move { + amd.wait().await.expect("amdaemon failed to run") + }); + + set.spawn(async move { + game.wait().await.expect("mu3 failed to run") + }); + + let res = set.join_next().await.expect("No spawn").expect("No result"); + + log::info!("One of the processes died with return code {}", res); + + _ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("amdaemon.exe").output().await; + _ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("mu3.exe").output().await; + + set.join_next().await.expect("No spawn").expect("No result"); + + log::debug!("Fin"); + }); + + Ok(()) + //Ok((amd, game)) } \ No newline at end of file diff --git a/rust/src/util.rs b/rust/src/util.rs index 4e75b65..9a5b341 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Result}; use directories::ProjectDirs; use std::path::{Path, PathBuf}; @@ -31,6 +32,11 @@ pub fn cache_dir() -> PathBuf { get_dirs().cache_dir().to_owned() } +pub fn path_to_str(p: impl AsRef) -> Result { + Ok(p.as_ref().to_str() + .ok_or_else(|| anyhow!("Invalid path: {}", p.as_ref().to_string_lossy()))?.to_owned()) +} + pub fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> { std::fs::create_dir_all(&dst).unwrap(); for entry in std::fs::read_dir(src)? { diff --git a/rust/tauri.conf.json b/rust/tauri.conf.json index 86fdd9c..37db1df 100644 --- a/rust/tauri.conf.json +++ b/rust/tauri.conf.json @@ -31,11 +31,11 @@ ], "security": { "csp": { - "img-src": "'self' asset: https: blob: data:" + "img-src": "'self' asset: https: http://asset.localhost blob: data:" }, "assetProtocol": { "enable": true, - "scope": ["**/*", "**/.*/**/*"] + "scope": ["**", "**/*", "**/.*/**/*", "**\\*", "**\\.*\\**\\*"] } } }, diff --git a/src/components/App.vue b/src/components/App.vue index 6ec25f3..11eb019 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -24,7 +24,7 @@ const loadProfile = async () => { await store.reloadProfile(); if (store.profile === null) { - const file = await open({ + const exePath = await open({ multiple: false, directory: false, filters: [ @@ -34,8 +34,8 @@ const loadProfile = async () => { }, ], }); - if (file !== null) { - await store.initProfile(file); + if (exePath !== null) { + await store.initProfile(exePath); } } if (store.profile !== null) { @@ -48,8 +48,8 @@ const loadProfile = async () => { const isProfileDisabled = computed(() => store.profile === null); -const startline = () => { - invoke('startline'); +const startline = async () => { + await invoke('startline'); //startDisabled.value = true; }; diff --git a/src/stores.ts b/src/stores.ts index 34dd1e6..18983e0 100644 --- a/src/stores.ts +++ b/src/stores.ts @@ -77,8 +77,8 @@ export const usePkgStore = defineStore('pkg', { Object.assign(this.pkg[key], rv); }, - async initProfile(path: string) { - this.prf = await invoke('init_profile', { path }); + async initProfile(exePath: string) { + this.prf = await invoke('init_profile', { exePath }); }, async saveProfile() {