forked from akanyan/STARTLINER
feat: partial support for patches
This commit is contained in:
1045
rust/Cargo.lock
generated
1045
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -42,6 +42,7 @@ junction = "1.2.0"
|
||||
tauri-plugin-fs = "2"
|
||||
yaml-rust2 = "0.10.0"
|
||||
enumflags2 = { version = "0.7.11", features = ["serde"] }
|
||||
sha256 = "1.6.0"
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-cli = "2"
|
||||
|
@ -1,5 +1,6 @@
|
||||
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::{model::misc::Game, pkg::PkgKey};
|
||||
@ -17,6 +18,7 @@ pub struct AppData {
|
||||
pub pkgs: PackageStore,
|
||||
pub cfg: GlobalConfig,
|
||||
pub state: GlobalState,
|
||||
pub patch_set: PatchFileVec,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
@ -37,13 +39,18 @@ impl AppData {
|
||||
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);
|
||||
|
||||
AppData {
|
||||
profile: profile,
|
||||
pkgs: PackageStore::new(apph.clone()),
|
||||
cfg,
|
||||
state: GlobalState { remain_open: true }
|
||||
state: GlobalState { remain_open: true },
|
||||
patch_set
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ use tokio::fs;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use crate::model::config::GlobalConfigField;
|
||||
use crate::model::misc::Game;
|
||||
use crate::model::patch::Patch;
|
||||
use crate::pkg::{Package, PkgKey};
|
||||
use crate::pkg_store::{InstallResult, PackageStore};
|
||||
use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths};
|
||||
@ -443,3 +444,14 @@ pub async fn list_directories() -> Result<util::Dirs, ()> {
|
||||
pub async fn file_exists(path: String) -> Result<bool, ()> {
|
||||
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)
|
||||
}
|
@ -7,6 +7,7 @@ mod download_handler;
|
||||
mod appdata;
|
||||
mod modules;
|
||||
mod profiles;
|
||||
mod patcher;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use anyhow::anyhow;
|
||||
@ -208,6 +209,8 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::list_platform_capabilities,
|
||||
cmd::list_directories,
|
||||
cmd::file_exists,
|
||||
|
||||
cmd::list_patches
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application");
|
||||
|
@ -3,4 +3,4 @@ pub mod misc;
|
||||
pub mod rainy;
|
||||
pub mod profile;
|
||||
pub mod config;
|
||||
pub mod segatools_base;
|
||||
pub mod patch;
|
155
rust/src/model/patch.rs
Normal file
155
rust/src/model/patch.rs
Normal 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?
|
||||
})
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
use std::path::{PathBuf, Path};
|
||||
use anyhow::{anyhow, Result};
|
||||
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;
|
||||
|
||||
impl Segatools {
|
||||
@ -66,8 +66,12 @@ impl Segatools {
|
||||
let ini_path = p.config_dir().join("segatools-base.ini");
|
||||
|
||||
if !ini_path.exists() {
|
||||
tokio::fs::write(&ini_path, segatools_base(game)).await
|
||||
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
|
||||
match game {
|
||||
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() {
|
||||
tokio::fs::create_dir(&pfx_dir).await
|
||||
|
45
rust/src/patcher.rs
Normal file
45
rust/src/patcher.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ pub enum Status {
|
||||
}
|
||||
|
||||
#[bitflags]
|
||||
#[repr(u8)]
|
||||
#[repr(u16)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Feature {
|
||||
Mod,
|
||||
@ -48,6 +48,10 @@ pub enum Feature {
|
||||
Mu3Hook,
|
||||
Mu3IO,
|
||||
ChusanHook,
|
||||
ChuniIO,
|
||||
Mempatcher,
|
||||
GameDLL,
|
||||
AmdDLL
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@ -224,6 +228,12 @@ impl Package {
|
||||
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(flags);
|
||||
|
@ -1,11 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use std::{collections::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 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 tauri::Emitter;
|
||||
use std::process::Stdio;
|
||||
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 std::fs::File;
|
||||
use tokio::process::Command;
|
||||
@ -57,7 +57,10 @@ pub struct ProfileData {
|
||||
pub mu3_ini: Option<Mu3Ini>,
|
||||
|
||||
#[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 {
|
||||
@ -83,13 +86,18 @@ impl Profile {
|
||||
} else {
|
||||
Some(Keyboard::Chunithm(ChunithmKeyboard::default()))
|
||||
},
|
||||
patches: if meta.game == Game::Chunithm { Some(PatchSelection(BTreeMap::new())) } else { None }
|
||||
},
|
||||
meta: meta.clone()
|
||||
};
|
||||
p.save()?;
|
||||
std::fs::create_dir_all(p.config_dir())?;
|
||||
std::fs::create_dir_all(p.data_dir())?;
|
||||
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base(meta.game))?;
|
||||
|
||||
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)
|
||||
}
|
||||
@ -101,12 +109,18 @@ impl Profile {
|
||||
|
||||
log::debug!("{:?}", data);
|
||||
|
||||
// Backwards compat
|
||||
if game == Game::Ongeki && data.keyboard.is_none() {
|
||||
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default()));
|
||||
}
|
||||
if game == Game::Chunithm && data.keyboard.is_none() {
|
||||
if game == Game::Chunithm {
|
||||
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 {
|
||||
meta: ProfileMeta {
|
||||
@ -183,6 +197,10 @@ impl Profile {
|
||||
if self.meta.game.has_module(ProfileModule::Keyboard) && source.keyboard.is_some() {
|
||||
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<()> {
|
||||
let info = match &self.data.display {
|
||||
|
@ -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]
|
||||
; Enable VFD emulation. Disable to use a real VFD
|
||||
; GP1232A02A FUTABA assembly.
|
||||
@ -87,7 +45,7 @@ monitor=0
|
||||
enable=1
|
||||
|
||||
[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
|
||||
; Output billboard LED strip data to serial
|
||||
cabLedOutputSerial=0
|
||||
@ -105,7 +63,7 @@ controllerLedOutputOpeNITHM=0
|
||||
;serialBaud=921600
|
||||
|
||||
; Data output a sequence of bytes, with JVS-like framing.
|
||||
; Each \"packet\" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere,
|
||||
; Each "packet" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere,
|
||||
; 0xD0 is used as an escape character -- if you receive D0 in the output, ignore
|
||||
; it and use the next sent byte plus one instead.
|
||||
;
|
||||
@ -137,6 +95,3 @@ controllerLedOutputOpeNITHM=0
|
||||
; x86 chuniio to path32, x64 to path64. Both are necessary.
|
||||
;path32=
|
||||
;path64=
|
||||
".to_owned()
|
||||
}
|
||||
}
|
36
rust/static/segatools-ongeki.ini
Normal file
36
rust/static/segatools-ongeki.ini
Normal 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
|
219
rust/static/standard-chunithm.json5
Normal file
219
rust/static/standard-chunithm.json5
Normal 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] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
@ -15,6 +15,7 @@ import { listen } from '@tauri-apps/api/event';
|
||||
import ModList from './ModList.vue';
|
||||
import ModStore from './ModStore.vue';
|
||||
import OptionList from './OptionList.vue';
|
||||
import PatchList from './PatchList.vue';
|
||||
import ProfileList from './ProfileList.vue';
|
||||
import StartButton from './StartButton.vue';
|
||||
import { invoke } from '../invoke';
|
||||
@ -55,14 +56,26 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
fetch_promise.then(async () => {
|
||||
await invoke('install_package', {
|
||||
const promises = [];
|
||||
promises.push(
|
||||
invoke('install_package', {
|
||||
key: 'segatools-mu3hook',
|
||||
force: false,
|
||||
});
|
||||
await invoke('install_package', {
|
||||
})
|
||||
);
|
||||
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="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">
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
@ -240,13 +256,7 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
||||
</footer>
|
||||
</TabPanel>
|
||||
<TabPanel :value="4">
|
||||
CHUNITHM patches are not implemented yet.<br />Use
|
||||
<a
|
||||
href="https://patcher.two-torial.xyz/"
|
||||
target="_blank"
|
||||
style="text-decoration: underline"
|
||||
>patcher.two-torial.xyz</a
|
||||
>
|
||||
<PatchList />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
<div v-if="currentTab === 5 || currentTab === 3">
|
||||
|
@ -62,7 +62,10 @@ const iconSrc = computed(() => {
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
v-if="hasFeature(pkg, Feature.Mu3IO)"
|
||||
v-if="
|
||||
hasFeature(pkg, Feature.Mu3IO) ||
|
||||
hasFeature(pkg, Feature.ChuniIO)
|
||||
"
|
||||
v-tooltip="'IO'"
|
||||
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"
|
||||
>
|
||||
</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
|
||||
v-if="showNamespace && pkg?.namespace"
|
||||
class="text-sm opacity-75"
|
||||
|
@ -7,6 +7,7 @@ const general = useGeneralStore();
|
||||
defineProps({
|
||||
title: String,
|
||||
collapsed: Boolean,
|
||||
alwaysFound: Boolean,
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -14,7 +15,7 @@ defineProps({
|
||||
<Fieldset
|
||||
:legend="title"
|
||||
:toggleable="true"
|
||||
v-show="general.cfgCategories.has(title ?? '')"
|
||||
v-show="general.cfgCategories.has(title ?? '') || alwaysFound"
|
||||
:collapsed="collapsed"
|
||||
>
|
||||
<div class="flex w-full flex-col gap-1">
|
||||
|
@ -9,6 +9,7 @@ const props = defineProps({
|
||||
title: String,
|
||||
tooltip: String,
|
||||
dangerousTooltip: String,
|
||||
greytext: String,
|
||||
});
|
||||
|
||||
const searched = computed(() => {
|
||||
@ -38,6 +39,12 @@ const searched = computed(() => {
|
||||
class="pi pi-exclamation-circle ml-2 text-red-500"
|
||||
v-tooltip="dangerousTooltip"
|
||||
></span>
|
||||
<span
|
||||
v-if="greytext"
|
||||
style="font-size: 0.65rem"
|
||||
class="ml-2 text-gray-400"
|
||||
>{{ greytext }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
|
34
src/components/PatchEntry.vue
Normal file
34
src/components/PatchEntry.vue
Normal 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>
|
54
src/components/PatchList.vue
Normal file
54
src/components/PatchList.vue
Normal 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>
|
14
src/types.ts
14
src/types.ts
@ -30,6 +30,10 @@ export enum Feature {
|
||||
Mu3Hook = 1 << 3,
|
||||
Mu3IO = 1 << 4,
|
||||
ChusanHook = 1 << 5,
|
||||
ChuniIO = 1 << 6,
|
||||
Mempatcher = 1 << 7,
|
||||
GameDLL = 1 << 8,
|
||||
AmdDLL = 1 << 9,
|
||||
}
|
||||
|
||||
export type Status =
|
||||
@ -54,6 +58,9 @@ export interface ProfileData {
|
||||
bepinex: BepInExConfig;
|
||||
mu3_ini: Mu3IniConfig | undefined;
|
||||
keyboard: KeyboardConfig | undefined;
|
||||
patches: {
|
||||
[key: string]: 'Enabled' | { Number: number } | { Hex: Int8Array };
|
||||
};
|
||||
}
|
||||
|
||||
export interface SegatoolsConfig {
|
||||
@ -149,3 +156,10 @@ export interface Dirs {
|
||||
data_dir: string;
|
||||
cache_dir: string;
|
||||
}
|
||||
|
||||
export interface Patch {
|
||||
id: string;
|
||||
name: string;
|
||||
tooltip: string;
|
||||
type: undefined | 'number';
|
||||
}
|
||||
|
Reference in New Issue
Block a user