forked from akanyan/STARTLINER
feat: chusanApp.exe patching
This commit is contained in:
@ -2,7 +2,7 @@ use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use crate::model::config::GlobalConfig;
|
||||
use crate::model::patch::PatchFileVec;
|
||||
use crate::pkg::{Feature, Status};
|
||||
use crate::profiles::Profile;
|
||||
use crate::profiles::types::Profile;
|
||||
use crate::{model::misc::Game, pkg::PkgKey};
|
||||
use crate::pkg_store::PackageStore;
|
||||
use crate::util;
|
||||
@ -18,7 +18,7 @@ pub struct AppData {
|
||||
pub pkgs: PackageStore,
|
||||
pub cfg: GlobalConfig,
|
||||
pub state: GlobalState,
|
||||
pub patch_set: PatchFileVec,
|
||||
pub patch_vec: PatchFileVec,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
@ -39,7 +39,7 @@ impl AppData {
|
||||
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}"))
|
||||
.unwrap_or_default();
|
||||
|
||||
@ -50,7 +50,7 @@ impl AppData {
|
||||
pkgs: PackageStore::new(apph.clone()),
|
||||
cfg,
|
||||
state: GlobalState { remain_open: true },
|
||||
patch_set
|
||||
patch_vec
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,8 +85,7 @@ impl AppData {
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
||||
|
||||
if let Status::OK(feature_set) = loc.status {
|
||||
log::debug!("{:?}", feature_set);
|
||||
if let Status::OK(feature_set, _) = loc.status {
|
||||
if feature_set.contains(Feature::Mod) {
|
||||
profile.mod_pkgs_mut().insert(key);
|
||||
}
|
||||
|
@ -8,9 +8,10 @@ use tauri::{AppHandle, Manager, State};
|
||||
use crate::model::config::GlobalConfigField;
|
||||
use crate::model::misc::Game;
|
||||
use crate::model::patch::Patch;
|
||||
use crate::modules::package::prepare_dlls;
|
||||
use crate::pkg::{Package, PkgKey};
|
||||
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::model::misc::StartCheckError;
|
||||
use crate::util;
|
||||
@ -59,18 +60,40 @@ pub async fn startline(app: AppHandle, refresh: bool) -> Result<(), String> {
|
||||
let state = app.state::<Mutex<AppData>>();
|
||||
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 {
|
||||
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);
|
||||
p.line_up(hash, refresh, app.clone()).await
|
||||
let info = p.prepare_display()
|
||||
.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 p_clone = p.clone();
|
||||
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);
|
||||
}
|
||||
});
|
||||
@ -152,6 +175,10 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
|
||||
|
||||
let appd = state.lock().await;
|
||||
|
||||
let pkgs_all = appd.pkgs.get_all();
|
||||
|
||||
log::debug!("pkgs_all: {:?}", pkgs_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;
|
||||
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)
|
||||
}
|
@ -8,6 +8,7 @@ use anyhow::Result;
|
||||
pub struct PatchSelection(pub BTreeMap<String, PatchSelectionData>);
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PatchSelectionData {
|
||||
Enabled,
|
||||
Number(i8),
|
||||
@ -108,10 +109,10 @@ impl<'de> serde::Deserialize<'de> for Patch {
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| 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)
|
||||
.ok_or_else(|| de::Error::missing_field("min"))?
|
||||
).map_err(|_| de::Error::missing_field("min"))?
|
||||
.ok_or_else(|| de::Error::missing_field("max"))?
|
||||
).map_err(|_| de::Error::missing_field("max"))?
|
||||
}),
|
||||
None => {
|
||||
let mut patches = vec![];
|
||||
|
@ -164,8 +164,11 @@ pub struct Mu3Ini {
|
||||
pub blacklist: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct OngekiKeyboard {
|
||||
#[serde(default = "default_true")] pub enabled: bool,
|
||||
pub use_mouse: bool,
|
||||
pub coin: i32,
|
||||
pub svc: i32,
|
||||
@ -185,6 +188,7 @@ pub struct OngekiKeyboard {
|
||||
impl Default for OngekiKeyboard {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
use_mouse: true,
|
||||
test: 0x70,
|
||||
svc: 0x71,
|
||||
@ -205,6 +209,7 @@ impl Default for OngekiKeyboard {
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct ChunithmKeyboard {
|
||||
#[serde(default = "default_true")] pub enabled: bool,
|
||||
pub coin: i32,
|
||||
pub svc: i32,
|
||||
pub test: i32,
|
||||
@ -215,6 +220,7 @@ pub struct ChunithmKeyboard {
|
||||
impl Default for ChunithmKeyboard {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
test: 0x70,
|
||||
svc: 0x71,
|
||||
coin: 0x72,
|
||||
|
@ -77,34 +77,46 @@ impl Keyboard {
|
||||
pub fn line_up(&self, ini: &mut Ini) -> Result<()> {
|
||||
match self {
|
||||
Keyboard::Ongeki(kb) => {
|
||||
ini.with_section(Some("io4"))
|
||||
.set("test", kb.test.to_string())
|
||||
.set("service", kb.svc.to_string())
|
||||
.set("coin", kb.coin.to_string())
|
||||
.set("left1", kb.l1.to_string())
|
||||
.set("left2", kb.l2.to_string())
|
||||
.set("left3", kb.l3.to_string())
|
||||
.set("right1", kb.r1.to_string())
|
||||
.set("right2", kb.r2.to_string())
|
||||
.set("right3", kb.r3.to_string())
|
||||
.set("leftSide", kb.lwad.to_string())
|
||||
.set("rightSide", kb.rwad.to_string())
|
||||
.set("leftMenu", kb.lmenu.to_string())
|
||||
.set("rightMenu", kb.rmenu.to_string())
|
||||
.set("mouse", if kb.use_mouse { "1" } else { "0" });
|
||||
if kb.enabled {
|
||||
ini.with_section(Some("io4"))
|
||||
.set("test", kb.test.to_string())
|
||||
.set("service", kb.svc.to_string())
|
||||
.set("coin", kb.coin.to_string())
|
||||
.set("left1", kb.l1.to_string())
|
||||
.set("left2", kb.l2.to_string())
|
||||
.set("left3", kb.l3.to_string())
|
||||
.set("right1", kb.r1.to_string())
|
||||
.set("right2", kb.r2.to_string())
|
||||
.set("right3", kb.r3.to_string())
|
||||
.set("leftSide", kb.lwad.to_string())
|
||||
.set("rightSide", kb.rwad.to_string())
|
||||
.set("leftMenu", kb.lmenu.to_string())
|
||||
.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) => {
|
||||
for (i, cell) in kb.cell.iter().enumerate() {
|
||||
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string());
|
||||
if kb.enabled {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
use anyhow::Result;
|
||||
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::profiles::ProfilePaths;
|
||||
use crate::profiles::types::ProfilePaths;
|
||||
|
||||
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> {
|
||||
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 {
|
||||
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 {
|
||||
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("BepInEx");
|
||||
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() {
|
||||
let x = opt_dir.read_dir().unwrap().next().unwrap()?;
|
||||
if x.metadata()?.is_dir() {
|
||||
@ -46,3 +48,26 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
|
||||
|
||||
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))
|
||||
}
|
@ -20,7 +20,7 @@ pub enum PackageSource {
|
||||
Local(Game)
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Package {
|
||||
pub namespace: String,
|
||||
@ -31,11 +31,17 @@ pub struct Package {
|
||||
pub source: PackageSource,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
|
||||
pub enum Status {
|
||||
Unchecked,
|
||||
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]
|
||||
@ -54,7 +60,7 @@ pub enum Feature {
|
||||
AmdDLL
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Local {
|
||||
pub version: String,
|
||||
@ -64,7 +70,7 @@ pub struct Local {
|
||||
pub icon: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Remote {
|
||||
pub version: String,
|
||||
@ -77,6 +83,14 @@ pub struct Remote {
|
||||
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 {
|
||||
pub fn from_rainy(mut p: rainy::V1Package) -> Option<Package> {
|
||||
if p.versions.len() == 0 {
|
||||
@ -209,37 +223,50 @@ impl Package {
|
||||
|
||||
fn parse_status(mft: &PackageManifest) -> Status {
|
||||
if mft.installers.len() == 0 {
|
||||
return Status::OK(make_bitflags!(Feature::Mod));//Unchecked
|
||||
} else if mft.installers.len() == 1 {
|
||||
if let Some(serde_json::Value::String(id)) = &mft.installers[0].get("identifier") {
|
||||
if id == "rainycolor" {
|
||||
return Status::OK(make_bitflags!(Feature::Mod));
|
||||
} else if id == "segatools" {
|
||||
// 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 == "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 == "mu3io" {
|
||||
flags |= Feature::Mu3IO;
|
||||
} else if module == "chuniio" {
|
||||
flags |= Feature::ChuniIO;
|
||||
} else if module == "mempatcher" {
|
||||
flags |= Feature::Mempatcher;
|
||||
} else if module == "game-dll" {
|
||||
flags |= Feature::GameDLL;
|
||||
return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None }); //Unchecked
|
||||
} else {
|
||||
let mut flags = BitFlags::default();
|
||||
let mut game_dll = None;
|
||||
let mut amd_dll = None;
|
||||
for installer in &mft.installers {
|
||||
if let Some(serde_json::Value::String(id)) = installer.get("identifier") {
|
||||
if id == "rainycolor" {
|
||||
flags |= Feature::Mod;
|
||||
} else if id == "segatools" {
|
||||
// Multiple features in the same dll (yubideck etc.) should be supported at some point
|
||||
if let Some(serde_json::Value::String(module)) = installer.get("module") {
|
||||
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 == "mu3io" {
|
||||
flags |= Feature::Mu3IO;
|
||||
} else if module == "chuniio" {
|
||||
flags |= Feature::ChuniIO;
|
||||
}
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ use tokio::task::JoinSet;
|
||||
use crate::model::local::{PackageList, PackageListEntry};
|
||||
use crate::model::misc::Game;
|
||||
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::download_handler::DownloadHandler;
|
||||
|
||||
@ -67,6 +67,21 @@ impl PackageStore {
|
||||
.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) {
|
||||
let dir = util::pkg_dir().join(&key.0);
|
||||
if let Ok(pkg) = Package::from_dir(dir, PackageSource::Rainy).await {
|
||||
|
@ -1,7 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
|
||||
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 std::process::Stdio;
|
||||
use crate::model::profile::BepInEx;
|
||||
@ -11,57 +10,7 @@ use std::fs::File;
|
||||
use tokio::process::Command;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
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 mod types;
|
||||
|
||||
impl Profile {
|
||||
pub fn new(mut meta: ProfileMeta) -> Result<Self> {
|
||||
@ -205,27 +154,15 @@ impl Profile {
|
||||
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 {
|
||||
None => None,
|
||||
Some(display) => display.prepare()?
|
||||
};
|
||||
|
||||
let res = self.line_up_the_rest(pkg_hash, refresh).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
|
||||
Ok(info)
|
||||
}
|
||||
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() {
|
||||
tokio::fs::create_dir(self.data_dir()).await?;
|
||||
}
|
||||
@ -235,10 +172,13 @@ impl Profile {
|
||||
util::clean_up_opts(self.data_dir().join("option"))?;
|
||||
|
||||
let hash_check = Self::hash_check(&hash_path, &pkg_hash).await? || refresh;
|
||||
|
||||
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)?;
|
||||
|
||||
if let Some(display) = &self.data.display {
|
||||
@ -260,10 +200,18 @@ impl Profile {
|
||||
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(())
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
log::debug!("With path {:?}", ini_path);
|
||||
@ -294,10 +242,23 @@ impl Profile {
|
||||
&ini_path,
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.arg("/C")
|
||||
.raw_arg("/C")
|
||||
.arg(&sgt_dir.join(self.meta.game.inject_amd()))
|
||||
.args(["-d", "-k"])
|
||||
.arg(sgt_dir.join(self.meta.game.hook_amd()))
|
||||
.raw_arg("-d")
|
||||
.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")
|
||||
.args(self.meta.game.amd_args());
|
||||
|
||||
@ -317,9 +278,16 @@ impl Profile {
|
||||
self.config_dir().join("saekawa.toml"),
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.args(["-d", "-k"])
|
||||
.arg(sgt_dir.join(self.meta.game.hook_exe()))
|
||||
.arg(self.meta.game.exe());
|
||||
.raw_arg("-d")
|
||||
.raw_arg("-k")
|
||||
.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 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")]
|
||||
{
|
||||
amd_builder.env("WINEPREFIX", &self.wine.prefix);
|
||||
@ -383,7 +356,7 @@ impl Profile {
|
||||
(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);
|
||||
}
|
||||
|
||||
@ -401,7 +374,7 @@ impl Profile {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
64
rust/src/profiles/types.rs
Normal file
64
rust/src/profiles/types.rs
Normal 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>,
|
||||
}
|
@ -90,8 +90,8 @@
|
||||
type: 'number',
|
||||
default: 3,
|
||||
offset: 3768513,
|
||||
size: 4,
|
||||
min: 3,
|
||||
size: 1,
|
||||
min: 1,
|
||||
max: 12,
|
||||
},
|
||||
{
|
||||
|
@ -78,7 +78,6 @@ const iconSrc = computed(() => {
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
hasFeature(pkg, Feature.Mempatcher) ||
|
||||
hasFeature(pkg, Feature.GameDLL) ||
|
||||
hasFeature(pkg, Feature.AmdDLL)
|
||||
"
|
||||
|
@ -3,37 +3,53 @@ import InputNumber from 'primevue/inputnumber';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import OptionRow from './OptionRow.vue';
|
||||
import { usePrfStore } from '../stores';
|
||||
import { Patch } from '@/types';
|
||||
|
||||
const prf = usePrfStore();
|
||||
|
||||
const toggleUnary = (key: string, val: boolean) => {
|
||||
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 {
|
||||
delete prf.current!.data.patches[key];
|
||||
}
|
||||
};
|
||||
|
||||
defineProps({
|
||||
id: String,
|
||||
name: String,
|
||||
tooltip: String,
|
||||
type: String,
|
||||
defaultValue: Number,
|
||||
patch: Object as () => Patch,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionRow :title="name" :tooltip="tooltip" :greytext="id">
|
||||
<OptionRow
|
||||
:title="patch?.name"
|
||||
:tooltip="patch?.tooltip"
|
||||
:greytext="patch?.id"
|
||||
>
|
||||
<ToggleSwitch
|
||||
v-if="type === undefined"
|
||||
:model-value="prf.current!.data.patches[id!] !== undefined"
|
||||
@update:model-value="(v: boolean) => toggleUnary(id!, v)"
|
||||
v-if="patch?.type === undefined"
|
||||
:model-value="prf.current!.data.patches[patch!.id!] !== undefined"
|
||||
@update:model-value="(v: boolean) => toggleUnary(patch!.id!, v)"
|
||||
/>
|
||||
<InputNumber
|
||||
v-else
|
||||
v-else-if="patch?.type === 'number'"
|
||||
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>
|
||||
</template>
|
||||
|
@ -39,11 +39,7 @@ const errorMessage =
|
||||
<PatchEntry
|
||||
v-if="gamePatches !== null"
|
||||
v-for="p in gamePatches"
|
||||
:id="p.id"
|
||||
:title="p.name"
|
||||
:tooltip="p.tooltip"
|
||||
:type="p.type"
|
||||
:default-value="p.default"
|
||||
:patch="p"
|
||||
/>
|
||||
<div v-if="gamePatches === null">Loading...</div>
|
||||
<div v-if="gamePatches !== null && gamePatches.length === 0">
|
||||
@ -54,11 +50,7 @@ const errorMessage =
|
||||
<PatchEntry
|
||||
v-if="amdPatches !== null"
|
||||
v-for="p in amdPatches"
|
||||
:id="p.id"
|
||||
:title="p.name"
|
||||
:tooltip="p.tooltip"
|
||||
:type="p.type"
|
||||
:default-value="p.default"
|
||||
:patch="p"
|
||||
/>
|
||||
<div v-if="gamePatches === null">Loading...</div>
|
||||
<div v-if="amdPatches !== null && amdPatches.length === 0">
|
||||
|
@ -6,13 +6,14 @@ import OptionCategory from '../OptionCategory.vue';
|
||||
import OptionRow from '../OptionRow.vue';
|
||||
import { usePrfStore } from '../../stores';
|
||||
|
||||
ToggleSwitch;
|
||||
|
||||
const prf = usePrfStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="Keyboard">
|
||||
<OptionRow title="Enable">
|
||||
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" />
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
title="Lever mode"
|
||||
v-if="prf.current!.data.keyboard!.game === 'Ongeki'"
|
||||
@ -28,7 +29,6 @@ const prf = usePrfStore();
|
||||
option-value="value"
|
||||
/>
|
||||
</OptionRow>
|
||||
<OptionRow v-if="prf.current!.data.keyboard!.game === 'Chunithm'" />
|
||||
<div
|
||||
:style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`"
|
||||
>
|
||||
|
@ -40,7 +40,7 @@ export type Status =
|
||||
| 'Unchecked'
|
||||
| 'Unsupported'
|
||||
| {
|
||||
OK: Feature;
|
||||
OK: [Feature, String, String];
|
||||
};
|
||||
|
||||
export type Game = 'ongeki' | 'chunithm';
|
||||
@ -59,7 +59,7 @@ export interface ProfileData {
|
||||
mu3_ini: Mu3IniConfig | undefined;
|
||||
keyboard: KeyboardConfig | undefined;
|
||||
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 {
|
||||
use_mouse: boolean;
|
||||
enabled: boolean;
|
||||
coin: number;
|
||||
svc: number;
|
||||
test: number;
|
||||
@ -128,6 +129,7 @@ export interface OngekiButtons {
|
||||
}
|
||||
|
||||
export interface ChunithmButtons {
|
||||
enabled: boolean;
|
||||
coin: number;
|
||||
svc: number;
|
||||
test: number;
|
||||
@ -164,4 +166,6 @@ export interface Patch {
|
||||
tooltip: string;
|
||||
type: undefined | 'number';
|
||||
default: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ export const hasFeature = (pkg: Package | undefined, feature: Feature) => {
|
||||
pkg.loc !== null &&
|
||||
pkg.loc !== undefined &&
|
||||
typeof pkg.loc?.status !== 'string' &&
|
||||
pkg.loc.status.OK & feature
|
||||
pkg.loc.status.OK[0] & feature
|
||||
);
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user