From 9ea66dbeabc6e9792d91fecc984a9654996d1c10 Mon Sep 17 00:00:00 2001 From: akanyan Date: Thu, 10 Apr 2025 13:32:49 +0000 Subject: [PATCH] feat: segatools.ini loading --- rust/src/cmd.rs | 31 +++- rust/src/lib.rs | 6 +- rust/src/model/misc.rs | 4 +- rust/src/model/profile.rs | 72 +++++++- rust/src/model/segatools_base.rs | 158 +---------------- rust/src/modules/keyboard.rs | 120 +++++++++++++ rust/src/modules/mod.rs | 1 + rust/src/modules/network.rs | 26 +++ rust/src/modules/segatools.rs | 8 + rust/src/profiles/mod.rs | 30 +++- rust/tauri.conf.json | 2 +- src/components/App.vue | 20 +++ src/components/KeyboardKey.vue | 245 +++++++++++++++++++++++++++ src/components/OptionCategory.vue | 2 + src/components/OptionList.vue | 2 + src/components/StartButton.vue | 47 +---- src/components/options/Keyboard.vue | 177 +++++++++++++++++++ src/components/options/Misc.vue | 1 + src/components/options/Segatools.vue | 23 ++- src/stores.ts | 2 +- src/types.ts | 37 ++++ src/util.ts | 4 + 22 files changed, 804 insertions(+), 214 deletions(-) create mode 100644 rust/src/modules/keyboard.rs create mode 100644 src/components/KeyboardKey.vue create mode 100644 src/components/options/Keyboard.vue diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index 35f299b..a6a4b36 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -1,5 +1,7 @@ +use ini::Ini; use log; use std::collections::HashMap; +use std::path::PathBuf; use tokio::sync::Mutex; use tokio::fs; use tauri::{AppHandle, Manager, State}; @@ -319,7 +321,7 @@ pub async fn get_current_profile(state: State<'_, Mutex>) -> Result>, data: ProfileData) -> Result<(), String> { - log::debug!("invoke: sync_current_profile"); + log::debug!("invoke: sync_current_profile {:?}", data); let mut appd = state.lock().await; if let Some(p) = &mut appd.profile { @@ -345,6 +347,27 @@ pub async fn save_current_profile(state: State<'_, Mutex>) -> Result<() } } +#[tauri::command] +pub async fn load_segatools_ini(state: State<'_, Mutex>, path: PathBuf) -> Result<(), String> { + log::debug!("invoke: load_segatools_ini({:?})", path); + + let mut appd = state.lock().await; + if let Some(p) = &mut appd.profile { + let str = std::fs::read_to_string(path).map_err(|e| e.to_string())?; + // Stupid path escape hack for the ini reader + let str = str.replace("\\", "\\\\").replace("\\\\\\\\", "\\\\"); + let ini = Ini::load_from_str(&str).map_err(|e| e.to_string())?; + p.data.sgt.load_from_ini(&ini); + p.data.network.load_from_ini(&ini).map_err(|e| e.to_string())?; + if let Some(kb) = &mut p.data.keyboard { + kb.load_from_ini(&ini).map_err(|e| e.to_string())?; + } + p.save().map_err(|e| e.to_string())?; + } + + Ok(()) +} + #[tauri::command] pub async fn list_platform_capabilities() -> Result, ()> { log::debug!("invoke: list_platform_capabilities"); @@ -413,4 +436,10 @@ pub async fn list_directories() -> Result { log::debug!("invoke: list_directores"); Ok(util::all_dirs().clone()) +} + +// Tauri fs api is useless +#[tauri::command] +pub async fn file_exists(path: String) -> Result { + Ok(std::fs::exists(path).unwrap_or(false)) } \ No newline at end of file diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 6320d92..304f7a5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -71,8 +71,8 @@ pub async fn run(_args: Vec) { } else { tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into())) .title("STARTLINER") - .inner_size(760f64, 480f64) - .min_inner_size(760f64, 480f64) + .inner_size(900f64, 480f64) + .min_inner_size(900f64, 480f64) .build()?; start_immediately = false; } @@ -199,6 +199,7 @@ pub async fn run(_args: Vec) { cmd::get_current_profile, cmd::sync_current_profile, cmd::save_current_profile, + cmd::load_segatools_ini, cmd::get_global_config, cmd::set_global_config, @@ -206,6 +207,7 @@ pub async fn run(_args: Vec) { cmd::list_displays, cmd::list_platform_capabilities, cmd::list_directories, + cmd::file_exists, ]) .build(tauri::generate_context!()) .expect("error while building tauri application"); diff --git a/rust/src/model/misc.rs b/rust/src/model/misc.rs index 25eb101..ebbedfa 100644 --- a/rust/src/model/misc.rs +++ b/rust/src/model/misc.rs @@ -65,8 +65,8 @@ impl Game { pub fn has_module(&self, module: ProfileModule) -> bool { match self { - Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini}), - Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Network}), + Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini | Keyboard}), + Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Network | Keyboard}), }.contains(module) } } diff --git a/rust/src/model/profile.rs b/rust/src/model/profile.rs index cd7dfa4..fed1743 100644 --- a/rust/src/model/profile.rs +++ b/rust/src/model/profile.rs @@ -162,6 +162,75 @@ pub struct Mu3Ini { pub blacklist: Option<(i32, i32)>, } +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct OngekiKeyboard { + pub use_mouse: bool, + pub coin: i32, + pub svc: i32, + pub test: i32, + pub lmenu: i32, + pub rmenu: i32, + pub l1: i32, + pub l2: i32, + pub l3: i32, + pub r1: i32, + pub r2: i32, + pub r3: i32, + pub lwad: i32, + pub rwad: i32, +} + +impl Default for OngekiKeyboard { + fn default() -> Self { + Self { + use_mouse: true, + test: 0x70, + svc: 0x71, + coin: 0x72, + lmenu: 0x55, + rmenu: 0x4F, + lwad: 0x01, + rwad: 0x02, + l1: 0x41, + l2: 0x53, + l3: 0x44, + r1: 0x4A, + r2: 0x4B, + r3: 0x4C + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct ChunithmKeyboard { + pub split_ir: bool, + pub coin: i32, + pub svc: i32, + pub test: i32, + pub cell: [i32; 32], + pub ir: [i32; 6], +} + +impl Default for ChunithmKeyboard { + fn default() -> Self { + Self { + split_ir: false, + test: 0x70, + svc: 0x71, + coin: 0x72, + cell: Default::default(), + ir: Default::default(), + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(tag = "game", content = "data")] +pub enum Keyboard { + Ongeki(OngekiKeyboard), + Chunithm(ChunithmKeyboard), +} + #[bitflags] #[repr(u8)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -170,5 +239,6 @@ pub enum ProfileModule { Network, Display, BepInEx, - Mu3Ini + Mu3Ini, + Keyboard, } \ No newline at end of file diff --git a/rust/src/model/segatools_base.rs b/rust/src/model/segatools_base.rs index fca27ea..e3d0c96 100644 --- a/rust/src/model/segatools_base.rs +++ b/rust/src/model/segatools_base.rs @@ -38,49 +38,7 @@ cabLedOutputSerial=0 ; Output slider LED data to the named pipe controllerLedOutputPipe=1 ; Output slider LED data to the serial port -controllerLedOutputSerial=0 - -[io4] -; Test button virtual-key code. Default is the F1 key. -test=0x70 -; Service button virtual-key code. Default is the F2 key. -service=0x71 -; Keyboard button to increment coin counter. Default is the F3 key. -coin=0x72 - -; Set \"1\" to enable mouse lever emulation, \"0\" to use XInput -mouse=1 - -; XInput input bindings -; -; Left Stick Lever -; Left Trigger Lever (move to the left) -; Right Trigger Lever (move to the right) -; Left Left red button -; Up Left green button -; Right Left blue button -; Left Shoulder Left side button -; Right Shoulder Right side button -; X Right red button -; Y Right green button -; A Right blue button -; Back Left menu button -; Start Right menu button - -; Keyboard input bindings -left1=0x41 ; A -left2=0x53 ; S -left3=0x44 ; D - -leftSide=0x01 ; Mouse Left -rightSide=0x02 ; Mouse Right - -right1=0x4A ; J -right2=0x4B ; K -right3=0x4C ; L - -leftMenu=0x55 ; U -rightMenu=0x4F ; O".to_owned(), +controllerLedOutputSerial=0".to_owned(), Game::Chunithm => " [vfd] ; Enable VFD emulation. Disable to use a real VFD @@ -179,120 +137,6 @@ controllerLedOutputOpeNITHM=0 ; x86 chuniio to path32, x64 to path64. Both are necessary. ;path32= ;path64= - -; ----------------------------------------------------------------------------- -; Input settings -; ----------------------------------------------------------------------------- - -; Keyboard bindings are specified as hexadecimal (prefixed with 0x) or decimal -; (not prefixed with 0x) virtual-key codes, a list of which can be found here: -; -; https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes -; -; This is, admittedly, not the most user-friendly configuration method in the -; world. An improved solution will be provided later. - -[io3] - -test=0x31 - -service=0x32 - -coin=0x33 - -ir=0x00 - -ir6=0x39 - -ir5=0x38 - -ir4=0x37 - -ir3=0x36 - -ir2=0x35 - -ir1=0x34 - -[ir] - -ir6=0x39 - -ir5=0x38 - -ir4=0x37 - -ir3=0x36 - -ir2=0x35 - -ir1=0x34 - -[slider] - -cell32=0x51 - -cell30=0x5A - -cell28=0x53 - -cell26=0x45 - -cell24=0x43 - -cell22=0x46 - -cell20=0x54 - -cell18=0x42 - -cell16=0x48 - -cell14=0x55 - -cell12=0x4D - -cell10=0x4B - -cell8=0x4F - -cell6=190 - -cell4=186 - -cell2=219 - -cell31=0x41 - -cell29=0x57 - -cell27=0x58 - -cell25=0x44 - -cell23=0x52 - -cell21=0x56 - -cell19=0x47 - -cell17=0x59 - -cell15=0x4E - -cell13=0x4A - -cell11=0x49 - -cell9=188 - -cell7=0x4C - -cell5=0x50 - -cell3=191 - -cell1=222 ".to_owned() } } \ No newline at end of file diff --git a/rust/src/modules/keyboard.rs b/rust/src/modules/keyboard.rs new file mode 100644 index 0000000..c461dfd --- /dev/null +++ b/rust/src/modules/keyboard.rs @@ -0,0 +1,120 @@ +use ini::Ini; +use anyhow::Result; +use crate::model::profile::Keyboard; + +macro_rules! parse_int_field { + ($section:expr,$sgt:expr,$sl:expr) => { + if let Some(field) = $section.get($sgt) { + let field = &field[0..field.chars().position(|c| c == ';').unwrap_or(field.len())].trim(); + log::debug!("loading {}={}", $sgt, field); + + let res = if field.starts_with("0x") { + i32::from_str_radix(&field.trim()[2..], 16) + } else { + field.trim().parse::() + }; + + match res { + Ok(v) => $sl = v, + Err(e) => log::warn!("unable to read a segatools.ini field key={} value={}: {:?}", $sgt, field.trim(), e) + }; + } else { + log::debug!("unable to load {}", $sgt); + } + } +} + +impl Keyboard { + pub fn load_from_ini(&mut self, ini: &Ini) -> Result<()> { + log::debug!("loading kb"); + match self { + Keyboard::Ongeki(kb) => { + if let Some(s) = ini.section(Some("io4")) { + parse_int_field!(s, "test", kb.test); + parse_int_field!(s, "service", kb.svc); + parse_int_field!(s, "coin", kb.coin); + parse_int_field!(s, "left1", kb.l1); + parse_int_field!(s, "left2", kb.l2); + parse_int_field!(s, "left3", kb.l3); + parse_int_field!(s, "right1", kb.r1); + parse_int_field!(s, "right2", kb.r2); + parse_int_field!(s, "right3", kb.r3); + parse_int_field!(s, "leftMenu", kb.lmenu); + parse_int_field!(s, "rightMenu", kb.rmenu); + parse_int_field!(s, "leftSide", kb.lwad); + parse_int_field!(s, "rightSide", kb.rwad); + + let mut mouse: i32 = 1; + parse_int_field!(s, "mouse", mouse); + kb.use_mouse = if mouse == 1 { true } else { false }; + } + } + Keyboard::Chunithm(kb) => { + if let Some(s) = ini.section(Some("io3")) { + parse_int_field!(s, "test", kb.test); + parse_int_field!(s, "service", kb.svc); + parse_int_field!(s, "coin", kb.coin); + + let mut ir: i32 = 1; + parse_int_field!(s, "ir", ir); + kb.split_ir = if ir == 0 { true } else { false }; + } + + if let Some(s) = ini.section(Some("slider")) { + for i in 0..kb.cell.len() { + parse_int_field!(s, format!("cell{}", i + 1), kb.cell[i]); + } + } + + if let Some(s) = ini.section(Some("ir")) { + for i in 0..kb.ir.len() { + parse_int_field!(s, format!("ir{}", i + 1), kb.ir[i]); + } + } + } + } + + Ok(()) + } + + // This is assumed to run in sync after the segatools module + pub fn line_up(&self, ini: &mut Ini) -> Result<()> { + match self { + Keyboard::Ongeki(kb) => { + ini.with_section(Some("io4")) + .set("test", kb.test.to_string()) + .set("service", kb.svc.to_string()) + .set("coin", kb.coin.to_string()) + .set("left1", kb.l1.to_string()) + .set("left2", kb.l2.to_string()) + .set("left3", kb.l3.to_string()) + .set("right1", kb.r1.to_string()) + .set("right2", kb.r2.to_string()) + .set("right3", kb.r3.to_string()) + .set("leftSide", kb.lwad.to_string()) + .set("rightSide", kb.rwad.to_string()) + .set("leftMenu", kb.lmenu.to_string()) + .set("rightMenu", kb.rmenu.to_string()) + .set("mouse", if kb.use_mouse { "1" } else { "0" }); + } + Keyboard::Chunithm(kb) => { + for (i, cell) in kb.cell.iter().enumerate() { + ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string()); + } + if kb.split_ir { + for (i, ir) in kb.ir.iter().enumerate() { + ini.with_section(Some("ir")).set(format!("ir{}", i + 1), ir.to_string()); + } + } else { + ini.with_section(Some("io3")).set("ir", kb.ir[0].to_string()); + } + ini.with_section(Some("io3")) + .set("test", kb.test.to_string()) + .set("service", kb.svc.to_string()) + .set("coin", kb.coin.to_string()); + } + } + + Ok(()) + } +} \ No newline at end of file diff --git a/rust/src/modules/mod.rs b/rust/src/modules/mod.rs index edb39c7..eca6bde 100644 --- a/rust/src/modules/mod.rs +++ b/rust/src/modules/mod.rs @@ -3,6 +3,7 @@ pub mod segatools; pub mod network; pub mod bepinex; pub mod mu3ini; +pub mod keyboard; #[cfg(target_os = "windows")] pub mod display_windows; \ No newline at end of file diff --git a/rust/src/modules/network.rs b/rust/src/modules/network.rs index 41ba16f..10173cb 100644 --- a/rust/src/modules/network.rs +++ b/rust/src/modules/network.rs @@ -5,6 +5,32 @@ use ini::Ini; use crate::model::profile::{Network, NetworkType}; impl Network { + pub fn load_from_ini(&mut self, ini: &Ini) -> Result<()> { + log::debug!("loading network"); + if let Some(s) = ini.section(Some("dns")) { + if let Some(default) = s.get("default") { + if default.starts_with("192.") || default.starts_with("127.") { + self.network_type = NetworkType::Artemis; + } else { + self.network_type = NetworkType::Remote; + self.remote_address = default.to_owned(); + } + } + } + + if let Some(s) = ini.section(Some("netenv")) { + s.get("addrSuffix").map(|v| + self.suffix = v.parse::().ok() + ); + } + + if let Some(s) = ini.section(Some("keychip")) { + s.get("subnet").map(|v| self.subnet = v.to_owned()); + s.get("id").map(|v| self.keychip = v.to_owned()); + } + + Ok(()) + } pub fn line_up(&self, ini: &mut Ini) -> Result<()> { log::debug!("begin line-up: network"); diff --git a/rust/src/modules/segatools.rs b/rust/src/modules/segatools.rs index 486420d..7d1baa8 100644 --- a/rust/src/modules/segatools.rs +++ b/rust/src/modules/segatools.rs @@ -31,6 +31,14 @@ impl Segatools { _ => {}, } } + pub fn load_from_ini(&mut self, ini: &Ini) { + log::debug!("loading sgt"); + if let Some(s) = ini.section(Some("vfs")) { + s.get("amfs").map(|v| self.amfs = PathBuf::from(v)); + s.get("appdata").map(|v| self.appdata = PathBuf::from(v)); + s.get("option").map(|v| self.option = PathBuf::from(v)); + } + } pub async fn line_up(&self, p: &impl ProfilePaths, game: Game) -> Result { log::debug!("begin line-up: segatools"); diff --git a/rust/src/profiles/mod.rs b/rust/src/profiles/mod.rs index 6cee94c..012fb6d 100644 --- a/rust/src/profiles/mod.rs +++ b/rust/src/profiles/mod.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use tauri::AppHandle; use std::{collections::BTreeSet, path::{Path, PathBuf}}; -use crate::{model::{misc::Game, profile::{Aime, Mu3Ini, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util}; +use crate::{model::{misc::Game, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util}; use tauri::Emitter; use std::process::Stdio; use crate::model::profile::BepInEx; @@ -54,7 +54,10 @@ pub struct ProfileData { pub wine: crate::model::profile::Wine, #[serde(skip_serializing_if = "Option::is_none")] - pub mu3_ini: Option + pub mu3_ini: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub keyboard: Option } impl Profile { @@ -74,6 +77,12 @@ impl Profile { #[cfg(not(target_os = "windows"))] wine: crate::model::profile::Wine::default(), mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini { audio: None, blacklist: None }) } else { None }, + keyboard: + if meta.game == Game::Ongeki { + Some(Keyboard::Ongeki(OngekiKeyboard::default())) + } else { + Some(Keyboard::Chunithm(ChunithmKeyboard::default())) + }, }, meta: meta.clone() }; @@ -87,11 +96,18 @@ impl Profile { pub fn load(game: Game, name: String) -> Result { let path = util::profile_config_dir(game, &name).join("profile.json"); if let Ok(s) = std::fs::read_to_string(&path) { - let data = serde_json::from_str::(&s) + let mut data = serde_json::from_str::(&s) .map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?; log::debug!("{:?}", data); + if game == Game::Ongeki && data.keyboard.is_none() { + data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default())); + } + if game == Game::Chunithm && data.keyboard.is_none() { + data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default())); + } + Ok(Profile { meta: ProfileMeta { game, name @@ -163,6 +179,10 @@ impl Profile { if self.meta.game.has_module(ProfileModule::Mu3Ini) && source.mu3_ini.is_some() { self.data.mu3_ini = source.mu3_ini; } + + if self.meta.game.has_module(ProfileModule::Keyboard) && source.keyboard.is_some() { + self.data.keyboard = source.keyboard; + } } pub async fn line_up(&self, pkg_hash: String, refresh: bool, _app: AppHandle) -> Result<()> { let info = match &self.data.display { @@ -200,6 +220,10 @@ impl Profile { .map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?; self.data.network.line_up(&mut ini)?; + if let Some(keyboard) = &self.data.keyboard { + keyboard.line_up(&mut ini)?; + } + ini.write_to_file(self.data_dir().join("segatools.ini")) .map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?; diff --git a/rust/tauri.conf.json b/rust/tauri.conf.json index 7e3c68f..27cd23c 100644 --- a/rust/tauri.conf.json +++ b/rust/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "STARTLINER", - "version": "0.4.0", + "version": "0.5.0", "identifier": "zip.patafour.startliner", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/components/App.vue b/src/components/App.vue index 65a143a..7abe54b 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -1,9 +1,11 @@ + + + + diff --git a/src/components/OptionCategory.vue b/src/components/OptionCategory.vue index 37291c2..02efa87 100644 --- a/src/components/OptionCategory.vue +++ b/src/components/OptionCategory.vue @@ -6,6 +6,7 @@ const general = useGeneralStore(); defineProps({ title: String, + collapsed: Boolean, }); @@ -14,6 +15,7 @@ defineProps({ :legend="title" :toggleable="true" v-show="general.cfgCategories.has(title ?? '')" + :collapsed="collapsed" >
diff --git a/src/components/OptionList.vue b/src/components/OptionList.vue index 90471d7..3657ec2 100644 --- a/src/components/OptionList.vue +++ b/src/components/OptionList.vue @@ -7,6 +7,7 @@ import OptionCategory from './OptionCategory.vue'; import OptionRow from './OptionRow.vue'; import AimeOptions from './options/Aime.vue'; import DisplayOptions from './options/Display.vue'; +import KeyboardOptions from './options/Keyboard.vue'; import MiscOptions from './options/Misc.vue'; import NetworkOptions from './options/Network.vue'; import SegatoolsOptions from './options/Segatools.vue'; @@ -129,6 +130,7 @@ prf.reload(); v-model="blacklistMaxModel" /> --> + diff --git a/src/components/StartButton.vue b/src/components/StartButton.vue index 9ab03e9..112d699 100644 --- a/src/components/StartButton.vue +++ b/src/components/StartButton.vue @@ -1,9 +1,7 @@ + + diff --git a/src/components/options/Misc.vue b/src/components/options/Misc.vue index 1a69cd7..f14f1c7 100644 --- a/src/components/options/Misc.vue +++ b/src/components/options/Misc.vue @@ -17,6 +17,7 @@ const prf = usePrfStore(); title="More segatools options" tooltip="Advanced options not covered by STARTLINER" > + diff --git a/src/components/options/Segatools.vue b/src/components/options/Segatools.vue index 56144de..f4b86b4 100644 --- a/src/components/options/Segatools.vue +++ b/src/components/options/Segatools.vue @@ -1,15 +1,19 @@