feat: new config format
This commit is contained in:
@ -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 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);
|
||||
}
|
||||
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.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 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)? {
|
||||
@ -77,4 +78,70 @@ 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user