feat: initial chunithm support

This commit is contained in:
2025-03-19 17:39:12 +00:00
parent 1191cdd95c
commit 8ac45df3e1
31 changed files with 1368 additions and 884 deletions

View File

@ -7,6 +7,11 @@
"core:default",
"opener:default",
"core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-set-focus",
"core:window:allow-hide",
"core:window:allow-show",
"core:app:allow-app-hide",
"shell:default",
"dialog:default",

View File

@ -1,7 +1,7 @@
use std::hash::{DefaultHasher, Hash, Hasher};
use crate::model::config::GlobalConfig;
use crate::pkg::{Feature, Status};
use crate::profiles::AnyProfile;
use crate::profiles::Profile;
use crate::{model::misc::Game, pkg::PkgKey};
use crate::pkg_store::PackageStore;
use crate::util;
@ -13,7 +13,7 @@ pub struct GlobalState {
}
pub struct AppData {
pub profile: Option<AnyProfile>,
pub profile: Option<Profile>,
pub pkgs: PackageStore,
pub cfg: GlobalConfig,
pub state: GlobalState,
@ -33,7 +33,7 @@ impl AppData {
.unwrap_or_default();
let profile = match cfg.recent_profile {
Some((ref game, ref name)) => AnyProfile::load(game.clone(), name.clone()).ok(),
Some((game, ref name)) => Profile::load(game, name.clone()).ok(),
None => None
};
@ -52,7 +52,7 @@ impl AppData {
}
pub fn switch_profile(&mut self, game: Game, name: String) -> Result<()> {
match AnyProfile::load(game.clone(), name.clone()) {
match Profile::load(game.clone(), name.clone()) {
Ok(profile) => {
self.profile = Some(profile);
self.cfg.recent_profile = Some((game, name));
@ -103,7 +103,7 @@ impl AppData {
Ok(())
}
pub fn sum_packages(&self, p: &AnyProfile) -> String {
pub fn sum_packages(&self, p: &Profile) -> String {
let mut hasher = DefaultHasher::new();
for key in p.mod_pkgs().into_iter() {
if let Ok(pkg) = self.pkgs.get(&key) {

View File

@ -6,8 +6,7 @@ use tauri::{AppHandle, Manager, State};
use crate::model::misc::Game;
use crate::pkg::{Package, PkgKey};
use crate::pkg_store::{InstallResult, PackageStore};
use crate::profiles::ongeki::OngekiProfile;
use crate::profiles::{self, AnyProfile, Profile, ProfileMeta, ProfilePaths};
use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths};
use crate::appdata::{AppData, ToggleAction};
use crate::model::misc::StartCheckError;
use crate::util;
@ -147,10 +146,21 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
Ok(appd.pkgs.get_all())
}
#[tauri::command]
pub async fn get_game_packages(state: State<'_, Mutex<AppData>>, game: Game) -> Result<Vec<PkgKey>, ()> {
log::debug!("invoke: get_game_packages {game}");
let appd = state.lock().await;
Ok(appd.pkgs.get_game_list(game))
}
#[tauri::command]
pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
log::debug!("invoke: fetch_listings");
// let game;
{
let appd = state.lock().await;
if !appd.pkgs.is_offline() {
@ -161,13 +171,26 @@ pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), Stri
log::info!("fetch_listings: skipped");
return Err("offline mode".to_owned());
}
// if let Some(profile) = &appd.profile {
// game = profile.meta.game;
// } else {
// return Err("No profile".to_owned());
// }
}
let listings = PackageStore::fetch_listings().await
// Can be this lazy for now as there are only two short lists
let listings1 = PackageStore::fetch_listings(Game::Ongeki).await
.map_err(|e| e.to_string())?;
let listings2 = PackageStore::fetch_listings(Game::Chunithm).await
.map_err(|e| e.to_string())?;
let mut appd = state.lock().await;
appd.pkgs.process_fetched_listings(listings);
appd.pkgs.process_fetched_listings(listings1, Game::Ongeki);
appd.pkgs.process_fetched_listings(listings2, Game::Chunithm);
appd.pkgs.save().await
.map_err(|e| e.to_string())?;
Ok(())
}
@ -189,15 +212,10 @@ pub async fn init_profile(
log::debug!("invoke: init_profile({}, {})", game, name);
let mut appd = state.lock().await;
let new_profile = OngekiProfile::new(name)
let new_profile = Profile::new(ProfileMeta { game, name })
.map_err(|e| format!("Unable to create profile: {}", e))?;
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()));
appd.profile = Some(new_profile);
Ok(())
}
@ -242,7 +260,7 @@ pub async fn rename_profile(
let mut appd = state.lock().await;
if let Some(current) = &mut appd.profile {
if current.meta() == profile {
if current.meta == profile {
current.rename(new_meta.name);
}
}
@ -277,7 +295,7 @@ pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMe
let mut appd = state.lock().await;
if let Some(current) = &mut appd.profile {
if current.meta() == profile {
if current.meta == profile {
appd.profile = None;
}
}
@ -286,7 +304,7 @@ pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMe
}
#[tauri::command]
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<AnyProfile>, ()> {
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<Profile>, ()> {
log::debug!("invoke: get_current_profile");
let appd = state.lock().await;
@ -294,12 +312,12 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
}
#[tauri::command]
pub async fn sync_current_profile(state: State<'_, Mutex<AppData>>, profile: AnyProfile) -> Result<(), String> {
pub async fn sync_current_profile(state: State<'_, Mutex<AppData>>, data: ProfileData) -> Result<(), String> {
log::debug!("invoke: sync_current_profile");
let mut appd = state.lock().await;
if let Some(p) = &mut appd.profile {
p.sync(profile);
p.sync(data);
}
Ok(())

View File

@ -69,8 +69,8 @@ pub async fn run(_args: Vec<String>) {
} else {
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into()))
.title("STARTLINER")
.inner_size(640f64, 480f64)
.min_inner_size(640f64, 480f64)
.inner_size(720f64, 480f64)
.min_inner_size(720f64, 480f64)
.build()?;
start_immediately = false;
}
@ -176,6 +176,7 @@ pub async fn run(_args: Vec<String>) {
cmd::get_package,
cmd::get_all_packages,
cmd::get_game_packages,
cmd::reload_all_packages,
cmd::fetch_listings,
cmd::install_package,

View File

@ -1,6 +1,8 @@
use std::collections::{BTreeMap, BTreeSet};
use serde::Deserialize;
use crate::pkg::PkgKeyVersion;
use serde::{Deserialize, Serialize};
use crate::pkg::{Status, PkgKey, PkgKeyVersion};
use super::misc::Game;
// manifest.json
@ -13,4 +15,13 @@ pub struct PackageManifest {
#[serde(default)]
pub installers: Vec<BTreeMap<String, serde_json::Value>>
}
pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
#[derive(Serialize, Deserialize, Clone)]
pub struct PackageListEntry {
pub version: String,
pub status: Status,
pub games: Vec<Game>,
}

View File

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use crate::pkg::PkgKey;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)]
pub enum Game {
#[serde(rename = "ongeki")]
Ongeki,
@ -17,6 +17,48 @@ impl Game {
_ => None
}
}
pub fn hook_exe(&self) -> &'static str {
match self {
Game::Ongeki => "mu3hook.dll",
Game::Chunithm => "chusanhook_x86.dll",
}
}
pub fn hook_amd(&self) -> &'static str {
match self {
Game::Ongeki => "mu3hook.dll",
Game::Chunithm => "chusanhook_x64.dll",
}
}
pub fn inject_exe(&self) -> &'static str {
match self {
Game::Ongeki => "inject.exe",
Game::Chunithm => "inject_x86.exe",
}
}
pub fn inject_amd(&self) -> &'static str {
match self {
Game::Ongeki => "inject.exe",
Game::Chunithm => "inject_x64.exe",
}
}
pub fn exe(&self) -> &'static str {
match self {
Game::Ongeki => "mu3.exe",
Game::Chunithm => "chusanApp.exe",
}
}
pub fn amd_args(&self) -> Vec<&'static str> {
match self {
Game::Ongeki => vec!["-f", "-c", "config_common.json", "config_server.json", "config_client.json"],
Game::Chunithm => vec!["-c", "config_common.json", "config_server.json", "config_client.json", "config_cvt.json", "config_sp.json", "config_hook.json"]
}
}
}
impl std::fmt::Display for Game {

View File

@ -2,7 +2,9 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::pkg::PkgKey;
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
use super::misc::Game;
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
pub enum Aime {
Disabled,
#[default] BuiltIn,
@ -10,7 +12,7 @@ pub enum Aime {
Other(PkgKey),
}
#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct AMNet {
pub name: String,
pub addr: String,
@ -23,7 +25,7 @@ impl Default for AMNet {
}
}
#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Segatools {
pub target: PathBuf,
pub hook: Option<PkgKey>,
@ -38,11 +40,14 @@ pub struct Segatools {
pub amnet: AMNet,
}
impl Default for Segatools {
fn default() -> Self {
impl Segatools {
pub fn default_for(game: Game) -> Self {
Segatools {
target: PathBuf::default(),
hook: Some(PkgKey("segatools-mu3hook".to_owned())),
hook: match game {
Game::Ongeki => Some(PkgKey("segatools-mu3hook".to_owned())),
Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned()))
},
io: None,
amfs: PathBuf::default(),
option: PathBuf::default(),
@ -54,14 +59,14 @@ impl Default for Segatools {
}
}
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
pub enum DisplayMode {
Window,
#[default] Borderless,
Fullscreen
}
#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Display {
pub target: String,
pub rez: (i32, i32),
@ -71,26 +76,32 @@ pub struct Display {
pub borderless_fullscreen: bool,
}
impl Default for Display {
fn default() -> Self {
impl Display {
pub fn default_for(game: Game) -> Self {
Display {
target: "default".to_owned(),
rez: (1080, 1920),
rez: match game {
Game::Chunithm => (1920, 1080),
Game::Ongeki => (1080, 1920),
},
mode: DisplayMode::Borderless,
rotation: 0,
frequency: 60,
frequency: match game {
Game::Chunithm => 120,
Game::Ongeki => 60,
},
borderless_fullscreen: true,
}
}
}
#[derive(Deserialize, Serialize, Clone, Default, PartialEq)]
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
pub enum NetworkType {
#[default] Remote,
Artemis,
}
#[derive(Deserialize, Serialize, Clone, Default)]
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
pub struct Network {
pub network_type: NetworkType,
@ -104,7 +115,7 @@ pub struct Network {
pub suffix: Option<i32>,
}
#[derive(Deserialize, Serialize, Clone, Default)]
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
pub struct BepInEx {
pub console: bool,
}

View File

@ -1,4 +1,8 @@
pub fn segatools_base() -> String {
use super::misc::Game;
pub fn segatools_base(game: Game) -> String {
match game {
Game::Ongeki =>
"[vfd]
; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly.
@ -76,5 +80,219 @@ right2=0x4B ; K
right3=0x4C ; L
leftMenu=0x55 ; U
rightMenu=0x4F ; O".to_owned()
rightMenu=0x4F ; O".to_owned(),
Game::Chunithm => "
[vfd]
; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly.
enable=1
[system]
; Enable ALLS system settings.
enable=1
; Enable freeplay mode. This will disable the coin slot and set the game to
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
; allow you to start a game in freeplay mode.
freeplay=0
; LAN Install: If multiple machines are present on the same LAN then set
; this to 1 on exactly one machine and set this to 0 on all others.
dipsw1=1
; Monitor type: 0 = 120FPS, 1 = 60FPS
dipsw2=1
; Cab type: 0 = SP, 1 = CVT. SP will enable VFD and eMoney. This setting will switch
; the LED 837-15093-06 COM port and the AiMe reder hardware generation as well.
dipsw3=1
; -----------------------------------------------------------------------------
; Misc. hooks settings
; -----------------------------------------------------------------------------
[gfx]
; Enables the graphics hook.
enable=1
; Force the game to run windowed.
windowed=1
; Add a frame to the game window if running windowed.
framed=0
; Select the monitor to run the game on. (Fullscreen only, 0 =primary screen)
monitor=0
; -----------------------------------------------------------------------------
; LED settings
; -----------------------------------------------------------------------------
[led15093]
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
; RGBs and the rear LED panel (billboard) on the cabinet.
enable=1
[led]
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\chuni_led\"
cabLedOutputPipe=1
; Output billboard LED strip data to serial
cabLedOutputSerial=0
; Output slider LED data to the named pipe
controllerLedOutputPipe=1
; Output slider LED data to the serial port
controllerLedOutputSerial=0
; Use the OpeNITHM protocol for serial LED output
controllerLedOutputOpeNITHM=0
; Serial port to send data to if using serial output. Default is COM5.
;serialPort=COM5
; Baud rate for serial data (set to 115200 if using OpeNITHM)
;serialBaud=921600
; Data output a sequence of bytes, with JVS-like framing.
; Each \"packet\" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere,
; 0xD0 is used as an escape character -- if you receive D0 in the output, ignore
; it and use the next sent byte plus one instead.
;
; After the sync is one byte for the board number that was updated, followed by
; the red, green and blue values for each LED.
;
; Board 0 has 53 LEDs:
; [0]-[49]: snakes through left half of billboard (first column starts at top)
; [50]-[52]: left side partition LEDs
;
; Board 1 has 63 LEDs:
; [0]-[59]: right half of billboard (first column starts at bottom)
; [60]-[62]: right side partition LEDs
;
; Board 2 is the slider and has 31 LEDs:
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers
; -----------------------------------------------------------------------------
; Custom IO settings
; -----------------------------------------------------------------------------
[chuniio]
; Uncomment this if you have custom chuniio implementation comprised of a single 32bit DLL.
; (will use chu2to3 engine internally)
;path=
; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs.
; x86 chuniio to path32, x64 to path64. Both are necessary.
;path32=
;path64=
; -----------------------------------------------------------------------------
; Input settings
; -----------------------------------------------------------------------------
; Keyboard bindings are specified as hexadecimal (prefixed with 0x) or decimal
; (not prefixed with 0x) virtual-key codes, a list of which can be found here:
;
; https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
;
; This is, admittedly, not the most user-friendly configuration method in the
; world. An improved solution will be provided later.
[io3]
test=0x31
service=0x32
coin=0x33
ir=0x00
ir6=0x39
ir5=0x38
ir4=0x37
ir3=0x36
ir2=0x35
ir1=0x34
[ir]
ir6=0x39
ir5=0x38
ir4=0x37
ir3=0x36
ir2=0x35
ir1=0x34
[slider]
cell32=0x51
cell30=0x5A
cell28=0x53
cell26=0x45
cell24=0x43
cell22=0x46
cell20=0x54
cell18=0x42
cell16=0x48
cell14=0x55
cell12=0x4D
cell10=0x4B
cell8=0x4F
cell6=190
cell4=186
cell2=219
cell31=0x41
cell29=0x57
cell27=0x58
cell25=0x44
cell23=0x52
cell21=0x56
cell19=0x47
cell17=0x59
cell15=0x4E
cell13=0x4A
cell11=0x49
cell9=188
cell7=0x4C
cell5=0x50
cell3=191
cell1=222
".to_owned()
}
}

View File

@ -4,14 +4,16 @@ 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<()> {
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> 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 redo_bepinex {
if pfx_dir.join("BepInEx").exists() {
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?;
}
}
if !opt_dir.exists() {
@ -21,11 +23,14 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
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_directory(&bpx_dir, &pfx_dir.join("BepInEx"), true)?;
if redo_bepinex {
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
.join("app")
.join("BepInEx");
if bpx_dir.exists() {
util::copy_directory(&bpx_dir, &pfx_dir.join("BepInEx"), true)?;
}
}
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option");

View File

@ -2,7 +2,7 @@ use std::path::PathBuf;
use anyhow::{anyhow, Result};
use ini::Ini;
use crate::{model::{profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}};
use crate::{model::{misc::Game, profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}};
use crate::pkg_store::PackageStore;
impl Segatools {
@ -31,7 +31,7 @@ impl Segatools {
_ => {},
}
}
pub async fn line_up(&self, p: &impl ProfilePaths) -> Result<Ini> {
pub async fn line_up(&self, p: &impl ProfilePaths, game: Game) -> Result<Ini> {
log::debug!("begin line-up: segatools");
let pfx_dir = p.data_dir();
@ -42,7 +42,7 @@ impl Segatools {
let ini_path = p.config_dir().join("segatools-base.ini");
if !ini_path.exists() {
tokio::fs::write(&ini_path, segatools_base()).await
tokio::fs::write(&ini_path, segatools_base(game)).await
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
}
if !pfx_dir.exists() {
@ -69,12 +69,14 @@ impl Segatools {
.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 game == Game::Ongeki {
ini_out.with_section(Some("unity"))
.set("enable", "1")
.set(
"targetAssembly",
pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()?
);
}
if self.aime != Aime::Disabled {
ini_out.with_section(Some("aime"))
@ -84,11 +86,16 @@ impl Segatools {
let mut aimeio = ini_out.with_section(Some("aimeio"));
aimeio
.set("path", util::pkg_dir().join(key.to_string()).join("segatools").join("aimeio.dll").stringify()?)
.set("gameId", "SDDT")
.set("serverAddress", &self.amnet.addr)
.set("useAimeDBForPhysicalCards", if self.amnet.physical { "1" } else { "0" })
.set("enableKeyboardMode", "0");
match game {
Game::Ongeki => aimeio.set("gameId", "SDDT"),
Game::Chunithm => aimeio.set("gameId", "SDHD")
};
if let Ok(keyboard_code) = std::fs::read_to_string(p.config_dir().join("aime.txt")) {
log::debug!("{} {}", keyboard_code, keyboard_code.len());
if keyboard_code.len() == 20 {
@ -105,12 +112,14 @@ impl Segatools {
.set("enable", "0");
}
if let Some(io) = &self.io {
ini_out.with_section(Some("mu3io"))
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
} else {
ini_out.with_section(Some("mu3io"))
.set("path", "");
if game == Game::Ongeki {
if let Some(io) = &self.io {
ini_out.with_section(Some("mu3io"))
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
} else {
ini_out.with_section(Some("mu3io"))
.set("path", "");
}
}
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);

View File

@ -7,11 +7,11 @@ use enumflags2::{bitflags, make_bitflags, BitFlags};
use crate::{model::{local::{self, PackageManifest}, rainy}, util};
// {namespace}-{name}
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)]
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
pub struct PkgKey(pub String);
// {namespace}-{name}-{version}
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)]
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
pub struct PkgKeyVersion(String);
#[derive(Clone, Default, Serialize, Deserialize)]
@ -36,10 +36,11 @@ pub enum Status {
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Feature {
Mod,
Hook,
GameIO,
Aime,
AMNet,
Mu3Hook,
Mu3IO,
ChusanHook,
}
#[derive(Clone, Serialize, Deserialize)]
@ -196,14 +197,16 @@ impl Package {
// Multiple features in the same dll (yubideck etc.) should be supported at some point
let mut flags = BitFlags::default();
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") {
if module.ends_with("hook") {
flags |= Feature::Hook;
if module == "mu3hook" {
flags |= Feature::Mu3Hook;
} else if module == "chusanhook" {
flags |= Feature::ChusanHook;
} else if module == "amnet" {
flags |= Feature::AMNet | Feature::Aime;
} else if module == "aimeio" {
flags |= Feature::Aime;
} else if module.ends_with("io") {
flags |= Feature::GameIO;
} else if module == "mu3io" {
flags |= Feature::Mu3IO;
}
}
return Status::OK(flags);

View File

@ -5,15 +5,20 @@ use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use tokio::fs;
use tokio::task::JoinSet;
use crate::model::local::{PackageList, PackageListEntry};
use crate::model::misc::Game;
use crate::model::rainy;
use crate::pkg::{Package, PkgKey, Remote};
use crate::pkg::{Package, PkgKey, Remote, Status};
use crate::util;
use crate::download_handler::DownloadHandler;
pub struct PackageStore {
store: HashMap<PkgKey, Package>,
app: AppHandle,
meta_list: PackageList,
dlh: DownloadHandler,
app: AppHandle,
offline: bool,
}
@ -29,8 +34,17 @@ pub enum InstallResult {
impl PackageStore {
pub fn new(app: AppHandle) -> PackageStore {
let meta_list = std::fs::read_to_string(util::config_dir().join("package-list.json"))
.map_err(|e| anyhow!(e))
.and_then(|s| serde_json::from_str::<PackageList>(&s).map_err(|e| anyhow!(e)))
.unwrap_or_else(|e| {
log::warn!("unable to read package-list: {e}");
PackageList::new()
});
PackageStore {
store: HashMap::new(),
meta_list,
app: app.clone(),
dlh: DownloadHandler::new(app),
offline: true
@ -46,6 +60,13 @@ impl PackageStore {
self.store.clone()
}
pub fn get_game_list(&self, game: Game) -> Vec<PkgKey> {
self.meta_list.iter()
.filter(|(_, v)| v.games.contains(&game))
.map(|(k, _)| k.clone())
.collect()
}
pub async fn reload_package(&mut self, key: PkgKey) {
let dir = util::pkg_dir().join(&key.0);
if let Ok(pkg) = Package::from_dir(dir).await {
@ -75,14 +96,23 @@ impl PackageStore {
Ok(())
}
pub async fn fetch_listings() -> Result<Vec<rainy::V1Package>> {
pub async fn save(&self) -> Result<()> {
tokio::fs::write(
util::config_dir().join("package-list.json"),
serde_json::to_string_pretty(&self.meta_list)?
).await?;
Ok(())
}
pub async fn fetch_listings(game: Game) -> Result<Vec<rainy::V1Package>> {
use async_compression::futures::bufread::GzipDecoder;
use futures::{
io::{self, BufReader, ErrorKind},
prelude::*,
};
let response = reqwest::get("https://rainy.patafour.zip/c/ongeki/api/v1/package/").await?;
let response = reqwest::get(format!("https://rainy.patafour.zip/c/{game}/api/v1/package/")).await?;
let reader = response
.bytes_stream()
@ -100,11 +130,23 @@ impl PackageStore {
self.offline
}
pub fn process_fetched_listings(&mut self, listings: Vec<rainy::V1Package>) {
pub fn process_fetched_listings(&mut self, listings: Vec<rainy::V1Package>, game: Game) {
for listing in listings {
// This is None if the package has no versions for whatever reason
if let Some(r) = Package::from_rainy(listing) {
//log::warn!("D {}", &r.rmt.as_ref().unwrap().dependencies.first().unwrap_or(&"Nothing".to_owned()));
let mut meta_entry = self.meta_list.remove(&r.key()).unwrap_or_else(|| {
PackageListEntry {
// from_rainy() is guaranteed to include rmt
version: r.rmt.as_ref().unwrap().version.clone(),
status: Status::Unchecked,
games: vec![ game ],
}
});
if !meta_entry.games.contains(&game) {
meta_entry.games.push(game);
}
self.meta_list.insert(r.key(), meta_entry);
match self.store.get_mut(&r.key()) {
Some(l) => {
l.rmt = r.rmt;
@ -226,9 +268,8 @@ impl PackageStore {
async fn clean_up_file(path: impl AsRef<Path>, name: &str, force: bool) -> Result<()> {
let path = path.as_ref().join(name);
if force || path.exists() {
tokio::fs::remove_file(path)
.await
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
tokio::fs::remove_file(path).await
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
}
Ok(())
@ -242,6 +283,7 @@ impl PackageStore {
Self::clean_up_file(&path, "icon.png", true).await?;
Self::clean_up_file(&path, "manifest.json", true).await?;
Self::clean_up_file(&path, "README.md", true).await?;
Self::clean_up_file(&path, "post_load.ps1", false).await?;
tokio::fs::remove_dir(path.as_ref())
.await

View File

@ -1,15 +1,19 @@
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, pkg_store::PackageStore, util};
use crate::{model::{misc::Game, profile::Aime}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
use tauri::Emitter;
use std::process::Stdio;
use crate::model::profile::BepInEx;
use crate::model::{profile::{Display, DisplayMode, Network, Segatools}, segatools_base::segatools_base};
use anyhow::{anyhow, Result};
use std::fs::File;
use tokio::process::Command;
use tokio::task::JoinSet;
pub mod ongeki;
#[derive(Deserialize, Serialize, Clone)]
pub enum AnyProfile {
OngekiProfile(OngekiProfile)
pub trait ProfilePaths {
fn config_dir(&self) -> PathBuf;
fn data_dir(&self) -> PathBuf;
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
@ -18,147 +22,311 @@ pub struct ProfileMeta {
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<()>;
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))
}
}
pub trait ProfilePaths {
fn config_dir(&self) -> PathBuf;
fn data_dir(&self) -> PathBuf;
#[derive(Deserialize, Serialize, Clone)]
pub struct Profile {
pub meta: ProfileMeta,
pub data: ProfileData,
}
impl AnyProfile {
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ProfileData {
pub mods: BTreeSet<PkgKey>,
pub sgt: Segatools,
pub display: Option<Display>,
pub network: Network,
#[serde(skip_serializing_if = "Option::is_none")]
pub bepinex: Option<BepInEx>,
#[cfg(not(target_os = "windows"))]
pub wine: crate::model::profile::Wine,
}
impl Profile {
pub fn new(mut meta: ProfileMeta) -> Result<Self> {
meta.name = fixed_name(&meta, true);
log::debug!("created profile-{:?}", &meta);
let p = Profile {
data: ProfileData {
mods: BTreeSet::new(),
sgt: Segatools::default_for(meta.game),
#[cfg(target_os = "windows")]
display: if meta.game == Game::Ongeki { Some(Display::default_for(meta.game)) } else { None },
#[cfg(not(target_os = "windows"))]
display: None,
network: Network::default(),
bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None },
#[cfg(not(target_os = "windows"))]
wine: crate::model::profile::Wine::default(),
},
meta: meta.clone()
};
p.save()?;
std::fs::create_dir_all(p.config_dir())?;
std::fs::create_dir_all(p.data_dir())?;
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base(meta.game))?;
Ok(p)
}
pub fn load(game: Game, name: String) -> Result<Self> {
Ok(match game {
Game::Ongeki => AnyProfile::OngekiProfile(OngekiProfile::load(name)?),
Game::Chunithm => panic!("Not implemented")
})
let path = util::profile_config_dir(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))?;
log::debug!("{:?}", data);
Ok(Profile {
meta: ProfileMeta {
game, name
},
data
})
} else {
Err(anyhow!("Unable to open {:?}", path))
}
}
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()
}
}
let path = self.config_dir().join("profile.json");
let s = serde_json::to_string_pretty(&self.data)?;
if !self.config_dir().exists() {
std::fs::create_dir(self.config_dir())
.map_err(|e| anyhow!("error when creating profile directory: {}", e))?;
}
std::fs::write(&path, s)
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
log::info!("Written to {:?}", path);
Ok(())
}
pub fn rename(&mut self, name: String) {
match self {
Self::OngekiProfile(p) => {
p.name = Some(fixed_name(&ProfileMeta { name, game: Game::Ongeki }, false));
}
}
self.meta.name = fixed_name(&ProfileMeta { game: self.meta.game, name}, false);
}
pub fn mod_pkgs(&self) -> &BTreeSet<PkgKey> {
match self {
Self::OngekiProfile(p) => &p.mods
}
&self.data.mods
}
pub fn mod_pkgs_mut(&mut self) -> &mut BTreeSet<PkgKey> {
match self {
Self::OngekiProfile(p) => &mut p.mods
}
&mut self.data.mods
}
pub fn special_pkgs(&self) -> Vec<PkgKey> {
let mut res = Vec::new();
match self {
Self::OngekiProfile(p) => {
if let Some(hook) = &p.sgt.hook {
res.push(hook.clone());
}
if let Some(io) = &p.sgt.io {
res.push(io.clone());
}
}
if let Some(hook) = &self.data.sgt.hook {
res.push(hook.clone());
}
if let Some(io) = &self.data.sgt.io {
res.push(io.clone());
}
if let Aime::AMNet(aime) = &self.data.sgt.aime {
res.push(aime.clone());
} else if let Aime::Other(aime) = &self.data.sgt.aime {
res.push(aime.clone());
}
res
}
pub fn fix(&mut self, store: &PackageStore) {
match self {
Self::OngekiProfile(p) => p.sgt.fix(store)
}
self.data.sgt.fix(store);
}
pub fn sync(&mut self, source: AnyProfile) {
match self {
Self::OngekiProfile(p) => {
#[allow(irrefutable_let_patterns)]
if let AnyProfile::OngekiProfile(source) = source {
p.bepinex = source.bepinex;
p.display = source.display;
p.network = source.network;
p.sgt = source.sgt;
} else {
log::error!("sync: invalid profile type {:?}", source);
}
}
pub fn sync(&mut self, source: ProfileData) {
if self.data.bepinex.is_some() {
self.data.bepinex = source.bepinex;
}
if self.data.display.is_some() {
self.data.display = source.display;
}
// if self.data.network.is_some() {
self.data.network = source.network;
// }
// if self.data.sgt.is_some() {
self.data.sgt = source.sgt;
// }
}
pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> {
match self {
Self::OngekiProfile(_p) => {
#[cfg(target_os = "windows")]
let info = _p.display.line_up()?;
let info = match &self.data.display {
None => None,
Some(display) => display.line_up()?
};
let res = self.line_up_the_rest(pkg_hash).await;
let res = self.line_up_the_rest(pkg_hash).await;
#[cfg(target_os = "windows")]
if let Some(info) = info {
use crate::model::profile::Display;
if res.is_ok() {
Display::wait_for_exit(_app, info);
} else {
Display::clean_up(&info)?;
}
}
res
#[cfg(target_os = "windows")]
if let Some(info) = info {
use crate::model::profile::Display;
if res.is_ok() {
Display::wait_for_exit(_app, info);
} else {
Display::clean_up(&info)?;
}
}
res
}
async fn line_up_the_rest(&self, 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();
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(())
}
if !self.data_dir().exists() {
tokio::fs::create_dir(self.data_dir()).await?;
}
let hash_path = self.data_dir().join(".sl-state");
util::clean_up_opts(self.data_dir().join("option"))?;
let hash_check = Self::hash_check(&hash_path, &pkg_hash).await?;
prepare_packages(&self.meta, &self.data.mods, hash_check).await
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
self.data.network.line_up(&mut ini)?;
ini.write_to_file(self.data_dir().join("segatools.ini"))
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
if let Some(bepinex) = &self.data.bepinex {
bepinex.line_up(&self.meta)?;
}
Ok(())
}
pub async fn start(&self, app: AppHandle) -> Result<()> {
match self {
Self::OngekiProfile(p) => p.start(app).await
let ini_path = self.data_dir().join("segatools.ini");
log::debug!("With path {:?}", ini_path);
let mut game_builder;
let mut amd_builder;
let target_path = PathBuf::from(&self.data.sgt.target);
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
let sgt_dir = self.data.sgt.hook_dir()?;
#[cfg(target_os = "windows")]
{
game_builder = Command::new(sgt_dir.join(self.meta.game.inject_exe()));
amd_builder = Command::new("cmd.exe");
}
#[cfg(target_os = "linux")]
{
game_builder = Command::new(&self.wine.runtime);
amd_builder = Command::new(&self.wine.runtime);
game_builder.arg(sgt_dir.join(self.meta.game.inject_exe()));
amd_builder.arg("cmd.exe");
}
amd_builder.env(
"SEGATOOLS_CONFIG_PATH",
&ini_path,
)
.current_dir(&exe_dir)
.arg("/C")
.arg(&sgt_dir.join(self.meta.game.inject_amd()))
.args(["-d", "-k"])
.arg(sgt_dir.join(self.meta.game.hook_amd()))
.arg("amdaemon.exe")
.args(self.meta.game.amd_args());
game_builder
.env(
"SEGATOOLS_CONFIG_PATH",
ini_path,
)
.env(
"INOHARA_CONFIG_PATH",
self.config_dir().join("inohara.cfg"),
)
.current_dir(&exe_dir)
.args(["-d", "-k"])
.arg(sgt_dir.join(self.meta.game.hook_exe()))
.arg(self.meta.game.exe());
if let Some(display) = &self.data.display {
game_builder.args([
"-monitor 1",
"-screen-width", &display.rez.0.to_string(),
"-screen-height", &display.rez.1.to_string(),
"-screen-fullscreen", if display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
]);
if display.mode == DisplayMode::Borderless {
game_builder.arg("-popupwindow");
}
}
#[cfg(target_os = "linux")]
{
amd_builder.env("WINEPREFIX", &self.wine.prefix);
game_builder.env("WINEPREFIX", &self.wine.prefix);
}
let amd_log = File::create(self.data_dir().join("amdaemon.exe.log"))?;
let game_log = File::create(self.data_dir().join(format!("{}.log", self.meta.game.exe())))?;
amd_builder
.stdout(Stdio::from(amd_log));
// do they use stderr?
game_builder
.stdout(Stdio::from(game_log));
#[cfg(target_os = "windows")]
{
amd_builder.creation_flags(util::CREATE_NO_WINDOW);
game_builder.creation_flags(util::CREATE_NO_WINDOW);
}
if self.data.sgt.intel == true {
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
}
util::pkill("amdaemon.exe").await;
log::info!("Launching amdaemon: {:?}", amd_builder);
log::info!("Launching {}: {:?}", self.meta.game, game_builder);
let mut amd = amd_builder.spawn()?;
let mut game = game_builder.spawn()?;
let mut set = JoinSet::new();
set.spawn(async move {
(amd.wait().await.expect("amdaemon failed to run"), "amdaemon")
});
set.spawn(async move {
(game.wait().await.expect("game failed to run"), "game")
});
if let Err(e) = app.emit("launch-start", "") {
log::warn!("Unable to emit launch-start: {}", e);
}
let (rc, process) = set.join_next().await.expect("No spawn").expect("No result");
log::info!("{} died with return code {}", process, rc);
if process == "amdaemon" {
util::pkill(self.meta.game.exe()).await;
} else {
util::pkill("amdaemon.exe").await;
}
set.join_next().await.expect("No spawn").expect("No result");
log::debug!("Fin");
if let Err(e) = app.emit("launch-end", "") {
log::warn!("Unable to emit launch-end: {}", e);
}
Ok(())
}
async fn hash_check(prev_hash_path: &impl AsRef<Path>, new_hash: &str) -> Result<bool> {
@ -174,11 +342,19 @@ impl AnyProfile {
}
}
impl std::fmt::Debug for AnyProfile {
impl ProfilePaths for Profile {
fn config_dir(&self) -> PathBuf {
self.meta.config_dir()
}
fn data_dir(&self) -> PathBuf {
self.meta.data_dir()
}
}
impl std::fmt::Debug for Profile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OngekiProfile(p) => f.debug_tuple("ongeki").field(&p.name).finish(),
}
f.debug_tuple(&self.meta.game.to_string()).field(&self.meta.name).finish()
}
}
@ -227,7 +403,7 @@ pub fn fixed_name(meta: &ProfileMeta, prepend_new: bool) -> String {
.replace(" ", "-")
.replace("..", "").replace("/", "").replace("\\", "");
while prepend_new && util::profile_config_dir(&meta.game, &name).exists() {
while prepend_new && util::profile_config_dir(meta.game, &name).exists() {
name = format!("new-{}", name);
}

View File

@ -1,229 +0,0 @@
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use tauri::Emitter;
use std::{collections::BTreeSet, path::PathBuf, process::Stdio};
use crate::model::profile::BepInEx;
use crate::profiles::fixed_name;
use crate::{model::{profile::{Display, DisplayMode, Network, Segatools}, misc::Game, segatools_base::segatools_base}, pkg::PkgKey, util};
use super::{Profile, ProfileMeta, ProfilePaths};
use anyhow::{anyhow, Result};
use std::fs::File;
use tokio::process::Command;
use tokio::task::JoinSet;
#[derive(Deserialize, Serialize, Clone)]
pub struct OngekiProfile {
// this is an Option only to deceive serde
// file serialization doesn't need the name, but IPC does
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub mods: BTreeSet<PkgKey>,
pub sgt: Segatools,
pub display: Display,
pub network: Network,
pub bepinex: BepInEx,
#[cfg(not(target_os = "windows"))]
pub wine: crate::model::profile::Wine,
}
impl Profile for OngekiProfile {
fn new(name: String) -> Result<Self> {
let name = fixed_name(&ProfileMeta { name, game: Game::Ongeki }, true);
let p = OngekiProfile {
name: Some(name.clone()),
mods: BTreeSet::new(),
sgt: Segatools::default(),
display: Display::default(),
network: Network::default(),
bepinex: BepInEx::default(),
#[cfg(not(target_os = "windows"))]
wine: crate::model::profile::Wine::default(),
};
p.save()?;
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base())?;
log::debug!("created profile-ongeki-{}", &name);
Ok(p)
}
fn load(name: String) -> Result<Self> {
let path = util::profile_config_dir(&Game::Ongeki, &name).join("profile.json");
if let Ok(s) = std::fs::read_to_string(&path) {
let mut data = serde_json::from_str::<OngekiProfile>(&s)
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
data.name = Some(name);
Ok(data)
} else {
Err(anyhow!("Unable to open {:?}", path))
}
}
fn save(&self) -> Result<()> {
let path = self.config_dir().join("profile.json");
let mut cpy = self.clone();
cpy.name = None;
let s = serde_json::to_string_pretty(&cpy)?;
if !self.config_dir().exists() {
std::fs::create_dir(self.config_dir())
.map_err(|e| anyhow!("error when creating profile directory: {}", e))?;
}
std::fs::write(&path, s)
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
log::info!("Written to {:?}", path);
Ok(())
}
async fn start(&self, app: AppHandle) -> Result<()> {
let ini_path = self.data_dir().join("segatools.ini");
log::debug!("With path {:?}", ini_path);
let mut game_builder;
let mut amd_builder;
let target_path = PathBuf::from(&self.sgt.target);
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
let sgt_dir = self.sgt.hook_dir()?;
#[cfg(target_os = "windows")]
{
game_builder = Command::new(sgt_dir.join("inject.exe"));
amd_builder = Command::new("cmd.exe");
}
#[cfg(target_os = "linux")]
{
game_builder = Command::new(&self.wine.runtime);
amd_builder = Command::new(&self.wine.runtime);
game_builder.arg(sgt_dir.join("inject.exe"));
amd_builder.arg("cmd.exe");
}
amd_builder.env(
"SEGATOOLS_CONFIG_PATH",
&ini_path,
)
.current_dir(&exe_dir)
.arg("/C")
.arg(&sgt_dir.join("inject.exe"))
.args(["-d", "-k"])
.arg(sgt_dir.join("mu3hook.dll"))
.args(["amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
]);
game_builder
.env(
"SEGATOOLS_CONFIG_PATH",
ini_path,
)
.env(
"INOHARA_CONFIG_PATH",
self.config_dir().join("inohara.cfg"),
)
.current_dir(&exe_dir)
.args(["-d", "-k"])
.arg(sgt_dir.join("mu3hook.dll"))
.args([
"mu3.exe", "-monitor 1",
"-screen-width", &self.display.rez.0.to_string(),
"-screen-height", &self.display.rez.1.to_string(),
"-screen-fullscreen", if self.display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
]);
if self.display.mode == DisplayMode::Borderless {
game_builder.arg("-popupwindow");
}
#[cfg(target_os = "linux")]
{
amd_builder.env("WINEPREFIX", &self.wine.prefix);
game_builder.env("WINEPREFIX", &self.wine.prefix);
}
let amd_log = File::create(self.data_dir().join("amdaemon.log"))?;
let game_log = File::create(self.data_dir().join("mu3.log"))?;
amd_builder
.stdout(Stdio::from(amd_log));
// do they use stderr?
game_builder
.stdout(Stdio::from(game_log));
#[cfg(target_os = "windows")]
{
amd_builder.creation_flags(util::CREATE_NO_WINDOW);
game_builder.creation_flags(util::CREATE_NO_WINDOW);
}
if self.sgt.intel == true {
amd_builder.env("OPENSSL_ia32cap", ":~0x20000000");
}
util::pkill("amdaemon.exe").await;
log::info!("Launching amdaemon: {:?}", amd_builder);
log::info!("Launching mu3: {:?}", game_builder);
let mut amd = amd_builder.spawn()?;
let mut game = game_builder.spawn()?;
let mut set = JoinSet::new();
set.spawn(async move {
(amd.wait().await.expect("amdaemon failed to run"), "amdaemon.exe")
});
set.spawn(async move {
(game.wait().await.expect("mu3 failed to run"), "mu3.exe")
});
if let Err(e) = app.emit("launch-start", "") {
log::warn!("Unable to emit launch-start: {}", e);
}
let (rc, process_name) = set.join_next().await.expect("No spawn").expect("No result");
log::info!("{} died with return code {}", process_name, rc);
if process_name == "amdaemon.exe" {
util::pkill("mu3.exe").await;
} else {
util::pkill("amdaemon.exe").await;
}
set.join_next().await.expect("No spawn").expect("No result");
log::debug!("Fin");
if let Err(e) = app.emit("launch-end", "") {
log::warn!("Unable to emit launch-end: {}", e);
}
Ok(())
}
}
impl ProfilePaths for OngekiProfile {
fn config_dir(&self) -> PathBuf {
util::profile_config_dir(&Game::Ongeki, &self.name.as_ref().unwrap())
}
fn data_dir(&self) -> PathBuf {
util::data_dir().join(format!("profile-{}-{}", &Game::Ongeki, self.name.as_ref().unwrap()))
}
}
impl ProfilePaths for ProfileMeta {
fn config_dir(&self) -> PathBuf {
util::profile_config_dir(&self.game, &self.name)
}
fn data_dir(&self) -> PathBuf {
util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name))
}
}

View File

@ -47,7 +47,7 @@ pub fn config_dir() -> &'static Path {
&DIRS.get().expect("Directories uninitialized").config_dir
}
pub fn profile_config_dir(game: &Game, name: &str) -> PathBuf {
pub fn profile_config_dir(game: Game, name: &str) -> PathBuf {
config_dir().join(format!("profile-{}-{}", game, name))
}