use serde::{Deserialize, Serialize}; use tauri::AppHandle; use tauri::Emitter; use std::{collections::BTreeSet, path::PathBuf, process::Stdio}; use crate::model::config::BepInEx; use crate::profiles::fixed_name; use crate::{model::{config::{Display, DisplayMode, Network, Segatools}, misc::Game, segatools_base::segatools_base}, pkg::PkgKey, util}; use super::{Profile, ProfileMeta, ProfilePaths}; use anyhow::{anyhow, Result}; use std::fs::File; use tokio::process::Command; use tokio::task::JoinSet; #[derive(Deserialize, Serialize, Clone)] pub struct OngekiProfile { // this is an Option only to deceive serde // file serialization doesn't need the name, but IPC does #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, pub mods: BTreeSet, pub sgt: Segatools, pub display: Display, pub network: Network, pub bepinex: BepInEx, #[cfg(not(target_os = "windows"))] pub wine: crate::model::config::Wine, } impl Profile for OngekiProfile { fn new(name: String) -> Result { let name = fixed_name(&ProfileMeta { name, game: Game::Ongeki }, true); let p = OngekiProfile { name: Some(name.clone()), mods: BTreeSet::new(), sgt: Segatools::default(), display: Display::default(), network: Network::default(), bepinex: BepInEx::default(), #[cfg(not(target_os = "windows"))] wine: crate::model::config::Wine::default(), }; p.save()?; std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base())?; log::debug!("created profile-ongeki-{}", &name); Ok(p) } fn load(name: String) -> Result { let path = util::profile_config_dir(&Game::Ongeki, &name).join("profile.json"); if let Ok(s) = std::fs::read_to_string(&path) { let mut data = serde_json::from_str::(&s) .map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?; data.name = Some(name); Ok(data) } else { Err(anyhow!("Unable to open {:?}", path)) } } fn save(&self) -> Result<()> { let path = self.config_dir().join("profile.json"); let mut cpy = self.clone(); cpy.name = None; let s = serde_json::to_string_pretty(&cpy)?; if !self.config_dir().exists() { std::fs::create_dir(self.config_dir()) .map_err(|e| anyhow!("error when creating profile directory: {}", e))?; } std::fs::write(&path, s) .map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?; log::info!("Written to {:?}", path); Ok(()) } async fn start(&self, app: AppHandle) -> Result<()> { let ini_path = self.data_dir().join("segatools.ini"); log::debug!("With path {:?}", ini_path); let mut game_builder; let mut amd_builder; let target_path = PathBuf::from(&self.sgt.target); let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?; let sgt_dir = self.sgt.hook_dir()?; #[cfg(target_os = "windows")] { game_builder = Command::new(sgt_dir.join("inject.exe")); amd_builder = Command::new("cmd.exe"); } #[cfg(target_os = "linux")] { game_builder = Command::new(&self.wine.runtime); amd_builder = Command::new(&self.wine.runtime); game_builder.arg(sgt_dir.join("inject.exe")); amd_builder.arg("cmd.exe"); } amd_builder.env( "SEGATOOLS_CONFIG_PATH", &ini_path, ) .current_dir(&exe_dir) .arg("/C") .arg(&sgt_dir.join("inject.exe")) .args(["-d", "-k"]) .arg(sgt_dir.join("mu3hook.dll")) .args(["amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json" ]); game_builder .env( "SEGATOOLS_CONFIG_PATH", ini_path, ) .env( "INOHARA_CONFIG_PATH", self.config_dir().join("inohara.cfg"), ) .current_dir(&exe_dir) .args(["-d", "-k"]) .arg(sgt_dir.join("mu3hook.dll")) .args([ "mu3.exe", "-monitor 1", "-screen-width", &self.display.rez.0.to_string(), "-screen-height", &self.display.rez.1.to_string(), "-screen-fullscreen", if self.display.mode == DisplayMode::Fullscreen { "1" } else { "0" } ]); if self.display.mode == DisplayMode::Borderless { game_builder.arg("-popupwindow"); } #[cfg(target_os = "linux")] { amd_builder.env("WINEPREFIX", &self.wine.prefix); game_builder.env("WINEPREFIX", &self.wine.prefix); } let amd_log = File::create(self.data_dir().join("amdaemon.log"))?; let game_log = File::create(self.data_dir().join("mu3.log"))?; amd_builder .stdout(Stdio::from(amd_log)); // do they use stderr? game_builder .stdout(Stdio::from(game_log)); #[cfg(target_os = "windows")] { amd_builder.creation_flags(util::CREATE_NO_WINDOW); game_builder.creation_flags(util::CREATE_NO_WINDOW); } if self.sgt.intel == true { amd_builder.env("OPENSSL_ia32cap", ":~0x20000000"); } util::pkill("amdaemon.exe").await; log::info!("Launching amdaemon: {:?}", amd_builder); log::info!("Launching mu3: {:?}", game_builder); let mut amd = amd_builder.spawn()?; let mut game = game_builder.spawn()?; let mut set = JoinSet::new(); set.spawn(async move { (amd.wait().await.expect("amdaemon failed to run"), "amdaemon.exe") }); set.spawn(async move { (game.wait().await.expect("mu3 failed to run"), "mu3.exe") }); if let Err(e) = app.emit("launch-start", "") { log::warn!("Unable to emit launch-start: {}", e); } let (rc, process_name) = set.join_next().await.expect("No spawn").expect("No result"); log::info!("{} died with return code {}", process_name, rc); if process_name == "amdaemon.exe" { util::pkill("mu3.exe").await; } else { util::pkill("amdaemon.exe").await; } set.join_next().await.expect("No spawn").expect("No result"); log::debug!("Fin"); if let Err(e) = app.emit("launch-end", "") { log::warn!("Unable to emit launch-end: {}", e); } Ok(()) } } impl ProfilePaths for OngekiProfile { fn config_dir(&self) -> PathBuf { util::profile_config_dir(&Game::Ongeki, &self.name.as_ref().unwrap()) } fn data_dir(&self) -> PathBuf { util::data_dir().join(format!("profile-{}-{}", &Game::Ongeki, self.name.as_ref().unwrap())) } } impl ProfilePaths for ProfileMeta { fn config_dir(&self) -> PathBuf { util::profile_config_dir(&self.game, &self.name) } fn data_dir(&self) -> PathBuf { util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name)) } }