16 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
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
2e17e0ae75 feat: diagnostic exports 2025-04-30 21:19:15 +00:00
edef5cc6dc fix: also replace download URLs 2025-04-30 07:35:54 +00:00
2dad0de4f1 fix: update rainycolor's domain 2025-04-30 06:59:38 +00:00
14a65eb5bb fix: keyboard unbinding and IR fixes 2025-04-29 19:59:21 +00:00
0add9200a6 feat: new grouping options 2025-04-28 22:00:33 +00:00
ee49da3665 fix: category sort 2025-04-28 16:47:45 +00:00
34 changed files with 1154 additions and 126 deletions

View File

@ -1,3 +1,44 @@
## 0.21.0
- 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
- 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・真
## 0.18.2
- Updated Rainycolor's domain
## 0.18.1
- Keys can now be unbinded with Esc
- Fixed CHUNITHM IR behavior on actual keyboards
## 0.18.0
- Added new grouping options to the package list
## 0.17.0 ## 0.17.0
- Added a package creation prompt - Added a package creation prompt

View File

@ -9,7 +9,7 @@ This is a program that seeks to streamline game data configuration, currently su
STARTLINER is four things: STARTLINER is four things:
- a mod installer and updater, powered by [Rainycolor Watercolor](https://rainy.patafour.zip), - a mod installer and updater, powered by [Rainycolor Watercolor](https://rainycolor.org),
- a configuration GUI for segatools, - a configuration GUI for segatools,
- a glorified `start.bat` clicker, with automatic monitor setup and rollback, - a glorified `start.bat` clicker, with automatic monitor setup and rollback,
- [an abstraction allowing data configuration without touching the game directory](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details). - [an abstraction allowing data configuration without touching the game directory](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details).

View File

@ -125,12 +125,13 @@ pub async fn kill() -> Result<(), String> {
pub async fn install_package( pub async fn install_package(
state: State<'_, tokio::sync::Mutex<AppData>>, state: State<'_, tokio::sync::Mutex<AppData>>,
key: PkgKey, key: PkgKey,
force: bool force: bool,
enable: bool
) -> Result<InstallResult, String> { ) -> Result<InstallResult, String> {
log::debug!("invoke: install_package({})", key); log::debug!("invoke: install_package({})", key);
let mut appd = state.lock().await; let mut appd = state.lock().await;
appd.pkgs.install_package(&key, force, true) appd.pkgs.install_package(&key, force, true, enable)
.await .await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
@ -480,13 +481,18 @@ pub async fn create_shortcut(_app: AppHandle, profile_meta: ProfileMeta) -> Resu
} }
#[tauri::command] #[tauri::command]
pub async fn export_profile(state: State<'_, Mutex<AppData>>, export_keychip: bool, files: Vec<String>) -> Result<(), String> { pub async fn export_profile(
state: State<'_, Mutex<AppData>>,
is_diagnostic: bool,
export_keychip: bool,
files: Vec<String>
) -> Result<(), String> {
log::debug!("invoke: export_profile({:?}, {:?} files)", export_keychip, files.len()); log::debug!("invoke: export_profile({:?}, {:?} files)", export_keychip, files.len());
let appd = state.lock().await; let appd = state.lock().await;
match &appd.profile { match &appd.profile {
Some(p) => { Some(p) => {
p.export(export_keychip, files) p.export(export_keychip, files, is_diagnostic)
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
} }
None => { None => {
@ -527,14 +533,14 @@ pub async fn clear_cache(state: State<'_, Mutex<AppData>>) -> Result<(), String>
} }
#[tauri::command] #[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"); log::debug!("invoke: list_platform_capabilities");
#[cfg(target_os = "windows")] #[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")] #[cfg(target_os = "linux")]
return Ok(vec!["wine".to_owned()]); return Ok(vec!["wine".to_owned(), "preload-sh".to_owned()]);
} }
#[tauri::command] #[tauri::command]

View File

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

View File

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

View File

@ -117,12 +117,16 @@ impl Keyboard {
} }
} }
Keyboard::Chunithm(kb) => { Keyboard::Chunithm(kb) => {
let mut enabled_ir = false;
if kb.enabled { if kb.enabled {
for (i, cell) in kb.cell.iter().enumerate() { for (i, cell) in kb.cell.iter().enumerate() {
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string()); ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string());
} }
for (i, ir) in kb.ir.iter().enumerate() { for (i, ir) in kb.ir.iter().enumerate() {
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), ir.to_string()); ini.with_section(Some("ir")).set(format!("ir{}", i + 1), (*ir).to_string());
if i > 0 && *ir != 0 {
enabled_ir = true;
}
} }
ini.with_section(Some("io3")) ini.with_section(Some("io3"))
.set("test", kb.test.to_string()) .set("test", kb.test.to_string())
@ -140,8 +144,13 @@ impl Keyboard {
.set("service", "0") .set("service", "0")
.set("coin", "0"); .set("coin", "0");
} }
ini.with_section(Some("io3")) if enabled_ir {
.set("ir", "0"); ini.with_section(Some("io3"))
.set("ir", "0");
} else {
ini.with_section(Some("io3"))
.set("ir", kb.ir[0].to_string());
}
} }
} }

