feat: partial support for patches

This commit is contained in:
2025-04-11 15:27:13 +00:00
parent b9a40d44a8
commit 1a68eda8c1
21 changed files with 1218 additions and 583 deletions

1045
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,7 @@ junction = "1.2.0"
tauri-plugin-fs = "2" tauri-plugin-fs = "2"
yaml-rust2 = "0.10.0" yaml-rust2 = "0.10.0"
enumflags2 = { version = "0.7.11", features = ["serde"] } enumflags2 = { version = "0.7.11", features = ["serde"] }
sha256 = "1.6.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-cli = "2" tauri-plugin-cli = "2"

View File

@ -1,5 +1,6 @@
use std::hash::{DefaultHasher, Hash, Hasher}; use std::hash::{DefaultHasher, Hash, Hasher};
use crate::model::config::GlobalConfig; use crate::model::config::GlobalConfig;
use crate::model::patch::PatchFileVec;
use crate::pkg::{Feature, Status}; use crate::pkg::{Feature, Status};
use crate::profiles::Profile; use crate::profiles::Profile;
use crate::{model::misc::Game, pkg::PkgKey}; use crate::{model::misc::Game, pkg::PkgKey};
@ -17,6 +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,
} }
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
@ -37,13 +39,18 @@ impl AppData {
None => None None => None
}; };
let patch_set = PatchFileVec::new(util::config_dir())
.map_err(|e| log::error!("unable to load patch set: {e}"))
.unwrap_or_default();
log::debug!("Recent profile: {:?}", profile); log::debug!("Recent profile: {:?}", profile);
AppData { AppData {
profile: profile, profile: profile,
pkgs: PackageStore::new(apph.clone()), pkgs: PackageStore::new(apph.clone()),
cfg, cfg,
state: GlobalState { remain_open: true } state: GlobalState { remain_open: true },
patch_set
} }
} }

View File

