feat: new config format
This commit is contained in:
36
rust/Cargo.lock
generated
36
rust/Cargo.lock
generated
@ -147,6 +147,12 @@ dependencies = [
|
|||||||
"derive_arbitrary",
|
"derive_arbitrary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arraydeque"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ashpd"
|
name = "ashpd"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@ -1480,6 +1486,12 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@ -1999,6 +2011,18 @@ name = "hashbrown"
|
|||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.15.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@ -4566,6 +4590,7 @@ dependencies = [
|
|||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
"tokio",
|
"tokio",
|
||||||
"winsafe 0.0.23",
|
"winsafe 0.0.23",
|
||||||
|
"yaml-rust2",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -6606,6 +6631,17 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yaml-rust2"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "232bdb534d65520716bef0bbb205ff8f2db72d807b19c0bc3020853b92a0cd4b"
|
||||||
|
dependencies = [
|
||||||
|
"arraydeque",
|
||||||
|
"encoding_rs",
|
||||||
|
"hashlink",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.7.5"
|
version = "0.7.5"
|
||||||
|
@ -40,6 +40,7 @@ closure = "0.3.0"
|
|||||||
derive_more = { version = "2.0.1", features = ["display"] }
|
derive_more = { version = "2.0.1", features = ["display"] }
|
||||||
junction = "1.2.0"
|
junction = "1.2.0"
|
||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
|
yaml-rust2 = "0.10.0"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-cli = "2"
|
tauri-plugin-cli = "2"
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
use crate::profiles::AnyProfile;
|
||||||
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, Profile};
|
use crate::util;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
@ -11,11 +12,15 @@ pub struct GlobalConfig {
|
|||||||
pub recent_profile: Option<(Game, String)>
|
pub recent_profile: Option<(Game, String)>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct GlobalState {
|
||||||
|
pub remain_open: bool
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
pub profile: Option<Profile>,
|
pub profile: Option<AnyProfile>,
|
||||||
pub pkgs: PackageStore,
|
pub pkgs: PackageStore,
|
||||||
pub cfg: GlobalConfig,
|
pub cfg: GlobalConfig,
|
||||||
pub remain_open: bool,
|
pub state: GlobalState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
@ -25,15 +30,15 @@ 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)) => Profile::load(game.clone(), name.clone()).ok(),
|
Some((ref game, ref name)) => AnyProfile::load(game.clone(), name.clone()).ok(),
|
||||||
None => None
|
None => None
|
||||||
};
|
};
|
||||||
|
|
||||||
AppData {
|
AppData {
|
||||||
profile,
|
profile: profile,
|
||||||
pkgs: PackageStore::new(apph.clone()),
|
pkgs: PackageStore::new(apph.clone()),
|
||||||
cfg,
|
cfg,
|
||||||
remain_open: true
|
state: GlobalState { remain_open: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +47,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 Profile::load(game.clone(), name.clone()) {
|
match AnyProfile::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));
|
||||||
@ -67,12 +72,12 @@ impl AppData {
|
|||||||
let loc = pkg.loc
|
let loc = pkg.loc
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
||||||
profile.data.mods.insert(key);
|
profile.pkgs_mut().insert(key);
|
||||||
for d in &loc.dependencies {
|
for d in &loc.dependencies {
|
||||||
_ = self.toggle_package(d.clone(), true);
|
_ = self.toggle_package(d.clone(), true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
profile.data.mods.remove(&key);
|
profile.pkgs_mut().remove(&key);
|
||||||
for (ckey, pkg) in self.pkgs.get_all() {
|
for (ckey, pkg) in self.pkgs.get_all() {
|
||||||
if let Some(loc) = pkg.loc {
|
if let Some(loc) = pkg.loc {
|
||||||
if loc.dependencies.contains(&key) {
|
if loc.dependencies.contains(&key) {
|
||||||
@ -85,10 +90,10 @@ impl AppData {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sum_packages(&self, p: &Profile) -> String {
|
pub fn sum_packages(&self, p: &AnyProfile) -> String {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
for pkg in &p.data.mods {
|
for pkg in p.pkgs().into_iter() {
|
||||||
let x = self.pkgs.get(pkg).unwrap().loc.as_ref().unwrap();
|
let x = self.pkgs.get(&pkg).unwrap().loc.as_ref().unwrap();
|
||||||
pkg.hash(&mut hasher);
|
pkg.hash(&mut hasher);
|
||||||
x.version.hash(&mut hasher);
|
x.version.hash(&mut hasher);
|
||||||
}
|
}
|
||||||
|
140
rust/src/cmd.rs
140
rust/src/cmd.rs
@ -2,39 +2,42 @@ use log;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
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;
|
use crate::pkg_store::InstallResult;
|
||||||
use crate::profile::Profile;
|
use crate::profiles::ongeki::OngekiProfile;
|
||||||
|
use crate::profiles::{AnyProfile, Profile, ProfileMeta, ProfilePaths};
|
||||||
use crate::appdata::AppData;
|
use crate::appdata::AppData;
|
||||||
use crate::{liner, start, util};
|
use crate::util;
|
||||||
|
|
||||||
use tauri::{AppHandle, Manager, State};
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn startline(app: AppHandle) -> Result<(), String> {
|
pub async fn startline(app: AppHandle) -> Result<(), String> {
|
||||||
log::debug!("invoke: startline");
|
log::debug!("invoke: startline");
|
||||||
|
|
||||||
let app_copy = app.clone();
|
|
||||||
let state = app.state::<Mutex<AppData>>();
|
let state = app.state::<Mutex<AppData>>();
|
||||||
let appd = state.lock().await;
|
let mut hash = "".to_owned();
|
||||||
|
|
||||||
|
let mut appd = state.lock().await;
|
||||||
if let Some(p) = &appd.profile {
|
if let Some(p) = &appd.profile {
|
||||||
let hash = appd.sum_packages(p);
|
hash = appd.sum_packages(p);
|
||||||
liner::line_up(p, hash).await
|
}
|
||||||
.map_err(|e| e.to_string())?;
|
if let Some(p) = &mut appd.profile {
|
||||||
|
p.line_up(app.clone(), hash).await
|
||||||
|
.map_err(|e| format!("Lineup failed:\n{}", e))?;
|
||||||
|
p.start(app.clone()).await
|
||||||
|
.map_err(|e| format!("Startup failed:\n{}", e))?;
|
||||||
|
|
||||||
start::start(p, app_copy).await
|
Ok(())
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
} else {
|
} else {
|
||||||
Err("No profile".to_owned())
|
Err("No profile".to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn kill() -> Result<(), String> {
|
pub async fn kill() -> Result<(), String> {
|
||||||
start::pkill("amdaemon.exe").await;
|
util::pkill("amdaemon.exe").await;
|
||||||
// The start routine will kill the other process
|
// The start routine will kill the other process
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -108,13 +111,35 @@ pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_profiles() -> Result<Vec<(Game, String)>, String> {
|
pub async fn list_profiles() -> Result<Vec<ProfileMeta>, String> {
|
||||||
log::debug!("invoke: list_profiles");
|
log::debug!("invoke: list_profiles");
|
||||||
|
|
||||||
let list = Profile::list().await.map_err(|e| e.to_string())?;
|
let list = crate::profiles::list_profiles().await.map_err(|e| e.to_string())?;
|
||||||
Ok(list)
|
Ok(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn init_profile(
|
||||||
|
state: State<'_, Mutex<AppData>>,
|
||||||
|
game: Game,
|
||||||
|
name: String
|
||||||
|
) -> Result<(), String> {
|
||||||
|
log::debug!("invoke: init_profile({}, {})", game, name);
|
||||||
|
|
||||||
|
let mut appd = state.lock().await;
|
||||||
|
let new_profile = OngekiProfile::new(name)
|
||||||
|
.map_err(|e| format!("Unable to create profile: {}", e))?;
|
||||||
|
|
||||||
|
fs::create_dir_all(new_profile.config_dir()).await
|
||||||
|
.map_err(|e| format!("Unable to create the profile config directory: {}", e))?;
|
||||||
|
fs::create_dir_all(new_profile.data_dir()).await
|
||||||
|
.map_err(|e| format!("Unable to create the profile data directory: {}", e))?;
|
||||||
|
|
||||||
|
appd.profile = Some(AnyProfile::OngekiProfile(new_profile.clone()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn load_profile(state: State<'_, Mutex<AppData>>, game: Game, name: String) -> Result<(), String> {
|
pub async fn load_profile(state: State<'_, Mutex<AppData>>, game: Game, name: String) -> Result<(), String> {
|
||||||
log::debug!("invoke: load_profile({} {:?})", game, name);
|
log::debug!("invoke: load_profile({} {:?})", game, name);
|
||||||
@ -125,7 +150,41 @@ pub async fn load_profile(state: State<'_, Mutex<AppData>>, game: Game, name: St
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<Profile>, ()> {
|
pub async fn rename_profile(
|
||||||
|
state: State<'_, Mutex<AppData>>,
|
||||||
|
profile: ProfileMeta,
|
||||||
|
name: String
|
||||||
|
) -> Result<(), String> {
|
||||||
|
log::debug!("invoke: rename_profile({:?} {:?})", profile, name);
|
||||||
|
|
||||||
|
let new_meta = ProfileMeta {
|
||||||
|
game: profile.game.clone(),
|
||||||
|
name: name.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
if new_meta.config_dir().exists() {
|
||||||
|
return Err(format!("Profile {} already exists", &name));
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::rename(profile.config_dir(), new_meta.config_dir()).await
|
||||||
|
.map_err(|e| format!("Unable to rename: {}", e))?;
|
||||||
|
|
||||||
|
if let Err(e) = fs::rename(profile.data_dir(), new_meta.data_dir()).await {
|
||||||
|
log::warn!("Unable to move data dir {}->{}: {}", &profile.name, &name, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut appd = state.lock().await;
|
||||||
|
if let Some(current) = &mut appd.profile {
|
||||||
|
if current.meta() == profile {
|
||||||
|
current.rename(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<AnyProfile>, ()> {
|
||||||
log::debug!("invoke: get_current_profile");
|
log::debug!("invoke: get_current_profile");
|
||||||
|
|
||||||
let appd = state.lock().await;
|
let appd = state.lock().await;
|
||||||
@ -133,41 +192,16 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_current_profile(state: State<'_, Mutex<AppData>>) -> Result<(), ()> {
|
pub async fn save_current_profile(state: State<'_, Mutex<AppData>>, profile: AnyProfile) -> Result<(), String> {
|
||||||
log::debug!("invoke: save_current_profile");
|
log::debug!("invoke: save_current_profile");
|
||||||
|
|
||||||
let appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
if let Some(p) = &appd.profile {
|
profile.save().map_err(|e| e.to_string())?;
|
||||||
p.save().await;
|
appd.profile = Some(profile);
|
||||||
} else {
|
|
||||||
log::warn!("No profile to save");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn init_profile(
|
|
||||||
state: State<'_, Mutex<AppData>>,
|
|
||||||
game: Game,
|
|
||||||
name: String
|
|
||||||
) -> Result<Profile, String> {
|
|
||||||
log::debug!("invoke: init_profile({}, {})", game, name);
|
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
|
||||||
let new_profile = Profile::new(game, name);
|
|
||||||
|
|
||||||
fs::create_dir_all(new_profile.config_dir()).await
|
|
||||||
.map_err(|e| format!("Unable to create the profile config directory: {}", e))?;
|
|
||||||
fs::create_dir_all(new_profile.data_dir()).await
|
|
||||||
.map_err(|e| format!("Unable to create the profile data directory: {}", e))?;
|
|
||||||
|
|
||||||
appd.profile = Some(new_profile.clone());
|
|
||||||
new_profile.save().await;
|
|
||||||
|
|
||||||
Ok(new_profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
|
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
|
||||||
log::debug!("invoke: list_platform_capabilities");
|
log::debug!("invoke: list_platform_capabilities");
|
||||||
@ -179,22 +213,6 @@ pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
|
|||||||
return Ok(vec!["wine".to_owned()]);
|
return Ok(vec!["wine".to_owned()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn set_cfg(
|
|
||||||
state: State<'_, Mutex<AppData>>,
|
|
||||||
key: String,
|
|
||||||
value: serde_json::Value
|
|
||||||
) -> Result<(), ()> {
|
|
||||||
log::debug!("invoke: set_cfg({}, {})", key, value);
|
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
|
||||||
if let Some(p) = &mut appd.profile {
|
|
||||||
p.data.cfg.insert(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn list_displays() -> Result<Vec<(String, String)>, String> {
|
pub async fn list_displays() -> Result<Vec<(String, String)>, String> {
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
use crate::profile::Profile;
|
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub struct DisplayInfo {
|
|
||||||
primary: String,
|
|
||||||
target: String,
|
|
||||||
target_settings: displayz::DisplaySettings
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
pub async fn prepare_display(_: &Profile) -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub async fn prepare_display(p: &Profile) -> Result<Option<DisplayInfo>> {
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use displayz::{query_displays, Orientation, Resolution, Frequency};
|
|
||||||
|
|
||||||
let display_name = p.get_str("display", "default");
|
|
||||||
let rotation = p.get_int("display-rotation", 0);
|
|
||||||
if display_name == "default" {
|
|
||||||
log::debug!("prepare display: skip");
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let display_set = query_displays()?;
|
|
||||||
|
|
||||||
let primary = display_set
|
|
||||||
.displays()
|
|
||||||
.find(|display| display.is_primary())
|
|
||||||
.ok_or_else(|| anyhow!("Primary display not found"))?;
|
|
||||||
|
|
||||||
let target = display_set
|
|
||||||
.displays()
|
|
||||||
.find(|display| display.name() == display_name)
|
|
||||||
.ok_or_else(|| anyhow!("Display {} not found", display_name))?;
|
|
||||||
|
|
||||||
target.set_primary()?;
|
|
||||||
let settings = target.settings()
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
|
||||||
|
|
||||||
let res = DisplayInfo {
|
|
||||||
primary: primary.name().to_owned(),
|
|
||||||
target: target.name().to_owned(),
|
|
||||||
target_settings: settings.borrow().clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
if rotation == 90 || rotation == 270 {
|
|
||||||
let rez = settings.borrow_mut().resolution;
|
|
||||||
settings.borrow_mut().orientation = if rotation == 90 { Orientation::PortraitFlipped } else { Orientation::Portrait };
|
|
||||||
if rez.height < rez.width {
|
|
||||||
settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let frequency: u32 = p.get_int("frequency", 60)
|
|
||||||
.try_into()
|
|
||||||
.map_err(|e| anyhow!("Invalid display frequency: {}", e))?;
|
|
||||||
|
|
||||||
let width: u32 = p.get_int("rez-w", 1080)
|
|
||||||
.try_into()
|
|
||||||
.map_err(|e| anyhow!("Invalid display width: {}", e))?;
|
|
||||||
|
|
||||||
let height: u32 = p.get_int("rez-h", 1080)
|
|
||||||
.try_into()
|
|
||||||
.map_err(|e| anyhow!("Invalid display height: {}", e))?;
|
|
||||||
|
|
||||||
settings.borrow_mut().frequency = Frequency::new(frequency);
|
|
||||||
|
|
||||||
if p.get_str("display-mode", "borderless") == "borderless" && p.get_bool("borderless-fullscreen", false) {
|
|
||||||
settings.borrow_mut().resolution = Resolution::new(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
display_set.apply()?;
|
|
||||||
displayz::refresh()?;
|
|
||||||
|
|
||||||
log::debug!("prepare display: done");
|
|
||||||
|
|
||||||
Ok(Some(res))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub async fn undo_display(info: DisplayInfo) -> Result<()> {
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use displayz::query_displays;
|
|
||||||
|
|
||||||
let display_set = query_displays()?;
|
|
||||||
|
|
||||||
let primary = display_set
|
|
||||||
.displays()
|
|
||||||
.find(|display| display.name() == info.primary)
|
|
||||||
.ok_or_else(|| anyhow!("Display {} not found", info.primary))?;
|
|
||||||
|
|
||||||
let target = display_set
|
|
||||||
.displays()
|
|
||||||
.find(|display| display.name() == info.target)
|
|
||||||
.ok_or_else(|| anyhow!("Display {} not found", info.target))?;
|
|
||||||
|
|
||||||
primary.set_primary()?;
|
|
||||||
|
|
||||||
let settings = target.settings()
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
|
||||||
settings.replace_with(|_| info.target_settings);
|
|
||||||
|
|
||||||
display_set.apply()?;
|
|
||||||
displayz::refresh()?;
|
|
||||||
|
|
||||||
log::debug!("undo display: done");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -2,21 +2,18 @@ mod cmd;
|
|||||||
mod model;
|
mod model;
|
||||||
mod pkg;
|
mod pkg;
|
||||||
mod pkg_store;
|
mod pkg_store;
|
||||||
mod profile;
|
|
||||||
mod util;
|
mod util;
|
||||||
mod start;
|
|
||||||
mod liner;
|
|
||||||
mod download_handler;
|
mod download_handler;
|
||||||
mod appdata;
|
mod appdata;
|
||||||
mod display;
|
mod modules;
|
||||||
|
mod profiles;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use closure::closure;
|
use closure::closure;
|
||||||
use appdata::AppData;
|
use appdata::AppData;
|
||||||
use model::misc::Game;
|
use model::misc::Game;
|
||||||
use pkg::PkgKey;
|
use pkg::PkgKey;
|
||||||
use profile::Profile;
|
use tauri::{AppHandle, Listener, Manager};
|
||||||
use tauri::{Listener, Manager};
|
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
use tauri_plugin_cli::CliExt;
|
use tauri_plugin_cli::CliExt;
|
||||||
use tokio::{sync::Mutex, fs, try_join};
|
use tokio::{sync::Mutex, fs, try_join};
|
||||||
@ -40,28 +37,7 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
.expect("No main window")
|
.expect("No main window")
|
||||||
.set_focus();
|
.set_focus();
|
||||||
if args.len() == 2 {
|
if args.len() == 2 {
|
||||||
// Todo deindent this chimera
|
deep_link(app.clone(), args);
|
||||||
let url = &args[1];
|
|
||||||
if &url[..13] == "rainycolor://" {
|
|
||||||
log::info!("Deep link: {}", url);
|
|
||||||
let regex = regex::Regex::new(
|
|
||||||
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
|
|
||||||
).expect("Invalid regex");
|
|
||||||
if let Some(caps) = regex.captures(url) {
|
|
||||||
if caps.len() == 3 {
|
|
||||||
let apph = app.clone();
|
|
||||||
let key = PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()));
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
let mutex = apph.state::<Mutex<AppData>>();
|
|
||||||
let mut appd = mutex.lock().await;
|
|
||||||
_ = appd.pkgs.fetch_listings().await;
|
|
||||||
if let Err(e) = appd.pkgs.install_package(&key, true, true).await {
|
|
||||||
log::warn!("Fail: {}", e.to_string());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.plugin(tauri_plugin_cli::init())
|
.plugin(tauri_plugin_cli::init())
|
||||||
@ -85,7 +61,7 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg);
|
log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg);
|
||||||
if start_arg.occurrences > 0 {
|
if start_arg.occurrences > 0 {
|
||||||
start_immediately = true;
|
start_immediately = true;
|
||||||
app_data.remain_open = false;
|
app_data.state.remain_open = false;
|
||||||
} 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")
|
||||||
@ -125,7 +101,6 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.listen("download-end", closure!(clone apph, |ev| {
|
app.listen("download-end", closure!(clone apph, |ev| {
|
||||||
let raw = ev.payload();
|
let raw = ev.payload();
|
||||||
let key = PkgKey(raw[1..raw.len()-1].to_owned());
|
let key = PkgKey(raw[1..raw.len()-1].to_owned());
|
||||||
@ -142,27 +117,24 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mutex = apph.state::<Mutex<AppData>>();
|
let mutex = apph.state::<Mutex<AppData>>();
|
||||||
let appd = mutex.lock().await;
|
let appd = mutex.lock().await;
|
||||||
if !appd.remain_open {
|
if !appd.state.remain_open {
|
||||||
apph.exit(0);
|
apph.exit(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if start_immediately == true {
|
if start_immediately == true {
|
||||||
let apph_clone = apph.clone();
|
let apph = apph.clone();
|
||||||
tauri::async_runtime::spawn(async {
|
tauri::async_runtime::spawn(async move {
|
||||||
let apph_clone_clone = apph_clone.clone();
|
let mtx = apph.state::<Mutex<AppData>>();
|
||||||
{
|
let mut appd = mtx.lock().await;
|
||||||
let mtx = apph_clone.state::<Mutex<AppData>>();
|
if let Err(e) = appd.pkgs.reload_all().await {
|
||||||
let mut appd = mtx.lock().await;
|
log::error!("Unable to reload packages: {}", e);
|
||||||
if let Err(e) = appd.pkgs.reload_all().await {
|
apph.exit(1);
|
||||||
log::error!("Unable to reload packages: {}", e);
|
|
||||||
apph_clone.exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if let Err(e) = cmd::startline(apph_clone).await {
|
if let Err(e) = cmd::startline(apph.clone()).await {
|
||||||
log::error!("Unable to launch: {}", e);
|
log::error!("Unable to launch: {}", e);
|
||||||
apph_clone_clone.exit(1);
|
apph.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -181,9 +153,9 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
cmd::list_profiles,
|
cmd::list_profiles,
|
||||||
cmd::init_profile,
|
cmd::init_profile,
|
||||||
cmd::load_profile,
|
cmd::load_profile,
|
||||||
|
cmd::rename_profile,
|
||||||
cmd::get_current_profile,
|
cmd::get_current_profile,
|
||||||
cmd::save_current_profile,
|
cmd::save_current_profile,
|
||||||
cmd::set_cfg,
|
|
||||||
|
|
||||||
cmd::startline,
|
cmd::startline,
|
||||||
cmd::kill,
|
cmd::kill,
|
||||||
@ -195,3 +167,31 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deep_link(app: AppHandle, args: Vec<String>) {
|
||||||
|
let url = &args[1];
|
||||||
|
let proto = "rainycolor://";
|
||||||
|
if &url[..proto.len()] == proto {
|
||||||
|
log::info!("Deep link: {}", url);
|
||||||
|
|
||||||
|
let regex = regex::Regex::new(
|
||||||
|
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
|
||||||
|
).expect("Invalid regex");
|
||||||
|
|
||||||
|
if let Some(caps) = regex.captures(url) {
|
||||||
|
if caps.len() == 3 {
|
||||||
|
let app = app.clone();
|
||||||
|
let key = PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()));
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let mutex = app.state::<Mutex<AppData>>();
|
||||||
|
let mut appd = mutex.lock().await;
|
||||||
|
if let Err(e) = appd.pkgs.fetch_listings().await {
|
||||||
|
log::warn!("Deep link fetch failed: {:?}", e);
|
||||||
|
} else if let Err(e) = appd.pkgs.install_package(&key, true, true).await {
|
||||||
|
log::warn!("Deep link installation failed: {}", e.to_string());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,121 +0,0 @@
|
|||||||
use anyhow::{Result, anyhow};
|
|
||||||
use tokio::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use ini::Ini;
|
|
||||||
use crate::util;
|
|
||||||
use crate::profile::Profile;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
|
||||||
fs::symlink(src, dst).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
|
||||||
//std::os::windows::fs::junction_point(src, dst) // is unstable
|
|
||||||
junction::create(src, dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn line_up(p: &Profile, pkg_hash: String) -> Result<()> {
|
|
||||||
let dir_out = p.data_dir();
|
|
||||||
|
|
||||||
if dir_out.join("option").exists() {
|
|
||||||
fs::remove_dir_all(dir_out.join("option")).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::create_dir_all(dir_out.join("option")).await?;
|
|
||||||
|
|
||||||
let hash_path = p.data_dir().join(".sl-state");
|
|
||||||
let prev_hash = fs::read_to_string(&hash_path).await.unwrap_or_default();
|
|
||||||
if prev_hash != pkg_hash {
|
|
||||||
log::debug!("state {} -> {}", prev_hash, pkg_hash);
|
|
||||||
fs::write(hash_path, pkg_hash).await
|
|
||||||
.map_err(|e| anyhow!("Unable to write the state file: {}", e))?;
|
|
||||||
prepare_packages(p).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
prepare_config(p).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn prepare_packages(p: &Profile) -> Result<()> {
|
|
||||||
let dir_out = p.data_dir();
|
|
||||||
|
|
||||||
if dir_out.join("BepInEx").exists() {
|
|
||||||
fs::remove_dir_all(dir_out.join("BepInEx")).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for m in &p.data.mods {
|
|
||||||
log::debug!("Preparing {}", m);
|
|
||||||
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
|
||||||
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
|
|
||||||
.join("app")
|
|
||||||
.join("BepInEx");
|
|
||||||
if bpx_dir.exists() {
|
|
||||||
util::copy_recursive(&bpx_dir, &dir_out.join("BepInEx"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option");
|
|
||||||
if opt_dir.exists() {
|
|
||||||
let x = opt_dir.read_dir().unwrap().next().unwrap()?;
|
|
||||||
if x.metadata()?.is_dir() {
|
|
||||||
symlink(&x.path(), &dir_out.join("option").join(x.file_name())).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("prepare packages: done");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn prepare_config(p: &Profile) -> Result<()> {
|
|
||||||
let dir_out = p.data_dir();
|
|
||||||
|
|
||||||
let target_path = PathBuf::from(p.get_str("target-path", ""));
|
|
||||||
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
|
||||||
let ini_in_raw = fs::read_to_string(p.config_dir().join("segatools-base.ini")).await?;
|
|
||||||
let ini_in = Ini::load_from_str(&ini_in_raw)?;
|
|
||||||
let mut opt_dir_in = PathBuf::from(p.get_str("option", ""));
|
|
||||||
if opt_dir_in.as_os_str().len() > 0 && opt_dir_in.is_relative() {
|
|
||||||
opt_dir_in = exe_dir.join(opt_dir_in);
|
|
||||||
}
|
|
||||||
let opt_dir_out = &dir_out.join("option");
|
|
||||||
|
|
||||||
let mut ini_out = ini_in.clone();
|
|
||||||
ini_out.with_section(Some("vfs"))
|
|
||||||
.set(
|
|
||||||
"option",
|
|
||||||
util::path_to_str(opt_dir_out)?
|
|
||||||
)
|
|
||||||
.set("amfs", p.get_str("amfs", ""))
|
|
||||||
.set("appdata", p.get_str("appdata", "appdata")
|
|
||||||
);
|
|
||||||
ini_out.with_section(Some("unity"))
|
|
||||||
.set("enable", "1")
|
|
||||||
.set(
|
|
||||||
"targetAssembly",
|
|
||||||
util::path_to_str(dir_out.join("BepInEx").join("core").join("BepInEx.Preloader.dll"))?
|
|
||||||
);
|
|
||||||
|
|
||||||
if p.get_bool("aime", false) {
|
|
||||||
ini_out.with_section(Some("aime"))
|
|
||||||
.set("enable", "1")
|
|
||||||
.set("aimePath", util::path_to_str(dir_out.join("aime.txt"))?);
|
|
||||||
}
|
|
||||||
|
|
||||||
ini_out.write_to_file(dir_out.join("segatools.ini"))?;
|
|
||||||
|
|
||||||
log::debug!("Option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
|
||||||
if opt_dir_in.as_os_str().len() > 0 {
|
|
||||||
for opt in opt_dir_in.read_dir()? {
|
|
||||||
let opt = opt?;
|
|
||||||
symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("prepare config: done");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
54
rust/src/model/config.rs
Normal file
54
rust/src/model/config.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
pub struct Segatools {
|
||||||
|
pub target: PathBuf,
|
||||||
|
pub amfs: PathBuf,
|
||||||
|
pub option: PathBuf,
|
||||||
|
pub appdata: PathBuf,
|
||||||
|
pub enable_aime: bool,
|
||||||
|
pub intel: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
|
||||||
|
pub enum DisplayMode {
|
||||||
|
Window,
|
||||||
|
#[default] Borderless,
|
||||||
|
Fullscreen
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
pub struct Display {
|
||||||
|
pub target: String,
|
||||||
|
pub rez: (i32, i32),
|
||||||
|
pub mode: DisplayMode,
|
||||||
|
pub rotation: i32,
|
||||||
|
pub frequency: i32,
|
||||||
|
pub borderless_fullscreen: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
|
||||||
|
pub enum NetworkType {
|
||||||
|
#[default] Remote,
|
||||||
|
Artemis,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||||
|
pub struct Network {
|
||||||
|
pub network_type: NetworkType,
|
||||||
|
|
||||||
|
pub local_path: String,
|
||||||
|
pub local_console: bool,
|
||||||
|
|
||||||
|
pub remote_address: String,
|
||||||
|
pub keychip: String,
|
||||||
|
|
||||||
|
pub subnet: String,
|
||||||
|
pub suffix: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||||
|
pub struct BepInEx {
|
||||||
|
pub console: bool
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||||
pub enum Game {
|
pub enum Game {
|
||||||
#[serde(rename = "ongeki")]
|
#[serde(rename = "ongeki")]
|
||||||
Ongeki,
|
Ongeki,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
pub mod local;
|
pub mod local;
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
pub mod rainy;
|
pub mod rainy;
|
||||||
|
pub mod config;
|
||||||
|
pub mod segatools_base;
|
84
rust/src/model/segatools_base.rs
Normal file
84
rust/src/model/segatools_base.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
pub fn segatools_base() -> String {
|
||||||
|
"; mu3io is TBD
|
||||||
|
[mu3io]
|
||||||
|
path=
|
||||||
|
|
||||||
|
[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: Set this to 1 on all machines.
|
||||||
|
dipsw1=1
|
||||||
|
|
||||||
|
[gfx]
|
||||||
|
; Enables the graphics hook.
|
||||||
|
enable=1
|
||||||
|
|
||||||
|
[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\\ongeki_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
|
||||||
|
|
||||||
|
[io4]
|
||||||
|
; Test button virtual-key code. Default is the F1 key.
|
||||||
|
test=0x70
|
||||||
|
; Service button virtual-key code. Default is the F2 key.
|
||||||
|
service=0x71
|
||||||
|
; Keyboard button to increment coin counter. Default is the F3 key.
|
||||||
|
coin=0x72
|
||||||
|
|
||||||
|
; Set \"1\" to enable mouse lever emulation, \"0\" to use XInput
|
||||||
|
mouse=1
|
||||||
|
|
||||||
|
; XInput input bindings
|
||||||
|
;
|
||||||
|
; Left Stick Lever
|
||||||
|
; Left Trigger Lever (move to the left)
|
||||||
|
; Right Trigger Lever (move to the right)
|
||||||
|
; Left Left red button
|
||||||
|
; Up Left green button
|
||||||
|
; Right Left blue button
|
||||||
|
; Left Shoulder Left side button
|
||||||
|
; Right Shoulder Right side button
|
||||||
|
; X Right red button
|
||||||
|
; Y Right green button
|
||||||
|
; A Right blue button
|
||||||
|
; Back Left menu button
|
||||||
|
; Start Right menu button
|
||||||
|
|
||||||
|
; Keyboard input bindings
|
||||||
|
left1=0x41 ; A
|
||||||
|
left2=0x53 ; S
|
||||||
|
left3=0x44 ; D
|
||||||
|
|
||||||
|
leftSide=0x01 ; Mouse Left
|
||||||
|
rightSide=0x02 ; Mouse Right
|
||||||
|
|
||||||
|
right1=0x4A ; J
|
||||||
|
right2=0x4B ; K
|
||||||
|
right3=0x4C ; L
|
||||||
|
|
||||||
|
leftMenu=0x55 ; U
|
||||||
|
rightMenu=0x4F ; O".to_owned()
|
||||||
|
}
|
22
rust/src/modules/bepinex.rs
Normal file
22
rust/src/modules/bepinex.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use ini::Ini;
|
||||||
|
use crate::{model::config::BepInEx, profiles::ProfilePaths};
|
||||||
|
|
||||||
|
impl BepInEx {
|
||||||
|
pub fn line_up(&self, p: &impl ProfilePaths) -> Result<()> {
|
||||||
|
let dir = p.data_dir().join("BepInEx");
|
||||||
|
|
||||||
|
if dir.exists() && dir.is_dir() {
|
||||||
|
let dir = dir.join("config");
|
||||||
|
std::fs::create_dir_all(&dir)?;
|
||||||
|
let mut ini = Ini::new();
|
||||||
|
|
||||||
|
ini.with_section(Some("Logging.Console"))
|
||||||
|
.set("Enabled", if self.console { "true" } else { "false" });
|
||||||
|
|
||||||
|
ini.write_to_file(dir.join("BepInEx.cfg"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
139
rust/src/modules/display.rs
Normal file
139
rust/src/modules/display.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
|
||||||
|
use crate::model::config::{Display, DisplayMode};
|
||||||
|
use anyhow::Result;
|
||||||
|
use displayz::{query_displays, DisplaySet};
|
||||||
|
use tauri::{AppHandle, Listener};
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DisplayInfo {
|
||||||
|
pub primary: String,
|
||||||
|
pub set: Option<DisplaySet>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DisplayInfo {
|
||||||
|
fn default() -> Self {
|
||||||
|
DisplayInfo {
|
||||||
|
primary: "default".to_owned(),
|
||||||
|
set: query_displays().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Display {
|
||||||
|
fn default() -> Self {
|
||||||
|
Display {
|
||||||
|
target: "default".to_owned(),
|
||||||
|
rez: (1080, 1920),
|
||||||
|
mode: DisplayMode::Borderless,
|
||||||
|
rotation: 0,
|
||||||
|
frequency: 60,
|
||||||
|
borderless_fullscreen: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
impl Display {
|
||||||
|
pub fn activate(&self, app: AppHandle) {
|
||||||
|
let display = self.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let info = display.line_up()?;
|
||||||
|
if let Some(info) = info {
|
||||||
|
app.listen("launch-end", move |_| {
|
||||||
|
if let Err(e) = Self::clean_up(&info) {
|
||||||
|
log::error!("Error cleaning up display: {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_up(&self) -> Result<Option<DisplayInfo>> {
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use displayz::{query_displays, Orientation, Resolution, Frequency};
|
||||||
|
|
||||||
|
if self.target == "default" {
|
||||||
|
log::debug!("prepare display: skip");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let display_set = query_displays()?;
|
||||||
|
|
||||||
|
let primary = display_set
|
||||||
|
.displays()
|
||||||
|
.find(|display| display.is_primary())
|
||||||
|
.ok_or_else(|| anyhow!("Primary display not found"))?;
|
||||||
|
|
||||||
|
let target = display_set
|
||||||
|
.displays()
|
||||||
|
.find(|display| display.name() == self.target)
|
||||||
|
.ok_or_else(|| anyhow!("Display {} not found", self.target))?;
|
||||||
|
|
||||||
|
target.set_primary()?;
|
||||||
|
let settings = target.settings()
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
||||||
|
|
||||||
|
let res = DisplayInfo {
|
||||||
|
primary: primary.name().to_owned(),
|
||||||
|
set: Some(display_set.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.rotation == 90 || self.rotation == 270 {
|
||||||
|
let rez = settings.borrow_mut().resolution;
|
||||||
|
settings.borrow_mut().orientation = if self.rotation == 90 { Orientation::PortraitFlipped } else { Orientation::Portrait };
|
||||||
|
if rez.height < rez.width {
|
||||||
|
settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let frequency: u32 = self.frequency
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| anyhow!("Invalid display frequency: {}", e))?;
|
||||||
|
|
||||||
|
let width: u32 = self.rez.0
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| anyhow!("Invalid display width: {}", e))?;
|
||||||
|
|
||||||
|
let height: u32 = self.rez.1
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| anyhow!("Invalid display height: {}", e))?;
|
||||||
|
|
||||||
|
settings.borrow_mut().frequency = Frequency::new(frequency);
|
||||||
|
|
||||||
|
if self.borderless_fullscreen && self.mode == DisplayMode::Borderless {
|
||||||
|
settings.borrow_mut().resolution = Resolution::new(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
display_set.apply()?;
|
||||||
|
displayz::refresh()?;
|
||||||
|
|
||||||
|
log::debug!("prepare display: done");
|
||||||
|
|
||||||
|
Ok(Some(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_up(info: &DisplayInfo) -> Result<()> {
|
||||||
|
use anyhow::anyhow;
|
||||||
|
|
||||||
|
let display_set = info.set.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
|
||||||
|
|
||||||
|
let primary = display_set
|
||||||
|
.displays()
|
||||||
|
.find(|display| display.name() == info.primary)
|
||||||
|
.ok_or_else(|| anyhow!("Display {} not found", info.primary))?;
|
||||||
|
|
||||||
|
primary.set_primary()?;
|
||||||
|
|
||||||
|
display_set.apply()?;
|
||||||
|
displayz::refresh()?;
|
||||||
|
|
||||||
|
log::debug!("undo display: done");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
5
rust/src/modules/mod.rs
Normal file
5
rust/src/modules/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod display;
|
||||||
|
pub mod package;
|
||||||
|
pub mod segatools;
|
||||||
|
pub mod network;
|
||||||
|
pub mod bepinex;
|
67
rust/src/modules/network.rs
Normal file
67
rust/src/modules/network.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
use std::{path::PathBuf, process::Command};
|
||||||
|
use yaml_rust2::YamlLoader;
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use ini::Ini;
|
||||||
|
use crate::model::config::{Network, NetworkType};
|
||||||
|
|
||||||
|
impl Network {
|
||||||
|
pub fn line_up(&self, ini: &mut Ini) -> Result<()> {
|
||||||
|
log::debug!("begin line-up: network");
|
||||||
|
|
||||||
|
ini.with_section(Some("dns")).set("default", &self.remote_address);
|
||||||
|
|
||||||
|
let mut section_netenv = ini.with_section(Some("netenv"));
|
||||||
|
|
||||||
|
section_netenv.set("enable", "1");
|
||||||
|
|
||||||
|
if let Some(suffix) = self.suffix {
|
||||||
|
section_netenv.set("addrSuffix", suffix.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut section_keychip = ini.with_section(Some("keychip"));
|
||||||
|
|
||||||
|
if self.subnet.len() > 0 {
|
||||||
|
section_keychip.set("subnet", &self.subnet);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.network_type == NetworkType::Artemis {
|
||||||
|
let network_path = PathBuf::from(&self.local_path);
|
||||||
|
let artemis_dir = network_path.parent()
|
||||||
|
.ok_or_else(|| anyhow!("Invalid ARTEMiS path {}", &self.local_path))?;
|
||||||
|
let cfg_path = artemis_dir.join("config").join("core.yaml");
|
||||||
|
|
||||||
|
let cfg = std::fs::read_to_string(&cfg_path)
|
||||||
|
.map_err(|e| anyhow!("Unable to open core.yaml: {}", e))?;
|
||||||
|
let cfg = YamlLoader::load_from_str(&cfg)
|
||||||
|
.map_err(|e| anyhow!("Unable to read core.yaml: {}", e))?;
|
||||||
|
let cfg = &cfg[0];
|
||||||
|
log::debug!("{:?}", cfg);
|
||||||
|
let hostname = &cfg["server"]["hostname"];
|
||||||
|
let hostname = hostname.clone().into_string();
|
||||||
|
if let Some(hostname) = hostname {
|
||||||
|
ini.with_section(Some("dns")).set("default", hostname);
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let mut cmd = Command::new("cmd.exe");
|
||||||
|
|
||||||
|
cmd.arg("/C");
|
||||||
|
|
||||||
|
if self.local_console == true {
|
||||||
|
cmd.arg("start");
|
||||||
|
}
|
||||||
|
cmd.args(["python", &self.local_path]);
|
||||||
|
cmd.current_dir(artemis_dir);
|
||||||
|
cmd.spawn()
|
||||||
|
.map_err(|e| anyhow!("Unable to spawn artemis: {}", e))?;
|
||||||
|
} else {
|
||||||
|
log::warn!("unable to parse the artemis hostname");
|
||||||
|
}
|
||||||
|
} else if self.keychip.len() > 0 {
|
||||||
|
section_keychip.set("id", &self.keychip);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("end line-up: network");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
43
rust/src/modules/package.rs
Normal file
43
rust/src/modules/package.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use crate::pkg::PkgKey;
|
||||||
|
use crate::util;
|
||||||
|
use crate::profiles::ProfilePaths;
|
||||||
|
|
||||||
|
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>) -> Result<()> {
|
||||||
|
log::debug!("begin prepare packages");
|
||||||
|
|
||||||
|
let pfx_dir = p.data_dir();
|
||||||
|
let opt_dir = pfx_dir.join("option");
|
||||||
|
|
||||||
|
if pfx_dir.join("BepInEx").exists() {
|
||||||
|
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opt_dir.exists() {
|
||||||
|
tokio::fs::create_dir(opt_dir).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for m in pkgs {
|
||||||
|
log::debug!("preparing {}", m);
|
||||||
|
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
||||||
|
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
|
||||||
|
.join("app")
|
||||||
|
.join("BepInEx");
|
||||||
|
if bpx_dir.exists() {
|
||||||
|
util::copy_recursive(&bpx_dir, &pfx_dir.join("BepInEx"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option");
|
||||||
|
if opt_dir.exists() {
|
||||||
|
let x = opt_dir.read_dir().unwrap().next().unwrap()?;
|
||||||
|
if x.metadata()?.is_dir() {
|
||||||
|
util::symlink(&x.path(), &pfx_dir.join("option").join(x.file_name())).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("end prepare packages");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
93
rust/src/modules/segatools.rs
Normal file
93
rust/src/modules/segatools.rs
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use ini::Ini;
|
||||||
|
use crate::{model::{config::Segatools, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}};
|
||||||
|
|
||||||
|
impl Default for Segatools {
|
||||||
|
fn default() -> Self {
|
||||||
|
Segatools {
|
||||||
|
target: PathBuf::default(),
|
||||||
|
amfs: PathBuf::default(),
|
||||||
|
option: PathBuf::default(),
|
||||||
|
appdata: PathBuf::from("appdata"),
|
||||||
|
enable_aime: false,
|
||||||
|
intel: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Segatools {
|
||||||
|
pub async fn line_up(&self, p: &impl ProfilePaths) -> Result<Ini> {
|
||||||
|
log::debug!("begin line-up: segatools");
|
||||||
|
|
||||||
|
let pfx_dir = p.data_dir();
|
||||||
|
let exe_dir = self.target.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
||||||
|
|
||||||
|
log::debug!("segatools: {:?} {:?}", pfx_dir, exe_dir);
|
||||||
|
|
||||||
|
let ini_path = p.config_dir().join("segatools-base.ini");
|
||||||
|
|
||||||
|
if !ini_path.exists() {
|
||||||
|
tokio::fs::write(&ini_path, segatools_base()).await
|
||||||
|
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
|
||||||
|
}
|
||||||
|
if !pfx_dir.exists() {
|
||||||
|
tokio::fs::create_dir(&pfx_dir).await
|
||||||
|
.map_err(|e| anyhow!("Error creating {:?}: {}", pfx_dir, e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ini_in = tokio::fs::read_to_string(&ini_path).await?;
|
||||||
|
let ini_in = Ini::load_from_str(&ini_in)?;
|
||||||
|
|
||||||
|
let opt_dir_out = &pfx_dir.join("option");
|
||||||
|
let opt_dir_in = if self.option.as_os_str().len() > 0 && self.option.is_relative() {
|
||||||
|
exe_dir.join(&self.option)
|
||||||
|
} else {
|
||||||
|
self.option.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ini_out = ini_in.clone();
|
||||||
|
ini_out.with_section(Some("vfs"))
|
||||||
|
.set(
|
||||||
|
"option",
|
||||||
|
opt_dir_out.stringify()?
|
||||||
|
)
|
||||||
|
.set("amfs", self.amfs.stringify()?)
|
||||||
|
.set("appdata", self.appdata.stringify()?);
|
||||||
|
|
||||||
|
ini_out.with_section(Some("unity"))
|
||||||
|
.set("enable", "1")
|
||||||
|
.set(
|
||||||
|
"targetAssembly",
|
||||||
|
pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()?
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.enable_aime {
|
||||||
|
ini_out.with_section(Some("aime"))
|
||||||
|
.set("enable", "1")
|
||||||
|
.set("aimePath", p.config_dir().join("aime.txt").stringify()?);
|
||||||
|
} else {
|
||||||
|
ini_out.with_section(Some("aime"))
|
||||||
|
.set("enable", "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
||||||
|
|
||||||
|
if !opt_dir_out.exists() {
|
||||||
|
tokio::fs::create_dir(opt_dir_out).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt_dir_in.as_os_str().len() > 0 {
|
||||||
|
for opt in opt_dir_in.read_dir()? {
|
||||||
|
let opt = opt?;
|
||||||
|
util::symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("end line-up: segatools");
|
||||||
|
|
||||||
|
Ok(ini_out)
|
||||||
|
}
|
||||||
|
}
|
@ -1,141 +0,0 @@
|
|||||||
use anyhow::{Result, anyhow};
|
|
||||||
use std::{collections::{BTreeSet, BTreeMap}, path::{Path, PathBuf}};
|
|
||||||
use crate::{model::misc::{self, Game}, pkg::PkgKey, util};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
|
||||||
pub struct Profile {
|
|
||||||
pub game: misc::Game,
|
|
||||||
pub name: String,
|
|
||||||
pub data: ProfileData
|
|
||||||
}
|
|
||||||
|
|
||||||
// The contents of profile-{game}-{name}.json
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
|
||||||
pub struct ProfileData {
|
|
||||||
pub mods: BTreeSet<PkgKey>,
|
|
||||||
// cfg is temporarily just a map to make iteration easier
|
|
||||||
// eventually it should become strict
|
|
||||||
pub cfg: BTreeMap<String, serde_json::Value>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Profile {
|
|
||||||
pub fn new(game: Game, mut name: String) -> Profile {
|
|
||||||
name = name.trim().replace(" ", "-");
|
|
||||||
|
|
||||||
while Self::config_dir_f(&game, &name).exists() {
|
|
||||||
name = format!("new-{}", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
Profile {
|
|
||||||
name,
|
|
||||||
game,
|
|
||||||
data: ProfileData {
|
|
||||||
mods: BTreeSet::new(),
|
|
||||||
cfg: BTreeMap::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn config_dir_f(game: &Game, name: &str) -> PathBuf {
|
|
||||||
util::config_dir().join(format!("profile-{}-{}", game, name))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config_dir(&self) -> PathBuf {
|
|
||||||
Self::config_dir_f(&self.game, &self.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn data_dir(&self) -> PathBuf {
|
|
||||||
util::data_dir().join(format!("profile-{}-{}", self.game, self.name))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list() -> Result<Vec<(Game, String)>> {
|
|
||||||
let path = std::fs::read_dir(util::config_dir())?;
|
|
||||||
|
|
||||||
let mut res = Vec::new();
|
|
||||||
|
|
||||||
for f in path {
|
|
||||||
let f = f?;
|
|
||||||
|
|
||||||
if let Ok(meta) = f.metadata() {
|
|
||||||
if !meta.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
log::debug!("{:?}", f);
|
|
||||||
if let Some(pair) = Self::name_from_path(f.path()) {
|
|
||||||
res.push(pair);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(game: Game, name: String) -> Result<Profile> {
|
|
||||||
let path = Self::config_dir_f(&game, &name).join("profile.json");
|
|
||||||
if let Ok(s) = std::fs::read_to_string(&path) {
|
|
||||||
let data = serde_json::from_str::<ProfileData>(&s)
|
|
||||||
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
|
|
||||||
|
|
||||||
Ok(Profile {
|
|
||||||
game,
|
|
||||||
name,
|
|
||||||
data
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Unable to open {:?}", path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn save(&self) {
|
|
||||||
let path = self.config_dir().join("profile.json");
|
|
||||||
|
|
||||||
let s = serde_json::to_string_pretty(&self.data).unwrap();
|
|
||||||
fs::write(&path, s).await.unwrap();
|
|
||||||
log::info!("Written to {}", path.to_string_lossy());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn get_cfg(&self, key: &str) -> Result<&serde_json::Value> {
|
|
||||||
self.data.cfg.get(key)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Invalid config entry {}", key))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_bool(&self, key: &str, default: bool) -> bool {
|
|
||||||
self.data.cfg.get(key)
|
|
||||||
.and_then(|c| c.as_bool())
|
|
||||||
.unwrap_or(default)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_int(&self, key: &str, default: i64) -> i64 {
|
|
||||||
self.data.cfg.get(key)
|
|
||||||
.and_then(|c| c.as_i64())
|
|
||||||
.unwrap_or(default)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_str(&self, key: &str, default: &str) -> String {
|
|
||||||
self.data.cfg.get(key)
|
|
||||||
.and_then(|c| c.as_str())
|
|
||||||
.unwrap_or(default)
|
|
||||||
.to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name_from_path(path: impl AsRef<Path>) -> Option<(Game, String)> {
|
|
||||||
let regex = regex::Regex::new(
|
|
||||||
r"^profile-([^\-]+)-(.+)$"
|
|
||||||
).expect("Invalid regex");
|
|
||||||
|
|
||||||
let fname = path.as_ref().file_name().unwrap_or_default().to_string_lossy();
|
|
||||||
|
|
||||||
if let Some(caps) = regex.captures(&fname) {
|
|
||||||
let game = caps.get(1).unwrap().as_str();
|
|
||||||
let name = caps.get(2).unwrap().as_str().to_owned();
|
|
||||||
if let Some(game) = Game::from_str(game) {
|
|
||||||
return Some((game, name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
162
rust/src/profiles/mod.rs
Normal file
162
rust/src/profiles/mod.rs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use ongeki::OngekiProfile;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
||||||
|
use crate::{model::misc::Game, modules::package::prepare_packages, pkg::PkgKey, util};
|
||||||
|
|
||||||
|
pub mod ongeki;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
pub enum AnyProfile {
|
||||||
|
OngekiProfile(OngekiProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct ProfileMeta {
|
||||||
|
pub game: Game,
|
||||||
|
pub name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Profile: Sized {
|
||||||
|
fn new(name: String) -> Result<Self>;
|
||||||
|
fn load(name: String) -> Result<Self>;
|
||||||
|
fn save(&self) -> Result<()>;
|
||||||
|
async fn start(&self, app: AppHandle) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ProfilePaths {
|
||||||
|
fn config_dir(&self) -> PathBuf;
|
||||||
|
fn data_dir(&self) -> PathBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AnyProfile {
|
||||||
|
pub fn load(game: Game, name: String) -> Result<Self> {
|
||||||
|
Ok(match game {
|
||||||
|
Game::Ongeki => AnyProfile::OngekiProfile(OngekiProfile::load(name)?),
|
||||||
|
Game::Chunithm => panic!("Not implemented")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pub fn save(&self) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::OngekiProfile(p) => p.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn meta(&self) -> ProfileMeta {
|
||||||
|
match self {
|
||||||
|
Self::OngekiProfile(p) => {
|
||||||
|
ProfileMeta {
|
||||||
|
game: Game::Ongeki,
|
||||||
|
name: p.name.as_ref().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn rename(&mut self, name: String) {
|
||||||
|
match self {
|
||||||
|
Self::OngekiProfile(p) => {
|
||||||
|
p.name = Some(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pkgs(&self) -> &BTreeSet<PkgKey> {
|
||||||
|
match self {
|
||||||
|
Self::OngekiProfile(p) => &p.mods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn pkgs_mut(&mut self) -> &mut BTreeSet<PkgKey> {
|
||||||
|
match self {
|
||||||
|
Self::OngekiProfile(p) => &mut p.mods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn line_up(&self, app: AppHandle, pkg_hash: String) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::OngekiProfile(p) => {
|
||||||
|
if !p.data_dir().exists() {
|
||||||
|
tokio::fs::create_dir(p.data_dir()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hash_path = p.data_dir().join(".sl-state");
|
||||||
|
let meta = self.meta();
|
||||||
|
|
||||||
|
p.display.activate(app.clone());
|
||||||
|
|
||||||
|
util::clean_up_opts(p.data_dir().join("option"))?;
|
||||||
|
|
||||||
|
if Self::hash_check(&hash_path, &pkg_hash).await? == true {
|
||||||
|
prepare_packages(&meta, &p.mods).await
|
||||||
|
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
|
||||||
|
}
|
||||||
|
let mut ini = p.sgt.line_up(&meta).await
|
||||||
|
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
|
||||||
|
p.network.line_up(&mut ini)?;
|
||||||
|
|
||||||
|
ini.write_to_file(p.data_dir().join("segatools.ini"))
|
||||||
|
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
|
||||||
|
|
||||||
|
p.bepinex.line_up(&meta)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(&self, app: AppHandle) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::OngekiProfile(p) => p.start(app).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn hash_check(prev_hash_path: &impl AsRef<Path>, new_hash: &str) -> Result<bool> {
|
||||||
|
let prev_hash = tokio::fs::read_to_string(&prev_hash_path).await.unwrap_or_default();
|
||||||
|
if prev_hash != new_hash {
|
||||||
|
log::debug!("state {} -> {}", prev_hash, new_hash);
|
||||||
|
tokio::fs::write(prev_hash_path, new_hash).await
|
||||||
|
.map_err(|e| anyhow!("Unable to write the state file: {}", e))?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_profiles() -> Result<Vec<ProfileMeta>> {
|
||||||
|
let path = std::fs::read_dir(util::config_dir())?;
|
||||||
|
|
||||||
|
let mut res = Vec::new();
|
||||||
|
|
||||||
|
for f in path {
|
||||||
|
let f = f?;
|
||||||
|
|
||||||
|
if let Ok(meta) = f.metadata() {
|
||||||
|
if !meta.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log::debug!("{:?}", f);
|
||||||
|
if let Some(meta) = meta_from_path(f.path()) {
|
||||||
|
res.push(meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn meta_from_path(path: impl AsRef<Path>) -> Option<ProfileMeta> {
|
||||||
|
let regex = regex::Regex::new(
|
||||||
|
r"^profile-([^\-]+)-(.+)$"
|
||||||
|
).expect("Invalid regex");
|
||||||
|
|
||||||
|
let fname = path.as_ref().file_name().unwrap_or_default().to_string_lossy();
|
||||||
|
|
||||||
|
if let Some(caps) = regex.captures(&fname) {
|
||||||
|
let game = caps.get(1).unwrap().as_str();
|
||||||
|
let name = caps.get(2).unwrap().as_str().to_owned();
|
||||||
|
if let Some(game) = Game::from_str(game) {
|
||||||
|
return Some(ProfileMeta { game, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
234
rust/src/profiles/ongeki.rs
Normal file
234
rust/src/profiles/ongeki.rs
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::AppHandle;
|
||||||
|
use tauri::Emitter;
|
||||||
|
use std::{collections::BTreeSet, path::PathBuf, process::Stdio};
|
||||||
|
use crate::model::config::BepInEx;
|
||||||
|
use crate::util::PathStr;
|
||||||
|
use crate::{model::{config::{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
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Profile for OngekiProfile {
|
||||||
|
fn new(name: String) -> Result<Self> {
|
||||||
|
let mut fixed_name = name.trim().replace(" ", "-");
|
||||||
|
|
||||||
|
while util::profile_config_dir(&Game::Ongeki, &name).exists() {
|
||||||
|
fixed_name = format!("new-{}", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = OngekiProfile {
|
||||||
|
name: Some(fixed_name),
|
||||||
|
mods: BTreeSet::new(),
|
||||||
|
sgt: Segatools::default(),
|
||||||
|
display: Display::default(),
|
||||||
|
network: Network::default(),
|
||||||
|
bepinex: BepInEx::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.to_string_lossy());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start(&self, app: AppHandle) -> Result<()> {
|
||||||
|
let ini_path = self.data_dir().join("segatools.ini");
|
||||||
|
|
||||||
|
log::debug!("With path {}", ini_path.to_string_lossy());
|
||||||
|
|
||||||
|
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"))?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
game_builder = Command::new(exe_dir.join("inject.exe"));
|
||||||
|
amd_builder = Command::new("cmd.exe");
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let wine = p.data.wine_runtime.clone()
|
||||||
|
.unwrap_or_else(|| std::path::PathBuf::from("/usr/bin/wine"));
|
||||||
|
|
||||||
|
game_builder = Command::new(&wine);
|
||||||
|
amd_builder = Command::new(&wine);
|
||||||
|
|
||||||
|
game_builder.arg(exe_dir.join("inject.exe"));
|
||||||
|
amd_builder.arg("cmd.exe");
|
||||||
|
}
|
||||||
|
|
||||||
|
amd_builder.env(
|
||||||
|
"SEGATOOLS_CONFIG_PATH",
|
||||||
|
&ini_path,
|
||||||
|
)
|
||||||
|
.current_dir(&exe_dir)
|
||||||
|
.args([
|
||||||
|
"/C",
|
||||||
|
&exe_dir.join("inject.exe").stringify()?, "-d", "-k", "mu3hook.dll",
|
||||||
|
"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", "mu3hook.dll",
|
||||||
|
"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")]
|
||||||
|
{
|
||||||
|
let wineprefix = p.data.wine_prefix.clone().unwrap_or_else(||
|
||||||
|
directories::UserDirs::new()
|
||||||
|
.expect("No home directory")
|
||||||
|
.home_dir()
|
||||||
|
.join(".wine")
|
||||||
|
);
|
||||||
|
amd_builder.env("WINEPREFIX", &wineprefix);
|
||||||
|
game_builder.env("WINEPREFIX", &wineprefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@ -1,177 +0,0 @@
|
|||||||
use anyhow::{anyhow, Result};
|
|
||||||
use std::fs::File;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use tokio::process::Command;
|
|
||||||
use tauri::{AppHandle, Emitter};
|
|
||||||
use std::process::Stdio;
|
|
||||||
use crate::profile::Profile;
|
|
||||||
use crate::util;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
static CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
||||||
|
|
||||||
pub async fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|
||||||
use tokio::task::JoinSet;
|
|
||||||
|
|
||||||
let ini_path = p.data_dir().join("segatools.ini");
|
|
||||||
|
|
||||||
log::debug!("With path {}", ini_path.to_string_lossy());
|
|
||||||
|
|
||||||
let mut game_builder;
|
|
||||||
let mut amd_builder;
|
|
||||||
|
|
||||||
let target_path = PathBuf::from(p.get_str("target-path", ""));
|
|
||||||
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
let display_info = crate::display::prepare_display(p).await?;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
game_builder = Command::new(exe_dir.join("inject.exe"));
|
|
||||||
amd_builder = Command::new("cmd.exe");
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let wine = p.data.wine_runtime.clone()
|
|
||||||
.unwrap_or_else(|| std::path::PathBuf::from("/usr/bin/wine"));
|
|
||||||
|
|
||||||
game_builder = Command::new(&wine);
|
|
||||||
amd_builder = Command::new(&wine);
|
|
||||||
|
|
||||||
game_builder.arg(exe_dir.join("inject.exe"));
|
|
||||||
amd_builder.arg("cmd.exe");
|
|
||||||
}
|
|
||||||
|
|
||||||
let display_mode = p.get_str("display-mode", "borderless");
|
|
||||||
|
|
||||||
amd_builder.env(
|
|
||||||
"SEGATOOLS_CONFIG_PATH",
|
|
||||||
&ini_path,
|
|
||||||
)
|
|
||||||
.current_dir(&exe_dir)
|
|
||||||
.args([
|
|
||||||
"/C",
|
|
||||||
&util::path_to_str(exe_dir.join("inject.exe"))?, "-d", "-k", "mu3hook.dll",
|
|
||||||
"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",
|
|
||||||
p.config_dir().join("inohara.cfg"),
|
|
||||||
)
|
|
||||||
.current_dir(&exe_dir)
|
|
||||||
.args([
|
|
||||||
"-d", "-k", "mu3hook.dll",
|
|
||||||
"mu3.exe", "-monitor 1",
|
|
||||||
"-screen-width", &p.get_int("rez-w", 1080).to_string(),
|
|
||||||
"-screen-height", &p.get_int("rez-h", 1920).to_string(),
|
|
||||||
"-screen-fullscreen", if display_mode == "fullscreen" { "1" } else { "0" }
|
|
||||||
]);
|
|
||||||
|
|
||||||
if display_mode == "borderless" {
|
|
||||||
game_builder.arg("-popupwindow");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let wineprefix = p.data.wine_prefix.clone().unwrap_or_else(||
|
|
||||||
directories::UserDirs::new()
|
|
||||||
.expect("No home directory")
|
|
||||||
.home_dir()
|
|
||||||
.join(".wine")
|
|
||||||
);
|
|
||||||
amd_builder.env("WINEPREFIX", &wineprefix);
|
|
||||||
game_builder.env("WINEPREFIX", &wineprefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let amd_log = File::create(p.data_dir().join("amdaemon.log"))?;
|
|
||||||
let game_log = File::create(p.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(CREATE_NO_WINDOW);
|
|
||||||
game_builder.creation_flags(CREATE_NO_WINDOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.get_bool("intel", false) == true {
|
|
||||||
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
|
|
||||||
}
|
|
||||||
|
|
||||||
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()?;
|
|
||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
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" {
|
|
||||||
pkill("mu3.exe").await;
|
|
||||||
} else {
|
|
||||||
pkill("amdaemon.exe").await;
|
|
||||||
}
|
|
||||||
|
|
||||||
set.join_next().await.expect("No spawn").expect("No result");
|
|
||||||
|
|
||||||
log::debug!("Fin");
|
|
||||||
|
|
||||||
if let Err(e) = app.emit("launch-start", "") {
|
|
||||||
log::warn!("Unable to emit launch-end: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
if let Some(display_info) = display_info {
|
|
||||||
if let Err(e) = crate::display::undo_display(display_info).await {
|
|
||||||
log::error!("undo display failed: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub async fn pkill(process_name: &str) {
|
|
||||||
_ = Command::new("taskkill.exe").arg("/f").arg("/im").arg(process_name)
|
|
||||||
.creation_flags(CREATE_NO_WINDOW).output().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub async fn pkill(process_name: &str) {
|
|
||||||
_ = Command::new("pkill").arg(process_name)
|
|
||||||
.output().await;
|
|
||||||
}
|
|
@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use tauri::{AppHandle, Manager};
|
use tauri::{AppHandle, Manager};
|
||||||
use std::{path::{Path, PathBuf}, sync::OnceLock};
|
use std::{path::{Path, PathBuf}, sync::OnceLock};
|
||||||
|
|
||||||
|
use crate::model::misc::Game;
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
static NAME: &str = "startliner";
|
static NAME: &str = "startliner";
|
||||||
|
|
||||||
@ -22,9 +24,9 @@ pub fn init_dirs(apph: &AppHandle) {
|
|||||||
DIRS.get_or_init(|| {
|
DIRS.get_or_init(|| {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
Dirs {
|
Dirs {
|
||||||
config_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME).join("cfg"),
|
config_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME),
|
||||||
data_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME).join("data"),
|
data_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME).join("data"),
|
||||||
cache_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME),
|
cache_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME).join("cache"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Dirs {
|
Dirs {
|
||||||
@ -44,6 +46,10 @@ 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 {
|
||||||
|
config_dir().join(format!("profile-{}-{}", game, name))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn data_dir() -> &'static Path {
|
pub fn data_dir() -> &'static Path {
|
||||||
&DIRS.get().expect("Directories uninitialized").data_dir
|
&DIRS.get().expect("Directories uninitialized").data_dir
|
||||||
}
|
}
|
||||||
@ -60,11 +66,6 @@ pub fn pkg_dir_of(namespace: &str, name: &str) -> PathBuf {
|
|||||||
pkg_dir().join(format!("{}-{}", namespace, name))
|
pkg_dir().join(format!("{}-{}", namespace, name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_to_str(p: impl AsRef<Path>) -> Result<String> {
|
|
||||||
Ok(p.as_ref().to_str()
|
|
||||||
.ok_or_else(|| anyhow!("Invalid path: {}", p.as_ref().to_string_lossy()))?.to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
|
pub fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
|
||||||
std::fs::create_dir_all(&dst).unwrap();
|
std::fs::create_dir_all(&dst).unwrap();
|
||||||
for entry in std::fs::read_dir(src)? {
|
for entry in std::fs::read_dir(src)? {
|
||||||
@ -78,3 +79,69 @@ pub fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
||||||
|
fs::symlink(src, dst).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
|
||||||
|
//std::os::windows::fs::junction_point(src, dst) // is unstable
|
||||||
|
junction::create(src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub static CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub async fn pkill(process_name: &str) {
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
_ = Command::new("taskkill.exe").arg("/f").arg("/im").arg(process_name)
|
||||||
|
.creation_flags(CREATE_NO_WINDOW).output().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub async fn pkill(process_name: &str) {
|
||||||
|
_ = Command::new("pkill").arg(process_name)
|
||||||
|
.output().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clean_up_opts(dir: impl AsRef<Path>) -> Result<()> {
|
||||||
|
log::debug!("begin clean_up_opts");
|
||||||
|
if dir.as_ref().is_dir() {
|
||||||
|
for entry in std::fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
log::debug!("{:?}", path);
|
||||||
|
if path.is_symlink() {
|
||||||
|
std::fs::remove_dir(path)?;
|
||||||
|
} else {
|
||||||
|
log::error!("Not a symlink: {:?}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!("end clean_up_opts");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PathStr {
|
||||||
|
fn stringify(&self) -> Result<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_to_str(p: impl AsRef<Path>) -> Result<String> {
|
||||||
|
Ok(p.as_ref().to_str().ok_or_else(|| anyhow!("Invalid path: {:?}", p.as_ref()))?.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathStr for Path {
|
||||||
|
fn stringify(&self) -> Result<String> {
|
||||||
|
path_to_str(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathStr for PathBuf {
|
||||||
|
fn stringify(&self) -> Result<String> {
|
||||||
|
path_to_str(&self)
|
||||||
|
}
|
||||||
|
}
|
@ -30,8 +30,7 @@ onMounted(async () => {
|
|||||||
general.dirs = d as Dirs;
|
general.dirs = d as Dirs;
|
||||||
});
|
});
|
||||||
|
|
||||||
await prf.reloadList();
|
await Promise.all([prf.reloadList(), prf.reload()]);
|
||||||
await prf.reload();
|
|
||||||
|
|
||||||
if (prf.current !== null) {
|
if (prf.current !== null) {
|
||||||
await pkg.reloadAll();
|
await pkg.reloadAll();
|
||||||
|
@ -2,25 +2,16 @@
|
|||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { usePrfStore } from '../stores';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
field: String,
|
|
||||||
default: String,
|
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
directory: Boolean,
|
directory: Boolean,
|
||||||
promptname: String,
|
promptname: String,
|
||||||
extension: String,
|
extension: String,
|
||||||
|
value: String,
|
||||||
|
callback: Function,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (props.field === undefined || props.default === undefined) {
|
|
||||||
throw new Error('Invalid FilePicker');
|
|
||||||
}
|
|
||||||
|
|
||||||
const prf = usePrfStore();
|
|
||||||
|
|
||||||
const cfg = prf.cfg(props.field, props.default);
|
|
||||||
|
|
||||||
const filePick = async () => {
|
const filePick = async () => {
|
||||||
const res = await open({
|
const res = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
@ -35,9 +26,9 @@ const filePick = async () => {
|
|||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
});
|
});
|
||||||
if (res != null) {
|
if (res != null && props.callback !== undefined) {
|
||||||
cfg.value =
|
props.callback(res);
|
||||||
/*path.relative(cfgs.current?.data.exe_dir ?? '', res) */ res;
|
/*path.relative(cfgs.current?.data.exe_dir ?? '', res) */
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -48,6 +39,7 @@ const filePick = async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
type="text"
|
type="text"
|
||||||
v-model="cfg"
|
:model-value="value"
|
||||||
|
@update:model-value="(value) => callback && callback(value)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -66,7 +66,7 @@ const aimeCodeModel = computed({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const extraDisplayOptionsDisabled = computed(() => {
|
const extraDisplayOptionsDisabled = computed(() => {
|
||||||
return prf.cfg('display', 'default').value === 'default';
|
return prf.current?.display.target === 'default';
|
||||||
});
|
});
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -79,16 +79,16 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
<OptionCategory title="General">
|
<OptionCategory title="General">
|
||||||
<OptionRow title="mu3.exe">
|
<OptionRow title="mu3.exe">
|
||||||
<FilePicker
|
<FilePicker
|
||||||
field="target-path"
|
|
||||||
default=""
|
|
||||||
:directory="false"
|
:directory="false"
|
||||||
promptname="mu3.exe"
|
promptname="mu3.exe"
|
||||||
extension="exe"
|
extension="exe"
|
||||||
|
:value="prf.current!.sgt.target"
|
||||||
|
:callback="(value: string) => (prf.current!.sgt.target = value)"
|
||||||
></FilePicker>
|
></FilePicker>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="mu3hook">
|
<OptionRow title="mu3hook">
|
||||||
<Select
|
<Select
|
||||||
:model-value="prf.cfg('hook', 'segatools-mu3hook')"
|
model-value="segatools-mu3hook"
|
||||||
:options="hookList"
|
:options="hookList"
|
||||||
option-label="title"
|
option-label="title"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
@ -96,24 +96,27 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="amfs">
|
<OptionRow title="amfs">
|
||||||
<FilePicker
|
<FilePicker
|
||||||
field="amfs"
|
|
||||||
default=""
|
|
||||||
placeholder="amfs"
|
|
||||||
:directory="true"
|
:directory="true"
|
||||||
|
placeholder="amfs"
|
||||||
|
:value="prf.current!.sgt.amfs"
|
||||||
|
:callback="(value: string) => (prf.current!.sgt.amfs = value)"
|
||||||
></FilePicker>
|
></FilePicker>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="option">
|
<OptionRow title="option">
|
||||||
<FilePicker
|
<FilePicker
|
||||||
field="option"
|
|
||||||
default="option"
|
|
||||||
:directory="true"
|
:directory="true"
|
||||||
|
placeholder="option"
|
||||||
|
:value="prf.current!.sgt.option"
|
||||||
|
:callback="(value: string) => (prf.current!.sgt.option = value)"
|
||||||
></FilePicker>
|
></FilePicker>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="appdata">
|
<OptionRow title="appdata">
|
||||||
<FilePicker
|
<FilePicker
|
||||||
field="appdata"
|
|
||||||
default="appdata"
|
|
||||||
:directory="true"
|
:directory="true"
|
||||||
|
:value="prf.current!.sgt.appdata"
|
||||||
|
:callback="
|
||||||
|
(value: string) => (prf.current!.sgt.appdata = value)
|
||||||
|
"
|
||||||
></FilePicker>
|
></FilePicker>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
</OptionCategory>
|
</OptionCategory>
|
||||||
@ -123,7 +126,7 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
title="Target display"
|
title="Target display"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
:model-value="prf.cfg('display', 'default')"
|
v-model="prf.current!.display.target"
|
||||||
:options="displayList"
|
:options="displayList"
|
||||||
option-label="title"
|
option-label="title"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
@ -136,7 +139,7 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
:min="480"
|
:min="480"
|
||||||
:max="9999"
|
:max="9999"
|
||||||
:use-grouping="false"
|
:use-grouping="false"
|
||||||
:model-value="prf.cfgAny('rez-w', 1080)"
|
v-model="prf.current!.display.rez[0]"
|
||||||
/>
|
/>
|
||||||
x
|
x
|
||||||
<InputNumber
|
<InputNumber
|
||||||
@ -145,17 +148,18 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
:min="640"
|
:min="640"
|
||||||
:max="9999"
|
:max="9999"
|
||||||
:use-grouping="false"
|
:use-grouping="false"
|
||||||
:model-value="prf.cfgAny('rez-h', 1920)"
|
v-model="prf.current!.display.rez[1]"
|
||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="Display mode">
|
<OptionRow title="Display mode">
|
||||||
<SelectButton
|
<SelectButton
|
||||||
:model-value="prf.cfg('display-mode', 'borderless')"
|
v-model="prf.current!.display.mode"
|
||||||
:options="[
|
:options="[
|
||||||
{ title: 'Window', value: 'window' },
|
{ title: 'Window', value: 'Window' },
|
||||||
{ title: 'Borderless window', value: 'borderless' },
|
{ title: 'Borderless window', value: 'Borderless' },
|
||||||
{ title: 'Fullscreen', value: 'fullscreen' },
|
{ title: 'Fullscreen', value: 'Fullscreen' },
|
||||||
]"
|
]"
|
||||||
|
:allow-empty="false"
|
||||||
option-label="title"
|
option-label="title"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
/>
|
/>
|
||||||
@ -165,12 +169,13 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
v-if="capabilities.includes('display')"
|
v-if="capabilities.includes('display')"
|
||||||
>
|
>
|
||||||
<SelectButton
|
<SelectButton
|
||||||
:model-value="prf.cfg('display-rotation', 0)"
|
v-model="prf.current!.display.rotation"
|
||||||
:options="[
|
:options="[
|
||||||
{ title: 'Unchanged', value: 0 },
|
{ title: 'Unchanged', value: 0 },
|
||||||
{ title: 'Portrait', value: 90 },
|
{ title: 'Portrait', value: 90 },
|
||||||
{ title: 'Portrait (flipped)', value: 270 },
|
{ title: 'Portrait (flipped)', value: 270 },
|
||||||
]"
|
]"
|
||||||
|
:allow-empty="false"
|
||||||
option-label="title"
|
option-label="title"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
:disabled="extraDisplayOptionsDisabled"
|
:disabled="extraDisplayOptionsDisabled"
|
||||||
@ -183,7 +188,7 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
:min="60"
|
:min="60"
|
||||||
:max="999"
|
:max="999"
|
||||||
:use-grouping="false"
|
:use-grouping="false"
|
||||||
:model-value="prf.cfgAny('frequency', 60)"
|
v-model="prf.current!.display.frequency"
|
||||||
:disabled="extraDisplayOptionsDisabled"
|
:disabled="extraDisplayOptionsDisabled"
|
||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
@ -191,32 +196,69 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
title="Match display resolution with the game"
|
title="Match display resolution with the game"
|
||||||
v-if="capabilities.includes('display')"
|
v-if="capabilities.includes('display')"
|
||||||
>
|
>
|
||||||
<!-- @vue-expect-error -->
|
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
:disabled="
|
:disabled="
|
||||||
extraDisplayOptionsDisabled ||
|
extraDisplayOptionsDisabled ||
|
||||||
prf.cfg('display-mode', 'borderless').value != 'borderless'
|
prf.current?.display.mode !== 'Borderless'
|
||||||
"
|
"
|
||||||
:model-value="prf.cfg('borderless-fullscreen', false)"
|
v-model="prf.current!.display.borderless_fullscreen"
|
||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
</OptionCategory>
|
</OptionCategory>
|
||||||
<OptionCategory title="Network">
|
<OptionCategory title="Network">
|
||||||
<OptionRow title="Server address">
|
<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
|
<InputText
|
||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
:maxlength="40"
|
:maxlength="40"
|
||||||
placeholder="192.168.1.234"
|
placeholder="192.168.1.234"
|
||||||
:model-value="prf.cfgAny<string>('dns-default', '')"
|
v-model="prf.current!.network.remote_address"
|
||||||
/> </OptionRow
|
/> </OptionRow
|
||||||
><OptionRow title="Keychip">
|
><OptionRow
|
||||||
|
v-if="prf.current!.network.network_type == 'Remote'"
|
||||||
|
title="Keychip"
|
||||||
|
>
|
||||||
<InputText
|
<InputText
|
||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
:maxlength="16"
|
:maxlength="16"
|
||||||
placeholder="A123-01234567890"
|
placeholder="A123-01234567890"
|
||||||
:model-value="prf.cfgAny('keychip', '')"
|
v-model="prf.current!.network.keychip"
|
||||||
/> </OptionRow
|
/> </OptionRow
|
||||||
><OptionRow title="Subnet">
|
><OptionRow title="Subnet">
|
||||||
<InputText
|
<InputText
|
||||||
@ -224,33 +266,33 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
size="small"
|
size="small"
|
||||||
:maxlength="15"
|
:maxlength="15"
|
||||||
placeholder="192.168.1.0"
|
placeholder="192.168.1.0"
|
||||||
:model-value="prf.cfgAny('subnet', '')"
|
v-model="prf.current!.network.subnet"
|
||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="Address suffix">
|
<OptionRow title="Address suffix">
|
||||||
<InputText
|
<InputNumber
|
||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
:maxlength="3"
|
:maxlength="3"
|
||||||
|
:min="0"
|
||||||
|
:max="255"
|
||||||
placeholder="12"
|
placeholder="12"
|
||||||
:model-value="prf.cfgAny('addrsuffix', '')"
|
v-model="prf.current!.network.suffix"
|
||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
</OptionCategory>
|
</OptionCategory>
|
||||||
<OptionCategory title="Misc">
|
<OptionCategory title="Misc">
|
||||||
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
|
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
|
||||||
<!-- @vue-expect-error -->
|
<ToggleSwitch v-model="prf.current!.sgt.intel" />
|
||||||
<ToggleSwitch :model-value="prf.cfg('intel', false)" />
|
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="Aime emulation">
|
<OptionRow title="Aime emulation">
|
||||||
<!-- @vue-expect-error -->
|
<ToggleSwitch v-model="prf.current!.sgt.enable_aime" />
|
||||||
<ToggleSwitch :model-value="prf.cfg('aime', false)" />
|
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="Aime code">
|
<OptionRow title="Aime code">
|
||||||
<InputText
|
<InputText
|
||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="prf.cfg<boolean>('aime', false).value !== true"
|
:disabled="prf.current?.sgt.enable_aime !== true"
|
||||||
:maxlength="20"
|
:maxlength="20"
|
||||||
placeholder="00000000000000000000"
|
placeholder="00000000000000000000"
|
||||||
v-model="aimeCodeModel"
|
v-model="aimeCodeModel"
|
||||||
@ -268,6 +310,9 @@ const extraDisplayOptionsDisabled = computed(() => {
|
|||||||
extension="cfg"
|
extension="cfg"
|
||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
|
<OptionRow title="BepInEx console">
|
||||||
|
<ToggleSwitch v-model="prf.current!.bepinex.console" />
|
||||||
|
</OptionRow>
|
||||||
</OptionCategory>
|
</OptionCategory>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
|
import ProfileListEntry from './ProfileListEntry.vue';
|
||||||
import { usePrfStore } from '../stores';
|
import { usePrfStore } from '../stores';
|
||||||
|
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
@ -18,70 +19,25 @@ 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">
|
||||||
<div v-for="p in prf.list">
|
<div v-for="p in prf.list">
|
||||||
<div class="flex flex-row flex-wrap align-middle gap-2">
|
<ProfileListEntry :p="p" />
|
||||||
<Button
|
|
||||||
:disabled="
|
|
||||||
prf.current?.game === p.game &&
|
|
||||||
prf.current?.name === p.name
|
|
||||||
"
|
|
||||||
:label="p.name"
|
|
||||||
:class="
|
|
||||||
(p.game === 'chunithm'
|
|
||||||
? 'chunithm-button'
|
|
||||||
: 'ongeki-button') +
|
|
||||||
' ' +
|
|
||||||
'self-center profile-button'
|
|
||||||
"
|
|
||||||
@click="prf.switchTo(p.game, p.name)"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
rounded
|
|
||||||
icon="pi pi-trash"
|
|
||||||
severity="danger"
|
|
||||||
aria-label="remove"
|
|
||||||
size="small"
|
|
||||||
class="self-center ml-2"
|
|
||||||
style="width: 2rem; height: 2rem"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
rounded
|
|
||||||
icon="pi pi-clone"
|
|
||||||
severity="warn"
|
|
||||||
aria-label="duplicate"
|
|
||||||
size="small"
|
|
||||||
class="self-center"
|
|
||||||
style="width: 2rem; height: 2rem"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
rounded
|
|
||||||
icon="pi pi-pencil"
|
|
||||||
severity="help"
|
|
||||||
aria-label="duplicate"
|
|
||||||
size="small"
|
|
||||||
class="self-center"
|
|
||||||
style="width: 2rem; height: 2rem"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.profile-button {
|
.profile-button {
|
||||||
width: 14em;
|
width: 14em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ongeki-button {
|
.ongeki-button {
|
||||||
background-color: var(--p-pink-400);
|
background-color: var(--p-pink-400) !important;
|
||||||
border-color: var(--p-pink-400);
|
border-color: var(--p-pink-400) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ongeki-button:hover,
|
.ongeki-button:hover,
|
||||||
@ -91,8 +47,8 @@ const prf = usePrfStore();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chunithm-button {
|
.chunithm-button {
|
||||||
background-color: var(--p-yellow-400);
|
background-color: var(--p-yellow-400) !important;
|
||||||
border-color: var(--p-yellow-400);
|
border-color: var(--p-yellow-400) !important;
|
||||||
}
|
}
|
||||||
.chunithm-button:hover,
|
.chunithm-button:hover,
|
||||||
.chunithm-button:active {
|
.chunithm-button:active {
|
||||||
|
113
src/components/ProfileListEntry.vue
Normal file
113
src/components/ProfileListEntry.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import * as path from '@tauri-apps/api/path';
|
||||||
|
import { open } from '@tauri-apps/plugin-shell';
|
||||||
|
import { useGeneralStore, usePrfStore } from '../stores';
|
||||||
|
import { ProfileMeta } from '../types';
|
||||||
|
|
||||||
|
const prf = usePrfStore();
|
||||||
|
const general = useGeneralStore();
|
||||||
|
const isEditing = ref(false);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
p: Object as () => ProfileMeta,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.p === undefined) {
|
||||||
|
throw new Error('Invalid ProfileListEntry');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rename = async (event: KeyboardEvent) => {
|
||||||
|
if (event.key !== 'Enter') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditing.value = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.target !== null &&
|
||||||
|
'value' in event.target &&
|
||||||
|
typeof event.target.value === 'string'
|
||||||
|
) {
|
||||||
|
const value = event.target.value
|
||||||
|
.replaceAll('..', '')
|
||||||
|
.replaceAll('\\', '')
|
||||||
|
.replaceAll('/', '');
|
||||||
|
if (value.length > 0) {
|
||||||
|
await prf.rename(props.p!, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-row flex-wrap align-middle gap-2">
|
||||||
|
<Button
|
||||||
|
:disabled="
|
||||||
|
prf.current?.game === p!.game && prf.current?.name === p!.name
|
||||||
|
"
|
||||||
|
:class="
|
||||||
|
(p!.game === 'chunithm' ? 'chunithm-button' : 'ongeki-button') +
|
||||||
|
' ' +
|
||||||
|
'self-center profile-button'
|
||||||
|
"
|
||||||
|
@click="prf.switchTo(p!.game, p!.name)"
|
||||||
|
>
|
||||||
|
<div v-if="!isEditing">{{ p!.name }}</div>
|
||||||
|
<div v-else>
|
||||||
|
<InputText
|
||||||
|
:model-value="p!.name"
|
||||||
|
@vue:mounted="$event?.el?.focus()"
|
||||||
|
@keyup="rename"
|
||||||
|
@focusout="isEditing = false"
|
||||||
|
>
|
||||||
|
</InputText></div
|
||||||
|
></Button>
|
||||||
|
<Button
|
||||||
|
rounded
|
||||||
|
icon="pi pi-trash"
|
||||||
|
severity="danger"
|
||||||
|
aria-label="remove"
|
||||||
|
size="small"
|
||||||
|
class="self-center ml-2"
|
||||||
|
style="width: 2rem; height: 2rem"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
rounded
|
||||||
|
icon="pi pi-clone"
|
||||||
|
severity="help"
|
||||||
|
aria-label="duplicate"
|
||||||
|
size="small"
|
||||||
|
class="self-center"
|
||||||
|
style="width: 2rem; height: 2rem"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
rounded
|
||||||
|
icon="pi pi-pencil"
|
||||||
|
severity="help"
|
||||||
|
aria-label="rename"
|
||||||
|
size="small"
|
||||||
|
class="self-center"
|
||||||
|
style="width: 2rem; height: 2rem"
|
||||||
|
@click="isEditing = true"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
rounded
|
||||||
|
icon="pi pi-folder"
|
||||||
|
severity="help"
|
||||||
|
aria-label="open-directory"
|
||||||
|
size="small"
|
||||||
|
class="self-center"
|
||||||
|
style="width: 2rem; height: 2rem"
|
||||||
|
@click="
|
||||||
|
path
|
||||||
|
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
|
||||||
|
.then(open)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -25,10 +25,10 @@ const kill = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const disabledTooltip = computed(() => {
|
const disabledTooltip = computed(() => {
|
||||||
if (prf.cfg('target-path', '').value.length === 0) {
|
if (prf.current?.sgt.target.length === 0) {
|
||||||
return 'The game path must be specified';
|
return 'The game path must be specified';
|
||||||
}
|
}
|
||||||
if (prf.cfg('amfs', '').value.length === 0) {
|
if (prf.current?.sgt.amfs.length === 0) {
|
||||||
return 'The amfs path must be specified';
|
return 'The amfs path must be specified';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Ref, computed, ref } from 'vue';
|
import { Ref, computed, ref, watchEffect } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import * as path from '@tauri-apps/api/path';
|
import * as path from '@tauri-apps/api/path';
|
||||||
@ -110,43 +110,25 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
() =>
|
() =>
|
||||||
pkg !== undefined &&
|
pkg !== undefined &&
|
||||||
current.value !== null &&
|
current.value !== null &&
|
||||||
current.value?.data.mods.includes(pkgKey(pkg))
|
current.value?.mods.includes(pkgKey(pkg))
|
||||||
);
|
);
|
||||||
|
|
||||||
const reload = async () => {
|
const reload = async () => {
|
||||||
current.value = await invoke('get_current_profile');
|
const p: any = await invoke('get_current_profile');
|
||||||
|
if (p['OngekiProfile'] !== undefined) {
|
||||||
|
current.value = { ...p.OngekiProfile, game: 'ongeki' };
|
||||||
|
}
|
||||||
if (current.value !== null) {
|
if (current.value !== null) {
|
||||||
changePrimaryColor(current.value.game);
|
changePrimaryColor(current.value.game);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
await invoke('save_current_profile');
|
|
||||||
};
|
|
||||||
|
|
||||||
const cfg = <T extends string | boolean | number>(key: string, dflt: T) =>
|
|
||||||
computed({
|
|
||||||
get() {
|
|
||||||
return (current.value?.data.cfg[key] as T | undefined) ?? dflt;
|
|
||||||
},
|
|
||||||
async set(value) {
|
|
||||||
if (value !== undefined) {
|
|
||||||
await invoke('set_cfg', { key, value: value });
|
|
||||||
await reload();
|
|
||||||
await save();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hack around PrimeVu not supporting WritableComputedRef
|
|
||||||
const cfgAny = <T extends string | boolean | number>(
|
|
||||||
key: string,
|
|
||||||
dflt: T
|
|
||||||
) => cfg(key, dflt) as any;
|
|
||||||
|
|
||||||
const create = async (game: Game) => {
|
const create = async (game: Game) => {
|
||||||
try {
|
try {
|
||||||
await invoke('init_profile', { game, name: 'new-profile' });
|
await invoke('init_profile', {
|
||||||
|
game,
|
||||||
|
name: 'new-profile',
|
||||||
|
});
|
||||||
await reload();
|
await reload();
|
||||||
await reloadList();
|
await reloadList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -159,6 +141,22 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rename = async (profile: ProfileMeta, name: string) => {
|
||||||
|
await invoke('rename_profile', {
|
||||||
|
profile,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
current.value?.game === profile.game &&
|
||||||
|
current.value.name === profile.name
|
||||||
|
) {
|
||||||
|
current.value.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
await reloadList();
|
||||||
|
};
|
||||||
|
|
||||||
const switchTo = async (game: Game, name: string) => {
|
const switchTo = async (game: Game, name: string) => {
|
||||||
await invoke('load_profile', { game, name });
|
await invoke('load_profile', { game, name });
|
||||||
await reload();
|
await reload();
|
||||||
@ -169,14 +167,9 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const reloadList = async () => {
|
const reloadList = async () => {
|
||||||
const raw = (await invoke('list_profiles')) as [Game, string][];
|
// list.value.splice(0, list.value.length);
|
||||||
|
list.value = (await invoke('list_profiles')) as ProfileMeta[];
|
||||||
list.value = raw.map(([game, name]) => {
|
console.log(list.value);
|
||||||
return {
|
|
||||||
game,
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePkg = async (pkg: Package | undefined, enable: boolean) => {
|
const togglePkg = async (pkg: Package | undefined, enable: boolean) => {
|
||||||
@ -185,7 +178,6 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
}
|
}
|
||||||
await invoke('toggle_package', { key: pkgKey(pkg), enable });
|
await invoke('toggle_package', { key: pkgKey(pkg), enable });
|
||||||
await reload();
|
await reload();
|
||||||
await save();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generalStore = useGeneralStore();
|
const generalStore = useGeneralStore();
|
||||||
@ -201,15 +193,21 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
await reload();
|
await reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watchEffect(async () => {
|
||||||
|
if (current.value !== null) {
|
||||||
|
await invoke('save_current_profile', {
|
||||||
|
profile: { OngekiProfile: current.value },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
current,
|
current,
|
||||||
list,
|
list,
|
||||||
isPkgEnabled,
|
isPkgEnabled,
|
||||||
reload,
|
reload,
|
||||||
save,
|
|
||||||
cfg,
|
|
||||||
cfgAny,
|
|
||||||
create,
|
create,
|
||||||
|
rename,
|
||||||
switchTo,
|
switchTo,
|
||||||
reloadList,
|
reloadList,
|
||||||
togglePkg,
|
togglePkg,
|
||||||
|
44
src/types.ts
44
src/types.ts
@ -26,13 +26,47 @@ export interface ProfileMeta {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Profile extends ProfileMeta {
|
export interface SegatoolsConfig {
|
||||||
data: {
|
target: string;
|
||||||
mods: string[];
|
amfs: string;
|
||||||
cfg: { [key: string]: string | boolean | number };
|
option: string;
|
||||||
};
|
appdata: string;
|
||||||
|
enable_aime: boolean;
|
||||||
|
intel: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DisplayConfig {
|
||||||
|
target: String;
|
||||||
|
rez: [number, number];
|
||||||
|
mode: 'Window' | 'Borderless' | 'Fullscreen';
|
||||||
|
rotation: number;
|
||||||
|
frequency: number;
|
||||||
|
borderless_fullscreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkConfig {
|
||||||
|
network_type: 'Remote' | 'Artemis';
|
||||||
|
local_path: string;
|
||||||
|
local_console: boolean;
|
||||||
|
remote_address: string;
|
||||||
|
keychip: string;
|
||||||
|
subnet: string;
|
||||||
|
suffix: number | null;
|
||||||
|
}
|
||||||
|
export interface BepInExConfig {
|
||||||
|
console: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Profile extends ProfileMeta {
|
||||||
|
mods: string[];
|
||||||
|
sgt: SegatoolsConfig;
|
||||||
|
display: DisplayConfig;
|
||||||
|
network: NetworkConfig;
|
||||||
|
bepinex: BepInExConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Module = 'sgt' | 'display' | 'network';
|
||||||
|
|
||||||
export interface Dirs {
|
export interface Dirs {
|
||||||
config_dir: string;
|
config_dir: string;
|
||||||
data_dir: string;
|
data_dir: string;
|
||||||
|
Reference in New Issue
Block a user