feat: initial support for segatools pkgs
This commit is contained in:
60
README.md
60
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).
|
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.
|
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
|
```sh
|
||||||
bun install
|
bun install
|
||||||
bun run tauri dev
|
|
||||||
bun run tauri build
|
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
|
```sh
|
||||||
startliner --start --game ongeki --profile <name>
|
startliner --start --game ongeki --profile <name>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Package format
|
To create a desktop shortcut: `Copy -> Paste Shortcut -> Properties`, and then append `--start --game ongeki --profile <name>` to `Target`.
|
||||||
|
|
||||||
|
## Package format
|
||||||
|
|
||||||
- [Package format requirements](https://rainy.patafour.zip/package/create/docs/)
|
- [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
|
├───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.
|
Arbitrary scripts are not supported by design and that will probably never change.
|
||||||
|
|
||||||
### Features
|
## See also
|
||||||
|
|
||||||
- Clean data modding
|
- [BlueSteel launcher (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)
|
||||||
- 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)
|
|
||||||
|
12
TODO.md
Normal file
12
TODO.md
Normal file
@ -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)
|
@ -45,11 +45,15 @@ pub async fn kill() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn install_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<InstallResult, String> {
|
pub async fn install_package(
|
||||||
|
state: State<'_, tokio::sync::Mutex<AppData>>,
|
||||||
|
key: PkgKey,
|
||||||
|
force: bool
|
||||||
|
) -> Result<InstallResult, String> {
|
||||||
log::debug!("invoke: install_package({})", key);
|
log::debug!("invoke: install_package({})", key);
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
appd.pkgs.install_package(&key, true, true)
|
appd.pkgs.install_package(&key, force, true)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::pkg::PkgKey;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct Segatools {
|
pub struct Segatools {
|
||||||
pub target: PathBuf,
|
pub target: PathBuf,
|
||||||
|
pub hook: Option<PkgKey>,
|
||||||
|
pub io: Option<PkgKey>,
|
||||||
pub amfs: PathBuf,
|
pub amfs: PathBuf,
|
||||||
pub option: PathBuf,
|
pub option: PathBuf,
|
||||||
pub appdata: PathBuf,
|
pub appdata: PathBuf,
|
||||||
@ -15,6 +18,8 @@ impl Default for Segatools {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Segatools {
|
Segatools {
|
||||||
target: PathBuf::default(),
|
target: PathBuf::default(),
|
||||||
|
hook: Some(PkgKey("segatools-mu3hook".to_owned())),
|
||||||
|
io: None,
|
||||||
amfs: PathBuf::default(),
|
amfs: PathBuf::default(),
|
||||||
option: PathBuf::default(),
|
option: PathBuf::default(),
|
||||||
appdata: PathBuf::from("appdata"),
|
appdata: PathBuf::from("appdata"),
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use crate::pkg::PkgKeyVersion;
|
use crate::pkg::PkgKeyVersion;
|
||||||
|
|
||||||
// manifest.json
|
// manifest.json
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct PackageManifest {
|
pub struct PackageManifest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version_number: String,
|
pub version_number: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub dependencies: BTreeSet<PkgKeyVersion>
|
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub installers: Vec<BTreeMap<String, serde_json::Value>>
|
||||||
}
|
}
|
@ -1,9 +1,5 @@
|
|||||||
pub fn segatools_base() -> String {
|
pub fn segatools_base() -> String {
|
||||||
"; mu3io is TBD
|
"[vfd]
|
||||||
[mu3io]
|
|
||||||
path=
|
|
||||||
|
|
||||||
[vfd]
|
|
||||||
; Enable VFD emulation. Disable to use a real VFD
|
; Enable VFD emulation. Disable to use a real VFD
|
||||||
; GP1232A02A FUTABA assembly.
|
; GP1232A02A FUTABA assembly.
|
||||||
enable=1
|
enable=1
|
||||||
|
@ -57,6 +57,14 @@ impl Segatools {
|
|||||||
.set("enable", "0");
|
.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);
|
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
||||||
|
|
||||||
if !opt_dir_out.exists() {
|
if !opt_dir_out.exists() {
|
||||||
|
@ -3,7 +3,7 @@ use derive_more::Display;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use crate::{model::{local, rainy}, util};
|
use crate::{model::{local::{self, PackageManifest}, rainy}, util};
|
||||||
|
|
||||||
// {namespace}-{name}
|
// {namespace}-{name}
|
||||||
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)]
|
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)]
|
||||||
@ -27,8 +27,10 @@ pub struct Package {
|
|||||||
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Kind {
|
pub enum Kind {
|
||||||
Unchecked,
|
Unchecked,
|
||||||
|
Unsupported,
|
||||||
#[default] Mod,
|
#[default] Mod,
|
||||||
Unsupported
|
Hook,
|
||||||
|
IO,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||||
@ -84,6 +86,7 @@ impl Package {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
|
let kind = Self::parse_kind(&mft);
|
||||||
let dependencies = Self::sanitize_deps(mft.dependencies);
|
let dependencies = Self::sanitize_deps(mft.dependencies);
|
||||||
|
|
||||||
Ok(Package {
|
Ok(Package {
|
||||||
@ -94,7 +97,7 @@ impl Package {
|
|||||||
loc: Some(Local {
|
loc: Some(Local {
|
||||||
version: mft.version_number,
|
version: mft.version_number,
|
||||||
path: dir.to_owned(),
|
path: dir.to_owned(),
|
||||||
kind: Kind::Mod,
|
kind,
|
||||||
dependencies
|
dependencies
|
||||||
}),
|
}),
|
||||||
rmt: None
|
rmt: None
|
||||||
@ -166,4 +169,25 @@ impl Package {
|
|||||||
}
|
}
|
||||||
res
|
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
|
||||||
|
}
|
||||||
}
|
}
|
@ -120,13 +120,14 @@ impl PackageStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn install_package(&mut self, key: &PkgKey, force: bool, install_deps: bool) -> Result<InstallResult> {
|
pub async fn install_package(&mut self, key: &PkgKey, force: bool, install_deps: bool) -> Result<InstallResult> {
|
||||||
log::debug!("Installing {}", key);
|
log::info!("installation request: {}/{}/{}", key, force, install_deps);
|
||||||
|
|
||||||
let pkg = self.store.get(key)
|
let pkg = self.store.get(key)
|
||||||
.ok_or_else(|| anyhow!("Attempted to install a nonexistent pkg"))?
|
.ok_or_else(|| anyhow!("Attempted to install a nonexistent pkg"))?
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
if pkg.loc.is_some() && !force {
|
if pkg.loc.is_some() && !force {
|
||||||
|
log::debug!("installation skipped");
|
||||||
return Ok(InstallResult::Ready);
|
return Ok(InstallResult::Ready);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +153,7 @@ impl PackageStore {
|
|||||||
|
|
||||||
if !zip_path.exists() {
|
if !zip_path.exists() {
|
||||||
self.dlh.download_zip(&zip_path, &pkg)?;
|
self.dlh.download_zip(&zip_path, &pkg)?;
|
||||||
log::debug!("Deferring {}", key);
|
log::debug!("deferring {}", key);
|
||||||
return Ok(InstallResult::Deferred);
|
return Ok(InstallResult::Deferred);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,13 +171,13 @@ impl PackageStore {
|
|||||||
pkg: key.to_owned()
|
pkg: key.to_owned()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
log::info!("Installed {}", key);
|
log::info!("installed {}", key);
|
||||||
|
|
||||||
Ok(InstallResult::Ready)
|
Ok(InstallResult::Ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_package(&mut self, key: &PkgKey, force: bool) -> Result<()> {
|
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)
|
let pkg = self.store.get_mut(key)
|
||||||
.ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?;
|
.ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?;
|
||||||
@ -191,7 +192,7 @@ impl PackageStore {
|
|||||||
self.app.emit("install-end", Payload {
|
self.app.emit("install-end", Payload {
|
||||||
pkg: key.to_owned()
|
pkg: key.to_owned()
|
||||||
})?;
|
})?;
|
||||||
log::info!("Deleted {}", key);
|
log::info!("deleted {}", key);
|
||||||
}
|
}
|
||||||
rv
|
rv
|
||||||
} else {
|
} else {
|
||||||
@ -216,7 +217,7 @@ impl PackageStore {
|
|||||||
if path.exists() {
|
if path.exists() {
|
||||||
tokio::fs::remove_dir_all(path)
|
tokio::fs::remove_dir_all(path)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
|
.map_err(|e| anyhow!("could not delete {}: {}", name, e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -237,6 +238,7 @@ impl PackageStore {
|
|||||||
// todo case sensitivity for linux
|
// todo case sensitivity for linux
|
||||||
Self::clean_up_dir(&path, "app").await?;
|
Self::clean_up_dir(&path, "app").await?;
|
||||||
Self::clean_up_dir(&path, "option").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, "icon.png", true).await?;
|
||||||
Self::clean_up_file(&path, "manifest.json", 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, "README.md", true).await?;
|
||||||
|
@ -3,7 +3,7 @@ use ongeki::OngekiProfile;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
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;
|
pub mod ongeki;
|
||||||
|
|
||||||
@ -72,14 +72,15 @@ impl AnyProfile {
|
|||||||
}
|
}
|
||||||
pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> {
|
pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Self::OngekiProfile(p) => {
|
Self::OngekiProfile(_p) => {
|
||||||
#[cfg(target_os = "windows")]
|
#[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;
|
let res = self.line_up_the_rest(pkg_hash).await;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
if let Some(info) = info {
|
if let Some(info) = info {
|
||||||
|
use crate::model::config::Display;
|
||||||
if res.is_ok() {
|
if res.is_ok() {
|
||||||
Display::wait_for_exit(_app, info);
|
Display::wait_for_exit(_app, info);
|
||||||
} else {
|
} else {
|
||||||
|
@ -88,10 +88,13 @@ impl Profile for OngekiProfile {
|
|||||||
|
|
||||||
let target_path = PathBuf::from(&self.sgt.target);
|
let target_path = PathBuf::from(&self.sgt.target);
|
||||||
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
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")]
|
#[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");
|
amd_builder = Command::new("cmd.exe");
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@ -99,7 +102,7 @@ impl Profile for OngekiProfile {
|
|||||||
game_builder = Command::new(&self.wine.runtime);
|
game_builder = Command::new(&self.wine.runtime);
|
||||||
amd_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");
|
amd_builder.arg("cmd.exe");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,10 +112,10 @@ impl Profile for OngekiProfile {
|
|||||||
)
|
)
|
||||||
.current_dir(&exe_dir)
|
.current_dir(&exe_dir)
|
||||||
.arg("/C")
|
.arg("/C")
|
||||||
.arg(&exe_dir.join("inject.exe"))
|
.arg(&sgt_dir.join("inject.exe"))
|
||||||
.args([
|
.args(["-d", "-k"])
|
||||||
"-d", "-k", "mu3hook.dll",
|
.arg(sgt_dir.join("mu3hook.dll"))
|
||||||
"amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
.args(["amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
||||||
]);
|
]);
|
||||||
game_builder
|
game_builder
|
||||||
.env(
|
.env(
|
||||||
@ -124,8 +127,9 @@ impl Profile for OngekiProfile {
|
|||||||
self.config_dir().join("inohara.cfg"),
|
self.config_dir().join("inohara.cfg"),
|
||||||
)
|
)
|
||||||
.current_dir(&exe_dir)
|
.current_dir(&exe_dir)
|
||||||
|
.args(["-d", "-k"])
|
||||||
|
.arg(sgt_dir.join("mu3hook.dll"))
|
||||||
.args([
|
.args([
|
||||||
"-d", "-k", "mu3hook.dll",
|
|
||||||
"mu3.exe", "-monitor 1",
|
"mu3.exe", "-monitor 1",
|
||||||
"-screen-width", &self.display.rez.0.to_string(),
|
"-screen-width", &self.display.rez.0.to_string(),
|
||||||
"-screen-height", &self.display.rez.1.to_string(),
|
"-screen-height", &self.display.rez.1.to_string(),
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { Ref, computed, onMounted, ref } from 'vue';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
|
import InputIcon from 'primevue/inputicon';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
import Tab from 'primevue/tab';
|
import Tab from 'primevue/tab';
|
||||||
import TabList from 'primevue/tablist';
|
import TabList from 'primevue/tablist';
|
||||||
import TabPanel from 'primevue/tabpanel';
|
import TabPanel from 'primevue/tabpanel';
|
||||||
@ -21,7 +23,9 @@ const general = useGeneralStore();
|
|||||||
|
|
||||||
pkg.setupListeners();
|
pkg.setupListeners();
|
||||||
|
|
||||||
const currentTab = ref('3');
|
const currentTab: Ref<string | number> = ref(3);
|
||||||
|
const searchPkg = ref('');
|
||||||
|
const searchCfg = ref('');
|
||||||
|
|
||||||
const isProfileDisabled = computed(() => prf.current === null);
|
const isProfileDisabled = computed(() => prf.current === null);
|
||||||
|
|
||||||
@ -30,47 +34,81 @@ onMounted(async () => {
|
|||||||
general.dirs = d as Dirs;
|
general.dirs = d as Dirs;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fetch_promise = pkg.fetch();
|
||||||
|
|
||||||
await Promise.all([prf.reloadList(), prf.reload()]);
|
await Promise.all([prf.reloadList(), prf.reload()]);
|
||||||
|
|
||||||
if (prf.current !== null) {
|
if (prf.current !== null) {
|
||||||
await pkg.reloadAll();
|
await pkg.reloadAll();
|
||||||
currentTab.value = '0';
|
currentTab.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetch_promise.then(async () => {
|
||||||
|
await invoke('install_package', {
|
||||||
|
key: 'segatools-mu3hook',
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<Tabs lazy :value="currentTab" class="h-screen">
|
<Tabs
|
||||||
|
lazy
|
||||||
|
:value="currentTab"
|
||||||
|
v-on:update:value="(value) => (currentTab = value)"
|
||||||
|
class="h-screen"
|
||||||
|
>
|
||||||
<div class="fixed w-full flex z-100">
|
<div class="fixed w-full flex z-100">
|
||||||
<TabList class="grow">
|
<TabList class="grow">
|
||||||
<Tab :disabled="isProfileDisabled" value="0"
|
<Tab :disabled="isProfileDisabled" :value="0"
|
||||||
><div class="pi pi-list-check"></div
|
><div class="pi pi-list-check"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<Tab :disabled="isProfileDisabled" value="1"
|
<Tab :disabled="isProfileDisabled" :value="1"
|
||||||
><div class="pi pi-download"></div
|
><div class="pi pi-download"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<Tab :disabled="isProfileDisabled" value="2"
|
<Tab :disabled="isProfileDisabled" :value="2"
|
||||||
><div class="pi pi-cog"></div
|
><div class="pi pi-cog"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<Tab value="3"
|
<Tab :value="3"
|
||||||
><div class="pi pi-question-circle"></div
|
><div class="pi pi-question-circle"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
|
<div class="flex" v-if="currentTab !== 3">
|
||||||
|
<InputIcon class="self-center mr-2">
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText
|
||||||
|
v-if="currentTab === 2"
|
||||||
|
class="self-center"
|
||||||
|
size="small"
|
||||||
|
placeholder="Search"
|
||||||
|
v-model="searchCfg"
|
||||||
|
/>
|
||||||
|
<InputText
|
||||||
|
v-else
|
||||||
|
class="self-center"
|
||||||
|
size="small"
|
||||||
|
placeholder="Search"
|
||||||
|
v-model="searchPkg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="grow"></div>
|
||||||
<StartButton />
|
<StartButton />
|
||||||
</TabList>
|
</TabList>
|
||||||
</div>
|
</div>
|
||||||
<TabPanels class="w-full grow mt-[3rem]">
|
<TabPanels class="w-full grow mt-[3rem]">
|
||||||
<TabPanel value="0">
|
<TabPanel :value="0">
|
||||||
<ModList />
|
<ModList :search="searchPkg" />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value="1">
|
<TabPanel :value="1">
|
||||||
<ModStore />
|
<ModStore :search="searchPkg" />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value="2">
|
<TabPanel :value="2">
|
||||||
<OptionList />
|
<OptionList />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value="3">
|
<TabPanel :value="3">
|
||||||
<strong>UNDER CONSTRUCTION</strong><br />Some features are
|
<strong>UNDER CONSTRUCTION</strong><br />Some features are
|
||||||
missing.<br />Existing features are expected to break
|
missing.<br />Existing features are expected to break
|
||||||
sometimes.
|
sometimes.
|
||||||
|
@ -14,7 +14,10 @@ const install = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invoke('install_package', { key: pkgKey(props.pkg) });
|
await invoke('install_package', {
|
||||||
|
key: pkgKey(props.pkg),
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
if (props.pkg !== undefined) {
|
if (props.pkg !== undefined) {
|
||||||
|
@ -1,29 +1,44 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
import Fieldset from 'primevue/fieldset';
|
import Fieldset from 'primevue/fieldset';
|
||||||
import ModListEntry from './ModListEntry.vue';
|
import ModListEntry from './ModListEntry.vue';
|
||||||
import { usePkgStore, usePrfStore } from '../stores';
|
import { usePkgStore } from '../stores';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
search: String,
|
||||||
|
});
|
||||||
|
|
||||||
const pkg = usePkgStore();
|
const pkg = usePkgStore();
|
||||||
const prf = usePrfStore();
|
const empty = ref(true);
|
||||||
|
|
||||||
const group = () => {
|
const group = () => {
|
||||||
const a = Object.assign(
|
const a = Object.assign(
|
||||||
{},
|
{},
|
||||||
Object.groupBy(
|
Object.groupBy(
|
||||||
pkg.allLocal
|
pkg.allLocal
|
||||||
|
.filter(
|
||||||
|
(p) =>
|
||||||
|
props.search === undefined ||
|
||||||
|
p.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(props.search.toLowerCase()) ||
|
||||||
|
p.namespace
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(props.search.toLowerCase())
|
||||||
|
)
|
||||||
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
|
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
|
||||||
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
|
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
|
||||||
({ namespace }) => namespace
|
({ namespace }) => namespace
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
empty.value = Object.keys(a).length === 0;
|
||||||
return a;
|
return a;
|
||||||
};
|
};
|
||||||
|
|
||||||
prf.reload();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Fieldset v-for="(namespace, key) in group()" :legend="key.toString()">
|
<Fieldset v-for="(namespace, key) in group()" :legend="key.toString()">
|
||||||
<ModListEntry v-for="p in namespace" :pkg="p" />
|
<ModListEntry v-for="p in namespace" :pkg="p" />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
|
<div v-if="empty" class="text-3xl">∅</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,24 +1,37 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue';
|
import { ref } from 'vue';
|
||||||
import ModStoreEntry from './ModStoreEntry.vue';
|
import ModStoreEntry from './ModStoreEntry.vue';
|
||||||
import { usePkgStore } from '../stores';
|
import { usePkgStore } from '../stores';
|
||||||
|
|
||||||
const pkgs = usePkgStore();
|
const pkgs = usePkgStore();
|
||||||
|
const empty = ref(true);
|
||||||
|
|
||||||
onMounted(() => {
|
const props = defineProps({
|
||||||
pkgs.fetch();
|
search: String,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const list = () => {
|
||||||
|
const res = pkgs.allRemote
|
||||||
|
.filter(
|
||||||
|
(p) =>
|
||||||
|
props.search === undefined ||
|
||||||
|
p.name.toLowerCase().includes(props.search.toLowerCase()) ||
|
||||||
|
p.namespace
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(props.search.toLowerCase()) ||
|
||||||
|
p.description.toLowerCase().includes(props.search.toLowerCase())
|
||||||
|
)
|
||||||
|
.sort((p1, p2) => p1.name.localeCompare(p2.name));
|
||||||
|
empty.value = res.length === 0;
|
||||||
|
return res;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div v-for="p in list()" class="flex flex-row">
|
||||||
v-for="p in pkgs.allRemote.sort((p1, p2) =>
|
|
||||||
p1.name.localeCompare(p2.name)
|
|
||||||
)"
|
|
||||||
class="flex flex-row"
|
|
||||||
>
|
|
||||||
<ModStoreEntry :pkg="p" />
|
<ModStoreEntry :pkg="p" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="empty" class="text-3xl">∅</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -12,8 +12,10 @@ import FilePicker from './FilePicker.vue';
|
|||||||
import OptionCategory from './OptionCategory.vue';
|
import OptionCategory from './OptionCategory.vue';
|
||||||
import OptionRow from './OptionRow.vue';
|
import OptionRow from './OptionRow.vue';
|
||||||
import { invoke } from '../invoke';
|
import { invoke } from '../invoke';
|
||||||
import { usePrfStore } from '../stores';
|
import { usePkgStore, usePrfStore } from '../stores';
|
||||||
|
import { pkgKey } from '../util';
|
||||||
|
|
||||||
|
const pkg = usePkgStore();
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
|
|
||||||
const aimeCode = ref('');
|
const aimeCode = ref('');
|
||||||
@ -26,13 +28,6 @@ const displayList: Ref<{ title: string; value: string }[]> = ref([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// const hookList: Ref<{ title: string; value: string }[]> = ref([
|
|
||||||
// {
|
|
||||||
// title: 'segatools-mu3hook',
|
|
||||||
// value: 'segatools-mu3hook',
|
|
||||||
// },
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
invoke('list_platform_capabilities')
|
invoke('list_platform_capabilities')
|
||||||
.then(async (v: unknown) => {
|
.then(async (v: unknown) => {
|
||||||
if (Array.isArray(v)) {
|
if (Array.isArray(v)) {
|
||||||
@ -86,14 +81,18 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
:callback="(value: string) => (prf.current!.sgt.target = value)"
|
:callback="(value: string) => (prf.current!.sgt.target = value)"
|
||||||
></FilePicker>
|
></FilePicker>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<!-- <OptionRow title="mu3hook">
|
<OptionRow title="mu3hook">
|
||||||
<Select
|
<Select
|
||||||
model-value="segatools-mu3hook"
|
v-model="prf.current!.sgt.hook"
|
||||||
:options="hookList"
|
:options="
|
||||||
|
pkg.hooks.map((p) => {
|
||||||
|
return { title: pkgKey(p), value: pkgKey(p) };
|
||||||
|
})
|
||||||
|
"
|
||||||
option-label="title"
|
option-label="title"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
></Select>
|
></Select>
|
||||||
</OptionRow> -->
|
</OptionRow>
|
||||||
<OptionRow title="amfs">
|
<OptionRow title="amfs">
|
||||||
<FilePicker
|
<FilePicker
|
||||||
:directory="true"
|
:directory="true"
|
||||||
@ -119,6 +118,20 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
"
|
"
|
||||||
></FilePicker>
|
></FilePicker>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
|
<OptionRow title="mu3io">
|
||||||
|
<Select
|
||||||
|
v-model="prf.current!.sgt.io"
|
||||||
|
placeholder="segatools built-in"
|
||||||
|
:options="[
|
||||||
|
{ title: 'segatools built-in', value: null },
|
||||||
|
...pkg.ios.map((p) => {
|
||||||
|
return { title: pkgKey(p), value: pkgKey(p) };
|
||||||
|
}),
|
||||||
|
]"
|
||||||
|
option-label="title"
|
||||||
|
option-value="value"
|
||||||
|
></Select>
|
||||||
|
</OptionRow>
|
||||||
</OptionCategory>
|
</OptionCategory>
|
||||||
<OptionCategory title="Display">
|
<OptionCategory title="Display">
|
||||||
<OptionRow
|
<OptionRow
|
||||||
|
@ -31,6 +31,9 @@ const disabledTooltip = computed(() => {
|
|||||||
if (prf.current?.sgt.amfs.length === 0) {
|
if (prf.current?.sgt.amfs.length === 0) {
|
||||||
return 'The amfs path must be specified';
|
return 'The amfs path must be specified';
|
||||||
}
|
}
|
||||||
|
if (prf.current?.sgt.hook === null || prf.current?.sgt.hook === undefined) {
|
||||||
|
return 'A segatools hook package is necessary';
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,7 +14,10 @@ const install = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invoke('install_package', { key: pkgKey(props.pkg) });
|
await invoke('install_package', {
|
||||||
|
key: pkgKey(props.pkg),
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (props.pkg !== undefined) {
|
if (props.pkg !== undefined) {
|
||||||
props.pkg.js.busy = false;
|
props.pkg.js.busy = false;
|
||||||
|
@ -46,8 +46,13 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
fromName: (state) => (namespace: string, name: string) =>
|
fromName: (state) => (namespace: string, name: string) =>
|
||||||
state.pkg[`${namespace}-${name}`] ?? null,
|
state.pkg[`${namespace}-${name}`] ?? null,
|
||||||
all: (state) => Object.values(state),
|
all: (state) => Object.values(state),
|
||||||
allLocal: (state) => Object.values(state.pkg).filter((p) => p.loc),
|
allLocal: (state) =>
|
||||||
|
Object.values(state.pkg).filter((p) => p.loc?.kind === 'Mod'),
|
||||||
allRemote: (state) => Object.values(state.pkg).filter((p) => p.rmt),
|
allRemote: (state) => Object.values(state.pkg).filter((p) => p.rmt),
|
||||||
|
hooks: (state) =>
|
||||||
|
Object.values(state.pkg).filter((p) => p.loc?.kind === 'Hook'),
|
||||||
|
ios: (state) =>
|
||||||
|
Object.values(state.pkg).filter((p) => p.loc?.kind === 'IO'),
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setupListeners() {
|
setupListeners() {
|
||||||
|
@ -7,6 +7,7 @@ export interface Package {
|
|||||||
version: string;
|
version: string;
|
||||||
path: string;
|
path: string;
|
||||||
dependencies: string[];
|
dependencies: string[];
|
||||||
|
kind: 'Unchecked' | 'Unsupported' | 'Mod' | 'Hook' | 'IO';
|
||||||
} | null;
|
} | null;
|
||||||
rmt: {
|
rmt: {
|
||||||
version: string;
|
version: string;
|
||||||
@ -28,6 +29,8 @@ export interface ProfileMeta {
|
|||||||
|
|
||||||
export interface SegatoolsConfig {
|
export interface SegatoolsConfig {
|
||||||
target: string;
|
target: string;
|
||||||
|
hook: string | null;
|
||||||
|
io: string | null;
|
||||||
amfs: string;
|
amfs: string;
|
||||||
option: string;
|
option: string;
|
||||||
appdata: string;
|
appdata: string;
|
||||||
|
Reference in New Issue
Block a user