@ -7,6 +7,7 @@ use tokio::fs;
use tauri::{AppHandle, Manager, State}; 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::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};
@ -442,4 +443,15 @@ pub async fn list_directories() -> Result<util::Dirs, ()> {
#[tauri::command] #[tauri::command]
pub async fn file_exists(path: String) -> Result<bool, ()> { pub async fn file_exists(path: String) -> Result<bool, ()> {
Ok(std::fs::exists(path).unwrap_or(false)) Ok(std::fs::exists(path).unwrap_or(false))
}
#[tauri::command]
pub async fn list_patches(state: State<'_, Mutex<AppData>>, target: String) -> Result<Vec<Patch>, String> {
log::debug!("invoke: list_patches({})", target);
let mut appd = state.lock().await;
appd.fix();
let list = appd.patch_set.find_patches(target).map_err(|e| e.to_string())?;
Ok(list)
} }

View File

@ -7,6 +7,7 @@ mod download_handler;
mod appdata; mod appdata;
mod modules; mod modules;
mod profiles; mod profiles;
mod patcher;
use std::sync::OnceLock; use std::sync::OnceLock;
use anyhow::anyhow; use anyhow::anyhow;
@ -208,6 +209,8 @@ pub async fn run(_args: Vec<String>) {
cmd::list_platform_capabilities, cmd::list_platform_capabilities,
cmd::list_directories, cmd::list_directories,
cmd::file_exists, cmd::file_exists,
cmd::list_patches
]) ])
.build(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while building tauri application"); .expect("error while building tauri application");

View File

@ -3,4 +3,4 @@ pub mod misc;
pub mod rainy; pub mod rainy;
pub mod profile; pub mod profile;
pub mod config; pub mod config;
pub mod segatools_base; pub mod patch;

155
rust/src/model/patch.rs Normal file
View File

@ -0,0 +1,155 @@
use std::collections::BTreeMap;
use serde::{de, Deserialize, Deserializer, Serialize};
use serde::ser::{Serializer, SerializeStruct};
use serde_json::Value;
use anyhow::Result;
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct PatchSelection(pub BTreeMap<String, PatchSelectionData>);
#[derive(Deserialize, Serialize, Clone, Debug)]
pub enum PatchSelectionData {
Enabled,
Number(i8),
Hex(Vec<u8>)
}
#[derive(Default)]
pub struct PatchFileVec(pub Vec<PatchFile>);
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct PatchFile(pub Vec<PatchList>);
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct PatchList {
pub filename: String,
pub version: String,
pub sha256: String,
pub patches: Vec<Patch>
}
#[derive(Clone, Debug)]
pub struct Patch {
pub id: String,
pub name: String,
pub tooltip: Option<String>,
pub data: PatchData
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(untagged)]
pub enum PatchData {
Normal(NormalPatch),
Number(NumberPatch),
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct NormalPatch {
pub patches: Vec<NormalPatchField>
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct NormalPatchField {
pub offset: u64,
pub off: Vec<u8>,
pub on: Vec<u8>
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct NumberPatch {
pub offset: u64,
pub size: i64,
pub min: i32,
pub max: i32
}
impl Serialize for Patch {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
let mut state = serializer.serialize_struct("Patch", 7)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("name", &self.name)?;
state.serialize_field("tooltip", &self.tooltip)?;
match &self.data {
PatchData::Normal(patch) => {
state.serialize_field("patches", &patch.patches)?;
}
PatchData::Number(patch) => {
state.serialize_field("type", "number")?;
state.serialize_field("offset", &patch.offset)?;
state.serialize_field("size", &patch.size)?;
state.serialize_field("min", &patch.min)?;
state.serialize_field("max", &patch.max)?;
}
}
state.end()
}
}
impl<'de> serde::Deserialize<'de> for Patch {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let value = Value::deserialize(d)?;
let data = Ok(match value.get("type").and_then(Value::as_str) {
Some("number") => PatchData::Number(NumberPatch {
offset: value.get("offset")
.and_then(Value::as_u64)
.ok_or_else(|| de::Error::missing_field("offset"))?,
size: value.get("size")
.and_then(Value::as_i64)
.ok_or_else(|| de::Error::missing_field("size"))?,
min: i32::try_from(value.get("min")
.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")
.and_then(Value::as_i64)
.ok_or_else(|| de::Error::missing_field("min"))?
).map_err(|_| de::Error::missing_field("min"))?
}),
None => {
let mut patches = vec![];
for patch in value.get("patches").and_then(Value::as_array).unwrap() {
let mut off_list: Vec<u8> = Vec::new();
let mut on_list: Vec<u8> = Vec::new();
for off in patch.get("off").and_then(Value::as_array).unwrap() {
off_list.push(u8::try_from(
off.as_u64().ok_or_else(|| de::Error::missing_field("off"))?
).map_err(|_| de::Error::missing_field("off"))?);
}
for on in patch.get("on").and_then(Value::as_array).unwrap() {
on_list.push(u8::try_from(
on.as_u64().ok_or_else(|| de::Error::missing_field("on"))?
).map_err(|_| de::Error::missing_field("on"))?);
}
patches.push(NormalPatchField {
offset: patch.get("offset")
.and_then(Value::as_u64)
.ok_or_else(|| de::Error::missing_field("offset"))?,
off: off_list,
on: on_list
})
}
PatchData::Normal(NormalPatch {
patches
})
},
Some(&_) => return Err(de::Error::custom("unsupported type"))
});
Ok(Patch {
id: value.get("id")
.and_then(Value::as_str)
.ok_or_else(|| de::Error::missing_field("id"))?
.to_owned(),
name: value.get("name")
.and_then(Value::as_str)
.ok_or_else(|| de::Error::missing_field("name"))?
.to_owned(),
tooltip: value.get("tooltip")
.and_then(Value::as_str)
.and_then(|s| Some(s.to_owned())),
data: data?
})
}
}

View File

@ -1,7 +1,7 @@
use std::path::{PathBuf, Path}; use std::path::{PathBuf, Path};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ini::Ini; use ini::Ini;
use crate::{model::{misc::Game, profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}}; use crate::{model::{misc::Game, profile::{Aime, Segatools}}, profiles::ProfilePaths, util::{self, PathStr}};
use crate::pkg_store::PackageStore; use crate::pkg_store::PackageStore;
impl Segatools { impl Segatools {
@ -66,8 +66,12 @@ impl Segatools {
let ini_path = p.config_dir().join("segatools-base.ini"); let ini_path = p.config_dir().join("segatools-base.ini");
if !ini_path.exists() { if !ini_path.exists() {
tokio::fs::write(&ini_path, segatools_base(game)).await match game {
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?; Game::Ongeki => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-ongeki.ini"))
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?,
Game::Chunithm => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-chunithm.ini"))
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?
}
} }
if !pfx_dir.exists() { if !pfx_dir.exists() {
tokio::fs::create_dir(&pfx_dir).await tokio::fs::create_dir(&pfx_dir).await

45
rust/src/patcher.rs Normal file
View File

@ -0,0 +1,45 @@
use std::path::Path;
use anyhow::Result;
use sha256::try_digest;
use crate::model::patch::{Patch, PatchFile, PatchFileVec};
impl PatchFileVec {
pub fn new(config_path: impl AsRef<Path>) -> Result<PatchFileVec> {
let path = config_path.as_ref().join("patches");
if !path.exists() {
std::fs::create_dir(&path)?;
}
std::fs::write(path.join("builtin-chunithm.json5"), include_bytes!("../static/standard-chunithm.json5"))?;
let mut res = Vec::new();
for f in std::fs::read_dir(path)? {
let f = f?;
let f = f.path();
res.push(
serde_json5::from_str::<PatchFile>(&std::fs::read_to_string(f)?)?
);
}
Ok(PatchFileVec(res))
}
pub fn find_patches(&self, target: impl AsRef<Path>) -> Result<Vec<Patch>> {
let checksum = try_digest(target.as_ref())?;
let mut res = Vec::new();
for pfile in &self.0 {
for plist in &pfile.0 {
log::debug!("checking {}", plist.sha256);
if plist.sha256 == checksum {
let mut cloned = plist.clone().patches;
res.append(&mut cloned);
}
}
}
if res.len() == 0 {
log::warn!("no matching patchset for {:?} ({})", target.as_ref(), checksum);
}
Ok(res)
}
}

View File

@ -39,7 +39,7 @@ pub enum Status {
} }
#[bitflags] #[bitflags]
#[repr(u8)] #[repr(u16)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Feature { pub enum Feature {
Mod, Mod,
@ -48,6 +48,10 @@ pub enum Feature {
Mu3Hook, Mu3Hook,
Mu3IO, Mu3IO,
ChusanHook, ChusanHook,
ChuniIO,
Mempatcher,
GameDLL,
AmdDLL
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
@ -224,6 +228,12 @@ impl Package {
flags |= Feature::Aime; flags |= Feature::Aime;
} else if module == "mu3io" { } else if module == "mu3io" {
flags |= Feature::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(flags); return Status::OK(flags);

View File

@ -1,11 +1,11 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::AppHandle; use tauri::AppHandle;
use std::{collections::BTreeSet, path::{Path, PathBuf}}; use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
use crate::{model::{misc::Game, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util}; 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 tauri::Emitter; use tauri::Emitter;
use std::process::Stdio; use std::process::Stdio;
use crate::model::profile::BepInEx; use crate::model::profile::BepInEx;
use crate::model::{profile::{Display, DisplayMode, Network, Segatools}, segatools_base::segatools_base}; use crate::model::profile::{Display, DisplayMode, Network, Segatools};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::fs::File; use std::fs::File;
use tokio::process::Command; use tokio::process::Command;
@ -57,7 +57,10 @@ pub struct ProfileData {
pub mu3_ini: Option<Mu3Ini>, pub mu3_ini: Option<Mu3Ini>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard> pub keyboard: Option<Keyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patches: Option<PatchSelection>,
} }
impl Profile { impl Profile {
@ -83,13 +86,18 @@ impl Profile {
} else { } else {
Some(Keyboard::Chunithm(ChunithmKeyboard::default())) Some(Keyboard::Chunithm(ChunithmKeyboard::default()))
}, },
patches: if meta.game == Game::Chunithm { Some(PatchSelection(BTreeMap::new())) } else { None }
}, },
meta: meta.clone() meta: meta.clone()
}; };
p.save()?; p.save()?;
std::fs::create_dir_all(p.config_dir())?; std::fs::create_dir_all(p.config_dir())?;
std::fs::create_dir_all(p.data_dir())?; std::fs::create_dir_all(p.data_dir())?;
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base(meta.game))?;
match meta.game {
Game::Ongeki => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-ongeki.ini"))?,
Game::Chunithm => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-chunithm.ini"))?,
};
Ok(p) Ok(p)
} }
@ -101,11 +109,17 @@ impl Profile {
log::debug!("{:?}", data); log::debug!("{:?}", data);
// Backwards compat
if game == Game::Ongeki && data.keyboard.is_none() { if game == Game::Ongeki && data.keyboard.is_none() {
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default())); data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default()));
} }
if game == Game::Chunithm && data.keyboard.is_none() { if game == Game::Chunithm {
data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default())); if data.keyboard.is_none() {
data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default()));
}
if data.patches.is_none() {
data.patches = Some(PatchSelection(BTreeMap::new()));
}
} }
Ok(Profile { Ok(Profile {
@ -183,6 +197,10 @@ impl Profile {
if self.meta.game.has_module(ProfileModule::Keyboard) && source.keyboard.is_some() { if self.meta.game.has_module(ProfileModule::Keyboard) && source.keyboard.is_some() {
self.data.keyboard = source.keyboard; self.data.keyboard = source.keyboard;
} }
if self.data.patches.is_some() && source.patches.is_some() {
self.data.patches = source.patches;
}
} }
pub async fn line_up(&self, pkg_hash: String, refresh: bool, _app: AppHandle) -> Result<()> { pub async fn line_up(&self, pkg_hash: String, refresh: bool, _app: AppHandle) -> Result<()> {
let info = match &self.data.display { let info = match &self.data.display {

View File

@ -1,45 +1,3 @@
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.
enable=1
[system]
; Enable ALLS system settings.
enable=1
; Enable freeplay mode. This will disable the coin slot and set the game to
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
; allow you to start a game in freeplay mode.
freeplay=0
; LAN Install: Set this to 1 on all machines.
dipsw1=1
[gfx]
; Enables the graphics hook.
enable=1
[led15093]
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
; RGBs and the rear LED panel (billboard) on the cabinet.
enable=1
[led]
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\ongeki_led\"
cabLedOutputPipe=1
; Output billboard LED strip data to serial
cabLedOutputSerial=0
; Output slider LED data to the named pipe
controllerLedOutputPipe=1
; Output slider LED data to the serial port
controllerLedOutputSerial=0".to_owned(),
Game::Chunithm => "
[vfd] [vfd]
; Enable VFD emulation. Disable to use a real VFD ; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly. ; GP1232A02A FUTABA assembly.
@ -87,7 +45,7 @@ monitor=0
enable=1 enable=1
[led] [led]
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\chuni_led\" ; Output billboard LED strip data to a named pipe called "\\.\pipe\chuni_led"
cabLedOutputPipe=1 cabLedOutputPipe=1
; Output billboard LED strip data to serial ; Output billboard LED strip data to serial
cabLedOutputSerial=0 cabLedOutputSerial=0
@ -105,7 +63,7 @@ controllerLedOutputOpeNITHM=0
;serialBaud=921600 ;serialBaud=921600
; Data output a sequence of bytes, with JVS-like framing. ; Data output a sequence of bytes, with JVS-like framing.
; Each \"packet\" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere, ; 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 ; 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. ; it and use the next sent byte plus one instead.
; ;
@ -136,7 +94,4 @@ controllerLedOutputOpeNITHM=0
; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs. ; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs.
; x86 chuniio to path32, x64 to path64. Both are necessary. ; x86 chuniio to path32, x64 to path64. Both are necessary.
;path32= ;path32=
;path64= ;path64=
".to_owned()
}
}

View File

@ -0,0 +1,36 @@
[vfd]
; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly.
enable=1
[system]
; Enable ALLS system settings.
enable=1
; Enable freeplay mode. This will disable the coin slot and set the game to
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
; allow you to start a game in freeplay mode.
freeplay=0
; LAN Install: Set this to 1 on all machines.
dipsw1=1
[gfx]
; Enables the graphics hook.
enable=1
[led15093]
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
; RGBs and the rear LED panel (billboard) on the cabinet.
enable=1
[led]
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\ongeki_led\"
cabLedOutputPipe=1
; Output billboard LED strip data to serial
cabLedOutputSerial=0
; Output slider LED data to the named pipe
controllerLedOutputPipe=1
; Output slider LED data to the serial port
controllerLedOutputSerial=0

View File

@ -0,0 +1,219 @@
[
{
filename: 'chusanApp.exe',
version: '2.30.00',
sha256: 'd624da8a397c2885b3937e7b8bd0de6fc4e8da4beaf5c229569b29bb2847d694',
patches: [
{
id: 'standard-shared-audio',
name: 'Force shared audio mode, system audio sample rate must be 48000Hz',
tooltip: 'Improves compatibility, but may increase latency',
patches: [
{
offset: 16181386,
off: [1],
on: [0],
},
],
},
{
id: 'standard-2ch',
name: 'Force 2 channel audio output',
tooltip: 'May cause bass overload',
patches: [
{
offset: 16181601,
off: [117, 63],
on: [144, 144],
},
],
},
{
id: 'standard-song-timer',
name: 'Disable song select timer',
patches: [
{
offset: 10766682,
off: [116],
on: [235],
},
],
},
{
id: 'standard-map-timer',
name: 'Map selection timer',
tooltip: 'If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)',
type: 'number',
default: 30,
offset: 10111639,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-ticket-timer',
name: 'Ticket selection timer',
tooltip: 'If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)',
type: 'number',
default: 60,
offset: 10060322,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-course-timer',
name: 'Course selection timer',
tooltip: 'If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)',
type: 'number',
default: 30,
offset: 10812315,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-unlimited-tracks',
name: 'Unlimited maximum tracks',
tooltip: 'Must check to play more than 7 tracks per credit',
patches: [
{
offset: 7635328,
off: [240],
on: [192],
},
],
},
{
id: 'standard-maximum-tracks',
name: 'Maximum tracks',
type: 'number',
default: 3,
offset: 3768513,
size: 4,
min: 3,
max: 12,
},
{
id: 'standard-no-encryption',
name: 'No encryption',
tooltip: 'Will also disable TLS',
patches: [
{
offset: 31812584,
off: [230],
on: [0],
},
{
offset: 31812588,
off: [230],
on: [0],
},
],
},
{
id: 'standard-no-tls',
name: 'No TLS',
tooltip: 'Title server workaround',
patches: [
{
offset: 16062679,
off: [128],
on: [0],
},
],
},
{
id: 'standard-head-to-head',
name: 'Patch for head-to-head play',
tooltip: 'Fix infinite sync while trying to connect to head to head play',
patches: [
{
offset: 6795139,
off: [1],
on: [0],
},
],
},
{
id: 'standard-bypass-1080p',
name: 'Bypass 1080p monitor check',
patches: [
{
offset: 117951,
off: [
129, 188, 36, 184, 2, 0, 0, 128, 7, 0, 0, 117, 31,
129, 188, 36, 188, 2, 0, 0, 56, 4, 0, 0, 117, 18,
],
on: [
144, 144, 144, 144, 144, 144, 144, 144, 144, 144,
144, 144, 144, 144, 144, 144, 144, 144, 144, 144,
144, 144, 144, 144, 144, 144,
],
},
],
},
{
id: 'standard-bypass-120hz',
name: 'Bypass 120Hz monitor check',
patches: [
{
offset: 117937,
off: [133, 192, 116, 63],
on: [235, 48, 235, 46],
},
],
},
{
id: 'standard-force-free-play-text',
name: 'Force FREE PLAY credit text',
tooltip: 'Replaces the credit count with FREE PLAY',
patches: [
{
offset: 3700132,
off: [60, 1],
on: [56, 192],
},
],
},
],
},
{
filename: 'amdaemon.exe',
version: '2.30.00',
sha256: 'd4809220578374865370e31c541ed6e406b854d8c26cfe7464c2c15145113bfd',
patches: [
{
id: 'standard-localhost',
name: 'Allow 127.0.0.1/localhost as the network server',
patches: [
{
offset: 0x6e1ca4,
off: [0x31, 0x32, 0x37, 0x2f],
on: [0x30, 0x2f, 0x38, 0x00],
},
{
offset: 0x3c88c4,
off: [0xff, 0x15, 0xc6, 0x2f, 0x1b, 0x00, 0x8b],
on: [0x33, 0xc0, 0x48, 0x83, 0xc4, 0x28, 0xc3],
},
],
},
{
id: 'standard-credit-freeze',
name: 'Credit freeze',
tooltip: 'Prevents credits from being used. At least one credit must be available to start the game or purchase premium tickets.',
patches: [{ offset: 0x2bafc8, off: [0x28], on: [0x08] }],
},
{
id: 'standard-openssl-fix',
name: 'OpenSSL SHA crash bug fix',
tooltip: 'Fix crashes on 10th generation and newer Intel CPUs',
patches: [
{ offset: 0x4d4a43, off: [0x48], on: [0x4c] },
{ offset: 0x4d4a4b, off: [0x48], on: [0x49] },
],
},
],
},
]

View File

@ -15,6 +15,7 @@ import { listen } from '@tauri-apps/api/event';
import ModList from './ModList.vue'; import ModList from './ModList.vue';
import ModStore from './ModStore.vue'; import ModStore from './ModStore.vue';
import OptionList from './OptionList.vue'; import OptionList from './OptionList.vue';
import PatchList from './PatchList.vue';
import ProfileList from './ProfileList.vue'; import ProfileList from './ProfileList.vue';
import StartButton from './StartButton.vue'; import StartButton from './StartButton.vue';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
@ -55,14 +56,26 @@ onMounted(async () => {
} }
fetch_promise.then(async () => { fetch_promise.then(async () => {
await invoke('install_package', { const promises = [];
key: 'segatools-mu3hook', promises.push(
force: false, invoke('install_package', {
}); key: 'segatools-mu3hook',
await invoke('install_package', { force: false,
key: 'segatools-chusanhook', })
force: false, );
}); promises.push(
invoke('install_package', {
key: 'segatools-chusanhook',
force: false,
})
);
promises.push(
invoke('install_package', {
key: 'mempatcher-mempatcher',
force: false,
})
);
await Promise.all(promises);
}); });
}); });
@ -163,7 +176,10 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
<div class="grow"></div> <div class="grow"></div>
<div class="flex gap-4"> <div class="flex gap-4">
<div class="flex" v-if="currentTab !== 3"> <div
class="flex"
v-if="[0, 1, 2].includes(currentTab as number)"
>
<InputIcon class="self-center mr-2"> <InputIcon class="self-center mr-2">
<i class="pi pi-search" /> <i class="pi pi-search" />
</InputIcon> </InputIcon>
@ -240,13 +256,7 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
</footer> </footer>
</TabPanel> </TabPanel>
<TabPanel :value="4"> <TabPanel :value="4">
CHUNITHM patches are not implemented yet.<br />Use <PatchList />
<a
href="https://patcher.two-torial.xyz/"
target="_blank"
style="text-decoration: underline"
>patcher.two-torial.xyz</a
>
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
<div v-if="currentTab === 5 || currentTab === 3"> <div v-if="currentTab === 5 || currentTab === 3">

View File

@ -62,7 +62,10 @@ const iconSrc = computed(() => {
> >
</span> </span>
<span <span
v-if="hasFeature(pkg, Feature.Mu3IO)" v-if="
hasFeature(pkg, Feature.Mu3IO) ||
hasFeature(pkg, Feature.ChuniIO)
"
v-tooltip="'IO'" v-tooltip="'IO'"
class="pi pi-wrench ml-1 text-green-400" class="pi pi-wrench ml-1 text-green-400"
> >
@ -73,6 +76,16 @@ const iconSrc = computed(() => {
class="pi pi-credit-card ml-1 text-purple-400" class="pi pi-credit-card ml-1 text-purple-400"
> >
</span> </span>
<span
v-if="
hasFeature(pkg, Feature.Mempatcher) ||
hasFeature(pkg, Feature.GameDLL) ||
hasFeature(pkg, Feature.AmdDLL)
"
v-tooltip="'DLL'"
class="pi pi-cog ml-1 text-red-400"
>
</span>
<span <span
v-if="showNamespace && pkg?.namespace" v-if="showNamespace && pkg?.namespace"
class="text-sm opacity-75" class="text-sm opacity-75"

View File

@ -7,6 +7,7 @@ const general = useGeneralStore();
defineProps({ defineProps({
title: String, title: String,
collapsed: Boolean, collapsed: Boolean,
alwaysFound: Boolean,
}); });
</script> </script>
@ -14,7 +15,7 @@ defineProps({
<Fieldset <Fieldset
:legend="title" :legend="title"
:toggleable="true" :toggleable="true"
v-show="general.cfgCategories.has(title ?? '')" v-show="general.cfgCategories.has(title ?? '') || alwaysFound"
:collapsed="collapsed" :collapsed="collapsed"
> >
<div class="flex w-full flex-col gap-1"> <div class="flex w-full flex-col gap-1">

View File

@ -9,6 +9,7 @@ const props = defineProps({
title: String, title: String,
tooltip: String, tooltip: String,
dangerousTooltip: String, dangerousTooltip: String,
greytext: String,
}); });
const searched = computed(() => { const searched = computed(() => {
@ -38,6 +39,12 @@ const searched = computed(() => {
class="pi pi-exclamation-circle ml-2 text-red-500" class="pi pi-exclamation-circle ml-2 text-red-500"
v-tooltip="dangerousTooltip" v-tooltip="dangerousTooltip"
></span> ></span>
<span
v-if="greytext"
style="font-size: 0.65rem"
class="ml-2 text-gray-400"
>{{ greytext }}</span
>
</div> </div>
<slot /> <slot />

View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import InputNumber from 'primevue/inputnumber';
import ToggleSwitch from 'primevue/toggleswitch';
import OptionRow from './OptionRow.vue';
import { usePrfStore } from '../stores';
const prf = usePrfStore();
const toggleUnary = (key: string, val: boolean) => {
if (val) {
prf.current!.data.patches[key] = 'Enabled';
} else {
delete prf.current!.data.patches[key];
}
};
defineProps({
id: String,
name: String,
tooltip: String,
type: String,
});
</script>
<template>
<OptionRow :title="name" :tooltip="tooltip" :greytext="id">
<ToggleSwitch
v-if="type === undefined"
:model-value="prf.current!.data.patches[id!] !== undefined"
@update:model-value="(v: boolean) => toggleUnary(id!, v)"
/>
<InputNumber v-else class="number-input" />
</OptionRow>
</template>

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import { Ref, ref } from 'vue';
import * as path from '@tauri-apps/api/path';
import OptionCategory from './OptionCategory.vue';
import OptionRow from './OptionRow.vue';
import PatchEntry from './PatchEntry.vue';
import { invoke } from '../invoke';
import { usePrfStore } from '../stores';
import { Patch } from '../types';
const prf = usePrfStore();
prf.reload();
const gamePatches: Ref<Patch[]> = ref([]);
const amdPatches: Ref<Patch[]> = ref([]);
invoke('list_patches', { target: prf.current!.data.sgt.target }).then(
(patches) => {
gamePatches.value = patches as Patch[];
}
);
(async () => {
const amd = await path.join(
prf.current!.data.sgt.target,
'../amdaemon.exe'
);
amdPatches.value = (await invoke('list_patches', {
target: amd,
})) as Patch[];
})();
</script>
<template>
<OptionCategory title="chusanApp.exe" always-found>
<PatchEntry
v-for="p in gamePatches"
:id="p.id"
:title="p.name"
:tooltip="p.tooltip"
:type="p.type"
/>
</OptionCategory>
<OptionCategory title="amdaemon.exe" always-found>
<PatchEntry
v-for="p in amdPatches"
:id="p.id"
:title="p.name"
:tooltip="p.tooltip"
:type="p.type"
/>
</OptionCategory>
</template>

View File

@ -30,6 +30,10 @@ export enum Feature {
Mu3Hook = 1 << 3, Mu3Hook = 1 << 3,
Mu3IO = 1 << 4, Mu3IO = 1 << 4,
ChusanHook = 1 << 5, ChusanHook = 1 << 5,
ChuniIO = 1 << 6,
Mempatcher = 1 << 7,
GameDLL = 1 << 8,
AmdDLL = 1 << 9,
} }
export type Status = export type Status =
@ -54,6 +58,9 @@ export interface ProfileData {
bepinex: BepInExConfig; bepinex: BepInExConfig;
mu3_ini: Mu3IniConfig | undefined; mu3_ini: Mu3IniConfig | undefined;
keyboard: KeyboardConfig | undefined; keyboard: KeyboardConfig | undefined;
patches: {
[key: string]: 'Enabled' | { Number: number } | { Hex: Int8Array };
};
} }
export interface SegatoolsConfig { export interface SegatoolsConfig {
@ -149,3 +156,10 @@ export interface Dirs {
data_dir: string; data_dir: string;
cache_dir: string; cache_dir: string;
} }
export interface Patch {
id: string;
name: string;
tooltip: string;
type: undefined | 'number';
}