feat: chuniio
@ -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
|
||||||
|
31
README.md
@ -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
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
BIN
res/cfg.png
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 43 KiB |
BIN
res/cfg2.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
res/list.png
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 70 KiB |
BIN
res/store.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
rust/icons/icon.ico
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
rust/icons/icon.png
Normal file
After Width: | Height: | Size: 516 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 23 KiB |
@ -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"),
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
|