feat: audio mode

This commit is contained in:
2025-03-29 00:55:31 +00:00
parent ad5a800d1b
commit 17411e8f0d
9 changed files with 203 additions and 8 deletions

View File

@ -1,6 +1,9 @@
use enumflags2::make_bitflags;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::pkg::PkgKey; use crate::pkg::PkgKey;
use super::profile::ProfileModule;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)]
pub enum Game { pub enum Game {
#[serde(rename = "ongeki")] #[serde(rename = "ongeki")]
@ -59,6 +62,13 @@ impl Game {
Game::Chunithm => vec!["-c", "config_common.json", "config_server.json", "config_client.json", "config_cvt.json", "config_sp.json", "config_hook.json"] Game::Chunithm => vec!["-c", "config_common.json", "config_server.json", "config_client.json", "config_cvt.json", "config_sp.json", "config_hook.json"]
} }
} }
pub fn has_module(&self, module: ProfileModule) -> bool {
match self {
Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini}),
Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Network}),
}.contains(module)
}
} }
impl std::fmt::Display for Game { impl std::fmt::Display for Game {

View File

@ -1,6 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::pkg::PkgKey; use crate::pkg::PkgKey;
use enumflags2::bitflags;
use super::misc::Game; use super::misc::Game;
@ -135,4 +136,31 @@ impl Default for Wine {
.unwrap_or_default() .unwrap_or_default()
} }
} }
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Debug, Copy)]
pub enum Mu3Audio {
Shared,
Excl6Ch,
Excl2Ch,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Mu3Ini {
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<Mu3Audio>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blacklist: Option<(i32, i32)>,
}
#[bitflags]
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ProfileModule {
Segatools,
Network,
Display,
BepInEx,
Mu3Ini
} }

View File

@ -2,6 +2,7 @@ pub mod package;
pub mod segatools; pub mod segatools;
pub mod network; pub mod network;
pub mod bepinex; pub mod bepinex;
pub mod mu3ini;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub mod display_windows; pub mod display_windows;

View File

@ -0,0 +1,30 @@
use std::path::Path;
use 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");
if !file.exists() {
std::fs::write(&file, "")?;
}
let mut ini = Ini::load_from_file(&file)?;
if let Some(audio) = self.audio {
let value = match audio {
Mu3Audio::Shared => "0",
Mu3Audio::Excl6Ch => "1",
Mu3Audio::Excl2Ch => "2",
};
ini.with_section(Some("Sound")).set("WasapiExclusive", value);
}
ini.write_to_file(file)?;
Ok(())
}
}

View File

@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::AppHandle; use tauri::AppHandle;
use std::{collections::BTreeSet, path::{Path, PathBuf}}; use std::{collections::BTreeSet, path::{Path, PathBuf}};
use crate::{model::{misc::Game, profile::Aime}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util}; use crate::{model::{misc::Game, profile::{Aime, Mu3Ini, 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;
@ -42,14 +42,19 @@ pub struct Profile {
pub struct ProfileData { pub struct ProfileData {
pub mods: BTreeSet<PkgKey>, pub mods: BTreeSet<PkgKey>,
pub sgt: Segatools, pub sgt: Segatools,
pub display: Option<Display>,
pub network: Network, pub network: Network,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub bepinex: Option<BepInEx>, pub bepinex: Option<BepInEx>,
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
pub wine: crate::model::profile::Wine, pub wine: crate::model::profile::Wine,
#[serde(skip_serializing_if = "Option::is_none")]
pub mu3_ini: Option<Mu3Ini>
} }
impl Profile { impl Profile {
@ -68,6 +73,7 @@ impl Profile {
bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None }, bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None },
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
wine: crate::model::profile::Wine::default(), wine: crate::model::profile::Wine::default(),
mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini { audio: None, blacklist: None }) } else { None },
}, },
meta: meta.clone() meta: meta.clone()
}; };
@ -138,18 +144,25 @@ impl Profile {
self.data.sgt.fix(store); self.data.sgt.fix(store);
} }
pub fn sync(&mut self, source: ProfileData) { pub fn sync(&mut self, source: ProfileData) {
if self.data.bepinex.is_some() { if self.meta.game.has_module(ProfileModule::BepInEx) && source.bepinex.is_some() {
self.data.bepinex = source.bepinex; self.data.bepinex = source.bepinex;
} }
if self.data.display.is_some() {
if self.meta.game.has_module(ProfileModule::Display) && source.display.is_some() {
self.data.display = source.display; self.data.display = source.display;
} }
// if self.data.network.is_some() {
if self.meta.game.has_module(ProfileModule::Network) {
self.data.network = source.network; self.data.network = source.network;
// } }
// if self.data.sgt.is_some() {
if self.meta.game.has_module(ProfileModule::Segatools) {
self.data.sgt = source.sgt; self.data.sgt = source.sgt;
// } }
if self.meta.game.has_module(ProfileModule::Mu3Ini) && source.mu3_ini.is_some() {
self.data.mu3_ini = source.mu3_ini;
}
} }
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 {
@ -194,6 +207,10 @@ impl Profile {
bepinex.line_up(&self.meta)?; bepinex.line_up(&self.meta)?;
} }
if let Some(mu3ini) = &self.data.mu3_ini {
mu3ini.line_up(&self.data.sgt.target.parent().unwrap())?;
}
Ok(()) Ok(())
} }

