forked from akanyan/STARTLINER
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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arraydeque"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.10.2"
|
||||
@ -1480,6 +1486,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@ -1999,6 +2011,18 @@ name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "heck"
|
||||
@ -4566,6 +4590,7 @@ dependencies = [
|
||||
"tauri-plugin-single-instance",
|
||||
"tokio",
|
||||
"winsafe 0.0.23",
|
||||
"yaml-rust2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@ -6606,6 +6631,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "yoke"
|
||||
version = "0.7.5"
|
||||
|
@ -40,6 +40,7 @@ closure = "0.3.0"
|
||||
derive_more = { version = "2.0.1", features = ["display"] }
|
||||
junction = "1.2.0"
|
||||
tauri-plugin-fs = "2"
|
||||
yaml-rust2 = "0.10.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-cli = "2"
|
||||
|
@ -1,7 +1,8 @@
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use crate::profiles::AnyProfile;
|
||||
use crate::{model::misc::Game, pkg::PkgKey};
|
||||
use crate::pkg_store::PackageStore;
|
||||
use crate::{util, Profile};
|
||||
use crate::util;
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
@ -11,11 +12,15 @@ pub struct GlobalConfig {
|
||||
pub recent_profile: Option<(Game, String)>
|
||||
}
|
||||
|
||||
pub struct GlobalState {
|
||||
pub remain_open: bool
|
||||
}
|
||||
|
||||
pub struct AppData {
|
||||
pub profile: Option<Profile>,
|
||||
pub profile: Option<AnyProfile>,
|
||||
pub pkgs: PackageStore,
|
||||
pub cfg: GlobalConfig,
|
||||
pub remain_open: bool,
|
||||
pub state: GlobalState,
|
||||
}
|
||||
|
||||
impl AppData {
|
||||
@ -25,15 +30,15 @@ impl AppData {
|
||||
.unwrap_or_default();
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
AppData {
|
||||
profile,
|
||||
profile: profile,
|
||||
pkgs: PackageStore::new(apph.clone()),
|
||||
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<()> {
|
||||
match Profile::load(game.clone(), name.clone()) {
|
||||
match AnyProfile::load(game.clone(), name.clone()) {
|
||||
Ok(profile) => {
|
||||
self.profile = Some(profile);
|
||||
self.cfg.recent_profile = Some((game, name));
|
||||
@ -67,12 +72,12 @@ impl AppData {
|
||||
let loc = pkg.loc
|
||||
.clone()
|
||||
.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 {
|
||||
_ = self.toggle_package(d.clone(), true);
|
||||
}
|
||||
} else {
|
||||
profile.data.mods.remove(&key);
|
||||
profile.pkgs_mut().remove(&key);
|
||||
for (ckey, pkg) in self.pkgs.get_all() {
|
||||
if let Some(loc) = pkg.loc {
|
||||
if loc.dependencies.contains(&key) {
|
||||
@ -85,10 +90,10 @@ impl AppData {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sum_packages(&self, p: &Profile) -> String {
|
||||
pub fn sum_packages(&self, p: &AnyProfile) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
for pkg in &p.data.mods {
|
||||
let x = self.pkgs.get(pkg).unwrap().loc.as_ref().unwrap();
|
||||
for pkg in p.pkgs().into_iter() {
|
||||
let x = self.pkgs.get(&pkg).unwrap().loc.as_ref().unwrap();
|
||||
pkg.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 tokio::sync::Mutex;
|
||||
use tokio::fs;
|
||||
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use crate::model::misc::Game;
|
||||
use crate::pkg::{Package, PkgKey};
|
||||
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::{liner, start, util};
|
||||
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use crate::util;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn startline(app: AppHandle) -> Result<(), String> {
|
||||
log::debug!("invoke: startline");
|
||||
|
||||
let app_copy = app.clone();
|
||||
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 {
|
||||
let hash = appd.sum_packages(p);
|
||||
liner::line_up(p, hash).await
|
||||
.map_err(|e| e.to_string())?;
|
||||
hash = appd.sum_packages(p);
|
||||
}
|
||||
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
|
||||
.map_err(|e| e.to_string())
|
||||
Ok(())
|
||||
} else {
|
||||
Err("No profile".to_owned())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn kill() -> Result<(), String> {
|
||||
start::pkill("amdaemon.exe").await;
|
||||
util::pkill("amdaemon.exe").await;
|
||||
// The start routine will kill the other process
|
||||
|
||||
Ok(())
|
||||
@ -108,13 +111,35 @@ pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), Stri
|
||||
}
|
||||
|
||||
#[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");
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
#[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]
|
||||
pub async fn load_profile(state: State<'_, Mutex<AppData>>, game: Game, name: String) -> Result<(), String> {
|
||||
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]
|
||||
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");
|
||||
|
||||
let appd = state.lock().await;
|
||||
@ -133,41 +192,16 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
|
||||
}
|
||||
|
||||
#[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");
|
||||
|
||||
let appd = state.lock().await;
|
||||
if let Some(p) = &appd.profile {
|
||||
p.save().await;
|
||||
} else {
|
||||
log::warn!("No profile to save");
|
||||
}
|
||||
let mut appd = state.lock().await;
|
||||
profile.save().map_err(|e| e.to_string())?;
|
||||
appd.profile = Some(profile);
|
||||
|
||||
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]
|
||||
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
|
||||
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()]);
|
||||
}
|
||||
|
||||
#[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]
|
||||
#[cfg(target_os = "windows")]
|
||||
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 pkg;
|
||||
mod pkg_store;
|
||||
mod profile;
|
||||
mod util;
|
||||
mod start;
|
||||
mod liner;
|
||||
mod download_handler;
|
||||
mod appdata;
|
||||
mod display;
|
||||
mod modules;
|
||||
mod profiles;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use closure::closure;
|
||||
use appdata::AppData;
|
||||
use model::misc::Game;
|
||||
use pkg::PkgKey;
|
||||
use profile::Profile;
|
||||
use tauri::{Listener, Manager};
|
||||
use tauri::{AppHandle, Listener, Manager};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_cli::CliExt;
|
||||
use tokio::{sync::Mutex, fs, try_join};
|
||||
@ -40,28 +37,7 @@ pub async fn run(_args: Vec<String>) {
|
||||
.expect("No main window")
|
||||
.set_focus();
|
||||
if args.len() == 2 {
|
||||
// Todo deindent this chimera
|
||||
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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
deep_link(app.clone(), args);
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_cli::init())
|
||||
@ -85,7 +61,7 @@ pub async fn run(_args: Vec<String>) {
|
||||
log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg);
|
||||
if start_arg.occurrences > 0 {
|
||||
start_immediately = true;
|
||||
app_data.remain_open = false;
|
||||
app_data.state.remain_open = false;
|
||||
} else {
|
||||
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into()))
|
||||
.title("STARTLINER")
|
||||
@ -125,7 +101,6 @@ pub async fn run(_args: Vec<String>) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.listen("download-end", closure!(clone apph, |ev| {
|
||||
let raw = ev.payload();
|
||||
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 {
|
||||
let mutex = apph.state::<Mutex<AppData>>();
|
||||
let appd = mutex.lock().await;
|
||||
if !appd.remain_open {
|
||||
if !appd.state.remain_open {
|
||||
apph.exit(0);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
if start_immediately == true {
|
||||
let apph_clone = apph.clone();
|
||||
tauri::async_runtime::spawn(async {
|
||||
let apph_clone_clone = apph_clone.clone();
|
||||
{
|
||||
let mtx = apph_clone.state::<Mutex<AppData>>();
|
||||
let apph = apph.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mtx = apph.state::<Mutex<AppData>>();
|
||||
let mut appd = mtx.lock().await;
|
||||
if let Err(e) = appd.pkgs.reload_all().await {
|
||||
log::error!("Unable to reload packages: {}", e);
|
||||
apph_clone.exit(1);
|
||||
apph.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);
|
||||
apph_clone_clone.exit(1);
|
||||
apph.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -181,9 +153,9 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::list_profiles,
|
||||
cmd::init_profile,
|
||||
cmd::load_profile,
|
||||
cmd::rename_profile,
|
||||
cmd::get_current_profile,
|
||||
cmd::save_current_profile,
|
||||
cmd::set_cfg,
|
||||
|
||||
cmd::startline,
|
||||
cmd::kill,
|
||||
@ -195,3 +167,31 @@ pub async fn run(_args: Vec<String>) {
|
||||
.run(tauri::generate_context!())
|
||||
.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};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub enum Game {
|
||||
#[serde(rename = "ongeki")]
|
||||
Ongeki,
|
||||
|
@ -1,3 +1,5 @@
|
||||
pub mod local;
|
||||
pub mod misc;
|
||||
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 std::{path::{Path, PathBuf}, sync::OnceLock};
|
||||
|
||||
use crate::model::misc::Game;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
static NAME: &str = "startliner";
|
||||
|
||||
@ -22,9 +24,9 @@ pub fn init_dirs(apph: &AppHandle) {
|
||||
DIRS.get_or_init(|| {
|
||||
if cfg!(windows) {
|
||||
Dirs {
|
||||
config_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME).join("cfg"),
|
||||
data_dir: apph.path().data_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),
|
||||
config_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME),
|
||||
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).join("cache"),
|
||||
}
|
||||
} else {
|
||||
Dirs {
|
||||
@ -44,6 +46,10 @@ pub fn config_dir() -> &'static Path {
|
||||
&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 {
|
||||
&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))
|
||||
}
|
||||
|
||||
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<()> {
|
||||
std::fs::create_dir_all(&dst).unwrap();
|
||||
for entry in std::fs::read_dir(src)? {
|
||||
@ -78,3 +79,69 @@ pub fn copy_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
await prf.reloadList();
|
||||
await prf.reload();
|
||||
await Promise.all([prf.reloadList(), prf.reload()]);
|
||||
|
||||
if (prf.current !== null) {
|
||||
await pkg.reloadAll();
|
||||
|
@ -2,25 +2,16 @@
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { usePrfStore } from '../stores';
|
||||
|
||||
const props = defineProps({
|
||||
field: String,
|
||||
default: String,
|
||||
placeholder: String,
|
||||
directory: Boolean,
|
||||
promptname: 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 res = await open({
|
||||
multiple: false,
|
||||
@ -35,9 +26,9 @@ const filePick = async () => {
|
||||
]
|
||||
: [],
|
||||
});
|
||||
if (res != null) {
|
||||
cfg.value =
|
||||
/*path.relative(cfgs.current?.data.exe_dir ?? '', res) */ res;
|
||||
if (res != null && props.callback !== undefined) {
|
||||
props.callback(res);
|
||||
/*path.relative(cfgs.current?.data.exe_dir ?? '', res) */
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -48,6 +39,7 @@ const filePick = async () => {
|
||||
size="small"
|
||||
:placeholder="placeholder"
|
||||
type="text"
|
||||
v-model="cfg"
|
||||
:model-value="value"
|
||||
@update:model-value="(value) => callback && callback(value)"
|
||||
/>
|
||||
</template>
|
||||
|
@ -66,7 +66,7 @@ const aimeCodeModel = computed({
|
||||
});
|
||||
|
||||
const extraDisplayOptionsDisabled = computed(() => {
|
||||
return prf.cfg('display', 'default').value === 'default';
|
||||
return prf.current?.display.target === 'default';
|
||||
});
|
||||
|
||||
(async () => {
|
||||
@ -79,16 +79,16 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
<OptionCategory title="General">
|
||||
<OptionRow title="mu3.exe">
|
||||
<FilePicker
|
||||
field="target-path"
|
||||
default=""
|
||||
:directory="false"
|
||||
promptname="mu3.exe"
|
||||
extension="exe"
|
||||
:value="prf.current!.sgt.target"
|
||||
:callback="(value: string) => (prf.current!.sgt.target = value)"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow title="mu3hook">
|
||||
<Select
|
||||
:model-value="prf.cfg('hook', 'segatools-mu3hook')"
|
||||
model-value="segatools-mu3hook"
|
||||
:options="hookList"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
@ -96,24 +96,27 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
</OptionRow>
|
||||
<OptionRow title="amfs">
|
||||
<FilePicker
|
||||
field="amfs"
|
||||
default=""
|
||||
placeholder="amfs"
|
||||
:directory="true"
|
||||
placeholder="amfs"
|
||||
:value="prf.current!.sgt.amfs"
|
||||
:callback="(value: string) => (prf.current!.sgt.amfs = value)"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow title="option">
|
||||
<FilePicker
|
||||
field="option"
|
||||
default="option"
|
||||
:directory="true"
|
||||
placeholder="option"
|
||||
:value="prf.current!.sgt.option"
|
||||
:callback="(value: string) => (prf.current!.sgt.option = value)"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow title="appdata">
|
||||
<FilePicker
|
||||
field="appdata"
|
||||
default="appdata"
|
||||
:directory="true"
|
||||
:value="prf.current!.sgt.appdata"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.sgt.appdata = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
@ -123,7 +126,7 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
title="Target display"
|
||||
>
|
||||
<Select
|
||||
:model-value="prf.cfg('display', 'default')"
|
||||
v-model="prf.current!.display.target"
|
||||
:options="displayList"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
@ -136,7 +139,7 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
:min="480"
|
||||
:max="9999"
|
||||
:use-grouping="false"
|
||||
:model-value="prf.cfgAny('rez-w', 1080)"
|
||||
v-model="prf.current!.display.rez[0]"
|
||||
/>
|
||||
x
|
||||
<InputNumber
|
||||
@ -145,17 +148,18 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
:min="640"
|
||||
:max="9999"
|
||||
:use-grouping="false"
|
||||
:model-value="prf.cfgAny('rez-h', 1920)"
|
||||
v-model="prf.current!.display.rez[1]"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="Display mode">
|
||||
<SelectButton
|
||||
:model-value="prf.cfg('display-mode', 'borderless')"
|
||||
v-model="prf.current!.display.mode"
|
||||
:options="[
|
||||
{ title: 'Window', value: 'window' },
|
||||
{ title: 'Borderless window', value: 'borderless' },
|
||||
{ title: 'Fullscreen', value: 'fullscreen' },
|
||||
{ title: 'Window', value: 'Window' },
|
||||
{ title: 'Borderless window', value: 'Borderless' },
|
||||
{ title: 'Fullscreen', value: 'Fullscreen' },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
/>
|
||||
@ -165,12 +169,13 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
v-if="capabilities.includes('display')"
|
||||
>
|
||||
<SelectButton
|
||||
:model-value="prf.cfg('display-rotation', 0)"
|
||||
v-model="prf.current!.display.rotation"
|
||||
:options="[
|
||||
{ title: 'Unchanged', value: 0 },
|
||||
{ title: 'Portrait', value: 90 },
|
||||
{ title: 'Portrait (flipped)', value: 270 },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
:disabled="extraDisplayOptionsDisabled"
|
||||
@ -183,7 +188,7 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
:min="60"
|
||||
:max="999"
|
||||
:use-grouping="false"
|
||||
:model-value="prf.cfgAny('frequency', 60)"
|
||||
v-model="prf.current!.display.frequency"
|
||||
:disabled="extraDisplayOptionsDisabled"
|
||||
/>
|
||||
</OptionRow>
|
||||
@ -191,32 +196,69 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
title="Match display resolution with the game"
|
||||
v-if="capabilities.includes('display')"
|
||||
>
|
||||
<!-- @vue-expect-error -->
|
||||
<ToggleSwitch
|
||||
:disabled="
|
||||
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>
|
||||
</OptionCategory>
|
||||
<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
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="40"
|
||||
placeholder="192.168.1.234"
|
||||
:model-value="prf.cfgAny<string>('dns-default', '')"
|
||||
v-model="prf.current!.network.remote_address"
|
||||
/> </OptionRow
|
||||
><OptionRow title="Keychip">
|
||||
><OptionRow
|
||||
v-if="prf.current!.network.network_type == 'Remote'"
|
||||
title="Keychip"
|
||||
>
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="16"
|
||||
placeholder="A123-01234567890"
|
||||
:model-value="prf.cfgAny('keychip', '')"
|
||||
v-model="prf.current!.network.keychip"
|
||||
/> </OptionRow
|
||||
><OptionRow title="Subnet">
|
||||
<InputText
|
||||
@ -224,33 +266,33 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
size="small"
|
||||
:maxlength="15"
|
||||
placeholder="192.168.1.0"
|
||||
:model-value="prf.cfgAny('subnet', '')"
|
||||
v-model="prf.current!.network.subnet"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="Address suffix">
|
||||
<InputText
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:maxlength="3"
|
||||
:min="0"
|
||||
:max="255"
|
||||
placeholder="12"
|
||||
:model-value="prf.cfgAny('addrsuffix', '')"
|
||||
v-model="prf.current!.network.suffix"
|
||||
/>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
<OptionCategory title="Misc">
|
||||
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
|
||||
<!-- @vue-expect-error -->
|
||||
<ToggleSwitch :model-value="prf.cfg('intel', false)" />
|
||||
<ToggleSwitch v-model="prf.current!.sgt.intel" />
|
||||
</OptionRow>
|
||||
<OptionRow title="Aime emulation">
|
||||
<!-- @vue-expect-error -->
|
||||
<ToggleSwitch :model-value="prf.cfg('aime', false)" />
|
||||
<ToggleSwitch v-model="prf.current!.sgt.enable_aime" />
|
||||
</OptionRow>
|
||||
<OptionRow title="Aime code">
|
||||
<InputText
|
||||
class="shrink"
|
||||
size="small"
|
||||
:disabled="prf.cfg<boolean>('aime', false).value !== true"
|
||||
:disabled="prf.current?.sgt.enable_aime !== true"
|
||||
:maxlength="20"
|
||||
placeholder="00000000000000000000"
|
||||
v-model="aimeCodeModel"
|
||||
@ -268,6 +310,9 @@ const extraDisplayOptionsDisabled = computed(() => {
|
||||
extension="cfg"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow title="BepInEx console">
|
||||
<ToggleSwitch v-model="prf.current!.bepinex.console" />
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
</template>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button';
|
||||
import ProfileListEntry from './ProfileListEntry.vue';
|
||||
import { usePrfStore } from '../stores';
|
||||
|
||||
const prf = usePrfStore();
|
||||
@ -18,70 +19,25 @@ const prf = usePrfStore();
|
||||
icon="pi pi-plus"
|
||||
class="chunithm-button profile-button"
|
||||
@click="() => prf.create('chunithm')"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
|
||||
<div v-for="p in prf.list">
|
||||
<div class="flex flex-row flex-wrap align-middle gap-2">
|
||||
<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>
|
||||
<ProfileListEntry :p="p" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
.profile-button {
|
||||
width: 14em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ongeki-button {
|
||||
background-color: var(--p-pink-400);
|
||||
border-color: var(--p-pink-400);
|
||||
background-color: var(--p-pink-400) !important;
|
||||
border-color: var(--p-pink-400) !important;
|
||||
}
|
||||
|
||||
.ongeki-button:hover,
|
||||
@ -91,8 +47,8 @@ const prf = usePrfStore();
|
||||
}
|
||||
|
||||
.chunithm-button {
|
||||
background-color: var(--p-yellow-400);
|
||||
border-color: var(--p-yellow-400);
|
||||
background-color: var(--p-yellow-400) !important;
|
||||
border-color: var(--p-yellow-400) !important;
|
||||
}
|
||||
.chunithm-button:hover,
|
||||
.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(() => {
|
||||
if (prf.cfg('target-path', '').value.length === 0) {
|
||||
if (prf.current?.sgt.target.length === 0) {
|
||||
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 null;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import { Ref, computed, ref, watchEffect } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
@ -110,43 +110,25 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
() =>
|
||||
pkg !== undefined &&
|
||||
current.value !== null &&
|
||||
current.value?.data.mods.includes(pkgKey(pkg))
|
||||
current.value?.mods.includes(pkgKey(pkg))
|
||||
);
|
||||
|
||||
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) {
|
||||
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) => {
|
||||
try {
|
||||
await invoke('init_profile', { game, name: 'new-profile' });
|
||||
await invoke('init_profile', {
|
||||
game,
|
||||
name: 'new-profile',
|
||||
});
|
||||
await reload();
|
||||
await reloadList();
|
||||
} 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) => {
|
||||
await invoke('load_profile', { game, name });
|
||||
await reload();
|
||||
@ -169,14 +167,9 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
};
|
||||
|
||||
const reloadList = async () => {
|
||||
const raw = (await invoke('list_profiles')) as [Game, string][];
|
||||
|
||||
list.value = raw.map(([game, name]) => {
|
||||
return {
|
||||
game,
|
||||
name,
|
||||
};
|
||||
});
|
||||
// list.value.splice(0, list.value.length);
|
||||
list.value = (await invoke('list_profiles')) as ProfileMeta[];
|
||||
console.log(list.value);
|
||||
};
|
||||
|
||||
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 reload();
|
||||
await save();
|
||||
};
|
||||
|
||||
const generalStore = useGeneralStore();
|
||||
@ -201,15 +193,21 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
await reload();
|
||||
});
|
||||
|
||||
watchEffect(async () => {
|
||||
if (current.value !== null) {
|
||||
await invoke('save_current_profile', {
|
||||
profile: { OngekiProfile: current.value },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
current,
|
||||
list,
|
||||
isPkgEnabled,
|
||||
reload,
|
||||
save,
|
||||
cfg,
|
||||
cfgAny,
|
||||
create,
|
||||
rename,
|
||||
switchTo,
|
||||
reloadList,
|
||||
togglePkg,
|
||||
|
44
src/types.ts
44
src/types.ts
@ -26,13 +26,47 @@ export interface ProfileMeta {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Profile extends ProfileMeta {
|
||||
data: {
|
||||
mods: string[];
|
||||
cfg: { [key: string]: string | boolean | number };
|
||||
};
|
||||
export interface SegatoolsConfig {
|
||||
target: string;
|
||||
amfs: string;
|
||||
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 {
|
||||
config_dir: string;
|
||||
data_dir: string;
|
||||
|
Reference in New Issue
Block a user