feat: chusanApp.exe patching

This commit is contained in:
2025-04-13 18:15:41 +00:00
parent 6270fce05f
commit 4247e19996
18 changed files with 406 additions and 187 deletions

View File

@ -2,7 +2,7 @@ use std::hash::{DefaultHasher, Hash, Hasher};
use crate::model::config::GlobalConfig; use crate::model::config::GlobalConfig;
use crate::model::patch::PatchFileVec; use crate::model::patch::PatchFileVec;
use crate::pkg::{Feature, Status}; use crate::pkg::{Feature, Status};
use crate::profiles::Profile; use crate::profiles::types::Profile;
use crate::{model::misc::Game, pkg::PkgKey}; use crate::{model::misc::Game, pkg::PkgKey};
use crate::pkg_store::PackageStore; use crate::pkg_store::PackageStore;
use crate::util; use crate::util;
@ -18,7 +18,7 @@ pub struct AppData {
pub pkgs: PackageStore, pub pkgs: PackageStore,
pub cfg: GlobalConfig, pub cfg: GlobalConfig,
pub state: GlobalState, pub state: GlobalState,
pub patch_set: PatchFileVec, pub patch_vec: PatchFileVec,
} }
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
@ -39,7 +39,7 @@ impl AppData {
None => None None => None
}; };
let patch_set = PatchFileVec::new(util::config_dir()) let patch_vec = PatchFileVec::new(util::config_dir())
.map_err(|e| log::error!("unable to load patch set: {e}")) .map_err(|e| log::error!("unable to load patch set: {e}"))
.unwrap_or_default(); .unwrap_or_default();
@ -50,7 +50,7 @@ impl AppData {
pkgs: PackageStore::new(apph.clone()), pkgs: PackageStore::new(apph.clone()),
cfg, cfg,
state: GlobalState { remain_open: true }, state: GlobalState { remain_open: true },
patch_set patch_vec
} }
} }
@ -85,8 +85,7 @@ impl AppData {
.clone() .clone()
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?; .ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
if let Status::OK(feature_set) = loc.status { if let Status::OK(feature_set, _) = loc.status {
log::debug!("{:?}", feature_set);
if feature_set.contains(Feature::Mod) { if feature_set.contains(Feature::Mod) {
profile.mod_pkgs_mut().insert(key); profile.mod_pkgs_mut().insert(key);
} }

View File

@ -8,9 +8,10 @@ use tauri::{AppHandle, Manager, State};
use crate::model::config::GlobalConfigField; use crate::model::config::GlobalConfigField;
use crate::model::misc::Game; use crate::model::misc::Game;
use crate::model::patch::Patch; use crate::model::patch::Patch;
use crate::modules::package::prepare_dlls;
use crate::pkg::{Package, PkgKey}; use crate::pkg::{Package, PkgKey};
use crate::pkg_store::{InstallResult, PackageStore}; use crate::pkg_store::{InstallResult, PackageStore};
use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths}; use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use crate::appdata::{AppData, ToggleAction}; use crate::appdata::{AppData, ToggleAction};
use crate::model::misc::StartCheckError; use crate::model::misc::StartCheckError;
use crate::util; use crate::util;
@ -59,18 +60,40 @@ pub async fn startline(app: AppHandle, refresh: bool) -> Result<(), String> {
let state = app.state::<Mutex<AppData>>(); let state = app.state::<Mutex<AppData>>();
let mut hash = "".to_owned(); let mut hash = "".to_owned();
let mut appd = state.lock().await; let appd = state.lock().await;
let mut game_dlls = Vec::new();
let mut amd_dlls = Vec::new();
if let Some(p) = &appd.profile { if let Some(p) = &appd.profile {
hash = appd.sum_packages(p); hash = appd.sum_packages(p);
(game_dlls, amd_dlls) = prepare_dlls(p.mod_pkgs(), &appd.pkgs).map_err(|e| e.to_string())?
} }
if let Some(p) = &mut appd.profile { if let Some(p) = &appd.profile {
log::debug!("{}", hash); log::debug!("{}", hash);
p.line_up(hash, refresh, app.clone()).await let info = p.prepare_display()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let lineup_res = p.line_up(hash, refresh, &appd.patch_vec).await
.map_err(|e| e.to_string());
#[cfg(target_os = "windows")]
if let Some(info) = info {
use crate::model::profile::Display;
if lineup_res.is_ok() {
Display::wait_for_exit(app.clone(), info);
} else {
Display::clean_up(&info).map_err(|e| e.to_string())?;
}
}
lineup_res?;
let app_clone = app.clone(); let app_clone = app.clone();
let p_clone = p.clone(); let p_clone = p.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
if let Err(e) = p_clone.start(app_clone).await { if let Err(e) = p_clone.start(StartPayload {
app: app_clone,
game_dlls,
amd_dlls
}).await {
log::error!("Startup failed:\n{}", e); log::error!("Startup failed:\n{}", e);
} }
}); });
@ -152,6 +175,10 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
let appd = state.lock().await; let appd = state.lock().await;
let pkgs_all = appd.pkgs.get_all();
log::debug!("pkgs_all: {:?}", pkgs_all);
Ok(appd.pkgs.get_all()) Ok(appd.pkgs.get_all())
} }
@ -466,7 +493,7 @@ pub async fn list_patches(state: State<'_, Mutex<AppData>>, target: String) -> R
let mut appd = state.lock().await; let mut appd = state.lock().await;
appd.fix(); appd.fix();
let list = appd.patch_set.find_patches(target).map_err(|e| e.to_string())?; let list = appd.patch_vec.find_patches(target).map_err(|e| e.to_string())?;
Ok(list) Ok(list)
} }

