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

View File

@ -1,9 +1,11 @@
<script setup lang="ts">
import { Ref, computed, onMounted, ref } from 'vue';
import Button from 'primevue/button';
import ConfirmDialog from 'primevue/confirmdialog';
import Dialog from 'primevue/dialog';
import InputIcon from 'primevue/inputicon';
import InputText from 'primevue/inputtext';
import ScrollPanel from 'primevue/scrollpanel';
import Tab from 'primevue/tab';
import TabList from 'primevue/tablist';
import TabPanel from 'primevue/tabpanel';
@ -23,6 +25,7 @@ import {
usePrfStore,
} from '../stores';
import { Dirs } from '../types';
import { messageSplit } from '../util';
const pkg = usePkgStore();
const prf = usePrfStore();
@ -86,6 +89,23 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
: 'main-scale-xl'
"
>
<ConfirmDialog>
<template #message="{ message }">
<ScrollPanel
v-if="messageSplit(message).length > 5"
style="width: 100%; height: 40vh"
>
<p v-for="m in messageSplit(message)">
{{ m }}
</p></ScrollPanel
>
<div v-else>
<p v-for="m in messageSplit(message)">
{{ m }}
</p>
</div>
</template>
</ConfirmDialog>
<Dialog
modal
:visible="errorVisible"

View File

@ -0,0 +1,245 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import InputText from 'primevue/inputtext';
import { usePrfStore } from '../stores';
import { OngekiButtons } from '../types';
const prf = usePrfStore();
const hasClickedM1Once = ref(false);
const handleKey = (
button: string | undefined,
event: KeyboardEvent,
index?: number
) => {
event.preventDefault();
const keycode = toKeycode(event.code);
if (keycode !== null && button !== undefined) {
const data = prf.current!.data.keyboard!.data as any;
if (index !== undefined) {
data[button][index] = keycode;
} else {
data[button] = keycode;
}
}
};
const handleMouse = (
button: string | undefined,
event: MouseEvent,
index?: number
) => {
if (button === undefined || button == 'use_mouse') {
return;
}
if (event.button === 0) {
if (hasClickedM1Once.value === false) {
hasClickedM1Once.value = true;
return;
}
} else {
event.preventDefault();
}
let keycode;
switch (event.button) {
case 0:
keycode = 1;
break;
case 1:
keycode = 4;
break;
case 2:
keycode = 2;
break;
case 3:
keycode = 5;
break;
case 4:
keycode = 6;
break;
default:
break;
}
if (keycode !== undefined) {
const data = prf.current!.data.keyboard!.data as any;
if (index !== undefined) {
data[button][index] = keycode;
} else {
data[button] = keycode;
}
}
};
const getKey = (key: keyof OngekiButtons, index?: number) =>
computed(() => {
const data = prf.current!.data.keyboard?.data as any;
const keycode =
index === undefined
? (data[key] as number | undefined)
: (data[key]?.[index] as number | undefined);
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '';
});
const KEY_MAP: { [key: number]: string } = {
1: 'M1',
2: 'M2',
4: 'M3',
5: 'M4',
6: 'M5',
8: 'Backspace',
9: 'Tab',
13: 'Enter',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: 'Space',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
45: 'Insert',
46: 'Delete',
48: 'Digit0',
49: 'Digit1',
50: 'Digit2',
51: 'Digit3',
52: 'Digit4',
53: 'Digit5',
54: 'Digit6',
55: 'Digit7',
56: 'Digit8',
57: 'Digit9',
65: 'KeyA',
66: 'KeyB',
67: 'KeyC',
68: 'KeyD',
69: 'KeyE',
70: 'KeyF',
71: 'KeyG',
72: 'KeyH',
73: 'KeyI',
74: 'KeyJ',
75: 'KeyK',
76: 'KeyL',
77: 'KeyM',
78: 'KeyN',
79: 'KeyO',
80: 'KeyP',
81: 'KeyQ',
82: 'KeyR',
83: 'KeyS',
84: 'KeyT',
85: 'KeyU',
86: 'KeyV',
87: 'KeyW',
88: 'KeyX',
89: 'KeyY',
90: 'KeyZ',
91: 'MetaLeft',
92: 'MetaRight',
93: 'ContextMenu',
96: 'Numpad0',
97: 'Numpad1',
98: 'Numpad2',
99: 'Numpad3',
100: 'Numpad4',
101: 'Numpad5',
102: 'Numpad6',
103: 'Numpad7',
104: 'Numpad8',
105: 'Numpad9',
106: 'NumpadMultiply',
107: 'NumpadAdd',
109: 'NumpadSubtract',
110: 'NumpadDecimal',
111: 'NumpadDivide',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
160: 'ShiftLeft',
161: 'ShiftRight',
162: 'ControlLeft',
163: 'ControlRight',
164: 'AltLeft',
165: 'AltRight',
186: 'Semicolon',
187: 'Equal',
188: 'Comma',
189: 'Minus',
190: 'Period',
191: 'Slash',
192: 'Backquote',
219: 'BracketLeft',
220: 'Backslash',
221: 'BracketRight',
222: 'Quote',
};
const fromKeycode = (keyCode: number): string | null => {
return KEY_MAP[keyCode] ?? null;
};
const toKeycode = (key: string): number | null => {
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
return res ? parseInt(res) : null;
};
defineProps({
small: Boolean,
verySmall: Boolean,
tall: Boolean,
tooltip: String,
button: String,
color: String,
index: Number,
});
</script>
<template>
<InputText
:style="{
width: small ? '3em' : '5em',
height: small ? '3em' : tall ? '10em' : '5em',
fontSize: small ? '0.9em' : '1em',
backgroundColor: color,
}"
unstyled
class="text-center buttoninputtext"
v-tooltip="tooltip"
@contextmenu.prevent="() => {}"
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
@mousedown="
(ev: MouseEvent) =>
handleMouse(button as keyof OngekiButtons, ev, index)
"
@focusout="() => (hasClickedM1Once = false)"
:model-value="getKey(button as keyof OngekiButtons, index) as any"
/>
</template>
<style scoped lang="css">
.buttoninputtext {
border-radius: 6px;
border: 1px solid rgba(200, 200, 200, 0.3);
}
</style>

