diff --git a/rust/Cargo.lock b/rust/Cargo.lock index c326aa6..9f7c0dc 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -147,6 +147,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "ashpd" version = "0.10.2" @@ -1480,6 +1486,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1999,6 +2011,18 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", +] [[package]] name = "heck" @@ -4566,6 +4590,7 @@ dependencies = [ "tauri-plugin-single-instance", "tokio", "winsafe 0.0.23", + "yaml-rust2", "zip", ] @@ -6606,6 +6631,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "yaml-rust2" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232bdb534d65520716bef0bbb205ff8f2db72d807b19c0bc3020853b92a0cd4b" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6f88286..69720f1 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -40,6 +40,7 @@ closure = "0.3.0" derive_more = { version = "2.0.1", features = ["display"] } junction = "1.2.0" tauri-plugin-fs = "2" +yaml-rust2 = "0.10.0" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-cli = "2" diff --git a/rust/src/appdata.rs b/rust/src/appdata.rs index d6f34d5..b4d3c66 100644 --- a/rust/src/appdata.rs +++ b/rust/src/appdata.rs @@ -1,7 +1,8 @@ use std::hash::{DefaultHasher, Hash, Hasher}; +use crate::profiles::AnyProfile; use crate::{model::misc::Game, pkg::PkgKey}; use crate::pkg_store::PackageStore; -use crate::{util, Profile}; +use crate::util; use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use tauri::AppHandle; @@ -11,11 +12,15 @@ pub struct GlobalConfig { pub recent_profile: Option<(Game, String)> } +pub struct GlobalState { + pub remain_open: bool +} + pub struct AppData { - pub profile: Option, + pub profile: Option, pub pkgs: PackageStore, pub cfg: GlobalConfig, - pub remain_open: bool, + pub state: GlobalState, } impl AppData { @@ -25,15 +30,15 @@ impl AppData { .unwrap_or_default(); let profile = match cfg.recent_profile { - Some((ref game, ref name)) => Profile::load(game.clone(), name.clone()).ok(), + Some((ref game, ref name)) => AnyProfile::load(game.clone(), name.clone()).ok(), None => None }; AppData { - profile, + profile: profile, pkgs: PackageStore::new(apph.clone()), cfg, - remain_open: true + state: GlobalState { remain_open: true } } } @@ -42,7 +47,7 @@ impl AppData { } pub fn switch_profile(&mut self, game: Game, name: String) -> Result<()> { - match Profile::load(game.clone(), name.clone()) { + match AnyProfile::load(game.clone(), name.clone()) { Ok(profile) => { self.profile = Some(profile); self.cfg.recent_profile = Some((game, name)); @@ -67,12 +72,12 @@ impl AppData { let loc = pkg.loc .clone() .ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?; - profile.data.mods.insert(key); + profile.pkgs_mut().insert(key); for d in &loc.dependencies { _ = self.toggle_package(d.clone(), true); } } else { - profile.data.mods.remove(&key); + profile.pkgs_mut().remove(&key); for (ckey, pkg) in self.pkgs.get_all() { if let Some(loc) = pkg.loc { if loc.dependencies.contains(&key) { @@ -85,10 +90,10 @@ impl AppData { Ok(()) } - pub fn sum_packages(&self, p: &Profile) -> String { + pub fn sum_packages(&self, p: &AnyProfile) -> String { let mut hasher = DefaultHasher::new(); - for pkg in &p.data.mods { - let x = self.pkgs.get(pkg).unwrap().loc.as_ref().unwrap(); + for pkg in p.pkgs().into_iter() { + let x = self.pkgs.get(&pkg).unwrap().loc.as_ref().unwrap(); pkg.hash(&mut hasher); x.version.hash(&mut hasher); } diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index 3293be2..cecac67 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -2,39 +2,42 @@ use log; use std::collections::HashMap; use tokio::sync::Mutex; use tokio::fs; - +use tauri::{AppHandle, Manager, State}; use crate::model::misc::Game; use crate::pkg::{Package, PkgKey}; use crate::pkg_store::InstallResult; -use crate::profile::Profile; +use crate::profiles::ongeki::OngekiProfile; +use crate::profiles::{AnyProfile, Profile, ProfileMeta, ProfilePaths}; use crate::appdata::AppData; -use crate::{liner, start, util}; - -use tauri::{AppHandle, Manager, State}; +use crate::util; #[tauri::command] pub async fn startline(app: AppHandle) -> Result<(), String> { log::debug!("invoke: startline"); - let app_copy = app.clone(); let state = app.state::>(); - let appd = state.lock().await; + let mut hash = "".to_owned(); + let mut appd = state.lock().await; if let Some(p) = &appd.profile { - let hash = appd.sum_packages(p); - liner::line_up(p, hash).await - .map_err(|e| e.to_string())?; + hash = appd.sum_packages(p); + } + if let Some(p) = &mut appd.profile { + p.line_up(app.clone(), hash).await + .map_err(|e| format!("Lineup failed:\n{}", e))?; + p.start(app.clone()).await + .map_err(|e| format!("Startup failed:\n{}", e))?; - start::start(p, app_copy).await - .map_err(|e| e.to_string()) + Ok(()) } else { Err("No profile".to_owned()) } + } #[tauri::command] pub async fn kill() -> Result<(), String> { - start::pkill("amdaemon.exe").await; + util::pkill("amdaemon.exe").await; // The start routine will kill the other process Ok(()) @@ -108,13 +111,35 @@ pub async fn fetch_listings(state: State<'_, Mutex>) -> Result<(), Stri } #[tauri::command] -pub async fn list_profiles() -> Result, String> { +pub async fn list_profiles() -> Result, String> { log::debug!("invoke: list_profiles"); - let list = Profile::list().await.map_err(|e| e.to_string())?; + let list = crate::profiles::list_profiles().await.map_err(|e| e.to_string())?; Ok(list) } +#[tauri::command] +pub async fn init_profile( + state: State<'_, Mutex>, + game: Game, + name: String +) -> Result<(), String> { + log::debug!("invoke: init_profile({}, {})", game, name); + + let mut appd = state.lock().await; + let new_profile = OngekiProfile::new(name) + .map_err(|e| format!("Unable to create profile: {}", e))?; + + fs::create_dir_all(new_profile.config_dir()).await + .map_err(|e| format!("Unable to create the profile config directory: {}", e))?; + fs::create_dir_all(new_profile.data_dir()).await + .map_err(|e| format!("Unable to create the profile data directory: {}", e))?; + + appd.profile = Some(AnyProfile::OngekiProfile(new_profile.clone())); + + Ok(()) +} + #[tauri::command] pub async fn load_profile(state: State<'_, Mutex>, game: Game, name: String) -> Result<(), String> { log::debug!("invoke: load_profile({} {:?})", game, name); @@ -125,7 +150,41 @@ pub async fn load_profile(state: State<'_, Mutex>, game: Game, name: St } #[tauri::command] -pub async fn get_current_profile(state: State<'_, Mutex>) -> Result, ()> { +pub async fn rename_profile( + state: State<'_, Mutex>, + profile: ProfileMeta, + name: String +) -> Result<(), String> { + log::debug!("invoke: rename_profile({:?} {:?})", profile, name); + + let new_meta = ProfileMeta { + game: profile.game.clone(), + name: name.clone() + }; + + if new_meta.config_dir().exists() { + return Err(format!("Profile {} already exists", &name)); + } + + fs::rename(profile.config_dir(), new_meta.config_dir()).await + .map_err(|e| format!("Unable to rename: {}", e))?; + + if let Err(e) = fs::rename(profile.data_dir(), new_meta.data_dir()).await { + log::warn!("Unable to move data dir {}->{}: {}", &profile.name, &name, e); + } + + let mut appd = state.lock().await; + if let Some(current) = &mut appd.profile { + if current.meta() == profile { + current.rename(name); + } + } + + Ok(()) +} + +#[tauri::command] +pub async fn get_current_profile(state: State<'_, Mutex>) -> Result, ()> { log::debug!("invoke: get_current_profile"); let appd = state.lock().await; @@ -133,41 +192,16 @@ pub async fn get_current_profile(state: State<'_, Mutex>) -> Result>) -> Result<(), ()> { +pub async fn save_current_profile(state: State<'_, Mutex>, profile: AnyProfile) -> Result<(), String> { log::debug!("invoke: save_current_profile"); - let appd = state.lock().await; - if let Some(p) = &appd.profile { - p.save().await; - } else { - log::warn!("No profile to save"); - } + let mut appd = state.lock().await; + profile.save().map_err(|e| e.to_string())?; + appd.profile = Some(profile); Ok(()) } -#[tauri::command] -pub async fn init_profile( - state: State<'_, Mutex>, - game: Game, - name: String -) -> Result { - log::debug!("invoke: init_profile({}, {})", game, name); - - let mut appd = state.lock().await; - let new_profile = Profile::new(game, name); - - fs::create_dir_all(new_profile.config_dir()).await - .map_err(|e| format!("Unable to create the profile config directory: {}", e))?; - fs::create_dir_all(new_profile.data_dir()).await - .map_err(|e| format!("Unable to create the profile data directory: {}", e))?; - - appd.profile = Some(new_profile.clone()); - new_profile.save().await; - - Ok(new_profile) -} - #[tauri::command] pub async fn list_platform_capabilities() -> Result, ()> { log::debug!("invoke: list_platform_capabilities"); @@ -179,22 +213,6 @@ pub async fn list_platform_capabilities() -> Result, ()> { return Ok(vec!["wine".to_owned()]); } -#[tauri::command] -pub async fn set_cfg( - state: State<'_, Mutex>, - key: String, - value: serde_json::Value -) -> Result<(), ()> { - log::debug!("invoke: set_cfg({}, {})", key, value); - - let mut appd = state.lock().await; - if let Some(p) = &mut appd.profile { - p.data.cfg.insert(key, value); - } - - Ok(()) -} - #[tauri::command] #[cfg(target_os = "windows")] pub async fn list_displays() -> Result, String> { diff --git a/rust/src/display.rs b/rust/src/display.rs deleted file mode 100644 index 5298b93..0000000 --- a/rust/src/display.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::profile::Profile; -use anyhow::Result; - -#[cfg(target_os = "windows")] -pub struct DisplayInfo { - primary: String, - target: String, - target_settings: displayz::DisplaySettings -} - -#[allow(dead_code)] -#[cfg(not(target_os = "windows"))] -pub async fn prepare_display(_: &Profile) -> Result<()> { - Ok(()) -} - -#[cfg(target_os = "windows")] -pub async fn prepare_display(p: &Profile) -> Result> { - use anyhow::anyhow; - use displayz::{query_displays, Orientation, Resolution, Frequency}; - - let display_name = p.get_str("display", "default"); - let rotation = p.get_int("display-rotation", 0); - if display_name == "default" { - log::debug!("prepare display: skip"); - return Ok(None); - } - - let display_set = query_displays()?; - - let primary = display_set - .displays() - .find(|display| display.is_primary()) - .ok_or_else(|| anyhow!("Primary display not found"))?; - - let target = display_set - .displays() - .find(|display| display.name() == display_name) - .ok_or_else(|| anyhow!("Display {} not found", display_name))?; - - target.set_primary()?; - let settings = target.settings() - .as_ref() - .ok_or_else(|| anyhow!("Unable to query display settings"))?; - - let res = DisplayInfo { - primary: primary.name().to_owned(), - target: target.name().to_owned(), - target_settings: settings.borrow().clone() - }; - - if rotation == 90 || rotation == 270 { - let rez = settings.borrow_mut().resolution; - settings.borrow_mut().orientation = if rotation == 90 { Orientation::PortraitFlipped } else { Orientation::Portrait }; - if rez.height < rez.width { - settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width); - } - } - - let frequency: u32 = p.get_int("frequency", 60) - .try_into() - .map_err(|e| anyhow!("Invalid display frequency: {}", e))?; - - let width: u32 = p.get_int("rez-w", 1080) - .try_into() - .map_err(|e| anyhow!("Invalid display width: {}", e))?; - - let height: u32 = p.get_int("rez-h", 1080) - .try_into() - .map_err(|e| anyhow!("Invalid display height: {}", e))?; - - settings.borrow_mut().frequency = Frequency::new(frequency); - - if p.get_str("display-mode", "borderless") == "borderless" && p.get_bool("borderless-fullscreen", false) { - settings.borrow_mut().resolution = Resolution::new(width, height); - } - - display_set.apply()?; - displayz::refresh()?; - - log::debug!("prepare display: done"); - - Ok(Some(res)) -} - -#[cfg(target_os = "windows")] -pub async fn undo_display(info: DisplayInfo) -> Result<()> { - use anyhow::anyhow; - use displayz::query_displays; - - let display_set = query_displays()?; - - let primary = display_set - .displays() - .find(|display| display.name() == info.primary) - .ok_or_else(|| anyhow!("Display {} not found", info.primary))?; - - let target = display_set - .displays() - .find(|display| display.name() == info.target) - .ok_or_else(|| anyhow!("Display {} not found", info.target))?; - - primary.set_primary()?; - - let settings = target.settings() - .as_ref() - .ok_or_else(|| anyhow!("Unable to query display settings"))?; - settings.replace_with(|_| info.target_settings); - - display_set.apply()?; - displayz::refresh()?; - - log::debug!("undo display: done"); - - Ok(()) -} \ No newline at end of file diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 1297f31..fa395bd 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2,21 +2,18 @@ mod cmd; mod model; mod pkg; mod pkg_store; -mod profile; mod util; -mod start; -mod liner; mod download_handler; mod appdata; -mod display; +mod modules; +mod profiles; use anyhow::anyhow; use closure::closure; use appdata::AppData; use model::misc::Game; use pkg::PkgKey; -use profile::Profile; -use tauri::{Listener, Manager}; +use tauri::{AppHandle, Listener, Manager}; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_cli::CliExt; use tokio::{sync::Mutex, fs, try_join}; @@ -40,28 +37,7 @@ pub async fn run(_args: Vec) { .expect("No main window") .set_focus(); if args.len() == 2 { - // Todo deindent this chimera - let url = &args[1]; - if &url[..13] == "rainycolor://" { - log::info!("Deep link: {}", url); - let regex = regex::Regex::new( - r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/" - ).expect("Invalid regex"); - if let Some(caps) = regex.captures(url) { - if caps.len() == 3 { - let apph = app.clone(); - let key = PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str())); - tauri::async_runtime::spawn(async move { - let mutex = apph.state::>(); - let mut appd = mutex.lock().await; - _ = appd.pkgs.fetch_listings().await; - if let Err(e) = appd.pkgs.install_package(&key, true, true).await { - log::warn!("Fail: {}", e.to_string()); - } - }); - } - } - } + deep_link(app.clone(), args); } })) .plugin(tauri_plugin_cli::init()) @@ -85,7 +61,7 @@ pub async fn run(_args: Vec) { log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg); if start_arg.occurrences > 0 { start_immediately = true; - app_data.remain_open = false; + app_data.state.remain_open = false; } else { tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into())) .title("STARTLINER") @@ -125,7 +101,6 @@ pub async fn run(_args: Vec) { } }); - app.listen("download-end", closure!(clone apph, |ev| { let raw = ev.payload(); let key = PkgKey(raw[1..raw.len()-1].to_owned()); @@ -142,27 +117,24 @@ pub async fn run(_args: Vec) { tauri::async_runtime::spawn(async move { let mutex = apph.state::>(); let appd = mutex.lock().await; - if !appd.remain_open { + if !appd.state.remain_open { apph.exit(0); } }); })); if start_immediately == true { - let apph_clone = apph.clone(); - tauri::async_runtime::spawn(async { - let apph_clone_clone = apph_clone.clone(); - { - let mtx = apph_clone.state::>(); - let mut appd = mtx.lock().await; - if let Err(e) = appd.pkgs.reload_all().await { - log::error!("Unable to reload packages: {}", e); - apph_clone.exit(1); - } + let apph = apph.clone(); + tauri::async_runtime::spawn(async move { + let mtx = apph.state::>(); + let mut appd = mtx.lock().await; + if let Err(e) = appd.pkgs.reload_all().await { + log::error!("Unable to reload packages: {}", e); + apph.exit(1); } - if let Err(e) = cmd::startline(apph_clone).await { + if let Err(e) = cmd::startline(apph.clone()).await { log::error!("Unable to launch: {}", e); - apph_clone_clone.exit(1); + apph.exit(1); } }); } @@ -181,9 +153,9 @@ pub async fn run(_args: Vec) { cmd::list_profiles, cmd::init_profile, cmd::load_profile, + cmd::rename_profile, cmd::get_current_profile, cmd::save_current_profile, - cmd::set_cfg, cmd::startline, cmd::kill, @@ -195,3 +167,31 @@ pub async fn run(_args: Vec) { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +fn deep_link(app: AppHandle, args: Vec) { + let url = &args[1]; + let proto = "rainycolor://"; + if &url[..proto.len()] == proto { + log::info!("Deep link: {}", url); + + let regex = regex::Regex::new( + r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/" + ).expect("Invalid regex"); + + if let Some(caps) = regex.captures(url) { + if caps.len() == 3 { + let app = app.clone(); + let key = PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str())); + tauri::async_runtime::spawn(async move { + let mutex = app.state::>(); + let mut appd = mutex.lock().await; + if let Err(e) = appd.pkgs.fetch_listings().await { + log::warn!("Deep link fetch failed: {:?}", e); + } else if let Err(e) = appd.pkgs.install_package(&key, true, true).await { + log::warn!("Deep link installation failed: {}", e.to_string()); + } + }); + } + } + } +} \ No newline at end of file diff --git a/rust/src/liner.rs b/rust/src/liner.rs deleted file mode 100644 index 226aa51..0000000 --- a/rust/src/liner.rs +++ /dev/null @@ -1,121 +0,0 @@ -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) // is unstable - junction::create(src, dst) -} - -pub async fn line_up(p: &Profile, pkg_hash: String) -> Result<()> { - let dir_out = p.data_dir(); - - if dir_out.join("option").exists() { - fs::remove_dir_all(dir_out.join("option")).await?; - } - - fs::create_dir_all(dir_out.join("option")).await?; - - let hash_path = p.data_dir().join(".sl-state"); - let prev_hash = fs::read_to_string(&hash_path).await.unwrap_or_default(); - if prev_hash != pkg_hash { - log::debug!("state {} -> {}", prev_hash, pkg_hash); - fs::write(hash_path, pkg_hash).await - .map_err(|e| anyhow!("Unable to write the state file: {}", e))?; - prepare_packages(p).await?; - } - - prepare_config(p).await?; - - Ok(()) -} - -async fn prepare_packages(p: &Profile) -> Result<()> { - let dir_out = p.data_dir(); - - if dir_out.join("BepInEx").exists() { - fs::remove_dir_all(dir_out.join("BepInEx")).await?; - } - - for m in &p.data.mods { - log::debug!("Preparing {}", m); - let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition")); - let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen - .join("app") - .join("BepInEx"); - if bpx_dir.exists() { - util::copy_recursive(&bpx_dir, &dir_out.join("BepInEx"))?; - } - - let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option"); - if opt_dir.exists() { - let x = opt_dir.read_dir().unwrap().next().unwrap()?; - if x.metadata()?.is_dir() { - symlink(&x.path(), &dir_out.join("option").join(x.file_name())).await?; - } - } - } - - log::debug!("prepare packages: done"); - - Ok(()) -} - -async fn prepare_config(p: &Profile) -> Result<()> { - let dir_out = p.data_dir(); - - let target_path = PathBuf::from(p.get_str("target-path", "")); - let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?; - let ini_in_raw = fs::read_to_string(p.config_dir().join("segatools-base.ini")).await?; - let ini_in = Ini::load_from_str(&ini_in_raw)?; - let mut opt_dir_in = PathBuf::from(p.get_str("option", "")); - if opt_dir_in.as_os_str().len() > 0 && opt_dir_in.is_relative() { - opt_dir_in = 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)? - ) - .set("amfs", p.get_str("amfs", "")) - .set("appdata", p.get_str("appdata", "appdata") - ); - 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"))? - ); - - if p.get_bool("aime", false) { - ini_out.with_section(Some("aime")) - .set("enable", "1") - .set("aimePath", util::path_to_str(dir_out.join("aime.txt"))?); - } - - ini_out.write_to_file(dir_out.join("segatools.ini"))?; - - log::debug!("Option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out); - if opt_dir_in.as_os_str().len() > 0 { - for opt in opt_dir_in.read_dir()? { - let opt = opt?; - symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?; - } - } - - log::debug!("prepare config: done"); - - Ok(()) -} \ No newline at end of file diff --git a/rust/src/model/config.rs b/rust/src/model/config.rs new file mode 100644 index 0000000..d050883 --- /dev/null +++ b/rust/src/model/config.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Clone)] +pub struct Segatools { + pub target: PathBuf, + pub amfs: PathBuf, + pub option: PathBuf, + pub appdata: PathBuf, + pub enable_aime: bool, + pub intel: bool, +} + +#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] +pub enum DisplayMode { + Window, + #[default] Borderless, + Fullscreen +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Display { + pub target: String, + pub rez: (i32, i32), + pub mode: DisplayMode, + pub rotation: i32, + pub frequency: i32, + pub borderless_fullscreen: bool, +} + +#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] +pub enum NetworkType { + #[default] Remote, + Artemis, +} + +#[derive(Deserialize, Serialize, Clone, Default)] +pub struct Network { + pub network_type: NetworkType, + + pub local_path: String, + pub local_console: bool, + + pub remote_address: String, + pub keychip: String, + + pub subnet: String, + pub suffix: Option, +} + +#[derive(Deserialize, Serialize, Clone, Default)] +pub struct BepInEx { + pub console: bool +} \ No newline at end of file diff --git a/rust/src/model/misc.rs b/rust/src/model/misc.rs index 772a2d0..144496c 100644 --- a/rust/src/model/misc.rs +++ b/rust/src/model/misc.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub enum Game { #[serde(rename = "ongeki")] Ongeki, diff --git a/rust/src/model/mod.rs b/rust/src/model/mod.rs index 70866bd..1fbc3f8 100644 --- a/rust/src/model/mod.rs +++ b/rust/src/model/mod.rs @@ -1,3 +1,5 @@ pub mod local; pub mod misc; -pub mod rainy; \ No newline at end of file +pub mod rainy; +pub mod config; +pub mod segatools_base; \ No newline at end of file diff --git a/rust/src/model/segatools_base.rs b/rust/src/model/segatools_base.rs new file mode 100644 index 0000000..7563cb3 --- /dev/null +++ b/rust/src/model/segatools_base.rs @@ -0,0 +1,84 @@ +pub fn segatools_base() -> String { +"; mu3io is TBD +[mu3io] +path= + +[vfd] +; Enable VFD emulation. Disable to use a real VFD +; GP1232A02A FUTABA assembly. +enable=1 + +[system] +; Enable ALLS system settings. +enable=1 + +; Enable freeplay mode. This will disable the coin slot and set the game to +; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not +; allow you to start a game in freeplay mode. +freeplay=0 + +; LAN Install: Set this to 1 on all machines. +dipsw1=1 + +[gfx] +; Enables the graphics hook. +enable=1 + +[led15093] +; Enable emulation of the 15093-06 controlled lights, which handle the air tower +; RGBs and the rear LED panel (billboard) on the cabinet. +enable=1 + +[led] +; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\ongeki_led\" +cabLedOutputPipe=1 +; Output billboard LED strip data to serial +cabLedOutputSerial=0 + +; Output slider LED data to the named pipe +controllerLedOutputPipe=1 +; Output slider LED data to the serial port +controllerLedOutputSerial=0 + +[io4] +; Test button virtual-key code. Default is the F1 key. +test=0x70 +; Service button virtual-key code. Default is the F2 key. +service=0x71 +; Keyboard button to increment coin counter. Default is the F3 key. +coin=0x72 + +; Set \"1\" to enable mouse lever emulation, \"0\" to use XInput +mouse=1 + +; XInput input bindings +; +; Left Stick Lever +; Left Trigger Lever (move to the left) +; Right Trigger Lever (move to the right) +; Left Left red button +; Up Left green button +; Right Left blue button +; Left Shoulder Left side button +; Right Shoulder Right side button +; X Right red button +; Y Right green button +; A Right blue button +; Back Left menu button +; Start Right menu button + +; Keyboard input bindings +left1=0x41 ; A +left2=0x53 ; S +left3=0x44 ; D + +leftSide=0x01 ; Mouse Left +rightSide=0x02 ; Mouse Right + +right1=0x4A ; J +right2=0x4B ; K +right3=0x4C ; L + +leftMenu=0x55 ; U +rightMenu=0x4F ; O".to_owned() +} \ No newline at end of file diff --git a/rust/src/modules/bepinex.rs b/rust/src/modules/bepinex.rs new file mode 100644 index 0000000..fd1790a --- /dev/null +++ b/rust/src/modules/bepinex.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use ini::Ini; +use crate::{model::config::BepInEx, profiles::ProfilePaths}; + +impl BepInEx { + pub fn line_up(&self, p: &impl ProfilePaths) -> Result<()> { + let dir = p.data_dir().join("BepInEx"); + + if dir.exists() && dir.is_dir() { + let dir = dir.join("config"); + std::fs::create_dir_all(&dir)?; + let mut ini = Ini::new(); + + ini.with_section(Some("Logging.Console")) + .set("Enabled", if self.console { "true" } else { "false" }); + + ini.write_to_file(dir.join("BepInEx.cfg"))?; + } + + Ok(()) + } +} \ No newline at end of file diff --git a/rust/src/modules/display.rs b/rust/src/modules/display.rs new file mode 100644 index 0000000..1efbc41 --- /dev/null +++ b/rust/src/modules/display.rs @@ -0,0 +1,139 @@ + +use crate::model::config::{Display, DisplayMode}; +use anyhow::Result; +use displayz::{query_displays, DisplaySet}; +use tauri::{AppHandle, Listener}; + +#[cfg(target_os = "windows")] +#[derive(Clone)] +pub struct DisplayInfo { + pub primary: String, + pub set: Option +} + +impl Default for DisplayInfo { + fn default() -> Self { + DisplayInfo { + primary: "default".to_owned(), + set: query_displays().ok() + } + } +} + +impl Default for Display { + fn default() -> Self { + Display { + target: "default".to_owned(), + rez: (1080, 1920), + mode: DisplayMode::Borderless, + rotation: 0, + frequency: 60, + borderless_fullscreen: true, + } + } +} + +#[cfg(target_os = "windows")] +impl Display { + pub fn activate(&self, app: AppHandle) { + let display = self.clone(); + tauri::async_runtime::spawn(async move { + let info = display.line_up()?; + if let Some(info) = info { + app.listen("launch-end", move |_| { + if let Err(e) = Self::clean_up(&info) { + log::error!("Error cleaning up display: {:?}", e); + } + }); + } + + Ok::<(), anyhow::Error>(()) + }); + } + + fn line_up(&self) -> Result> { + use anyhow::anyhow; + use displayz::{query_displays, Orientation, Resolution, Frequency}; + + if self.target == "default" { + log::debug!("prepare display: skip"); + return Ok(None); + } + + let display_set = query_displays()?; + + let primary = display_set + .displays() + .find(|display| display.is_primary()) + .ok_or_else(|| anyhow!("Primary display not found"))?; + + let target = display_set + .displays() + .find(|display| display.name() == self.target) + .ok_or_else(|| anyhow!("Display {} not found", self.target))?; + + target.set_primary()?; + let settings = target.settings() + .as_ref() + .ok_or_else(|| anyhow!("Unable to query display settings"))?; + + let res = DisplayInfo { + primary: primary.name().to_owned(), + set: Some(display_set.clone()) + }; + + if self.rotation == 90 || self.rotation == 270 { + let rez = settings.borrow_mut().resolution; + settings.borrow_mut().orientation = if self.rotation == 90 { Orientation::PortraitFlipped } else { Orientation::Portrait }; + if rez.height < rez.width { + settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width); + } + } + + let frequency: u32 = self.frequency + .try_into() + .map_err(|e| anyhow!("Invalid display frequency: {}", e))?; + + let width: u32 = self.rez.0 + .try_into() + .map_err(|e| anyhow!("Invalid display width: {}", e))?; + + let height: u32 = self.rez.1 + .try_into() + .map_err(|e| anyhow!("Invalid display height: {}", e))?; + + settings.borrow_mut().frequency = Frequency::new(frequency); + + if self.borderless_fullscreen && self.mode == DisplayMode::Borderless { + settings.borrow_mut().resolution = Resolution::new(width, height); + } + + display_set.apply()?; + displayz::refresh()?; + + log::debug!("prepare display: done"); + + Ok(Some(res)) + } + + fn clean_up(info: &DisplayInfo) -> Result<()> { + use anyhow::anyhow; + + let display_set = info.set.as_ref() + .ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?; + + let primary = display_set + .displays() + .find(|display| display.name() == info.primary) + .ok_or_else(|| anyhow!("Display {} not found", info.primary))?; + + primary.set_primary()?; + + display_set.apply()?; + displayz::refresh()?; + + log::debug!("undo display: done"); + + Ok(()) + } +} \ No newline at end of file diff --git a/rust/src/modules/mod.rs b/rust/src/modules/mod.rs new file mode 100644 index 0000000..876e176 --- /dev/null +++ b/rust/src/modules/mod.rs @@ -0,0 +1,5 @@ +pub mod display; +pub mod package; +pub mod segatools; +pub mod network; +pub mod bepinex; \ No newline at end of file diff --git a/rust/src/modules/network.rs b/rust/src/modules/network.rs new file mode 100644 index 0000000..3436118 --- /dev/null +++ b/rust/src/modules/network.rs @@ -0,0 +1,67 @@ +use std::{path::PathBuf, process::Command}; +use yaml_rust2::YamlLoader; +use anyhow::{Result, anyhow}; +use ini::Ini; +use crate::model::config::{Network, NetworkType}; + +impl Network { + pub fn line_up(&self, ini: &mut Ini) -> Result<()> { + log::debug!("begin line-up: network"); + + ini.with_section(Some("dns")).set("default", &self.remote_address); + + let mut section_netenv = ini.with_section(Some("netenv")); + + section_netenv.set("enable", "1"); + + if let Some(suffix) = self.suffix { + section_netenv.set("addrSuffix", suffix.to_string()); + } + + let mut section_keychip = ini.with_section(Some("keychip")); + + if self.subnet.len() > 0 { + section_keychip.set("subnet", &self.subnet); + } + + if self.network_type == NetworkType::Artemis { + let network_path = PathBuf::from(&self.local_path); + let artemis_dir = network_path.parent() + .ok_or_else(|| anyhow!("Invalid ARTEMiS path {}", &self.local_path))?; + let cfg_path = artemis_dir.join("config").join("core.yaml"); + + let cfg = std::fs::read_to_string(&cfg_path) + .map_err(|e| anyhow!("Unable to open core.yaml: {}", e))?; + let cfg = YamlLoader::load_from_str(&cfg) + .map_err(|e| anyhow!("Unable to read core.yaml: {}", e))?; + let cfg = &cfg[0]; + log::debug!("{:?}", cfg); + let hostname = &cfg["server"]["hostname"]; + let hostname = hostname.clone().into_string(); + if let Some(hostname) = hostname { + ini.with_section(Some("dns")).set("default", hostname); + + #[cfg(target_os = "windows")] + let mut cmd = Command::new("cmd.exe"); + + cmd.arg("/C"); + + if self.local_console == true { + cmd.arg("start"); + } + cmd.args(["python", &self.local_path]); + cmd.current_dir(artemis_dir); + cmd.spawn() + .map_err(|e| anyhow!("Unable to spawn artemis: {}", e))?; + } else { + log::warn!("unable to parse the artemis hostname"); + } + } else if self.keychip.len() > 0 { + section_keychip.set("id", &self.keychip); + } + + log::debug!("end line-up: network"); + + Ok(()) + } +} \ No newline at end of file diff --git a/rust/src/modules/package.rs b/rust/src/modules/package.rs new file mode 100644 index 0000000..0aa070c --- /dev/null +++ b/rust/src/modules/package.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use std::collections::BTreeSet; +use crate::pkg::PkgKey; +use crate::util; +use crate::profiles::ProfilePaths; + +pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet) -> Result<()> { + log::debug!("begin prepare packages"); + + let pfx_dir = p.data_dir(); + let opt_dir = pfx_dir.join("option"); + + if pfx_dir.join("BepInEx").exists() { + tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?; + } + + if !opt_dir.exists() { + tokio::fs::create_dir(opt_dir).await?; + } + + for m in pkgs { + log::debug!("preparing {}", m); + let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition")); + let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen + .join("app") + .join("BepInEx"); + if bpx_dir.exists() { + util::copy_recursive(&bpx_dir, &pfx_dir.join("BepInEx"))?; + } + + let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option"); + if opt_dir.exists() { + let x = opt_dir.read_dir().unwrap().next().unwrap()?; + if x.metadata()?.is_dir() { + util::symlink(&x.path(), &pfx_dir.join("option").join(x.file_name())).await?; + } + } + } + + log::debug!("end prepare packages"); + + Ok(()) +} \ No newline at end of file diff --git a/rust/src/modules/segatools.rs b/rust/src/modules/segatools.rs new file mode 100644 index 0000000..8e17493 --- /dev/null +++ b/rust/src/modules/segatools.rs @@ -0,0 +1,93 @@ + +use std::path::PathBuf; + +use anyhow::{anyhow, Result}; +use ini::Ini; +use crate::{model::{config::Segatools, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}}; + +impl Default for Segatools { + fn default() -> Self { + Segatools { + target: PathBuf::default(), + amfs: PathBuf::default(), + option: PathBuf::default(), + appdata: PathBuf::from("appdata"), + enable_aime: false, + intel: false + } + } +} + +impl Segatools { + pub async fn line_up(&self, p: &impl ProfilePaths) -> Result { + log::debug!("begin line-up: segatools"); + + let pfx_dir = p.data_dir(); + let exe_dir = self.target.parent().ok_or_else(|| anyhow!("Invalid target path"))?; + + log::debug!("segatools: {:?} {:?}", pfx_dir, exe_dir); + + let ini_path = p.config_dir().join("segatools-base.ini"); + + if !ini_path.exists() { + tokio::fs::write(&ini_path, segatools_base()).await + .map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?; + } + if !pfx_dir.exists() { + tokio::fs::create_dir(&pfx_dir).await + .map_err(|e| anyhow!("Error creating {:?}: {}", pfx_dir, e))?; + } + + let ini_in = tokio::fs::read_to_string(&ini_path).await?; + let ini_in = Ini::load_from_str(&ini_in)?; + + let opt_dir_out = &pfx_dir.join("option"); + let opt_dir_in = if self.option.as_os_str().len() > 0 && self.option.is_relative() { + exe_dir.join(&self.option) + } else { + self.option.clone() + }; + + let mut ini_out = ini_in.clone(); + ini_out.with_section(Some("vfs")) + .set( + "option", + opt_dir_out.stringify()? + ) + .set("amfs", self.amfs.stringify()?) + .set("appdata", self.appdata.stringify()?); + + ini_out.with_section(Some("unity")) + .set("enable", "1") + .set( + "targetAssembly", + pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()? + ); + + if self.enable_aime { + ini_out.with_section(Some("aime")) + .set("enable", "1") + .set("aimePath", p.config_dir().join("aime.txt").stringify()?); + } else { + ini_out.with_section(Some("aime")) + .set("enable", "0"); + } + + log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out); + + if !opt_dir_out.exists() { + tokio::fs::create_dir(opt_dir_out).await?; + } + + if opt_dir_in.as_os_str().len() > 0 { + for opt in opt_dir_in.read_dir()? { + let opt = opt?; + util::symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?; + } + } + + log::debug!("end line-up: segatools"); + + Ok(ini_out) + } +} \ No newline at end of file diff --git a/rust/src/profile.rs b/rust/src/profile.rs deleted file mode 100644 index 48196d1..0000000 --- a/rust/src/profile.rs +++ /dev/null @@ -1,141 +0,0 @@ -use anyhow::{Result, anyhow}; -use std::{collections::{BTreeSet, BTreeMap}, path::{Path, PathBuf}}; -use crate::{model::misc::{self, Game}, pkg::PkgKey, util}; -use serde::{Deserialize, Serialize}; -use tokio::fs; - -#[derive(Deserialize, Serialize, Clone)] -pub struct Profile { - pub game: misc::Game, - pub name: String, - pub data: ProfileData -} - -// The contents of profile-{game}-{name}.json -#[derive(Deserialize, Serialize, Clone)] -pub struct ProfileData { - pub mods: BTreeSet, - // cfg is temporarily just a map to make iteration easier - // eventually it should become strict - pub cfg: BTreeMap -} - -impl Profile { - pub fn new(game: Game, mut name: String) -> Profile { - name = name.trim().replace(" ", "-"); - - while Self::config_dir_f(&game, &name).exists() { - name = format!("new-{}", name); - } - - Profile { - name, - game, - data: ProfileData { - mods: BTreeSet::new(), - cfg: BTreeMap::new() - } - } - } - - fn config_dir_f(game: &Game, name: &str) -> PathBuf { - util::config_dir().join(format!("profile-{}-{}", game, name)) - } - - pub fn config_dir(&self) -> PathBuf { - Self::config_dir_f(&self.game, &self.name) - } - - pub fn data_dir(&self) -> PathBuf { - util::data_dir().join(format!("profile-{}-{}", self.game, self.name)) - } - - pub async fn list() -> Result> { - let path = std::fs::read_dir(util::config_dir())?; - - let mut res = Vec::new(); - - for f in path { - let f = f?; - - if let Ok(meta) = f.metadata() { - if !meta.is_dir() { - continue; - } - log::debug!("{:?}", f); - if let Some(pair) = Self::name_from_path(f.path()) { - res.push(pair); - } - } - } - - Ok(res) - } - - pub fn load(game: Game, name: String) -> Result { - let path = Self::config_dir_f(&game, &name).join("profile.json"); - if let Ok(s) = std::fs::read_to_string(&path) { - let data = serde_json::from_str::(&s) - .map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?; - - Ok(Profile { - game, - name, - data - }) - } else { - Err(anyhow!("Unable to open {:?}", path)) - } - } - - pub async fn save(&self) { - let path = self.config_dir().join("profile.json"); - - let s = serde_json::to_string_pretty(&self.data).unwrap(); - fs::write(&path, s).await.unwrap(); - log::info!("Written to {}", path.to_string_lossy()); - } - - #[allow(dead_code)] - pub fn get_cfg(&self, key: &str) -> Result<&serde_json::Value> { - self.data.cfg.get(key) - .ok_or_else(|| anyhow::anyhow!("Invalid config entry {}", key)) - } - - pub fn get_bool(&self, key: &str, default: bool) -> bool { - self.data.cfg.get(key) - .and_then(|c| c.as_bool()) - .unwrap_or(default) - } - - pub fn get_int(&self, key: &str, default: i64) -> i64 { - self.data.cfg.get(key) - .and_then(|c| c.as_i64()) - .unwrap_or(default) - } - - pub fn get_str(&self, key: &str, default: &str) -> String { - self.data.cfg.get(key) - .and_then(|c| c.as_str()) - .unwrap_or(default) - .to_owned() - } - - fn name_from_path(path: impl AsRef) -> Option<(Game, String)> { - let regex = regex::Regex::new( - r"^profile-([^\-]+)-(.+)$" - ).expect("Invalid regex"); - - let fname = path.as_ref().file_name().unwrap_or_default().to_string_lossy(); - - if let Some(caps) = regex.captures(&fname) { - let game = caps.get(1).unwrap().as_str(); - let name = caps.get(2).unwrap().as_str().to_owned(); - if let Some(game) = Game::from_str(game) { - return Some((game, name)); - } - } - - None - } -} diff --git a/rust/src/profiles/mod.rs b/rust/src/profiles/mod.rs new file mode 100644 index 0000000..cee05ac --- /dev/null +++ b/rust/src/profiles/mod.rs @@ -0,0 +1,162 @@ +use anyhow::{Result, anyhow}; +use ongeki::OngekiProfile; +use serde::{Deserialize, Serialize}; +use tauri::AppHandle; +use std::{collections::BTreeSet, path::{Path, PathBuf}}; +use crate::{model::misc::Game, modules::package::prepare_packages, pkg::PkgKey, util}; + +pub mod ongeki; + +#[derive(Deserialize, Serialize, Clone)] +pub enum AnyProfile { + OngekiProfile(OngekiProfile) +} + +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +pub struct ProfileMeta { + pub game: Game, + pub name: String +} + +pub trait Profile: Sized { + fn new(name: String) -> Result; + fn load(name: String) -> Result; + fn save(&self) -> Result<()>; + async fn start(&self, app: AppHandle) -> Result<()>; +} + +pub trait ProfilePaths { + fn config_dir(&self) -> PathBuf; + fn data_dir(&self) -> PathBuf; +} + +impl AnyProfile { + pub fn load(game: Game, name: String) -> Result { + Ok(match game { + Game::Ongeki => AnyProfile::OngekiProfile(OngekiProfile::load(name)?), + Game::Chunithm => panic!("Not implemented") + }) + } + pub fn save(&self) -> Result<()> { + match self { + Self::OngekiProfile(p) => p.save() + } + } + pub fn meta(&self) -> ProfileMeta { + match self { + Self::OngekiProfile(p) => { + ProfileMeta { + game: Game::Ongeki, + name: p.name.as_ref().unwrap().clone() + } + } + } + } + pub fn rename(&mut self, name: String) { + match self { + Self::OngekiProfile(p) => { + p.name = Some(name); + } + } + } + + pub fn pkgs(&self) -> &BTreeSet { + match self { + Self::OngekiProfile(p) => &p.mods + } + } + pub fn pkgs_mut(&mut self) -> &mut BTreeSet { + match self { + Self::OngekiProfile(p) => &mut p.mods + } + } + pub async fn line_up(&self, app: AppHandle, pkg_hash: String) -> Result<()> { + match self { + Self::OngekiProfile(p) => { + if !p.data_dir().exists() { + tokio::fs::create_dir(p.data_dir()).await?; + } + + let hash_path = p.data_dir().join(".sl-state"); + let meta = self.meta(); + + p.display.activate(app.clone()); + + util::clean_up_opts(p.data_dir().join("option"))?; + + if Self::hash_check(&hash_path, &pkg_hash).await? == true { + prepare_packages(&meta, &p.mods).await + .map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?; + } + let mut ini = p.sgt.line_up(&meta).await + .map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?; + p.network.line_up(&mut ini)?; + + ini.write_to_file(p.data_dir().join("segatools.ini")) + .map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?; + + p.bepinex.line_up(&meta)?; + + Ok(()) + } + } + } + + pub async fn start(&self, app: AppHandle) -> Result<()> { + match self { + Self::OngekiProfile(p) => p.start(app).await + } + } + + async fn hash_check(prev_hash_path: &impl AsRef, new_hash: &str) -> Result { + let prev_hash = tokio::fs::read_to_string(&prev_hash_path).await.unwrap_or_default(); + if prev_hash != new_hash { + log::debug!("state {} -> {}", prev_hash, new_hash); + tokio::fs::write(prev_hash_path, new_hash).await + .map_err(|e| anyhow!("Unable to write the state file: {}", e))?; + Ok(true) + } else { + Ok(false) + } + } +} + +pub async fn list_profiles() -> Result> { + let path = std::fs::read_dir(util::config_dir())?; + + let mut res = Vec::new(); + + for f in path { + let f = f?; + + if let Ok(meta) = f.metadata() { + if !meta.is_dir() { + continue; + } + log::debug!("{:?}", f); + if let Some(meta) = meta_from_path(f.path()) { + res.push(meta); + } + } + } + + Ok(res) +} + +fn meta_from_path(path: impl AsRef) -> Option { + let regex = regex::Regex::new( + r"^profile-([^\-]+)-(.+)$" + ).expect("Invalid regex"); + + let fname = path.as_ref().file_name().unwrap_or_default().to_string_lossy(); + + if let Some(caps) = regex.captures(&fname) { + let game = caps.get(1).unwrap().as_str(); + let name = caps.get(2).unwrap().as_str().to_owned(); + if let Some(game) = Game::from_str(game) { + return Some(ProfileMeta { game, name }); + } + } + + None +} \ No newline at end of file diff --git a/rust/src/profiles/ongeki.rs b/rust/src/profiles/ongeki.rs new file mode 100644 index 0000000..327294c --- /dev/null +++ b/rust/src/profiles/ongeki.rs @@ -0,0 +1,234 @@ +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::util::PathStr; +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 +} + +impl Profile for OngekiProfile { + fn new(name: String) -> Result { + let mut fixed_name = name.trim().replace(" ", "-"); + + while util::profile_config_dir(&Game::Ongeki, &name).exists() { + fixed_name = format!("new-{}", name); + } + + let p = OngekiProfile { + name: Some(fixed_name), + mods: BTreeSet::new(), + sgt: Segatools::default(), + display: Display::default(), + network: Network::default(), + bepinex: BepInEx::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.to_string_lossy()); + + Ok(()) + } + + async fn start(&self, app: AppHandle) -> Result<()> { + let ini_path = self.data_dir().join("segatools.ini"); + + log::debug!("With path {}", ini_path.to_string_lossy()); + + 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"))?; + + #[cfg(target_os = "windows")] + { + game_builder = Command::new(exe_dir.join("inject.exe")); + amd_builder = Command::new("cmd.exe"); + } + #[cfg(target_os = "linux")] + { + let wine = p.data.wine_runtime.clone() + .unwrap_or_else(|| std::path::PathBuf::from("/usr/bin/wine")); + + game_builder = Command::new(&wine); + amd_builder = Command::new(&wine); + + game_builder.arg(exe_dir.join("inject.exe")); + amd_builder.arg("cmd.exe"); + } + + amd_builder.env( + "SEGATOOLS_CONFIG_PATH", + &ini_path, + ) + .current_dir(&exe_dir) + .args([ + "/C", + &exe_dir.join("inject.exe").stringify()?, "-d", "-k", "mu3hook.dll", + "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", "mu3hook.dll", + "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")] + { + let wineprefix = p.data.wine_prefix.clone().unwrap_or_else(|| + directories::UserDirs::new() + .expect("No home directory") + .home_dir() + .join(".wine") + ); + amd_builder.env("WINEPREFIX", &wineprefix); + game_builder.env("WINEPREFIX", &wineprefix); + } + + + 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)) + } +} \ No newline at end of file diff --git a/rust/src/start.rs b/rust/src/start.rs deleted file mode 100644 index 7eb855f..0000000 --- a/rust/src/start.rs +++ /dev/null @@ -1,177 +0,0 @@ -use anyhow::{anyhow, Result}; -use std::fs::File; -use std::path::PathBuf; -use tokio::process::Command; -use tauri::{AppHandle, Emitter}; -use std::process::Stdio; -use crate::profile::Profile; -use crate::util; - -#[cfg(target_os = "windows")] -static CREATE_NO_WINDOW: u32 = 0x08000000; - -pub async fn start(p: &Profile, app: AppHandle) -> Result<()> { - use tokio::task::JoinSet; - - let ini_path = p.data_dir().join("segatools.ini"); - - log::debug!("With path {}", ini_path.to_string_lossy()); - - let mut game_builder; - let mut amd_builder; - - let target_path = PathBuf::from(p.get_str("target-path", "")); - let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?; - - #[cfg(target_os = "windows")] - let display_info = crate::display::prepare_display(p).await?; - - #[cfg(target_os = "windows")] - { - game_builder = Command::new(exe_dir.join("inject.exe")); - amd_builder = Command::new("cmd.exe"); - } - #[cfg(target_os = "linux")] - { - let wine = p.data.wine_runtime.clone() - .unwrap_or_else(|| std::path::PathBuf::from("/usr/bin/wine")); - - game_builder = Command::new(&wine); - amd_builder = Command::new(&wine); - - game_builder.arg(exe_dir.join("inject.exe")); - amd_builder.arg("cmd.exe"); - } - - let display_mode = p.get_str("display-mode", "borderless"); - - amd_builder.env( - "SEGATOOLS_CONFIG_PATH", - &ini_path, - ) - .current_dir(&exe_dir) - .args([ - "/C", - &util::path_to_str(exe_dir.join("inject.exe"))?, "-d", "-k", "mu3hook.dll", - "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", - p.config_dir().join("inohara.cfg"), - ) - .current_dir(&exe_dir) - .args([ - "-d", "-k", "mu3hook.dll", - "mu3.exe", "-monitor 1", - "-screen-width", &p.get_int("rez-w", 1080).to_string(), - "-screen-height", &p.get_int("rez-h", 1920).to_string(), - "-screen-fullscreen", if display_mode == "fullscreen" { "1" } else { "0" } - ]); - - if display_mode == "borderless" { - game_builder.arg("-popupwindow"); - } - - #[cfg(target_os = "linux")] - { - let wineprefix = p.data.wine_prefix.clone().unwrap_or_else(|| - directories::UserDirs::new() - .expect("No home directory") - .home_dir() - .join(".wine") - ); - amd_builder.env("WINEPREFIX", &wineprefix); - game_builder.env("WINEPREFIX", &wineprefix); - } - - - let amd_log = File::create(p.data_dir().join("amdaemon.log"))?; - let game_log = File::create(p.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(CREATE_NO_WINDOW); - game_builder.creation_flags(CREATE_NO_WINDOW); - } - - if p.get_bool("intel", false) == true { - amd_builder.env("OPENSSL_ia32cap", ":~0x20000000"); - } - - 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()?; - - tauri::async_runtime::spawn(async move { - 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" { - pkill("mu3.exe").await; - } else { - pkill("amdaemon.exe").await; - } - - set.join_next().await.expect("No spawn").expect("No result"); - - log::debug!("Fin"); - - if let Err(e) = app.emit("launch-start", "") { - log::warn!("Unable to emit launch-end: {}", e); - } - - #[cfg(target_os = "windows")] - { - if let Some(display_info) = display_info { - if let Err(e) = crate::display::undo_display(display_info).await { - log::error!("undo display failed: {}", e); - } - } - } - }); - - Ok(()) -} - -#[cfg(target_os = "windows")] -pub async fn pkill(process_name: &str) { - _ = Command::new("taskkill.exe").arg("/f").arg("/im").arg(process_name) - .creation_flags(CREATE_NO_WINDOW).output().await; -} - -#[cfg(target_os = "linux")] -pub async fn pkill(process_name: &str) { - _ = Command::new("pkill").arg(process_name) - .output().await; -} \ No newline at end of file diff --git a/rust/src/util.rs b/rust/src/util.rs index 90aae21..2185190 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager}; use std::{path::{Path, PathBuf}, sync::OnceLock}; +use crate::model::misc::Game; + #[cfg(not(target_os = "windows"))] static NAME: &str = "startliner"; @@ -22,9 +24,9 @@ pub fn init_dirs(apph: &AppHandle) { DIRS.get_or_init(|| { if cfg!(windows) { Dirs { - config_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME).join("cfg"), - data_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME).join("data"), - cache_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME), + config_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME), + data_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME).join("data"), + cache_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME).join("cache"), } } else { Dirs { @@ -44,6 +46,10 @@ pub fn config_dir() -> &'static Path { &DIRS.get().expect("Directories uninitialized").config_dir } +pub fn profile_config_dir(game: &Game, name: &str) -> PathBuf { + config_dir().join(format!("profile-{}-{}", game, name)) +} + pub fn data_dir() -> &'static Path { &DIRS.get().expect("Directories uninitialized").data_dir } @@ -60,11 +66,6 @@ pub fn pkg_dir_of(namespace: &str, name: &str) -> PathBuf { pkg_dir().join(format!("{}-{}", namespace, name)) } -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)? { @@ -77,4 +78,70 @@ pub fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> { } } Ok(()) +} + +#[cfg(target_os = "linux")] +pub async fn symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { + fs::symlink(src, dst).await +} + +#[cfg(target_os = "windows")] +pub async fn symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { + //std::os::windows::fs::junction_point(src, dst) // is unstable + junction::create(src, dst) +} + +#[cfg(target_os = "windows")] +pub static CREATE_NO_WINDOW: u32 = 0x08000000; + +#[cfg(target_os = "windows")] +pub async fn pkill(process_name: &str) { + use tokio::process::Command; + + _ = Command::new("taskkill.exe").arg("/f").arg("/im").arg(process_name) + .creation_flags(CREATE_NO_WINDOW).output().await; +} + +#[cfg(target_os = "linux")] +pub async fn pkill(process_name: &str) { + _ = Command::new("pkill").arg(process_name) + .output().await; +} + +pub fn clean_up_opts(dir: impl AsRef) -> Result<()> { + log::debug!("begin clean_up_opts"); + if dir.as_ref().is_dir() { + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + log::debug!("{:?}", path); + if path.is_symlink() { + std::fs::remove_dir(path)?; + } else { + log::error!("Not a symlink: {:?}", path); + } + } + } + log::debug!("end clean_up_opts"); + Ok(()) +} + +pub trait PathStr { + fn stringify(&self) -> Result; +} + +fn path_to_str(p: impl AsRef) -> Result { + Ok(p.as_ref().to_str().ok_or_else(|| anyhow!("Invalid path: {:?}", p.as_ref()))?.to_owned()) +} + +impl PathStr for Path { + fn stringify(&self) -> Result { + path_to_str(self) + } +} + +impl PathStr for PathBuf { + fn stringify(&self) -> Result { + path_to_str(&self) + } } \ No newline at end of file diff --git a/src/components/App.vue b/src/components/App.vue index a4bd778..b8882c9 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -30,8 +30,7 @@ onMounted(async () => { general.dirs = d as Dirs; }); - await prf.reloadList(); - await prf.reload(); + await Promise.all([prf.reloadList(), prf.reload()]); if (prf.current !== null) { await pkg.reloadAll(); diff --git a/src/components/FilePicker.vue b/src/components/FilePicker.vue index ca32f7e..f37ea37 100644 --- a/src/components/FilePicker.vue +++ b/src/components/FilePicker.vue @@ -2,25 +2,16 @@ import Button from 'primevue/button'; import InputText from 'primevue/inputtext'; import { open } from '@tauri-apps/plugin-dialog'; -import { usePrfStore } from '../stores'; const props = defineProps({ - field: String, - default: String, placeholder: String, directory: Boolean, promptname: String, extension: String, + value: String, + callback: Function, }); -if (props.field === undefined || props.default === undefined) { - throw new Error('Invalid FilePicker'); -} - -const prf = usePrfStore(); - -const cfg = prf.cfg(props.field, props.default); - const filePick = async () => { const res = await open({ multiple: false, @@ -35,9 +26,9 @@ const filePick = async () => { ] : [], }); - if (res != null) { - cfg.value = - /*path.relative(cfgs.current?.data.exe_dir ?? '', res) */ res; + if (res != null && props.callback !== undefined) { + props.callback(res); + /*path.relative(cfgs.current?.data.exe_dir ?? '', res) */ } }; @@ -48,6 +39,7 @@ const filePick = async () => { size="small" :placeholder="placeholder" type="text" - v-model="cfg" + :model-value="value" + @update:model-value="(value) => callback && callback(value)" /> diff --git a/src/components/OptionList.vue b/src/components/OptionList.vue index fb2ec1b..a7a6edb 100644 --- a/src/components/OptionList.vue +++ b/src/components/OptionList.vue @@ -66,7 +66,7 @@ const aimeCodeModel = computed({ }); const extraDisplayOptionsDisabled = computed(() => { - return prf.cfg('display', 'default').value === 'default'; + return prf.current?.display.target === 'default'; }); (async () => { @@ -79,16 +79,16 @@ const extraDisplayOptionsDisabled = computed(() => { { :min="480" :max="9999" :use-grouping="false" - :model-value="prf.cfgAny('rez-w', 1080)" + v-model="prf.current!.display.rez[0]" /> x { :min="640" :max="9999" :use-grouping="false" - :model-value="prf.cfgAny('rez-h', 1920)" + v-model="prf.current!.display.rez[1]" /> @@ -165,12 +169,13 @@ const extraDisplayOptionsDisabled = computed(() => { v-if="capabilities.includes('display')" > { :min="60" :max="999" :use-grouping="false" - :model-value="prf.cfgAny('frequency', 60)" + v-model="prf.current!.display.frequency" :disabled="extraDisplayOptionsDisabled" /> @@ -191,32 +196,69 @@ const extraDisplayOptionsDisabled = computed(() => { title="Match display resolution with the game" v-if="capabilities.includes('display')" > - - + + + + + + + + + + + > { size="small" :maxlength="15" placeholder="192.168.1.0" - :model-value="prf.cfgAny('subnet', '')" + v-model="prf.current!.network.subnet" /> - - - + - - + { extension="cfg" /> + + + diff --git a/src/components/ProfileList.vue b/src/components/ProfileList.vue index ca98fbc..2ea6352 100644 --- a/src/components/ProfileList.vue +++ b/src/components/ProfileList.vue @@ -1,5 +1,6 @@ + + diff --git a/src/components/StartButton.vue b/src/components/StartButton.vue index e5356f0..42fb632 100644 --- a/src/components/StartButton.vue +++ b/src/components/StartButton.vue @@ -25,10 +25,10 @@ const kill = async () => { }; const disabledTooltip = computed(() => { - if (prf.cfg('target-path', '').value.length === 0) { + if (prf.current?.sgt.target.length === 0) { return 'The game path must be specified'; } - if (prf.cfg('amfs', '').value.length === 0) { + if (prf.current?.sgt.amfs.length === 0) { return 'The amfs path must be specified'; } return null; diff --git a/src/stores.ts b/src/stores.ts index 0f7fe30..35dbb9b 100644 --- a/src/stores.ts +++ b/src/stores.ts @@ -1,4 +1,4 @@ -import { Ref, computed, ref } from 'vue'; +import { Ref, computed, ref, watchEffect } from 'vue'; import { defineStore } from 'pinia'; import { listen } from '@tauri-apps/api/event'; import * as path from '@tauri-apps/api/path'; @@ -110,43 +110,25 @@ export const usePrfStore = defineStore('prf', () => { () => pkg !== undefined && current.value !== null && - current.value?.data.mods.includes(pkgKey(pkg)) + current.value?.mods.includes(pkgKey(pkg)) ); const reload = async () => { - current.value = await invoke('get_current_profile'); + const p: any = await invoke('get_current_profile'); + if (p['OngekiProfile'] !== undefined) { + current.value = { ...p.OngekiProfile, game: 'ongeki' }; + } if (current.value !== null) { changePrimaryColor(current.value.game); } }; - const save = async () => { - await invoke('save_current_profile'); - }; - - const cfg = (key: string, dflt: T) => - computed({ - get() { - return (current.value?.data.cfg[key] as T | undefined) ?? dflt; - }, - async set(value) { - if (value !== undefined) { - await invoke('set_cfg', { key, value: value }); - await reload(); - await save(); - } - }, - }); - - // Hack around PrimeVu not supporting WritableComputedRef - const cfgAny = ( - key: string, - dflt: T - ) => cfg(key, dflt) as any; - const create = async (game: Game) => { try { - await invoke('init_profile', { game, name: 'new-profile' }); + await invoke('init_profile', { + game, + name: 'new-profile', + }); await reload(); await reloadList(); } catch (e) { @@ -159,6 +141,22 @@ export const usePrfStore = defineStore('prf', () => { } }; + const rename = async (profile: ProfileMeta, name: string) => { + await invoke('rename_profile', { + profile, + name, + }); + + if ( + current.value?.game === profile.game && + current.value.name === profile.name + ) { + current.value.name = name; + } + + await reloadList(); + }; + const switchTo = async (game: Game, name: string) => { await invoke('load_profile', { game, name }); await reload(); @@ -169,14 +167,9 @@ export const usePrfStore = defineStore('prf', () => { }; const reloadList = async () => { - const raw = (await invoke('list_profiles')) as [Game, string][]; - - list.value = raw.map(([game, name]) => { - return { - game, - name, - }; - }); + // list.value.splice(0, list.value.length); + list.value = (await invoke('list_profiles')) as ProfileMeta[]; + console.log(list.value); }; const togglePkg = async (pkg: Package | undefined, enable: boolean) => { @@ -185,7 +178,6 @@ export const usePrfStore = defineStore('prf', () => { } await invoke('toggle_package', { key: pkgKey(pkg), enable }); await reload(); - await save(); }; const generalStore = useGeneralStore(); @@ -201,15 +193,21 @@ export const usePrfStore = defineStore('prf', () => { await reload(); }); + watchEffect(async () => { + if (current.value !== null) { + await invoke('save_current_profile', { + profile: { OngekiProfile: current.value }, + }); + } + }); + return { current, list, isPkgEnabled, reload, - save, - cfg, - cfgAny, create, + rename, switchTo, reloadList, togglePkg, diff --git a/src/types.ts b/src/types.ts index 45978b4..ece0439 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,13 +26,47 @@ export interface ProfileMeta { name: string; } -export interface Profile extends ProfileMeta { - data: { - mods: string[]; - cfg: { [key: string]: string | boolean | number }; - }; +export interface SegatoolsConfig { + target: string; + amfs: string; + option: string; + appdata: string; + enable_aime: boolean; + intel: boolean; } +export interface DisplayConfig { + target: String; + rez: [number, number]; + mode: 'Window' | 'Borderless' | 'Fullscreen'; + rotation: number; + frequency: number; + borderless_fullscreen: boolean; +} + +export interface NetworkConfig { + network_type: 'Remote' | 'Artemis'; + local_path: string; + local_console: boolean; + remote_address: string; + keychip: string; + subnet: string; + suffix: number | null; +} +export interface BepInExConfig { + console: boolean; +} + +export interface Profile extends ProfileMeta { + mods: string[]; + sgt: SegatoolsConfig; + display: DisplayConfig; + network: NetworkConfig; + bepinex: BepInExConfig; +} + +export type Module = 'sgt' | 'display' | 'network'; + export interface Dirs { config_dir: string; data_dir: string;