feat: new config format

This commit is contained in:
2025-03-13 23:26:00 +00:00
parent 48dc9ec4df
commit fd27000c05
30 changed files with 1447 additions and 833 deletions

View File

@ -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);
}

View File

@ -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> {

View File

@ -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(())
}

View File

@ -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());
}
});
}
}
}
}

View File

@ -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
View 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
}

View File

@ -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,

View File

@ -1,3 +1,5 @@
pub mod local;
pub mod misc;
pub mod rainy;
pub mod rainy;
pub mod config;
pub mod segatools_base;

View 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()
}

View 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
View 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
View File

@ -0,0 +1,5 @@
pub mod display;
pub mod package;
pub mod segatools;
pub mod network;
pub mod bepinex;

View 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(())
}
}

View 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(())
}

View 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)
}
}

View File

@ -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
View 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
View 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))
}
}

View File

@ -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;
}

View File

@ -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)
}
}