feat: chuniio

This commit is contained in:
2025-04-15 13:12:12 +00:00
parent ac18c34895
commit b82fcc942f
20 changed files with 150 additions and 61 deletions

View File

@ -1,3 +1,12 @@
## 0.8.0
- Added support for ChuniIO
## 0.7.1
- Hotfixed amdaemon crashing at launch
- Greyed out packages currently incompatible with STARTLINER
## 0.7.0 ## 0.7.0
- Hopefully fixed issues with the download button - Hopefully fixed issues with the download button

View File

@ -11,9 +11,6 @@ Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome
- Display configuration with automatic rollback - Display configuration with automatic rollback
- Support for multiple configurations pointing at the same data - Support for multiple configurations pointing at the same data
![Mod list](res/list.png)
![Configuration](res/cfg.png)
## Usage ## Usage
Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself: Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:
@ -35,25 +32,15 @@ To create a desktop shortcut: `Copy -> Paste Shortcut -> Properties`, and then a
## Package format ## Package format
- [Package format requirements](https://rainy.patafour.zip/package/create/docs/) Refer to [the wiki](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Package-format).
- 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
│ └───BepInEx
│ └───*
├───option
│ └───Axyz
│ └───*
├───icon.png
├───README.md
└───manifest.json
```
More file overrides may be supported in the future.
Arbitrary scripts are not supported by design and that will probably never change.
## See also ## See also
- [BlueSteel launcher (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou) [BlueSteel launcher (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)
## Screenshots
![Package list](res/list.png)
![Package store](res/store.png)
![Configuration](res/cfg.png)
![Keyboard](res/cfg2.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 43 KiB

BIN
res/cfg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 70 KiB

BIN
res/store.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
rust/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
rust/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -13,6 +13,14 @@ pub enum Aime {
Other(PkgKey), Other(PkgKey),
} }
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
#[serde(rename_all = "snake_case")]
pub enum IOSelection {
Hardware,
#[default] SegatoolsBuiltIn,
Custom(PkgKey)
}
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)] #[serde(default)]
pub struct AMNet { pub struct AMNet {
@ -32,7 +40,9 @@ impl Default for AMNet {
pub struct Segatools { pub struct Segatools {
pub target: PathBuf, pub target: PathBuf,
pub hook: Option<PkgKey>, pub hook: Option<PkgKey>,
#[serde(skip_serializing_if = "Option::is_none")]
pub io: Option<PkgKey>, pub io: Option<PkgKey>,
pub io2: IOSelection,
pub aime: Aime, pub aime: Aime,
pub amfs: PathBuf, pub amfs: PathBuf,
pub option: PathBuf, pub option: PathBuf,
@ -51,6 +61,7 @@ impl Segatools {
Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned())) Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned()))
}, },
io: None, io: None,
io2: IOSelection::SegatoolsBuiltIn,
amfs: PathBuf::default(), amfs: PathBuf::default(),
option: PathBuf::default(), option: PathBuf::default(),
appdata: PathBuf::from("appdata"), appdata: PathBuf::from("appdata"),

View File

@ -95,7 +95,19 @@ impl Keyboard {
.set("mouse", if kb.use_mouse { "1" } else { "0" }); .set("mouse", if kb.use_mouse { "1" } else { "0" });
} else { } else {
ini.with_section(Some("io4")) ini.with_section(Some("io4"))
.set("enable", "0"); .set("test", "0")
.set("service", "0")
.set("coin", "0")
.set("left1", "0")
.set("left2", "0")
.set("left3", "0")
.set("right1", "0")
.set("right2", "0")
.set("right3", "0")
.set("leftSide", "0")
.set("rightSide", "0")
.set("leftMenu", "0")
.set("rightMenu", "0");
} }
} }
Keyboard::Chunithm(kb) => { Keyboard::Chunithm(kb) => {
@ -109,14 +121,21 @@ impl Keyboard {
ini.with_section(Some("io3")) ini.with_section(Some("io3"))
.set("test", kb.test.to_string()) .set("test", kb.test.to_string())
.set("service", kb.svc.to_string()) .set("service", kb.svc.to_string())
.set("coin", kb.coin.to_string()) .set("coin", kb.coin.to_string());
.set("ir", "0");
} else { } else {
ini.with_section(Some("io4")) for (i, _) in kb.cell.iter().enumerate() {
.set("enable", "0"); ini.with_section(Some("slider")).set(format!("cell{}", i + 1), "0");
ini.with_section(Some("slider")) }
.set("enable", "0"); for (i, _) in kb.ir.iter().enumerate() {
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), "0");
}
ini.with_section(Some("io3"))
.set("test", "0")
.set("service", "0")
.set("coin", "0");
} }
ini.with_section(Some("io3"))
.set("ir", "0");
} }
} }

View File

@ -1,7 +1,7 @@
use std::path::{PathBuf, Path}; use std::path::{PathBuf, Path};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ini::Ini; use ini::Ini;
use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, ConfigHookAuth, Game}, profile::{Aime, Segatools}}, profiles::ProfilePaths, util::{self, PathStr}}; use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, ConfigHookAuth, Game}, profile::{Aime, IOSelection, Segatools}}, profiles::ProfilePaths, util::{self, PathStr}};
use crate::pkg_store::PackageStore; use crate::pkg_store::PackageStore;
impl Segatools { impl Segatools {
@ -21,8 +21,8 @@ impl Segatools {
if let Some(key) = &self.hook { if let Some(key) = &self.hook {
remove_if_nonpresent!(self.hook, key, None, store); remove_if_nonpresent!(self.hook, key, None, store);
} }
if let Some(key) = &self.io { if let IOSelection::Custom(key) = &self.io2 {
remove_if_nonpresent!(self.io, key, None, store); remove_if_nonpresent!(self.io2, key, IOSelection::default(), store);
} }
match &self.aime { match &self.aime {
Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store), Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
@ -141,7 +141,7 @@ impl Segatools {
} }
if game == Game::Ongeki { if game == Game::Ongeki {
if let Some(io) = &self.io { if let IOSelection::Custom(io) = &self.io2 {
ini_out.with_section(Some("mu3io")) ini_out.with_section(Some("mu3io"))
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?); .set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
} else { } else {
@ -149,6 +149,42 @@ impl Segatools {
.set("path", ""); .set("path", "");
} }
} }
match game {
Game::Ongeki => {
match &self.io2 {
IOSelection::Custom(io) => {
ini_out.with_section(Some("mu3io"))
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
}
IOSelection::SegatoolsBuiltIn => {
ini_out.with_section(Some("mu3io"))
.set("path", "");
}
IOSelection::Hardware => {
ini_out.with_section(Some("io4"))
.set("enable", "0");
}
}
},
Game::Chunithm => {
match &self.io2 {
IOSelection::Custom(io) => {
ini_out.with_section(Some("chuniio"))
.set("path32", util::pkg_dir().join(io.to_string()).join("segatools").join("chuniio32.dll").stringify()?)
.set("path64", util::pkg_dir().join(io.to_string()).join("segatools").join("chuniio64.dll").stringify()?);
}
IOSelection::SegatoolsBuiltIn => {
ini_out.with_section(Some("chuniio"))
.set("path32", "")
.set("path64", "");
}
IOSelection::Hardware => {
ini_out.with_section(Some("io4"))
.set("enable", "0");
}
}
}
};
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out); log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);

View File

@ -239,20 +239,14 @@ impl Package {
if id == "rainycolor" { if id == "rainycolor" {
flags |= Feature::Mod; flags |= Feature::Mod;
} else if id == "segatools" { } else if id == "segatools" {
// Multiple features in the same dll (yubideck etc.) should be supported at some point
if let Some(serde_json::Value::String(module)) = installer.get("module") { if let Some(serde_json::Value::String(module)) = installer.get("module") {
if module == "mu3hook" { flags |= Self::parse_segatools_module(&module);
flags |= Feature::Mu3Hook; }
} else if module == "chusanhook" { if let Some(serde_json::Value::Array(arr)) = installer.get("module") {
flags |= Feature::ChusanHook; for elem in arr {
} else if module == "amnet" { if let serde_json::Value::String(module) = elem {
flags |= Feature::AMNet | Feature::Aime; flags |= Self::parse_segatools_module(module);
} else if module == "aimeio" { }
flags |= Feature::Aime;
} else if module == "mu3io" {
flags |= Feature::Mu3IO;
} else if module == "chuniio" {
flags |= Feature::ChuniIO;
} }
} }
} else if id == "native_mod" { } else if id == "native_mod" {
@ -280,4 +274,16 @@ impl Package {
Status::OK(flags, DLLs { game: game_dll, amd: amd_dll }) Status::OK(flags, DLLs { game: game_dll, amd: amd_dll })
} }
} }
fn parse_segatools_module(module: &str) -> BitFlags<Feature, u16> {
match module {
"mu3hook" => make_bitflags!(Feature::Mu3Hook),
"chusanhook" => make_bitflags!(Feature::ChusanHook),
"amnet" => make_bitflags!(Feature::{AMNet | Aime}),
"aimeio" => make_bitflags!(Feature::Aime),
"mu3io" => make_bitflags!(Feature::Mu3IO),
"chuniio" => make_bitflags!(Feature::ChuniIO),
_ => BitFlags::default()
}
}
} }

View File

@ -1,6 +1,6 @@
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload}; pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}}; use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util}; use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
use tauri::Emitter; use tauri::Emitter;
use std::process::Stdio; use std::process::Stdio;
use crate::model::profile::BepInEx; use crate::model::profile::BepInEx;
@ -59,8 +59,14 @@ impl Profile {
log::debug!("{:?}", data); log::debug!("{:?}", data);
// Backwards compat // Backwards compat
if game == Game::Ongeki && data.keyboard.is_none() { if game == Game::Ongeki {
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default())); if data.keyboard.is_none() {
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default()));
}
if let Some(io) = data.sgt.io {
data.sgt.io2 = IOSelection::Custom(io);
data.sgt.io = None;
}
} }
if game == Game::Chunithm { if game == Game::Chunithm {
if data.keyboard.is_none() { if data.keyboard.is_none() {
@ -112,7 +118,7 @@ impl Profile {
if let Some(hook) = &self.data.sgt.hook { if let Some(hook) = &self.data.sgt.hook {
res.push(hook.clone()); res.push(hook.clone());
} }
if let Some(io) = &self.data.sgt.io { if let IOSelection::Custom(io) = &self.data.sgt.io2 {
res.push(io.clone()); res.push(io.clone());
} }
if let Aime::AMNet(aime) = &self.data.sgt.aime { if let Aime::AMNet(aime) = &self.data.sgt.aime {

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "STARTLINER", "productName": "STARTLINER",
"version": "0.7.0", "version": "0.8.0",
"identifier": "zip.patafour.startliner", "identifier": "zip.patafour.startliner",
"build": { "build": {
"beforeDevCommand": "bun run dev", "beforeDevCommand": "bun run dev",
@ -66,7 +66,7 @@
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
"icon": ["icons/slow.png", "icons/slow.ico"], "icon": ["icons/icon.png", "icons/icon.ico"],
"createUpdaterArtifacts": true "createUpdaterArtifacts": true
} }
} }

View File

@ -11,7 +11,10 @@ const prf = usePrfStore();
<template> <template>
<OptionCategory title="Keyboard"> <OptionCategory title="Keyboard">
<OptionRow title="Enable"> <OptionRow
title="Enable"
tooltip="Only applicable if the IO module is set to segatools built-in (keyboard) or a compatible third-party module (like mu3io.NET)"
>
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" /> <ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" />
</OptionRow> </OptionRow>
<OptionRow <OptionRow
@ -30,6 +33,7 @@ const prf = usePrfStore();
/> />
</OptionRow> </OptionRow>
<div <div
v-if="prf.current!.data.keyboard!.data.enabled"
:style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`" :style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`"
> >
<div <div

View File

@ -126,17 +126,28 @@ const checkSegatoolsIni = async (target: string) => {
</OptionRow> </OptionRow>
<OptionRow <OptionRow
:title="names.io" :title="names.io"
v-if="prf.current?.meta.game === 'ongeki'"
tooltip="IO plugins can be downloaded from the package store." tooltip="IO plugins can be downloaded from the package store."
> >
<Select <Select
v-model="prf.current!.data.sgt.io" v-model="prf.current!.data.sgt.io2"
placeholder="segatools built-in"
:options="[ :options="[
{ title: 'segatools built-in', value: null }, { title: 'native io4', value: 'hardware' },
...pkgs.byFeature(Feature.Mu3IO).map((p) => { {
return { title: pkgKey(p), value: pkgKey(p) }; title: 'segatools built-in (keyboard)',
}), value: 'segatools_built_in',
},
...pkgs
.byFeature(
prf.current?.meta.game === 'ongeki'
? Feature.Mu3IO
: Feature.ChuniIO
)
.map((p) => {
return {
title: pkgKey(p),
value: { custom: pkgKey(p) },
};
}),
]" ]"
option-label="title" option-label="title"
option-value="value" option-value="value"

View File

@ -66,7 +66,7 @@ export interface ProfileData {
export interface SegatoolsConfig { export interface SegatoolsConfig {
target: string; target: string;
hook: string | null; hook: string | null;
io: string | null; io2: 'segatools_built_in' | 'hardware' | { custom: string };
amfs: string; amfs: string;
option: string; option: string;
appdata: string; appdata: string;