View File

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch'; import ToggleSwitch from 'primevue/toggleswitch';
import FileEditor from './FileEditor.vue'; import FileEditor from './FileEditor.vue';
import OptionCategory from './OptionCategory.vue'; import OptionCategory from './OptionCategory.vue';
@ -12,6 +14,54 @@ import { usePrfStore } from '../stores';
const prf = usePrfStore(); const prf = usePrfStore();
const audioModel = 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 = {};
}
prf.current!.data.mu3_ini!.audio = value;
},
});
// 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,
// ];
// },
// });
prf.reload(); prf.reload();
</script> </script>
@ -36,6 +86,47 @@ prf.reload();
<!-- @vue-expect-error --> <!-- @vue-expect-error -->
<ToggleSwitch v-model="prf.current!.data.bepinex.console" /> <ToggleSwitch v-model="prf.current!.data.bepinex.console" />
</OptionRow> </OptionRow>
<OptionRow
title="Audio mode"
tooltip="Exclusive 2-channel mode requires a patch"
>
<SelectButton
v-model="audioModel"
:options="[
{ title: 'Shared', value: 'Shared' },
{ title: 'Exclusive 6-channel', value: 'Excl6Ch' },
{ title: 'Exclusive 2-channel', value: 'Excl2Ch' },
]"
:allow-empty="true"
option-label="title"
option-value="value"
/></OptionRow>
<!-- <OptionRow
class="number-input"
title="Song ID Blacklist"
tooltip="Requires a patch"
><InputNumber
class="shrink"
size="small"
:min="10000"
:max="99999"
placeholder="10000"
:use-grouping="false"
:allow-empty="false"
v-model="blacklistMinModel" />
x
<InputNumber
class="shrink"
size="small"
:min="10000"
:max="99999"
placeholder="19999"
:use-grouping="false"
:allow-empty="false"
v-model="blacklistMaxModel"
/></OptionRow> -->
</OptionCategory> </OptionCategory>
</template> </template>

View File

@ -19,6 +19,7 @@ const prf = usePrfStore();
icon="pi pi-plus" icon="pi pi-plus"
class="chunithm-button profile-button" class="chunithm-button profile-button"
@click="() => prf.create('chunithm')" @click="() => prf.create('chunithm')"
v-tooltip="'!!! Experimental !!!'"
/> />
</div> </div>
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4"> <div class="mt-12 flex flex-col flex-wrap align-middle gap-4">

View File

@ -31,6 +31,15 @@ const aimeCodeModel = computed({
}, },
}); });
const aimeCodePaste = (ev: ClipboardEvent) => {
aimeCodeModel.value =
ev.clipboardData
?.getData('text/plain')
.split('')
.filter((c) => c >= '0' && c <= '9')
.join('') ?? '';
};
(async () => { (async () => {
const aime_path = await path.join(await prf.configDir, 'aime.txt'); const aime_path = await path.join(await prf.configDir, 'aime.txt');
aimeCode.value = await readTextFile(aime_path).catch(() => ''); aimeCode.value = await readTextFile(aime_path).catch(() => '');
@ -67,6 +76,7 @@ const aimeCodeModel = computed({
:maxlength="20" :maxlength="20"
placeholder="00000000000000000000" placeholder="00000000000000000000"
v-model="aimeCodeModel" v-model="aimeCodeModel"
@paste="aimeCodePaste"
/> />
</OptionRow> </OptionRow>
<div v-if="prf.current!.data.sgt.aime?.hasOwnProperty('AMNet')"> <div v-if="prf.current!.data.sgt.aime?.hasOwnProperty('AMNet')">

View File

@ -52,6 +52,7 @@ export interface ProfileData {
display: DisplayConfig; display: DisplayConfig;
network: NetworkConfig; network: NetworkConfig;
bepinex: BepInExConfig; bepinex: BepInExConfig;
mu3_ini: Mu3IniConfig | undefined;
} }
export interface SegatoolsConfig { export interface SegatoolsConfig {
@ -88,10 +89,16 @@ export interface NetworkConfig {
subnet: string; subnet: string;
suffix: number | null; suffix: number | null;
} }
export interface BepInExConfig { export interface BepInExConfig {
console: boolean; console: boolean;
} }
export interface Mu3IniConfig {
audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch';
// blacklist?: [number, number];
}
export interface Profile { export interface Profile {
meta: ProfileMeta; meta: ProfileMeta;
data: ProfileData; data: ProfileData;