6 Commits

Author SHA1 Message Date
67872bc4d1 fix: also add post scripts 2025-05-09 21:45:59 +00:00
469ba5f574 feat: add prelaunch scripts 2025-05-09 16:07:58 +00:00
8f05a04350 fix: file editor not updating state properly 2025-05-09 16:07:13 +00:00
4ddc54d528 feat: add japanese localization 2025-05-09 16:06:04 +00:00
c4d023ed43 fix: change the deep link domain 2025-05-01 16:48:24 +00:00
9b86af282e fix: update button enabling its package 2025-05-01 16:32:10 +00:00
21 changed files with 539 additions and 60 deletions

View File

@ -1,3 +1,23 @@
## 0.20.1
- Added japanese localization
- Added user-customizable scripts
- The launch script runs directly before the game, and is equivalent to adding lines above `start "AM Daemon" ...` in start.bat
- The end script runs after the game has closed for any reason, and is equivalent to adding lines below `taskkill /f /im amdaemon.exe ...` in start.bat
- This functionality is needed for cursed things such as brokenithm
- Fixed the file editor not updating its state properly
- Fixed diagnostic exports not exporting paths
- Fixed "Install recommended packages" not enabling the segatools hook
## 0.19.1
- Fixed the update button enabling the package
- Fixed deep URLs with rainycolor.org
## 0.19.0
- Added diagnostic exports
## 0.18.3
- Updated Rainycolor's domain・真

View File