View File

@ -6,6 +6,7 @@ const general = useGeneralStore();
defineProps({
title: String,
collapsed: Boolean,
});
</script>
@ -14,6 +15,7 @@ defineProps({
:legend="title"
:toggleable="true"
v-show="general.cfgCategories.has(title ?? '')"
:collapsed="collapsed"
>
<div class="flex w-full flex-col gap-1">
<slot />

View File

@ -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"
/></OptionRow> -->
</OptionCategory>
<KeyboardOptions />
<StartlinerOptions />
</template>

View File

@ -1,9 +1,7 @@
<script setup lang="ts">
import { Ref, computed, ref } from 'vue';
import Button from 'primevue/button';
import ConfirmDialog from 'primevue/confirmdialog';
import ContextMenu from 'primevue/contextmenu';
import ScrollPanel from 'primevue/scrollpanel';
import { useConfirm } from 'primevue/useconfirm';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
@ -38,6 +36,8 @@ const startline = async (force: boolean, refresh: boolean) => {
confirmDialog.require({
message: message.join('\n'),
header: 'Start check failed',
acceptLabel: 'Run anyway',
rejectLabel: 'Cancel',
accept: () => {
startline(true, refresh);
},
@ -85,10 +85,6 @@ listen('launch-end', () => {
getCurrentWindow().setFocus();
});
const messageSplit = (message: any) => {
return message.message?.split('\n');
};
const menuItems = [
{
label: 'Refresh and start',
@ -111,45 +107,6 @@ const showContextMenu = (event: Event) => {
<template>
<ContextMenu ref="menu" :model="menuItems" />
<ConfirmDialog>
<template #container="{ message, acceptCallback, rejectCallback }">
<div
class="flex flex-col p-8 bg-surface-0 dark:bg-surface-900 rounded"
>
<span class="font-bold self-center text-2xl block mb-2 mt-2">{{
message.header
}}</span>
<ScrollPanel
v-if="messageSplit(message).length > 5"
style="width: 100%; height: 40vh"
>
<p v-for="m in messageSplit(message)">
{{ m }}
</p></ScrollPanel
>
<div v-else>
<p v-for="m in messageSplit(message)">
{{ m }}
</p>
</div>
<div class="flex self-center items-center gap-2 mt-6">
<Button
label="Run anyway"
@click="acceptCallback"
size="small"
class="w-32"
></Button>
<Button
label="Cancel"
outlined
size="small"
@click="rejectCallback"
class="w-32"
></Button>
</div>
</div>
</template>
</ConfirmDialog>
<Button
v-if="startStatus === 'ready'"
v-tooltip="disabledTooltip"

View File

@ -0,0 +1,177 @@
<script setup lang="ts">
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import KeyboardKey from '../KeyboardKey.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
import { ChunithmButtons } from '@/types';
ToggleSwitch;
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Keyboard">
<OptionRow
title="Lever mode"
v-if="prf.current!.data.keyboard!.game === 'Ongeki'"
>
<SelectButton
v-model="prf.current!.data.keyboard!.data.use_mouse"
:options="[
{ title: 'XInput', value: false },
{ title: 'Mouse', value: true },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</OptionRow>
<OptionRow
title="Enable multiple IRs"
v-if="prf.current!.data.keyboard!.game === 'Chunithm'"
>
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.split_ir" />
</OptionRow>
<div
:style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`"
>
<div
class="absolute left-1/6 top-1/10"
style="transform: translateX(-30%) translateY(-50%)"
>
<div class="flex flex-row gap-2 self-center w-full">
<KeyboardKey button="test" small tooltip="Test" />
<KeyboardKey button="svc" small tooltip="Service" />
<KeyboardKey button="coin" small tooltip="Coin" />
</div>
</div>
<div v-if="prf.current?.meta.game === 'ongeki'">
<div
class="absolute left-1/2 top-1/2"
style="transform: translateX(-540%) translateY(-200%)"
>
<KeyboardKey
button="lmenu"
small
color="rgba(255, 0, 0, 0.2)"
/>
</div>
<div
class="absolute right-1/2 top-1/2"
style="transform: translateX(540%) translateY(-200%)"
>
<KeyboardKey
button="rmenu"
small
color="rgba(255, 255, 0, 0.2)"
/>
</div>
<div
class="absolute left-1/2 top-1/2"
style="transform: translateX(-50%) translateY(-20%)"
>
<div class="flex flex-row gap-2 self-center w-full">
<KeyboardKey
button="lwad"
tall
color="rgba(180, 0, 255, 0.2)"
/>
<div style="width: 0.7em"></div>
<KeyboardKey button="l1" color="rgba(255, 0, 0, 0.2)" />
<KeyboardKey button="l2" color="rgba(0, 255, 0, 0.2)" />
<KeyboardKey button="l3" color="rgba(0, 0, 255, 0.2)" />
<div style="width: 0.7em"></div>
<KeyboardKey button="r1" color="rgba(255, 0, 0, 0.2)" />
<KeyboardKey button="r2" color="rgba(0, 255, 0, 0.2)" />
<KeyboardKey button="r3" color="rgba(0, 0, 255, 0.2)" />
<div style="width: 0.7em"></div>
<KeyboardKey
button="rwad"
tall
color="rgba(255, 0, 180, 0.2)"
/>
</div>
</div>
</div>
<div v-if="prf.current?.meta.game === 'chunithm'">
<div class="absolute left-1/2 top-1/5">
<div
class="flex flex-row flex-nowrap gap-2 self-center w-full"
>
<div
v-if="
(
prf.current!.data.keyboard!
.data as ChunithmButtons
).split_ir
"
v-for="idx in Array(6)
.fill(0)
.map((_, i) => i + 1)"
>
<KeyboardKey
button="ir"
:index="idx - 1"
:tooltip="`ir${idx}`"
small
color="rgba(0, 255, 0, 0.2)"
/>
</div>
<div v-else>
<KeyboardKey
button="ir"
:index="0"
:tooltip="`ir0`"
small
color="rgba(0, 255, 0, 0.2)"
/>
</div>
</div>
</div>
<div
class="absolute left-1/2 top-1/2"
style="transform: translateX(-50%) translateY(-5%)"
>
<div
class="flex flex-row flex-nowrap gap-2 self-center w-full"
>
<div
v-for="idx in Array(16)
.fill(0)
.map((_, i) => 16 - i)"
>
<KeyboardKey
button="cell"
:index="idx - 1"
:tooltip="`cell${idx}`"
small
color="rgba(255, 255, 0, 0.2)"
/>
</div>
</div>
<div style="height: 0.6em"></div>
<div
class="flex flex-row flex-nowrap gap-2 self-center w-full"
>
<div
v-for="idx in Array(16)
.fill(0)
.map((_, i) => 32 - i)"
>
<KeyboardKey
button="cell"
:index="idx - 1"
:tooltip="`cell${idx}`"
small
color="rgba(255, 255, 0, 0.2)"
/>
</div>
</div>
</div>
</div>
</div>
</OptionCategory>
</template>

View File

@ -17,6 +17,7 @@ const prf = usePrfStore();
title="More segatools options"
tooltip="Advanced options not covered by STARTLINER"
>
<!-- <Button icon="pi pi-refresh" size="small" /> -->
<FileEditor filename="segatools-base.ini" />
</OptionRow>
</OptionCategory>

View File

@ -1,15 +1,19 @@
<script setup lang="ts">
import { computed } from 'vue';
import Select from 'primevue/select';
import { useConfirm } from 'primevue/useconfirm';
import * as path from '@tauri-apps/api/path';
import FilePicker from '../FilePicker.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { invoke } from '../../invoke';
import { usePkgStore, usePrfStore } from '../../stores';
import { Feature } from '../../types';
import { pkgKey } from '../../util';
const prf = usePrfStore();
const pkgs = usePkgStore();
const confirmDialog = useConfirm();
const names = computed(() => {
switch (prf.current?.meta.game) {
@ -31,6 +35,20 @@ const names = computed(() => {
throw new Error('Option tab without a profile');
}
});
const checkSegatoolsIni = async (target: string) => {
const iniPath = await path.join(target, '../segatools.ini');
if (await invoke('file_exists', { path: iniPath })) {
confirmDialog.require({
message: 'Would you like to load the existing configuration data?',
header: 'segatools.ini found',
accept: async () => {
await invoke('load_segatools_ini', { path: iniPath });
await prf.reload();
},
});
}
};
</script>
<template>
@ -45,7 +63,10 @@ const names = computed(() => {
extension="exe"
:value="prf.current!.data.sgt.target"
:callback="
(value: string) => (prf.current!.data.sgt.target = value)
(value: string) => (
(prf.current!.data.sgt.target = value),
checkSegatoolsIni(value)
)
"
></FilePicker>
</OptionRow>

View File

@ -347,7 +347,7 @@ export const useClientStore = defineStore('client', () => {
scaleFactor.value = value;
const window = getCurrentWindow();
const w = Math.floor(scaleValue(value) * 760);
const w = Math.floor(scaleValue(value) * 900);
const h = Math.floor(scaleValue(value) * 480);
let size = await window.innerSize();

View File

@ -53,6 +53,7 @@ export interface ProfileData {
network: NetworkConfig;
bepinex: BepInExConfig;
mu3_ini: Mu3IniConfig | undefined;
keyboard: KeyboardConfig | undefined;
}
export interface SegatoolsConfig {
@ -101,6 +102,42 @@ export interface Mu3IniConfig {
// blacklist?: [number, number];
}
export interface OngekiButtons {
use_mouse: boolean;
coin: number;
svc: number;
test: number;
lmenu: number;
rmenu: number;
l1: number;
l2: number;
l3: number;
r1: number;
r2: number;
r3: number;
lwad: number;
rwad: number;
}
export interface ChunithmButtons {
split_ir: boolean;
coin: number;
svc: number;
test: number;
cell: number[];
ir: number[];
}
export type KeyboardConfig =
| {
game: 'Ongeki';
data: OngekiButtons;
}
| {
game: 'Chunithm';
data: ChunithmButtons;
};
export interface Profile {
meta: ProfileMeta;
data: ProfileData;

View File

@ -55,3 +55,7 @@ export const hasFeature = (pkg: Package | undefined, feature: Feature) => {
pkg.loc.status.OK & feature
);
};
export const messageSplit = (message: any) => {
return message.message?.split('\n');
};