feat: 0.12 update

This commit is contained in:
2025-04-19 19:48:08 +00:00
parent aaeed669df
commit 3479804dca
14 changed files with 366 additions and 74 deletions

View File

@ -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

View File

@ -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<PkgKey, PackageListEntry>;
#[derive(Serialize, Deserialize, Clone)]
pub struct PackageListEntry {
pub version: String,
pub status: Status,
pub games: Vec<Game>,
}

View File

@ -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<Mu3Audio>,
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)]

View File

@ -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<Path>) -> Result<()> {
let file = game_path.as_ref().join("mu3.ini");
pub fn line_up(&self, data_dir: impl AsRef<Path>, cfg_dir: impl AsRef<Path>) -> 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(())

View File

@ -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::<PatchFile>(&std::fs::read_to_string(f)?)?
);
let f = &f.path();
match serde_json5::from_str::<PatchFile>(&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);
}

View File

@ -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 ],
}
});

View File

@ -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 {

View File

@ -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',

View File

@ -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",

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue';
import InputNumber from 'primevue/inputnumber';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import FileEditor from './FileEditor.vue';
@ -16,53 +17,35 @@ import { usePrfStore } from '../stores';
const prf = usePrfStore();
const audioModel = computed({
const blacklistMinModel = computed({
get() {
return prf.current?.data.mu3_ini?.audio ?? null;
},
set(value: 'Shared' | 'Excl6Ch' | 'Excl2Ch') {
if (prf.current!.data.mu3_ini === undefined) {
prf.current!.data.mu3_ini = {};
if (prf.current?.data.mu3_ini?.blacklist === undefined) {
return null;
}
prf.current!.data.mu3_ini!.audio = value;
return prf.current?.data.mu3_ini?.blacklist[0];
},
set(value: number) {
prf.current!.data.mu3_ini!.blacklist = [
value,
prf.current!.data.mu3_ini!.blacklist?.[1] ?? 19999,
];
},
});
// const blacklistMinModel = computed({
// get() {
// if (prf.current?.data.mu3_ini?.blacklist === undefined) {
// return null;
// }
// return prf.current?.data.mu3_ini?.blacklist[0];
// },
// set(value: number) {
// if (prf.current!.data.mu3_ini === undefined) {
// prf.current!.data.mu3_ini = {};
// }
// prf.current!.data.mu3_ini!.blacklist = [
// value,
// prf.current!.data.mu3_ini!.blacklist?.[1] ?? 19999,
// ];
// },
// });
// const blacklistMaxModel = computed({
// get() {
// if (prf.current?.data.mu3_ini?.blacklist === undefined) {
// return null;
// }
// return prf.current?.data.mu3_ini?.blacklist[1];
// },
// set(value: number) {
// if (prf.current!.data.mu3_ini === undefined) {
// prf.current!.data.mu3_ini = {};
// }
// prf.current!.data.mu3_ini!.blacklist = [
// prf.current!.data.mu3_ini!.blacklist?.[0] ?? 10000,
// value,
// ];
// },
// });
const blacklistMaxModel = computed({
get() {
if (prf.current?.data.mu3_ini?.blacklist === undefined) {
return null;
}
return prf.current?.data.mu3_ini.blacklist[1];
},
set(value: number) {
prf.current!.data.mu3_ini!.blacklist = [
prf.current!.data.mu3_ini!.blacklist?.[0] ?? 10000,
value,
];
},
});
prf.reload();
</script>
@ -102,34 +85,59 @@ prf.reload();
<OptionRow
title="Audio mode"
tooltip="Exclusive 2-channel mode requires a patch"
tooltip="Exclusive 2-channel mode requires 7EVENDAYSHOLIDAYS-ExclusiveAudio"
>
<SelectButton
v-model="audioModel"
v-model="prf.current!.data.mu3_ini!.audio"
:options="[
{ title: 'Shared', value: 'Shared' },
{ title: 'Exclusive 6-channel', value: 'Excl6Ch' },
{ title: 'Exclusive 2-channel', value: 'Excl2Ch' },
]"
:allow-empty="true"
:allow-empty="false"
option-label="title"
option-value="value"
/></OptionRow>
<!-- <OptionRow
<OptionRow
title="Sample rate"
v-if="
prf.current?.data.mods.includes(
'7EVENDAYSHOLIDAYS-ExclusiveAudio'
)
"
>
<SelectButton
v-model="prf.current!.data.mu3_ini!.sample_rate"
:disabled="prf.current!.data.mu3_ini!.audio === 'Shared'"
:options="[
{ title: '44.1KHz', value: 44100 },
{ title: '48KHz', value: 48000 },
{ title: '96KHz', value: 96000 },
{ title: '192KHz', value: 192000 },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/></OptionRow>
<OptionRow
v-if="
prf.current?.data.mods.includes('7EVENDAYSHOLIDAYS-Blacklist')
"
class="number-input"
title="Song ID Blacklist"
tooltip="Requires a patch"
tooltip="Scores on charts within this ID range will not be saved nor uploaded"
><InputNumber
class="shrink"
size="small"
:min="10000"
:min="9000"
:max="99999"
placeholder="10000"
:use-grouping="false"
:allow-empty="false"
v-model="blacklistMinModel" />
x
~
<InputNumber
class="shrink"
size="small"
@ -139,7 +147,36 @@ prf.reload();
:use-grouping="false"
:allow-empty="false"
v-model="blacklistMaxModel"
/></OptionRow> -->
/></OptionRow>
<OptionRow
class="number-input"
title="GP"
v-if="
prf.current?.data.mods.includes('7EVENDAYSHOLIDAYS-DisableGP')
"
><InputNumber
class="shrink"
size="small"
:min="0"
:max="9999"
:use-grouping="false"
:allow-empty="false"
v-model="prf.current!.data.mu3_ini!.gp"
/>
</OptionRow>
<OptionRow
title="Unlock Bonus Tracks"
tooltip="Disabling this option can help declutter the song list"
v-if="
prf.current?.data.mods.includes(
'7EVENDAYSHOLIDAYS-UnlockAllMusic'
)
"
>
<ToggleSwitch
v-model="prf.current!.data.mu3_ini!.enable_bonus_tracks"
/>
</OptionRow>
</OptionCategory>
<KeyboardOptions />
<StartlinerOptions />

View File

@ -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 });
}
);
</script>
<template>
@ -124,10 +132,27 @@ const promptDeleteProfile = async () => {
@click="isEditing = true"
/>
<Button
rounded
icon="pi pi-cog"
severity="help"
aria-label="open-config-directory"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
@click="
path
.join(general.configDir, `profile-${p!.game}-${p!.name}`)
.then(async (path) => {
await invoke('open_file', { path });
})
"
/>
<Button
v-if="dataExists"
rounded
icon="pi pi-folder"
severity="help"
aria-label="open-directory"
aria-label="open-data-directory"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
@ -135,9 +160,7 @@ const promptDeleteProfile = async () => {
path
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
.then(async (path) => {
if (await invoke('file_exists', { path })) {
await invoke('open_file', { path });
}
})
"
/>

View File

@ -107,7 +107,10 @@ export interface BepInExConfig {
export interface Mu3IniConfig {
audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch';
// blacklist?: [number, number];
sample_rate: number;
blacklist?: [number, number];
gp: number;
enable_bonus_tracks: boolean;
}
export interface OngekiButtons {