feat: initial chunithm support

This commit is contained in:
2025-03-19 17:39:12 +00:00
parent 1191cdd95c
commit 8ac45df3e1
31 changed files with 1368 additions and 884 deletions

View File

@ -16,6 +16,7 @@ import StartButton from './StartButton.vue';
import { invoke } from '../invoke';
import { useGeneralStore, usePkgStore, usePrfStore } from '../stores';
import { Dirs } from '../types';
import { listen } from '@tauri-apps/api/event';
const pkg = usePkgStore();
const prf = usePrfStore();
@ -27,6 +28,7 @@ const currentTab: Ref<string | number> = ref(3);
const pkgSearchTerm = ref('');
const isProfileDisabled = computed(() => prf.current === null);
const isRunning = ref(false);
onMounted(async () => {
invoke('list_directories').then((d) => {
@ -47,8 +49,23 @@ onMounted(async () => {
key: 'segatools-mu3hook',
force: false,
});
await invoke('install_package', {
key: 'segatools-chusanhook',
force: false,
});
});
});
listen('launch-start', () => {
isRunning.value = true;
currentTab.value = 5;
});
listen('launch-end', () => {
isRunning.value = false;
currentTab.value = 0;
});
</script>
<template>
@ -61,20 +78,27 @@ onMounted(async () => {
>
<div class="fixed w-full flex z-100">
<TabList class="grow">
<Tab :disabled="isProfileDisabled" :value="0"
><div class="pi pi-list-check"></div
<Tab :value="3" :disabled="isRunning"
><div class="pi pi-question-circle"></div
></Tab>
<Tab :disabled="isProfileDisabled || isRunning" :value="0"
><div class="pi pi-box"></div
></Tab>
<Tab v-if="prf.current?.meta.game === 'chunithm'" :disabled="isRunning" :value="4"
><div class="pi pi-ticket"></div
></Tab>
<Tab
v-if="pkg.networkStatus === 'online'"
:disabled="isProfileDisabled"
:disabled="isProfileDisabled || isRunning"
:value="1"
><div class="pi pi-download"></div
></Tab>
<Tab :disabled="isProfileDisabled" :value="2"
<Tab :disabled="isProfileDisabled || isRunning" :value="2"
><div class="pi pi-cog"></div
></Tab>
<Tab :value="3"
><div class="pi pi-question-circle"></div
<Tab :value="5" v-if="isRunning"
><div class="pi pi-sparkles"></div
></Tab>
<div class="grow"></div>
<div class="flex gap-4">
@ -131,16 +155,6 @@ onMounted(async () => {
missing.<br />Existing features are expected to break
sometimes.
<ProfileList />
<img
v-if="prf.current?.game === 'ongeki'"
src="/sticker-ongeki.svg"
class="fixed bottom-0 right-0 z-999"
/>
<img
v-else-if="prf.current?.game === 'chunithm'"
src="/sticker-chunithm.svg"
class="fixed bottom-0 right-0 z-999"
/>
<br /><br /><br />
<footer>
<Button
@ -151,7 +165,24 @@ onMounted(async () => {
/>
</footer>
</TabPanel>
<TabPanel :value="4">
CHUNITHM patches are not yet implemented.<br />Use
<a href=https://patcher.two-torial.xyz/ target="_blank" style="text-decoration: underline;">patcher.two-torial.xyz</a>
</TabPanel>
<TabPanel :value="5">Running!</TabPanel>
</TabPanels>
<div v-if="currentTab === 5 || currentTab === 3">
<img
v-if="prf.current?.meta.game === 'ongeki'"
src="/sticker-ongeki.svg"
class="fixed bottom-0 right-0 z-999"
/>
<img
v-else-if="prf.current?.meta.game === 'chunithm'"
src="/sticker-chunithm.svg"
class="fixed bottom-0 right-0 z-999"
/>
</div>
</Tabs>
</main>
</template>

View File

@ -1,11 +1,13 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Ref, computed, ref } from 'vue';
import Button from 'primevue/button';
import Fieldset from 'primevue/fieldset';
import ModListEntry from './ModListEntry.vue';
import ModTitlecard from './ModTitlecard.vue';
import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { Package } from '../types';
import { pkgKey } from '../util';
const props = defineProps({
search: String,
@ -14,12 +16,20 @@ const props = defineProps({
const pkgs = usePkgStore();
const prf = usePrfStore();
const empty = ref(true);
const gameSublist: Ref<string[]> = ref([]);
const group = () => {
const a = Object.assign(
invoke('get_game_packages', {
game: prf.current?.meta.game,
}).then((list) => {
gameSublist.value = list as string[];
});
const group = computed(() => {
const res = Object.assign(
{},
Object.groupBy(
pkgs.allLocal
.filter((p) => gameSublist.value.includes(pkgKey(p)))
.filter(
(p) =>
props.search === undefined ||
@ -35,12 +45,12 @@ const group = () => {
({ namespace }) => namespace
)
);
empty.value = Object.keys(a).length === 0;
return a;
};
empty.value = Object.keys(res).length === 0;
return res;
});
const missing = computed(() => {
return prf.current?.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
return prf.current?.data.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
});
</script>
@ -68,7 +78,7 @@ const missing = computed(() => {
/>
</div>
</Fieldset>
<Fieldset v-for="(namespace, key) in group()" :legend="key.toString()">
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()">
<ModListEntry v-for="p in namespace" :pkg="p" />
</Fieldset>
<div v-if="empty" class="text-3xl"></div>

View File

@ -32,7 +32,6 @@ const model = computed({
<div class="flex items-center">
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
<UpdateButton :pkg="pkg" />
<!-- @vue-expect-error Can't 'as any' because it breaks VSCode -->
<ToggleSwitch
v-if="hasFeature(pkg, Feature.Mod)"
class="scale-[1.33] shrink-0"

View File

@ -1,20 +1,32 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Ref, ref } from 'vue';
import Divider from 'primevue/divider';
import MultiSelect from 'primevue/multiselect';
import ToggleSwitch from 'primevue/toggleswitch';
import ModStoreEntry from './ModStoreEntry.vue';
import { usePkgStore } from '../stores';
import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { pkgKey } from '../util';
const pkgs = usePkgStore();
const prf = usePrfStore();
const empty = ref(true);
const props = defineProps({
search: String,
});
const gameSublist: Ref<string[]> = ref([]);
invoke('get_game_packages', {
game: prf.current?.meta.game,
}).then((list) => {
gameSublist.value = list as string[];
});
const list = () => {
const res = pkgs.allRemote
.filter((p) => gameSublist.value.includes(pkgKey(p)))
.filter(
(p) =>
props.search === undefined ||

View File

@ -53,13 +53,16 @@ const iconSrc = computed(() => {
>
</span>
<span
v-if="hasFeature(pkg, Feature.Hook)"
v-if="
hasFeature(pkg, Feature.ChusanHook) ||
hasFeature(pkg, Feature.Mu3Hook)
"
v-tooltip="'Hook'"
class="pi pi-wrench ml-1 text-blue-400"
>
</span>
<span
v-if="hasFeature(pkg, Feature.GameIO)"
v-if="hasFeature(pkg, Feature.Mu3IO)"
v-tooltip="'IO'"
class="pi pi-wrench ml-1 text-green-400"
>

View File

@ -1,399 +1,30 @@
<script setup lang="ts">
import { Ref, computed, ref } from 'vue';
import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext';
import Select from 'primevue/select';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import * as path from '@tauri-apps/api/path';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
import FileEditor from './FileEditor.vue';
import FilePicker from './FilePicker.vue';
import OptionCategory from './OptionCategory.vue';
import OptionRow from './OptionRow.vue';
import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { Feature } from '../types';
import { hasFeature, pkgKey } from '../util';
import AimeOptions from './options/Aime.vue';
import DisplayOptions from './options/Display.vue';
import MiscOptions from './options/Misc.vue';
import NetworkOptions from './options/Network.vue';
import SegatoolsOptions from './options/Segatools.vue';
import { usePrfStore } from '../stores';
const pkgs = usePkgStore();
const prf = usePrfStore();
const aimeCode = ref('');
const capabilities: Ref<string[]> = ref([]);
const displayList: Ref<{ title: string; value: string }[]> = ref([
{
title: 'Primary',
value: 'default',
},
]);
const loadDisplays = () => {
const newList = [
{
title: 'Primary',
value: 'default',
},
];
invoke('list_platform_capabilities')
.then(async (v: unknown) => {
let different = false;
if (Array.isArray(v)) {
capabilities.value.push(...v);
}
if (capabilities.value.includes('display')) {
for (const [devName, devString] of (await invoke(
'list_displays'
)) as Array<[string, string]>) {
newList.push({
title: `${devName.replace('\\\\.\\', '')} (${devString})`,
value: devName,
});
if (
displayList.value.find(
(item) => item.value === devName
) === undefined
) {
different = true;
}
}
}
if (displayList.value.length !== newList.length) {
different = true;
}
if (different) {
displayList.value = newList;
}
})
.catch(() => {});
};
loadDisplays();
prf.reload();
const aimeCodeModel = computed({
get() {
return aimeCode.value;
},
async set(value: string) {
aimeCode.value = value;
if (value.match(/^[0-9]{20}$/) || value.length === 0) {
const aime_path = await path.join(await prf.configDir, 'aime.txt');
await writeTextFile(aime_path, aimeCode.value);
}
},
});
const extraDisplayOptionsDisabled = computed(() => {
return prf.current?.display.target === 'default';
});
(async () => {
const aime_path = await path.join(await prf.configDir, 'aime.txt');
aimeCode.value = await readTextFile(aime_path).catch(() => '');
})();
</script>
<template>
<OptionCategory title="General">
<OptionRow title="mu3.exe">
<FilePicker
:directory="false"
promptname="mu3.exe"
extension="exe"
:value="prf.current!.sgt.target"
:callback="(value: string) => (prf.current!.sgt.target = value)"
></FilePicker>
</OptionRow>
<OptionRow title="amfs">
<FilePicker
:directory="true"
placeholder="amfs"
:value="prf.current!.sgt.amfs"
:callback="(value: string) => (prf.current!.sgt.amfs = value)"
></FilePicker>
</OptionRow>
<OptionRow title="option">
<FilePicker
:directory="true"
placeholder="option"
:value="prf.current!.sgt.option"
:callback="(value: string) => (prf.current!.sgt.option = value)"
></FilePicker>
</OptionRow>
<OptionRow title="appdata">
<FilePicker
:directory="true"
:value="prf.current!.sgt.appdata"
:callback="
(value: string) => (prf.current!.sgt.appdata = value)
"
></FilePicker>
</OptionRow>
<OptionRow title="mu3hook">
<Select
v-model="prf.current!.sgt.hook"
:options="
pkgs.hooks.map((p) => {
return { title: pkgKey(p), value: pkgKey(p) };
})
"
option-label="title"
option-value="value"
></Select>
</OptionRow>
<OptionRow title="mu3io">
<Select
v-model="prf.current!.sgt.io"
placeholder="segatools built-in"
:options="[
{ title: 'segatools built-in', value: null },
...pkgs.gameIOs.map((p) => {
return { title: pkgKey(p), value: pkgKey(p) };
}),
]"
option-label="title"
option-value="value"
></Select>
</OptionRow>
</OptionCategory>
<OptionCategory title="Display">
<OptionRow
v-if="capabilities.includes('display')"
title="Target display"
>
<Select
v-model="prf.current!.display.target"
:options="displayList"
option-label="title"
option-value="value"
placeholder="(Disconnected)"
@show="loadDisplays"
></Select>
</OptionRow>
<OptionRow class="number-input" title="Game resolution">
<InputNumber
class="shrink"
size="small"
:min="480"
:max="9999"
:use-grouping="false"
v-model="prf.current!.display.rez[0]"
/>
x
<InputNumber
class="shrink"
size="small"
:min="640"
:max="9999"
:use-grouping="false"
v-model="prf.current!.display.rez[1]"
/>
</OptionRow>
<OptionRow title="Display mode">
<SelectButton
v-model="prf.current!.display.mode"
:options="[
{ title: 'Window', value: 'Window' },
{ title: 'Borderless window', value: 'Borderless' },
{ title: 'Fullscreen', value: 'Fullscreen' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</OptionRow>
<OptionRow
title="Display rotation"
v-if="capabilities.includes('display')"
>
<SelectButton
v-model="prf.current!.display.rotation"
:options="[
{ title: 'Unchanged', value: 0 },
{ title: 'Portrait', value: 90 },
{ title: 'Portrait (flipped)', value: 270 },
]"
:allow-empty="false"
option-label="title"
option-value="value"
:disabled="extraDisplayOptionsDisabled"
/>
</OptionRow>
<OptionRow
v-if="capabilities.includes('display')"
class="number-input"
title="Refresh Rate"
>
<InputNumber
class="shrink"
size="small"
:min="60"
:max="999"
:use-grouping="false"
v-model="prf.current!.display.frequency"
:disabled="extraDisplayOptionsDisabled"
/>
</OptionRow>
<OptionRow
title="Borderless fullscreen"
v-if="capabilities.includes('display')"
tooltip="Match display resolution with the game."
>
<ToggleSwitch
:disabled="
extraDisplayOptionsDisabled ||
prf.current?.display.mode !== 'Borderless'
"
v-model="prf.current!.display.borderless_fullscreen"
/>
</OptionRow>
</OptionCategory>
<OptionCategory title="Network">
<OptionRow title="Network type">
<SelectButton
v-model="prf.current!.network.network_type"
:options="[
{ title: 'Remote', value: 'Remote' },
{ title: 'Local (ARTEMiS)', value: 'Artemis' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</OptionRow>
<OptionRow
v-if="prf.current!.network.network_type == 'Artemis'"
title="ARTEMiS path"
>
<FilePicker
:directory="false"
promptname="index.py"
extension="py"
:value="prf.current!.network.local_path"
:callback="
(value: string) => (prf.current!.network.local_path = value)
"
></FilePicker>
</OptionRow>
<!-- <OptionRow
v-if="prf.current!.network.network_type == 'Artemis'"
title="ARTEMiS console"
>
<ToggleSwitch v-model="prf.current!.network.local_console" />
</OptionRow> -->
<OptionRow
v-if="prf.current!.network.network_type == 'Remote'"
title="Server address"
>
<InputText
class="shrink"
size="small"
:maxlength="40"
placeholder="192.168.1.234"
v-model="prf.current!.network.remote_address"
/> </OptionRow
><OptionRow
v-if="prf.current!.network.network_type == 'Remote'"
title="Keychip"
>
<InputText
class="shrink"
size="small"
:maxlength="16"
placeholder="A123-01234567890"
v-model="prf.current!.network.keychip"
/> </OptionRow
><OptionRow title="Subnet">
<InputText
class="shrink"
size="small"
:maxlength="15"
placeholder="192.168.1.0"
v-model="prf.current!.network.subnet"
/>
</OptionRow>
<OptionRow title="Address suffix">
<InputNumber
class="shrink"
size="small"
:maxlength="3"
:min="0"
:max="255"
placeholder="12"
v-model="prf.current!.network.suffix"
/>
</OptionRow>
</OptionCategory>
<OptionCategory title="Aime">
<OptionRow title="Aime emulation">
<Select
v-model="prf.current!.sgt.aime"
:options="[
{ title: 'none', value: 'Disabled' },
{ title: 'segatools built-in', value: 'BuiltIn' },
...pkgs.aimes.map((p) => {
return {
title: pkgKey(p),
value: hasFeature(p, Feature.AMNet)
? { AMNet: pkgKey(p) }
: { Other: pkgKey(p) },
};
}),
]"
placeholder="none"
option-label="title"
option-value="value"
></Select>
</OptionRow>
<OptionRow title="Aime code">
<InputText
class="shrink"
size="small"
:disabled="prf.current!.sgt.aime === 'Disabled'"
:maxlength="20"
placeholder="00000000000000000000"
v-model="aimeCodeModel"
/>
</OptionRow>
<div v-if="prf.current!.sgt.aime?.hasOwnProperty('AMNet')">
<OptionRow title="Server name">
<InputText
class="shrink"
size="small"
placeholder="CHUNI-PENGUIN"
:maxlength="50"
v-model="prf.current!.sgt.amnet.name"
/>
</OptionRow>
<OptionRow title="Server address">
<InputText
class="shrink"
size="small"
placeholder="http://+:6070"
:maxlength="50"
v-model="prf.current!.sgt.amnet.addr"
/>
</OptionRow>
<OptionRow
title="Use AiMeDB for physical cards"
tooltip="Whether physical cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, enable this option to load the same account data/profile as you would get on a physical cab."
>
<ToggleSwitch v-model="prf.current!.sgt.amnet.physical" />
</OptionRow>
</div>
</OptionCategory>
<OptionCategory title="Misc">
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
<ToggleSwitch v-model="prf.current!.sgt.intel" />
</OptionRow>
<OptionRow title="More segatools options">
<FileEditor filename="segatools-base.ini" />
</OptionRow>
</OptionCategory>
<OptionCategory title="Extensions">
<SegatoolsOptions />
<DisplayOptions v-if="prf.current!.meta.game === 'ongeki'" />
<NetworkOptions />
<AimeOptions />
<MiscOptions />
<OptionCategory
title="Extensions"
v-if="prf.current!.meta.game === 'ongeki'"
>
<OptionRow title="Inohara config">
<FileEditor
filename="inohara.cfg"
@ -402,7 +33,8 @@ const extraDisplayOptionsDisabled = computed(() => {
/>
</OptionRow>
<OptionRow title="BepInEx console">
<ToggleSwitch v-model="prf.current!.bepinex.console" />
<!-- @vue-expect-error -->
<ToggleSwitch v-model="prf.current!.data.bepinex.console" />
</OptionRow>
</OptionCategory>
</template>

View File

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

View File

@ -61,7 +61,8 @@ const deleteProfile = async () => {
<div class="flex flex-row flex-wrap align-middle gap-2">
<Button
:disabled="
prf.current?.game === p!.game && prf.current?.name === p!.name
prf.current?.meta.game === p!.game &&
prf.current?.meta.name === p!.name
"
:class="
(p!.game === 'chunithm' ? 'chunithm-button' : 'ongeki-button') +

View File

@ -5,6 +5,7 @@ import ConfirmDialog from 'primevue/confirmdialog';
import ScrollPanel from 'primevue/scrollpanel';
import { useConfirm } from 'primevue/useconfirm';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { invoke } from '../invoke';
import { usePrfStore } from '../stores';
@ -54,17 +55,19 @@ const startline = async (force: boolean) => {
const kill = async () => {
await invoke('kill');
startStatus.value = 'ready';
};
const disabledTooltip = computed(() => {
if (prf.current?.sgt.target.length === 0) {
if (prf.current?.data.sgt.target.length === 0) {
return 'The game path must be specified';
}
if (prf.current?.sgt.amfs.length === 0) {
if (prf.current?.data.sgt.amfs.length === 0) {
return 'The amfs path must be specified';
}
if (prf.current?.sgt.hook === null || prf.current?.sgt.hook === undefined) {
if (
prf.current?.data.sgt.hook === null ||
prf.current?.data.sgt.hook === undefined
) {
return 'A segatools hook package is necessary';
}
return null;
@ -72,10 +75,13 @@ const disabledTooltip = computed(() => {
listen('launch-start', () => {
startStatus.value = 'running';
getCurrentWindow().minimize();
});
listen('launch-end', () => {
startStatus.value = 'ready';
getCurrentWindow().unminimize();
getCurrentWindow().setFocus();
});
const messageSplit = (message: any) => {

View File

@ -0,0 +1,99 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import InputText from 'primevue/inputtext';
import Select from 'primevue/select';
import ToggleSwitch from 'primevue/toggleswitch';
import * as path from '@tauri-apps/api/path';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePkgStore, usePrfStore } from '../../stores';
import { Feature } from '../../types';
import { hasFeature, pkgKey } from '../../util';
const pkgs = usePkgStore();
const prf = usePrfStore();
const aimeCode = ref('');
prf.reload();
const aimeCodeModel = computed({
get() {
return aimeCode.value;
},
async set(value: string) {
aimeCode.value = value;
if (value.match(/^[0-9]{20}$/) || value.length === 0) {
const aime_path = await path.join(await prf.configDir, 'aime.txt');
await writeTextFile(aime_path, aimeCode.value);
}
},
});
(async () => {
const aime_path = await path.join(await prf.configDir, 'aime.txt');
aimeCode.value = await readTextFile(aime_path).catch(() => '');
})();
</script>
<template>
<OptionCategory title="Aime">
<OptionRow title="Aime emulation">
<Select
v-model="prf.current!.data.sgt.aime"
:options="[
{ title: 'none', value: 'Disabled' },
{ title: 'segatools built-in', value: 'BuiltIn' },
...pkgs.byFeature(Feature.Aime).map((p) => {
return {
title: pkgKey(p),
value: hasFeature(p, Feature.AMNet)
? { AMNet: pkgKey(p) }
: { Other: pkgKey(p) },
};
}),
]"
placeholder="none"
option-label="title"
option-value="value"
></Select>
</OptionRow>
<OptionRow title="Aime code">
<InputText
class="shrink"
size="small"
:disabled="prf.current!.data.sgt.aime === 'Disabled'"
:maxlength="20"
placeholder="00000000000000000000"
v-model="aimeCodeModel"
/>
</OptionRow>
<div v-if="prf.current!.data.sgt.aime?.hasOwnProperty('AMNet')">
<OptionRow title="Server name">
<InputText
class="shrink"
size="small"
placeholder="CHUNI-PENGUIN"
:maxlength="50"
v-model="prf.current!.data.sgt.amnet.name"
/>
</OptionRow>
<OptionRow title="Server address">
<InputText
class="shrink"
size="small"
placeholder="http://+:6070"
:maxlength="50"
v-model="prf.current!.data.sgt.amnet.addr"
/>
</OptionRow>
<OptionRow
title="Use AiMeDB for physical cards"
tooltip="Whether physical cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, enable this option to load the same account data/profile as you would get on a physical cab."
>
<ToggleSwitch v-model="prf.current!.data.sgt.amnet.physical" />
</OptionRow>
</div>
</OptionCategory>
</template>

View File

@ -0,0 +1,161 @@
<script setup lang="ts">
import { Ref, computed, ref } from 'vue';
import InputNumber from 'primevue/inputnumber';
import Select from 'primevue/select';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { invoke } from '../../invoke';
import { usePrfStore } from '../../stores';
const capabilities: Ref<string[]> = ref([]);
const displayList: Ref<{ title: string; value: string }[]> = ref([
{
title: 'Primary',
value: 'default',
},
]);
const prf = usePrfStore();
const extraDisplayOptionsDisabled = computed(() => {
return prf.current?.data.display.target === 'default';
});
const loadDisplays = () => {
const newList = [
{
title: 'Primary',
value: 'default',
},
];
invoke('list_platform_capabilities')
.then(async (v: unknown) => {
let different = false;
if (Array.isArray(v)) {
capabilities.value.push(...v);
}
if (capabilities.value.includes('display')) {
for (const [devName, devString] of (await invoke(
'list_displays'
)) as Array<[string, string]>) {
newList.push({
title: `${devName.replace('\\\\.\\', '')} (${devString})`,
value: devName,
});
if (
displayList.value.find(
(item) => item.value === devName
) === undefined
) {
different = true;
}
}
}
if (displayList.value.length !== newList.length) {
different = true;
}
if (different) {
displayList.value = newList;
}
})
.catch(() => {});
};
loadDisplays();
</script>
<template>
<OptionCategory title="Display">
<OptionRow
v-if="capabilities.includes('display')"
title="Target display"
>
<Select
v-model="prf.current!.data.display.target"
:options="displayList"
option-label="title"
option-value="value"
placeholder="(Disconnected)"
@show="loadDisplays"
></Select>
</OptionRow>
<OptionRow class="number-input" title="Game resolution">
<InputNumber
class="shrink"
size="small"
:min="480"
:max="9999"
:use-grouping="false"
v-model="prf.current!.data.display.rez[0]"
/>
x
<InputNumber
class="shrink"
size="small"
:min="640"
:max="9999"
:use-grouping="false"
v-model="prf.current!.data.display.rez[1]"
/>
</OptionRow>
<OptionRow title="Display mode">
<SelectButton
v-model="prf.current!.data.display.mode"
:options="[
{ title: 'Window', value: 'Window' },
{ title: 'Borderless window', value: 'Borderless' },
{ title: 'Fullscreen', value: 'Fullscreen' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</OptionRow>
<OptionRow
title="Display rotation"
v-if="capabilities.includes('display')"
>
<SelectButton
v-model="prf.current!.data.display.rotation"
:options="[
{ title: 'Unchanged', value: 0 },
{ title: 'Portrait', value: 90 },
{ title: 'Portrait (flipped)', value: 270 },
]"
:allow-empty="false"
option-label="title"
option-value="value"
:disabled="extraDisplayOptionsDisabled"
/>
</OptionRow>
<OptionRow
v-if="capabilities.includes('display')"
class="number-input"
title="Refresh Rate"
>
<InputNumber
class="shrink"
size="small"
:min="60"
:max="999"
:use-grouping="false"
v-model="prf.current!.data.display.frequency"
:disabled="extraDisplayOptionsDisabled"
/>
</OptionRow>
<OptionRow
title="Borderless fullscreen"
v-if="capabilities.includes('display')"
tooltip="Match display resolution with the game."
>
<ToggleSwitch
:disabled="
extraDisplayOptionsDisabled ||
prf.current?.data.display.mode !== 'Borderless'
"
v-model="prf.current!.data.display.borderless_fullscreen"
/>
</OptionRow>
</OptionCategory>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import ToggleSwitch from 'primevue/toggleswitch';
import FileEditor from '../FileEditor.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Misc">
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
<ToggleSwitch v-model="prf.current!.data.sgt.intel" />
</OptionRow>
<OptionRow title="More segatools options">
<FileEditor filename="segatools-base.ini" />
</OptionRow>
</OptionCategory>
</template>

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext';
import SelectButton from 'primevue/selectbutton';
import FilePicker from '../FilePicker.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Network">
<OptionRow title="Network type">
<SelectButton
v-model="prf.current!.data.network.network_type"
:options="[
{ title: 'Remote', value: 'Remote' },
{ title: 'Local (ARTEMiS)', value: 'Artemis' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</OptionRow>
<OptionRow
v-if="prf.current!.data.network.network_type == 'Artemis'"
title="ARTEMiS path"
>
<FilePicker
:directory="false"
promptname="index.py"
extension="py"
:value="prf.current!.data.network.local_path"
:callback="
(value: string) =>
(prf.current!.data.network.local_path = value)
"
></FilePicker>
</OptionRow>
<!-- <OptionRow
v-if="prf.current!.data.network.network_type == 'Artemis'"
title="ARTEMiS console"
>
<ToggleSwitch v-model="prf.current!.data.network.local_console" />
</OptionRow> -->
<OptionRow
v-if="prf.current!.data.network.network_type == 'Remote'"
title="Server address"
>
<InputText
class="shrink"
size="small"
:maxlength="40"
placeholder="192.168.1.234"
v-model="prf.current!.data.network.remote_address"
/> </OptionRow
><OptionRow
v-if="prf.current!.data.network.network_type == 'Remote'"
title="Keychip"
>
<InputText
class="shrink"
size="small"
:maxlength="16"
placeholder="A123-01234567890"
v-model="prf.current!.data.network.keychip"
/> </OptionRow
><OptionRow title="Subnet">
<InputText
class="shrink"
size="small"
:maxlength="15"
placeholder="192.168.1.0"
v-model="prf.current!.data.network.subnet"
/>
</OptionRow>
<OptionRow title="Address suffix">
<InputNumber
class="shrink"
size="small"
:maxlength="3"
:min="0"
:max="255"
placeholder="12"
v-model="prf.current!.data.network.suffix"
/>
</OptionRow>
</OptionCategory>
</template>

View File

@ -0,0 +1,112 @@
<script setup lang="ts">
import { computed } from 'vue';
import Select from 'primevue/select';
import FilePicker from '../FilePicker.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePkgStore, usePrfStore } from '../../stores';
import { Feature } from '../../types';
import { pkgKey } from '../../util';
const prf = usePrfStore();
const pkgs = usePkgStore();
const names = computed(() => {
switch (prf.current?.meta.game) {
case 'ongeki': {
return {
exe: 'mu3.exe',
hook: 'mu3hook',
io: 'mu3io',
};
}
case 'chunithm': {
return {
exe: 'chusanApp.exe',
hook: 'chusanhook',
io: 'chuniio',
};
}
case undefined:
throw new Error('Option tab without a profile');
}
});
</script>
<template>
<OptionCategory title="General">
<OptionRow :title="names.exe">
<FilePicker
:directory="false"
:promptname="names.exe"
extension="exe"
:value="prf.current!.data.sgt.target"
:callback="
(value: string) => (prf.current!.data.sgt.target = value)
"
></FilePicker>
</OptionRow>
<OptionRow title="amfs">
<FilePicker
:directory="true"
placeholder="amfs"
:value="prf.current!.data.sgt.amfs"
:callback="
(value: string) => (prf.current!.data.sgt.amfs = value)
"
></FilePicker>
</OptionRow>
<OptionRow title="option">
<FilePicker
:directory="true"
placeholder="option"
:value="prf.current!.data.sgt.option"
:callback="
(value: string) => (prf.current!.data.sgt.option = value)
"
></FilePicker>
</OptionRow>
<OptionRow title="appdata">
<FilePicker
:directory="true"
:value="prf.current!.data.sgt.appdata"
:callback="
(value: string) => (prf.current!.data.sgt.appdata = value)
"
></FilePicker>
</OptionRow>
<OptionRow :title="names.hook">
<Select
v-model="prf.current!.data.sgt.hook"
:options="
pkgs
.byFeature(
prf.current?.meta.game === 'ongeki'
? Feature.Mu3Hook
: Feature.ChusanHook
)
.map((p) => {
return { title: pkgKey(p), value: pkgKey(p) };
})
"
option-label="title"
option-value="value"
></Select>
</OptionRow>
<OptionRow :title="names.io" v-if="prf.current?.meta.game === 'ongeki'">
<Select
v-model="prf.current!.data.sgt.io"
placeholder="segatools built-in"
:options="[
{ title: 'segatools built-in', value: null },
...pkgs.byFeature(Feature.Mu3IO).map((p) => {
return { title: pkgKey(p), value: pkgKey(p) };
}),
]"
option-label="title"
option-value="value"
></Select>
</OptionRow>
</OptionCategory>
</template>

View File

@ -100,14 +100,8 @@ export const usePkgStore = defineStore('pkg', {
(c) => !state.excludeCategories.includes(c)
))
),
hooks: (state) =>
Object.values(state.pkg).filter((p) => hasFeature(p, Feature.Hook)),
gameIOs: (state) =>
Object.values(state.pkg).filter((p) =>
hasFeature(p, Feature.GameIO)
),
aimes: (state) =>
Object.values(state.pkg).filter((p) => hasFeature(p, Feature.Aime)),
byFeature: (state) => (feature: Feature) =>
Object.values(state.pkg).filter((p) => hasFeature(p, feature)),
},
actions: {
setupListeners() {
@ -189,18 +183,14 @@ export const usePrfStore = defineStore('prf', () => {
() =>
pkg !== undefined &&
current.value !== null &&
current.value?.mods.includes(pkgKey(pkg))
current.value?.data.mods.includes(pkgKey(pkg))
);
const reload = async () => {
const p = (await invoke('get_current_profile')) as any;
if (p != null && 'OngekiProfile' in p) {
current.value = { ...p.OngekiProfile, game: 'ongeki' };
} else {
current.value = null;
}
const p = (await invoke('get_current_profile')) as Profile;
current.value = p;
if (current.value !== null) {
changePrimaryColor(current.value.game);
changePrimaryColor(current.value.meta.game);
} else {
changePrimaryColor(null);
}
@ -231,10 +221,10 @@ export const usePrfStore = defineStore('prf', () => {
});
if (
current.value?.game === profile.game &&
current.value.name === profile.name
current.value?.meta.game === profile.game &&
current.value.meta.name === profile.name
) {
current.value.name = name;
current.value.meta.name = name;
}
await reloadList();
@ -272,7 +262,7 @@ export const usePrfStore = defineStore('prf', () => {
const configDir = computed(async () => {
return await path.join(
generalStore.configDir,
`profile-${current.value?.game}-${current.value?.name}`
`profile-${current.value?.meta.game}-${current.value?.meta.name}`
);
});
@ -283,7 +273,7 @@ export const usePrfStore = defineStore('prf', () => {
watchEffect(async () => {
if (current.value !== null) {
await invoke('sync_current_profile', {
profile: { OngekiProfile: current.value },
data: current.value.data,
});
if (timeout !== null) {
clearTimeout(timeout);

View File

@ -24,11 +24,12 @@ export interface Package {
}
export enum Feature {
Mod = 0b00001,
Hook = 0b00010,
GameIO = 0b00100,
Aime = 0b01000,
AMNet = 0b10000,
Mod = 1 << 0,
Aime = 1 << 1,
AMNet = 1 << 2,
Mu3Hook = 1 << 3,
Mu3IO = 1 << 4,
ChusanHook = 1 << 5,
}
export type Status =
@ -45,6 +46,14 @@ export interface ProfileMeta {
name: string;
}
export interface ProfileData {
mods: string[];
sgt: SegatoolsConfig;
display: DisplayConfig;
network: NetworkConfig;
bepinex: BepInExConfig;
}
export interface SegatoolsConfig {
target: string;
hook: string | null;
@ -83,12 +92,9 @@ export interface BepInExConfig {
console: boolean;
}
export interface Profile extends ProfileMeta {
mods: string[];
sgt: SegatoolsConfig;
display: DisplayConfig;
network: NetworkConfig;
bepinex: BepInExConfig;
export interface Profile {
meta: ProfileMeta;
data: ProfileData;
}
export type Module = 'sgt' | 'display' | 'network';