View File

@ -29,6 +29,10 @@ impl PatchFileVec {
} }
pub fn find_patches(&self, target: impl AsRef<Path>) -> Result<Vec<Patch>> { pub fn find_patches(&self, target: impl AsRef<Path>) -> Result<Vec<Patch>> {
if !target.as_ref().exists() {
log::warn!("invalid target path: {:?}", target.as_ref());
anyhow::bail!("Unable to open {:?}. Make sure the game path is correct.", target.as_ref());
}
let checksum = try_digest(target.as_ref())?; let checksum = try_digest(target.as_ref())?;
let mut res_patches = Vec::new(); let mut res_patches = Vec::new();

View File

@ -109,7 +109,7 @@ impl Package {
loc: None, loc: None,
rmt: Some(Remote { rmt: Some(Remote {
package_url: p.package_url, package_url: p.package_url,
download_url: v.download_url, download_url: v.download_url.replace("https://rainy.patafour.zip/", "https://www.rainycolor.org/"),
icon: v.icon, icon: v.icon,
deprecated: p.is_deprecated, deprecated: p.is_deprecated,
nsfw: p.has_nsfw_content, nsfw: p.has_nsfw_content,

View File

@ -23,7 +23,8 @@ pub struct PackageStore {
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Payload { pub struct Payload {
pub pkg: PkgKey pub pkg: PkgKey,
pub enable: bool,
} }
#[derive(Clone, Copy, Serialize, Deserialize, Debug)] #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
@ -132,7 +133,7 @@ impl PackageStore {
prelude::*, prelude::*,
}; };
let response = reqwest::get(format!("https://rainy.patafour.zip/c/{game}/api/v1/package/")).await?; let response = reqwest::get(format!("https://www.rainycolor.org/c/{game}/api/v1/package/")).await?;
let reader = response let reader = response
.bytes_stream() .bytes_stream()
@ -180,7 +181,13 @@ impl PackageStore {
self.offline = false; 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); log::info!("installation request: {}/{}/{}", key, force, install_deps);
let pkg = self.store.get(key) let pkg = self.store.get(key)
@ -193,7 +200,8 @@ impl PackageStore {
} }
self.app.emit("install-start", Payload { self.app.emit("install-start", Payload {
pkg: key.to_owned() pkg: key.to_owned(),
enable
})?; })?;
let rmt = pkg.rmt.as_ref() let rmt = pkg.rmt.as_ref()
@ -203,7 +211,7 @@ impl PackageStore {
let mut set = HashSet::new(); let mut set = HashSet::new();
self.resolve_deps(rmt.clone(), &mut set)?; self.resolve_deps(rmt.clone(), &mut set)?;
for dep in 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"); let part_path = zip_path.join(".part");
if !zip_path.exists() && !part_path.exists() { 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); log::debug!("deferring {}", key);
return Ok(InstallResult::Deferred); return Ok(InstallResult::Deferred);
} }
@ -230,7 +238,8 @@ impl PackageStore {
self.reload_package(key.to_owned()).await; self.reload_package(key.to_owned()).await;
self.app.emit("install-end-prelude", Payload { self.app.emit("install-end-prelude", Payload {
pkg: key.to_owned() pkg: key.to_owned(),
enable
})?; })?;
log::info!("installed {}", key); log::info!("installed {}", key);
@ -252,7 +261,8 @@ impl PackageStore {
if rv.is_ok() { if rv.is_ok() {
self.app.emit("install-end-prelude", Payload { self.app.emit("install-end-prelude", Payload {
pkg: key.to_owned() pkg: key.to_owned(),
enable: false
})?; })?;
log::info!("deleted {}", key); log::info!("deleted {}", key);
} }

View File

@ -255,6 +255,7 @@ impl Profile {
let mut game_builder; let mut game_builder;
let mut amd_builder; let mut amd_builder;
let mut prescript = None;
let target_path = PathBuf::from(&self.data.sgt.target); let target_path = PathBuf::from(&self.data.sgt.target);
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?; let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
@ -274,6 +275,12 @@ impl Profile {
amd_builder.arg("cmd.exe"); 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( amd_builder.env(
"SEGATOOLS_CONFIG_PATH", "SEGATOOLS_CONFIG_PATH",
&ini_path, &ini_path,
@ -420,6 +427,23 @@ impl Profile {
util::pkill("amdaemon.exe").await; 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"); set.join_next().await.expect("No spawn").expect("No result");
log::debug!("Fin"); log::debug!("Fin");

View File

@ -36,7 +36,7 @@ impl Profile {
Ok(()) Ok(())
} }
pub fn export(&self, export_keychip: bool, extra_files: Vec<String>) -> anyhow::Result<()> { pub fn export(&self, export_keychip: bool, extra_files: Vec<String>, is_diagnostic: bool) -> anyhow::Result<()> {
let mut prf = self.clone(); let mut prf = self.clone();
let dir = util::config_dir().join("exports"); let dir = util::config_dir().join("exports");
@ -45,19 +45,21 @@ impl Profile {
std::fs::create_dir(&dir)?; std::fs::create_dir(&dir)?;
} }
let path = dir.join(format!("{}-{}-template.zip", &self.meta.game, &self.meta.name)); let path = dir.join(format!("{}-{}-{}.zip", &self.meta.game, &self.meta.name, if is_diagnostic { "diagnostic" } else { "template" } ));
{ {
let sgt = &mut prf.data.sgt; let sgt = &mut prf.data.sgt;
sgt.target = PathBuf::new(); if !is_diagnostic {
if sgt.amfs.is_absolute() { sgt.target = PathBuf::new();
sgt.amfs = PathBuf::new(); if sgt.amfs.is_absolute() {
} sgt.amfs = PathBuf::new();
if sgt.option.is_absolute() { }
sgt.option = PathBuf::new(); if sgt.option.is_absolute() {
} sgt.option = PathBuf::new();
if sgt.appdata.is_absolute() { }
sgt.appdata = PathBuf::new(); if sgt.appdata.is_absolute() {
sgt.appdata = PathBuf::new();
}
} }
} }
@ -66,7 +68,7 @@ impl Profile {
if network.local_path.is_absolute() { if network.local_path.is_absolute() {
network.local_path = PathBuf::new(); network.local_path = PathBuf::new();
} }
if !export_keychip { if !export_keychip || is_diagnostic {
network.keychip = String::new(); network.keychip = String::new();
} }
} }
@ -83,6 +85,29 @@ impl Profile {
zip.write_all(&std::fs::read(self.config_dir().join(file))?)?; zip.write_all(&std::fs::read(self.config_dir().join(file))?)?;
} }
if is_diagnostic {
let name = "mu3.exe.log";
let path = self.data_dir().join(name);
if path.exists() {
zip.start_file(name, options)?;
zip.write_all(&std::fs::read(path)?)?;
}
let name = "chusanApp.exe.log";
let path = self.data_dir().join(name);
if path.exists() {
zip.start_file(name, options)?;
zip.write_all(&std::fs::read(path)?)?;
}
let name = "amdaemon.exe.log";
let path = self.data_dir().join(name);
if path.exists() {
zip.start_file(name, options)?;
zip.write_all(&std::fs::read(path)?)?;
}
}
zip.finish()?; zip.finish()?;
Ok(()) Ok(())

View File

@ -105,6 +105,12 @@ pub async fn pkill(process_name: &str) {
.creation_flags(CREATE_NO_WINDOW).output().await; .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")] #[cfg(target_os = "linux")]
pub async fn pkill(process_name: &str) { pub async fn pkill(process_name: &str) {
_ = Command::new("pkill").arg(process_name) _ = 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)?; file.Save(Some(lnk_path.to_str().ok_or_else(|| anyhow!("Illegal shortcut path"))?), true)?;
Ok(()) 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", "$schema": "https://schema.tauri.app/config/2",
"productName": "STARTLINER", "productName": "STARTLINER",
"version": "0.17.0", "version": "0.21.0",
"identifier": "zip.patafour.startliner", "identifier": "zip.patafour.startliner",
"build": { "build": {
"beforeDevCommand": "bun run dev", "beforeDevCommand": "bun run dev",

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,9 @@ import InputText from 'primevue/inputtext';
import { fromKeycode, toKeycode } from '../keyboard'; import { fromKeycode, toKeycode } from '../keyboard';
import { usePrfStore } from '../stores'; import { usePrfStore } from '../stores';
import { OngekiButtons } from '../types'; import { OngekiButtons } from '../types';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore(); const prf = usePrfStore();
@ -61,6 +64,10 @@ const handleKey = (
} }
} }
if (event.code === 'Escape') {
keycode = 0;
}
if (index !== undefined) { if (index !== undefined) {
data[button][index] = keycode; data[button][index] = keycode;
} else { } else {
@ -160,13 +167,24 @@ const fontSize = computed(() => {
<InputText <InputText
:style="{ :style="{
width: small ? '2.8rem' : '5rem', width: small ? '2.8rem' : '5rem',
height: small ? '2.8rem' : tall ? '10rem' : '5rem', height:
small && tall
? '5rem'
: small
? '2.8rem'
: tall
? '10rem'
: '5rem',
fontSize, fontSize,
backgroundColor: color, backgroundColor: color,
}" }"
unstyled unstyled
class="text-center buttoninputtext" class="text-center buttoninputtext"
v-tooltip="tooltip ? `${tooltip}: ${modelValue}` : undefined" v-tooltip="
tooltip
? `${tooltip}: ${modelValue} ${tooltip.startsWith('ir') ? `\n${t('cfg.keyboard.irTooltip')}` : ''}`
: undefined
"
@contextmenu.prevent="() => {}" @contextmenu.prevent="() => {}"
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)" @keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
@mousedown=" @mousedown="

View File

@ -4,13 +4,15 @@ import Button from 'primevue/button';
import Dialog from 'primevue/dialog'; import Dialog from 'primevue/dialog';
import Fieldset from 'primevue/fieldset'; import Fieldset from 'primevue/fieldset';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import MultiSelect from 'primevue/multiselect';
import SelectButton from 'primevue/selectbutton'; import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch'; import ToggleSwitch from 'primevue/toggleswitch';
import { emit } from '@tauri-apps/api/event';
import ModListEntry from './ModListEntry.vue'; import ModListEntry from './ModListEntry.vue';
import ModTitlecard from './ModTitlecard.vue'; import ModTitlecard from './ModTitlecard.vue';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { useClientStore, usePkgStore, usePrfStore } from '../stores'; import { useClientStore, usePkgStore, usePrfStore } from '../stores';
import { Game, Package } from '../types'; import { Feature, Game, Package } from '../types';
import { pkgKey } from '../util'; import { pkgKey } from '../util';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -35,32 +37,105 @@ const loadPackages = () => {
loadPackages(); loadPackages();
const group = computed(() => { const allCategories = computed(() => {
const grouped = Object.groupBy( const res = new Set<string>();
pkgs.allLocal for (const pkg of pkgs.allLocal) {
.filter((p) => gameSublist.value.includes(pkgKey(p))) for (const cat of pkg.rmt?.categories ?? []) {
.filter( res.add(cat);
(p) => }
props.search === undefined ||
p.name.toLowerCase().includes(props.search.toLowerCase()) ||
p.namespace
.toLowerCase()
.includes(props.search.toLowerCase())
),
({ namespace }) => namespace
);
if (!('local' in grouped)) {
grouped['local'] = [];
} }
const res: [string, Package[]][] = []; return [...res.values()].sort((a, b) => a.localeCompare(b));
});
const local = computed(() => {
return pkgs.allLocal
.filter((p) => gameSublist.value.includes(pkgKey(p)))
.filter((p) => p.namespace === 'local');
});
const groupedList = computed(() => {
const searchedPkgs = pkgs.allLocal
.filter((p) => gameSublist.value.includes(pkgKey(p)))
.filter((p) => p.namespace !== 'local')
.filter(
(p) =>
props.search === undefined ||
p.name.toLowerCase().includes(props.search.toLowerCase()) ||
p.namespace.toLowerCase().includes(props.search.toLowerCase())
);
let grouped;
if (client.pkgListMode === 'namespace') {
grouped = Object.groupBy(searchedPkgs, ({ namespace }) => namespace);
} else if (client.pkgListMode === 'type') {
grouped = {
standard: [] as Package[],
native: [] as Package[],
segatools: [] as Package[],
unsupported: [] as Package[] | undefined,
};
grouped.unsupported = [];
for (const pkg of searchedPkgs) {
const loc = pkg.loc;
if (!loc || !loc.status || typeof loc.status === 'string') {
grouped.unsupported.push(pkg);
} else {
if (
loc.status.OK[0] &
(Feature.GameDLL | Feature.Mempatcher | Feature.AmdDLL)
) {
grouped.native.push(pkg);
} else if (loc.status.OK[0] & Feature.Mod) {
grouped.standard.push(pkg);
}
if (
loc.status.OK[0] &
(Feature.AMNet |
Feature.Aime |
Feature.ChuniIO |
Feature.ChusanHook |
Feature.Mu3IO |
Feature.Mu3Hook)
) {
grouped.segatools.push(pkg);
}
}
}
if (grouped.unsupported.length === 0) {
delete grouped.unsupported;
}
} else {
grouped = {} as { [key: string]: Package[] };
for (const pkg of searchedPkgs) {
for (const cat of pkg.rmt?.categories ?? []) {
if (client.hiddenCategories.includes(cat)) {
continue;
}
if (!(cat in grouped)) {
grouped[cat] = [] as Package[];
}
grouped[cat].push(pkg);
}
}
}
let res: [string, Package[]][] = [];
for (const [k, v] of Object.entries(grouped)) { for (const [k, v] of Object.entries(grouped)) {
if (v !== undefined) { if (v !== undefined) {
res.push([k, v]); res.push([k, v]);
} }
} }
res.sort((a, b) => {
return a[0] === 'local' ? -1000000 : `${a[0]}`.localeCompare(`${b[0]}`); if (
}); client.pkgListMode === 'namespace' ||
client.pkgListMode === 'category'
) {
res.sort((a, b) => `${a[0]}`.localeCompare(`${b[0]}`));
} else if (client.pkgListMode === 'type') {
for (const entry of res) {
entry[0] = t(`pkglist.${entry[0]}`);
}
}
return res; return res;
}); });
@ -195,7 +270,43 @@ const gameModelChunithm = gameModel('chunithm');
/> />
</div> </div>
</Dialog> </Dialog>
<Fieldset :legend="t('store.missing')" v-if="(missing?.length ?? 0) > 0"> <div class="flex flex-row">
<SelectButton
:options="[
{ title: t('pkglist.namespace'), value: 'namespace' },
{ title: t('pkglist.type'), value: 'type' },
{ title: t('pkglist.category'), value: 'category' },
]"
v-model="client.pkgListMode"
v-on:update:model-value="
client.save();
emit('reload-icons');
"
:allow-empty="false"
option-label="title"
option-value="value"
/>
<div
class="grow text-right mr-2 self-center"
v-if="client.pkgListMode === 'category'"
>
{{ t('pkglist.exclusions') }}
</div>
<MultiSelect
v-if="client.pkgListMode === 'category'"
style="width: 30%"
:showToggleAll="false"
v-model="client.hiddenCategories"
v-on:value-change="
client.save();
emit('reload-icons');
"
:options="allCategories"
class="w-full grow"
/>
</div>
<Fieldset :legend="t('pkglist.missing')" v-if="(missing?.length ?? 0) > 0">
<div class="flex items-center" v-for="p in missing"> <div class="flex items-center" v-for="p in missing">
<ModTitlecard <ModTitlecard
show-namespace show-namespace
@ -218,20 +329,21 @@ const gameModelChunithm = gameModel('chunithm');
/> />
</div> </div>
</Fieldset> </Fieldset>
<Fieldset v-for="[namespace, pkgs] in group" :legend="namespace"> <Fieldset :legend="t('pkglist.local')">
<ModListEntry v-for="p in local" :pkg="p" />
<Button
rounded
icon="pi pi-plus"
severity="success"
aria-label="install"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
v-on:click="() => (dialogVisible = true)"
/>
</Fieldset>
<Fieldset v-for="[namespace, pkgs] in groupedList" :legend="namespace">
<ModListEntry v-for="p in pkgs" :pkg="p" /> <ModListEntry v-for="p in pkgs" :pkg="p" />
<div v-if="namespace === 'local'">
<Button
rounded
icon="pi pi-plus"
severity="success"
aria-label="install"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
v-on:click="() => (dialogVisible = true)"
/>
</div>
</Fieldset> </Fieldset>
</template> </template>

View File

@ -7,7 +7,7 @@ import LinkButton from './LinkButton.vue';
import ModTitlecard from './ModTitlecard.vue'; import ModTitlecard from './ModTitlecard.vue';
import UpdateButton from './UpdateButton.vue'; import UpdateButton from './UpdateButton.vue';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores'; import { useClientStore, usePkgStore, usePrfStore } from '../stores';
import { Feature, Package } from '../types'; import { Feature, Package } from '../types';
import { hasFeature } from '../util'; import { hasFeature } from '../util';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -16,6 +16,7 @@ const { t } = useI18n();
const prf = usePrfStore(); const prf = usePrfStore();
const pkgs = usePkgStore(); const pkgs = usePkgStore();
const client = useClientStore();
const props = defineProps({ const props = defineProps({
pkg: Object as () => Package, pkg: Object as () => Package,
@ -39,7 +40,13 @@ if (unsupported.value === true && model.value === true) {
<template> <template>
<div class="flex items-center"> <div class="flex items-center">
<ModTitlecard show-version show-icon show-description :pkg="pkg" /> <ModTitlecard
show-version
show-icon
show-description
:show-namespace="client.pkgListMode !== 'namespace'"
:pkg="pkg"
/>
<UpdateButton :pkg="pkg" /> <UpdateButton :pkg="pkg" />
<span v-tooltip="unsupported && t('store.incompatible')"> <span v-tooltip="unsupported && t('store.incompatible')">
<ToggleSwitch <ToggleSwitch

View File

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

View File

@ -2,9 +2,13 @@
import { ref } from 'vue'; import { ref } from 'vue';
import Chip from 'primevue/chip'; import Chip from 'primevue/chip';
import { convertFileSrc } from '@tauri-apps/api/core'; import { convertFileSrc } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { Feature, Package } from '../types'; import { Feature, Package } from '../types';
import { hasFeature, needsUpdate } from '../util'; import { hasFeature, needsUpdate } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
pkg: Object as () => Package, pkg: Object as () => Package,
@ -17,7 +21,7 @@ const props = defineProps({
const icon = ref('/no-icon.png'); const icon = ref('/no-icon.png');
(async () => { const reloadIcons = async () => {
const src = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon; const src = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
if (src === undefined) { if (src === undefined) {
@ -32,7 +36,11 @@ const icon = ref('/no-icon.png');
icon.value = '/no-icon.png'; icon.value = '/no-icon.png';
} }
} }
})(); };
reloadIcons();
listen('reload-icons', reloadIcons);
</script> </script>
<template> <template>
@ -97,7 +105,12 @@ const icon = ref('/no-icon.png');
v-if="showNamespace && pkg?.namespace" v-if="showNamespace && pkg?.namespace"
class="text-sm opacity-75" class="text-sm opacity-75"
> >
by&nbsp;{{ pkg.namespace }} &nbsp;{{
t('by', { namespace: pkg.namespace }).replaceAll(
' ',
'&nbsp;'
)
}}
</span> </span>
<span class="m-2"> <span class="m-2">
<span <span

View File

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

View File

@ -29,30 +29,47 @@ const files = new Set<string>();
).includes('chunithm'); ).includes('chunithm');
})(); })();
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'],
chunithm: ['segatools-base.ini'],
};
const diagnostic = ref(false);
const exportTemplate = async () => { const exportTemplate = async () => {
const fl = [...files.values()]; const fl = [...files.values()];
exportVisible.value = false; exportVisible.value = false;
await invoke('export_profile', { await invoke('export_profile', {
exportKeychip: exportKeychip.value, exportKeychip: exportKeychip.value,
files: fl, isDiagnostic: diagnostic.value,
files:
diagnostic.value === true
? diagnosticList[prf.current!.meta.game]
: fl,
}); });
await invoke('open_file', { await invoke('open_file', {
path: await path.join(await general.configDir, 'exports'), path: await path.join(await general.configDir, 'exports'),
}); });
}; };
const fileList = {
ongeki: ['aime.txt', 'inohara.cfg', 'mu3.ini', 'segatools-base.ini'],
chunithm: ['aime.txt', 'saekawa.toml', 'segatools-base.ini'],
};
const fileListCurrent: Ref<string[]> = ref([]); const fileListCurrent: Ref<string[]> = ref([]);
const recalcFileList = async () => { const recalcFileList = async () => {
const res: string[] = []; const res: string[] = [];
files.clear(); files.clear();
for (const idx in fileList[prf.current!.meta.game]) { for (const f of fileList) {
const f = fileList[prf.current!.meta.game][idx];
const p = await path.join(await prf.configDir, f); const p = await path.join(await prf.configDir, f);
if (await invoke('file_exists', { path: p })) { if (await invoke('file_exists', { path: p })) {
res.push(f); res.push(f);
@ -91,16 +108,36 @@ const importPick = async () => {
:visible="exportVisible" :visible="exportVisible"
:closable="false /*this shit doesn't work */" :closable="false /*this shit doesn't work */"
:header="`${t('profile.export')} ${prf.current?.meta.name}`" :header="`${t('profile.export')} ${prf.current?.meta.name}`"
:style="{ width: '300px', scale: client.scaleValue }" :style="{ width: '330px', scale: client.scaleValue }"
> >
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-col items-center">
<SelectButton
v-model="diagnostic"
:options="[
{
title: t('profile.standardExport'),
value: false,
},
{
title: t('profile.diagnostic'),
value: true,
},
]"
:allow-empty="false"
option-label="title"
option-value="value"
>
</SelectButton>
</div>
<div class="flex flex-row"> <div class="flex flex-row">
<div class="grow">{{ t('profile.export') }} keychip</div> <div class="grow">{{ t('profile.export') }} keychip</div>
<ToggleSwitch v-model="exportKeychip" /> <ToggleSwitch :disabled="diagnostic" v-model="exportKeychip" />
</div> </div>
<div class="flex flex-row" v-for="f in fileListCurrent"> <div class="flex flex-row" v-for="f in fileListCurrent">
<div class="grow">{{ t('profile.export') }} {{ f }}</div> <div class="grow">{{ t('profile.export') }} {{ f }}</div>
<ToggleSwitch <ToggleSwitch
:disabled="diagnostic"
:model-value="true" :model-value="true"
@update:model-value=" @update:model-value="
(v) => { (v) => {
@ -178,8 +215,9 @@ const importPick = async () => {
style="width: 200px" style="width: 200px"
:options="[ :options="[
{ title: 'English', value: 'en' }, { title: 'English', value: 'en' },
// { title: '日本語', value: 'ja' }, { title: '日本語', value: 'ja' },
{ title: 'Polski', value: 'pl' }, { title: 'Polski', value: 'pl' },
{ title: '简体中文', value: 'zh-Hans' },
]" ]"
size="small" size="small"
option-label="title" option-label="title"

View File

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

View File

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

View File

@ -95,7 +95,7 @@ const prf = usePrfStore();
</div> </div>
</div> </div>
<div v-if="prf.current?.meta.game === 'chunithm'"> <div v-if="prf.current?.meta.game === 'chunithm'">
<div class="absolute left-1/2 top-1/5"> <div class="absolute left-9/17 top-1/12">
<div <div
class="flex flex-row flex-nowrap gap-2 self-center w-full" class="flex flex-row flex-nowrap gap-2 self-center w-full"
> >
@ -108,6 +108,7 @@ const prf = usePrfStore();
button="ir" button="ir"
:index="idx - 1" :index="idx - 1"
:tooltip="`ir${idx}`" :tooltip="`ir${idx}`"
tall
small small
color="rgba(0, 255, 0, 0.2)" color="rgba(0, 255, 0, 0.2)"
/> />

View File

@ -1,13 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
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';
import OptionRow from '../OptionRow.vue'; import OptionRow from '../OptionRow.vue';
import { invoke } from '../../invoke';
import { usePrfStore } from '../../stores'; import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); 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(); const prf = usePrfStore();
</script> </script>
@ -26,5 +40,35 @@ const prf = usePrfStore();
<!-- <Button icon="pi pi-refresh" size="small" /> --> <!-- <Button icon="pi pi-refresh" size="small" /> -->
<FileEditor filename="segatools-base.ini" /> <FileEditor filename="segatools-base.ini" />
</OptionRow> </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> </OptionCategory>
</template> </template>

View File

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

View File

@ -8,6 +8,8 @@ export default {
next: 'Next', next: 'Next',
skip: 'Skip', skip: 'Skip',
close: 'Close', close: 'Close',
by: 'by {namespace}',
updateAll: 'UPDATE ALL',
start: { start: {
failed: 'Start check failed', failed: 'Start check failed',
accept: 'Run anyway', accept: 'Run anyway',
@ -43,8 +45,10 @@ export default {
reallyDelete: 'Are you sure you want to delete {profile}?', reallyDelete: 'Are you sure you want to delete {profile}?',
template: 'STARTLINER template', template: 'STARTLINER template',
importTemplate: 'Import template', importTemplate: 'Import template',
exportTemplate: 'Export template', exportTemplate: 'Export profile',
export: 'Export', export: 'Export',
standardExport: 'Template',
diagnostic: 'Diagnostic',
}, },
creator: { creator: {
header: 'Package creator', header: 'Package creator',
@ -65,10 +69,22 @@ export default {
deprecated: 'Show deprecated', deprecated: 'Show deprecated',
nsfw: 'Show NSFW', nsfw: 'Show NSFW',
incompatible: 'This package is currently incompatible with STARTLINER.', incompatible: 'This package is currently incompatible with STARTLINER.',
missing: 'Missing',
includeCategories: 'Include categories', includeCategories: 'Include categories',
excludeCategories: 'Exclude categories', excludeCategories: 'Exclude categories',
}, },
pkglist: {
missing: 'Missing',
local: 'Local packages',
namespace: 'By namespace',
type: 'By type',
category: 'By category',
standard: 'Standard mods',
native: 'Native mods',
segatools: 'segatools',
unsupported: 'Unsupported',
exclusions: 'Exclusions:',
},
patch: { patch: {
loading: 'Loading...', loading: 'Loading...',
noneFound: noneFound:
@ -150,6 +166,11 @@ export default {
other: 'Other segatools options', other: 'Other segatools options',
otherTooltip: otherTooltip:
'Advanced or situational options not covered by STARTLINER', '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: { extensions: {
title: 'Extensions', title: 'Extensions',
@ -176,6 +197,8 @@ export default {
'Only applicable if the IO module is set to segatools built-in (keyboard) or a compatible third-party module (like mu3io.NET)', 'Only applicable if the IO module is set to segatools built-in (keyboard) or a compatible third-party module (like mu3io.NET)',
leverMode: 'Lever mode', leverMode: 'Lever mode',
mouse: 'Mouse', mouse: 'Mouse',
irTooltip:
'When playing on an actual keyboard, only bind ir1; leave the rest unbound',
}, },
wine: { wine: {
prefix: 'Wine prefix', prefix: 'Wine prefix',

View File

@ -1,14 +1,295 @@
export default { 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: { game: {
ongeki: 'オンゲキ', ongeki: 'オンゲキ',
chunithm: 'チュウニズム', chunithm: 'チュウニズム',
}, },
profile: { profile: {
welcome: 'STARTLINERへようこそ! プロフィルの作成から始めよう。',
create: '{game}のプロフィル', 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: { 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: { aime: {
type: '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

@ -8,6 +8,8 @@ export default {
next: 'Dalej', next: 'Dalej',
skip: 'Pomiń', skip: 'Pomiń',
close: 'Zamknij', close: 'Zamknij',
by: 'od {namespace}',
updateAll: 'ZAKTUALIZUJ WSZYSTKO',
start: { start: {
failed: 'Uruchomienie nie powiodło się', failed: 'Uruchomienie nie powiodło się',
accept: 'Uruchom mimo to', accept: 'Uruchom mimo to',
@ -43,8 +45,10 @@ export default {
reallyDelete: 'Czy na pewno chcesz usunąć {profile}?', reallyDelete: 'Czy na pewno chcesz usunąć {profile}?',
template: 'Szablon', template: 'Szablon',
importTemplate: 'Importuj szablon', importTemplate: 'Importuj szablon',
exportTemplate: 'Eksportuj szablon', exportTemplate: 'Eksportuj profil',
export: 'Eksportuj', export: 'Eksportuj',
standardExport: 'Szablon',
diagnostic: 'Diagnostyka',
}, },
creator: { creator: {
header: 'Kreator pakietów', header: 'Kreator pakietów',
@ -66,10 +70,21 @@ export default {
nsfw: 'Pokaż mityczny O.N.G.E.K.I. Sex Mod dlaczego ta opcja w ogóle tu jest', nsfw: 'Pokaż mityczny O.N.G.E.K.I. Sex Mod dlaczego ta opcja w ogóle tu jest',
incompatible: incompatible:
'Ten pakiet jest obecnie niekompatybilny ze STARTLINEREM.', 'Ten pakiet jest obecnie niekompatybilny ze STARTLINEREM.',
missing: 'Niedostępne',
includeCategories: 'Włącz kategorie', includeCategories: 'Włącz kategorie',
excludeCategories: 'Wyłącz kategorie', excludeCategories: 'Wyłącz kategorie',
}, },
pkglist: {
missing: 'Niedostępne',
local: 'Lokalne pakiety',
namespace: 'Po przestrzeni nazw',
type: 'Po typie',
category: 'Po kategorii',
standard: 'Standardowe mody',
native: 'Natywne mody',
segatools: 'segatools',
unsupported: 'Niewspierane',
exclusions: 'Czarna lista:',
},
patch: { patch: {
loading: 'Wczytuję...', loading: 'Wczytuję...',
noneFound: noneFound:
@ -191,6 +206,11 @@ export default {
other: 'Inne opcje segatools', other: 'Inne opcje segatools',
otherTooltip: otherTooltip:
'Zaawansowane lub sytuacyjne opcje, które nie są objęte przez STARTLINERA', '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: { extensions: {
title: 'Rozszerzenia', title: 'Rozszerzenia',
@ -217,6 +237,8 @@ export default {
'Dotyczy tylko wtedy, gdy moduł IO jest ustawiony na wbudowaną emulację lub zgodny moduł (np. mu3io.NET)', 'Dotyczy tylko wtedy, gdy moduł IO jest ustawiony na wbudowaną emulację lub zgodny moduł (np. mu3io.NET)',
leverMode: 'Tryb wajchy', leverMode: 'Tryb wajchy',
mouse: 'Mysz', mouse: 'Mysz',
irTooltip:
'Jeśli grasz na klawiaturze, ustaw tylko ir1; pozostałe zostaw wyłączone',
}, },
wine: { wine: {
prefix: 'Wine prefix', prefix: 'Wine prefix',

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}
## 玩得愉快
`,
},
};

View File

@ -189,7 +189,7 @@ export const usePkgStore = defineStore('pkg', {
await this.reloadAll(); await this.reloadAll();
}, },
async install(pkg: Package | undefined) { async install(pkg: Package | undefined, enable: boolean) {
if (pkg === undefined) { if (pkg === undefined) {
return; return;
} }
@ -198,6 +198,7 @@ export const usePkgStore = defineStore('pkg', {
await invoke('install_package', { await invoke('install_package', {
key: pkgKey(pkg), key: pkgKey(pkg),
force: true, force: true,
enable,
}); });
} catch (err) { } catch (err) {
if (pkg !== undefined) { if (pkg !== undefined) {
@ -211,6 +212,7 @@ export const usePkgStore = defineStore('pkg', {
await invoke('install_package', { await invoke('install_package', {
key, key,
force: true, force: true,
enable: false,
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -221,7 +223,7 @@ export const usePkgStore = defineStore('pkg', {
const list = []; const list = [];
for (const pkg of this.allLocal) { for (const pkg of this.allLocal) {
if (pkg.rmt && pkg.rmt.version > pkg.loc!.version) { if (pkg.rmt && pkg.rmt.version > pkg.loc!.version) {
list.push(this.install(pkg)); list.push(this.install(pkg, false));
} }
} }
await Promise.all(list); await Promise.all(list);
@ -322,9 +324,10 @@ export const usePrfStore = defineStore('prf', () => {
const generalStore = useGeneralStore(); const generalStore = useGeneralStore();
const configDir = computed(async () => { const configDir = computed(async () => {
const title = `profile-${current.value?.meta.game}-${current.value?.meta.name}`;
return path.join( return path.join(
await generalStore.configDir, await generalStore.configDir,
`profile-${current.value?.meta.game}-${current.value?.meta.name}` title
); );
}); });
@ -375,6 +378,9 @@ export const useClientStore = defineStore('client', () => {
const onboarded: Ref<Game[]> = ref([]); const onboarded: Ref<Game[]> = ref([]);
const locale: Ref<Locale> = ref('en'); const locale: Ref<Locale> = ref('en');
const currentTab: Ref<string> = ref('users'); const currentTab: Ref<string> = ref('users');
const pkgListMode: Ref<'namespace' | 'type' | 'category'> =
ref('namespace');
const hiddenCategories: Ref<string[]> = ref([]);
const _scaleValue = (value: ScaleType) => const _scaleValue = (value: ScaleType) =>
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2; value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
@ -445,6 +451,14 @@ export const useClientStore = defineStore('client', () => {
if (input.currentTab) { if (input.currentTab) {
currentTab.value = input.currentTab; currentTab.value = input.currentTab;
} }
if (input.pkgListMode) {
pkgListMode.value = input.pkgListMode;
}
if (input.hiddenCategories) {
hiddenCategories.value = input.hiddenCategories;
}
await setLocale(locale.value); await setLocale(locale.value);
await setTheme(theme.value); await setTheme(theme.value);
} catch (e) { } catch (e) {
@ -484,6 +498,8 @@ export const useClientStore = defineStore('client', () => {
onboarded: onboarded.value, onboarded: onboarded.value,
locale: locale.value, locale: locale.value,
currentTab: currentTab.value, currentTab: currentTab.value,
pkgListMode: pkgListMode.value,
hiddenCategories: hiddenCategories.value,
}) })
); );
}; };
@ -560,6 +576,8 @@ export const useClientStore = defineStore('client', () => {
timeout, timeout,
scaleModel, scaleModel,
currentTab, currentTab,
pkgListMode,
hiddenCategories,
_scaleValue, _scaleValue,
scaleValue, scaleValue,
load, load,