diff --git a/README.md b/README.md index daf8108..9750166 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ wipwipwipwipwipwipwipwipwipwipwipwip ```sh bun install bun run tauri dev +bun run tauri build ``` ### Package format @@ -44,16 +45,28 @@ Arbitrary scripts are not supported by design and that will probably never chang - Clean data modding - Technically multi-platform +### Architecture details + +- Downloaded packages are stored in `%APPDATA%\7EVENDAYSHOLIDAYS\STARTLINER\data\pkg` (on Linux it's `~/.local/share/startliner` by default). +- Each profile is associated with a prefix directory `%APPDATA\7EVENDAYSHOLIDAYS\STARTLINER\data\profile-x` which includes the following: + - `option` with junctions of vanilla opts as well as Rainycolor package opts + - `BepInEx` with Rainycolor mods copied over + - It's currently pointless to symlink those since they are measured in kilobytes + - `segatools.ini` generated on-the-fly and pointing at `option` + - It's currently based on the existing `segatools.ini` but that will change in the future + - `aime.txt` (if enabled) + ### Todo -- Updates and auto-updates -- Run from CLI -- Support CHUNITHM -- Support segatools as a special package -- Progress bars -- Only rebuild the profile when needed +- Auto-updates +- Running from CLI +- Multiple profiles +- CHUNITHM support +- segatools as a special package +- Progress bars and other GUI sugar +- Rebuilding the profile only when necessary -## Endgame +### Endgame -- Support IO DLLs and artemis as special packages. -- Support other arcade games (if there is demand). +- IO DLLs and artemis as special packages +- Other arcade games (if there is demand) diff --git a/rust/src/appdata.rs b/rust/src/appdata.rs index 6ac902d..616dfea 100644 --- a/rust/src/appdata.rs +++ b/rust/src/appdata.rs @@ -16,13 +16,13 @@ impl AppData { .ok_or_else(|| anyhow!("No profile"))?; if enable { - let pkg = self.pkgs.get(key.clone())?; + let pkg = self.pkgs.get(&key)?; let loc = pkg.loc .clone() .ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?; profile.mods.insert(key); for d in &loc.dependencies { - self.toggle_package(d.clone(), true)?; + _ = self.toggle_package(d.clone(), true); } } else { profile.mods.remove(&key); diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index fc2dac1..a24abcf 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -55,7 +55,7 @@ pub async fn get_package(state: State<'_, tokio::sync::Mutex>, key: Pkg log::debug!("invoke: get_package({})", key); let appd = state.lock().await; - appd.pkgs.get(key) + appd.pkgs.get(&key) .map_err(|e| e.to_string()) .cloned() } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 9e0f370..783b3a5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -93,16 +93,6 @@ pub async fn run(_args: Vec) { }); })); - app.listen("install-end", closure!(clone apph, |ev| { - let payload = serde_json::from_str::(&ev.payload()); - let apph = apph.clone(); - tauri::async_runtime::spawn(async move { - let mutex = apph.state::>(); - let mut appd = mutex.lock().await; - _ = appd.toggle_package(payload.unwrap().pkg, true); - }); - })); - Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/rust/src/liner.rs b/rust/src/liner.rs index c3d4757..e23d5fd 100644 --- a/rust/src/liner.rs +++ b/rust/src/liner.rs @@ -13,7 +13,7 @@ async fn symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Resul #[cfg(target_os = "windows")] async fn symlink(src: impl AsRef, dst: impl AsRef) -> std::io::Result<()> { - //std::os::windows::fs::junction_point(src, dst) + //std::os::windows::fs::junction_point(src, dst) // is unstable junction::create(src, dst) } @@ -77,6 +77,13 @@ pub async fn line_up(p: &Profile) -> Result<()> { "targetAssembly", util::path_to_str(dir_out.join("BepInEx").join("core").join("BepInEx.Preloader.dll"))? ); + + if prepare_aime(p).await.unwrap_or(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.to_string_lossy(), opt_dir_out.to_string_lossy()); @@ -87,3 +94,15 @@ pub async fn line_up(p: &Profile) -> Result<()> { Ok(()) } + +// Todo multiple codes +async fn prepare_aime(p: &Profile) -> Result { + if p.get_bool("aime", true) { + if let Some(code) = p.cfg.get("aime-code") { + let code = code.as_str().expect("Invalid config"); + fs::write(util::profile_dir(&p).join("aime.txt"), code).await?; + return Ok(true); + } + } + Ok(false) +} \ No newline at end of file diff --git a/rust/src/pkg.rs b/rust/src/pkg.rs index 8c666f0..7d72cde 100644 --- a/rust/src/pkg.rs +++ b/rust/src/pkg.rs @@ -24,10 +24,11 @@ pub struct Package { pub rmt: Option } -#[derive(Clone, Default, Serialize, Deserialize)] +#[derive(Clone, Default, PartialEq, Serialize, Deserialize)] pub enum Kind { + Unchecked, #[default] Mod, - UnsupportedMod + Unsupported } #[derive(Clone, Default, Serialize, Deserialize)] diff --git a/rust/src/pkg_store.rs b/rust/src/pkg_store.rs index a4808a4..8da6ebb 100644 --- a/rust/src/pkg_store.rs +++ b/rust/src/pkg_store.rs @@ -37,8 +37,8 @@ impl PackageStore { } } - pub fn get(&self, key: PkgKey) -> Result<&Package> { - self.store.get(&key) + pub fn get(&self, key: &PkgKey) -> Result<&Package> { + self.store.get(key) .ok_or_else(|| anyhow!("Invalid package key")) } @@ -49,7 +49,7 @@ impl PackageStore { 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 { - self.update_package(key, pkg); + self.update_nonremote(key, pkg); } else { log::error!("couldn't reload {}", key); } @@ -68,7 +68,7 @@ impl PackageStore { while let Some(res) = futures.join_next().await { if let Ok(Ok(pkg)) = res { - self.update_package(pkg.key(), pkg); + self.update_nonremote(pkg.key(), pkg); } } @@ -203,10 +203,11 @@ impl PackageStore { } } - fn update_package(&mut self, key: PkgKey, mut new: Package) { - if let Some(old) = self.store.get(&key) { - new.rmt = old.rmt.clone(); + fn update_nonremote(&mut self, key: PkgKey, mut new: Package) { + if let Some(old) = self.store.remove(&key) { + new.rmt = old.rmt; } + self.store.insert(key, new); } diff --git a/rust/src/profile.rs b/rust/src/profile.rs index 75187fe..a55d3c2 100644 --- a/rust/src/profile.rs +++ b/rust/src/profile.rs @@ -14,6 +14,8 @@ pub struct Profile { pub mods: HashSet, pub wine_runtime: Option, pub wine_prefix: Option, + // cfg is temporarily just a map to make iteration easier + // eventually it should become strict pub cfg: HashMap } @@ -62,4 +64,23 @@ impl Profile { fs::write(&path, s).await.unwrap(); log::info!("Written to {}", path.to_string_lossy()); } + + pub fn get_bool(&self, key: &str, default: bool) -> bool { + self.cfg.get(key) + .and_then(|c| c.as_bool()) + .unwrap_or(default) + } + + pub fn get_int(&self, key: &str, default: i64) -> i64 { + self.cfg.get(key) + .and_then(|c| c.as_i64()) + .unwrap_or(default) + } + + pub fn get_str(&self, key: &str, default: &str) -> String { + self.cfg.get(key) + .and_then(|c| c.as_str()) + .unwrap_or(default) + .to_owned() + } } diff --git a/rust/src/start.rs b/rust/src/start.rs index e9c4b4b..f9f1665 100644 --- a/rust/src/start.rs +++ b/rust/src/start.rs @@ -44,41 +44,62 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> { log::info!("Launching amdaemon"); let mut amd_builder = Command::new("cmd.exe"); + let mut game_builder = Command::new(p.exe_dir.join("inject.exe")); + + let display_mode = p.get_str("display-mode", "borderless"); + amd_builder.env( "SEGATOOLS_CONFIG_PATH", &ini_path, ) - .creation_flags(create_no_window) .current_dir(&p.exe_dir) - .args(["/C", &util::path_to_str(p.exe_dir.join( "inject.exe"))?, "-d", "-k", "mu3hook.dll", "amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"]) - // Obviously this is a meme - // Output will be handled properly at a later time - .stdout(Stdio::null()) - .stderr(Stdio::null()); - - if let Some(v) = p.cfg.get("intel") { - if v == true { - amd_builder.env("OPENSSL_ia32cap", ":~0x20000000"); - } - } - - let mut amd = amd_builder.spawn()?; - - log::info!("Launching mu3"); - let mut game = Command::new(p.exe_dir.join( "inject.exe")) + .args([ + "/C", + &util::path_to_str(p.exe_dir.join("inject.exe"))?, "-d", "-k", "mu3hook.dll", + "amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json" + ]); + game_builder .env( "SEGATOOLS_CONFIG_PATH", ini_path, ) - .creation_flags(create_no_window) .current_dir(&p.exe_dir) - .args(["-d", "-k", "mu3hook.dll", "mu3.exe", "-monitor 1", "-screen-fullscreen", "0", "-popupwindow", "-screen-width", "1080", "-screen-height", "1920"]) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn()?; + .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"); + } + + if !cfg!(debug_assertions) { + amd_builder + .creation_flags(create_no_window) + // Obviously, this is a meme + // Output will be handled properly at a later time + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + game_builder + .creation_flags(create_no_window) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + } + + if p.get_bool("intel", false) == true { + amd_builder.env("OPENSSL_ia32cap", ":~0x20000000"); + } + + 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") }); @@ -91,8 +112,8 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> { log::info!("One of the processes died with return code {}", res); - _ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("amdaemon.exe").output().await; - _ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("mu3.exe").output().await; + _ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("amdaemon.exe").creation_flags(create_no_window).output().await; + _ = Command::new("taskkill.exe").arg("/f").arg("/im").arg("mu3.exe").creation_flags(create_no_window).output().await; set.join_next().await.expect("No spawn").expect("No result"); @@ -102,5 +123,4 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> { }); Ok(()) - //Ok((amd, game)) } \ No newline at end of file diff --git a/src/components/ModList.vue b/src/components/ModList.vue index 74f38c6..2819a92 100644 --- a/src/components/ModList.vue +++ b/src/components/ModList.vue @@ -13,7 +13,12 @@ const pkgs = usePkgStore(); const group = () => { const a = Object.assign( {}, - Object.groupBy(pkgs.allLocal, ({ namespace }) => namespace) + Object.groupBy( + pkgs.allLocal + .sort((p1, p2) => p1.namespace.localeCompare(p2.namespace)) + .sort((p1, p2) => p1.name.localeCompare(p2.name)), + ({ namespace }) => namespace + ) ); return a; }; diff --git a/src/components/Options.vue b/src/components/Options.vue index ccaa0e7..e50be30 100644 --- a/src/components/Options.vue +++ b/src/components/Options.vue @@ -22,18 +22,18 @@ const cfgIntel = _cfg('intel', false); const cfgRezW = _cfg('rez-w', 1080); const cfgRezH = _cfg('rez-h', 1920); const cfgDisplayMode = _cfg('display-mode', 'borderless'); -const cfgAime = _cfg('aime', true); -const cfgAimeCode = _cfg('aime-code', '00001111222233334444'); +const cfgAime = _cfg('aime', false); +const cfgAimeCode = _cfg('aime-code', '');