View File

@ -8,6 +8,7 @@ use anyhow::Result;
pub struct PatchSelection(pub BTreeMap<String, PatchSelectionData>); pub struct PatchSelection(pub BTreeMap<String, PatchSelectionData>);
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum PatchSelectionData { pub enum PatchSelectionData {
Enabled, Enabled,
Number(i8), Number(i8),
@ -108,10 +109,10 @@ impl<'de> serde::Deserialize<'de> for Patch {
.and_then(Value::as_i64) .and_then(Value::as_i64)
.ok_or_else(|| de::Error::missing_field("min"))? .ok_or_else(|| de::Error::missing_field("min"))?
).map_err(|_| de::Error::missing_field("min"))?, ).map_err(|_| de::Error::missing_field("min"))?,
max: i32::try_from(value.get("min") max: i32::try_from(value.get("max")
.and_then(Value::as_i64) .and_then(Value::as_i64)
.ok_or_else(|| de::Error::missing_field("min"))? .ok_or_else(|| de::Error::missing_field("max"))?
).map_err(|_| de::Error::missing_field("min"))? ).map_err(|_| de::Error::missing_field("max"))?
}), }),
None => { None => {
let mut patches = vec![]; let mut patches = vec![];

View File

@ -164,8 +164,11 @@ pub struct Mu3Ini {
pub blacklist: Option<(i32, i32)>, pub blacklist: Option<(i32, i32)>,
} }
fn default_true() -> bool { true }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
pub struct OngekiKeyboard { pub struct OngekiKeyboard {
#[serde(default = "default_true")] pub enabled: bool,
pub use_mouse: bool, pub use_mouse: bool,
pub coin: i32, pub coin: i32,
pub svc: i32, pub svc: i32,
@ -185,6 +188,7 @@ pub struct OngekiKeyboard {
impl Default for OngekiKeyboard { impl Default for OngekiKeyboard {
fn default() -> Self { fn default() -> Self {
Self { Self {
enabled: true,
use_mouse: true, use_mouse: true,
test: 0x70, test: 0x70,
svc: 0x71, svc: 0x71,
@ -205,6 +209,7 @@ impl Default for OngekiKeyboard {
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ChunithmKeyboard { pub struct ChunithmKeyboard {
#[serde(default = "default_true")] pub enabled: bool,
pub coin: i32, pub coin: i32,
pub svc: i32, pub svc: i32,
pub test: i32, pub test: i32,
@ -215,6 +220,7 @@ pub struct ChunithmKeyboard {
impl Default for ChunithmKeyboard { impl Default for ChunithmKeyboard {
fn default() -> Self { fn default() -> Self {
Self { Self {
enabled: true,
test: 0x70, test: 0x70,
svc: 0x71, svc: 0x71,
coin: 0x72, coin: 0x72,

View File

@ -77,34 +77,46 @@ impl Keyboard {
pub fn line_up(&self, ini: &mut Ini) -> Result<()> { pub fn line_up(&self, ini: &mut Ini) -> Result<()> {
match self { match self {
Keyboard::Ongeki(kb) => { Keyboard::Ongeki(kb) => {
ini.with_section(Some("io4")) if kb.enabled {
.set("test", kb.test.to_string()) ini.with_section(Some("io4"))
.set("service", kb.svc.to_string()) .set("test", kb.test.to_string())
.set("coin", kb.coin.to_string()) .set("service", kb.svc.to_string())
.set("left1", kb.l1.to_string()) .set("coin", kb.coin.to_string())
.set("left2", kb.l2.to_string()) .set("left1", kb.l1.to_string())
.set("left3", kb.l3.to_string()) .set("left2", kb.l2.to_string())
.set("right1", kb.r1.to_string()) .set("left3", kb.l3.to_string())
.set("right2", kb.r2.to_string()) .set("right1", kb.r1.to_string())
.set("right3", kb.r3.to_string()) .set("right2", kb.r2.to_string())
.set("leftSide", kb.lwad.to_string()) .set("right3", kb.r3.to_string())
.set("rightSide", kb.rwad.to_string()) .set("leftSide", kb.lwad.to_string())
.set("leftMenu", kb.lmenu.to_string()) .set("rightSide", kb.rwad.to_string())
.set("rightMenu", kb.rmenu.to_string()) .set("leftMenu", kb.lmenu.to_string())
.set("mouse", if kb.use_mouse { "1" } else { "0" }); .set("rightMenu", kb.rmenu.to_string())
.set("mouse", if kb.use_mouse { "1" } else { "0" });
} else {
ini.with_section(Some("io4"))
.set("enable", "0");
}
} }
Keyboard::Chunithm(kb) => { Keyboard::Chunithm(kb) => {
for (i, cell) in kb.cell.iter().enumerate() { if kb.enabled {
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string()); for (i, cell) in kb.cell.iter().enumerate() {
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string());
}
for (i, ir) in kb.ir.iter().enumerate() {
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), ir.to_string());
}
ini.with_section(Some("io3"))
.set("test", kb.test.to_string())
.set("service", kb.svc.to_string())
.set("coin", kb.coin.to_string())
.set("ir", "0");
} else {
ini.with_section(Some("io4"))
.set("enable", "0");
ini.with_section(Some("slider"))
.set("enable", "0");
} }
for (i, ir) in kb.ir.iter().enumerate() {
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), ir.to_string());
}
ini.with_section(Some("io3"))
.set("test", kb.test.to_string())
.set("service", kb.svc.to_string())
.set("coin", kb.coin.to_string())
.set("ir", "0");
} }
} }

View File

@ -0,0 +1,59 @@
use std::path::Path;
use anyhow::Result;
use crate::model::patch::{Patch, PatchData, PatchFileVec, PatchSelection, PatchSelectionData};
impl PatchSelection {
pub async fn render_to_file(
&self,
filename: &str,
patches: &PatchFileVec,
path: impl AsRef<Path>
) -> Result<()> {
let mut res = "".to_owned();
for file in &patches.0 {
for list in &file.0 {
if list.filename != filename {
continue;
}
for patch in &list.patches {
if let Some(selection) = self.0.get(&patch.id) {
res += &Self::render(filename, patch, selection);
}
}
}
}
tokio::fs::write(path, res).await?;
Ok(())
}
fn render(filename: &str, patch: &Patch, sel: &PatchSelectionData) -> String {
let mut res = "".to_owned();
match &patch.data {
PatchData::Normal(data) => {
for p in &data.patches {
res += &format!("{} F+{:X} ", filename, p.offset);
for on in &p.on {
res += &format!("{:02X}", on);
}
res += " ";
for off in &p.off {
res += &format!("{:02X}", off);
}
res += "\n";
}
},
PatchData::Number(data) => {
if let PatchSelectionData::Number(val) = sel {
let width = (data.size as usize) * 2usize;
res += &format!("{} F+{:X} {:0width$X} {:0width$X}", filename, data.offset, val, data.default, width = width);
} else {
log::error!("invalid number patch {:?}", patch);
}
}
}
format!("{}\n", res)
}
}

View File

@ -1,8 +1,10 @@
use anyhow::Result; use anyhow::Result;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use crate::pkg::PkgKey; use std::path::PathBuf;
use crate::pkg::{PkgKey, Status};
use crate::pkg_store::PackageStore;
use crate::util; use crate::util;
use crate::profiles::ProfilePaths; use crate::profiles::types::ProfilePaths;
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> { pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> {
log::debug!("begin prepare packages"); log::debug!("begin prepare packages");
@ -22,10 +24,10 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
for m in pkgs { for m in pkgs {
log::debug!("preparing {}", m); log::debug!("preparing {}", m);
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition")); let (namespace, name) = m.split()?;
if redo_bepinex { if redo_bepinex {
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen let bpx_dir = util::pkg_dir_of(&namespace, &name)
.join("app") .join("app")
.join("BepInEx"); .join("BepInEx");
if bpx_dir.exists() { if bpx_dir.exists() {
@ -33,7 +35,7 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
} }
} }
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option"); let opt_dir = util::pkg_dir_of(&namespace, &name).join("option");
if opt_dir.exists() { if opt_dir.exists() {
let x = opt_dir.read_dir().unwrap().next().unwrap()?; let x = opt_dir.read_dir().unwrap().next().unwrap()?;
if x.metadata()?.is_dir() { if x.metadata()?.is_dir() {
@ -45,4 +47,27 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
log::debug!("end prepare packages"); log::debug!("end prepare packages");
Ok(()) Ok(())
}
pub fn prepare_dlls(
enabled_pkgs: &BTreeSet<PkgKey>,
store: &PackageStore,
) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
let mut res_game = Vec::new();
let mut res_amd = Vec::new();
for pkg in enabled_pkgs {
if let Ok(pkg) = store.get(&pkg) {
if let Some(loc) = &pkg.loc {
if let Status::OK(_, dlls) = &loc.status {
if let Some(game_dll) = &dlls.game {
res_game.push(pkg.path().join(game_dll.clone()));
}
if let Some(amd_dll) = &dlls.amd {
res_amd.push(pkg.path().join(amd_dll.clone()));
}
}
}
}
}
Ok((res_game, res_amd))
} }

View File

@ -20,7 +20,7 @@ pub enum PackageSource {
Local(Game) Local(Game)
} }
#[derive(Clone, Default, Serialize, Deserialize)] #[derive(Clone, Default, Serialize, Deserialize, Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct Package { pub struct Package {
pub namespace: String, pub namespace: String,
@ -31,11 +31,17 @@ pub struct Package {
pub source: PackageSource, pub source: PackageSource,
} }
#[derive(Clone, PartialEq, Serialize, Deserialize)] #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum Status { pub enum Status {
Unchecked, Unchecked,
Unsupported, Unsupported,
OK(BitFlags<Feature>) OK(BitFlags<Feature>, DLLs),
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct DLLs {
pub game: Option<String>,
pub amd: Option<String>
} }
#[bitflags] #[bitflags]
@ -54,7 +60,7 @@ pub enum Feature {
AmdDLL AmdDLL
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize, Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct Local { pub struct Local {
pub version: String, pub version: String,
@ -64,7 +70,7 @@ pub struct Local {
pub icon: String, pub icon: String,
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize, Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct Remote { pub struct Remote {
pub version: String, pub version: String,
@ -77,6 +83,14 @@ pub struct Remote {
pub dependencies: BTreeSet<PkgKey>, pub dependencies: BTreeSet<PkgKey>,
} }
impl PkgKey {
pub fn split(&self) -> Result<(String, String)> {
let (namespace, name) = self.0
.split_at(self.0.find("-").ok_or_else(|| anyhow!("Invalid package key"))?);
Ok((namespace.to_owned(), name[1..].to_owned())) // cut the hyphen
}
}
impl Package { impl Package {
pub fn from_rainy(mut p: rainy::V1Package) -> Option<Package> { pub fn from_rainy(mut p: rainy::V1Package) -> Option<Package> {
if p.versions.len() == 0 { if p.versions.len() == 0 {
@ -209,37 +223,50 @@ impl Package {
fn parse_status(mft: &PackageManifest) -> Status { fn parse_status(mft: &PackageManifest) -> Status {
if mft.installers.len() == 0 { if mft.installers.len() == 0 {
return Status::OK(make_bitflags!(Feature::Mod));//Unchecked return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None }); //Unchecked
} else if mft.installers.len() == 1 { } else {
if let Some(serde_json::Value::String(id)) = &mft.installers[0].get("identifier") { let mut flags = BitFlags::default();
if id == "rainycolor" { let mut game_dll = None;
return Status::OK(make_bitflags!(Feature::Mod)); let mut amd_dll = None;
} else if id == "segatools" { for installer in &mft.installers {
// Multiple features in the same dll (yubideck etc.) should be supported at some point if let Some(serde_json::Value::String(id)) = installer.get("identifier") {
let mut flags = BitFlags::default(); if id == "rainycolor" {
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") { flags |= Feature::Mod;
if module == "mu3hook" { } else if id == "segatools" {
flags |= Feature::Mu3Hook; // Multiple features in the same dll (yubideck etc.) should be supported at some point
} else if module == "chusanhook" { if let Some(serde_json::Value::String(module)) = installer.get("module") {
flags |= Feature::ChusanHook; if module == "mu3hook" {
} else if module == "amnet" { flags |= Feature::Mu3Hook;
flags |= Feature::AMNet | Feature::Aime; } else if module == "chusanhook" {
} else if module == "aimeio" { flags |= Feature::ChusanHook;
flags |= Feature::Aime; } else if module == "amnet" {
} else if module == "mu3io" { flags |= Feature::AMNet | Feature::Aime;
flags |= Feature::Mu3IO; } else if module == "aimeio" {
} else if module == "chuniio" { flags |= Feature::Aime;
flags |= Feature::ChuniIO; } else if module == "mu3io" {
} else if module == "mempatcher" { flags |= Feature::Mu3IO;
flags |= Feature::Mempatcher; } else if module == "chuniio" {
} else if module == "game-dll" { flags |= Feature::ChuniIO;
flags |= Feature::GameDLL; }
} }
} else if id == "native-mod" {
if let Some(serde_json::Value::String(path)) = installer.get("dll-game") {
flags |= Feature::GameDLL;
flags |= Feature::Mod;
game_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll-amdaemon") {
flags |= Feature::AmdDLL;
flags |= Feature::Mod;
amd_dll = Some(path.to_owned());
}
} else {
return Status::Unsupported;
} }
return Status::OK(flags);
} }
} }
log::debug!("{} parse result: {:?} {:?} {:?}", mft.name, flags, game_dll, amd_dll);
Status::OK(flags, DLLs { game: game_dll, amd: amd_dll })
} }
Status::Unsupported
} }
} }

View File

@ -8,7 +8,7 @@ use tokio::task::JoinSet;
use crate::model::local::{PackageList, PackageListEntry}; use crate::model::local::{PackageList, PackageListEntry};
use crate::model::misc::Game; use crate::model::misc::Game;
use crate::model::rainy; use crate::model::rainy;
use crate::pkg::{Package, PackageSource, PkgKey, Remote, Status}; use crate::pkg::{Feature, Package, PackageSource, PkgKey, Remote, Status};
use crate::util; use crate::util;
use crate::download_handler::DownloadHandler; use crate::download_handler::DownloadHandler;
@ -67,6 +67,21 @@ impl PackageStore {
.collect() .collect()
} }
#[allow(dead_code)]
pub fn get_by_feature(&self, feature: Feature) -> Vec<PkgKey> {
self.store.iter()
.filter(|(_, v)| {
if let Some(loc) = &v.loc {
if let Status::OK(flags, _) = loc.status {
return flags.contains(feature);
}
}
return false
})
.map(|(k, _)| k.clone())
.collect()
}
pub async fn reload_package(&mut self, key: PkgKey) { pub async fn reload_package(&mut self, key: PkgKey) {
let dir = util::pkg_dir().join(&key.0); let dir = util::pkg_dir().join(&key.0);
if let Ok(pkg) = Package::from_dir(dir, PackageSource::Rainy).await { if let Ok(pkg) = Package::from_dir(dir, PackageSource::Rainy).await {

View File

@ -1,7 +1,6 @@
use serde::{Deserialize, Serialize}; pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use tauri::AppHandle;
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}}; use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
use crate::{model::{misc::Game, patch::PatchSelection, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util}; use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
use tauri::Emitter; use tauri::Emitter;
use std::process::Stdio; use std::process::Stdio;
use crate::model::profile::BepInEx; use crate::model::profile::BepInEx;
@ -11,57 +10,7 @@ use std::fs::File;
use tokio::process::Command; use tokio::process::Command;
use tokio::task::JoinSet; use tokio::task::JoinSet;
pub trait ProfilePaths { pub mod types;
fn config_dir(&self) -> PathBuf;
fn data_dir(&self) -> PathBuf;
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct ProfileMeta {
pub game: Game,
pub name: String
}
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))
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Profile {
pub meta: ProfileMeta,
pub data: ProfileData,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ProfileData {
pub mods: BTreeSet<PkgKey>,
pub sgt: Segatools,
pub network: Network,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bepinex: Option<BepInEx>,
#[cfg(not(target_os = "windows"))]
pub wine: crate::model::profile::Wine,
#[serde(skip_serializing_if = "Option::is_none")]
pub mu3_ini: Option<Mu3Ini>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patches: Option<PatchSelection>,
}
impl Profile { impl Profile {
pub fn new(mut meta: ProfileMeta) -> Result<Self> { pub fn new(mut meta: ProfileMeta) -> Result<Self> {
@ -205,27 +154,15 @@ impl Profile {
self.data.patches = source.patches; self.data.patches = source.patches;
} }
} }
pub async fn line_up(&self, pkg_hash: String, refresh: bool, _app: AppHandle) -> Result<()> { pub fn prepare_display(&self) -> Result<Option<DisplayInfo>> {
let info = match &self.data.display { let info = match &self.data.display {
None => None, None => None,
Some(display) => display.prepare()? Some(display) => display.prepare()?
}; };
let res = self.line_up_the_rest(pkg_hash, refresh).await; Ok(info)
#[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, refresh: bool) -> Result<()> { pub async fn line_up(&self, pkg_hash: String, refresh: bool, patch_files: &PatchFileVec) -> Result<()> {
if !self.data_dir().exists() { if !self.data_dir().exists() {
tokio::fs::create_dir(self.data_dir()).await?; tokio::fs::create_dir(self.data_dir()).await?;
} }
@ -235,10 +172,13 @@ impl Profile {
util::clean_up_opts(self.data_dir().join("option"))?; util::clean_up_opts(self.data_dir().join("option"))?;
let hash_check = Self::hash_check(&hash_path, &pkg_hash).await? || refresh; let hash_check = Self::hash_check(&hash_path, &pkg_hash).await? || refresh;
prepare_packages(&self.meta, &self.data.mods, hash_check).await prepare_packages(&self.meta, &self.data.mods, hash_check).await
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?; .map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?; .map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
self.data.network.line_up(&mut ini)?; self.data.network.line_up(&mut ini)?;
if let Some(display) = &self.data.display { if let Some(display) = &self.data.display {
@ -260,10 +200,18 @@ impl Profile {
mu3ini.line_up(&self.data.sgt.target.parent().unwrap())?; mu3ini.line_up(&self.data.sgt.target.parent().unwrap())?;
} }
if let Some(patches) = &self.data.patches {
futures::try_join!(
patches.render_to_file("amdaemon.exe", patch_files, self.data_dir().join("patch-amd.mph")),
patches.render_to_file("chusanApp.exe", patch_files, self.data_dir().join("patch-game.mph"))
)?;
}
Ok(()) Ok(())
} }
pub async fn start(&self, app: AppHandle) -> Result<()> { pub async fn start(&self, payload: StartPayload) -> Result<()> {
let ini_path = self.data_dir().join("segatools.ini"); let ini_path = self.data_dir().join("segatools.ini");
log::debug!("With path {:?}", ini_path); log::debug!("With path {:?}", ini_path);
@ -294,10 +242,23 @@ impl Profile {
&ini_path, &ini_path,
) )
.current_dir(&exe_dir) .current_dir(&exe_dir)
.arg("/C") .raw_arg("/C")
.arg(&sgt_dir.join(self.meta.game.inject_amd())) .arg(&sgt_dir.join(self.meta.game.inject_amd()))
.args(["-d", "-k"]) .raw_arg("-d")
.arg(sgt_dir.join(self.meta.game.hook_amd())) .raw_arg("-k")
.arg(sgt_dir.join(self.meta.game.hook_amd()));
// for dll in payload.amd_dlls {
// amd_builder.arg("-k");
// amd_builder.arg(dll);
// }
// if self.meta.game.has_module(ProfileModule::Mempatcher) {
// amd_builder.arg("--mempatch");
// amd_builder.arg(self.data_dir().join("patch-amd.mph"));
// }
amd_builder
.arg("amdaemon.exe") .arg("amdaemon.exe")
.args(self.meta.game.amd_args()); .args(self.meta.game.amd_args());
@ -317,9 +278,16 @@ impl Profile {
self.config_dir().join("saekawa.toml"), self.config_dir().join("saekawa.toml"),
) )
.current_dir(&exe_dir) .current_dir(&exe_dir)
.args(["-d", "-k"]) .raw_arg("-d")
.arg(sgt_dir.join(self.meta.game.hook_exe())) .raw_arg("-k")
.arg(self.meta.game.exe()); .arg(sgt_dir.join(self.meta.game.hook_exe()));
for dll in payload.game_dlls {
game_builder.raw_arg("-k");
game_builder.arg(dll);
}
game_builder.arg(self.meta.game.exe());
if self.meta.game.has_module(ProfileModule::BepInEx) { if self.meta.game.has_module(ProfileModule::BepInEx) {
if let Some(display) = &self.data.display { if let Some(display) = &self.data.display {
@ -339,6 +307,11 @@ impl Profile {
} }
} }
if self.meta.game.has_module(ProfileModule::Mempatcher) {
game_builder.arg("--mempatch");
game_builder.arg(self.data_dir().join("patch-game.mph"));
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
amd_builder.env("WINEPREFIX", &self.wine.prefix); amd_builder.env("WINEPREFIX", &self.wine.prefix);
@ -383,7 +356,7 @@ impl Profile {
(game.wait().await.expect("game failed to run"), "game") (game.wait().await.expect("game failed to run"), "game")
}); });
if let Err(e) = app.emit("launch-start", "") { if let Err(e) = payload.app.emit("launch-start", "") {
log::warn!("Unable to emit launch-start: {}", e); log::warn!("Unable to emit launch-start: {}", e);
} }
@ -401,7 +374,7 @@ impl Profile {
log::debug!("Fin"); log::debug!("Fin");
if let Err(e) = app.emit("launch-end", "") { if let Err(e) = payload.app.emit("launch-end", "") {
log::warn!("Unable to emit launch-end: {}", e); log::warn!("Unable to emit launch-end: {}", e);
} }

View File

@ -0,0 +1,64 @@
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use std::{collections::BTreeSet, path::PathBuf};
use crate::{model::{misc::Game, patch::PatchSelection, profile::{Keyboard, Mu3Ini}}, pkg::PkgKey, util};
use crate::model::profile::BepInEx;
use crate::model::profile::{Display, Network, Segatools};
pub trait ProfilePaths {
fn config_dir(&self) -> PathBuf;
fn data_dir(&self) -> PathBuf;
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct ProfileMeta {
pub game: Game,
pub name: String
}
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))
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Profile {
pub meta: ProfileMeta,
pub data: ProfileData,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ProfileData {
pub mods: BTreeSet<PkgKey>,
pub sgt: Segatools,
pub network: Network,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bepinex: Option<BepInEx>,
#[cfg(not(target_os = "windows"))]
pub wine: crate::model::profile::Wine,
#[serde(skip_serializing_if = "Option::is_none")]
pub mu3_ini: Option<Mu3Ini>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patches: Option<PatchSelection>,
}
pub struct StartPayload {
pub app: AppHandle,
pub game_dlls: Vec<PathBuf>,
pub amd_dlls: Vec<PathBuf>,
}

View File

@ -90,8 +90,8 @@
type: 'number', type: 'number',
default: 3, default: 3,
offset: 3768513, offset: 3768513,
size: 4, size: 1,
min: 3, min: 1,
max: 12, max: 12,
}, },
{ {

View File

@ -78,7 +78,6 @@ const iconSrc = computed(() => {
</span> </span>
<span <span
v-if=" v-if="
hasFeature(pkg, Feature.Mempatcher) ||
hasFeature(pkg, Feature.GameDLL) || hasFeature(pkg, Feature.GameDLL) ||
hasFeature(pkg, Feature.AmdDLL) hasFeature(pkg, Feature.AmdDLL)
" "

View File

@ -3,37 +3,53 @@ import InputNumber from 'primevue/inputnumber';
import ToggleSwitch from 'primevue/toggleswitch'; import ToggleSwitch from 'primevue/toggleswitch';
import OptionRow from './OptionRow.vue'; import OptionRow from './OptionRow.vue';
import { usePrfStore } from '../stores'; import { usePrfStore } from '../stores';
import { Patch } from '@/types';
const prf = usePrfStore(); const prf = usePrfStore();
const toggleUnary = (key: string, val: boolean) => { const toggleUnary = (key: string, val: boolean) => {
if (val) { if (val) {
prf.current!.data.patches[key] = 'Enabled'; prf.current!.data.patches[key] = 'enabled';
} else {
delete prf.current!.data.patches[key];
}
};
const setNumber = (key: string, val: number) => {
if (val) {
prf.current!.data.patches[key] = { number: val };
} else { } else {
delete prf.current!.data.patches[key]; delete prf.current!.data.patches[key];
} }
}; };
defineProps({ defineProps({
id: String, patch: Object as () => Patch,
name: String,
tooltip: String,
type: String,
defaultValue: Number,
}); });
</script> </script>
<template> <template>
<OptionRow :title="name" :tooltip="tooltip" :greytext="id"> <OptionRow
:title="patch?.name"
:tooltip="patch?.tooltip"
:greytext="patch?.id"
>
<ToggleSwitch <ToggleSwitch
v-if="type === undefined" v-if="patch?.type === undefined"
:model-value="prf.current!.data.patches[id!] !== undefined" :model-value="prf.current!.data.patches[patch!.id!] !== undefined"
@update:model-value="(v: boolean) => toggleUnary(id!, v)" @update:model-value="(v: boolean) => toggleUnary(patch!.id!, v)"
/> />
<InputNumber <InputNumber
v-else v-else-if="patch?.type === 'number'"
class="number-input" class="number-input"
:placeholder="(defaultValue ?? 0).toString()" :model-value="
(prf.current!.data.patches[patch!.id!] as { number: number })
?.number
"
@update:model-value="(v: number) => setNumber(patch!.id!, v)"
:min="patch?.min"
:max="patch?.max"
:placeholder="(patch?.default ?? 0).toString()"
/> />
</OptionRow> </OptionRow>
</template> </template>

View File

@ -39,11 +39,7 @@ const errorMessage =
<PatchEntry <PatchEntry
v-if="gamePatches !== null" v-if="gamePatches !== null"
v-for="p in gamePatches" v-for="p in gamePatches"
:id="p.id" :patch="p"
:title="p.name"
:tooltip="p.tooltip"
:type="p.type"
:default-value="p.default"
/> />
<div v-if="gamePatches === null">Loading...</div> <div v-if="gamePatches === null">Loading...</div>
<div v-if="gamePatches !== null && gamePatches.length === 0"> <div v-if="gamePatches !== null && gamePatches.length === 0">
@ -54,11 +50,7 @@ const errorMessage =
<PatchEntry <PatchEntry
v-if="amdPatches !== null" v-if="amdPatches !== null"
v-for="p in amdPatches" v-for="p in amdPatches"
:id="p.id" :patch="p"
:title="p.name"
:tooltip="p.tooltip"
:type="p.type"
:default-value="p.default"
/> />
<div v-if="gamePatches === null">Loading...</div> <div v-if="gamePatches === null">Loading...</div>
<div v-if="amdPatches !== null && amdPatches.length === 0"> <div v-if="amdPatches !== null && amdPatches.length === 0">

View File

@ -6,13 +6,14 @@ import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue'; import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores'; import { usePrfStore } from '../../stores';
ToggleSwitch;
const prf = usePrfStore(); const prf = usePrfStore();
</script> </script>
<template> <template>
<OptionCategory title="Keyboard"> <OptionCategory title="Keyboard">
<OptionRow title="Enable">
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" />
</OptionRow>
<OptionRow <OptionRow
title="Lever mode" title="Lever mode"
v-if="prf.current!.data.keyboard!.game === 'Ongeki'" v-if="prf.current!.data.keyboard!.game === 'Ongeki'"
@ -28,7 +29,6 @@ const prf = usePrfStore();
option-value="value" option-value="value"
/> />
</OptionRow> </OptionRow>
<OptionRow v-if="prf.current!.data.keyboard!.game === 'Chunithm'" />
<div <div
:style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`" :style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`"
> >

View File

@ -40,7 +40,7 @@ export type Status =
| 'Unchecked' | 'Unchecked'
| 'Unsupported' | 'Unsupported'
| { | {
OK: Feature; OK: [Feature, String, String];
}; };
export type Game = 'ongeki' | 'chunithm'; export type Game = 'ongeki' | 'chunithm';
@ -59,7 +59,7 @@ export interface ProfileData {
mu3_ini: Mu3IniConfig | undefined; mu3_ini: Mu3IniConfig | undefined;
keyboard: KeyboardConfig | undefined; keyboard: KeyboardConfig | undefined;
patches: { patches: {
[key: string]: 'Enabled' | { Number: number } | { Hex: Int8Array }; [key: string]: 'enabled' | { number: number } | { hex: Int8Array };
}; };
} }
@ -112,6 +112,7 @@ export interface Mu3IniConfig {
export interface OngekiButtons { export interface OngekiButtons {
use_mouse: boolean; use_mouse: boolean;
enabled: boolean;
coin: number; coin: number;
svc: number; svc: number;
test: number; test: number;
@ -128,6 +129,7 @@ export interface OngekiButtons {
} }
export interface ChunithmButtons { export interface ChunithmButtons {
enabled: boolean;
coin: number; coin: number;
svc: number; svc: number;
test: number; test: number;
@ -164,4 +166,6 @@ export interface Patch {
tooltip: string; tooltip: string;
type: undefined | 'number'; type: undefined | 'number';
default: number; default: number;
min: number;
max: number;
} }

View File

@ -52,7 +52,7 @@ export const hasFeature = (pkg: Package | undefined, feature: Feature) => {
pkg.loc !== null && pkg.loc !== null &&
pkg.loc !== undefined && pkg.loc !== undefined &&
typeof pkg.loc?.status !== 'string' && typeof pkg.loc?.status !== 'string' &&
pkg.loc.status.OK & feature pkg.loc.status.OK[0] & feature
); );
}; };