forked from akanyan/STARTLINER
feat: initial chunithm support
This commit is contained in:
@ -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",
|
||||
|
@ -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) {
|
||||
|
@ -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(())
|
||||
|
@ -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,
|
||||
|
@ -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>,
|
||||
}
|
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user