diff --git a/CHANGELOG.md b/CHANGELOG.md index 12864db..7002f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.12.0 + +- Ongeki: cache and mu3.ini config are now split per-profile (requires mu3-mods 3.7+) +- Ongeki: added the few config options of mu3.ini that aren't available in TestMenuConfig, or require a restart +- Chunithm: added Lumi+ patches +- Added a button linking to the profile config folder +- Fixed the button linking to the data folder showing up when the folder does not exist + ## 0.11.1 - Improved help pages diff --git a/README.md b/README.md index 3d5d157..a07bf35 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ STARTLINER is four things: - a glorified `start.bat` clicker, with automatic monitor setup and rollback, - [an abstraction allowing data configuration without touching the game directory](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details). -STARTLINER's core design principle is to modify, configure and launch games without tampering with them. +STARTLINER's core design principle is to modify, configure and launch games without tampering with them. This makes it possible to keep data cleaner than ever, and to have several configurations pointing at the same data. Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome. diff --git a/rust/src/model/local.rs b/rust/src/model/local.rs index 7ed8fcb..dd82c6b 100644 --- a/rust/src/model/local.rs +++ b/rust/src/model/local.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; use serde::{Deserialize, Serialize}; -use crate::pkg::{Status, PkgKey, PkgKeyVersion}; +use crate::pkg::{PkgKey, PkgKeyVersion}; use super::misc::Game; @@ -22,6 +22,5 @@ pub type PackageList = BTreeMap; #[derive(Serialize, Deserialize, Clone)] pub struct PackageListEntry { pub version: String, - pub status: Status, pub games: Vec, } \ No newline at end of file diff --git a/rust/src/model/profile.rs b/rust/src/model/profile.rs index 2e41250..4ce819a 100644 --- a/rust/src/model/profile.rs +++ b/rust/src/model/profile.rs @@ -166,11 +166,26 @@ pub enum Mu3Audio { Excl2Ch, } -#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[derive(Deserialize, Serialize, Clone, Debug)] #[serde(default)] pub struct Mu3Ini { pub audio: Option, + pub sample_rate: i32, pub blacklist: Option<(i32, i32)>, + pub gp: i32, + pub enable_bonus_tracks: bool, +} + +impl Default for Mu3Ini { + fn default() -> Self { + Self { + audio: Some(Mu3Audio::Shared), + sample_rate: 48_000, + blacklist: Some((10000, 19999)), + gp: 999, + enable_bonus_tracks: true + } + } } #[derive(Deserialize, Serialize, Clone, Debug)] diff --git a/rust/src/modules/mu3ini.rs b/rust/src/modules/mu3ini.rs index 70ce1c2..69fb65b 100644 --- a/rust/src/modules/mu3ini.rs +++ b/rust/src/modules/mu3ini.rs @@ -1,11 +1,11 @@ use std::path::Path; -use anyhow::Result; +use anyhow::{anyhow, Result}; use ini::Ini; use crate::model::profile::{Mu3Audio, Mu3Ini}; impl Mu3Ini { - pub fn line_up(&self, game_path: impl AsRef) -> Result<()> { - let file = game_path.as_ref().join("mu3.ini"); + pub fn line_up(&self, data_dir: impl AsRef, cfg_dir: impl AsRef) -> Result<()> { + let file = cfg_dir.as_ref().join("mu3.ini"); if !file.exists() { std::fs::write(&file, "")?; @@ -20,9 +20,26 @@ impl Mu3Ini { Mu3Audio::Excl2Ch => "2", }; - ini.with_section(Some("Sound")).set("WasapiExclusive", value); + ini.with_section(Some("Sound")) + .set("WasapiExclusive", value) + .set("SampleRate", self.sample_rate.to_string()); } + if let Some(blacklist) = self.blacklist { + ini.with_section(Some("Extra")) + .set("BlacklistMin", blacklist.0.to_string()) + .set("BlacklistMax", blacklist.1.to_string()); + } + + let cache_path = data_dir.as_ref().join("mu3-mods-cache"); + let cache_path = cache_path.to_str() + .ok_or_else(|| anyhow!("Invalid cache path"))?; + + ini.with_section(Some("Extra")) + .set("GP", self.gp.to_string()) + .set("CacheDir", cache_path) + .set("UnlockBonusTracks", crate::util::bool_to_01(self.enable_bonus_tracks)); + ini.write_to_file(file)?; Ok(()) diff --git a/rust/src/patcher.rs b/rust/src/patcher.rs index 28c6d99..7ade0b5 100644 --- a/rust/src/patcher.rs +++ b/rust/src/patcher.rs @@ -15,10 +15,14 @@ impl PatchFileVec { 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::(&std::fs::read_to_string(f)?)? - ); + let f = &f.path(); + match serde_json5::from_str::(&std::fs::read_to_string(f)?) { + Ok(parsed) => res.push(parsed), + Err(e) => { + log::error!("Error parsing {f:?}: {e}"); + anyhow::bail!("Error parsing {f:?}: {e}"); + } + } } Ok(PatchFileVec(res)) } @@ -29,8 +33,8 @@ impl PatchFileVec { let mut res = Vec::new(); for pfile in &self.0 { for plist in &pfile.0 { - log::debug!("checking {}", plist.sha256); - if plist.sha256 == checksum { + log::debug!("checking {}", plist.sha256.to_ascii_lowercase()); + if plist.sha256.to_ascii_lowercase() == checksum { let mut cloned = plist.clone().patches; res.append(&mut cloned); } diff --git a/rust/src/pkg_store.rs b/rust/src/pkg_store.rs index e7f7fee..0fb7e2e 100644 --- a/rust/src/pkg_store.rs +++ b/rust/src/pkg_store.rs @@ -152,7 +152,6 @@ impl PackageStore { PackageListEntry { // from_rainy() is guaranteed to include rmt version: r.rmt.as_ref().unwrap().version.clone(), - status: Status::Unchecked, games: vec![ game ], } }); diff --git a/rust/src/profiles/mod.rs b/rust/src/profiles/mod.rs index cf3553f..dde67f4 100644 --- a/rust/src/profiles/mod.rs +++ b/rust/src/profiles/mod.rs @@ -28,7 +28,7 @@ impl Profile { bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None }, #[cfg(not(target_os = "windows"))] wine: crate::model::profile::Wine::default(), - mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini { audio: None, blacklist: None }) } else { None }, + mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini::default()) } else { None }, keyboard: if meta.game == Game::Ongeki { Some(Keyboard::Ongeki(OngekiKeyboard::default())) @@ -43,6 +43,12 @@ impl Profile { std::fs::create_dir_all(p.config_dir())?; std::fs::create_dir_all(p.data_dir())?; + if meta.game == Game::Ongeki { + if let Err(e) = Self::load_existing_mu3_ini(&p.data, &p.meta) { + log::error!("unable to load existing mu3.ini: {e}"); + } + } + 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"))?, @@ -67,6 +73,18 @@ impl Profile { data.sgt.io2 = IOSelection::Custom(io); data.sgt.io = None; } + if let Some(ini) = &mut data.mu3_ini { + if ini.audio.is_none() { + ini.audio = Some(crate::model::profile::Mu3Audio::Shared); + } + if ini.blacklist.is_none() { + ini.blacklist = Some((10000, 19999)); + } + } else { + data.mu3_ini = Some(Mu3Ini::default()); + } + + Self::load_existing_mu3_ini(&data, &ProfileMeta { game, name: name.clone() })?; } if game == Game::Chunithm { if data.keyboard.is_none() { @@ -203,7 +221,7 @@ impl Profile { } if let Some(mu3ini) = &self.data.mu3_ini { - mu3ini.line_up(&self.data.sgt.target.parent().unwrap())?; + mu3ini.line_up(&self.data_dir(), &self.config_dir())?; } if let Some(patches) = &self.data.patches { @@ -283,6 +301,14 @@ impl Profile { "ONGEKI_LANG_PATH", self.data_dir().join("lang"), ) + .env( + "MU3_MODS_CONFIG_PATH", + self.config_dir().join("mu3.ini"), + ) + .env( + "STARTLINER", + "1" + ) .current_dir(&exe_dir) .raw_arg("-d") .raw_arg("-k") @@ -403,6 +429,17 @@ impl Profile { Ok(false) } } + + fn load_existing_mu3_ini(data: &ProfileData, meta: &ProfileMeta) -> Result<()> { + let mu3_ini_target_path = data.sgt.target.parent().ok_or_else(|| anyhow!("invalid target directory"))?.join("mu3.ini"); + let mu3_ini_profile_path = util::profile_config_dir(meta.game, &meta.name).join("mu3.ini"); + log::debug!("mu3.ini paths: {:?} {:?}", mu3_ini_target_path, mu3_ini_profile_path); + if mu3_ini_target_path.exists() && !mu3_ini_profile_path.exists() { + std::fs::copy(&mu3_ini_target_path, &mu3_ini_profile_path)?; + log::info!("copied mu3.ini from {:?}", &mu3_ini_target_path); + } + Ok(()) + } } impl ProfilePaths for Profile { diff --git a/rust/src/util.rs b/rust/src/util.rs index 54f324b..3922fc1 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -199,7 +199,7 @@ pub fn create_shortcut( obj.SetDescription(&format!("{} – {} (STARTLINER)", &meta.game.print(), &meta.name))?; obj.SetArguments(&format!("--start --game {} --profile {}", &meta.game, &meta.name))?; obj.SetIconLocation( - target_dir.join(format!("icon-{}.ico", &meta.game)).to_str().ok_or_else(|| anyhow!("Illegal icon path"))?, + target_dir.join(format!("icon-{}.ico", &meta.game)).to_str().ok_or_else(|| anyhow!("Illegal icon path"))?, 0 )?; diff --git a/rust/static/standard-chunithm.json5 b/rust/static/standard-chunithm.json5 index 91fbdc9..173ae1a 100644 --- a/rust/static/standard-chunithm.json5 +++ b/rust/static/standard-chunithm.json5 @@ -1,4 +1,154 @@ [ + { + filename: 'chusanApp.exe', + version: '2.26.00', + sha256: 'AD2DCC02CE52B3FFF24A2919F8617854581DD2E2C0378EA13D84438FCCA2D522', + 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: 0xF233DA, off: [0x01], on: [0x00]} + ] + }, + { + id: 'standard-2ch', + name: "Force 2 channel audio output", + tooltip: "May cause bass overload", + patches: [ + {offset: 0xF234B1, off: [0x75, 0x3f], on: [0x90, 0x90]} + ] + }, + { + id: 'standard-song-timer', + name: "Disable song select timer", + patches: [ + {offset: 0xA03916, off: [0x74], on: [0xeb]} + ] + }, + { + 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", + offset: 0x965B37, + default: 30, + 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", + offset: 0x9592C2, + default: 60, + 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", + offset: 0xA0EADB, + default: 30, + 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: 0x71E2E0, off: [0xf0], on: [0xc0]} + ] + }, + { + id: 'standard-maximum-tracks', + type: "number", + name: "Maximum tracks", + offset: 0x3980C1, + default: 3, + size: 1, + min: 3, + max: 12 + }, + { + id: 'standard-no-encryption', + name: "No encryption", + tooltip: "Will also disable TLS", + patches: [ + {offset: 0x1DE29E8, off: [0xE1], on: [0x00]}, + {offset: 0x1DE29EC, off: [0xE1], on: [0x00]} + ] + }, + { + id: 'standard-no-tls', + name: "No TLS", + tooltip: "Title server workaround", + patches: [ + {offset: 0xF06447, off: [0x80], on: [0x00]} + ] + }, + { + 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: 0x6533A3, off: [0x01], on: [0x00]} + ] + }, + { + id: 'standard-bypass-1080p', + name: "Bypass 1080p monitor check", + patches: [ + {offset: 0x1CCBF, off: [0x81, 0xbc, 0x24, 0xb8, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x75, 0x1f, 0x81, 0xbc, 0x24, 0xbc, 0x02, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x75, 0x12], on: [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90]} + ] + }, + { + id: 'standard-bypass-120hz', + name: "Bypass 120Hz monitor check", + patches: [ + {offset: 0x1CCB1, off: [0x85, 0xc0], on: [0xeb, 0x30]} + ] + }, + { + id: 'standard-force-free-play-text', + name: "Force FREE PLAY credit text", + tooltip: "Replaces the credit count with FREE PLAY", + patches: [ + {offset: 0x3875A4, off: [0x3c, 0x01], on: [0x38, 0xc0]} + ] + }, + ], + }, + { + filename: 'amdaemon.exe', + version: '2.25.00', + sha256: '00FB867D1EE821033101B8773FAC116A45DF1939D23C38E9DAFC9B86CD5A3777', + patches: [ + { + id: 'standard-localhost', + name: "Allow 127.0.0.1/localhost as the network server", + patches: [ + { offset: 0x6E28A4, off: [0x31, 0x32, 0x37, 0x2F], on: [0x30, 0x2F, 0x38, 0x00] }, + { offset: 0x3C94C4, off: [0xFF, 0x15, 0xC6, 0x2F, 0x1B, 0x00, 0x8B], on: [0x33, 0xC0, 0x48, 0x83, 0xC4, 0x28, 0xC3] } + ] + }, + { + id: 'standard-credit-freeze', + name: "Infinite credits", + patches: [ + { offset: 0x2BBBC8, off: [0x28], on: [0x08] } + ] + } + ] + }, { filename: 'chusanApp.exe', version: '2.30.00', diff --git a/rust/tauri.conf.json b/rust/tauri.conf.json index 515c680..4f0bb63 100644 --- a/rust/tauri.conf.json +++ b/rust/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "STARTLINER", - "version": "0.11.1", + "version": "0.12.0", "identifier": "zip.patafour.startliner", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/components/OptionList.vue b/src/components/OptionList.vue index cf919d3..113ea5c 100644 --- a/src/components/OptionList.vue +++ b/src/components/OptionList.vue @@ -1,5 +1,6 @@ @@ -102,34 +85,59 @@ prf.reload(); - + /> + + + + + diff --git a/src/components/ProfileListEntry.vue b/src/components/ProfileListEntry.vue index 57b09cb..c86a182 100644 --- a/src/components/ProfileListEntry.vue +++ b/src/components/ProfileListEntry.vue @@ -65,6 +65,14 @@ const promptDeleteProfile = async () => { accept: deleteProfile, }); }; + +const dataExists = ref(false); + +path.join(general.dataDir, `profile-${props.p!.game}-${props.p!.name}`).then( + async (p) => { + dataExists.value = await invoke('file_exists', { path: p }); + } +);