From caa20a3aa0c3418de3ed4b53b18d26974e8cfb3c Mon Sep 17 00:00:00 2001 From: akanyan Date: Sat, 15 Mar 2025 00:08:33 +0100 Subject: [PATCH] feat: initial support for segatools pkgs --- README.md | 60 ++++++++++------------------- TODO.md | 12 ++++++ rust/src/cmd.rs | 8 +++- rust/src/model/config.rs | 5 +++ rust/src/model/local.rs | 10 +++-- rust/src/model/segatools_base.rs | 6 +-- rust/src/modules/segatools.rs | 8 ++++ rust/src/pkg.rs | 30 +++++++++++++-- rust/src/pkg_store.rs | 14 ++++--- rust/src/profiles/mod.rs | 7 ++-- rust/src/profiles/ongeki.rs | 18 +++++---- src/components/App.vue | 66 +++++++++++++++++++++++++------- src/components/InstallButton.vue | 5 ++- src/components/ModList.vue | 23 +++++++++-- src/components/ModStore.vue | 31 ++++++++++----- src/components/OptionList.vue | 37 ++++++++++++------ src/components/StartButton.vue | 3 ++ src/components/UpdateButton.vue | 5 ++- src/stores.ts | 7 +++- src/types.ts | 3 ++ 20 files changed, 246 insertions(+), 112 deletions(-) create mode 100644 TODO.md diff --git a/README.md b/README.md index 18ef624..233964d 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,39 @@ -## STARTLINER +# STARTLINER A simple and easy to use launcher, configuration tool and mod manager for [many games](https://silentblue.remywiki.com/ONGEKI:bright_MEMORY) (more to come) using [Rainycolor Watercolor](https://rainy.patafour.zip). -Intended for those who just want a glorified `start.bat` clicker, without VHDs, keychips etc. -(for an all-in-one solution, check out the [BlueSteel launcher](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)). - Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome. -### Usage +## Features + +- [Clean](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details) data modding +- Segatools configuration +- Display configuration with automatic rollback +- Support for multiple configurations pointing at the same data + +## Usage + +Download a prebuilt binary from [Modding Re:Fresh](https://discord.gg/jxvzHjjEmc) or build it yourself: ```sh bun install -bun run tauri dev bun run tauri build ``` -Once a profile is set up, it is possible to bypass the GUI: +Create a profile, then click on things in the configuration tab (game path, `amfs` and network at the least). STARTLINER expects clean data with unpacked binaries. Anything else you can have in the game directory (segatools, BepInEx, etc.) can be present, but will not be used. + +Once a profile has been set up, it is possible to bypass the GUI: ```sh startliner --start --game ongeki --profile ``` -### Package format +To create a desktop shortcut: `Copy -> Paste Shortcut -> Properties`, and then append `--start --game ongeki --profile ` to `Target`. + +## Package format - [Package format requirements](https://rainy.patafour.zip/package/create/docs/) -- A subset of [the simple Rainycolor format](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou/wiki/Create-Module#user-content-rainycolor-simple) is currently supported. +- A subset of the simple BlueSteel Rainycolor format is currently supported. [Full reference (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou/wiki/Create-Module#user-content-rainycolor-simple) ``` ├───app @@ -42,35 +51,6 @@ More file overrides may be supported in the future. Arbitrary scripts are not supported by design and that will probably never change. -### Features +## See also -- Clean data modding -- Monitor selection -- segatools configuration UI -- Etc - -### Architecture details - -- Downloaded packages are stored in `%LOCALAPPDATA%\STARTLINER\data\pkg` (or `$XDG_DATA_HOME/startliner/pkg`). -- Each profile is associated with a prefix directory `%LOCALAPPDATA%\STARTLINER\data\profile-x` which includes: - - `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 - - logs -- Persistent configuration is stored in `%APPDATA%\STARTLINER` (or `$XDG_CONFIG_HOME/startliner`). - -### Todo - -- Auto-updates -- CHUNITHM support -- segatools as a special package -- Progress bars and other GUI sugar -- Search bar -- Start check - -### Endgame - -- IO DLLs and artemis as special packages -- Other arcade games (if there is demand) +- [BlueSteel launcher (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou) diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..bd33db9 --- /dev/null +++ b/TODO.md @@ -0,0 +1,12 @@ +### Short-term + +- CHUNITHM support +- Start checks +- https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63 + +### Long-term + +- Auto-updates +- Progress bars and other GUI sugar +- IO DLLs and artemis as special packages +- Other arcade games (if there is demand) diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index 4dd5398..cbc04ab 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -45,11 +45,15 @@ pub async fn kill() -> Result<(), String> { } #[tauri::command] -pub async fn install_package(state: State<'_, tokio::sync::Mutex>, key: PkgKey) -> Result { +pub async fn install_package( + state: State<'_, tokio::sync::Mutex>, + key: PkgKey, + force: bool +) -> Result { log::debug!("invoke: install_package({})", key); let mut appd = state.lock().await; - appd.pkgs.install_package(&key, true, true) + appd.pkgs.install_package(&key, force, true) .await .map_err(|e| e.to_string()) } diff --git a/rust/src/model/config.rs b/rust/src/model/config.rs index 28b6ee5..66cb7e1 100644 --- a/rust/src/model/config.rs +++ b/rust/src/model/config.rs @@ -1,9 +1,12 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; +use crate::pkg::PkgKey; #[derive(Deserialize, Serialize, Clone)] pub struct Segatools { pub target: PathBuf, + pub hook: Option, + pub io: Option, pub amfs: PathBuf, pub option: PathBuf, pub appdata: PathBuf, @@ -15,6 +18,8 @@ impl Default for Segatools { fn default() -> Self { Segatools { target: PathBuf::default(), + hook: Some(PkgKey("segatools-mu3hook".to_owned())), + io: None, amfs: PathBuf::default(), option: PathBuf::default(), appdata: PathBuf::from("appdata"), diff --git a/rust/src/model/local.rs b/rust/src/model/local.rs index 3a66345..28df86b 100644 --- a/rust/src/model/local.rs +++ b/rust/src/model/local.rs @@ -1,14 +1,16 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use serde::Deserialize; use crate::pkg::PkgKeyVersion; // manifest.json #[derive(Deserialize)] -#[allow(dead_code)] pub struct PackageManifest { pub name: String, pub version_number: String, pub description: String, - pub dependencies: BTreeSet -} + pub dependencies: BTreeSet, + + #[serde(default)] + pub installers: Vec> +} \ No newline at end of file diff --git a/rust/src/model/segatools_base.rs b/rust/src/model/segatools_base.rs index 7563cb3..ae14175 100644 --- a/rust/src/model/segatools_base.rs +++ b/rust/src/model/segatools_base.rs @@ -1,9 +1,5 @@ pub fn segatools_base() -> String { -"; mu3io is TBD -[mu3io] -path= - -[vfd] +"[vfd] ; Enable VFD emulation. Disable to use a real VFD ; GP1232A02A FUTABA assembly. enable=1 diff --git a/rust/src/modules/segatools.rs b/rust/src/modules/segatools.rs index e3ee79f..d4c6987 100644 --- a/rust/src/modules/segatools.rs +++ b/rust/src/modules/segatools.rs @@ -57,6 +57,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("mu3io.dll").stringify()?); + } else { + ini_out.with_section(Some("mu3io")) + .set("path", ""); + } + log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out); if !opt_dir_out.exists() { diff --git a/rust/src/pkg.rs b/rust/src/pkg.rs index b5c5d0d..04431b5 100644 --- a/rust/src/pkg.rs +++ b/rust/src/pkg.rs @@ -3,7 +3,7 @@ use derive_more::Display; use serde::{Deserialize, Serialize}; use std::{collections::BTreeSet, path::{Path, PathBuf}}; use tokio::fs; -use crate::{model::{local, rainy}, util}; +use crate::{model::{local::{self, PackageManifest}, rainy}, util}; // {namespace}-{name} #[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)] @@ -27,8 +27,10 @@ pub struct Package { #[derive(Clone, Default, PartialEq, Serialize, Deserialize)] pub enum Kind { Unchecked, + Unsupported, #[default] Mod, - Unsupported + Hook, + IO, } #[derive(Clone, Default, Serialize, Deserialize)] @@ -84,6 +86,7 @@ impl Package { .unwrap() .to_owned(); + let kind = Self::parse_kind(&mft); let dependencies = Self::sanitize_deps(mft.dependencies); Ok(Package { @@ -94,7 +97,7 @@ impl Package { loc: Some(Local { version: mft.version_number, path: dir.to_owned(), - kind: Kind::Mod, + kind, dependencies }), rmt: None @@ -166,4 +169,25 @@ impl Package { } res } + + fn parse_kind(mft: &PackageManifest) -> Kind { + if mft.installers.len() == 0 { + return Kind::Mod;//Unchecked + } else if mft.installers.len() == 1 { + if let Some(serde_json::Value::String(id)) = &mft.installers[0].get("identifier") { + if id == "rainycolor" { + return Kind::Mod + } else if id == "segatools" { + if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") { + if module.ends_with("hook") { + return Kind::Hook; + } else if module.ends_with("io") { + return Kind::IO; + } + } + } + } + } + return Kind::Unsupported + } } \ No newline at end of file diff --git a/rust/src/pkg_store.rs b/rust/src/pkg_store.rs index 8da6ebb..8b82251 100644 --- a/rust/src/pkg_store.rs +++ b/rust/src/pkg_store.rs @@ -120,13 +120,14 @@ impl PackageStore { } pub async fn install_package(&mut self, key: &PkgKey, force: bool, install_deps: bool) -> Result { - log::debug!("Installing {}", key); + log::info!("installation request: {}/{}/{}", key, force, install_deps); let pkg = self.store.get(key) .ok_or_else(|| anyhow!("Attempted to install a nonexistent pkg"))? .clone(); if pkg.loc.is_some() && !force { + log::debug!("installation skipped"); return Ok(InstallResult::Ready); } @@ -152,7 +153,7 @@ impl PackageStore { if !zip_path.exists() { self.dlh.download_zip(&zip_path, &pkg)?; - log::debug!("Deferring {}", key); + log::debug!("deferring {}", key); return Ok(InstallResult::Deferred); } @@ -170,13 +171,13 @@ impl PackageStore { pkg: key.to_owned() })?; - log::info!("Installed {}", key); + log::info!("installed {}", key); Ok(InstallResult::Ready) } pub async fn delete_package(&mut self, key: &PkgKey, force: bool) -> Result<()> { - log::debug!("Will delete {} {}", key, force); + log::debug!("will delete {} {}", key, force); let pkg = self.store.get_mut(key) .ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?; @@ -191,7 +192,7 @@ impl PackageStore { self.app.emit("install-end", Payload { pkg: key.to_owned() })?; - log::info!("Deleted {}", key); + log::info!("deleted {}", key); } rv } else { @@ -216,7 +217,7 @@ impl PackageStore { if path.exists() { tokio::fs::remove_dir_all(path) .await - .map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?; + .map_err(|e| anyhow!("could not delete {}: {}", name, e))?; } Ok(()) @@ -237,6 +238,7 @@ impl PackageStore { // todo case sensitivity for linux Self::clean_up_dir(&path, "app").await?; Self::clean_up_dir(&path, "option").await?; + Self::clean_up_dir(&path, "segatools").await?; 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?; diff --git a/rust/src/profiles/mod.rs b/rust/src/profiles/mod.rs index 3528982..7a5a01b 100644 --- a/rust/src/profiles/mod.rs +++ b/rust/src/profiles/mod.rs @@ -3,7 +3,7 @@ use ongeki::OngekiProfile; use serde::{Deserialize, Serialize}; use tauri::AppHandle; use std::{collections::BTreeSet, path::{Path, PathBuf}}; -use crate::{model::{config::Display, misc::Game}, modules::package::prepare_packages, pkg::PkgKey, util}; +use crate::{model::misc::Game, modules::package::prepare_packages, pkg::PkgKey, util}; pub mod ongeki; @@ -72,14 +72,15 @@ impl AnyProfile { } pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> { match self { - Self::OngekiProfile(p) => { + Self::OngekiProfile(_p) => { #[cfg(target_os = "windows")] - let info = p.display.line_up()?; + let info = _p.display.line_up()?; let res = self.line_up_the_rest(pkg_hash).await; #[cfg(target_os = "windows")] if let Some(info) = info { + use crate::model::config::Display; if res.is_ok() { Display::wait_for_exit(_app, info); } else { diff --git a/rust/src/profiles/ongeki.rs b/rust/src/profiles/ongeki.rs index e56ad96..be36b45 100644 --- a/rust/src/profiles/ongeki.rs +++ b/rust/src/profiles/ongeki.rs @@ -88,10 +88,13 @@ impl Profile for OngekiProfile { let target_path = PathBuf::from(&self.sgt.target); let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?; + let sgt_dir = util::pkg_dir() + .join(self.sgt.hook.as_ref().ok_or_else(|| anyhow!("No hook"))?.to_string()) + .join("segatools"); #[cfg(target_os = "windows")] { - game_builder = Command::new(exe_dir.join("inject.exe")); + game_builder = Command::new(sgt_dir.join("inject.exe")); amd_builder = Command::new("cmd.exe"); } #[cfg(target_os = "linux")] @@ -99,7 +102,7 @@ impl Profile for OngekiProfile { game_builder = Command::new(&self.wine.runtime); amd_builder = Command::new(&self.wine.runtime); - game_builder.arg(exe_dir.join("inject.exe")); + game_builder.arg(sgt_dir.join("inject.exe")); amd_builder.arg("cmd.exe"); } @@ -109,10 +112,10 @@ impl Profile for OngekiProfile { ) .current_dir(&exe_dir) .arg("/C") - .arg(&exe_dir.join("inject.exe")) - .args([ - "-d", "-k", "mu3hook.dll", - "amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json" + .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( @@ -124,8 +127,9 @@ impl Profile for OngekiProfile { self.config_dir().join("inohara.cfg"), ) .current_dir(&exe_dir) + .args(["-d", "-k"]) + .arg(sgt_dir.join("mu3hook.dll")) .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(), diff --git a/src/components/App.vue b/src/components/App.vue index b8882c9..6e19c80 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -1,6 +1,8 @@