diff --git a/rust/capabilities/default.json b/rust/capabilities/default.json index 75802b3..3c384db 100644 --- a/rust/capabilities/default.json +++ b/rust/capabilities/default.json @@ -7,6 +7,11 @@ "core:default", "opener:default", "core:window:allow-close", + "core:window:allow-minimize", + "core:window:allow-unminimize", + "core:window:allow-set-focus", + "core:window:allow-hide", + "core:window:allow-show", "core:app:allow-app-hide", "shell:default", "dialog:default", diff --git a/rust/src/appdata.rs b/rust/src/appdata.rs index b2c4c37..b9ec149 100644 --- a/rust/src/appdata.rs +++ b/rust/src/appdata.rs @@ -1,7 +1,7 @@ use std::hash::{DefaultHasher, Hash, Hasher}; use crate::model::config::GlobalConfig; use crate::pkg::{Feature, Status}; -use crate::profiles::AnyProfile; +use crate::profiles::Profile; use crate::{model::misc::Game, pkg::PkgKey}; use crate::pkg_store::PackageStore; use crate::util; @@ -13,7 +13,7 @@ pub struct GlobalState { } pub struct AppData { - pub profile: Option, + pub profile: Option, pub pkgs: PackageStore, pub cfg: GlobalConfig, pub state: GlobalState, @@ -33,7 +33,7 @@ impl AppData { .unwrap_or_default(); let profile = match cfg.recent_profile { - Some((ref game, ref name)) => AnyProfile::load(game.clone(), name.clone()).ok(), + Some((game, ref name)) => Profile::load(game, name.clone()).ok(), None => None }; @@ -52,7 +52,7 @@ impl AppData { } pub fn switch_profile(&mut self, game: Game, name: String) -> Result<()> { - match AnyProfile::load(game.clone(), name.clone()) { + match Profile::load(game.clone(), name.clone()) { Ok(profile) => { self.profile = Some(profile); self.cfg.recent_profile = Some((game, name)); @@ -103,7 +103,7 @@ impl AppData { Ok(()) } - pub fn sum_packages(&self, p: &AnyProfile) -> String { + pub fn sum_packages(&self, p: &Profile) -> String { let mut hasher = DefaultHasher::new(); for key in p.mod_pkgs().into_iter() { if let Ok(pkg) = self.pkgs.get(&key) { diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index aff1f8d..0b55d89 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -6,8 +6,7 @@ use tauri::{AppHandle, Manager, State}; use crate::model::misc::Game; use crate::pkg::{Package, PkgKey}; use crate::pkg_store::{InstallResult, PackageStore}; -use crate::profiles::ongeki::OngekiProfile; -use crate::profiles::{self, AnyProfile, Profile, ProfileMeta, ProfilePaths}; +use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths}; use crate::appdata::{AppData, ToggleAction}; use crate::model::misc::StartCheckError; use crate::util; @@ -147,10 +146,21 @@ pub async fn get_all_packages(state: State<'_, Mutex>) -> Result>, game: Game) -> Result, ()> { + log::debug!("invoke: get_game_packages {game}"); + + let appd = state.lock().await; + + Ok(appd.pkgs.get_game_list(game)) +} + #[tauri::command] pub async fn fetch_listings(state: State<'_, Mutex>) -> Result<(), String> { log::debug!("invoke: fetch_listings"); + // let game; { let appd = state.lock().await; if !appd.pkgs.is_offline() { @@ -161,13 +171,26 @@ pub async fn fetch_listings(state: State<'_, Mutex>) -> Result<(), Stri log::info!("fetch_listings: skipped"); return Err("offline mode".to_owned()); } + // if let Some(profile) = &appd.profile { + // game = profile.meta.game; + // } else { + // return Err("No profile".to_owned()); + // } } - let listings = PackageStore::fetch_listings().await + // Can be this lazy for now as there are only two short lists + let listings1 = PackageStore::fetch_listings(Game::Ongeki).await + .map_err(|e| e.to_string())?; + + let listings2 = PackageStore::fetch_listings(Game::Chunithm).await .map_err(|e| e.to_string())?; let mut appd = state.lock().await; - appd.pkgs.process_fetched_listings(listings); + appd.pkgs.process_fetched_listings(listings1, Game::Ongeki); + appd.pkgs.process_fetched_listings(listings2, Game::Chunithm); + + appd.pkgs.save().await + .map_err(|e| e.to_string())?; Ok(()) } @@ -189,15 +212,10 @@ pub async fn init_profile( log::debug!("invoke: init_profile({}, {})", game, name); let mut appd = state.lock().await; - let new_profile = OngekiProfile::new(name) + let new_profile = Profile::new(ProfileMeta { game, 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())); + appd.profile = Some(new_profile); Ok(()) } @@ -242,7 +260,7 @@ pub async fn rename_profile( let mut appd = state.lock().await; if let Some(current) = &mut appd.profile { - if current.meta() == profile { + if current.meta == profile { current.rename(new_meta.name); } } @@ -277,7 +295,7 @@ pub async fn delete_profile(state: State<'_, Mutex>, profile: ProfileMe let mut appd = state.lock().await; if let Some(current) = &mut appd.profile { - if current.meta() == profile { + if current.meta == profile { appd.profile = None; } } @@ -286,7 +304,7 @@ pub async fn delete_profile(state: State<'_, Mutex>, profile: ProfileMe } #[tauri::command] -pub async fn get_current_profile(state: State<'_, Mutex>) -> Result, ()> { +pub async fn get_current_profile(state: State<'_, Mutex>) -> Result, ()> { log::debug!("invoke: get_current_profile"); let appd = state.lock().await; @@ -294,12 +312,12 @@ pub async fn get_current_profile(state: State<'_, Mutex>) -> Result>, profile: AnyProfile) -> Result<(), String> { +pub async fn sync_current_profile(state: State<'_, Mutex>, data: ProfileData) -> Result<(), String> { log::debug!("invoke: sync_current_profile"); let mut appd = state.lock().await; if let Some(p) = &mut appd.profile { - p.sync(profile); + p.sync(data); } Ok(()) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 0ed7893..b30e0f8 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -69,8 +69,8 @@ pub async fn run(_args: Vec) { } else { tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into())) .title("STARTLINER") - .inner_size(640f64, 480f64) - .min_inner_size(640f64, 480f64) + .inner_size(720f64, 480f64) + .min_inner_size(720f64, 480f64) .build()?; start_immediately = false; } @@ -176,6 +176,7 @@ pub async fn run(_args: Vec) { cmd::get_package, cmd::get_all_packages, + cmd::get_game_packages, cmd::reload_all_packages, cmd::fetch_listings, cmd::install_package, diff --git a/rust/src/model/local.rs b/rust/src/model/local.rs index 28df86b..7ed8fcb 100644 --- a/rust/src/model/local.rs +++ b/rust/src/model/local.rs @@ -1,6 +1,8 @@ use std::collections::{BTreeMap, BTreeSet}; -use serde::Deserialize; -use crate::pkg::PkgKeyVersion; +use serde::{Deserialize, Serialize}; +use crate::pkg::{Status, PkgKey, PkgKeyVersion}; + +use super::misc::Game; // manifest.json @@ -13,4 +15,13 @@ pub struct PackageManifest { #[serde(default)] pub installers: Vec> +} + +pub type PackageList = BTreeMap; + +#[derive(Serialize, Deserialize, Clone)] +pub struct PackageListEntry { + pub version: String, + pub status: Status, + pub games: Vec, } \ No newline at end of file diff --git a/rust/src/model/misc.rs b/rust/src/model/misc.rs index 2cfdfe4..4529adc 100644 --- a/rust/src/model/misc.rs +++ b/rust/src/model/misc.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::pkg::PkgKey; -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)] pub enum Game { #[serde(rename = "ongeki")] Ongeki, @@ -17,6 +17,48 @@ impl Game { _ => None } } + + pub fn hook_exe(&self) -> &'static str { + match self { + Game::Ongeki => "mu3hook.dll", + Game::Chunithm => "chusanhook_x86.dll", + } + } + + pub fn hook_amd(&self) -> &'static str { + match self { + Game::Ongeki => "mu3hook.dll", + Game::Chunithm => "chusanhook_x64.dll", + } + } + + pub fn inject_exe(&self) -> &'static str { + match self { + Game::Ongeki => "inject.exe", + Game::Chunithm => "inject_x86.exe", + } + } + + pub fn inject_amd(&self) -> &'static str { + match self { + Game::Ongeki => "inject.exe", + Game::Chunithm => "inject_x64.exe", + } + } + + pub fn exe(&self) -> &'static str { + match self { + Game::Ongeki => "mu3.exe", + Game::Chunithm => "chusanApp.exe", + } + } + + pub fn amd_args(&self) -> Vec<&'static str> { + match self { + Game::Ongeki => vec!["-f", "-c", "config_common.json", "config_server.json", "config_client.json"], + Game::Chunithm => vec!["-c", "config_common.json", "config_server.json", "config_client.json", "config_cvt.json", "config_sp.json", "config_hook.json"] + } + } } impl std::fmt::Display for Game { diff --git a/rust/src/model/profile.rs b/rust/src/model/profile.rs index 84b5802..81d9afa 100644 --- a/rust/src/model/profile.rs +++ b/rust/src/model/profile.rs @@ -2,7 +2,9 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use crate::pkg::PkgKey; -#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] +use super::misc::Game; + +#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)] pub enum Aime { Disabled, #[default] BuiltIn, @@ -10,7 +12,7 @@ pub enum Aime { Other(PkgKey), } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct AMNet { pub name: String, pub addr: String, @@ -23,7 +25,7 @@ impl Default for AMNet { } } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct Segatools { pub target: PathBuf, pub hook: Option, @@ -38,11 +40,14 @@ pub struct Segatools { pub amnet: AMNet, } -impl Default for Segatools { - fn default() -> Self { +impl Segatools { + pub fn default_for(game: Game) -> Self { Segatools { target: PathBuf::default(), - hook: Some(PkgKey("segatools-mu3hook".to_owned())), + hook: match game { + Game::Ongeki => Some(PkgKey("segatools-mu3hook".to_owned())), + Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned())) + }, io: None, amfs: PathBuf::default(), option: PathBuf::default(), @@ -54,14 +59,14 @@ impl Default for Segatools { } } -#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] +#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)] pub enum DisplayMode { Window, #[default] Borderless, Fullscreen } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct Display { pub target: String, pub rez: (i32, i32), @@ -71,26 +76,32 @@ pub struct Display { pub borderless_fullscreen: bool, } -impl Default for Display { - fn default() -> Self { +impl Display { + pub fn default_for(game: Game) -> Self { Display { target: "default".to_owned(), - rez: (1080, 1920), + rez: match game { + Game::Chunithm => (1920, 1080), + Game::Ongeki => (1080, 1920), + }, mode: DisplayMode::Borderless, rotation: 0, - frequency: 60, + frequency: match game { + Game::Chunithm => 120, + Game::Ongeki => 60, + }, borderless_fullscreen: true, } } } -#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] +#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)] pub enum NetworkType { #[default] Remote, Artemis, } -#[derive(Deserialize, Serialize, Clone, Default)] +#[derive(Deserialize, Serialize, Clone, Default, Debug)] pub struct Network { pub network_type: NetworkType, @@ -104,7 +115,7 @@ pub struct Network { pub suffix: Option, } -#[derive(Deserialize, Serialize, Clone, Default)] +#[derive(Deserialize, Serialize, Clone, Default, Debug)] pub struct BepInEx { pub console: bool, } diff --git a/rust/src/model/segatools_base.rs b/rust/src/model/segatools_base.rs index ae14175..fca27ea 100644 --- a/rust/src/model/segatools_base.rs +++ b/rust/src/model/segatools_base.rs @@ -1,4 +1,8 @@ -pub fn segatools_base() -> String { +use super::misc::Game; + +pub fn segatools_base(game: Game) -> String { + match game { + Game::Ongeki => "[vfd] ; Enable VFD emulation. Disable to use a real VFD ; GP1232A02A FUTABA assembly. @@ -76,5 +80,219 @@ right2=0x4B ; K right3=0x4C ; L leftMenu=0x55 ; U -rightMenu=0x4F ; O".to_owned() +rightMenu=0x4F ; O".to_owned(), + Game::Chunithm => " +[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: If multiple machines are present on the same LAN then set +; this to 1 on exactly one machine and set this to 0 on all others. +dipsw1=1 +; Monitor type: 0 = 120FPS, 1 = 60FPS +dipsw2=1 +; Cab type: 0 = SP, 1 = CVT. SP will enable VFD and eMoney. This setting will switch +; the LED 837-15093-06 COM port and the AiMe reder hardware generation as well. +dipsw3=1 + +; ----------------------------------------------------------------------------- +; Misc. hooks settings +; ----------------------------------------------------------------------------- + +[gfx] +; Enables the graphics hook. +enable=1 +; Force the game to run windowed. +windowed=1 +; Add a frame to the game window if running windowed. +framed=0 +; Select the monitor to run the game on. (Fullscreen only, 0 =primary screen) +monitor=0 + +; ----------------------------------------------------------------------------- +; LED settings +; ----------------------------------------------------------------------------- + +[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\\chuni_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 +; Use the OpeNITHM protocol for serial LED output +controllerLedOutputOpeNITHM=0 + +; Serial port to send data to if using serial output. Default is COM5. +;serialPort=COM5 +; Baud rate for serial data (set to 115200 if using OpeNITHM) +;serialBaud=921600 + +; Data output a sequence of bytes, with JVS-like framing. +; Each \"packet\" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere, +; 0xD0 is used as an escape character -- if you receive D0 in the output, ignore +; it and use the next sent byte plus one instead. +; +; After the sync is one byte for the board number that was updated, followed by +; the red, green and blue values for each LED. +; +; Board 0 has 53 LEDs: +; [0]-[49]: snakes through left half of billboard (first column starts at top) +; [50]-[52]: left side partition LEDs +; +; Board 1 has 63 LEDs: +; [0]-[59]: right half of billboard (first column starts at bottom) +; [60]-[62]: right side partition LEDs +; +; Board 2 is the slider and has 31 LEDs: +; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers + + +; ----------------------------------------------------------------------------- +; Custom IO settings +; ----------------------------------------------------------------------------- + +[chuniio] +; Uncomment this if you have custom chuniio implementation comprised of a single 32bit DLL. +; (will use chu2to3 engine internally) +;path= + +; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs. +; x86 chuniio to path32, x64 to path64. Both are necessary. +;path32= +;path64= + +; ----------------------------------------------------------------------------- +; Input settings +; ----------------------------------------------------------------------------- + +; Keyboard bindings are specified as hexadecimal (prefixed with 0x) or decimal +; (not prefixed with 0x) virtual-key codes, a list of which can be found here: +; +; https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes +; +; This is, admittedly, not the most user-friendly configuration method in the +; world. An improved solution will be provided later. + +[io3] + +test=0x31 + +service=0x32 + +coin=0x33 + +ir=0x00 + +ir6=0x39 + +ir5=0x38 + +ir4=0x37 + +ir3=0x36 + +ir2=0x35 + +ir1=0x34 + +[ir] + +ir6=0x39 + +ir5=0x38 + +ir4=0x37 + +ir3=0x36 + +ir2=0x35 + +ir1=0x34 + +[slider] + +cell32=0x51 + +cell30=0x5A + +cell28=0x53 + +cell26=0x45 + +cell24=0x43 + +cell22=0x46 + +cell20=0x54 + +cell18=0x42 + +cell16=0x48 + +cell14=0x55 + +cell12=0x4D + +cell10=0x4B + +cell8=0x4F + +cell6=190 + +cell4=186 + +cell2=219 + +cell31=0x41 + +cell29=0x57 + +cell27=0x58 + +cell25=0x44 + +cell23=0x52 + +cell21=0x56 + +cell19=0x47 + +cell17=0x59 + +cell15=0x4E + +cell13=0x4A + +cell11=0x49 + +cell9=188 + +cell7=0x4C + +cell5=0x50 + +cell3=191 + +cell1=222 +".to_owned() + } } \ No newline at end of file diff --git a/rust/src/modules/package.rs b/rust/src/modules/package.rs index 4d9ace1..aafbcd2 100644 --- a/rust/src/modules/package.rs +++ b/rust/src/modules/package.rs @@ -4,14 +4,16 @@ use crate::pkg::PkgKey; use crate::util; use crate::profiles::ProfilePaths; -pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet) -> Result<()> { +pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet, redo_bepinex: bool) -> 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 redo_bepinex { + if pfx_dir.join("BepInEx").exists() { + tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?; + } } if !opt_dir.exists() { @@ -21,11 +23,14 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet {}, } } - pub async fn line_up(&self, p: &impl ProfilePaths) -> Result { + pub async fn line_up(&self, p: &impl ProfilePaths, game: Game) -> Result { log::debug!("begin line-up: segatools"); let pfx_dir = p.data_dir(); @@ -42,7 +42,7 @@ impl Segatools { let ini_path = p.config_dir().join("segatools-base.ini"); if !ini_path.exists() { - tokio::fs::write(&ini_path, segatools_base()).await + tokio::fs::write(&ini_path, segatools_base(game)).await .map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?; } if !pfx_dir.exists() { @@ -69,12 +69,14 @@ impl Segatools { .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 game == Game::Ongeki { + ini_out.with_section(Some("unity")) + .set("enable", "1") + .set( + "targetAssembly", + pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()? + ); + } if self.aime != Aime::Disabled { ini_out.with_section(Some("aime")) @@ -84,11 +86,16 @@ impl Segatools { let mut aimeio = ini_out.with_section(Some("aimeio")); aimeio .set("path", util::pkg_dir().join(key.to_string()).join("segatools").join("aimeio.dll").stringify()?) - .set("gameId", "SDDT") + .set("serverAddress", &self.amnet.addr) .set("useAimeDBForPhysicalCards", if self.amnet.physical { "1" } else { "0" }) .set("enableKeyboardMode", "0"); + match game { + Game::Ongeki => aimeio.set("gameId", "SDDT"), + Game::Chunithm => aimeio.set("gameId", "SDHD") + }; + if let Ok(keyboard_code) = std::fs::read_to_string(p.config_dir().join("aime.txt")) { log::debug!("{} {}", keyboard_code, keyboard_code.len()); if keyboard_code.len() == 20 { @@ -105,12 +112,14 @@ impl Segatools { .set("enable", "0"); } - if let Some(io) = &self.io { - ini_out.with_section(Some("mu3io")) - .set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?); - } else { - ini_out.with_section(Some("mu3io")) - .set("path", ""); + if game == Game::Ongeki { + if let Some(io) = &self.io { + ini_out.with_section(Some("mu3io")) + .set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?); + } else { + ini_out.with_section(Some("mu3io")) + .set("path", ""); + } } log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out); diff --git a/rust/src/pkg.rs b/rust/src/pkg.rs index 571de35..ee6794e 100644 --- a/rust/src/pkg.rs +++ b/rust/src/pkg.rs @@ -7,11 +7,11 @@ use enumflags2::{bitflags, make_bitflags, BitFlags}; use crate::{model::{local::{self, PackageManifest}, rainy}, util}; // {namespace}-{name} -#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)] +#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)] pub struct PkgKey(pub String); // {namespace}-{name}-{version} -#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)] +#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)] pub struct PkgKeyVersion(String); #[derive(Clone, Default, Serialize, Deserialize)] @@ -36,10 +36,11 @@ pub enum Status { #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Feature { Mod, - Hook, - GameIO, Aime, AMNet, + Mu3Hook, + Mu3IO, + ChusanHook, } #[derive(Clone, Serialize, Deserialize)] @@ -196,14 +197,16 @@ impl Package { // Multiple features in the same dll (yubideck etc.) should be supported at some point let mut flags = BitFlags::default(); if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") { - if module.ends_with("hook") { - flags |= Feature::Hook; + if module == "mu3hook" { + flags |= Feature::Mu3Hook; + } else if module == "chusanhook" { + flags |= Feature::ChusanHook; } else if module == "amnet" { flags |= Feature::AMNet | Feature::Aime; } else if module == "aimeio" { flags |= Feature::Aime; - } else if module.ends_with("io") { - flags |= Feature::GameIO; + } else if module == "mu3io" { + flags |= Feature::Mu3IO; } } return Status::OK(flags); diff --git a/rust/src/pkg_store.rs b/rust/src/pkg_store.rs index d2bbb61..d1988b1 100644 --- a/rust/src/pkg_store.rs +++ b/rust/src/pkg_store.rs @@ -5,15 +5,20 @@ use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter}; use tokio::fs; use tokio::task::JoinSet; +use crate::model::local::{PackageList, PackageListEntry}; +use crate::model::misc::Game; use crate::model::rainy; -use crate::pkg::{Package, PkgKey, Remote}; +use crate::pkg::{Package, PkgKey, Remote, Status}; use crate::util; use crate::download_handler::DownloadHandler; pub struct PackageStore { store: HashMap, - app: AppHandle, + meta_list: PackageList, dlh: DownloadHandler, + + app: AppHandle, + offline: bool, } @@ -29,8 +34,17 @@ pub enum InstallResult { impl PackageStore { pub fn new(app: AppHandle) -> PackageStore { + let meta_list = std::fs::read_to_string(util::config_dir().join("package-list.json")) + .map_err(|e| anyhow!(e)) + .and_then(|s| serde_json::from_str::(&s).map_err(|e| anyhow!(e))) + .unwrap_or_else(|e| { + log::warn!("unable to read package-list: {e}"); + PackageList::new() + }); + PackageStore { store: HashMap::new(), + meta_list, app: app.clone(), dlh: DownloadHandler::new(app), offline: true @@ -46,6 +60,13 @@ impl PackageStore { self.store.clone() } + pub fn get_game_list(&self, game: Game) -> Vec { + self.meta_list.iter() + .filter(|(_, v)| v.games.contains(&game)) + .map(|(k, _)| k.clone()) + .collect() + } + pub async fn reload_package(&mut self, key: PkgKey) { let dir = util::pkg_dir().join(&key.0); if let Ok(pkg) = Package::from_dir(dir).await { @@ -75,14 +96,23 @@ impl PackageStore { Ok(()) } - pub async fn fetch_listings() -> Result> { + pub async fn save(&self) -> Result<()> { + tokio::fs::write( + util::config_dir().join("package-list.json"), + serde_json::to_string_pretty(&self.meta_list)? + ).await?; + + Ok(()) + } + + pub async fn fetch_listings(game: Game) -> Result> { use async_compression::futures::bufread::GzipDecoder; use futures::{ io::{self, BufReader, ErrorKind}, prelude::*, }; - let response = reqwest::get("https://rainy.patafour.zip/c/ongeki/api/v1/package/").await?; + let response = reqwest::get(format!("https://rainy.patafour.zip/c/{game}/api/v1/package/")).await?; let reader = response .bytes_stream() @@ -100,11 +130,23 @@ impl PackageStore { self.offline } - pub fn process_fetched_listings(&mut self, listings: Vec) { + pub fn process_fetched_listings(&mut self, listings: Vec, game: Game) { for listing in listings { // This is None if the package has no versions for whatever reason if let Some(r) = Package::from_rainy(listing) { - //log::warn!("D {}", &r.rmt.as_ref().unwrap().dependencies.first().unwrap_or(&"Nothing".to_owned())); + let mut meta_entry = self.meta_list.remove(&r.key()).unwrap_or_else(|| { + PackageListEntry { + // from_rainy() is guaranteed to include rmt + version: r.rmt.as_ref().unwrap().version.clone(), + status: Status::Unchecked, + games: vec![ game ], + } + }); + if !meta_entry.games.contains(&game) { + meta_entry.games.push(game); + } + self.meta_list.insert(r.key(), meta_entry); + match self.store.get_mut(&r.key()) { Some(l) => { l.rmt = r.rmt; @@ -226,9 +268,8 @@ impl PackageStore { async fn clean_up_file(path: impl AsRef, name: &str, force: bool) -> Result<()> { let path = path.as_ref().join(name); if force || path.exists() { - tokio::fs::remove_file(path) - .await - .map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?; + tokio::fs::remove_file(path).await + .map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?; } Ok(()) @@ -242,6 +283,7 @@ impl PackageStore { Self::clean_up_file(&path, "icon.png", true).await?; Self::clean_up_file(&path, "manifest.json", true).await?; Self::clean_up_file(&path, "README.md", true).await?; + Self::clean_up_file(&path, "post_load.ps1", false).await?; tokio::fs::remove_dir(path.as_ref()) .await diff --git a/rust/src/profiles/mod.rs b/rust/src/profiles/mod.rs index 07bb386..55b275e 100644 --- a/rust/src/profiles/mod.rs +++ b/rust/src/profiles/mod.rs @@ -1,15 +1,19 @@ -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, pkg_store::PackageStore, util}; +use crate::{model::{misc::Game, profile::Aime}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util}; +use tauri::Emitter; +use std::process::Stdio; +use crate::model::profile::BepInEx; +use crate::model::{profile::{Display, DisplayMode, Network, Segatools}, segatools_base::segatools_base}; +use anyhow::{anyhow, Result}; +use std::fs::File; +use tokio::process::Command; +use tokio::task::JoinSet; -pub mod ongeki; - -#[derive(Deserialize, Serialize, Clone)] -pub enum AnyProfile { - OngekiProfile(OngekiProfile) +pub trait ProfilePaths { + fn config_dir(&self) -> PathBuf; + fn data_dir(&self) -> PathBuf; } #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] @@ -18,147 +22,311 @@ pub struct ProfileMeta { 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<()>; +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)) + } } -pub trait ProfilePaths { - fn config_dir(&self) -> PathBuf; - fn data_dir(&self) -> PathBuf; +#[derive(Deserialize, Serialize, Clone)] +pub struct Profile { + pub meta: ProfileMeta, + pub data: ProfileData, } -impl AnyProfile { +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct ProfileData { + pub mods: BTreeSet, + pub sgt: Segatools, + pub display: Option, + pub network: Network, + + #[serde(skip_serializing_if = "Option::is_none")] + pub bepinex: Option, + + #[cfg(not(target_os = "windows"))] + pub wine: crate::model::profile::Wine, +} + +impl Profile { + pub fn new(mut meta: ProfileMeta) -> Result { + meta.name = fixed_name(&meta, true); + log::debug!("created profile-{:?}", &meta); + let p = Profile { + data: ProfileData { + mods: BTreeSet::new(), + sgt: Segatools::default_for(meta.game), + #[cfg(target_os = "windows")] + display: if meta.game == Game::Ongeki { Some(Display::default_for(meta.game)) } else { None }, + #[cfg(not(target_os = "windows"))] + display: None, + network: Network::default(), + bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None }, + #[cfg(not(target_os = "windows"))] + wine: crate::model::profile::Wine::default(), + }, + meta: meta.clone() + }; + p.save()?; + std::fs::create_dir_all(p.config_dir())?; + std::fs::create_dir_all(p.data_dir())?; + std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base(meta.game))?; + + Ok(p) + } pub fn load(game: Game, name: String) -> Result { - Ok(match game { - Game::Ongeki => AnyProfile::OngekiProfile(OngekiProfile::load(name)?), - Game::Chunithm => panic!("Not implemented") - }) + let path = util::profile_config_dir(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))?; + + log::debug!("{:?}", data); + + Ok(Profile { + meta: ProfileMeta { + game, name + }, + data + }) + } else { + Err(anyhow!("Unable to open {:?}", path)) + } } 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() - } - } + let path = self.config_dir().join("profile.json"); + + let s = serde_json::to_string_pretty(&self.data)?; + 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(()) } pub fn rename(&mut self, name: String) { - match self { - Self::OngekiProfile(p) => { - p.name = Some(fixed_name(&ProfileMeta { name, game: Game::Ongeki }, false)); - } - } + self.meta.name = fixed_name(&ProfileMeta { game: self.meta.game, name}, false); } pub fn mod_pkgs(&self) -> &BTreeSet { - match self { - Self::OngekiProfile(p) => &p.mods - } + &self.data.mods } pub fn mod_pkgs_mut(&mut self) -> &mut BTreeSet { - match self { - Self::OngekiProfile(p) => &mut p.mods - } + &mut self.data.mods } pub fn special_pkgs(&self) -> Vec { let mut res = Vec::new(); - match self { - Self::OngekiProfile(p) => { - if let Some(hook) = &p.sgt.hook { - res.push(hook.clone()); - } - if let Some(io) = &p.sgt.io { - res.push(io.clone()); - } - } + if let Some(hook) = &self.data.sgt.hook { + res.push(hook.clone()); + } + if let Some(io) = &self.data.sgt.io { + res.push(io.clone()); + } + if let Aime::AMNet(aime) = &self.data.sgt.aime { + res.push(aime.clone()); + } else if let Aime::Other(aime) = &self.data.sgt.aime { + res.push(aime.clone()); } res } pub fn fix(&mut self, store: &PackageStore) { - match self { - Self::OngekiProfile(p) => p.sgt.fix(store) - } + self.data.sgt.fix(store); } - pub fn sync(&mut self, source: AnyProfile) { - match self { - Self::OngekiProfile(p) => { - #[allow(irrefutable_let_patterns)] - if let AnyProfile::OngekiProfile(source) = source { - p.bepinex = source.bepinex; - p.display = source.display; - p.network = source.network; - p.sgt = source.sgt; - } else { - log::error!("sync: invalid profile type {:?}", source); - } - } + pub fn sync(&mut self, source: ProfileData) { + if self.data.bepinex.is_some() { + self.data.bepinex = source.bepinex; } + if self.data.display.is_some() { + self.data.display = source.display; + } + // if self.data.network.is_some() { + self.data.network = source.network; + // } + // if self.data.sgt.is_some() { + self.data.sgt = source.sgt; + // } } pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> { - match self { - Self::OngekiProfile(_p) => { - #[cfg(target_os = "windows")] - let info = _p.display.line_up()?; + let info = match &self.data.display { + None => None, + Some(display) => display.line_up()? + }; - let res = self.line_up_the_rest(pkg_hash).await; + let res = self.line_up_the_rest(pkg_hash).await; - #[cfg(target_os = "windows")] - if let Some(info) = info { - use crate::model::profile::Display; - if res.is_ok() { - Display::wait_for_exit(_app, info); - } else { - Display::clean_up(&info)?; - } - } - - res + #[cfg(target_os = "windows")] + if let Some(info) = info { + use crate::model::profile::Display; + if res.is_ok() { + Display::wait_for_exit(_app, info); + } else { + Display::clean_up(&info)?; } } + + res } async fn line_up_the_rest(&self, 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(); - - 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(()) - } + if !self.data_dir().exists() { + tokio::fs::create_dir(self.data_dir()).await?; } + + let hash_path = self.data_dir().join(".sl-state"); + + util::clean_up_opts(self.data_dir().join("option"))?; + + let hash_check = Self::hash_check(&hash_path, &pkg_hash).await?; + prepare_packages(&self.meta, &self.data.mods, hash_check).await + .map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?; + let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await + .map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?; + self.data.network.line_up(&mut ini)?; + + ini.write_to_file(self.data_dir().join("segatools.ini")) + .map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?; + + if let Some(bepinex) = &self.data.bepinex { + bepinex.line_up(&self.meta)?; + } + + Ok(()) } pub async fn start(&self, app: AppHandle) -> Result<()> { - match self { - Self::OngekiProfile(p) => p.start(app).await + 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.data.sgt.target); + let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?; + let sgt_dir = self.data.sgt.hook_dir()?; + + #[cfg(target_os = "windows")] + { + game_builder = Command::new(sgt_dir.join(self.meta.game.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(self.meta.game.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(self.meta.game.inject_amd())) + .args(["-d", "-k"]) + .arg(sgt_dir.join(self.meta.game.hook_amd())) + .arg("amdaemon.exe") + .args(self.meta.game.amd_args()); + 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(self.meta.game.hook_exe())) + .arg(self.meta.game.exe()); + + if let Some(display) = &self.data.display { + game_builder.args([ + "-monitor 1", + "-screen-width", &display.rez.0.to_string(), + "-screen-height", &display.rez.1.to_string(), + "-screen-fullscreen", if display.mode == DisplayMode::Fullscreen { "1" } else { "0" } + ]); + if 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.exe.log"))?; + let game_log = File::create(self.data_dir().join(format!("{}.log", self.meta.game.exe())))?; + + 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.data.sgt.intel == true { + amd_builder.env("OPENSSL_ia32cap", ":~0x20000000"); + } + + util::pkill("amdaemon.exe").await; + + log::info!("Launching amdaemon: {:?}", amd_builder); + log::info!("Launching {}: {:?}", self.meta.game, 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") + }); + + set.spawn(async move { + (game.wait().await.expect("game failed to run"), "game") + }); + + if let Err(e) = app.emit("launch-start", "") { + log::warn!("Unable to emit launch-start: {}", e); + } + + let (rc, process) = set.join_next().await.expect("No spawn").expect("No result"); + + log::info!("{} died with return code {}", process, rc); + + if process == "amdaemon" { + util::pkill(self.meta.game.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(()) } async fn hash_check(prev_hash_path: &impl AsRef, new_hash: &str) -> Result { @@ -174,11 +342,19 @@ impl AnyProfile { } } -impl std::fmt::Debug for AnyProfile { +impl ProfilePaths for Profile { + fn config_dir(&self) -> PathBuf { + self.meta.config_dir() + } + + fn data_dir(&self) -> PathBuf { + self.meta.data_dir() + } +} + +impl std::fmt::Debug for Profile { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::OngekiProfile(p) => f.debug_tuple("ongeki").field(&p.name).finish(), - } + f.debug_tuple(&self.meta.game.to_string()).field(&self.meta.name).finish() } } @@ -227,7 +403,7 @@ pub fn fixed_name(meta: &ProfileMeta, prepend_new: bool) -> String { .replace(" ", "-") .replace("..", "").replace("/", "").replace("\\", ""); - while prepend_new && util::profile_config_dir(&meta.game, &name).exists() { + while prepend_new && util::profile_config_dir(meta.game, &name).exists() { name = format!("new-{}", name); } diff --git a/rust/src/profiles/ongeki.rs b/rust/src/profiles/ongeki.rs deleted file mode 100644 index c993ddc..0000000 --- a/rust/src/profiles/ongeki.rs +++ /dev/null @@ -1,229 +0,0 @@ -use serde::{Deserialize, Serialize}; -use tauri::AppHandle; -use tauri::Emitter; -use std::{collections::BTreeSet, path::PathBuf, process::Stdio}; -use crate::model::profile::BepInEx; -use crate::profiles::fixed_name; -use crate::{model::{profile::{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::profile::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::profile::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)) - } -} \ No newline at end of file diff --git a/rust/src/util.rs b/rust/src/util.rs index f3cf2c9..c367354 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -47,7 +47,7 @@ pub fn config_dir() -> &'static Path { &DIRS.get().expect("Directories uninitialized").config_dir } -pub fn profile_config_dir(game: &Game, name: &str) -> PathBuf { +pub fn profile_config_dir(game: Game, name: &str) -> PathBuf { config_dir().join(format!("profile-{}-{}", game, name)) } diff --git a/src/components/App.vue b/src/components/App.vue index b5203db..1bc49e2 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -16,6 +16,7 @@ import StartButton from './StartButton.vue'; import { invoke } from '../invoke'; import { useGeneralStore, usePkgStore, usePrfStore } from '../stores'; import { Dirs } from '../types'; +import { listen } from '@tauri-apps/api/event'; const pkg = usePkgStore(); const prf = usePrfStore(); @@ -27,6 +28,7 @@ const currentTab: Ref = ref(3); const pkgSearchTerm = ref(''); const isProfileDisabled = computed(() => prf.current === null); +const isRunning = ref(false); onMounted(async () => { invoke('list_directories').then((d) => { @@ -47,8 +49,23 @@ onMounted(async () => { key: 'segatools-mu3hook', force: false, }); + await invoke('install_package', { + key: 'segatools-chusanhook', + force: false, + }); }); }); + +listen('launch-start', () => { + isRunning.value = true; + currentTab.value = 5; +}); + +listen('launch-end', () => { + isRunning.value = false; + currentTab.value = 0; +}); + diff --git a/src/components/ModList.vue b/src/components/ModList.vue index da33155..5fa668b 100644 --- a/src/components/ModList.vue +++ b/src/components/ModList.vue @@ -1,11 +1,13 @@ @@ -68,7 +78,7 @@ const missing = computed(() => { /> -
+
diff --git a/src/components/ModListEntry.vue b/src/components/ModListEntry.vue index 52806c8..6d44d93 100644 --- a/src/components/ModListEntry.vue +++ b/src/components/ModListEntry.vue @@ -32,7 +32,6 @@ const model = computed({
- -import { ref } from 'vue'; +import { Ref, ref } from 'vue'; import Divider from 'primevue/divider'; import MultiSelect from 'primevue/multiselect'; import ToggleSwitch from 'primevue/toggleswitch'; import ModStoreEntry from './ModStoreEntry.vue'; -import { usePkgStore } from '../stores'; +import { invoke } from '../invoke'; +import { usePkgStore, usePrfStore } from '../stores'; +import { pkgKey } from '../util'; const pkgs = usePkgStore(); +const prf = usePrfStore(); const empty = ref(true); const props = defineProps({ search: String, }); +const gameSublist: Ref = ref([]); + +invoke('get_game_packages', { + game: prf.current?.meta.game, +}).then((list) => { + gameSublist.value = list as string[]; +}); + const list = () => { const res = pkgs.allRemote + .filter((p) => gameSublist.value.includes(pkgKey(p))) .filter( (p) => props.search === undefined || diff --git a/src/components/ModTitlecard.vue b/src/components/ModTitlecard.vue index 966cf3d..6726782 100644 --- a/src/components/ModTitlecard.vue +++ b/src/components/ModTitlecard.vue @@ -53,13 +53,16 @@ const iconSrc = computed(() => { > diff --git a/src/components/OptionList.vue b/src/components/OptionList.vue index 36c7d54..9565b94 100644 --- a/src/components/OptionList.vue +++ b/src/components/OptionList.vue @@ -1,399 +1,30 @@ diff --git a/src/components/ProfileList.vue b/src/components/ProfileList.vue index 2ea6352..c594675 100644 --- a/src/components/ProfileList.vue +++ b/src/components/ProfileList.vue @@ -19,7 +19,6 @@ const prf = usePrfStore(); icon="pi pi-plus" class="chunithm-button profile-button" @click="() => prf.create('chunithm')" - :disabled="true" />
diff --git a/src/components/ProfileListEntry.vue b/src/components/ProfileListEntry.vue index 16c2b3c..1d1b62e 100644 --- a/src/components/ProfileListEntry.vue +++ b/src/components/ProfileListEntry.vue @@ -61,7 +61,8 @@ const deleteProfile = async () => {