@ -125,12 +125,13 @@ pub async fn kill() -> Result<(), String> {
pub async fn install_package(
state: State<'_, tokio::sync::Mutex<AppData>>,
key: PkgKey,
force: bool
force: bool,
enable: bool
) -> Result<InstallResult, String> {
log::debug!("invoke: install_package({})", key);
let mut appd = state.lock().await;
appd.pkgs.install_package(&key, force, true)
appd.pkgs.install_package(&key, force, true, enable)
.await
.map_err(|e| e.to_string())
}
@ -532,14 +533,14 @@ pub async fn clear_cache(state: State<'_, Mutex<AppData>>) -> Result<(), String>
}
#[tauri::command]
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
pub fn list_platform_capabilities() -> Result<Vec<String>, ()> {
log::debug!("invoke: list_platform_capabilities");
#[cfg(target_os = "windows")]
return Ok(vec!["display".to_owned(), "shortcut".to_owned(), "chunithm".to_owned()]);
return Ok(vec!["display".to_owned(), "shortcut".to_owned(), "chunithm".to_owned(), "preload-bat".to_owned()]);
#[cfg(target_os = "linux")]
return Ok(vec!["wine".to_owned()]);
return Ok(vec!["wine".to_owned(), "preload-sh".to_owned()]);
}
#[tauri::command]

View File

@ -17,6 +17,12 @@ pub struct DownloadTick {
ratio: f32,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DownloadEndPayload {
pub key: PkgKey,
pub enable: bool
}
impl DownloadHandler {
pub fn new(app: AppHandle) -> DownloadHandler {
DownloadHandler {
@ -25,7 +31,7 @@ impl DownloadHandler {
}
}
pub fn download_zip(&mut self, zip_path: &PathBuf, pkg: &Package) -> Result<()> {
pub fn download_zip(&mut self, zip_path: &PathBuf, pkg: &Package, enable: bool) -> Result<()> {
let rmt = pkg.rmt.as_ref()
.ok_or_else(|| anyhow!("Attempted to download a package without remote data"))?
.clone();
@ -34,12 +40,18 @@ impl DownloadHandler {
} else {
// TODO clear cache button should clear this
self.paths.insert(zip_path.clone());
tauri::async_runtime::spawn(Self::download_zip_proc(self.app.clone(), zip_path.clone(), pkg.key(), rmt));
tauri::async_runtime::spawn(Self::download_zip_proc(self.app.clone(), zip_path.clone(), pkg.key(), rmt, enable));
Ok(())
}
}
async fn download_zip_proc(app: AppHandle, zip_path: PathBuf, pkg_key: PkgKey, rmt: Remote) -> Result<()> {
async fn download_zip_proc(
app: AppHandle,
zip_path: PathBuf,
pkg_key: PkgKey,
rmt: Remote,
enable: bool
) -> Result<()> {
use futures::StreamExt;
use tokio::io::AsyncWriteExt;
@ -66,7 +78,10 @@ impl DownloadHandler {
log::debug!("downloaded to {:?}", zip_path);
app.emit("download-end", pkg_key)?;
app.emit("download-end", DownloadEndPayload {
key: pkg_key,
enable
})?;
Ok(())
}

View File

@ -102,16 +102,23 @@ pub async fn run(_args: Vec<String>) {
});
app.listen("download-end", closure!(clone apph, |ev| {
let raw = ev.payload();
log::debug!("download-end triggered: {}", raw);
let key = PkgKey(raw[1..raw.len()-1].to_owned());
let apph = apph.clone();
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
let res = appd.pkgs.install_package(&key, true, false).await;
log::debug!("download-end install {:?}", res);
});
let payload = serde_json::from_str::<download_handler::DownloadEndPayload>(ev.payload());
match payload {
Ok(payload) => {
log::debug!("download-end triggered: {:?}", payload);
let key = payload.key;
let apph = apph.clone();
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
let res = appd.pkgs.install_package(&key, true, false, payload.enable).await;
log::debug!("download-end install {:?}", res);
});
},
Err(err) => {
log::error!("invalid download payload: {err}");
}
}
}));
app.listen("launch-end", closure!(clone apph, |_| {
@ -134,11 +141,13 @@ pub async fn run(_args: Vec<String>) {
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
let res = appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf);
log::debug!(
"install-end-prelude toggle {:?}",
res
);
if payload.enable == true {
let res = appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf);
log::debug!(
"install-end-prelude toggle {:?}",
res
);
}
use tauri::Emitter;
let res = apph.emit("install-end", payload);
log::debug!("install-end {:?}", res);
@ -261,7 +270,7 @@ fn deep_link(app: AppHandle, args: Vec<String>) {
log::info!("deep link: {}", url);
let regex = regex::Regex::new(
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
r"rainycolor://v1/install/www\.rainycolor\.org/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
).expect("Invalid regex");
if let Some(caps) = regex.captures(url) {
@ -273,11 +282,13 @@ fn deep_link(app: AppHandle, args: Vec<String>) {
let mut appd = mutex.lock().await;
if appd.pkgs.is_offline() {
log::warn!("Deep link installation failed: offline");
} else if let Err(e) = appd.pkgs.install_package(&key, true, true).await {
} else if let Err(e) = appd.pkgs.install_package(&key, true, true, true).await {
log::warn!("Deep link installation failed: {}", e.to_string());
}
});
}
} else {
log::error!("No caps");
}
}
}

View File

@ -23,7 +23,8 @@ pub struct PackageStore {
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Payload {
pub pkg: PkgKey
pub pkg: PkgKey,
pub enable: bool,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
@ -180,7 +181,13 @@ impl PackageStore {
self.offline = false;
}
pub async fn install_package(&mut self, key: &PkgKey, force: bool, install_deps: bool) -> Result<InstallResult> {
pub async fn install_package(
&mut self,
key: &PkgKey,
force: bool,
install_deps: bool,
enable: bool
) -> Result<InstallResult> {
log::info!("installation request: {}/{}/{}", key, force, install_deps);
let pkg = self.store.get(key)
@ -193,7 +200,8 @@ impl PackageStore {
}
self.app.emit("install-start", Payload {
pkg: key.to_owned()
pkg: key.to_owned(),
enable
})?;
let rmt = pkg.rmt.as_ref()
@ -203,7 +211,7 @@ impl PackageStore {
let mut set = HashSet::new();
self.resolve_deps(rmt.clone(), &mut set)?;
for dep in set {
Box::pin(self.install_package(&dep, false, false)).await?;
Box::pin(self.install_package(&dep, false, false, enable)).await?;
}
}
@ -214,7 +222,7 @@ impl PackageStore {
let part_path = zip_path.join(".part");
if !zip_path.exists() && !part_path.exists() {
self.dlh.download_zip(&zip_path, &pkg)?;
self.dlh.download_zip(&zip_path, &pkg, enable)?;
log::debug!("deferring {}", key);
return Ok(InstallResult::Deferred);
}
@ -230,7 +238,8 @@ impl PackageStore {
self.reload_package(key.to_owned()).await;
self.app.emit("install-end-prelude", Payload {
pkg: key.to_owned()
pkg: key.to_owned(),
enable
})?;
log::info!("installed {}", key);
@ -252,7 +261,8 @@ impl PackageStore {
if rv.is_ok() {
self.app.emit("install-end-prelude", Payload {
pkg: key.to_owned()
pkg: key.to_owned(),
enable: false
})?;
log::info!("deleted {}", key);
}

View File

@ -255,6 +255,7 @@ impl Profile {
let mut game_builder;
let mut amd_builder;
let mut prescript = None;
let target_path = PathBuf::from(&self.data.sgt.target);
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
@ -274,6 +275,12 @@ impl Profile {
amd_builder.arg("cmd.exe");
}
let script_ext = if cfg!(target_os = "windows") { "bat" } else { "sh" };
let prescript_path = self.config_dir().join(format!("pre.{script_ext}"));
if prescript_path.exists() {
prescript = util::spawn_script(prescript_path, &exe_dir, "\"STARTLINER launch script\"");
}
amd_builder.env(
"SEGATOOLS_CONFIG_PATH",
&ini_path,
@ -420,6 +427,23 @@ impl Profile {
util::pkill("amdaemon.exe").await;
}
if let Some(mut _child) = prescript {
#[cfg(target_os = "windows")]
{
// child.kill() doesn't work
util::pkill_title("STARTLINER launch script").await;
}
#[cfg(target_os = "linux")]
{
_child.start_kill()?;
}
}
let postscript_path = self.config_dir().join(format!("post.{script_ext}"));
if postscript_path.exists() {
_ = util::spawn_script(postscript_path, &exe_dir, "\"STARTLINER end script\"");
}
set.join_next().await.expect("No spawn").expect("No result");
log::debug!("Fin");

View File

@ -49,15 +49,17 @@ impl Profile {
{
let sgt = &mut prf.data.sgt;
sgt.target = PathBuf::new();
if sgt.amfs.is_absolute() {
sgt.amfs = PathBuf::new();
}
if sgt.option.is_absolute() {
sgt.option = PathBuf::new();
}
if sgt.appdata.is_absolute() {
sgt.appdata = PathBuf::new();
if !is_diagnostic {
sgt.target = PathBuf::new();
if sgt.amfs.is_absolute() {
sgt.amfs = PathBuf::new();
}
if sgt.option.is_absolute() {
sgt.option = PathBuf::new();
}
if sgt.appdata.is_absolute() {
sgt.appdata = PathBuf::new();
}
}
}

View File

@ -105,6 +105,12 @@ pub async fn pkill(process_name: &str) {
.creation_flags(CREATE_NO_WINDOW).output().await;
}
#[cfg(target_os = "windows")]
pub async fn pkill_title(window_title: &str) {
_ = Command::new("taskkill.exe").arg("/fi").raw_arg(format!("\"WindowTitle eq {window_title}\""))
.creation_flags(CREATE_NO_WINDOW).output().await;
}
#[cfg(target_os = "linux")]
pub async fn pkill(process_name: &str) {
_ = Command::new("pkill").arg(process_name)
@ -214,4 +220,38 @@ pub fn create_shortcut(
file.Save(Some(lnk_path.to_str().ok_or_else(|| anyhow!("Illegal shortcut path"))?), true)?;
Ok(())
}
pub fn spawn_script(path: impl AsRef<Path>, cwd: impl AsRef<Path>, title: &str) -> Option<tokio::process::Child> {
// Seems? like this autism is needed to:
// 1. pop up a cmd window
// 2. launch the batch
// 3. die afterwards
let mut c;
#[cfg(target_os = "windows")]
{
c = Command::new("cmd");
c.args(["/C", "start"]);
c.raw_arg(title);
c.args(["cmd", "/C"]);
c.arg(path.as_ref());
c.current_dir(cwd);
}
#[cfg(target_os = "linux")]
{
c = Command::new("sh");
c.arg(path.as_ref());
c.current_dir(cwd);
}
log::debug!("Script launch: {:?}", c);
match c.spawn() {
Ok(child) => Some(child),
Err(e) => {
log::error!("unable to launch {:?}: {e}", path.as_ref());
None
}
}
}

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "STARTLINER",
"version": "0.19.0",
"version": "0.20.1",
"identifier": "zip.patafour.startliner",
"build": {
"beforeDevCommand": "bun run dev",

View File

@ -295,7 +295,7 @@ listen<DownloadingStatus>('download-progress', (event) => {
pkg.hasAvailableUpdates
"
icon="pi pi-download"
label="UPDATE ALL"
:label="t('updateAll')"
size="small"
class="mr-4 m-2.5"
@click="pkg.updateAll()"

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import Button from 'primevue/button';
import * as path from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
@ -12,6 +12,7 @@ const props = defineProps({
filename: String,
promptname: String,
extension: String,
defaultValue: String,
});
const exists = ref(false);
@ -35,6 +36,12 @@ const save = async () => {
};
const filePick = async () => {
if (props.defaultValue !== undefined) {
contents.value = props.defaultValue;
exists.value = true;
await save();
return;
}
const p = await open({
multiple: false,
directory: false,
@ -54,13 +61,13 @@ const filePick = async () => {
}
};
(async () => {
onMounted(async () => {
if (props.filename === undefined) {
throw new Error('FileEditor without a filename');
}
target_path.value = await path.join(await prf.configDir, props.filename);
await load(target_path.value);
})();
});
</script>
<template>

View File

@ -12,8 +12,8 @@ invoke('get_changelog').then((s) => (changelog.value = s as string));
<template>
<h1>About</h1>
STARTLINER is a launcher, configuration tool and mod manager for
O.N.G.E.K.I. and CHUNITHM.
STARTLINER is a configuration tool, mod manager and start.bat automation
engine for O.N.G.E.K.I. and CHUNITHM.
<h1>Changelog</h1>
<ScrollPanel style="height: 200px">
<div class="markdown">

View File

@ -53,6 +53,6 @@ const remove = async () => {
class="self-center ml-4"
style="width: 2rem; height: 2rem"
:loading="pkg?.js.downloading"
v-on:click="async () => await pkgs.install(pkg)"
v-on:click="async () => await pkgs.install(pkg, true)"
/>
</template>

View File

@ -71,10 +71,12 @@ const recommendedTooltip = computed(() => {
const installRecommended = () => {
if (prf.current?.meta.game === 'ongeki') {
pkgs.installFromKey('segatools-mu3hook');
prf.current.data.sgt.hook = 'segatools-mu3hook';
}
if (prf.current?.meta.game === 'chunithm') {
pkgs.installFromKey('segatools-chusanhook');
pkgs.installFromKey('mempatcher-mempatcher');
prf.current.data.sgt.hook = 'segatools-chusanhook';
}
};
</script>

View File

@ -29,10 +29,17 @@ const files = new Set<string>();
).includes('chunithm');
})();
const fileList = {
ongeki: ['aime.txt', 'inohara.cfg', 'mu3.ini', 'segatools-base.ini'],
chunithm: ['aime.txt', 'saekawa.toml', 'segatools-base.ini'],
};
const fileList = [
'aime.txt',
'inohara.cfg',
'saekawa.toml',
'mu3.ini',
'segatools-base.ini',
'pre.sh',
'pre.bat',
'post.sh',
'post.bat',
];
const diagnosticList = {
ongeki: ['mu3.ini', 'segatools-base.ini'],
@ -62,8 +69,7 @@ const fileListCurrent: Ref<string[]> = ref([]);
const recalcFileList = async () => {
const res: string[] = [];
files.clear();
for (const idx in fileList[prf.current!.meta.game]) {
const f = fileList[prf.current!.meta.game][idx];
for (const f of fileList) {
const p = await path.join(await prf.configDir, f);
if (await invoke('file_exists', { path: p })) {
res.push(f);
@ -209,7 +215,7 @@ const importPick = async () => {
style="width: 200px"
:options="[
{ title: 'English', value: 'en' },
// { title: '日本語', value: 'ja' },
{ title: '日本語', value: 'ja' },
{ title: 'Polski', value: 'pl' },
]"
size="small"

View File

@ -17,6 +17,7 @@ const install = async () => {
await invoke('install_package', {
key: pkgKey(props.pkg),
force: true,
enable: false,
});
} catch (err) {
if (props.pkg !== undefined) {

View File

@ -1,13 +1,27 @@
<script setup lang="ts">
import { ref } from 'vue';
import ToggleSwitch from 'primevue/toggleswitch';
import FileEditor from '../FileEditor.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { invoke } from '../../invoke';
import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const extension = ref('');
invoke('list_platform_capabilities').then(async (v: unknown) => {
if (Array.isArray(v)) {
if (v.includes('preload-sh')) {
extension.value = 'sh';
} else if (v.includes('preload-bat')) {
extension.value = 'bat';
}
}
});
const prf = usePrfStore();
</script>
@ -26,5 +40,35 @@ const prf = usePrfStore();
<!-- <Button icon="pi pi-refresh" size="small" /> -->
<FileEditor filename="segatools-base.ini" />
</OptionRow>
<OptionRow
:title="t('cfg.misc.prescript')"
:tooltip="t('cfg.misc.prescriptTooltip')"
>
<FileEditor
v-if="extension === 'bat'"
filename="pre.bat"
:defaultValue="`@echo off\n\nREM This script will launch before (and alongside) the game\n`"
/>
<FileEditor
v-else-if="extension === 'sh'"
filename="pre.sh"
:defaultValue="`#!/bin/sh\n\n# This script will launch before (and alongside) the game\n`"
/>
</OptionRow>
<OptionRow
:title="t('cfg.misc.postscript')"
:tooltip="t('cfg.misc.postscriptTooltip')"
>
<FileEditor
v-if="extension === 'bat'"
filename="post.bat"
:defaultValue="`@echo off\n\nREM This script will launch after the game has died\n`"
/>
<FileEditor
v-else-if="extension === 'sh'"
filename="post.sh"
:defaultValue="`#!/bin/sh\n\n# This script will launch after the game has died\n`"
/>
</OptionRow>
</OptionCategory>
</template>

View File

@ -9,6 +9,7 @@ export default {
skip: 'Skip',
close: 'Close',
by: 'by {namespace}',
updateAll: 'UPDATE ALL',
start: {
failed: 'Start check failed',
accept: 'Run anyway',
@ -165,6 +166,11 @@ export default {
other: 'Other segatools options',
otherTooltip:
'Advanced or situational options not covered by STARTLINER',
prescript: 'Launch script',
prescriptTooltip: 'Optional script that runs before the game.',
postscript: 'End script',
postscriptTooltip:
'Optional script that runs after the game has ended.',
},
extensions: {
title: 'Extensions',

View File

@ -1,14 +1,295 @@
export default {
ok: 'OK',
cancel: 'キャンセル',
enable: '有効にする',
disable: '無効にする',
default: 'デフォルト',
search: '探索',
next: '次',
skip: 'スキップ',
close: '閉じる',
by: '{namespace}製',
updateAll: 'すべてを更新',
start: {
failed: '起動チェックに失敗',
accept: 'とにかく実行',
error: {
package: 'パッケージが見つからない',
dependency: '依存関係が見つからない',
tool: 'ツールが見つからない',
unknown: '不明エラー',
},
tooltip: {
game: 'ゲームパスを指定する必要があります',
amfs: 'amfsパスを指定する必要があります',
segatools: 'segatoolsフックパッケージが必要です',
},
button: {
start: '起動',
stop: '停止',
unchecked: 'チェックをスキップして起動',
shortcut: 'デスクトップショートカットを作成',
help: 'ヘルプ',
refresh: 'MODを再適用して起動',
cache: 'MODキャッシュのクリア',
},
},
game: {
ongeki: 'オンゲキ',
chunithm: 'チュウニズム',
},
profile: {
welcome: 'STARTLINERへようこそ! プロフィルの作成から始めよう。',
create: '{game}のプロフィル',
delete: 'プロフィル削除',
reallyDelete: '本当に{profile}を削除しますか?',
template: 'STARTLINERのテンプレート',
importTemplate: 'テンプレートのインポート',
exportTemplate: 'プロフィルのエクスポート',
export: 'エクスポート',
standardExport: 'テンプレート',
diagnostic: '診断',
},
creator: {
header: 'パッケージ製作者',
basic: '基本情報',
name: '名',
description: '説明',
website: 'ウェブサイト',
type: 'パッケージタイプ',
rainy: 'スタンダード',
segatools: 'segatools',
native: 'ネイティブ',
games: 'ゲーム',
packageFormat: 'パッケージフォーマット仕様',
},
store: {
installRecommended: 'おすすめパッケージのインストール',
installed: 'インストールされているの表示',
deprecated: '非推奨の表示',
nsfw: 'NSFWの表示',
incompatible: 'このパッケージは現在STARTLINERと互換性がありません。',
includeCategories: 'カテゴリーを含む',
excludeCategories: 'カテゴリーを除く',
},
pkglist: {
missing: '行方不明',
local: 'ローカルパッケージ',
namespace: '名前空間順',
type: 'タイプ順',
category: 'カテゴリ順',
standard: 'スタンダードMOD',
native: 'ネイティブMOD',
segatools: 'segatools',
unsupported: '未対応',
exclusions: '除外:',
},
patch: {
loading: 'ロード中...',
noneFound:
'互換性のあるパッチが見つかりません。パッチが適用されていないファイルを使用していることを確認してください。',
forceLoad: '強制ロード',
'standard-shared-audio':
'共有オーディオモードを強制、システムオーディオサンプルレートは48000Hzでなければなりません',
'standard-shared-audio-tooltip':
'互換性は向上するが、待ち時間が増える可能性があります',
'standard-2ch': '2チャンネルオーディオ出力を強制',
'standard-2ch-tooltip': '低音過負荷の可能性',
'standard-song-timer': '音楽選択タイマーを無効にする',
'standard-map-timer': 'マップ選択タイマー',
'standard-map-timer-tooltip':
'負に設定すると、タイマーは968値となる968-1967',
'standard-ticket-timer': 'チケット選択タイマー',
'standard-ticket-timer-tooltip':
'負に設定すると、タイマーは968値となる968-1967',
'standard-course-timer': 'コース選択タイマー',
'standard-course-timer-tooltip':
'負に設定すると、タイマーは968値となる968-1967',
'standard-unlimited-tracks': '最大トラック数無制限',
'standard-unlimited-tracks-tooltip':
'1クレジットにつき7曲以上再生する場合はチェックが必要',
'standard-maximum-tracks': '最大トラック数',
'standard-no-encryption': '暗号化無し',
'standard-no-encryption-tooltip': 'TLSも無効にする',
'standard-no-tls': 'TLS無し',
'standard-no-tls-tooltip': 'タイトルサーバーの回避策',
'standard-head-to-head': 'ヘッド・トゥ・ヘッドパッチ',
'standard-head-to-head-tooltip':
'ヘッド・トゥ・ヘッドプレイに接続しようとする際に、無限に同期しないことがあった問題を修正',
'standard-bypass-1080p': '1080pモニターチェックのバイパス',
'standard-bypass-120hz': '120hzモニターチェックのバイパス',
'standard-force-free-play-text': 'FREE PLAYクレジットテキストを強制',
'standard-force-free-play-text-tooltip':
'クレジット数をFREE PLAYに置き換える',
'standard-custom-free-play-length': 'カスタムFREE PLAYテキストの長さ',
'standard-custom-free-play-length-tooltip':
'強制FREE PLAYクレジットテキストが有効な場合に表示されるテキストの長さを変更します。',
'standard-custom-free-play-text': 'カスタムFREE PLAYテキスト',
'standard-custom-free-play-text-tooltip':
'無限クレジットを使用する場合、FREE PLAYのテキストを置き換える。',
'standard-localhost':
'ネットワークサーバーとして127.0.0.1/localhostを許可',
'standard-credit-freeze': 'クレジットフリーズ ',
'standard-credit-freeze-tooltip':
'クレジットの使用を防ぎます。ゲームを開始したり、プレミアムチケットを購入したりするには、少なくとも1つのクレジットが使用可能でなければならない。',
'standard-openssl-fix': 'OpenSSL SHAクラッシュのバグ修正',
'standard-openssl-fix-tooltip':
'第10世代以降のインテルCPUのクラッシュを修正',
},
cfg: {
afterRestart: '再起動後に適用',
hardware: 'ハードウェア',
segatools: {
general: '一般',
builtIn: 'segatools内蔵エミュレーション',
targetTooltip:
'STARTLINERはそれ以外のクリーンなデータにクラック実行可能ファイルを期待する。',
hooks: 'フック',
ioModules: 'IOモジュール',
ioModulesDesc: 'これは望ましい入力方法と一致するはずです。',
ioBuiltIn: 'segatools内蔵キーボードー',
io4: 'ネイティブIO4',
installTooltip:
'{thing}はパッケージストアからダウンロードできます。',
},
display: {
title: 'ディスプレイ',
resolution: 'ゲームの解像度',
primary: 'メイン',
target: 'ディスプレイ',
mode: 'モード',
rotation: '画面の向き',
refreshRate: 'リフレッシュレート',
borderlessFullscreen: 'ボーダレスフルスクリーン',
borderlessFullscreenTooltip:
'ディスプレイの解像度をゲームに合わせる。',
dontSwitchPrimary: '主ディスプレイの切り替えをスキップする',
dontSwitchPrimaryTooltip:
'プライマリディスプレイを切り替えると問題が発生する場合のみ、このオプションを有効にしてください。モニターのリフレッシュレートが一致している必要があります。',
index: 'ディスプレイインデックス',
portrait: '縦',
landscape: '横',
flipped: '反対向き',
window: 'ウィンドウ',
borderless: 'ボーダレス',
fullscreen: 'フルスクリーン',
},
network: {
title: 'ネットワーク',
type: 'ネットワークタイプ',
remote: 'リモート',
localArtemis: 'ローカルARTEMiS',
artemisPath: 'ARTEMiSパス',
address: 'サーバーアドレス',
keychip: 'キーチップ',
subnet: 'サブネット',
addrSuffix: 'アドレスサフィックス',
},
aime: {
type: 'Aimeタイプ',
modules: 'Aimeモジュール ',
code: 'アクセスコード',
codeTooltip:
'segatools内蔵エミュレーションまたは互換性のあるサードパーティ製パッケージでのみ使用可能。',
aimedb: '物理カードにはAiMeDBを使う',
aimedbTooltip:
'物理カードがアクセスコードを取得するためにAiMeDBを使用するかどうか。ゲームがホストされたネットワークを使用している場合、このオプションを有効にすると、物理筐体で取得するのと同じアカウントデータ/プロフィルがロードされます。',
serialPort: 'Aimeシリアルポート',
serialPortTooltip: `ポートはデバイスとプリンター、またはgooglechromelabs.github.io/serial-terminalで確認できます
AIC Picoの場合は、AIMEポートを選択する。`,
serverName: 'サーバー名',
},
misc: {
title: 'その他',
intel: '第10世代以降インテル向けOpenSSLバグの回避策',
intelTooltip: '代わりにamdaemonにパッチを当てることを推奨する。',
other: 'その他segatools設定',
otherTooltip: 'STARTLINERに含まれない上級者向けまたは状況別設定',
prescript: '起動前のスクリプト',
prescriptTooltip: 'ゲームの前に実行されるスクリプト',
postscript: '終了後のスクリプト',
postscriptTooltip: 'ゲーム終了後に実行されるスクリプト',
},
extensions: {
title: 'エクステンション',
bepInExConsole: 'BepInExコンソール',
audioMode: 'オーディオモード',
audioTooltip:
'排他2チャンネルモードには7EVENDAYSHOLIDAYS-ExclusiveAudioが必要です',
audioShared: '共有',
audio6Ch: '排他6チャンネル',
audio2Ch: '排他2チャンネル',
sampleRate: 'サンプルレート',
blacklist: '曲IDブラックリスト',
blacklistTooltip:
'このID範囲内の譜面のスコアは保存もアップロードもされない',
bonusTracks: 'ボーナストラックのアンロック',
bonusTracksTooltip:
'このオプションを無効にすると、曲リストが整理されます',
saekawa: 'Saekawa設定ファイル',
inohara: 'Inohara設定ファイル',
},
keyboard: {
title: 'キーボード',
tooltip:
'IOモジュールがsegatools内蔵キーボードまたは互換性のあるサードパーティモジュールmu3io.NETなどに設定されている場合のみ適用可能。',
leverMode: 'レバーモード',
mouse: 'マウス',
irTooltip:
'実際のキーボードで演奏する場合は、ir1だけをバインドし、残りはバインドしない。',
},
wine: {
prefix: 'Wineのプレフィックス',
runtime: 'Wineのランタイム',
},
startliner: {
offlineMode: 'オフラインモード',
offlineModeTooltip: 'パッケージストアを無効にする。',
autoUpdate: '自動更新',
verbose: '詳細ログ',
},
},
onboarding: {
or: 'または',
backButton: 'コントローラー背面のボタン',
standard: `
以下のような画面に引っかかるかもしれません:
{bigblack}Aグループの基準機から設定を取得{endbig}
その場合、テストメニューに移動し、{black}ゲーム設定{end}の{black}基準に従う{end}から{black}基準機{end}に切り替える必要があります。
テストメニューは%TESTMENU%でアクセスできます。
`,
'ongeki-system-processing': `
この画面が数分間引っかかるかもしれません。_これは普通のことです_。データのロードに時間がかかるだけです。
<code>7EVENDAYSHOLIDAYS/LoadBoost</code>をインストールすれば、それ以降の起動はずっと速くなります。
`,
'ongeki-lever': `
レバーのキャリブレーションを行わないと、3301エラーが発生する可能性があります。
{black}レバー設定{end}に移動し、レバーを両端に移動させ、{black}終了{end}を押してから{black}保存する{end}を押します。
`,
'chunithm-server': `
この画面に引っかかる場合は、ゲームを再起動してください。
問題が解決しない場合は、{link}ネットワーク設定を確認してください{endlink}
`,
finale: `
STARTボタンを右クリックすれば、いつでもこのページにアクセスできます。
その他のリソース:
- {segaguide}SEGAguide{endlink}
- {twotorial}two-torial{endlink}
## 楽しもう!
`,
},
};

View File

@ -9,6 +9,7 @@ export default {
skip: 'Pomiń',
close: 'Zamknij',
by: 'od {namespace}',
updateAll: 'ZAKTUALIZUJ WSZYSTKO',
start: {
failed: 'Uruchomienie nie powiodło się',
accept: 'Uruchom mimo to',
@ -205,6 +206,11 @@ export default {
other: 'Inne opcje segatools',
otherTooltip:
'Zaawansowane lub sytuacyjne opcje, które nie są objęte przez STARTLINERA',
prescript: 'Skrypt startowy',
prescriptTooltip: 'Opcjonalny skrypt uruchamiany przed grą.',
postscript: 'Skrypt końcowy',
postscriptTooltip:
'Opcjonalny skrypt uruchamiany po zakończeniu gry.',
},
extensions: {
title: 'Rozszerzenia',

View File

@ -189,7 +189,7 @@ export const usePkgStore = defineStore('pkg', {
await this.reloadAll();
},
async install(pkg: Package | undefined) {
async install(pkg: Package | undefined, enable: boolean) {
if (pkg === undefined) {
return;
}
@ -198,6 +198,7 @@ export const usePkgStore = defineStore('pkg', {
await invoke('install_package', {
key: pkgKey(pkg),
force: true,
enable,
});
} catch (err) {
if (pkg !== undefined) {
@ -211,6 +212,7 @@ export const usePkgStore = defineStore('pkg', {
await invoke('install_package', {
key,
force: true,
enable: false,
});
} catch (err) {
console.error(err);
@ -221,7 +223,7 @@ export const usePkgStore = defineStore('pkg', {
const list = [];
for (const pkg of this.allLocal) {
if (pkg.rmt && pkg.rmt.version > pkg.loc!.version) {
list.push(this.install(pkg));
list.push(this.install(pkg, false));
}
}
await Promise.all(list);
@ -322,9 +324,10 @@ export const usePrfStore = defineStore('prf', () => {
const generalStore = useGeneralStore();
const configDir = computed(async () => {
const title = `profile-${current.value?.meta.game}-${current.value?.meta.name}`;
return path.join(
await generalStore.configDir,
`profile-${current.value?.meta.game}-${current.value?.meta.name}`
title
);
});