4 Commits

Author SHA1 Message Date
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
14 changed files with 137 additions and 25 deletions

View File

@ -1,3 +1,16 @@
## 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 ## 0.18.0
- Added new grouping options to the package list - Added new grouping options to the package list

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

@ -480,13 +480,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 => {

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

@ -132,7 +132,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()

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,7 +45,7 @@ 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;
@ -66,7 +66,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 +83,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

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

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

@ -29,23 +29,34 @@ const files = new Set<string>();
).includes('chunithm'); ).includes('chunithm');
})(); })();
const fileList = {
ongeki: ['aime.txt', 'inohara.cfg', 'mu3.ini', 'segatools-base.ini'],
chunithm: ['aime.txt', 'saekawa.toml', 'segatools-base.ini'],
};
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 () => {
@ -91,16 +102,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) => {

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

@ -44,8 +44,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',
@ -189,6 +191,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

@ -44,8 +44,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',
@ -229,6 +231,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',