feat: initial chunithm support
This commit is contained in:
@ -7,6 +7,11 @@
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-unminimize",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:app:allow-app-hide",
|
||||
"shell:default",
|
||||
"dialog:default",
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use crate::model::config::GlobalConfig;
|
||||
use crate::pkg::{Feature, Status};
|
||||
use crate::profiles::AnyProfile;
|
||||
use crate::profiles::Profile;
|
||||
use crate::{model::misc::Game, pkg::PkgKey};
|
||||
use crate::pkg_store::PackageStore;
|
||||
use crate::util;
|
||||
@ -13,7 +13,7 @@ pub struct GlobalState {
|
||||
}
|
||||
|
||||
pub struct AppData {
|
||||
pub profile: Option<AnyProfile>,
|
||||
pub profile: Option<Profile>,
|
||||
pub pkgs: PackageStore,
|
||||
pub cfg: GlobalConfig,
|
||||
pub state: GlobalState,
|
||||
@ -33,7 +33,7 @@ impl AppData {
|
||||
.unwrap_or_default();
|
||||
|
||||
let profile = match cfg.recent_profile {
|
||||
Some((ref game, ref name)) => AnyProfile::load(game.clone(), name.clone()).ok(),
|
||||
Some((game, ref name)) => Profile::load(game, name.clone()).ok(),
|
||||
None => None
|
||||
};
|
||||
|
||||
@ -52,7 +52,7 @@ impl AppData {
|
||||
}
|
||||
|
||||
pub fn switch_profile(&mut self, game: Game, name: String) -> Result<()> {
|
||||
match AnyProfile::load(game.clone(), name.clone()) {
|
||||
match Profile::load(game.clone(), name.clone()) {
|
||||
Ok(profile) => {
|
||||
self.profile = Some(profile);
|
||||
self.cfg.recent_profile = Some((game, name));
|
||||
@ -103,7 +103,7 @@ impl AppData {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sum_packages(&self, p: &AnyProfile) -> String {
|
||||
pub fn sum_packages(&self, p: &Profile) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
for key in p.mod_pkgs().into_iter() {
|
||||
if let Ok(pkg) = self.pkgs.get(&key) {
|
||||
|
@ -6,8 +6,7 @@ use tauri::{AppHandle, Manager, State};
|
||||
use crate::model::misc::Game;
|
||||
use crate::pkg::{Package, PkgKey};
|
||||
use crate::pkg_store::{InstallResult, PackageStore};
|
||||
use crate::profiles::ongeki::OngekiProfile;
|
||||
use crate::profiles::{self, AnyProfile, Profile, ProfileMeta, ProfilePaths};
|
||||
use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths};
|
||||
use crate::appdata::{AppData, ToggleAction};
|
||||
use crate::model::misc::StartCheckError;
|
||||
use crate::util;
|
||||
@ -147,10 +146,21 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
|
||||
Ok(appd.pkgs.get_all())
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_game_packages(state: State<'_, Mutex<AppData>>, game: Game) -> Result<Vec<PkgKey>, ()> {
|
||||
log::debug!("invoke: get_game_packages {game}");
|
||||
|
||||
let appd = state.lock().await;
|
||||
|
||||
Ok(appd.pkgs.get_game_list(game))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
|
||||
log::debug!("invoke: fetch_listings");
|
||||
|
||||
// let game;
|
||||
{
|
||||
let appd = state.lock().await;
|
||||
if !appd.pkgs.is_offline() {
|
||||
@ -161,13 +171,26 @@ pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), Stri
|
||||
log::info!("fetch_listings: skipped");
|
||||
return Err("offline mode".to_owned());
|
||||
}
|
||||
// if let Some(profile) = &appd.profile {
|
||||
// game = profile.meta.game;
|
||||
// } else {
|
||||
// return Err("No profile".to_owned());
|
||||
// }
|
||||
}
|
||||
|
||||
let listings = PackageStore::fetch_listings().await
|
||||
// Can be this lazy for now as there are only two short lists
|
||||
let listings1 = PackageStore::fetch_listings(Game::Ongeki).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let listings2 = PackageStore::fetch_listings(Game::Chunithm).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
appd.pkgs.process_fetched_listings(listings);
|
||||
appd.pkgs.process_fetched_listings(listings1, Game::Ongeki);
|
||||
appd.pkgs.process_fetched_listings(listings2, Game::Chunithm);
|
||||
|
||||
appd.pkgs.save().await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -189,15 +212,10 @@ pub async fn init_profile(
|
||||
log::debug!("invoke: init_profile({}, {})", game, name);
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
let new_profile = OngekiProfile::new(name)
|
||||
let new_profile = Profile::new(ProfileMeta { game, name })
|
||||
.map_err(|e| format!("Unable to create profile: {}", e))?;
|
||||
|
||||
fs::create_dir_all(new_profile.config_dir()).await
|
||||
.map_err(|e| format!("Unable to create the profile config directory: {}", e))?;
|
||||
fs::create_dir_all(new_profile.data_dir()).await
|
||||
.map_err(|e| format!("Unable to create the profile data directory: {}", e))?;
|
||||
|
||||
appd.profile = Some(AnyProfile::OngekiProfile(new_profile.clone()));
|
||||
appd.profile = Some(new_profile);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -242,7 +260,7 @@ pub async fn rename_profile(
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
if let Some(current) = &mut appd.profile {
|
||||
if current.meta() == profile {
|
||||
if current.meta == profile {
|
||||
current.rename(new_meta.name);
|
||||
}
|
||||
}
|
||||
@ -277,7 +295,7 @@ pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMe
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
if let Some(current) = &mut appd.profile {
|
||||
if current.meta() == profile {
|
||||
if current.meta == profile {
|
||||
appd.profile = None;
|
||||
}
|
||||
}
|
||||
@ -286,7 +304,7 @@ pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMe
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<AnyProfile>, ()> {
|
||||
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<Profile>, ()> {
|
||||
log::debug!("invoke: get_current_profile");
|
||||
|
||||
let appd = state.lock().await;
|
||||
@ -294,12 +312,12 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sync_current_profile(state: State<'_, Mutex<AppData>>, profile: AnyProfile) -> Result<(), String> {
|
||||
pub async fn sync_current_profile(state: State<'_, Mutex<AppData>>, data: ProfileData) -> Result<(), String> {
|
||||
log::debug!("invoke: sync_current_profile");
|
||||
|
||||
let mut appd = state.lock().await;
|
||||
if let Some(p) = &mut appd.profile {
|
||||
p.sync(profile);
|
||||
p.sync(data);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -69,8 +69,8 @@ pub async fn run(_args: Vec<String>) {
|
||||
} else {
|
||||
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into()))
|
||||
.title("STARTLINER")
|
||||
.inner_size(640f64, 480f64)
|
||||
.min_inner_size(640f64, 480f64)
|
||||
.inner_size(720f64, 480f64)
|
||||
.min_inner_size(720f64, 480f64)
|
||||
.build()?;
|
||||
start_immediately = false;
|
||||
}
|
||||
@ -176,6 +176,7 @@ pub async fn run(_args: Vec<String>) {
|
||||
|
||||
cmd::get_package,
|
||||
cmd::get_all_packages,
|
||||
cmd::get_game_packages,
|
||||
cmd::reload_all_packages,
|
||||
cmd::fetch_listings,
|
||||
cmd::install_package,
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use serde::Deserialize;
|
||||
use crate::pkg::PkgKeyVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::pkg::{Status, PkgKey, PkgKeyVersion};
|
||||
|
||||
use super::misc::Game;
|
||||
|
||||
// manifest.json
|
||||
|
||||
@ -13,4 +15,13 @@ pub struct PackageManifest {
|
||||
|
||||
#[serde(default)]
|
||||
pub installers: Vec<BTreeMap<String, serde_json::Value>>
|
||||
}
|
||||
|
||||
pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PackageListEntry {
|
||||
pub version: String,
|
||||
pub status: Status,
|
||||
pub games: Vec<Game>,
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::pkg::PkgKey;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)]
|
||||
pub enum Game {
|
||||
#[serde(rename = "ongeki")]
|
||||
Ongeki,
|
||||
@ -17,6 +17,48 @@ impl Game {
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hook_exe(&self) -> &'static str {
|
||||
match self {
|
||||
Game::Ongeki => "mu3hook.dll",
|
||||
Game::Chunithm => "chusanhook_x86.dll",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hook_amd(&self) -> &'static str {
|
||||
match self {
|
||||
Game::Ongeki => "mu3hook.dll",
|
||||
Game::Chunithm => "chusanhook_x64.dll",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inject_exe(&self) -> &'static str {
|
||||
match self {
|
||||
Game::Ongeki => "inject.exe",
|
||||
Game::Chunithm => "inject_x86.exe",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inject_amd(&self) -> &'static str {
|
||||
match self {
|
||||
Game::Ongeki => "inject.exe",
|
||||
Game::Chunithm => "inject_x64.exe",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exe(&self) -> &'static str {
|
||||
match self {
|
||||
Game::Ongeki => "mu3.exe",
|
||||
Game::Chunithm => "chusanApp.exe",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn amd_args(&self) -> Vec<&'static str> {
|
||||
match self {
|
||||
Game::Ongeki => vec!["-f", "-c", "config_common.json", "config_server.json", "config_client.json"],
|
||||
Game::Chunithm => vec!["-c", "config_common.json", "config_server.json", "config_client.json", "config_cvt.json", "config_sp.json", "config_hook.json"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Game {
|
||||
|
@ -2,7 +2,9 @@ use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::pkg::PkgKey;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
|
||||
use super::misc::Game;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
|
||||
pub enum Aime {
|
||||
Disabled,
|
||||
#[default] BuiltIn,
|
||||
@ -10,7 +12,7 @@ pub enum Aime {
|
||||
Other(PkgKey),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct AMNet {
|
||||
pub name: String,
|
||||
pub addr: String,
|
||||
@ -23,7 +25,7 @@ impl Default for AMNet {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Segatools {
|
||||
pub target: PathBuf,
|
||||
pub hook: Option<PkgKey>,
|
||||
@ -38,11 +40,14 @@ pub struct Segatools {
|
||||
pub amnet: AMNet,
|
||||
}
|
||||
|
||||
impl Default for Segatools {
|
||||
fn default() -> Self {
|
||||
impl Segatools {
|
||||
pub fn default_for(game: Game) -> Self {
|
||||
Segatools {
|
||||
target: PathBuf::default(),
|
||||
hook: Some(PkgKey("segatools-mu3hook".to_owned())),
|
||||
hook: match game {
|
||||
Game::Ongeki => Some(PkgKey("segatools-mu3hook".to_owned())),
|
||||
Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned()))
|
||||
},
|
||||
io: None,
|
||||
amfs: PathBuf::default(),
|
||||
option: PathBuf::default(),
|
||||
@ -54,14 +59,14 @@ impl Default for Segatools {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
|
||||
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
|
||||
pub enum DisplayMode {
|
||||
Window,
|
||||
#[default] Borderless,
|
||||
Fullscreen
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct Display {
|
||||
pub target: String,
|
||||
pub rez: (i32, i32),
|
||||
@ -71,26 +76,32 @@ pub struct Display {
|
||||
pub borderless_fullscreen: bool,
|
||||
}
|
||||
|
||||
impl Default for Display {
|
||||
fn default() -> Self {
|
||||
impl Display {
|
||||
pub fn default_for(game: Game) -> Self {
|
||||
Display {
|
||||
target: "default".to_owned(),
|
||||
rez: (1080, 1920),
|
||||
rez: match game {
|
||||
Game::Chunithm => (1920, 1080),
|
||||
Game::Ongeki => (1080, 1920),
|
||||
},
|
||||
mode: DisplayMode::Borderless,
|
||||
rotation: 0,
|
||||
frequency: 60,
|
||||
frequency: match game {
|
||||
Game::Chunithm => 120,
|
||||
Game::Ongeki => 60,
|
||||
},
|
||||
borderless_fullscreen: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
|
||||
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
|
||||
pub enum NetworkType {
|
||||
#[default] Remote,
|
||||
Artemis,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
|
||||
pub struct Network {
|
||||
pub network_type: NetworkType,
|
||||
|
||||
@ -104,7 +115,7 @@ pub struct Network {
|
||||
pub suffix: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
|
||||
pub struct BepInEx {
|
||||
pub console: bool,
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
pub fn segatools_base() -> String {
|
||||
use super::misc::Game;
|
||||
|
||||
pub fn segatools_base(game: Game) -> String {
|
||||
match game {
|
||||
Game::Ongeki =>
|
||||
"[vfd]
|
||||
; Enable VFD emulation. Disable to use a real VFD
|
||||
; GP1232A02A FUTABA assembly.
|
||||
@ -76,5 +80,219 @@ right2=0x4B ; K
|
||||
right3=0x4C ; L
|
||||
|
||||
leftMenu=0x55 ; U
|
||||
rightMenu=0x4F ; O".to_owned()
|
||||
rightMenu=0x4F ; O".to_owned(),
|
||||
Game::Chunithm => "
|
||||
[vfd]
|
||||
; Enable VFD emulation. Disable to use a real VFD
|
||||
; GP1232A02A FUTABA assembly.
|
||||
enable=1
|
||||
|
||||
[system]
|
||||
; Enable ALLS system settings.
|
||||
enable=1
|
||||
|
||||
; Enable freeplay mode. This will disable the coin slot and set the game to
|
||||
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
|
||||
; allow you to start a game in freeplay mode.
|
||||
freeplay=0
|
||||
|
||||
; LAN Install: If multiple machines are present on the same LAN then set
|
||||
; this to 1 on exactly one machine and set this to 0 on all others.
|
||||
dipsw1=1
|
||||
; Monitor type: 0 = 120FPS, 1 = 60FPS
|
||||
dipsw2=1
|
||||
; Cab type: 0 = SP, 1 = CVT. SP will enable VFD and eMoney. This setting will switch
|
||||
; the LED 837-15093-06 COM port and the AiMe reder hardware generation as well.
|
||||
dipsw3=1
|
||||
|
||||
; -----------------------------------------------------------------------------
|
||||
; Misc. hooks settings
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
[gfx]
|
||||
; Enables the graphics hook.
|
||||
enable=1
|
||||
; Force the game to run windowed.
|
||||
windowed=1
|
||||
; Add a frame to the game window if running windowed.
|
||||
framed=0
|
||||
; Select the monitor to run the game on. (Fullscreen only, 0 =primary screen)
|
||||
monitor=0
|
||||
|
||||
; -----------------------------------------------------------------------------
|
||||
; LED settings
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
[led15093]
|
||||
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
|
||||
; RGBs and the rear LED panel (billboard) on the cabinet.
|
||||
enable=1
|
||||
|
||||
[led]
|
||||
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\chuni_led\"
|
||||
cabLedOutputPipe=1
|
||||
; Output billboard LED strip data to serial
|
||||
cabLedOutputSerial=0
|
||||
|
||||
; Output slider LED data to the named pipe
|
||||
controllerLedOutputPipe=1
|
||||
; Output slider LED data to the serial port
|
||||
controllerLedOutputSerial=0
|
||||
; Use the OpeNITHM protocol for serial LED output
|
||||
controllerLedOutputOpeNITHM=0
|
||||
|
||||
; Serial port to send data to if using serial output. Default is COM5.
|
||||
;serialPort=COM5
|
||||
; Baud rate for serial data (set to 115200 if using OpeNITHM)
|
||||
;serialBaud=921600
|
||||
|
||||
; Data output a sequence of bytes, with JVS-like framing.
|
||||
; Each \"packet\" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere,
|
||||
; 0xD0 is used as an escape character -- if you receive D0 in the output, ignore
|
||||
; it and use the next sent byte plus one instead.
|
||||
;
|
||||
; After the sync is one byte for the board number that was updated, followed by
|
||||
; the red, green and blue values for each LED.
|
||||
;
|
||||
; Board 0 has 53 LEDs:
|
||||
; [0]-[49]: snakes through left half of billboard (first column starts at top)
|
||||
; [50]-[52]: left side partition LEDs
|
||||
;
|
||||
; Board 1 has 63 LEDs:
|
||||
; [0]-[59]: right half of billboard (first column starts at bottom)
|
||||
; [60]-[62]: right side partition LEDs
|
||||
;
|
||||
; Board 2 is the slider and has 31 LEDs:
|
||||
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers
|
||||
|
||||
|
||||
; -----------------------------------------------------------------------------
|
||||
; Custom IO settings
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
[chuniio]
|
||||
; Uncomment this if you have custom chuniio implementation comprised of a single 32bit DLL.
|
||||
; (will use chu2to3 engine internally)
|
||||
;path=
|
||||
|
||||
; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs.
|
||||
; 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()
|
||||
}
|
||||
}
|
@ -4,14 +4,16 @@ use crate::pkg::PkgKey;
|
||||
use crate::util;
|
||||
use crate::profiles::ProfilePaths;
|
||||
|
||||
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>) -> Result<()> {
|
||||
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> {
|
||||
log::debug!("begin prepare packages");
|
||||
|
||||
let pfx_dir = p.data_dir();
|
||||
let opt_dir = pfx_dir.join("option");
|
||||
|
||||
if pfx_dir.join("BepInEx").exists() {
|
||||
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?;
|
||||
if redo_bepinex {
|
||||
if pfx_dir.join("BepInEx").exists() {
|
||||
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if !opt_dir.exists() {
|
||||
@ -21,11 +23,14 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
|
||||
for m in pkgs {
|
||||
log::debug!("preparing {}", m);
|
||||
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
||||
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
|
||||
.join("app")
|
||||
.join("BepInEx");
|
||||
if bpx_dir.exists() {
|
||||
util::copy_directory(&bpx_dir, &pfx_dir.join("BepInEx"), true)?;
|
||||
|
||||
if redo_bepinex {
|
||||
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
|
||||
.join("app")
|
||||
.join("BepInEx");
|
||||
if bpx_dir.exists() {
|
||||
util::copy_directory(&bpx_dir, &pfx_dir.join("BepInEx"), true)?;
|
||||
}
|
||||
}
|
||||
|
||||
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option");
|
||||
|
@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use ini::Ini;
|
||||
use crate::{model::{profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}};
|
||||
use crate::{model::{misc::Game, profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}};
|
||||
use crate::pkg_store::PackageStore;
|
||||
|
||||
impl Segatools {
|
||||
@ -31,7 +31,7 @@ impl Segatools {
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
pub async fn line_up(&self, p: &impl ProfilePaths) -> Result<Ini> {
|
||||
pub async fn line_up(&self, p: &impl ProfilePaths, game: Game) -> Result<Ini> {
|
||||
log::debug!("begin line-up: segatools");
|
||||
|
||||
let pfx_dir = p.data_dir();
|
||||
@ -42,7 +42,7 @@ impl Segatools {
|
||||
let ini_path = p.config_dir().join("segatools-base.ini");
|
||||
|
||||
if !ini_path.exists() {
|
||||
tokio::fs::write(&ini_path, segatools_base()).await
|
||||
tokio::fs::write(&ini_path, segatools_base(game)).await
|
||||
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
|
||||
}
|
||||
if !pfx_dir.exists() {
|
||||
@ -69,12 +69,14 @@ impl Segatools {
|
||||
.set("amfs", self.amfs.stringify()?)
|
||||
.set("appdata", self.appdata.stringify()?);
|
||||
|
||||
ini_out.with_section(Some("unity"))
|
||||
.set("enable", "1")
|
||||
.set(
|
||||
"targetAssembly",
|
||||
pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()?
|
||||
);
|
||||
if game == Game::Ongeki {
|
||||
ini_out.with_section(Some("unity"))
|
||||
.set("enable", "1")
|
||||
.set(
|
||||
"targetAssembly",
|
||||
pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()?
|
||||
);
|
||||
}
|
||||
|
||||
if self.aime != Aime::Disabled {
|
||||
ini_out.with_section(Some("aime"))
|
||||
@ -84,11 +86,16 @@ impl Segatools {
|
||||
let mut aimeio = ini_out.with_section(Some("aimeio"));
|
||||
aimeio
|
||||
.set("path", util::pkg_dir().join(key.to_string()).join("segatools").join("aimeio.dll").stringify()?)
|
||||
.set("gameId", "SDDT")
|
||||
|
||||
.set("serverAddress", &self.amnet.addr)
|
||||
.set("useAimeDBForPhysicalCards", if self.amnet.physical { "1" } else { "0" })
|
||||
.set("enableKeyboardMode", "0");
|
||||
|
||||
match game {
|
||||
Game::Ongeki => aimeio.set("gameId", "SDDT"),
|
||||
Game::Chunithm => aimeio.set("gameId", "SDHD")
|
||||
};
|
||||
|
||||
if let Ok(keyboard_code) = std::fs::read_to_string(p.config_dir().join("aime.txt")) {
|
||||
log::debug!("{} {}", keyboard_code, keyboard_code.len());
|
||||
if keyboard_code.len() == 20 {
|
||||
@ -105,12 +112,14 @@ impl Segatools {
|
||||
.set("enable", "0");
|
||||
}
|
||||
|
||||
if let Some(io) = &self.io {
|
||||
ini_out.with_section(Some("mu3io"))
|
||||
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
|
||||
} else {
|
||||
ini_out.with_section(Some("mu3io"))
|
||||
.set("path", "");
|
||||
if game == Game::Ongeki {
|
||||
if let Some(io) = &self.io {
|
||||
ini_out.with_section(Some("mu3io"))
|
||||
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
|
||||
} else {
|
||||
ini_out.with_section(Some("mu3io"))
|
||||
.set("path", "");
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
||||
|
@ -7,11 +7,11 @@ use enumflags2::{bitflags, make_bitflags, BitFlags};
|
||||
use crate::{model::{local::{self, PackageManifest}, rainy}, util};
|
||||
|
||||
// {namespace}-{name}
|
||||
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)]
|
||||
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
|
||||
pub struct PkgKey(pub String);
|
||||
|
||||
// {namespace}-{name}-{version}
|
||||
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)]
|
||||
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
|
||||
pub struct PkgKeyVersion(String);
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
@ -36,10 +36,11 @@ pub enum Status {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Feature {
|
||||
Mod,
|
||||
Hook,
|
||||
GameIO,
|
||||
Aime,
|
||||
AMNet,
|
||||
Mu3Hook,
|
||||
Mu3IO,
|
||||
ChusanHook,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@ -196,14 +197,16 @@ impl Package {
|
||||
// Multiple features in the same dll (yubideck etc.) should be supported at some point
|
||||
let mut flags = BitFlags::default();
|
||||
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") {
|
||||
if module.ends_with("hook") {
|
||||
flags |= Feature::Hook;
|
||||
if module == "mu3hook" {
|
||||
flags |= Feature::Mu3Hook;
|
||||
} else if module == "chusanhook" {
|
||||
flags |= Feature::ChusanHook;
|
||||
} else if module == "amnet" {
|
||||
flags |= Feature::AMNet | Feature::Aime;
|
||||
} else if module == "aimeio" {
|
||||
flags |= Feature::Aime;
|
||||
} else if module.ends_with("io") {
|
||||
flags |= Feature::GameIO;
|
||||
} else if module == "mu3io" {
|
||||
flags |= Feature::Mu3IO;
|
||||
}
|
||||
}
|
||||
return Status::OK(flags);
|
||||
|
@ -5,15 +5,20 @@ use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::fs;
|
||||
use tokio::task::JoinSet;
|
||||
use crate::model::local::{PackageList, PackageListEntry};
|
||||
use crate::model::misc::Game;
|
||||
use crate::model::rainy;
|
||||
use crate::pkg::{Package, PkgKey, Remote};
|
||||
use crate::pkg::{Package, PkgKey, Remote, Status};
|
||||
use crate::util;
|
||||
use crate::download_handler::DownloadHandler;
|
||||
|
||||
pub struct PackageStore {
|
||||
store: HashMap<PkgKey, Package>,
|
||||
app: AppHandle,
|
||||
meta_list: PackageList,
|
||||
dlh: DownloadHandler,
|
||||
|
||||
app: AppHandle,
|
||||
|
||||
offline: bool,
|
||||
}
|
||||
|
||||
@ -29,8 +34,17 @@ pub enum InstallResult {
|
||||
|
||||
impl PackageStore {
|
||||
pub fn new(app: AppHandle) -> PackageStore {
|
||||
let meta_list = std::fs::read_to_string(util::config_dir().join("package-list.json"))
|
||||
.map_err(|e| anyhow!(e))
|
||||
.and_then(|s| serde_json::from_str::<PackageList>(&s).map_err(|e| anyhow!(e)))
|
||||
.unwrap_or_else(|e| {
|
||||
log::warn!("unable to read package-list: {e}");
|
||||
PackageList::new()
|
||||
});
|
||||
|
||||
PackageStore {
|
||||
store: HashMap::new(),
|
||||
meta_list,
|
||||
app: app.clone(),
|
||||
dlh: DownloadHandler::new(app),
|
||||
offline: true
|
||||
@ -46,6 +60,13 @@ impl PackageStore {
|
||||
self.store.clone()
|
||||
}
|
||||
|
||||
pub fn get_game_list(&self, game: Game) -> Vec<PkgKey> {
|
||||
self.meta_list.iter()
|
||||
.filter(|(_, v)| v.games.contains(&game))
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn reload_package(&mut self, key: PkgKey) {
|
||||
let dir = util::pkg_dir().join(&key.0);
|
||||
if let Ok(pkg) = Package::from_dir(dir).await {
|
||||
@ -75,14 +96,23 @@ impl PackageStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_listings() -> Result<Vec<rainy::V1Package>> {
|
||||
pub async fn save(&self) -> Result<()> {
|
||||
tokio::fs::write(
|
||||
util::config_dir().join("package-list.json"),
|
||||
serde_json::to_string_pretty(&self.meta_list)?
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_listings(game: Game) -> Result<Vec<rainy::V1Package>> {
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use futures::{
|
||||
io::{self, BufReader, ErrorKind},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
let response = reqwest::get("https://rainy.patafour.zip/c/ongeki/api/v1/package/").await?;
|
||||
let response = reqwest::get(format!("https://rainy.patafour.zip/c/{game}/api/v1/package/")).await?;
|
||||
|
||||
let reader = response
|
||||
.bytes_stream()
|
||||
@ -100,11 +130,23 @@ impl PackageStore {
|
||||
self.offline
|
||||
}
|
||||
|
||||
pub fn process_fetched_listings(&mut self, listings: Vec<rainy::V1Package>) {
|
||||
pub fn process_fetched_listings(&mut self, listings: Vec<rainy::V1Package>, game: Game) {
|
||||
for listing in listings {
|
||||
// This is None if the package has no versions for whatever reason
|
||||
if let Some(r) = Package::from_rainy(listing) {
|
||||
//log::warn!("D {}", &r.rmt.as_ref().unwrap().dependencies.first().unwrap_or(&"Nothing".to_owned()));
|
||||
let mut meta_entry = self.meta_list.remove(&r.key()).unwrap_or_else(|| {
|
||||
PackageListEntry {
|
||||
// from_rainy() is guaranteed to include rmt
|
||||
version: r.rmt.as_ref().unwrap().version.clone(),
|
||||
status: Status::Unchecked,
|
||||
games: vec![ game ],
|
||||
}
|
||||
});
|
||||
if !meta_entry.games.contains(&game) {
|
||||
meta_entry.games.push(game);
|
||||
}
|
||||
self.meta_list.insert(r.key(), meta_entry);
|
||||
|
||||
match self.store.get_mut(&r.key()) {
|
||||
Some(l) => {
|
||||
l.rmt = r.rmt;
|
||||
@ -226,9 +268,8 @@ impl PackageStore {
|
||||
async fn clean_up_file(path: impl AsRef<Path>, name: &str, force: bool) -> Result<()> {
|
||||
let path = path.as_ref().join(name);
|
||||
if force || path.exists() {
|
||||
tokio::fs::remove_file(path)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
|
||||
tokio::fs::remove_file(path).await
|
||||
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -242,6 +283,7 @@ impl PackageStore {
|
||||
Self::clean_up_file(&path, "icon.png", true).await?;
|
||||
Self::clean_up_file(&path, "manifest.json", true).await?;
|
||||
Self::clean_up_file(&path, "README.md", true).await?;
|
||||
Self::clean_up_file(&path, "post_load.ps1", false).await?;
|
||||
|
||||
tokio::fs::remove_dir(path.as_ref())
|
||||
.await
|
||||
|
@ -1,15 +1,19 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use ongeki::OngekiProfile;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
||||
use crate::{model::misc::Game, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
use crate::{model::{misc::Game, profile::Aime}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
use tauri::Emitter;
|
||||
use std::process::Stdio;
|
||||
use crate::model::profile::BepInEx;
|
||||
use crate::model::{profile::{Display, DisplayMode, Network, Segatools}, segatools_base::segatools_base};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::fs::File;
|
||||
use tokio::process::Command;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
pub mod ongeki;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub enum AnyProfile {
|
||||
OngekiProfile(OngekiProfile)
|
||||
pub trait ProfilePaths {
|
||||
fn config_dir(&self) -> PathBuf;
|
||||
fn data_dir(&self) -> PathBuf;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
@ -18,147 +22,311 @@ pub struct ProfileMeta {
|
||||
pub name: String
|
||||
}
|
||||
|
||||
pub trait Profile: Sized {
|
||||
fn new(name: String) -> Result<Self>;
|
||||
fn load(name: String) -> Result<Self>;
|
||||
fn save(&self) -> Result<()>;
|
||||
async fn start(&self, app: AppHandle) -> Result<()>;
|
||||
impl ProfilePaths for ProfileMeta {
|
||||
fn config_dir(&self) -> PathBuf {
|
||||
util::profile_config_dir(self.game, &self.name)
|
||||
}
|
||||
|
||||
fn data_dir(&self) -> PathBuf {
|
||||
util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ProfilePaths {
|
||||
fn config_dir(&self) -> PathBuf;
|
||||
fn data_dir(&self) -> PathBuf;
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Profile {
|
||||
pub meta: ProfileMeta,
|
||||
pub data: ProfileData,
|
||||
}
|
||||
|
||||
impl AnyProfile {
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct ProfileData {
|
||||
pub mods: BTreeSet<PkgKey>,
|
||||
pub sgt: Segatools,
|
||||
pub display: Option<Display>,
|
||||
pub network: Network,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bepinex: Option<BepInEx>,
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub wine: crate::model::profile::Wine,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn new(mut meta: ProfileMeta) -> Result<Self> {
|
||||
meta.name = fixed_name(&meta, true);
|
||||
log::debug!("created profile-{:?}", &meta);
|
||||
let p = Profile {
|
||||
data: ProfileData {
|
||||
mods: BTreeSet::new(),
|
||||
sgt: Segatools::default_for(meta.game),
|
||||
#[cfg(target_os = "windows")]
|
||||
display: if meta.game == Game::Ongeki { Some(Display::default_for(meta.game)) } else { None },
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
display: None,
|
||||
network: Network::default(),
|
||||
bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None },
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
wine: crate::model::profile::Wine::default(),
|
||||
},
|
||||
meta: meta.clone()
|
||||
};
|
||||
p.save()?;
|
||||
std::fs::create_dir_all(p.config_dir())?;
|
||||
std::fs::create_dir_all(p.data_dir())?;
|
||||
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base(meta.game))?;
|
||||
|
||||
Ok(p)
|
||||
}
|
||||
pub fn load(game: Game, name: String) -> Result<Self> {
|
||||
Ok(match game {
|
||||
Game::Ongeki => AnyProfile::OngekiProfile(OngekiProfile::load(name)?),
|
||||
Game::Chunithm => panic!("Not implemented")
|
||||
})
|
||||
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)
|
||||
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
|
||||
|
||||
log::debug!("{:?}", data);
|
||||
|
||||
Ok(Profile {
|
||||
meta: ProfileMeta {
|
||||
game, name
|
||||
},
|
||||
data
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("Unable to open {:?}", path))
|
||||
}
|
||||
}
|
||||
pub fn save(&self) -> Result<()> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => p.save()
|
||||
}
|
||||
}
|
||||
pub fn meta(&self) -> ProfileMeta {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => {
|
||||
ProfileMeta {
|
||||
game: Game::Ongeki,
|
||||
name: p.name.as_ref().unwrap().clone()
|
||||
}
|
||||
}
|
||||
let path = self.config_dir().join("profile.json");
|
||||
|
||||
let s = serde_json::to_string_pretty(&self.data)?;
|
||||
if !self.config_dir().exists() {
|
||||
std::fs::create_dir(self.config_dir())
|
||||
.map_err(|e| anyhow!("error when creating profile directory: {}", e))?;
|
||||
}
|
||||
std::fs::write(&path, s)
|
||||
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
|
||||
log::info!("Written to {:?}", path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn rename(&mut self, name: String) {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => {
|
||||
p.name = Some(fixed_name(&ProfileMeta { name, game: Game::Ongeki }, false));
|
||||
}
|
||||
}
|
||||
self.meta.name = fixed_name(&ProfileMeta { game: self.meta.game, name}, false);
|
||||
}
|
||||
pub fn mod_pkgs(&self) -> &BTreeSet<PkgKey> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => &p.mods
|
||||
}
|
||||
&self.data.mods
|
||||
}
|
||||
pub fn mod_pkgs_mut(&mut self) -> &mut BTreeSet<PkgKey> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => &mut p.mods
|
||||
}
|
||||
&mut self.data.mods
|
||||
}
|
||||
pub fn special_pkgs(&self) -> Vec<PkgKey> {
|
||||
let mut res = Vec::new();
|
||||
match self {
|
||||
Self::OngekiProfile(p) => {
|
||||
if let Some(hook) = &p.sgt.hook {
|
||||
res.push(hook.clone());
|
||||
}
|
||||
if let Some(io) = &p.sgt.io {
|
||||
res.push(io.clone());
|
||||
}
|
||||
}
|
||||
if let Some(hook) = &self.data.sgt.hook {
|
||||
res.push(hook.clone());
|
||||
}
|
||||
if let Some(io) = &self.data.sgt.io {
|
||||
res.push(io.clone());
|
||||
}
|
||||
if let Aime::AMNet(aime) = &self.data.sgt.aime {
|
||||
res.push(aime.clone());
|
||||
} else if let Aime::Other(aime) = &self.data.sgt.aime {
|
||||
res.push(aime.clone());
|
||||
}
|
||||
res
|
||||
}
|
||||
pub fn fix(&mut self, store: &PackageStore) {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => p.sgt.fix(store)
|
||||
}
|
||||
self.data.sgt.fix(store);
|
||||
}
|
||||
pub fn sync(&mut self, source: AnyProfile) {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => {
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let AnyProfile::OngekiProfile(source) = source {
|
||||
p.bepinex = source.bepinex;
|
||||
p.display = source.display;
|
||||
p.network = source.network;
|
||||
p.sgt = source.sgt;
|
||||
} else {
|
||||
log::error!("sync: invalid profile type {:?}", source);
|
||||
}
|
||||
}
|
||||
pub fn sync(&mut self, source: ProfileData) {
|
||||
if self.data.bepinex.is_some() {
|
||||
self.data.bepinex = source.bepinex;
|
||||
}
|
||||
if self.data.display.is_some() {
|
||||
self.data.display = source.display;
|
||||
}
|
||||
// if self.data.network.is_some() {
|
||||
self.data.network = source.network;
|
||||
// }
|
||||
// if self.data.sgt.is_some() {
|
||||
self.data.sgt = source.sgt;
|
||||
// }
|
||||
}
|
||||
pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> {
|
||||
match self {
|
||||
Self::OngekiProfile(_p) => {
|
||||
#[cfg(target_os = "windows")]
|
||||
let info = _p.display.line_up()?;
|
||||
let info = match &self.data.display {
|
||||
None => None,
|
||||
Some(display) => display.line_up()?
|
||||
};
|
||||
|
||||
let res = self.line_up_the_rest(pkg_hash).await;
|
||||
let res = self.line_up_the_rest(pkg_hash).await;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(info) = info {
|
||||
use crate::model::profile::Display;
|
||||
if res.is_ok() {
|
||||
Display::wait_for_exit(_app, info);
|
||||
} else {
|
||||
Display::clean_up(&info)?;
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(info) = info {
|
||||
use crate::model::profile::Display;
|
||||
if res.is_ok() {
|
||||
Display::wait_for_exit(_app, info);
|
||||
} else {
|
||||
Display::clean_up(&info)?;
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
async fn line_up_the_rest(&self, pkg_hash: String) -> Result<()> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => {
|
||||
if !p.data_dir().exists() {
|
||||
tokio::fs::create_dir(p.data_dir()).await?;
|
||||
}
|
||||
|
||||
let hash_path = p.data_dir().join(".sl-state");
|
||||
let meta = self.meta();
|
||||
|
||||
util::clean_up_opts(p.data_dir().join("option"))?;
|
||||
|
||||
if Self::hash_check(&hash_path, &pkg_hash).await? == true {
|
||||
prepare_packages(&meta, &p.mods).await
|
||||
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
|
||||
}
|
||||
let mut ini = p.sgt.line_up(&meta).await
|
||||
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
|
||||
p.network.line_up(&mut ini)?;
|
||||
|
||||
ini.write_to_file(p.data_dir().join("segatools.ini"))
|
||||
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
|
||||
|
||||
p.bepinex.line_up(&meta)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
if !self.data_dir().exists() {
|
||||
tokio::fs::create_dir(self.data_dir()).await?;
|
||||
}
|
||||
|
||||
let hash_path = self.data_dir().join(".sl-state");
|
||||
|
||||
util::clean_up_opts(self.data_dir().join("option"))?;
|
||||
|
||||
let hash_check = Self::hash_check(&hash_path, &pkg_hash).await?;
|
||||
prepare_packages(&self.meta, &self.data.mods, hash_check).await
|
||||
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
|
||||
let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await
|
||||
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
|
||||
self.data.network.line_up(&mut ini)?;
|
||||
|
||||
ini.write_to_file(self.data_dir().join("segatools.ini"))
|
||||
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
|
||||
|
||||
if let Some(bepinex) = &self.data.bepinex {
|
||||
bepinex.line_up(&self.meta)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(&self, app: AppHandle) -> Result<()> {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => p.start(app).await
|
||||
let ini_path = self.data_dir().join("segatools.ini");
|
||||
|
||||
log::debug!("With path {:?}", ini_path);
|
||||
|
||||
let mut game_builder;
|
||||
let mut amd_builder;
|
||||
|
||||
let target_path = PathBuf::from(&self.data.sgt.target);
|
||||
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
||||
let sgt_dir = self.data.sgt.hook_dir()?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
game_builder = Command::new(sgt_dir.join(self.meta.game.inject_exe()));
|
||||
amd_builder = Command::new("cmd.exe");
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
game_builder = Command::new(&self.wine.runtime);
|
||||
amd_builder = Command::new(&self.wine.runtime);
|
||||
|
||||
game_builder.arg(sgt_dir.join(self.meta.game.inject_exe()));
|
||||
amd_builder.arg("cmd.exe");
|
||||
}
|
||||
|
||||
amd_builder.env(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
&ini_path,
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.arg("/C")
|
||||
.arg(&sgt_dir.join(self.meta.game.inject_amd()))
|
||||
.args(["-d", "-k"])
|
||||
.arg(sgt_dir.join(self.meta.game.hook_amd()))
|
||||
.arg("amdaemon.exe")
|
||||
.args(self.meta.game.amd_args());
|
||||
game_builder
|
||||
.env(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
ini_path,
|
||||
)
|
||||
.env(
|
||||
"INOHARA_CONFIG_PATH",
|
||||
self.config_dir().join("inohara.cfg"),
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.args(["-d", "-k"])
|
||||
.arg(sgt_dir.join(self.meta.game.hook_exe()))
|
||||
.arg(self.meta.game.exe());
|
||||
|
||||
if let Some(display) = &self.data.display {
|
||||
game_builder.args([
|
||||
"-monitor 1",
|
||||
"-screen-width", &display.rez.0.to_string(),
|
||||
"-screen-height", &display.rez.1.to_string(),
|
||||
"-screen-fullscreen", if display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
|
||||
]);
|
||||
if display.mode == DisplayMode::Borderless {
|
||||
game_builder.arg("-popupwindow");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
amd_builder.env("WINEPREFIX", &self.wine.prefix);
|
||||
game_builder.env("WINEPREFIX", &self.wine.prefix);
|
||||
}
|
||||
|
||||
let amd_log = File::create(self.data_dir().join("amdaemon.exe.log"))?;
|
||||
let game_log = File::create(self.data_dir().join(format!("{}.log", self.meta.game.exe())))?;
|
||||
|
||||
amd_builder
|
||||
.stdout(Stdio::from(amd_log));
|
||||
// do they use stderr?
|
||||
|
||||
game_builder
|
||||
.stdout(Stdio::from(game_log));
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
amd_builder.creation_flags(util::CREATE_NO_WINDOW);
|
||||
game_builder.creation_flags(util::CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
if self.data.sgt.intel == true {
|
||||
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
|
||||
}
|
||||
|
||||
util::pkill("amdaemon.exe").await;
|
||||
|
||||
log::info!("Launching amdaemon: {:?}", amd_builder);
|
||||
log::info!("Launching {}: {:?}", self.meta.game, game_builder);
|
||||
|
||||
let mut amd = amd_builder.spawn()?;
|
||||
let mut game = game_builder.spawn()?;
|
||||
|
||||
let mut set = JoinSet::new();
|
||||
|
||||
set.spawn(async move {
|
||||
(amd.wait().await.expect("amdaemon failed to run"), "amdaemon")
|
||||
});
|
||||
|
||||
set.spawn(async move {
|
||||
(game.wait().await.expect("game failed to run"), "game")
|
||||
});
|
||||
|
||||
if let Err(e) = app.emit("launch-start", "") {
|
||||
log::warn!("Unable to emit launch-start: {}", e);
|
||||
}
|
||||
|
||||
let (rc, process) = set.join_next().await.expect("No spawn").expect("No result");
|
||||
|
||||
log::info!("{} died with return code {}", process, rc);
|
||||
|
||||
if process == "amdaemon" {
|
||||
util::pkill(self.meta.game.exe()).await;
|
||||
} else {
|
||||
util::pkill("amdaemon.exe").await;
|
||||
}
|
||||
|
||||
set.join_next().await.expect("No spawn").expect("No result");
|
||||
|
||||
log::debug!("Fin");
|
||||
|
||||
if let Err(e) = app.emit("launch-end", "") {
|
||||
log::warn!("Unable to emit launch-end: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn hash_check(prev_hash_path: &impl AsRef<Path>, new_hash: &str) -> Result<bool> {
|
||||
@ -174,11 +342,19 @@ impl AnyProfile {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AnyProfile {
|
||||
impl ProfilePaths for Profile {
|
||||
fn config_dir(&self) -> PathBuf {
|
||||
self.meta.config_dir()
|
||||
}
|
||||
|
||||
fn data_dir(&self) -> PathBuf {
|
||||
self.meta.data_dir()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Profile {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::OngekiProfile(p) => f.debug_tuple("ongeki").field(&p.name).finish(),
|
||||
}
|
||||
f.debug_tuple(&self.meta.game.to_string()).field(&self.meta.name).finish()
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,7 +403,7 @@ pub fn fixed_name(meta: &ProfileMeta, prepend_new: bool) -> String {
|
||||
.replace(" ", "-")
|
||||
.replace("..", "").replace("/", "").replace("\\", "");
|
||||
|
||||
while prepend_new && util::profile_config_dir(&meta.game, &name).exists() {
|
||||
while prepend_new && util::profile_config_dir(meta.game, &name).exists() {
|
||||
name = format!("new-{}", name);
|
||||
}
|
||||
|
||||
|
@ -1,229 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use tauri::Emitter;
|
||||
use std::{collections::BTreeSet, path::PathBuf, process::Stdio};
|
||||
use crate::model::profile::BepInEx;
|
||||
use crate::profiles::fixed_name;
|
||||
use crate::{model::{profile::{Display, DisplayMode, Network, Segatools}, misc::Game, segatools_base::segatools_base}, pkg::PkgKey, util};
|
||||
use super::{Profile, ProfileMeta, ProfilePaths};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::fs::File;
|
||||
use tokio::process::Command;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct OngekiProfile {
|
||||
// this is an Option only to deceive serde
|
||||
// file serialization doesn't need the name, but IPC does
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
pub mods: BTreeSet<PkgKey>,
|
||||
pub sgt: Segatools,
|
||||
pub display: Display,
|
||||
pub network: Network,
|
||||
pub bepinex: BepInEx,
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub wine: crate::model::profile::Wine,
|
||||
}
|
||||
|
||||
impl Profile for OngekiProfile {
|
||||
fn new(name: String) -> Result<Self> {
|
||||
let name = fixed_name(&ProfileMeta { name, game: Game::Ongeki }, true);
|
||||
|
||||
let p = OngekiProfile {
|
||||
name: Some(name.clone()),
|
||||
mods: BTreeSet::new(),
|
||||
sgt: Segatools::default(),
|
||||
display: Display::default(),
|
||||
network: Network::default(),
|
||||
bepinex: BepInEx::default(),
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
wine: crate::model::profile::Wine::default(),
|
||||
};
|
||||
p.save()?;
|
||||
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base())?;
|
||||
log::debug!("created profile-ongeki-{}", &name);
|
||||
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
fn load(name: String) -> Result<Self> {
|
||||
let path = util::profile_config_dir(&Game::Ongeki, &name).join("profile.json");
|
||||
if let Ok(s) = std::fs::read_to_string(&path) {
|
||||
let mut data = serde_json::from_str::<OngekiProfile>(&s)
|
||||
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
|
||||
data.name = Some(name);
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(anyhow!("Unable to open {:?}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&self) -> Result<()> {
|
||||
let path = self.config_dir().join("profile.json");
|
||||
|
||||
let mut cpy = self.clone();
|
||||
cpy.name = None;
|
||||
let s = serde_json::to_string_pretty(&cpy)?;
|
||||
if !self.config_dir().exists() {
|
||||
std::fs::create_dir(self.config_dir())
|
||||
.map_err(|e| anyhow!("error when creating profile directory: {}", e))?;
|
||||
}
|
||||
std::fs::write(&path, s)
|
||||
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
|
||||
log::info!("Written to {:?}", path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start(&self, app: AppHandle) -> Result<()> {
|
||||
let ini_path = self.data_dir().join("segatools.ini");
|
||||
|
||||
log::debug!("With path {:?}", ini_path);
|
||||
|
||||
let mut game_builder;
|
||||
let mut amd_builder;
|
||||
|
||||
let target_path = PathBuf::from(&self.sgt.target);
|
||||
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
||||
let sgt_dir = self.sgt.hook_dir()?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
game_builder = Command::new(sgt_dir.join("inject.exe"));
|
||||
amd_builder = Command::new("cmd.exe");
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
game_builder = Command::new(&self.wine.runtime);
|
||||
amd_builder = Command::new(&self.wine.runtime);
|
||||
|
||||
game_builder.arg(sgt_dir.join("inject.exe"));
|
||||
amd_builder.arg("cmd.exe");
|
||||
}
|
||||
|
||||
amd_builder.env(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
&ini_path,
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.arg("/C")
|
||||
.arg(&sgt_dir.join("inject.exe"))
|
||||
.args(["-d", "-k"])
|
||||
.arg(sgt_dir.join("mu3hook.dll"))
|
||||
.args(["amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
||||
]);
|
||||
game_builder
|
||||
.env(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
ini_path,
|
||||
)
|
||||
.env(
|
||||
"INOHARA_CONFIG_PATH",
|
||||
self.config_dir().join("inohara.cfg"),
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.args(["-d", "-k"])
|
||||
.arg(sgt_dir.join("mu3hook.dll"))
|
||||
.args([
|
||||
"mu3.exe", "-monitor 1",
|
||||
"-screen-width", &self.display.rez.0.to_string(),
|
||||
"-screen-height", &self.display.rez.1.to_string(),
|
||||
"-screen-fullscreen", if self.display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
|
||||
]);
|
||||
|
||||
if self.display.mode == DisplayMode::Borderless {
|
||||
game_builder.arg("-popupwindow");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
amd_builder.env("WINEPREFIX", &self.wine.prefix);
|
||||
game_builder.env("WINEPREFIX", &self.wine.prefix);
|
||||
}
|
||||
|
||||
let amd_log = File::create(self.data_dir().join("amdaemon.log"))?;
|
||||
let game_log = File::create(self.data_dir().join("mu3.log"))?;
|
||||
|
||||
amd_builder
|
||||
.stdout(Stdio::from(amd_log));
|
||||
// do they use stderr?
|
||||
|
||||
game_builder
|
||||
.stdout(Stdio::from(game_log));
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
amd_builder.creation_flags(util::CREATE_NO_WINDOW);
|
||||
game_builder.creation_flags(util::CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
if self.sgt.intel == true {
|
||||
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
|
||||
}
|
||||
|
||||
util::pkill("amdaemon.exe").await;
|
||||
|
||||
log::info!("Launching amdaemon: {:?}", amd_builder);
|
||||
log::info!("Launching mu3: {:?}", game_builder);
|
||||
|
||||
let mut amd = amd_builder.spawn()?;
|
||||
let mut game = game_builder.spawn()?;
|
||||
|
||||
let mut set = JoinSet::new();
|
||||
|
||||
set.spawn(async move {
|
||||
(amd.wait().await.expect("amdaemon failed to run"), "amdaemon.exe")
|
||||
});
|
||||
|
||||
set.spawn(async move {
|
||||
(game.wait().await.expect("mu3 failed to run"), "mu3.exe")
|
||||
});
|
||||
|
||||
if let Err(e) = app.emit("launch-start", "") {
|
||||
log::warn!("Unable to emit launch-start: {}", e);
|
||||
}
|
||||
|
||||
let (rc, process_name) = set.join_next().await.expect("No spawn").expect("No result");
|
||||
|
||||
log::info!("{} died with return code {}", process_name, rc);
|
||||
|
||||
if process_name == "amdaemon.exe" {
|
||||
util::pkill("mu3.exe").await;
|
||||
} else {
|
||||
util::pkill("amdaemon.exe").await;
|
||||
}
|
||||
|
||||
set.join_next().await.expect("No spawn").expect("No result");
|
||||
|
||||
log::debug!("Fin");
|
||||
|
||||
if let Err(e) = app.emit("launch-end", "") {
|
||||
log::warn!("Unable to emit launch-end: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfilePaths for OngekiProfile {
|
||||
fn config_dir(&self) -> PathBuf {
|
||||
util::profile_config_dir(&Game::Ongeki, &self.name.as_ref().unwrap())
|
||||
}
|
||||
|
||||
fn data_dir(&self) -> PathBuf {
|
||||
util::data_dir().join(format!("profile-{}-{}", &Game::Ongeki, self.name.as_ref().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfilePaths for ProfileMeta {
|
||||
fn config_dir(&self) -> PathBuf {
|
||||
util::profile_config_dir(&self.game, &self.name)
|
||||
}
|
||||
|
||||
fn data_dir(&self) -> PathBuf {
|
||||
util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name))
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ pub fn config_dir() -> &'static Path {
|
||||
&DIRS.get().expect("Directories uninitialized").config_dir
|
||||
}
|
||||
|
||||
pub fn profile_config_dir(game: &Game, name: &str) -> PathBuf {
|
||||
pub fn profile_config_dir(game: Game, name: &str) -> PathBuf {
|
||||
config_dir().join(format!("profile-{}-{}", game, name))
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ import StartButton from './StartButton.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { useGeneralStore, usePkgStore, usePrfStore } from '../stores';
|
||||
import { Dirs } from '../types';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
const pkg = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
@ -27,6 +28,7 @@ const currentTab: Ref<string | number> = ref(3);
|
||||
const pkgSearchTerm = ref('');
|
||||
|
||||
const isProfileDisabled = computed(() => prf.current === null);
|
||||
const isRunning = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
invoke('list_directories').then((d) => {
|
||||
@ -47,8 +49,23 @@ onMounted(async () => {
|
||||
key: 'segatools-mu3hook',
|
||||
force: false,
|
||||
});
|
||||
await invoke('install_package', {
|
||||
key: 'segatools-chusanhook',
|
||||
force: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
listen('launch-start', () => {
|
||||
isRunning.value = true;
|
||||
currentTab.value = 5;
|
||||
});
|
||||
|
||||
listen('launch-end', () => {
|
||||
isRunning.value = false;
|
||||
currentTab.value = 0;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -61,20 +78,27 @@ onMounted(async () => {
|
||||
>
|
||||
<div class="fixed w-full flex z-100">
|
||||
<TabList class="grow">
|
||||
<Tab :disabled="isProfileDisabled" :value="0"
|
||||
><div class="pi pi-list-check"></div
|
||||
<Tab :value="3" :disabled="isRunning"
|
||||
><div class="pi pi-question-circle"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isProfileDisabled || isRunning" :value="0"
|
||||
><div class="pi pi-box"></div
|
||||
></Tab>
|
||||
<Tab v-if="prf.current?.meta.game === 'chunithm'" :disabled="isRunning" :value="4"
|
||||
><div class="pi pi-ticket"></div
|
||||
></Tab>
|
||||
<Tab
|
||||
v-if="pkg.networkStatus === 'online'"
|
||||
:disabled="isProfileDisabled"
|
||||
:disabled="isProfileDisabled || isRunning"
|
||||
:value="1"
|
||||
><div class="pi pi-download"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isProfileDisabled" :value="2"
|
||||
<Tab :disabled="isProfileDisabled || isRunning" :value="2"
|
||||
><div class="pi pi-cog"></div
|
||||
></Tab>
|
||||
<Tab :value="3"
|
||||
><div class="pi pi-question-circle"></div
|
||||
|
||||
<Tab :value="5" v-if="isRunning"
|
||||
><div class="pi pi-sparkles"></div
|
||||
></Tab>
|
||||
<div class="grow"></div>
|
||||
<div class="flex gap-4">
|
||||
@ -131,16 +155,6 @@ onMounted(async () => {
|
||||
missing.<br />Existing features are expected to break
|
||||
sometimes.
|
||||
<ProfileList />
|
||||
<img
|
||||
v-if="prf.current?.game === 'ongeki'"
|
||||
src="/sticker-ongeki.svg"
|
||||
class="fixed bottom-0 right-0 z-999"
|
||||
/>
|
||||
<img
|
||||
v-else-if="prf.current?.game === 'chunithm'"
|
||||
src="/sticker-chunithm.svg"
|
||||
class="fixed bottom-0 right-0 z-999"
|
||||
/>
|
||||
<br /><br /><br />
|
||||
<footer>
|
||||
<Button
|
||||
@ -151,7 +165,24 @@ onMounted(async () => {
|
||||
/>
|
||||
</footer>
|
||||
</TabPanel>
|
||||
<TabPanel :value="4">
|
||||
CHUNITHM patches are not yet implemented.<br />Use
|
||||
<a href=https://patcher.two-torial.xyz/ target="_blank" style="text-decoration: underline;">patcher.two-torial.xyz</a>
|
||||
</TabPanel>
|
||||
<TabPanel :value="5">Running!</TabPanel>
|
||||
</TabPanels>
|
||||
<div v-if="currentTab === 5 || currentTab === 3">
|
||||
<img
|
||||
v-if="prf.current?.meta.game === 'ongeki'"
|
||||
src="/sticker-ongeki.svg"
|
||||
class="fixed bottom-0 right-0 z-999"
|
||||
/>
|
||||
<img
|
||||
v-else-if="prf.current?.meta.game === 'chunithm'"
|
||||
src="/sticker-chunithm.svg"
|
||||
class="fixed bottom-0 right-0 z-999"
|
||||
/>
|
||||
</div>
|
||||
</Tabs>
|
||||
</main>
|
||||
</template>
|
||||
|
@ -1,11 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import Fieldset from 'primevue/fieldset';
|
||||
import ModListEntry from './ModListEntry.vue';
|
||||
import ModTitlecard from './ModTitlecard.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore, usePrfStore } from '../stores';
|
||||
import { Package } from '../types';
|
||||
import { pkgKey } from '../util';
|
||||
|
||||
const props = defineProps({
|
||||
search: String,
|
||||
@ -14,12 +16,20 @@ const props = defineProps({
|
||||
const pkgs = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
const empty = ref(true);
|
||||
const gameSublist: Ref<string[]> = ref([]);
|
||||
|
||||
const group = () => {
|
||||
const a = Object.assign(
|
||||
invoke('get_game_packages', {
|
||||
game: prf.current?.meta.game,
|
||||
}).then((list) => {
|
||||
gameSublist.value = list as string[];
|
||||
});
|
||||
|
||||
const group = computed(() => {
|
||||
const res = Object.assign(
|
||||
{},
|
||||
Object.groupBy(
|
||||
pkgs.allLocal
|
||||
.filter((p) => gameSublist.value.includes(pkgKey(p)))
|
||||
.filter(
|
||||
(p) =>
|
||||
props.search === undefined ||
|
||||
@ -35,12 +45,12 @@ const group = () => {
|
||||
({ namespace }) => namespace
|
||||
)
|
||||
);
|
||||
empty.value = Object.keys(a).length === 0;
|
||||
return a;
|
||||
};
|
||||
empty.value = Object.keys(res).length === 0;
|
||||
return res;
|
||||
});
|
||||
|
||||
const missing = computed(() => {
|
||||
return prf.current?.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
|
||||
return prf.current?.data.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -68,7 +78,7 @@ const missing = computed(() => {
|
||||
/>
|
||||
</div>
|
||||
</Fieldset>
|
||||
<Fieldset v-for="(namespace, key) in group()" :legend="key.toString()">
|
||||
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()">
|
||||
<ModListEntry v-for="p in namespace" :pkg="p" />
|
||||
</Fieldset>
|
||||
<div v-if="empty" class="text-3xl">∅</div>
|
||||
|
@ -32,7 +32,6 @@ const model = computed({
|
||||
<div class="flex items-center">
|
||||
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
|
||||
<UpdateButton :pkg="pkg" />
|
||||
<!-- @vue-expect-error Can't 'as any' because it breaks VSCode -->
|
||||
<ToggleSwitch
|
||||
v-if="hasFeature(pkg, Feature.Mod)"
|
||||
class="scale-[1.33] shrink-0"
|
||||
|
@ -1,20 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Ref, ref } from 'vue';
|
||||
import Divider from 'primevue/divider';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import ModStoreEntry from './ModStoreEntry.vue';
|
||||
import { usePkgStore } from '../stores';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore, usePrfStore } from '../stores';
|
||||
import { pkgKey } from '../util';
|
||||
|
||||
const pkgs = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
const empty = ref(true);
|
||||
|
||||
const props = defineProps({
|
||||
search: String,
|
||||
});
|
||||
|
||||
const gameSublist: Ref<string[]> = ref([]);
|
||||
|
||||
invoke('get_game_packages', {
|
||||
game: prf.current?.meta.game,
|
||||
}).then((list) => {
|
||||
gameSublist.value = list as string[];
|
||||
});
|
||||
|
||||
const list = () => {
|
||||
const res = pkgs.allRemote
|
||||
.filter((p) => gameSublist.value.includes(pkgKey(p)))
|
||||
.filter(
|
||||
(p) =>
|
||||
props.search === undefined ||
|
||||
|
@ -53,13 +53,16 @@ const iconSrc = computed(() => {
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
v-if="hasFeature(pkg, Feature.Hook)"
|
||||
v-if="
|
||||
hasFeature(pkg, Feature.ChusanHook) ||
|
||||
hasFeature(pkg, Feature.Mu3Hook)
|
||||
"
|
||||
v-tooltip="'Hook'"
|
||||
class="pi pi-wrench ml-1 text-blue-400"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
v-if="hasFeature(pkg, Feature.GameIO)"
|
||||
v-if="hasFeature(pkg, Feature.Mu3IO)"
|
||||
v-tooltip="'IO'"
|
||||
class="pi pi-wrench ml-1 text-green-400"
|
||||
>
|
||||
|
@ -1,399 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import FileEditor from './FileEditor.vue';
|
||||
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 { hasFeature, pkgKey } from '../util';
|
||||
import AimeOptions from './options/Aime.vue';
|
||||
import DisplayOptions from './options/Display.vue';
|
||||
import MiscOptions from './options/Misc.vue';
|
||||
import NetworkOptions from './options/Network.vue';
|
||||
import SegatoolsOptions from './options/Segatools.vue';
|
||||
import { usePrfStore } from '../stores';
|
||||
|
||||
const pkgs = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
|
||||
const aimeCode = ref('');
|
||||
const capabilities: Ref<string[]> = ref([]);
|
||||
|
||||
const displayList: Ref<{ title: string; value: string }[]> = ref([
|
||||
{
|
||||
title: 'Primary',
|
||||
value: 'default',
|
||||
},
|
||||
]);
|
||||
|
||||
const loadDisplays = () => {
|
||||
const newList = [
|
||||
{
|
||||
title: 'Primary',
|
||||
value: 'default',
|
||||
},
|
||||
];
|
||||
invoke('list_platform_capabilities')
|
||||
.then(async (v: unknown) => {
|
||||
let different = false;
|
||||
if (Array.isArray(v)) {
|
||||
capabilities.value.push(...v);
|
||||
}
|
||||
if (capabilities.value.includes('display')) {
|
||||
for (const [devName, devString] of (await invoke(
|
||||
'list_displays'
|
||||
)) as Array<[string, string]>) {
|
||||
newList.push({
|
||||
title: `${devName.replace('\\\\.\\', '')} (${devString})`,
|
||||
value: devName,
|
||||
});
|
||||
if (
|
||||
displayList.value.find(
|
||||
(item) => item.value === devName
|
||||
) === undefined
|
||||
) {
|
||||
different = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (displayList.value.length !== newList.length) {
|
||||
different = true;
|
||||
}
|
||||
if (different) {
|
||||
displayList.value = newList;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
loadDisplays();
|
||||
prf.reload();
|
||||
|
||||
const aimeCodeModel = computed({
|
||||
get() {
|
||||
return aimeCode.value;
|
||||
},
|
||||
async set(value: string) {
|
||||
aimeCode.value = value;
|
||||
if (value.match(/^[0-9]{20}$/) || value.length === 0) {
|
||||
const aime_path = await path.join(await prf.configDir, 'aime.txt');
|
||||
await writeTextFile(aime_path, aimeCode.value);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const extraDisplayOptionsDisabled = computed(() => {
|
||||
return prf.current?.display.target === 'default';
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const aime_path = await path.join(await prf.configDir, 'aime.txt');
|
||||
aimeCode.value = await readTextFile(aime_path).catch(() => '');
|
||||
})();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="General">
|
||||
<OptionRow title="mu3.exe">
|
||||
<FilePicker
|
||||
:directory="false"
|
||||
promptname="mu3.exe"
|
||||
extension="exe"
|
||||
:value="prf.current!.sgt.target"
|
||||
:callback="(value: string) => (prf.current!.sgt.target = value)"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
|
||||
<OptionRow title="amfs">
|
||||
<FilePicker
|
||||
:directory="true"
|
||||
placeholder="amfs"
|
||||
:value="prf.current!.sgt.amfs"
|
||||
:callback="(value: string) => (prf.current!.sgt.amfs = value)"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow title="option">
|
||||
<FilePicker
|
||||
:directory="true"
|
||||
placeholder="option"
|
||||
:value="prf.current!.sgt.option"
|
||||
:callback="(value: string) => (prf.current!.sgt.option = value)"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow title="appdata">
|
||||
<FilePicker
|
||||
:directory="true"
|
||||
:value="prf.current!.sgt.appdata"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.sgt.appdata = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow title="mu3hook">
|
||||
<Select
|
||||
v-model="prf.current!.sgt.hook"
|
||||
:options="
|
||||
pkgs.hooks.map((p) => {
|
||||
return { title: pkgKey(p), value: pkgKey(p) };
|
||||
})
|
||||
"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow title="mu3io">
|
||||
<Select
|
||||
v-model="prf.current!.sgt.io"
|
||||
placeholder="segatools built-in"
|
||||
:options="[
|
||||
{ title: 'segatools built-in', value: null },
|
||||
...pkgs.gameIOs.map((p) => {
|
||||
return { title: pkgKey(p), value: pkgKey(p) };
|
||||
}),
|
||||
]"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
<OptionCategory title="Display">
|
||||
<OptionRow
|
||||
v-if="capabilities.includes('display')"
|
||||
title="Target display"
|
||||
>
|
||||
<Select
|
||||
v-model="prf.current!.display.target"
|
||||
:options="displayList"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
placeholder="(Disconnected)"
|
||||
@show="loadDisplays"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow class="number-input" title="Game resolution">
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:min="480"
|
||||
:max="9999"
|
||||
:use-grouping="false"
|
||||
v-model="prf.current!.display.rez[0]"
|
||||
/>
|
||||
x
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:min="640"
|
||||
:max="9999"
|
||||
:use-grouping="false"
|
||||
v-model="prf.current!.display.rez[1]"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="Display mode">
|
||||
<SelectButton
|
||||
v-model="prf.current!.display.mode"
|
||||
:options="[
|
||||
{ title: 'Window', value: 'Window' },
|
||||
{ title: 'Borderless window', value: 'Borderless' },
|
||||
{ title: 'Fullscreen', value: 'Fullscreen' },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
title="Display rotation"
|
||||
v-if="capabilities.includes('display')"
|
||||
>
|
||||
<SelectButton
|
||||
v-model="prf.current!.display.rotation"
|
||||
:options="[
|
||||
{ title: 'Unchanged', value: 0 },
|
||||
{ title: 'Portrait', value: 90 },
|
||||
{ title: 'Portrait (flipped)', value: 270 },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
:disabled="extraDisplayOptionsDisabled"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
v-if="capabilities.includes('display')"
|
||||
class="number-input"
|
||||
title="Refresh Rate"
|
||||
>
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:min="60"
|
||||
:max="999"
|
||||
:use-grouping="false"
|
||||
v-model="prf.current!.display.frequency"
|
||||
:disabled="extraDisplayOptionsDisabled"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
title="Borderless fullscreen"
|
||||
v-if="capabilities.includes('display')"
|
||||
tooltip="Match display resolution with the game."
|
||||
>
|
||||
<ToggleSwitch
|
||||
:disabled="
|
||||
extraDisplayOptionsDisabled ||
|
||||
prf.current?.display.mode !== 'Borderless'
|
||||
"
|
||||
v-model="prf.current!.display.borderless_fullscreen"
|
||||
/>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
<OptionCategory title="Network">
|
||||
<OptionRow title="Network type">
|
||||
<SelectButton
|
||||
v-model="prf.current!.network.network_type"
|
||||
:options="[
|
||||
{ title: 'Remote', value: 'Remote' },
|
||||
{ title: 'Local (ARTEMiS)', value: 'Artemis' },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
v-if="prf.current!.network.network_type == 'Artemis'"
|
||||
title="ARTEMiS path"
|
||||
>
|
||||
<FilePicker
|
||||
:directory="false"
|
||||
promptname="index.py"
|
||||
extension="py"
|
||||
:value="prf.current!.network.local_path"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.network.local_path = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<!-- <OptionRow
|
||||
v-if="prf.current!.network.network_type == 'Artemis'"
|
||||
title="ARTEMiS console"
|
||||
>
|
||||
<ToggleSwitch v-model="prf.current!.network.local_console" />
|
||||
</OptionRow> -->
|
||||
<OptionRow
|
||||
v-if="prf.current!.network.network_type == 'Remote'"
|
||||
title="Server address"
|
||||
>
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="40"
|
||||
placeholder="192.168.1.234"
|
||||
v-model="prf.current!.network.remote_address"
|
||||
/> </OptionRow
|
||||
><OptionRow
|
||||
v-if="prf.current!.network.network_type == 'Remote'"
|
||||
title="Keychip"
|
||||
>
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="16"
|
||||
placeholder="A123-01234567890"
|
||||
v-model="prf.current!.network.keychip"
|
||||
/> </OptionRow
|
||||
><OptionRow title="Subnet">
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="15"
|
||||
placeholder="192.168.1.0"
|
||||
v-model="prf.current!.network.subnet"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="Address suffix">
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="3"
|
||||
:min="0"
|
||||
:max="255"
|
||||
placeholder="12"
|
||||
v-model="prf.current!.network.suffix"
|
||||
/>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
<OptionCategory title="Aime">
|
||||
<OptionRow title="Aime emulation">
|
||||
<Select
|
||||
v-model="prf.current!.sgt.aime"
|
||||
:options="[
|
||||
{ title: 'none', value: 'Disabled' },
|
||||
{ title: 'segatools built-in', value: 'BuiltIn' },
|
||||
...pkgs.aimes.map((p) => {
|
||||
return {
|
||||
title: pkgKey(p),
|
||||
value: hasFeature(p, Feature.AMNet)
|
||||
? { AMNet: pkgKey(p) }
|
||||
: { Other: pkgKey(p) },
|
||||
};
|
||||
}),
|
||||
]"
|
||||
placeholder="none"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow title="Aime code">
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:disabled="prf.current!.sgt.aime === 'Disabled'"
|
||||
:maxlength="20"
|
||||
placeholder="00000000000000000000"
|
||||
v-model="aimeCodeModel"
|
||||
/>
|
||||
</OptionRow>
|
||||
<div v-if="prf.current!.sgt.aime?.hasOwnProperty('AMNet')">
|
||||
<OptionRow title="Server name">
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
placeholder="CHUNI-PENGUIN"
|
||||
:maxlength="50"
|
||||
v-model="prf.current!.sgt.amnet.name"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="Server address">
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
placeholder="http://+:6070"
|
||||
:maxlength="50"
|
||||
v-model="prf.current!.sgt.amnet.addr"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
title="Use AiMeDB for physical cards"
|
||||
tooltip="Whether physical cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, enable this option to load the same account data/profile as you would get on a physical cab."
|
||||
>
|
||||
<ToggleSwitch v-model="prf.current!.sgt.amnet.physical" />
|
||||
</OptionRow>
|
||||
</div>
|
||||
</OptionCategory>
|
||||
<OptionCategory title="Misc">
|
||||
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
|
||||
<ToggleSwitch v-model="prf.current!.sgt.intel" />
|
||||
</OptionRow>
|
||||
<OptionRow title="More segatools options">
|
||||
<FileEditor filename="segatools-base.ini" />
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
<OptionCategory title="Extensions">
|
||||
<SegatoolsOptions />
|
||||
<DisplayOptions v-if="prf.current!.meta.game === 'ongeki'" />
|
||||
<NetworkOptions />
|
||||
<AimeOptions />
|
||||
<MiscOptions />
|
||||
<OptionCategory
|
||||
title="Extensions"
|
||||
v-if="prf.current!.meta.game === 'ongeki'"
|
||||
>
|
||||
<OptionRow title="Inohara config">
|
||||
<FileEditor
|
||||
filename="inohara.cfg"
|
||||
@ -402,7 +33,8 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="BepInEx console">
|
||||
<ToggleSwitch v-model="prf.current!.bepinex.console" />
|
||||
<!-- @vue-expect-error -->
|
||||
<ToggleSwitch v-model="prf.current!.data.bepinex.console" />
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
</template>
|
||||
|
@ -19,7 +19,6 @@ const prf = usePrfStore();
|
||||
icon="pi pi-plus"
|
||||
class="chunithm-button profile-button"
|
||||
@click="() => prf.create('chunithm')"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
|
||||
|
@ -61,7 +61,8 @@ const deleteProfile = async () => {
|
||||
<div class="flex flex-row flex-wrap align-middle gap-2">
|
||||
<Button
|
||||
:disabled="
|
||||
prf.current?.game === p!.game && prf.current?.name === p!.name
|
||||
prf.current?.meta.game === p!.game &&
|
||||
prf.current?.meta.name === p!.name
|
||||
"
|
||||
:class="
|
||||
(p!.game === 'chunithm' ? 'chunithm-button' : 'ongeki-button') +
|
||||
|
@ -5,6 +5,7 @@ import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import ScrollPanel from 'primevue/scrollpanel';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePrfStore } from '../stores';
|
||||
|
||||
@ -54,17 +55,19 @@ const startline = async (force: boolean) => {
|
||||
|
||||
const kill = async () => {
|
||||
await invoke('kill');
|
||||
startStatus.value = 'ready';
|
||||
};
|
||||
|
||||
const disabledTooltip = computed(() => {
|
||||
if (prf.current?.sgt.target.length === 0) {
|
||||
if (prf.current?.data.sgt.target.length === 0) {
|
||||
return 'The game path must be specified';
|
||||
}
|
||||
if (prf.current?.sgt.amfs.length === 0) {
|
||||
if (prf.current?.data.sgt.amfs.length === 0) {
|
||||
return 'The amfs path must be specified';
|
||||
}
|
||||
if (prf.current?.sgt.hook === null || prf.current?.sgt.hook === undefined) {
|
||||
if (
|
||||
prf.current?.data.sgt.hook === null ||
|
||||
prf.current?.data.sgt.hook === undefined
|
||||
) {
|
||||
return 'A segatools hook package is necessary';
|
||||
}
|
||||
return null;
|
||||
@ -72,10 +75,13 @@ const disabledTooltip = computed(() => {
|
||||
|
||||
listen('launch-start', () => {
|
||||
startStatus.value = 'running';
|
||||
getCurrentWindow().minimize();
|
||||
});
|
||||
|
||||
listen('launch-end', () => {
|
||||
startStatus.value = 'ready';
|
||||
getCurrentWindow().unminimize();
|
||||
getCurrentWindow().setFocus();
|
||||
});
|
||||
|
||||
const messageSplit = (message: any) => {
|
||||
|
99
src/components/options/Aime.vue
Normal file
99
src/components/options/Aime.vue
Normal file
@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import OptionCategory from '../OptionCategory.vue';
|
||||
import OptionRow from '../OptionRow.vue';
|
||||
import { usePkgStore, usePrfStore } from '../../stores';
|
||||
import { Feature } from '../../types';
|
||||
import { hasFeature, pkgKey } from '../../util';
|
||||
|
||||
const pkgs = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
|
||||
const aimeCode = ref('');
|
||||
|
||||
prf.reload();
|
||||
|
||||
const aimeCodeModel = computed({
|
||||
get() {
|
||||
return aimeCode.value;
|
||||
},
|
||||
async set(value: string) {
|
||||
aimeCode.value = value;
|
||||
if (value.match(/^[0-9]{20}$/) || value.length === 0) {
|
||||
const aime_path = await path.join(await prf.configDir, 'aime.txt');
|
||||
await writeTextFile(aime_path, aimeCode.value);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const aime_path = await path.join(await prf.configDir, 'aime.txt');
|
||||
aimeCode.value = await readTextFile(aime_path).catch(() => '');
|
||||
})();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="Aime">
|
||||
<OptionRow title="Aime emulation">
|
||||
<Select
|
||||
v-model="prf.current!.data.sgt.aime"
|
||||
:options="[
|
||||
{ title: 'none', value: 'Disabled' },
|
||||
{ title: 'segatools built-in', value: 'BuiltIn' },
|
||||
...pkgs.byFeature(Feature.Aime).map((p) => {
|
||||
return {
|
||||
title: pkgKey(p),
|
||||
value: hasFeature(p, Feature.AMNet)
|
||||
? { AMNet: pkgKey(p) }
|
||||
: { Other: pkgKey(p) },
|
||||
};
|
||||
}),
|
||||
]"
|
||||
placeholder="none"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow title="Aime code">
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:disabled="prf.current!.data.sgt.aime === 'Disabled'"
|
||||
:maxlength="20"
|
||||
placeholder="00000000000000000000"
|
||||
v-model="aimeCodeModel"
|
||||
/>
|
||||
</OptionRow>
|
||||
<div v-if="prf.current!.data.sgt.aime?.hasOwnProperty('AMNet')">
|
||||
<OptionRow title="Server name">
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
placeholder="CHUNI-PENGUIN"
|
||||
:maxlength="50"
|
||||
v-model="prf.current!.data.sgt.amnet.name"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="Server address">
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
placeholder="http://+:6070"
|
||||
:maxlength="50"
|
||||
v-model="prf.current!.data.sgt.amnet.addr"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
title="Use AiMeDB for physical cards"
|
||||
tooltip="Whether physical cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, enable this option to load the same account data/profile as you would get on a physical cab."
|
||||
>
|
||||
<ToggleSwitch v-model="prf.current!.data.sgt.amnet.physical" />
|
||||
</OptionRow>
|
||||
</div>
|
||||
</OptionCategory>
|
||||
</template>
|
161
src/components/options/Display.vue
Normal file
161
src/components/options/Display.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Select from 'primevue/select';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import OptionCategory from '../OptionCategory.vue';
|
||||
import OptionRow from '../OptionRow.vue';
|
||||
import { invoke } from '../../invoke';
|
||||
import { usePrfStore } from '../../stores';
|
||||
|
||||
const capabilities: Ref<string[]> = ref([]);
|
||||
const displayList: Ref<{ title: string; value: string }[]> = ref([
|
||||
{
|
||||
title: 'Primary',
|
||||
value: 'default',
|
||||
},
|
||||
]);
|
||||
const prf = usePrfStore();
|
||||
|
||||
const extraDisplayOptionsDisabled = computed(() => {
|
||||
return prf.current?.data.display.target === 'default';
|
||||
});
|
||||
|
||||
const loadDisplays = () => {
|
||||
const newList = [
|
||||
{
|
||||
title: 'Primary',
|
||||
value: 'default',
|
||||
},
|
||||
];
|
||||
invoke('list_platform_capabilities')
|
||||
.then(async (v: unknown) => {
|
||||
let different = false;
|
||||
if (Array.isArray(v)) {
|
||||
capabilities.value.push(...v);
|
||||
}
|
||||
if (capabilities.value.includes('display')) {
|
||||
for (const [devName, devString] of (await invoke(
|
||||
'list_displays'
|
||||
)) as Array<[string, string]>) {
|
||||
newList.push({
|
||||
title: `${devName.replace('\\\\.\\', '')} (${devString})`,
|
||||
value: devName,
|
||||
});
|
||||
if (
|
||||
displayList.value.find(
|
||||
(item) => item.value === devName
|
||||
) === undefined
|
||||
) {
|
||||
different = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (displayList.value.length !== newList.length) {
|
||||
different = true;
|
||||
}
|
||||
if (different) {
|
||||
displayList.value = newList;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
loadDisplays();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="Display">
|
||||
<OptionRow
|
||||
v-if="capabilities.includes('display')"
|
||||
title="Target display"
|
||||
>
|
||||
<Select
|
||||
v-model="prf.current!.data.display.target"
|
||||
:options="displayList"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
placeholder="(Disconnected)"
|
||||
@show="loadDisplays"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow class="number-input" title="Game resolution">
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:min="480"
|
||||
:max="9999"
|
||||
:use-grouping="false"
|
||||
v-model="prf.current!.data.display.rez[0]"
|
||||
/>
|
||||
x
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:min="640"
|
||||
:max="9999"
|
||||
:use-grouping="false"
|
||||
v-model="prf.current!.data.display.rez[1]"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="Display mode">
|
||||
<SelectButton
|
||||
v-model="prf.current!.data.display.mode"
|
||||
:options="[
|
||||
{ title: 'Window', value: 'Window' },
|
||||
{ title: 'Borderless window', value: 'Borderless' },
|
||||
{ title: 'Fullscreen', value: 'Fullscreen' },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
title="Display rotation"
|
||||
v-if="capabilities.includes('display')"
|
||||
>
|
||||
<SelectButton
|
||||
v-model="prf.current!.data.display.rotation"
|
||||
:options="[
|
||||
{ title: 'Unchanged', value: 0 },
|
||||
{ title: 'Portrait', value: 90 },
|
||||
{ title: 'Portrait (flipped)', value: 270 },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
:disabled="extraDisplayOptionsDisabled"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
v-if="capabilities.includes('display')"
|
||||
class="number-input"
|
||||
title="Refresh Rate"
|
||||
>
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:min="60"
|
||||
:max="999"
|
||||
:use-grouping="false"
|
||||
v-model="prf.current!.data.display.frequency"
|
||||
:disabled="extraDisplayOptionsDisabled"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
title="Borderless fullscreen"
|
||||
v-if="capabilities.includes('display')"
|
||||
tooltip="Match display resolution with the game."
|
||||
>
|
||||
<ToggleSwitch
|
||||
:disabled="
|
||||
extraDisplayOptionsDisabled ||
|
||||
prf.current?.data.display.mode !== 'Borderless'
|
||||
"
|
||||
v-model="prf.current!.data.display.borderless_fullscreen"
|
||||
/>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
</template>
|
20
src/components/options/Misc.vue
Normal file
20
src/components/options/Misc.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import FileEditor from '../FileEditor.vue';
|
||||
import OptionCategory from '../OptionCategory.vue';
|
||||
import OptionRow from '../OptionRow.vue';
|
||||
import { usePrfStore } from '../../stores';
|
||||
|
||||
const prf = usePrfStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="Misc">
|
||||
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
|
||||
<ToggleSwitch v-model="prf.current!.data.sgt.intel" />
|
||||
</OptionRow>
|
||||
<OptionRow title="More segatools options">
|
||||
<FileEditor filename="segatools-base.ini" />
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
</template>
|
91
src/components/options/Network.vue
Normal file
91
src/components/options/Network.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
import FilePicker from '../FilePicker.vue';
|
||||
import OptionCategory from '../OptionCategory.vue';
|
||||
import OptionRow from '../OptionRow.vue';
|
||||
import { usePrfStore } from '../../stores';
|
||||
|
||||
const prf = usePrfStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="Network">
|
||||
<OptionRow title="Network type">
|
||||
<SelectButton
|
||||
v-model="prf.current!.data.network.network_type"
|
||||
:options="[
|
||||
{ title: 'Remote', value: 'Remote' },
|
||||
{ title: 'Local (ARTEMiS)', value: 'Artemis' },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
v-if="prf.current!.data.network.network_type == 'Artemis'"
|
||||
title="ARTEMiS path"
|
||||
>
|
||||
<FilePicker
|
||||
:directory="false"
|
||||
promptname="index.py"
|
||||
extension="py"
|
||||
:value="prf.current!.data.network.local_path"
|
||||
:callback="
|
||||
(value: string) =>
|
||||
(prf.current!.data.network.local_path = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<!-- <OptionRow
|
||||
v-if="prf.current!.data.network.network_type == 'Artemis'"
|
||||
title="ARTEMiS console"
|
||||
>
|
||||
<ToggleSwitch v-model="prf.current!.data.network.local_console" />
|
||||
</OptionRow> -->
|
||||
<OptionRow
|
||||
v-if="prf.current!.data.network.network_type == 'Remote'"
|
||||
title="Server address"
|
||||
>
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="40"
|
||||
placeholder="192.168.1.234"
|
||||
v-model="prf.current!.data.network.remote_address"
|
||||
/> </OptionRow
|
||||
><OptionRow
|
||||
v-if="prf.current!.data.network.network_type == 'Remote'"
|
||||
title="Keychip"
|
||||
>
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="16"
|
||||
placeholder="A123-01234567890"
|
||||
v-model="prf.current!.data.network.keychip"
|
||||
/> </OptionRow
|
||||
><OptionRow title="Subnet">
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="15"
|
||||
placeholder="192.168.1.0"
|
||||
v-model="prf.current!.data.network.subnet"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="Address suffix">
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="3"
|
||||
:min="0"
|
||||
:max="255"
|
||||
placeholder="12"
|
||||
v-model="prf.current!.data.network.suffix"
|
||||
/>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
</template>
|
112
src/components/options/Segatools.vue
Normal file
112
src/components/options/Segatools.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import Select from 'primevue/select';
|
||||
import FilePicker from '../FilePicker.vue';
|
||||
import OptionCategory from '../OptionCategory.vue';
|
||||
import OptionRow from '../OptionRow.vue';
|
||||
import { usePkgStore, usePrfStore } from '../../stores';
|
||||
import { Feature } from '../../types';
|
||||
import { pkgKey } from '../../util';
|
||||
|
||||
const prf = usePrfStore();
|
||||
const pkgs = usePkgStore();
|
||||
|
||||
const names = computed(() => {
|
||||
switch (prf.current?.meta.game) {
|
||||
case 'ongeki': {
|
||||
return {
|
||||
exe: 'mu3.exe',
|
||||
hook: 'mu3hook',
|
||||
io: 'mu3io',
|
||||
};
|
||||
}
|
||||
case 'chunithm': {
|
||||
return {
|
||||
exe: 'chusanApp.exe',
|
||||
hook: 'chusanhook',
|
||||
io: 'chuniio',
|
||||
};
|
||||
}
|
||||
case undefined:
|
||||
throw new Error('Option tab without a profile');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="General">
|
||||
<OptionRow :title="names.exe">
|
||||
<FilePicker
|
||||
:directory="false"
|
||||
:promptname="names.exe"
|
||||
extension="exe"
|
||||
:value="prf.current!.data.sgt.target"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.data.sgt.target = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
|
||||
<OptionRow title="amfs">
|
||||
<FilePicker
|
||||
:directory="true"
|
||||
placeholder="amfs"
|
||||
:value="prf.current!.data.sgt.amfs"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.data.sgt.amfs = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow title="option">
|
||||
<FilePicker
|
||||
:directory="true"
|
||||
placeholder="option"
|
||||
:value="prf.current!.data.sgt.option"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.data.sgt.option = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow title="appdata">
|
||||
<FilePicker
|
||||
:directory="true"
|
||||
:value="prf.current!.data.sgt.appdata"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.data.sgt.appdata = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow :title="names.hook">
|
||||
<Select
|
||||
v-model="prf.current!.data.sgt.hook"
|
||||
:options="
|
||||
pkgs
|
||||
.byFeature(
|
||||
prf.current?.meta.game === 'ongeki'
|
||||
? Feature.Mu3Hook
|
||||
: Feature.ChusanHook
|
||||
)
|
||||
.map((p) => {
|
||||
return { title: pkgKey(p), value: pkgKey(p) };
|
||||
})
|
||||
"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow :title="names.io" v-if="prf.current?.meta.game === 'ongeki'">
|
||||
<Select
|
||||
v-model="prf.current!.data.sgt.io"
|
||||
placeholder="segatools built-in"
|
||||
:options="[
|
||||
{ title: 'segatools built-in', value: null },
|
||||
...pkgs.byFeature(Feature.Mu3IO).map((p) => {
|
||||
return { title: pkgKey(p), value: pkgKey(p) };
|
||||
}),
|
||||
]"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
</template>
|
@ -100,14 +100,8 @@ export const usePkgStore = defineStore('pkg', {
|
||||
(c) => !state.excludeCategories.includes(c)
|
||||
))
|
||||
),
|
||||
hooks: (state) =>
|
||||
Object.values(state.pkg).filter((p) => hasFeature(p, Feature.Hook)),
|
||||
gameIOs: (state) =>
|
||||
Object.values(state.pkg).filter((p) =>
|
||||
hasFeature(p, Feature.GameIO)
|
||||
),
|
||||
aimes: (state) =>
|
||||
Object.values(state.pkg).filter((p) => hasFeature(p, Feature.Aime)),
|
||||
byFeature: (state) => (feature: Feature) =>
|
||||
Object.values(state.pkg).filter((p) => hasFeature(p, feature)),
|
||||
},
|
||||
actions: {
|
||||
setupListeners() {
|
||||
@ -189,18 +183,14 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
() =>
|
||||
pkg !== undefined &&
|
||||
current.value !== null &&
|
||||
current.value?.mods.includes(pkgKey(pkg))
|
||||
current.value?.data.mods.includes(pkgKey(pkg))
|
||||
);
|
||||
|
||||
const reload = async () => {
|
||||
const p = (await invoke('get_current_profile')) as any;
|
||||
if (p != null && 'OngekiProfile' in p) {
|
||||
current.value = { ...p.OngekiProfile, game: 'ongeki' };
|
||||
} else {
|
||||
current.value = null;
|
||||
}
|
||||
const p = (await invoke('get_current_profile')) as Profile;
|
||||
current.value = p;
|
||||
if (current.value !== null) {
|
||||
changePrimaryColor(current.value.game);
|
||||
changePrimaryColor(current.value.meta.game);
|
||||
} else {
|
||||
changePrimaryColor(null);
|
||||
}
|
||||
@ -231,10 +221,10 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
});
|
||||
|
||||
if (
|
||||
current.value?.game === profile.game &&
|
||||
current.value.name === profile.name
|
||||
current.value?.meta.game === profile.game &&
|
||||
current.value.meta.name === profile.name
|
||||
) {
|
||||
current.value.name = name;
|
||||
current.value.meta.name = name;
|
||||
}
|
||||
|
||||
await reloadList();
|
||||
@ -272,7 +262,7 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
const configDir = computed(async () => {
|
||||
return await path.join(
|
||||
generalStore.configDir,
|
||||
`profile-${current.value?.game}-${current.value?.name}`
|
||||
`profile-${current.value?.meta.game}-${current.value?.meta.name}`
|
||||
);
|
||||
});
|
||||
|
||||
@ -283,7 +273,7 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
watchEffect(async () => {
|
||||
if (current.value !== null) {
|
||||
await invoke('sync_current_profile', {
|
||||
profile: { OngekiProfile: current.value },
|
||||
data: current.value.data,
|
||||
});
|
||||
if (timeout !== null) {
|
||||
clearTimeout(timeout);
|
||||
|
28
src/types.ts
28
src/types.ts
@ -24,11 +24,12 @@ export interface Package {
|
||||
}
|
||||
|
||||
export enum Feature {
|
||||
Mod = 0b00001,
|
||||
Hook = 0b00010,
|
||||
GameIO = 0b00100,
|
||||
Aime = 0b01000,
|
||||
AMNet = 0b10000,
|
||||
Mod = 1 << 0,
|
||||
Aime = 1 << 1,
|
||||
AMNet = 1 << 2,
|
||||
Mu3Hook = 1 << 3,
|
||||
Mu3IO = 1 << 4,
|
||||
ChusanHook = 1 << 5,
|
||||
}
|
||||
|
||||
export type Status =
|
||||
@ -45,6 +46,14 @@ export interface ProfileMeta {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ProfileData {
|
||||
mods: string[];
|
||||
sgt: SegatoolsConfig;
|
||||
display: DisplayConfig;
|
||||
network: NetworkConfig;
|
||||
bepinex: BepInExConfig;
|
||||
}
|
||||
|
||||
export interface SegatoolsConfig {
|
||||
target: string;
|
||||
hook: string | null;
|
||||
@ -83,12 +92,9 @@ export interface BepInExConfig {
|
||||
console: boolean;
|
||||
}
|
||||
|
||||
export interface Profile extends ProfileMeta {
|
||||
mods: string[];
|
||||
sgt: SegatoolsConfig;
|
||||
display: DisplayConfig;
|
||||
network: NetworkConfig;
|
||||
bepinex: BepInExConfig;
|
||||
export interface Profile {
|
||||
meta: ProfileMeta;
|
||||
data: ProfileData;
|
||||
}
|
||||
|
||||
export type Module = 'sgt' | 'display' | 'network';
|
||||
|
Reference in New Issue
Block a user