5 Commits

Author SHA1 Message Date
2304a78db4 chore: 0.21 changelog 2025-05-14 16:08:39 +00:00
81cf2cf413 fix: missing translation strings 2025-05-14 15:59:34 +00:00
45b06e4478 fix: zh -> zh-Hans 2025-05-14 15:58:11 +00:00
77f5b6cd5e add zh-cn support 2025-05-14 18:35:27 +08:00
67872bc4d1 fix: also add post scripts 2025-05-09 21:45:59 +00:00
16 changed files with 385 additions and 60 deletions

View File

@ -1,8 +1,17 @@
## 0.20.0
## 0.21.0
- Added user-customizable pre-launch scripts
- Added japanese localization
- Fixed the file editor not updating state properly
- Added simplified chinese localization (courtesy of Chilor)
## 0.20.1
- Added japanese localization (courtesy of SALEC)
- 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

View File

@ -255,7 +255,7 @@ impl Profile {
let mut game_builder;
let mut amd_builder;
let mut prelaunch = None;
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"))?;
@ -265,17 +265,6 @@ impl Profile {
{
game_builder = Command::new(sgt_dir.join(self.meta.game.inject_exe()));
amd_builder = Command::new("cmd.exe");
let prelaunch_path = self.config_dir().join("prelaunch.bat");
if prelaunch_path.exists() {
let mut c = Command::new("cmd");
c.args(["/C", "start"]);
c.raw_arg("\"STARTLINER Prelaunch\"");
c.args(["cmd", "/C"]);
c.arg(prelaunch_path);
log::debug!("Prelaunch: {:?}", c);
prelaunch = Some(c.spawn()?);
}
}
#[cfg(target_os = "linux")]
{
@ -284,14 +273,12 @@ impl Profile {
game_builder.arg(sgt_dir.join(self.meta.game.inject_exe()));
amd_builder.arg("cmd.exe");
}
let prelaunch_path = self.config_dir().join("prelaunch.sh");
if prelaunch_path.exists() {
prelaunch_builder = Some(Command::new("sh"));
c.arg(prelaunch_path);
log::debug!("Prelaunch: {:?}", c);
prelaunch = Some(c.spawn()?);
}
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(
@ -440,11 +427,11 @@ impl Profile {
util::pkill("amdaemon.exe").await;
}
if let Some(mut _child) = prelaunch {
if let Some(mut _child) = prescript {
#[cfg(target_os = "windows")]
{
// child.kill() doesn't work
util::pkill_title("STARTLINER Prelaunch").await;
util::pkill_title("STARTLINER launch script").await;
}
#[cfg(target_os = "linux")]
{
@ -452,6 +439,11 @@ impl Profile {
}
}
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

@ -220,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.20.0",
"version": "0.21.0",
"identifier": "zip.patafour.startliner",
"build": {
"beforeDevCommand": "bun run dev",

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

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

@ -60,7 +60,7 @@ prf.reload();
<AimeOptions />
<MiscOptions />
<OptionCategory
title="Extensions"
:title="t('cfg.extensions.title')"
v-if="prf.current?.meta.game === 'chunithm'"
>
<OptionRow :title="t('cfg.extensions.saekawa')">

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);
@ -211,6 +217,7 @@ const importPick = async () => {
{ title: 'English', value: 'en' },
{ title: '日本語', value: 'ja' },
{ title: 'Polski', value: 'pl' },
{ title: '简体中文', value: 'zh-Hans' },
]"
size="small"
option-label="title"

View File

@ -203,7 +203,7 @@ const tryStart = () => {
v-tooltip="disabledTooltip"
:disabled="disabledTooltip !== null"
icon="pi pi-play"
label="START"
:label="t('start.button.start')"
aria-label="start"
size="small"
class="m-2.5"

View File

@ -41,18 +41,33 @@ const prf = usePrfStore();
<FileEditor filename="segatools-base.ini" />
</OptionRow>
<OptionRow
:title="t('cfg.misc.prelaunch')"
:tooltip="t('cfg.misc.prelaunchTooltip')"
:title="t('cfg.misc.prescript')"
:tooltip="t('cfg.misc.prescriptTooltip')"
>
<FileEditor
v-if="extension === 'bat'"
filename="prelaunch.bat"
:defaultValue="`@echo off\n\nREM This script will be launched alongside the game\n`"
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="prelaunch.sh"
:defaultValue="`#!/bin/sh\n\n# This script will be launched alongside the game\n`"
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>

View File

@ -1,7 +1,7 @@
import en from './i18n/en';
import { createI18n } from 'vue-i18n';
export type Locale = 'en' | 'ja' | 'pl';
export type Locale = 'en' | 'ja' | 'pl' | 'zh-Hans';
const loadLocaleMessages = async (locale: Locale) => {
return (await import(`./i18n/${locale}.ts`)).default;
@ -13,7 +13,7 @@ const i18n = createI18n({
fallbackLocale: 'en',
warnHtmlInMessage: false,
warnHtmlMessage: false,
messages: { en, ja: {}, pl: {} },
messages: { en, ja: {}, pl: {}, 'zh-Hans': {} },
});
const setLocale = async (locale: Locale) => {

View File

@ -166,8 +166,11 @@ export default {
other: 'Other segatools options',
otherTooltip:
'Advanced or situational options not covered by STARTLINER',
prelaunch: 'Prelaunch script',
prelaunchTooltip: 'Optional script that runs before the game.',
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

@ -144,7 +144,7 @@ export default {
general: '一般',
builtIn: 'segatools内蔵エミュレーション',
targetTooltip:
'STARTLINERはそれ以外のクリーンなデータに解凍された実行可能ファイルを期待する。',
'STARTLINERはそれ以外のクリーンなデータにクラック実行可能ファイルを期待する。',
hooks: 'フック',
ioModules: 'IOモジュール',
ioModulesDesc: 'これは望ましい入力方法と一致するはずです。',
@ -206,8 +206,10 @@ export default {
intelTooltip: '代わりにamdaemonにパッチを当てることを推奨する。',
other: 'その他segatools設定',
otherTooltip: 'STARTLINERに含まれない上級者向けまたは状況別設定',
prelaunch: 'スタート前スクリプト',
prelaunchTooltip: 'ゲームの前に実行されるスクリプト',
prescript: '起動前のスクリプト',
prescriptTooltip: 'ゲームの前に実行されるスクリプト',
postscript: '終了後のスクリプト',
postscriptTooltip: 'ゲーム終了後に実行されるスクリプト',
},
extensions: {
title: 'エクステンション',

View File

@ -206,8 +206,11 @@ export default {
other: 'Inne opcje segatools',
otherTooltip:
'Zaawansowane lub sytuacyjne opcje, które nie są objęte przez STARTLINERA',
prelaunch: 'Skrypt przedstartowy',
prelaunchTooltip: 'Opcjonalny skrypt uruchamiany przed grą.',
prescript: 'Skrypt startowy',
prescriptTooltip: 'Opcjonalny skrypt uruchamiany przed grą.',
postscript: 'Skrypt końcowy',
postscriptTooltip:
'Opcjonalny skrypt uruchamiany po zakończeniu gry.',
},
extensions: {
title: 'Rozszerzenia',

256
src/i18n/zh-Hans.ts Normal file
View File

@ -0,0 +1,256 @@
export default {
ok: '确定',
cancel: '取消',
enable: '启用',
disable: '禁用',
default: '默认',
search: '搜索',
next: '下一个',
skip: '跳过',
close: '关闭',
by: '来自 {namespace}',
updateAll: '更新全部',
start: {
failed: '启动检查失败',
accept: '仍要运行',
error: {
package: 'Package缺失',
dependency: '依赖缺失',
tool: 'Tools缺失',
unknown: '未知错误',
},
tooltip: {
game: '需要指定游戏路径',
amfs: '需要指定amfs路径',
segatools: '需要segatools hook package',
},
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: 'Package作者',
basic: '基本信息',
name: '名称',
description: '介绍',
website: '网站',
type: 'Package类型',
rainy: '标准',
segatools: 'Segatools',
native: '原生',
games: '游戏',
packageFormat: 'Package格式规范',
},
store: {
installRecommended: '安装推荐的Packages',
installed: '显示已安装',
deprecated: '显示已弃用',
nsfw: '显示NSFW',
incompatible: 'STARTLINER暂不支持此Package.',
includeCategories: '包含类别',
excludeCategories: '排除类别',
},
pkglist: {
missing: '缺失',
local: '本地Packages',
namespace: '按名称',
type: '按类型',
category: '按类别',
standard: '标准MOD',
native: '原生MOD',
segatools: 'Segatools',
unsupported: '不支持',
exclusions: '排除项:',
},
patch: {
loading: '加载中...',
noneFound:
"未找到兼容的Patch. 请确认你使用的是已解密且未经修补的文件.",
forceLoad: '强制加载',
// Example patch name override
// 'standard-no-encryption': 'No encryption',
// 'standard-no-encryption-tooltip': 'Will also disable TLS',
// It is also possible to add a tooltip where there normally is none
// 'standard-maximum-tracks-tooltip': 'The number of tracks per credit',
// For more info check https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Translation-%26-Localization
},
cfg: {
afterRestart: '将在重新启动后应用',
hardware: '硬件',
segatools: {
general: '通用',
builtIn: 'Segatools内置模拟',
targetTooltip:
'STARTLINER希望将已解密的可执行文件放入其他干净的数据中.',
hooks: 'Hook',
ioModules: 'IO模块',
ioModulesDesc: '此处应符合您希望使用的输入方式.',
ioBuiltIn: 'Segatools内置(键盘)',
io4: '原生IO4',
installTooltip: '{thing} 可从Package Store下载.',
},
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: '加密狗号 (Keychip)',
subnet: '子网 (Subnet)',
addrSuffix: '地址后缀 (Address suffix)',
},
aime: {
type: 'Aime读卡器类型',
modules: 'AimeIO',
code: 'Aime卡号',
codeTooltip:
'仅适用于Segatools内置模拟或第三方AimeIO',
aimedb: '对实体卡使用AiMeDB',
aimedbTooltip:
'实体卡需要使用AiMeDB来解析真实卡号,如果您想要获取卡片背面印刷的真实卡号,请启用此选项.',
serialPort: '读卡器端口',
serialPortTooltip: `端口号可在 设备和打印机 或 设备管理器 中查看
对于AIC Pico,应选择AIME Port.`,
serverName: '服务器名称',
},
misc: {
title: '杂项',
intel: '修复Intel 10代及以上CPU存在的OpenSSL问题',
intelTooltip: '推荐使用该选项来代替给amdaemon打补丁.',
other: '其他segatools选项',
otherTooltip:
'未被STARTLINER覆盖的高级选项',
prescript: '启动脚本',
prescriptTooltip: '在游戏启动前运行的脚本.',
postscript: '结束脚本',
postscriptTooltip:
'在游戏进程结束后运行的脚本.',
},
extensions: {
title: '扩展',
bepInExConsole: 'BepInEx控制台',
audioMode: '音频模式',
audioTooltip:
'独占双声道音频需要MOD: 7EVENDAYSHOLIDAYS-ExclusiveAudio',
audioShared: '共享',
audio6Ch: '独占六声道',
audio2Ch: '独占双声道',
sampleRate: '采样率',
blacklist: '歌曲ID黑名单',
blacklistTooltip:
'黑名单中的歌曲分数不会被保存和上传',
bonusTracks: '解锁Bonus Track',
bonusTracksTooltip:
'禁用此选项可帮助你整理歌曲列表',
saekawa: 'Saekawa配置文件',
inohara: 'Inohara配置文件',
},
keyboard: {
title: '键盘',
tooltip:
'仅适用于Segatools内置(键盘)或兼容的第三方IO(如mu3io.NET)',
leverMode: '摇杆模式',
mouse: '鼠标',
irTooltip:
'当使用键盘游玩时,请只绑定ir1;其余的请设置为未绑定',
},
wine: {
prefix: 'Wine前缀',
runtime: 'Wine运行时',
},
startliner: {
offlineMode: '离线模式',
offlineModeTooltip: '禁用Package Store.',
autoUpdate: '自动更新',
verbose: '详细日志',
},
},
onboarding: {
or: '或',
backButton: '控制器背后的按钮',
standard: `
你可能会卡在这个界面:
{bigblack}Aグループの基準機から設定を取得{endbig}
在这种情况下, 你需要跳转到测试模式(Test), 在游戏设定中 {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}
## 玩得愉快
`,
},
};