feat: segatools.ini loading

This commit is contained in:
2025-04-10 13:32:49 +00:00
parent 4e795257ad
commit 9ea66dbeab
22 changed files with 804 additions and 214 deletions

View File

@ -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<AppData>>) -> Result<Opt
#[tauri::command]
pub async fn sync_current_profile(state: State<'_, Mutex<AppData>>, 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<AppData>>) -> Result<()
}
}
#[tauri::command]
pub async fn load_segatools_ini(state: State<'_, Mutex<AppData>>, 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<Vec<String>, ()> {
log::debug!("invoke: list_platform_capabilities");
@ -413,4 +436,10 @@ pub async fn list_directories() -> Result<util::Dirs, ()> {
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<bool, ()> {
Ok(std::fs::exists(path).unwrap_or(false))
}

View File

@ -71,8 +71,8 @@ pub async fn run(_args: Vec<String>) {
} 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<String>) {
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<String>) {
cmd::list_displays,
cmd::list_platform_capabilities,
cmd::list_directories,
cmd::file_exists,
])
.build(tauri::generate_context!())
.expect("error while building tauri application");

View File

@ -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)
}
}

View File

@ -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,
}

View File

@ -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()
}
}

View File

@ -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::<i32>()
};
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(())
}
}

View File

@ -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;

View File

@ -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::<i32>().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");

View File

@ -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<Ini> {
log::debug!("begin line-up: segatools");

View File

@ -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<Mu3Ini>
pub mu3_ini: Option<Mu3Ini>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard>
}
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<Self> {
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::<ProfileData>(&s)
let mut data = serde_json::from_str::<ProfileData>(&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))?;

View File

@ -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",