feat: initial chunithm support
This commit is contained in:
@ -7,6 +7,11 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"core:window:allow-close",
|
"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",
|
"core:app:allow-app-hide",
|
||||||
"shell:default",
|
"shell:default",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
use crate::model::config::GlobalConfig;
|
use crate::model::config::GlobalConfig;
|
||||||
use crate::pkg::{Feature, Status};
|
use crate::pkg::{Feature, Status};
|
||||||
use crate::profiles::AnyProfile;
|
use crate::profiles::Profile;
|
||||||
use crate::{model::misc::Game, pkg::PkgKey};
|
use crate::{model::misc::Game, pkg::PkgKey};
|
||||||
use crate::pkg_store::PackageStore;
|
use crate::pkg_store::PackageStore;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
@ -13,7 +13,7 @@ pub struct GlobalState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
pub profile: Option<AnyProfile>,
|
pub profile: Option<Profile>,
|
||||||
pub pkgs: PackageStore,
|
pub pkgs: PackageStore,
|
||||||
pub cfg: GlobalConfig,
|
pub cfg: GlobalConfig,
|
||||||
pub state: GlobalState,
|
pub state: GlobalState,
|
||||||
@ -33,7 +33,7 @@ impl AppData {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let profile = match cfg.recent_profile {
|
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
|
None => None
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn switch_profile(&mut self, game: Game, name: String) -> Result<()> {
|
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) => {
|
Ok(profile) => {
|
||||||
self.profile = Some(profile);
|
self.profile = Some(profile);
|
||||||
self.cfg.recent_profile = Some((game, name));
|
self.cfg.recent_profile = Some((game, name));
|
||||||
@ -103,7 +103,7 @@ impl AppData {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sum_packages(&self, p: &AnyProfile) -> String {
|
pub fn sum_packages(&self, p: &Profile) -> String {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
for key in p.mod_pkgs().into_iter() {
|
for key in p.mod_pkgs().into_iter() {
|
||||||
if let Ok(pkg) = self.pkgs.get(&key) {
|
if let Ok(pkg) = self.pkgs.get(&key) {
|
||||||
|
@ -6,8 +6,7 @@ use tauri::{AppHandle, Manager, State};
|
|||||||
use crate::model::misc::Game;
|
use crate::model::misc::Game;
|
||||||
use crate::pkg::{Package, PkgKey};
|
use crate::pkg::{Package, PkgKey};
|
||||||
use crate::pkg_store::{InstallResult, PackageStore};
|
use crate::pkg_store::{InstallResult, PackageStore};
|
||||||
use crate::profiles::ongeki::OngekiProfile;
|
use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths};
|
||||||
use crate::profiles::{self, AnyProfile, Profile, ProfileMeta, ProfilePaths};
|
|
||||||
use crate::appdata::{AppData, ToggleAction};
|
use crate::appdata::{AppData, ToggleAction};
|
||||||
use crate::model::misc::StartCheckError;
|
use crate::model::misc::StartCheckError;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
@ -147,10 +146,21 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
|
|||||||
Ok(appd.pkgs.get_all())
|
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]
|
#[tauri::command]
|
||||||
pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
|
pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
|
||||||
log::debug!("invoke: fetch_listings");
|
log::debug!("invoke: fetch_listings");
|
||||||
|
|
||||||
|
// let game;
|
||||||
{
|
{
|
||||||
let appd = state.lock().await;
|
let appd = state.lock().await;
|
||||||
if !appd.pkgs.is_offline() {
|
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");
|
log::info!("fetch_listings: skipped");
|
||||||
return Err("offline mode".to_owned());
|
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())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -189,15 +212,10 @@ pub async fn init_profile(
|
|||||||
log::debug!("invoke: init_profile({}, {})", game, name);
|
log::debug!("invoke: init_profile({}, {})", game, name);
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
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))?;
|
.map_err(|e| format!("Unable to create profile: {}", e))?;
|
||||||
|
|
||||||
fs::create_dir_all(new_profile.config_dir()).await
|
appd.profile = Some(new_profile);
|
||||||
.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()));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -242,7 +260,7 @@ pub async fn rename_profile(
|
|||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
if let Some(current) = &mut appd.profile {
|
if let Some(current) = &mut appd.profile {
|
||||||
if current.meta() == profile {
|
if current.meta == profile {
|
||||||
current.rename(new_meta.name);
|
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;
|
let mut appd = state.lock().await;
|
||||||
if let Some(current) = &mut appd.profile {
|
if let Some(current) = &mut appd.profile {
|
||||||
if current.meta() == profile {
|
if current.meta == profile {
|
||||||
appd.profile = None;
|
appd.profile = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -286,7 +304,7 @@ pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMe
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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");
|
log::debug!("invoke: get_current_profile");
|
||||||
|
|
||||||
let appd = state.lock().await;
|
let appd = state.lock().await;
|
||||||
@ -294,12 +312,12 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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");
|
log::debug!("invoke: sync_current_profile");
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
if let Some(p) = &mut appd.profile {
|
if let Some(p) = &mut appd.profile {
|
||||||
p.sync(profile);
|
p.sync(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -69,8 +69,8 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
} else {
|
} else {
|
||||||
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into()))
|
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into()))
|
||||||
.title("STARTLINER")
|
.title("STARTLINER")
|
||||||
.inner_size(640f64, 480f64)
|
.inner_size(720f64, 480f64)
|
||||||
.min_inner_size(640f64, 480f64)
|
.min_inner_size(720f64, 480f64)
|
||||||
.build()?;
|
.build()?;
|
||||||
start_immediately = false;
|
start_immediately = false;
|
||||||
}
|
}
|
||||||
@ -176,6 +176,7 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
|
|
||||||
cmd::get_package,
|
cmd::get_package,
|
||||||
cmd::get_all_packages,
|
cmd::get_all_packages,
|
||||||
|
cmd::get_game_packages,
|
||||||
cmd::reload_all_packages,
|
cmd::reload_all_packages,
|
||||||
cmd::fetch_listings,
|
cmd::fetch_listings,
|
||||||
cmd::install_package,
|
cmd::install_package,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::pkg::PkgKeyVersion;
|
use crate::pkg::{Status, PkgKey, PkgKeyVersion};
|
||||||
|
|
||||||
|
use super::misc::Game;
|
||||||
|
|
||||||
// manifest.json
|
// manifest.json
|
||||||
|
|
||||||
@ -14,3 +16,12 @@ pub struct PackageManifest {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub installers: Vec<BTreeMap<String, serde_json::Value>>
|
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 serde::{Deserialize, Serialize};
|
||||||
use crate::pkg::PkgKey;
|
use crate::pkg::PkgKey;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)]
|
||||||
pub enum Game {
|
pub enum Game {
|
||||||
#[serde(rename = "ongeki")]
|
#[serde(rename = "ongeki")]
|
||||||
Ongeki,
|
Ongeki,
|
||||||
@ -17,6 +17,48 @@ impl Game {
|
|||||||
_ => None
|
_ => 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 {
|
impl std::fmt::Display for Game {
|
||||||
|
@ -2,7 +2,9 @@ use std::path::PathBuf;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::pkg::PkgKey;
|
use crate::pkg::PkgKey;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
|
use super::misc::Game;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
|
||||||
pub enum Aime {
|
pub enum Aime {
|
||||||
Disabled,
|
Disabled,
|
||||||
#[default] BuiltIn,
|
#[default] BuiltIn,
|
||||||
@ -10,7 +12,7 @@ pub enum Aime {
|
|||||||
Other(PkgKey),
|
Other(PkgKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
pub struct AMNet {
|
pub struct AMNet {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub addr: 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 struct Segatools {
|
||||||
pub target: PathBuf,
|
pub target: PathBuf,
|
||||||
pub hook: Option<PkgKey>,
|
pub hook: Option<PkgKey>,
|
||||||
@ -38,11 +40,14 @@ pub struct Segatools {
|
|||||||
pub amnet: AMNet,
|
pub amnet: AMNet,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Segatools {
|
impl Segatools {
|
||||||
fn default() -> Self {
|
pub fn default_for(game: Game) -> Self {
|
||||||
Segatools {
|
Segatools {
|
||||||
target: PathBuf::default(),
|
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,
|
io: None,
|
||||||
amfs: PathBuf::default(),
|
amfs: PathBuf::default(),
|
||||||
option: 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 {
|
pub enum DisplayMode {
|
||||||
Window,
|
Window,
|
||||||
#[default] Borderless,
|
#[default] Borderless,
|
||||||
Fullscreen
|
Fullscreen
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
pub struct Display {
|
pub struct Display {
|
||||||
pub target: String,
|
pub target: String,
|
||||||
pub rez: (i32, i32),
|
pub rez: (i32, i32),
|
||||||
@ -71,26 +76,32 @@ pub struct Display {
|
|||||||
pub borderless_fullscreen: bool,
|
pub borderless_fullscreen: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Display {
|
impl Display {
|
||||||
fn default() -> Self {
|
pub fn default_for(game: Game) -> Self {
|
||||||
Display {
|
Display {
|
||||||
target: "default".to_owned(),
|
target: "default".to_owned(),
|
||||||
rez: (1080, 1920),
|
rez: match game {
|
||||||
|
Game::Chunithm => (1920, 1080),
|
||||||
|
Game::Ongeki => (1080, 1920),
|
||||||
|
},
|
||||||
mode: DisplayMode::Borderless,
|
mode: DisplayMode::Borderless,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
frequency: 60,
|
frequency: match game {
|
||||||
|
Game::Chunithm => 120,
|
||||||
|
Game::Ongeki => 60,
|
||||||
|
},
|
||||||
borderless_fullscreen: true,
|
borderless_fullscreen: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
|
||||||
pub enum NetworkType {
|
pub enum NetworkType {
|
||||||
#[default] Remote,
|
#[default] Remote,
|
||||||
Artemis,
|
Artemis,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
|
||||||
pub struct Network {
|
pub struct Network {
|
||||||
pub network_type: NetworkType,
|
pub network_type: NetworkType,
|
||||||
|
|
||||||
@ -104,7 +115,7 @@ pub struct Network {
|
|||||||
pub suffix: Option<i32>,
|
pub suffix: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
|
||||||
pub struct BepInEx {
|
pub struct BepInEx {
|
||||||
pub console: bool,
|
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]
|
"[vfd]
|
||||||
; Enable VFD emulation. Disable to use a real VFD
|
; Enable VFD emulation. Disable to use a real VFD
|
||||||
; GP1232A02A FUTABA assembly.
|
; GP1232A02A FUTABA assembly.
|
||||||
@ -76,5 +80,219 @@ right2=0x4B ; K
|
|||||||
right3=0x4C ; L
|
right3=0x4C ; L
|
||||||
|
|
||||||
leftMenu=0x55 ; U
|
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,15 +4,17 @@ use crate::pkg::PkgKey;
|
|||||||
use crate::util;
|
use crate::util;
|
||||||
use crate::profiles::ProfilePaths;
|
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");
|
log::debug!("begin prepare packages");
|
||||||
|
|
||||||
let pfx_dir = p.data_dir();
|
let pfx_dir = p.data_dir();
|
||||||
let opt_dir = pfx_dir.join("option");
|
let opt_dir = pfx_dir.join("option");
|
||||||
|
|
||||||
|
if redo_bepinex {
|
||||||
if pfx_dir.join("BepInEx").exists() {
|
if pfx_dir.join("BepInEx").exists() {
|
||||||
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?;
|
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !opt_dir.exists() {
|
if !opt_dir.exists() {
|
||||||
tokio::fs::create_dir(opt_dir).await?;
|
tokio::fs::create_dir(opt_dir).await?;
|
||||||
@ -21,12 +23,15 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
|
|||||||
for m in pkgs {
|
for m in pkgs {
|
||||||
log::debug!("preparing {}", m);
|
log::debug!("preparing {}", m);
|
||||||
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
||||||
|
|
||||||
|
if redo_bepinex {
|
||||||
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
|
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
|
||||||
.join("app")
|
.join("app")
|
||||||
.join("BepInEx");
|
.join("BepInEx");
|
||||||
if bpx_dir.exists() {
|
if bpx_dir.exists() {
|
||||||
util::copy_directory(&bpx_dir, &pfx_dir.join("BepInEx"), true)?;
|
util::copy_directory(&bpx_dir, &pfx_dir.join("BepInEx"), true)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option");
|
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option");
|
||||||
if opt_dir.exists() {
|
if opt_dir.exists() {
|
||||||
|
@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use ini::Ini;
|
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;
|
use crate::pkg_store::PackageStore;
|
||||||
|
|
||||||
impl Segatools {
|
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");
|
log::debug!("begin line-up: segatools");
|
||||||
|
|
||||||
let pfx_dir = p.data_dir();
|
let pfx_dir = p.data_dir();
|
||||||
@ -42,7 +42,7 @@ impl Segatools {
|
|||||||
let ini_path = p.config_dir().join("segatools-base.ini");
|
let ini_path = p.config_dir().join("segatools-base.ini");
|
||||||
|
|
||||||
if !ini_path.exists() {
|
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))?;
|
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
|
||||||
}
|
}
|
||||||
if !pfx_dir.exists() {
|
if !pfx_dir.exists() {
|
||||||
@ -69,12 +69,14 @@ impl Segatools {
|
|||||||
.set("amfs", self.amfs.stringify()?)
|
.set("amfs", self.amfs.stringify()?)
|
||||||
.set("appdata", self.appdata.stringify()?);
|
.set("appdata", self.appdata.stringify()?);
|
||||||
|
|
||||||
|
if game == Game::Ongeki {
|
||||||
ini_out.with_section(Some("unity"))
|
ini_out.with_section(Some("unity"))
|
||||||
.set("enable", "1")
|
.set("enable", "1")
|
||||||
.set(
|
.set(
|
||||||
"targetAssembly",
|
"targetAssembly",
|
||||||
pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()?
|
pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()?
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if self.aime != Aime::Disabled {
|
if self.aime != Aime::Disabled {
|
||||||
ini_out.with_section(Some("aime"))
|
ini_out.with_section(Some("aime"))
|
||||||
@ -84,11 +86,16 @@ impl Segatools {
|
|||||||
let mut aimeio = ini_out.with_section(Some("aimeio"));
|
let mut aimeio = ini_out.with_section(Some("aimeio"));
|
||||||
aimeio
|
aimeio
|
||||||
.set("path", util::pkg_dir().join(key.to_string()).join("segatools").join("aimeio.dll").stringify()?)
|
.set("path", util::pkg_dir().join(key.to_string()).join("segatools").join("aimeio.dll").stringify()?)
|
||||||
.set("gameId", "SDDT")
|
|
||||||
.set("serverAddress", &self.amnet.addr)
|
.set("serverAddress", &self.amnet.addr)
|
||||||
.set("useAimeDBForPhysicalCards", if self.amnet.physical { "1" } else { "0" })
|
.set("useAimeDBForPhysicalCards", if self.amnet.physical { "1" } else { "0" })
|
||||||
.set("enableKeyboardMode", "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")) {
|
if let Ok(keyboard_code) = std::fs::read_to_string(p.config_dir().join("aime.txt")) {
|
||||||
log::debug!("{} {}", keyboard_code, keyboard_code.len());
|
log::debug!("{} {}", keyboard_code, keyboard_code.len());
|
||||||
if keyboard_code.len() == 20 {
|
if keyboard_code.len() == 20 {
|
||||||
@ -105,6 +112,7 @@ impl Segatools {
|
|||||||
.set("enable", "0");
|
.set("enable", "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if game == Game::Ongeki {
|
||||||
if let Some(io) = &self.io {
|
if let Some(io) = &self.io {
|
||||||
ini_out.with_section(Some("mu3io"))
|
ini_out.with_section(Some("mu3io"))
|
||||||
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
|
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
|
||||||
@ -112,6 +120,7 @@ impl Segatools {
|
|||||||
ini_out.with_section(Some("mu3io"))
|
ini_out.with_section(Some("mu3io"))
|
||||||
.set("path", "");
|
.set("path", "");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
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};
|
use crate::{model::{local::{self, PackageManifest}, rainy}, util};
|
||||||
|
|
||||||
// {namespace}-{name}
|
// {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);
|
pub struct PkgKey(pub String);
|
||||||
|
|
||||||
// {namespace}-{name}-{version}
|
// {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);
|
pub struct PkgKeyVersion(String);
|
||||||
|
|
||||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||||
@ -36,10 +36,11 @@ pub enum Status {
|
|||||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Feature {
|
pub enum Feature {
|
||||||
Mod,
|
Mod,
|
||||||
Hook,
|
|
||||||
GameIO,
|
|
||||||
Aime,
|
Aime,
|
||||||
AMNet,
|
AMNet,
|
||||||
|
Mu3Hook,
|
||||||
|
Mu3IO,
|
||||||
|
ChusanHook,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
@ -196,14 +197,16 @@ impl Package {
|
|||||||
// Multiple features in the same dll (yubideck etc.) should be supported at some point
|
// Multiple features in the same dll (yubideck etc.) should be supported at some point
|
||||||
let mut flags = BitFlags::default();
|
let mut flags = BitFlags::default();
|
||||||
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") {
|
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") {
|
||||||
if module.ends_with("hook") {
|
if module == "mu3hook" {
|
||||||
flags |= Feature::Hook;
|
flags |= Feature::Mu3Hook;
|
||||||
|
} else if module == "chusanhook" {
|
||||||
|
flags |= Feature::ChusanHook;
|
||||||
} else if module == "amnet" {
|
} else if module == "amnet" {
|
||||||
flags |= Feature::AMNet | Feature::Aime;
|
flags |= Feature::AMNet | Feature::Aime;
|
||||||
} else if module == "aimeio" {
|
} else if module == "aimeio" {
|
||||||
flags |= Feature::Aime;
|
flags |= Feature::Aime;
|
||||||
} else if module.ends_with("io") {
|
} else if module == "mu3io" {
|
||||||
flags |= Feature::GameIO;
|
flags |= Feature::Mu3IO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Status::OK(flags);
|
return Status::OK(flags);
|
||||||
|
@ -5,15 +5,20 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
use crate::model::local::{PackageList, PackageListEntry};
|
||||||
|
use crate::model::misc::Game;
|
||||||
use crate::model::rainy;
|
use crate::model::rainy;
|
||||||
use crate::pkg::{Package, PkgKey, Remote};
|
use crate::pkg::{Package, PkgKey, Remote, Status};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
use crate::download_handler::DownloadHandler;
|
use crate::download_handler::DownloadHandler;
|
||||||
|
|
||||||
pub struct PackageStore {
|
pub struct PackageStore {
|
||||||
store: HashMap<PkgKey, Package>,
|
store: HashMap<PkgKey, Package>,
|
||||||
app: AppHandle,
|
meta_list: PackageList,
|
||||||
dlh: DownloadHandler,
|
dlh: DownloadHandler,
|
||||||
|
|
||||||
|
app: AppHandle,
|
||||||
|
|
||||||
offline: bool,
|
offline: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,8 +34,17 @@ pub enum InstallResult {
|
|||||||
|
|
||||||
impl PackageStore {
|
impl PackageStore {
|
||||||
pub fn new(app: AppHandle) -> 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 {
|
PackageStore {
|
||||||
store: HashMap::new(),
|
store: HashMap::new(),
|
||||||
|
meta_list,
|
||||||
app: app.clone(),
|
app: app.clone(),
|
||||||
dlh: DownloadHandler::new(app),
|
dlh: DownloadHandler::new(app),
|
||||||
offline: true
|
offline: true
|
||||||
@ -46,6 +60,13 @@ impl PackageStore {
|
|||||||
self.store.clone()
|
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) {
|
pub async fn reload_package(&mut self, key: PkgKey) {
|
||||||
let dir = util::pkg_dir().join(&key.0);
|
let dir = util::pkg_dir().join(&key.0);
|
||||||
if let Ok(pkg) = Package::from_dir(dir).await {
|
if let Ok(pkg) = Package::from_dir(dir).await {
|
||||||
@ -75,14 +96,23 @@ impl PackageStore {
|
|||||||
Ok(())
|
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 async_compression::futures::bufread::GzipDecoder;
|
||||||
use futures::{
|
use futures::{
|
||||||
io::{self, BufReader, ErrorKind},
|
io::{self, BufReader, ErrorKind},
|
||||||
prelude::*,
|
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
|
let reader = response
|
||||||
.bytes_stream()
|
.bytes_stream()
|
||||||
@ -100,11 +130,23 @@ impl PackageStore {
|
|||||||
self.offline
|
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 {
|
for listing in listings {
|
||||||
// This is None if the package has no versions for whatever reason
|
// This is None if the package has no versions for whatever reason
|
||||||
if let Some(r) = Package::from_rainy(listing) {
|
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()) {
|
match self.store.get_mut(&r.key()) {
|
||||||
Some(l) => {
|
Some(l) => {
|
||||||
l.rmt = r.rmt;
|
l.rmt = r.rmt;
|
||||||
@ -226,8 +268,7 @@ impl PackageStore {
|
|||||||
async fn clean_up_file(path: impl AsRef<Path>, name: &str, force: bool) -> Result<()> {
|
async fn clean_up_file(path: impl AsRef<Path>, name: &str, force: bool) -> Result<()> {
|
||||||
let path = path.as_ref().join(name);
|
let path = path.as_ref().join(name);
|
||||||
if force || path.exists() {
|
if force || path.exists() {
|
||||||
tokio::fs::remove_file(path)
|
tokio::fs::remove_file(path).await
|
||||||
.await
|
|
||||||
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
|
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,6 +283,7 @@ impl PackageStore {
|
|||||||
Self::clean_up_file(&path, "icon.png", true).await?;
|
Self::clean_up_file(&path, "icon.png", true).await?;
|
||||||
Self::clean_up_file(&path, "manifest.json", 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, "README.md", true).await?;
|
||||||
|
Self::clean_up_file(&path, "post_load.ps1", false).await?;
|
||||||
|
|
||||||
tokio::fs::remove_dir(path.as_ref())
|
tokio::fs::remove_dir(path.as_ref())
|
||||||
.await
|
.await
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
use anyhow::{Result, anyhow};
|
|
||||||
use ongeki::OngekiProfile;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
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;
|
pub trait ProfilePaths {
|
||||||
|
fn config_dir(&self) -> PathBuf;
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
fn data_dir(&self) -> PathBuf;
|
||||||
pub enum AnyProfile {
|
|
||||||
OngekiProfile(OngekiProfile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
@ -18,96 +22,140 @@ pub struct ProfileMeta {
|
|||||||
pub name: String
|
pub name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Profile: Sized {
|
impl ProfilePaths for ProfileMeta {
|
||||||
fn new(name: String) -> Result<Self>;
|
fn config_dir(&self) -> PathBuf {
|
||||||
fn load(name: String) -> Result<Self>;
|
util::profile_config_dir(self.game, &self.name)
|
||||||
fn save(&self) -> Result<()>;
|
|
||||||
async fn start(&self, app: AppHandle) -> Result<()>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ProfilePaths {
|
fn data_dir(&self) -> PathBuf {
|
||||||
fn config_dir(&self) -> PathBuf;
|
util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name))
|
||||||
fn data_dir(&self) -> PathBuf;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnyProfile {
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
pub struct Profile {
|
||||||
|
pub meta: ProfileMeta,
|
||||||
|
pub data: ProfileData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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> {
|
pub fn load(game: Game, name: String) -> Result<Self> {
|
||||||
Ok(match game {
|
let path = util::profile_config_dir(game, &name).join("profile.json");
|
||||||
Game::Ongeki => AnyProfile::OngekiProfile(OngekiProfile::load(name)?),
|
if let Ok(s) = std::fs::read_to_string(&path) {
|
||||||
Game::Chunithm => panic!("Not implemented")
|
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<()> {
|
pub fn save(&self) -> Result<()> {
|
||||||
match self {
|
let path = self.config_dir().join("profile.json");
|
||||||
Self::OngekiProfile(p) => p.save()
|
|
||||||
}
|
let s = serde_json::to_string_pretty(&self.data)?;
|
||||||
}
|
if !self.config_dir().exists() {
|
||||||
pub fn meta(&self) -> ProfileMeta {
|
std::fs::create_dir(self.config_dir())
|
||||||
match self {
|
.map_err(|e| anyhow!("error when creating profile directory: {}", e))?;
|
||||||
Self::OngekiProfile(p) => {
|
|
||||||
ProfileMeta {
|
|
||||||
game: Game::Ongeki,
|
|
||||||
name: p.name.as_ref().unwrap().clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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) {
|
pub fn rename(&mut self, name: String) {
|
||||||
match self {
|
self.meta.name = fixed_name(&ProfileMeta { game: self.meta.game, name}, false);
|
||||||
Self::OngekiProfile(p) => {
|
|
||||||
p.name = Some(fixed_name(&ProfileMeta { name, game: Game::Ongeki }, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn mod_pkgs(&self) -> &BTreeSet<PkgKey> {
|
pub fn mod_pkgs(&self) -> &BTreeSet<PkgKey> {
|
||||||
match self {
|
&self.data.mods
|
||||||
Self::OngekiProfile(p) => &p.mods
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn mod_pkgs_mut(&mut self) -> &mut BTreeSet<PkgKey> {
|
pub fn mod_pkgs_mut(&mut self) -> &mut BTreeSet<PkgKey> {
|
||||||
match self {
|
&mut self.data.mods
|
||||||
Self::OngekiProfile(p) => &mut p.mods
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn special_pkgs(&self) -> Vec<PkgKey> {
|
pub fn special_pkgs(&self) -> Vec<PkgKey> {
|
||||||
let mut res = Vec::new();
|
let mut res = Vec::new();
|
||||||
match self {
|
if let Some(hook) = &self.data.sgt.hook {
|
||||||
Self::OngekiProfile(p) => {
|
|
||||||
if let Some(hook) = &p.sgt.hook {
|
|
||||||
res.push(hook.clone());
|
res.push(hook.clone());
|
||||||
}
|
}
|
||||||
if let Some(io) = &p.sgt.io {
|
if let Some(io) = &self.data.sgt.io {
|
||||||
res.push(io.clone());
|
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
|
res
|
||||||
}
|
}
|
||||||
pub fn fix(&mut self, store: &PackageStore) {
|
pub fn fix(&mut self, store: &PackageStore) {
|
||||||
match self {
|
self.data.sgt.fix(store);
|
||||||
Self::OngekiProfile(p) => p.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<()> {
|
pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> {
|
||||||
match self {
|
let info = match &self.data.display {
|
||||||
Self::OngekiProfile(_p) => {
|
None => None,
|
||||||
#[cfg(target_os = "windows")]
|
Some(display) => display.line_up()?
|
||||||
let info = _p.display.line_up()?;
|
};
|
||||||
|
|
||||||
let res = self.line_up_the_rest(pkg_hash).await;
|
let res = self.line_up_the_rest(pkg_hash).await;
|
||||||
|
|
||||||
@ -123,42 +171,162 @@ impl AnyProfile {
|
|||||||
|
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
async fn line_up_the_rest(&self, pkg_hash: String) -> Result<()> {
|
async fn line_up_the_rest(&self, pkg_hash: String) -> Result<()> {
|
||||||
match self {
|
if !self.data_dir().exists() {
|
||||||
Self::OngekiProfile(p) => {
|
tokio::fs::create_dir(self.data_dir()).await?;
|
||||||
if !p.data_dir().exists() {
|
|
||||||
tokio::fs::create_dir(p.data_dir()).await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash_path = p.data_dir().join(".sl-state");
|
let hash_path = self.data_dir().join(".sl-state");
|
||||||
let meta = self.meta();
|
|
||||||
|
|
||||||
util::clean_up_opts(p.data_dir().join("option"))?;
|
util::clean_up_opts(self.data_dir().join("option"))?;
|
||||||
|
|
||||||
if Self::hash_check(&hash_path, &pkg_hash).await? == true {
|
let hash_check = Self::hash_check(&hash_path, &pkg_hash).await?;
|
||||||
prepare_packages(&meta, &p.mods).await
|
prepare_packages(&self.meta, &self.data.mods, hash_check).await
|
||||||
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
|
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
|
||||||
}
|
let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await
|
||||||
let mut ini = p.sgt.line_up(&meta).await
|
|
||||||
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
|
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
|
||||||
p.network.line_up(&mut ini)?;
|
self.data.network.line_up(&mut ini)?;
|
||||||
|
|
||||||
ini.write_to_file(p.data_dir().join("segatools.ini"))
|
ini.write_to_file(self.data_dir().join("segatools.ini"))
|
||||||
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
|
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
|
||||||
|
|
||||||
p.bepinex.line_up(&meta)?;
|
if let Some(bepinex) = &self.data.bepinex {
|
||||||
|
bepinex.line_up(&self.meta)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub 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.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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&self, app: AppHandle) -> Result<()> {
|
#[cfg(target_os = "linux")]
|
||||||
match self {
|
{
|
||||||
Self::OngekiProfile(p) => p.start(app).await
|
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> {
|
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 fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn config_dir(&self) -> PathBuf {
|
||||||
match self {
|
self.meta.config_dir()
|
||||||
Self::OngekiProfile(p) => f.debug_tuple("ongeki").field(&p.name).finish(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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("/", "").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);
|
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
|
&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))
|
config_dir().join(format!("profile-{}-{}", game, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import StartButton from './StartButton.vue';
|
|||||||
import { invoke } from '../invoke';
|
import { invoke } from '../invoke';
|
||||||
import { useGeneralStore, usePkgStore, usePrfStore } from '../stores';
|
import { useGeneralStore, usePkgStore, usePrfStore } from '../stores';
|
||||||
import { Dirs } from '../types';
|
import { Dirs } from '../types';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
const pkg = usePkgStore();
|
const pkg = usePkgStore();
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
@ -27,6 +28,7 @@ const currentTab: Ref<string | number> = ref(3);
|
|||||||
const pkgSearchTerm = ref('');
|
const pkgSearchTerm = ref('');
|
||||||
|
|
||||||
const isProfileDisabled = computed(() => prf.current === null);
|
const isProfileDisabled = computed(() => prf.current === null);
|
||||||
|
const isRunning = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
invoke('list_directories').then((d) => {
|
invoke('list_directories').then((d) => {
|
||||||
@ -47,8 +49,23 @@ onMounted(async () => {
|
|||||||
key: 'segatools-mu3hook',
|
key: 'segatools-mu3hook',
|
||||||
force: false,
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -61,20 +78,27 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
<div class="fixed w-full flex z-100">
|
<div class="fixed w-full flex z-100">
|
||||||
<TabList class="grow">
|
<TabList class="grow">
|
||||||
<Tab :disabled="isProfileDisabled" :value="0"
|
<Tab :value="3" :disabled="isRunning"
|
||||||
><div class="pi pi-list-check"></div
|
><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>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="pkg.networkStatus === 'online'"
|
v-if="pkg.networkStatus === 'online'"
|
||||||
:disabled="isProfileDisabled"
|
:disabled="isProfileDisabled || isRunning"
|
||||||
:value="1"
|
:value="1"
|
||||||
><div class="pi pi-download"></div
|
><div class="pi pi-download"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<Tab :disabled="isProfileDisabled" :value="2"
|
<Tab :disabled="isProfileDisabled || isRunning" :value="2"
|
||||||
><div class="pi pi-cog"></div
|
><div class="pi pi-cog"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<Tab :value="3"
|
|
||||||
><div class="pi pi-question-circle"></div
|
<Tab :value="5" v-if="isRunning"
|
||||||
|
><div class="pi pi-sparkles"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
@ -131,16 +155,6 @@ onMounted(async () => {
|
|||||||
missing.<br />Existing features are expected to break
|
missing.<br />Existing features are expected to break
|
||||||
sometimes.
|
sometimes.
|
||||||
<ProfileList />
|
<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 />
|
<br /><br /><br />
|
||||||
<footer>
|
<footer>
|
||||||
<Button
|
<Button
|
||||||
@ -151,7 +165,24 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
</TabPanel>
|
</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>
|
</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>
|
</Tabs>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { Ref, computed, ref } from 'vue';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import Fieldset from 'primevue/fieldset';
|
import Fieldset from 'primevue/fieldset';
|
||||||
import ModListEntry from './ModListEntry.vue';
|
import ModListEntry from './ModListEntry.vue';
|
||||||
import ModTitlecard from './ModTitlecard.vue';
|
import ModTitlecard from './ModTitlecard.vue';
|
||||||
|
import { invoke } from '../invoke';
|
||||||
import { usePkgStore, usePrfStore } from '../stores';
|
import { usePkgStore, usePrfStore } from '../stores';
|
||||||
import { Package } from '../types';
|
import { Package } from '../types';
|
||||||
|
import { pkgKey } from '../util';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
search: String,
|
search: String,
|
||||||
@ -14,12 +16,20 @@ const props = defineProps({
|
|||||||
const pkgs = usePkgStore();
|
const pkgs = usePkgStore();
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
const empty = ref(true);
|
const empty = ref(true);
|
||||||
|
const gameSublist: Ref<string[]> = ref([]);
|
||||||
|
|
||||||
const group = () => {
|
invoke('get_game_packages', {
|
||||||
const a = Object.assign(
|
game: prf.current?.meta.game,
|
||||||
|
}).then((list) => {
|
||||||
|
gameSublist.value = list as string[];
|
||||||
|
});
|
||||||
|
|
||||||
|
const group = computed(() => {
|
||||||
|
const res = Object.assign(
|
||||||
{},
|
{},
|
||||||
Object.groupBy(
|
Object.groupBy(
|
||||||
pkgs.allLocal
|
pkgs.allLocal
|
||||||
|
.filter((p) => gameSublist.value.includes(pkgKey(p)))
|
||||||
.filter(
|
.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
props.search === undefined ||
|
props.search === undefined ||
|
||||||
@ -35,12 +45,12 @@ const group = () => {
|
|||||||
({ namespace }) => namespace
|
({ namespace }) => namespace
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
empty.value = Object.keys(a).length === 0;
|
empty.value = Object.keys(res).length === 0;
|
||||||
return a;
|
return res;
|
||||||
};
|
});
|
||||||
|
|
||||||
const missing = computed(() => {
|
const missing = computed(() => {
|
||||||
return prf.current?.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
|
return prf.current?.data.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -68,7 +78,7 @@ const missing = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Fieldset>
|
</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" />
|
<ModListEntry v-for="p in namespace" :pkg="p" />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
<div v-if="empty" class="text-3xl">∅</div>
|
<div v-if="empty" class="text-3xl">∅</div>
|
||||||
|
@ -32,7 +32,6 @@ const model = computed({
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
|
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
|
||||||
<UpdateButton :pkg="pkg" />
|
<UpdateButton :pkg="pkg" />
|
||||||
<!-- @vue-expect-error Can't 'as any' because it breaks VSCode -->
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
v-if="hasFeature(pkg, Feature.Mod)"
|
v-if="hasFeature(pkg, Feature.Mod)"
|
||||||
class="scale-[1.33] shrink-0"
|
class="scale-[1.33] shrink-0"
|
||||||
|
@ -1,20 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { Ref, ref } from 'vue';
|
||||||
import Divider from 'primevue/divider';
|
import Divider from 'primevue/divider';
|
||||||
import MultiSelect from 'primevue/multiselect';
|
import MultiSelect from 'primevue/multiselect';
|
||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
import ModStoreEntry from './ModStoreEntry.vue';
|
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 pkgs = usePkgStore();
|
||||||
|
const prf = usePrfStore();
|
||||||
const empty = ref(true);
|
const empty = ref(true);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
search: String,
|
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 list = () => {
|
||||||
const res = pkgs.allRemote
|
const res = pkgs.allRemote
|
||||||
|
.filter((p) => gameSublist.value.includes(pkgKey(p)))
|
||||||
.filter(
|
.filter(
|
||||||
(p) =>
|
(p) =>
|
||||||
props.search === undefined ||
|
props.search === undefined ||
|
||||||
|
@ -53,13 +53,16 @@ const iconSrc = computed(() => {
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="hasFeature(pkg, Feature.Hook)"
|
v-if="
|
||||||
|
hasFeature(pkg, Feature.ChusanHook) ||
|
||||||
|
hasFeature(pkg, Feature.Mu3Hook)
|
||||||
|
"
|
||||||
v-tooltip="'Hook'"
|
v-tooltip="'Hook'"
|
||||||
class="pi pi-wrench ml-1 text-blue-400"
|
class="pi pi-wrench ml-1 text-blue-400"
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="hasFeature(pkg, Feature.GameIO)"
|
v-if="hasFeature(pkg, Feature.Mu3IO)"
|
||||||
v-tooltip="'IO'"
|
v-tooltip="'IO'"
|
||||||
class="pi pi-wrench ml-1 text-green-400"
|
class="pi pi-wrench ml-1 text-green-400"
|
||||||
>
|
>
|
||||||
|
@ -1,399 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<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 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 FileEditor from './FileEditor.vue';
|
||||||
import FilePicker from './FilePicker.vue';
|
|
||||||
import OptionCategory from './OptionCategory.vue';
|
import OptionCategory from './OptionCategory.vue';
|
||||||
import OptionRow from './OptionRow.vue';
|
import OptionRow from './OptionRow.vue';
|
||||||
import { invoke } from '../invoke';
|
import AimeOptions from './options/Aime.vue';
|
||||||
import { usePkgStore, usePrfStore } from '../stores';
|
import DisplayOptions from './options/Display.vue';
|
||||||
import { Feature } from '../types';
|
import MiscOptions from './options/Misc.vue';
|
||||||
import { hasFeature, pkgKey } from '../util';
|
import NetworkOptions from './options/Network.vue';
|
||||||
|
import SegatoolsOptions from './options/Segatools.vue';
|
||||||
|
import { usePrfStore } from '../stores';
|
||||||
|
|
||||||
const pkgs = usePkgStore();
|
|
||||||
const prf = usePrfStore();
|
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();
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<OptionCategory title="General">
|
<SegatoolsOptions />
|
||||||
<OptionRow title="mu3.exe">
|
<DisplayOptions v-if="prf.current!.meta.game === 'ongeki'" />
|
||||||
<FilePicker
|
<NetworkOptions />
|
||||||
:directory="false"
|
<AimeOptions />
|
||||||
promptname="mu3.exe"
|
<MiscOptions />
|
||||||
extension="exe"
|
<OptionCategory
|
||||||
:value="prf.current!.sgt.target"
|
title="Extensions"
|
||||||
:callback="(value: string) => (prf.current!.sgt.target = value)"
|
v-if="prf.current!.meta.game === 'ongeki'"
|
||||||
></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">
|
|
||||||
<OptionRow title="Inohara config">
|
<OptionRow title="Inohara config">
|
||||||
<FileEditor
|
<FileEditor
|
||||||
filename="inohara.cfg"
|
filename="inohara.cfg"
|
||||||
@ -402,7 +33,8 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="BepInEx console">
|
<OptionRow title="BepInEx console">
|
||||||
<ToggleSwitch v-model="prf.current!.bepinex.console" />
|
<!-- @vue-expect-error -->
|
||||||
|
<ToggleSwitch v-model="prf.current!.data.bepinex.console" />
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
</OptionCategory>
|
</OptionCategory>
|
||||||
</template>
|
</template>
|
||||||
|
@ -19,7 +19,6 @@ const prf = usePrfStore();
|
|||||||
icon="pi pi-plus"
|
icon="pi pi-plus"
|
||||||
class="chunithm-button profile-button"
|
class="chunithm-button profile-button"
|
||||||
@click="() => prf.create('chunithm')"
|
@click="() => prf.create('chunithm')"
|
||||||
:disabled="true"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
|
<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">
|
<div class="flex flex-row flex-wrap align-middle gap-2">
|
||||||
<Button
|
<Button
|
||||||
:disabled="
|
:disabled="
|
||||||
prf.current?.game === p!.game && prf.current?.name === p!.name
|
prf.current?.meta.game === p!.game &&
|
||||||
|
prf.current?.meta.name === p!.name
|
||||||
"
|
"
|
||||||
:class="
|
:class="
|
||||||
(p!.game === 'chunithm' ? 'chunithm-button' : 'ongeki-button') +
|
(p!.game === 'chunithm' ? 'chunithm-button' : 'ongeki-button') +
|
||||||
|
@ -5,6 +5,7 @@ import ConfirmDialog from 'primevue/confirmdialog';
|
|||||||
import ScrollPanel from 'primevue/scrollpanel';
|
import ScrollPanel from 'primevue/scrollpanel';
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
import { invoke } from '../invoke';
|
import { invoke } from '../invoke';
|
||||||
import { usePrfStore } from '../stores';
|
import { usePrfStore } from '../stores';
|
||||||
|
|
||||||
@ -54,17 +55,19 @@ const startline = async (force: boolean) => {
|
|||||||
|
|
||||||
const kill = async () => {
|
const kill = async () => {
|
||||||
await invoke('kill');
|
await invoke('kill');
|
||||||
startStatus.value = 'ready';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabledTooltip = computed(() => {
|
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';
|
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';
|
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 'A segatools hook package is necessary';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -72,10 +75,13 @@ const disabledTooltip = computed(() => {
|
|||||||
|
|
||||||
listen('launch-start', () => {
|
listen('launch-start', () => {
|
||||||
startStatus.value = 'running';
|
startStatus.value = 'running';
|
||||||
|
getCurrentWindow().minimize();
|
||||||
});
|
});
|
||||||
|
|
||||||
listen('launch-end', () => {
|
listen('launch-end', () => {
|
||||||
startStatus.value = 'ready';
|
startStatus.value = 'ready';
|
||||||
|
getCurrentWindow().unminimize();
|
||||||
|
getCurrentWindow().setFocus();
|
||||||
});
|
});
|
||||||
|
|
||||||
const messageSplit = (message: any) => {
|
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)
|
(c) => !state.excludeCategories.includes(c)
|
||||||
))
|
))
|
||||||
),
|
),
|
||||||
hooks: (state) =>
|
byFeature: (state) => (feature: Feature) =>
|
||||||
Object.values(state.pkg).filter((p) => hasFeature(p, Feature.Hook)),
|
Object.values(state.pkg).filter((p) => hasFeature(p, feature)),
|
||||||
gameIOs: (state) =>
|
|
||||||
Object.values(state.pkg).filter((p) =>
|
|
||||||
hasFeature(p, Feature.GameIO)
|
|
||||||
),
|
|
||||||
aimes: (state) =>
|
|
||||||
Object.values(state.pkg).filter((p) => hasFeature(p, Feature.Aime)),
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setupListeners() {
|
setupListeners() {
|
||||||
@ -189,18 +183,14 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
() =>
|
() =>
|
||||||
pkg !== undefined &&
|
pkg !== undefined &&
|
||||||
current.value !== null &&
|
current.value !== null &&
|
||||||
current.value?.mods.includes(pkgKey(pkg))
|
current.value?.data.mods.includes(pkgKey(pkg))
|
||||||
);
|
);
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
const p = (await invoke('get_current_profile')) as any;
|
const p = (await invoke('get_current_profile')) as Profile;
|
||||||
if (p != null && 'OngekiProfile' in p) {
|
current.value = p;
|
||||||
current.value = { ...p.OngekiProfile, game: 'ongeki' };
|
|
||||||
} else {
|
|
||||||
current.value = null;
|
|
||||||
}
|
|
||||||
if (current.value !== null) {
|
if (current.value !== null) {
|
||||||
changePrimaryColor(current.value.game);
|
changePrimaryColor(current.value.meta.game);
|
||||||
} else {
|
} else {
|
||||||
changePrimaryColor(null);
|
changePrimaryColor(null);
|
||||||
}
|
}
|
||||||
@ -231,10 +221,10 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
current.value?.game === profile.game &&
|
current.value?.meta.game === profile.game &&
|
||||||
current.value.name === profile.name
|
current.value.meta.name === profile.name
|
||||||
) {
|
) {
|
||||||
current.value.name = name;
|
current.value.meta.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
await reloadList();
|
await reloadList();
|
||||||
@ -272,7 +262,7 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
const configDir = computed(async () => {
|
const configDir = computed(async () => {
|
||||||
return await path.join(
|
return await path.join(
|
||||||
generalStore.configDir,
|
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 () => {
|
watchEffect(async () => {
|
||||||
if (current.value !== null) {
|
if (current.value !== null) {
|
||||||
await invoke('sync_current_profile', {
|
await invoke('sync_current_profile', {
|
||||||
profile: { OngekiProfile: current.value },
|
data: current.value.data,
|
||||||
});
|
});
|
||||||
if (timeout !== null) {
|
if (timeout !== null) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
28
src/types.ts
28
src/types.ts
@ -24,11 +24,12 @@ export interface Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum Feature {
|
export enum Feature {
|
||||||
Mod = 0b00001,
|
Mod = 1 << 0,
|
||||||
Hook = 0b00010,
|
Aime = 1 << 1,
|
||||||
GameIO = 0b00100,
|
AMNet = 1 << 2,
|
||||||
Aime = 0b01000,
|
Mu3Hook = 1 << 3,
|
||||||
AMNet = 0b10000,
|
Mu3IO = 1 << 4,
|
||||||
|
ChusanHook = 1 << 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Status =
|
export type Status =
|
||||||
@ -45,6 +46,14 @@ export interface ProfileMeta {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProfileData {
|
||||||
|
mods: string[];
|
||||||
|
sgt: SegatoolsConfig;
|
||||||
|
display: DisplayConfig;
|
||||||
|
network: NetworkConfig;
|
||||||
|
bepinex: BepInExConfig;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SegatoolsConfig {
|
export interface SegatoolsConfig {
|
||||||
target: string;
|
target: string;
|
||||||
hook: string | null;
|
hook: string | null;
|
||||||
@ -83,12 +92,9 @@ export interface BepInExConfig {
|
|||||||
console: boolean;
|
console: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Profile extends ProfileMeta {
|
export interface Profile {
|
||||||
mods: string[];
|
meta: ProfileMeta;
|
||||||
sgt: SegatoolsConfig;
|
data: ProfileData;
|
||||||
display: DisplayConfig;
|
|
||||||
network: NetworkConfig;
|
|
||||||
bepinex: BepInExConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Module = 'sgt' | 'display' | 'network';
|
export type Module = 'sgt' | 'display' | 'network';
|
||||||
|
Reference in New Issue
Block a user