Compare commits

...

47 Commits

Author SHA1 Message Date
69f2c83109 chore: update CHANGELOG.md 2025-04-19 20:13:11 +00:00
dbbd80c6c3 feat: add 'games' to the manifest 2025-04-19 20:09:32 +00:00
3479804dca feat: 0.12 update 2025-04-19 19:48:08 +00:00
aaeed669df chore: bump ver 2025-04-19 11:46:07 +00:00
7084f40404 fix: improve help pages 2025-04-19 11:44:16 +00:00
f7e9d7d7db docs: rewrite README.md 2025-04-18 19:55:42 +00:00
e87b661f08 feat: onboarding 2025-04-18 15:00:52 +00:00
5d2d407659 chore: update CHANGELOG.md 2025-04-18 06:59:55 +00:00
795e889bd0 fix: better keyboard
* Scale the font as necessary
* Fix CHUNITHM order
* Fix num-unlocked numpad
2025-04-18 06:55:59 +00:00
7071f19877 fix: don't switch primary, for real 2025-04-17 18:43:27 +00:00
a72ec25088 chore: update CHANGELOG.md 2025-04-17 07:49:03 +00:00
5893536daa chore: bump ver 2025-04-17 07:44:58 +00:00
e9550e8eee feat: global progress bar
Also fix me having no foresight and executing things
inside log::debug! macros
2025-04-17 07:44:05 +00:00
658a69a1e2 feat: theme switcher 2025-04-16 22:50:15 +00:00
f3ee0d0068 fix: create a data dir for fern 2025-04-16 22:05:27 +00:00
43f885cffc fix: hotfix '@/types' 2025-04-16 21:54:51 +00:00
d0ce3cddc7 fix: better handling of broken shortcuts 2025-04-16 15:46:39 +00:00
9cbdf2a9c8 docs: update readme and changelog 2025-04-16 15:14:16 +00:00
54a6476010 chore: replace the icon 2025-04-16 15:12:25 +00:00
e4dc0b1f55 fix: prettier print 2025-04-16 13:26:01 +00:00
e6c21ef04a feat: shortcuts 2025-04-16 13:20:43 +00:00
d3145bfc4e fix: aimeio not applying 2025-04-16 07:41:38 +00:00
c7ddeb53e6 fix: native io4 regression 2025-04-15 18:27:42 +00:00
b82fcc942f feat: chuniio 2025-04-15 13:12:12 +00:00
ac18c34895 fix: disable unsupported mods
Also hotfix amdaemon crashing
2025-04-15 07:32:18 +00:00
f588892b05 feat: verbose toggle, info tab, many misc fixes 2025-04-14 19:20:08 +00:00
37df371006 fix: support for translationengine 2025-04-13 23:24:32 +00:00
7ea31c7a87 fix: misc fixes for 0.6.0 2025-04-13 21:02:04 +00:00
073ff8cfb8 fix: native-mod -> native_mod 2025-04-13 20:25:22 +00:00
d93118683d fix: release build 2025-04-13 19:53:12 +00:00
7f68b8d28b feat: amdaemon.exe patching 2025-04-13 19:28:06 +00:00
4247e19996 feat: chusanApp.exe patching 2025-04-13 18:15:41 +00:00
6270fce05f feat: display module for chunithm
Also make the progress bar all shiny
2025-04-12 17:33:39 +00:00
7db36b7bc0 fix: remove update check from the ui
it is no longer necessary
2025-04-12 11:59:02 +00:00
f3016eb029 feat: autoupdate ui 2025-04-12 10:53:06 +00:00
28269c5d75 feat: hardware aime 2025-04-11 23:07:45 +00:00
1a68eda8c1 feat: partial support for patches 2025-04-11 15:27:13 +00:00
b9a40d44a8 fix: remove split_ir 2025-04-10 19:33:54 +00:00
b10c797d52 docs: update README.md 2025-04-10 14:10:47 +00:00
ff0a37dfdc feat: skeleton of proper local package support 2025-04-10 14:07:44 +00:00
d63d81e349 feat: also copy aime.txt 2025-04-10 13:53:20 +00:00
9ea66dbeab feat: segatools.ini loading 2025-04-10 13:32:49 +00:00
4e795257ad feat: add dont_switch_primary 2025-04-08 21:49:15 +00:00
41fbef0260 chore: bump ver 2025-04-04 22:20:05 +00:00
93e0a7e313 feat: new error banner, qol 2025-04-04 22:14:09 +00:00
ca871f069f feat: autoupdate toggle 2025-04-04 19:41:38 +00:00
8c3f9762a4 feat: ui scaling, update all 2025-04-04 14:37:16 +00:00
94 changed files with 5181 additions and 1577 deletions

104
CHANGELOG.md Normal file
View File

@ -0,0 +1,104 @@
## 0.12.0
- Ongeki: cache and mu3.ini config are now split per-profile (requires mu3-mods 3.7+)
- Ongeki: added the few config options of mu3.ini that aren't available in TestMenuConfig, or require a restart
- Chunithm: added Lumi+ patches
- Added support for a non-standard `games` manifest entry intended for local packages
- Expected to be an array containing "ongeki", "chunithm" or both
- Example: { "games": ["ongeki"] }
- Added a button linking to the profile config folder
- Fixed the button linking to the data folder showing up when the folder does not exist
- Uninstalled tool packages are no longer automatically deselected, as that caused issues
## 0.11.1
- Improved help pages
## 0.11.0
- Added help pages
## 0.10.1
- Fixed the order of cells in the CHUNITHM keyboard
- Fixed numpad bindings with numlock disabled
- Disabled primary monitor cleanup when "don't switch primary monitor" is enabled
## 0.10.0
- Added a global progress bar
- Fixed issues with downloading under certain conditions
## 0.9.0
- Added a light/dark theme switcher
## 0.8.1
- Hotfixed the program failing to launch if the data dir hadn't already been created
## 0.8.0
- Added support for ChuniIO
- CHUNITHM support is now complete
- Added a context menu option to create a desktop shortcut
- Added a confirmation prompt before deleting a profile
- Removed Slow
## 0.7.1
- Hotfixed amdaemon crashing at launch
- Greyed out packages currently incompatible with STARTLINER
## 0.7.0
- Hopefully fixed issues with the download button
- Added a verbose logging option
- Added an info tab
- Instead of auto-installing segatools & mempatcher at launch, the package store now shows a "install recommended" button
## 0.6.1
- Added support for O.N.G.E.K.I. English Translation
- Disabled the icon buttons as they broke at some point
## 0.6.0
- Chunithm: added support for DLLs (saekawa, mempatcher)
- Chunithm: added a patch interface
- Chunithm: added display settings
- Chunithm: removed split IR
- Both games: added hardware aime reader support
- Added an update progress bar
## 0.5.0
- Added a keyboard configuration UI (for both games)
- Added a prompt after selecting `mu3.exe`/`chusanApp.exe` in the file picker, allowing you to copy much of the data from `segatools.ini`, if it already exists.
- Added an option to not switch the primary monitor.
- This is not recommended to use but it may help when the primary monitor switcher doesn't work correctly.
## 0.4.0
- New error dialog.
- Added tooltips for IO, Aime, Hook.
- Added a welcome message.
- Added a separate auto-update toggle.
- Fixed a display bug with the offline mode toggle.
## 0.3.0
- Added UI scaling, offline mode, and an 'update all' button
- First public release
## 0.2.0
- Added a context menu for the start button with additional launch options
- Added an audio mode button for ongeki
- Fixed the launcher freezing while the game is running
- Probably added auto-updates
## 0.1.0
⚠️ this release is incomplete and potentially cursed
it's more of a preview of a preview

View File

@ -1,59 +1,44 @@
# STARTLINER # STARTLINER
A simple and easy to use launcher, configuration tool and mod manager for [many games](https://silentblue.remywiki.com/ONGEKI:bright_MEMORY) (more to come) using [Rainycolor Watercolor](https://rainy.patafour.zip). This is a program that seeks to streamline game data configuration, currently supporting O.N.G.E.K.I. and CHUNITHM.
STARTLINER is four things:
- a mod installer and updater, powered by [Rainycolor Watercolor](https://rainy.patafour.zip),
- a configuration GUI for segatools,
- 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).
STARTLINER's core design principle is to modify, configure and launch games without tampering with them.
This makes it possible to keep data cleaner than ever, and to have several configurations pointing at the same data.
Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome. Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome.
## Features
- [Clean](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details) data modding
- Segatools configuration
- Display configuration with automatic rollback
- Support for multiple configurations pointing at the same data
![Mod list](res/list.png)
![Configuration](res/cfg.png)
## Usage ## Usage
Download a prebuilt binary from [Modding Re:Fresh](https://discord.gg/jxvzHjjEmc) or build it yourself: Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:
```sh ```sh
bun install bun install
bun run tauri build bun run tauri build
``` ```
Create a profile, then click on things in the configuration tab (game path, `amfs` and network at the least). STARTLINER expects clean data with unpacked binaries. Anything else you can have in the game directory (segatools, BepInEx, etc.) can be present, but will not be used. Create a profile, then click on things in the configuration tab (game path, `amfs` and network at the least).
Once a profile has been set up, it is possible to bypass the GUI: STARTLINER expects clean data with unpacked binaries. Anything else you may have in the game directory
(segatools, BepInEx, etc.) can be present, but will not be used.
```sh
startliner --start --game ongeki --profile <name>
```
To create a desktop shortcut: `Copy -> Paste Shortcut -> Properties`, and then append `--start --game ongeki --profile <name>` to `Target`.
## Package format ## Package format
- [Package format requirements](https://rainy.patafour.zip/package/create/docs/) Refer to [the wiki](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Package-format).
- A subset of the simple BlueSteel Rainycolor format is currently supported. [Full reference (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou/wiki/Create-Module#user-content-rainycolor-simple)
```
├───app
│ └───BepInEx
│ └───*
├───option
│ └───Axyz
│ └───*
├───icon.png
├───README.md
└───manifest.json
```
More file overrides may be supported in the future.
Arbitrary scripts are not supported by design and that will probably never change.
## See also ## See also
- [BlueSteel launcher (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou) [BlueSteel launcher (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)
## Screenshots
![Package list](res/list.png)
![Package store](res/store.png)
![Configuration](res/cfg.png)
![Keyboard](res/cfg2.png)

View File

@ -1,11 +1,8 @@
### Short-term ### Short-term
- CHUNITHM support
- https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63 - https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63
### Long-term ### Long-term
- Auto-updates - artemis as a special package
- Progress bars and other GUI sugar
- IO DLLs and artemis as special packages
- Other arcade games (if there is demand) - Other arcade games (if there is demand)

271
bun.lock
View File

@ -4,41 +4,46 @@
"": { "": {
"name": "startliner", "name": "startliner",
"dependencies": { "dependencies": {
"@f3ve/vue-markdown-it": "^0.2.3",
"@mdi/font": "7.4.47", "@mdi/font": "7.4.47",
"@primevue/forms": "^4.3.1", "@primevue/forms": "^4.3.3",
"@primevue/themes": "^4.3.1", "@primevue/themes": "^4.3.3",
"@tailwindcss/vite": "^4.0.9", "@tailwindcss/vite": "^4.1.3",
"@tauri-apps/api": "^2.3.0", "@tauri-apps/api": "^2.4.1",
"@tauri-apps/plugin-deep-link": "~2.2.0", "@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-dialog": "~2.2.0", "@tauri-apps/plugin-deep-link": "~2.2.1",
"@tauri-apps/plugin-fs": "^2.2.0", "@tauri-apps/plugin-dialog": "~2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.6", "@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-shell": "~2.2.0", "@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"pinia": "^3.0.1", "@types/markdown-it": "^14.1.2",
"markdown-it": "^14.1.0",
"pinia": "^3.0.2",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.3.1", "primevue": "^4.3.3",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"tailwindcss": "^4.0.9", "tailwindcss": "^4.1.3",
"tailwindcss-primeui": "^0.4.0", "tailwindcss-primeui": "^0.4.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vuetify": "^3.7.14", "vuetify": "^3.8.1",
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.3.1", "@tauri-apps/cli": "^2.4.1",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.1",
"@types/node": "^22.13.9", "@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.3",
"@vue/eslint-config-typescript": "^14.4.0", "@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^7.0.2", "npm-run-all2": "^7.0.2",
"sass": "1.77.8", "sass": "1.77.8",
"sass-embedded": "^1.85.1", "sass-embedded": "^1.86.3",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"unplugin-fonts": "^1.3.1", "unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^0.27.5", "unplugin-vue-components": "^0.27.5",
"vite": "^6.2.0", "vite": "^6.2.6",
"vite-plugin-vuetify": "^2.1.0", "vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8", "vue-tsc": "^2.2.8",
}, },
}, },
@ -130,6 +135,8 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
"@f3ve/vue-markdown-it": ["@f3ve/vue-markdown-it@0.2.3", "", { "dependencies": { "markdown-it": "^14.1.0" }, "peerDependencies": { "vue": "^3.3.4" } }, "sha512-v0VNd7wb55kwsUUy3n6DLI9+0FYSG0PrCTD3bWuSRo6WS3OHD5wghh/aHzebVdsVkSBXfVpiEUlMA3DrxLs7Lw=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
@ -166,13 +173,13 @@
"@primeuix/utils": ["@primeuix/utils@0.5.1", "", {}, "sha512-/bYirtF3gJOGrRQfQ5tUyQOLEria7wg/UCqvpIydTAxLmj/UWgWwh2kAjYVp49eldm1+2sk4+TDkbAz8XcPpew=="], "@primeuix/utils": ["@primeuix/utils@0.5.1", "", {}, "sha512-/bYirtF3gJOGrRQfQ5tUyQOLEria7wg/UCqvpIydTAxLmj/UWgWwh2kAjYVp49eldm1+2sk4+TDkbAz8XcPpew=="],
"@primevue/core": ["@primevue/core@4.3.1", "", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/utils": "^0.5.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Z2JfYk7I477qdlWHH5yiqUK0cwNe5joZJLwFtSEFkmBi/ocXvkGNAYk8XYNCz6UTDePUQSHKseKJxMkFHlfRtw=="], "@primevue/core": ["@primevue/core@4.3.3", "", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/utils": "^0.5.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-kSkN5oourG7eueoFPIqiNX3oDT/f0I5IRK3uOY/ytz+VzTZp5yuaCN0Nt42ZQpVXjDxMxDvUhIdaXVrjr58NhQ=="],
"@primevue/forms": ["@primevue/forms@4.3.1", "", { "dependencies": { "@primeuix/forms": "^0.0.4", "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.1" } }, "sha512-2IWIrRCV82ey7fUb0dQhnJX8iqddcyZNajsEQpRMw1MO8NchN+vT26H2H1AYDI5uwwmv1FCqr+MiU9wk39Ub2g=="], "@primevue/forms": ["@primevue/forms@4.3.3", "", { "dependencies": { "@primeuix/forms": "^0.0.4", "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.3" } }, "sha512-GZYMd8wp+7/4DVMoGGUtRkAHw352peT3pgwgzaFYQqNIjxxGw9eI253XTxrppRCowrGJ2jEe80p9WfHi087B1g=="],
"@primevue/icons": ["@primevue/icons@4.3.1", "", { "dependencies": { "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.1" } }, "sha512-67GFk/NdbVDuPx4tlbO01BBWujLiZTJJJSce63dvLr7082YukPfrQq4Kru+y5Qmrfkq0uaP1I3+Ut9Skr6ATfQ=="], "@primevue/icons": ["@primevue/icons@4.3.3", "", { "dependencies": { "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.3" } }, "sha512-ouQaxHyeFB6MSfEGGbjaK5Qv9efS1xZGetZoU5jcPm090MSYLFtroP1CuK3lZZAQals06TZ6T6qcoNukSHpK5w=="],
"@primevue/themes": ["@primevue/themes@4.3.1", "", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/themes": "^1.0.0" } }, "sha512-aC7V5BkMoMxEMoq7hIf+PNEaY/AS5EAgVg96Gf3bvV1JqZ36hSQ2CV1SfuNsWeEvIPF/u4L5qvjwC/LGjT4qgw=="], "@primevue/themes": ["@primevue/themes@4.3.3", "", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/themes": "^1.0.0" } }, "sha512-LiYlSXsHeA8DFm8+yGyiDFQc3SEQwHcESTN1/rV+rrZ+UPuPisHY9fNIGRFQKA5XUQPDTQDQjtwYGx25Jikwhg=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
@ -214,95 +221,107 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.6", "", { "os": "win32", "cpu": "x64" }, "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.6", "", { "os": "win32", "cpu": "x64" }, "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w=="],
"@tailwindcss/node": ["@tailwindcss/node@4.0.9", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "tailwindcss": "4.0.9" } }, "sha512-tOJvdI7XfJbARYhxX+0RArAhmuDcczTC46DGCEziqxzzbIaPnfYaIyRT31n4u8lROrsO7Q6u/K9bmQHL2uL1bQ=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.3", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.3" } }, "sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.0.9", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.0.9", "@tailwindcss/oxide-darwin-arm64": "4.0.9", "@tailwindcss/oxide-darwin-x64": "4.0.9", "@tailwindcss/oxide-freebsd-x64": "4.0.9", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.9", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.9", "@tailwindcss/oxide-linux-arm64-musl": "4.0.9", "@tailwindcss/oxide-linux-x64-gnu": "4.0.9", "@tailwindcss/oxide-linux-x64-musl": "4.0.9", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.9", "@tailwindcss/oxide-win32-x64-msvc": "4.0.9" } }, "sha512-eLizHmXFqHswJONwfqi/WZjtmWZpIalpvMlNhTM99/bkHtUs6IqgI1XQ0/W5eO2HiRQcIlXUogI2ycvKhVLNcA=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.3", "@tailwindcss/oxide-darwin-arm64": "4.1.3", "@tailwindcss/oxide-darwin-x64": "4.1.3", "@tailwindcss/oxide-freebsd-x64": "4.1.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.3", "@tailwindcss/oxide-linux-arm64-musl": "4.1.3", "@tailwindcss/oxide-linux-x64-gnu": "4.1.3", "@tailwindcss/oxide-linux-x64-musl": "4.1.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.3", "@tailwindcss/oxide-win32-x64-msvc": "4.1.3" } }, "sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.0.9", "", { "os": "android", "cpu": "arm64" }, "sha512-YBgy6+2flE/8dbtrdotVInhMVIxnHJPbAwa7U1gX4l2ThUIaPUp18LjB9wEH8wAGMBZUb//SzLtdXXNBHPUl6Q=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.3", "", { "os": "android", "cpu": "arm64" }, "sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.0.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pWdl4J2dIHXALgy2jVkwKBmtEb73kqIfMpYmcgESr7oPQ+lbcQ4+tlPeVXaSAmang+vglAfFpXQCOvs/aGSqlw=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.0.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-4Dq3lKp0/C7vrRSkNPtBGVebEyWt9QPPlQctxJ0H3MDyiQYvzVYf8jKow7h5QkWNe8hbatEqljMj/Y0M+ERYJg=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.0.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-k7U1RwRODta8x0uealtVt3RoWAWqA+D5FAOsvVGpYoI6ObgmnzqWW6pnVwz70tL8UZ/QXjeMyiICXyjzB6OGtQ=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.0.9", "", { "os": "linux", "cpu": "arm" }, "sha512-NDDjVweHz2zo4j+oS8y3KwKL5wGCZoXGA9ruJM982uVJLdsF8/1AeKvUwKRlMBpxHt1EdWJSAh8a0Mfhl28GlQ=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3", "", { "os": "linux", "cpu": "arm" }, "sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.0.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-jk90UZ0jzJl3Dy1BhuFfRZ2KP9wVKMXPjmCtY4U6fF2LvrjP5gWFJj5VHzfzHonJexjrGe1lMzgtjriuZkxagg=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.0.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-3eMjyTC6HBxh9nRgOHzrc96PYh1/jWOwHZ3Kk0JN0Kl25BJ80Lj9HEvvwVDNTgPg154LdICwuFLuhfgH9DULmg=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.0.9", "", { "os": "linux", "cpu": "x64" }, "sha512-v0D8WqI/c3WpWH1kq/HP0J899ATLdGZmENa2/emmNjubT0sWtEke9W9+wXeEoACuGAhF9i3PO5MeyditpDCiWQ=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.0.9", "", { "os": "linux", "cpu": "x64" }, "sha512-Kvp0TCkfeXyeehqLJr7otsc4hd/BUPfcIGrQiwsTVCfaMfjQZCG7DjI+9/QqPZha8YapLA9UoIcUILRYO7NE1Q=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.0.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-m3+60T/7YvWekajNq/eexjhV8z10rswcz4BC9bioJ7YaN+7K8W2AmLmG0B79H14m6UHE571qB0XsPus4n0QVgQ=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.0.9", "", { "os": "win32", "cpu": "x64" }, "sha512-dpc05mSlqkwVNOUjGu/ZXd5U1XNch1kHFJ4/cHkZFvaW1RzbHmRt24gvM8/HC6IirMxNarzVw4IXVtvrOoZtxA=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.0.9", "", { "dependencies": { "@tailwindcss/node": "4.0.9", "@tailwindcss/oxide": "4.0.9", "lightningcss": "^1.29.1", "tailwindcss": "4.0.9" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-BIKJO+hwdIsN7V6I7SziMZIVHWWMsV/uCQKYEbeiGRDRld+TkqyRRl9+dQ0MCXbhcVr+D9T/qX2E84kT7V281g=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.1.3", "", { "dependencies": { "@tailwindcss/node": "4.1.3", "@tailwindcss/oxide": "4.1.3", "tailwindcss": "4.1.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ=="],
"@tauri-apps/api": ["@tauri-apps/api@2.3.0", "", {}, "sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA=="], "@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.3.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.3.1", "@tauri-apps/cli-darwin-x64": "2.3.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.3.1", "@tauri-apps/cli-linux-arm64-gnu": "2.3.1", "@tauri-apps/cli-linux-arm64-musl": "2.3.1", "@tauri-apps/cli-linux-x64-gnu": "2.3.1", "@tauri-apps/cli-linux-x64-musl": "2.3.1", "@tauri-apps/cli-win32-arm64-msvc": "2.3.1", "@tauri-apps/cli-win32-ia32-msvc": "2.3.1", "@tauri-apps/cli-win32-x64-msvc": "2.3.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-xewcw/ZsCqgilTy2h7+pp2Baxoy7zLR2wXOV7SZLzkb6SshHVbm1BFAjn8iFATURRW85KLzl6wSGJ2dQHjVHqw=="], "@tauri-apps/cli": ["@tauri-apps/cli@2.4.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.4.1", "@tauri-apps/cli-darwin-x64": "2.4.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.4.1", "@tauri-apps/cli-linux-arm64-gnu": "2.4.1", "@tauri-apps/cli-linux-arm64-musl": "2.4.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.4.1", "@tauri-apps/cli-linux-x64-gnu": "2.4.1", "@tauri-apps/cli-linux-x64-musl": "2.4.1", "@tauri-apps/cli-win32-arm64-msvc": "2.4.1", "@tauri-apps/cli-win32-ia32-msvc": "2.4.1", "@tauri-apps/cli-win32-x64-msvc": "2.4.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-9Ta81jx9+57FhtU/mPIckDcOBtPTUdKM75t4+aA0X84b8Sclb0jy1xA8NplmcRzp2fsfIHNngU2NiRxsW5+yOQ=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw=="], "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QME7s8XQwy3LWClTVlIlwXVSLKkeJ/z88pr917Mtn9spYOjnBfsgHAgGdmpWD3NfJxjg7CtLbhH49DxoFL+hLg=="],
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-LDwGg3AuBQ3aCeMAFaFwt0MSGOVFoXuXEe0z4QxQ7jZE5tdAOhKABaq4i569V5lShCgQZ6nLD/tmA5+GipvHnA=="], "@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/r89IcW6Ya1sEsFUEH7wLNruDTj7WmDWKGpPy7gATFtQr5JEY4heernqE82isjTUimnHZD8SCr0jA3NceI4ybw=="],
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-hu3HpbbtJBvHXw5i54QHwLxOUoXWqhf7CL2YYSPOrWEEQo10NKddulP61L5gfr5z+bSSaitfLwqgTidgnaNJCA=="], "@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.4.1", "", { "os": "linux", "cpu": "arm" }, "sha512-9tDijkRB+CchAGjXxYdY9l/XzFpLp1yihUtGXJz9eh+3qIoRI043n3e+6xmU8ZURr7XPnu+R4sCmXs6HD+NCEQ=="],
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mEGgwkiGSKYXWHhGodo7zU9PCd2I/d6KkR+Wp1nzK+DxsCrEK6yJ5XxYLSQSDcKkM4dCxpVEPUiVMbDhmn08jg=="], "@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-pnFGDEXBAzS4iDYAVxTRhAzNu3K2XPGflYyBc0czfHDBXopqRgMyj5Q9Wj7HAwv6cM8BqzXINxnb2ZJFGmbSgA=="],
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-tqQkafikGfnc7ISnGjSYkbpnzJKEyO8XSa0YOXTAL3J8R5Pss5ZIZY7G8kq1mwQSR/dPVR1ZLTVXgZGuysjP8w=="], "@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hp0zXgeZNKmT+eoJSCxSBUm2QndNuRxR55tmIeNm3vbyUMJN/49uW7nurZ5fBPsacN4Pzwlx1dIMK+Gnr9A69w=="],
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-I3puDJ2wGEauXlXbzIHn2etz78TaWs1cpN6zre02maHr6ZR7nf7euTCOGPhhfoMG0opA5mT/eLuYpVw648/VAA=="], "@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.4.1", "", { "os": "linux", "cpu": "none" }, "sha512-3T3bo2E4fdYRvzcXheWUeQOVB+LunEEi92iPRgOyuSVexVE4cmHYl+MPJF+EUV28Et0hIVTsHibmDO0/04lAFg=="],
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rbWiCOBuQN7tPySkUyBs914uUikE3mEUOqV/IFospvKESw4UC3G1DL5+ybfXH7Orb8/in3JpJuVzYQjo+OSbBA=="], "@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-kLN0FdNONO+2i+OpU9+mm6oTGufRC00e197TtwjpC0N6K2K8130w7Q3FeODIM2CMyg0ov3tH+QWqKW7GNhHFzg=="],
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-PdTmUzSeTHjJuBpCV7L+V29fPhPtToU+NZU46slHKSA1aT38MiFDXBZ/6P5Zudrt9QPMfIubqnJKbK8Ivvv7Ww=="], "@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8exvA5Ub9eg66a6hsMQKJIkf63QAf9OdiuFKOsEnKZkNN2x0NLgfvEcqdw88VY0UMs9dBoZ1AGbWMeYnLrLwQ=="],
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.3.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-K/Xa97kspWT4UWj3t26lL2D3QsopTAxS7kWi5kObdqtAGn3qD52qBi24FH38TdvHYz4QlnLIb30TukviCgh4gw=="], "@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-4JFrslsMCJQG1c573T9uqQSAbF3j/tMKkMWzsIssv8jvPiP++OG61A2/F+y9te9/Q/O95cKhDK63kaiO5xQaeg=="],
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-RgwzXbP8gAno3kQEsybMtgLp6D1Z1Nec2cftryYbPTJmoMJs6e4qgtxuTSbUz5SKnHe8rGgMiFSvEGoHvbG72Q=="], "@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.4.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-9eXfFORehYSCRwxg2KodfmX/mhr50CI7wyBYGbPLePCjr5z0jK/9IyW6r0tC+ZVjwpX48dkk7hKiUgI25jHjzA=="],
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.2.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-H6mkxr2KZ3XJcKL44tiq6cOjCw9DL8OgU1xjn3j26Qsn+H/roPFiyhR7CHuB8Ar+sQFj4YVlfmJwtBajK2FETQ=="], "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-60a4Ov7Jrwqz2hzDltlS7301dhSAmM9dxo+IRBD3xz7yobKrgaHXYpWvnRomYItHcDd51VaKc9292H8/eE/gsw=="],
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.2.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg=="], "@tauri-apps/plugin-cli": ["@tauri-apps/plugin-cli@2.2.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-rvNhMog9rHr01Xk+trBFKJ0eZICIvPkm9GX6ogB89/0hROU/lf+a/sb4vC0wtSeR7zrJuCSxwxYuvHCZheaYFA=="],
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.2.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-+08mApuONKI8/sCNEZ6AR8vf5vI9DXD4YfrQ9NQmhRxYKMLVhRW164vdW5BSLmMpuevftpQ2FVoL9EFkfG9Z+g=="], "@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.2.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-8skZ6qIH/kWaV8d6jj3aPvvkIOuqkVk0APRDey9n9N3Ueu3n4MIbuxpAKR2EdoAyQxnXxPTNVyjw2D35/vfGyg=="],
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.2.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A=="],
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.2.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-KdGzvvA4Eg0Dhw55MwczFbjxLxsTx0FvwwC/0StXlr6IxwPUxh5ziZQoaugkBFs8t+wfebdQrjBEzd8NmmDXNw=="],
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.2.6", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w=="], "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.2.6", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w=="],
"@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.2.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA=="], "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.2.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA=="],
"@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.7.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-oBug5UCH2wOsoYk0LW5LEMAT51mszjg11s8eungRH26x/qOrEjLvnuJJoxVVr9nsWowJ6vnpXKS+lUMfFTlvHQ=="],
"@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="], "@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="],
"@tsconfig/node22": ["@tsconfig/node22@22.0.0", "", {}, "sha512-twLQ77zevtxobBOD4ToAtVmuYrpeYUh3qh+TEp+08IWhpsrIflVHqQ1F1CiPxQGL7doCdBIOOCF+1Tm833faNg=="], "@tsconfig/node22": ["@tsconfig/node22@22.0.1", "", {}, "sha512-VkgOa3n6jvs1p+r3DiwBqeEwGAwEvnVCg/hIjiANl5IEcqP3G0u5m8cBJspe1t9qjZRlZ7WFgqq5bJrGdgAKMg=="],
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@22.13.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="], "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/type-utils": "8.24.0", "@typescript-eslint/utils": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ=="], "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA=="], "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0" } }, "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw=="], "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/utils": "8.24.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.29.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/type-utils": "8.29.0", "@typescript-eslint/utils": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.24.0", "", {}, "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.29.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/types": "8.29.0", "@typescript-eslint/typescript-estree": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.29.0", "", { "dependencies": { "@typescript-eslint/types": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0" } }, "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.29.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.29.0", "@typescript-eslint/utils": "8.29.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.29.0", "", {}, "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.1", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.29.0", "", { "dependencies": { "@typescript-eslint/types": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.29.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/types": "8.29.0", "@typescript-eslint/typescript-estree": "8.29.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.29.0", "", { "dependencies": { "@typescript-eslint/types": "8.29.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.3", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg=="],
"@volar/language-core": ["@volar/language-core@2.4.11", "", { "dependencies": { "@volar/source-map": "2.4.11" } }, "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg=="], "@volar/language-core": ["@volar/language-core@2.4.11", "", { "dependencies": { "@volar/source-map": "2.4.11" } }, "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg=="],
@ -326,7 +345,7 @@
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.2", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA=="], "@vue/devtools-shared": ["@vue/devtools-shared@7.7.2", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA=="],
"@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.4.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.23.0", "fast-glob": "^3.3.3", "typescript-eslint": "^8.23.0", "vue-eslint-parser": "^9.4.3" }, "peerDependencies": { "eslint": "^9.10.0", "eslint-plugin-vue": "^9.28.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-daU+eAekEeVz3CReE4PRW25fe+OJDKwE28jHN6LimDEnuFMbJ6H4WGogEpNof276wVP6UvzOeJQfLFjB5mW29A=="], "@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.5.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.26.0", "fast-glob": "^3.3.3", "typescript-eslint": "^8.26.0", "vue-eslint-parser": "^10.1.1" }, "peerDependencies": { "eslint": "^9.10.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-5oPOyuwkw++AP5gHDh5YFmST50dPfWOcm3/W7Nbh42IK5O3H74ytWAw0TrCRTaBoD/02khnWXuZf1Bz1xflavQ=="],
"@vue/language-core": ["@vue/language-core@2.2.8", "", { "dependencies": { "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ=="], "@vue/language-core": ["@vue/language-core@2.2.8", "", { "dependencies": { "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ=="],
@ -402,7 +421,7 @@
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="],
"enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="],
@ -510,27 +529,29 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.29.1", "", { "dependencies": { "detect-libc": "^1.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.1", "lightningcss-darwin-x64": "1.29.1", "lightningcss-freebsd-x64": "1.29.1", "lightningcss-linux-arm-gnueabihf": "1.29.1", "lightningcss-linux-arm64-gnu": "1.29.1", "lightningcss-linux-arm64-musl": "1.29.1", "lightningcss-linux-x64-gnu": "1.29.1", "lightningcss-linux-x64-musl": "1.29.1", "lightningcss-win32-arm64-msvc": "1.29.1", "lightningcss-win32-x64-msvc": "1.29.1" } }, "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q=="], "lightningcss": ["lightningcss@1.29.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.2", "lightningcss-darwin-x64": "1.29.2", "lightningcss-freebsd-x64": "1.29.2", "lightningcss-linux-arm-gnueabihf": "1.29.2", "lightningcss-linux-arm64-gnu": "1.29.2", "lightningcss-linux-arm64-musl": "1.29.2", "lightningcss-linux-x64-gnu": "1.29.2", "lightningcss-linux-x64-musl": "1.29.2", "lightningcss-win32-arm64-msvc": "1.29.2", "lightningcss-win32-x64-msvc": "1.29.2" } }, "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.29.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA=="], "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.29.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ=="], "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.29.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.1", "", { "os": "linux", "cpu": "arm" }, "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg=="], "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.29.2", "", { "os": "linux", "cpu": "arm" }, "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ=="], "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw=="], "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.29.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw=="], "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw=="], "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.29.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog=="], "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.29.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.1", "", { "os": "win32", "cpu": "x64" }, "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q=="], "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="], "local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="],
@ -542,6 +563,10 @@
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
@ -594,7 +619,7 @@
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
"pinia": ["pinia@3.0.1", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg=="], "pinia": ["pinia@3.0.2", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
@ -608,10 +633,12 @@
"primeicons": ["primeicons@7.0.0", "", {}, "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="], "primeicons": ["primeicons@7.0.0", "", {}, "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="],
"primevue": ["primevue@4.3.1", "", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/styles": "^1.0.0", "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.1", "@primevue/icons": "4.3.1" } }, "sha512-NSUpcWf2WpXgqOvjgXu5zQM3E5UEXoA2iXLi6xV+h1SBZ1TmgNfrjme96KRzfUY2RBsI0rTSUuPv0I+fXvtcmA=="], "primevue": ["primevue@4.3.3", "", { "dependencies": { "@primeuix/styled": "^0.5.0", "@primeuix/styles": "^1.0.0", "@primeuix/utils": "^0.5.1", "@primevue/core": "4.3.3", "@primevue/icons": "4.3.3" } }, "sha512-nooYVoEz5CdP3EhUkD6c3qTdRmpLHZh75fBynkUkl46K8y5rksHTjdSISiDijwTA5STQIOkyqLb+RM+HQ6nC1Q=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="], "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
@ -634,47 +661,47 @@
"sass": ["sass@1.77.8", "", { "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { "sass": "sass.js" } }, "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ=="], "sass": ["sass@1.77.8", "", { "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { "sass": "sass.js" } }, "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ=="],
"sass-embedded": ["sass-embedded@1.85.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0", "buffer-builder": "^0.2.0", "colorjs.io": "^0.5.0", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-android-arm": "1.85.1", "sass-embedded-android-arm64": "1.85.1", "sass-embedded-android-ia32": "1.85.1", "sass-embedded-android-riscv64": "1.85.1", "sass-embedded-android-x64": "1.85.1", "sass-embedded-darwin-arm64": "1.85.1", "sass-embedded-darwin-x64": "1.85.1", "sass-embedded-linux-arm": "1.85.1", "sass-embedded-linux-arm64": "1.85.1", "sass-embedded-linux-ia32": "1.85.1", "sass-embedded-linux-musl-arm": "1.85.1", "sass-embedded-linux-musl-arm64": "1.85.1", "sass-embedded-linux-musl-ia32": "1.85.1", "sass-embedded-linux-musl-riscv64": "1.85.1", "sass-embedded-linux-musl-x64": "1.85.1", "sass-embedded-linux-riscv64": "1.85.1", "sass-embedded-linux-x64": "1.85.1", "sass-embedded-win32-arm64": "1.85.1", "sass-embedded-win32-ia32": "1.85.1", "sass-embedded-win32-x64": "1.85.1" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-0i+3h2Df/c71afluxC1SXqyyMmJlnKWfu9ZGdzwuKRM1OftEa2XM2myt5tR36CF3PanYrMjFKtRIj8PfSf838w=="], "sass-embedded": ["sass-embedded@1.86.3", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0", "buffer-builder": "^0.2.0", "colorjs.io": "^0.5.0", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-android-arm": "1.86.3", "sass-embedded-android-arm64": "1.86.3", "sass-embedded-android-ia32": "1.86.3", "sass-embedded-android-riscv64": "1.86.3", "sass-embedded-android-x64": "1.86.3", "sass-embedded-darwin-arm64": "1.86.3", "sass-embedded-darwin-x64": "1.86.3", "sass-embedded-linux-arm": "1.86.3", "sass-embedded-linux-arm64": "1.86.3", "sass-embedded-linux-ia32": "1.86.3", "sass-embedded-linux-musl-arm": "1.86.3", "sass-embedded-linux-musl-arm64": "1.86.3", "sass-embedded-linux-musl-ia32": "1.86.3", "sass-embedded-linux-musl-riscv64": "1.86.3", "sass-embedded-linux-musl-x64": "1.86.3", "sass-embedded-linux-riscv64": "1.86.3", "sass-embedded-linux-x64": "1.86.3", "sass-embedded-win32-arm64": "1.86.3", "sass-embedded-win32-ia32": "1.86.3", "sass-embedded-win32-x64": "1.86.3" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-3pZSp24ibO1hdopj+W9DuiWsZOb2YY6AFRo/jjutKLBkqJGM1nJjXzhAYfzRV+Xn5BX1eTI4bBTE09P0XNHOZg=="],
"sass-embedded-android-arm": ["sass-embedded-android-arm@1.85.1", "", { "os": "android", "cpu": "arm" }, "sha512-GkcgUGMZtEF9gheuE1dxCU0ZSAifuaFXi/aX7ZXvjtdwmTl9Zc/OHR9oiUJkc8IW9UI7H8TuwlTAA8+SwgwIeQ=="], "sass-embedded-android-arm": ["sass-embedded-android-arm@1.86.3", "", { "os": "android", "cpu": "arm" }, "sha512-UyeXrFzZSvrGbvrWUBcspbsbivGgAgebLGJdSqJulgSyGbA6no3DWQ5Qpdd6+OAUC39BlpPu74Wx9s4RrVuaFw=="],
"sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.85.1", "", { "os": "android", "cpu": "arm64" }, "sha512-27oRheqNA3SJM2hAxpVbs7mCKUwKPWmEEhyiNFpBINb5ELVLg+Ck5RsGg+SJmo130ul5YX0vinmVB5uPWc8X5w=="], "sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.86.3", "", { "os": "android", "cpu": "arm64" }, "sha512-q+XwFp6WgAv+UgnQhsB8KQ95kppvWAB7DSoJp+8Vino8b9ND+1ai3cUUZPE5u4SnLZrgo5NtrbPvN5KLc4Pfyg=="],
"sass-embedded-android-ia32": ["sass-embedded-android-ia32@1.85.1", "", { "os": "android", "cpu": "ia32" }, "sha512-f3x16NyRgtXFksIaO/xXKrUhttUBv8V0XsAR2Dhdb/yz4yrDrhzw9Wh8fmw7PlQqECcQvFaoDr3XIIM6lKzasw=="], "sass-embedded-android-ia32": ["sass-embedded-android-ia32@1.86.3", "", { "os": "android", "cpu": "ia32" }, "sha512-gTJjVh2cRzvGujXj5ApPk/owUTL5SiO7rDtNLrzYAzi1N5HRuLYXqk3h1IQY3+eCOBjGl7mQ9XyySbJs/3hDvg=="],
"sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.85.1", "", { "os": "android", "cpu": "none" }, "sha512-IP6OijpJ8Mqo7XqCe0LsuZVbAxEFVboa0kXqqR5K55LebEplsTIA2GnmRyMay3Yr/2FVGsZbCb6Wlgkw23eCiA=="], "sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.86.3", "", { "os": "android", "cpu": "none" }, "sha512-Po3JnyiCS16kd6REo1IMUbFGYtvL9O0rmKaXx5vOuBaJD1LPy2LiSSp7TU7wkJ9IxsTDGzFaSeP1I9qb6D8VVg=="],
"sass-embedded-android-x64": ["sass-embedded-android-x64@1.85.1", "", { "os": "android", "cpu": "x64" }, "sha512-Mh7CA53wR3ADvXAYipFc/R3vV4PVOzoKwWzPxmq+7i8UZrtsVjKONxGtqWe9JG1mna0C9CRZAx0sv/BzbOJxWg=="], "sass-embedded-android-x64": ["sass-embedded-android-x64@1.86.3", "", { "os": "android", "cpu": "x64" }, "sha512-+7h3jdDv/0kUFx0BvxYlq2fa7CcHiDPlta6k5OxO5K6jyqJwo9hc0Z052BoYEauWTqZ+vK6bB5rv2BIzq4U9nA=="],
"sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.85.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-msWxzhvcP9hqGVegxVePVEfv9mVNTlUgGr6k7O7Ihji702mbtrH/lKwF4aRkkt4g1j7tv10+JtQXmTNi/pi9kA=="], "sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.86.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgLwV4ORm5Hr0DmIXo0Xw/vlzwLnfAiqD2jDXIglkBsc5czJmo4/IBdGXOP65TRnsgJEqvbU3aQhuawX5++x9A=="],
"sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.85.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-J4UFHUiyI9Z+mwYMwz11Ky9TYr3hY1fCxeQddjNGL/+ovldtb0yAIHvoVM0BGprQDm5JqhtUk8KyJ3RMJqpaAA=="], "sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.86.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-dfKhfrGPRNLWLC82vy/vQGmNKmAiKWpdFuWiePRtg/E95pqw+sCu6080Y6oQLfFu37Iq3MpnXiSpDuSo7UnPWA=="],
"sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.85.1", "", { "os": "linux", "cpu": "arm" }, "sha512-X0fDh95nNSw1wfRlnkE4oscoEA5Au4nnk785s9jghPFkTBg+A+5uB6trCjf0fM22+Iw6kiP4YYmDdw3BqxAKLQ=="], "sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.86.3", "", { "os": "linux", "cpu": "arm" }, "sha512-+fVCIH+OR0SMHn2NEhb/VfbpHuUxcPtqMS34OCV3Ka99LYZUJZqth4M3lT/ppGl52mwIVLNYzR4iLe6mdZ6mYA=="],
"sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.85.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jGadetB03BMFG2rq3OXub/uvC/lGpbQOiLGEz3NLb2nRZWyauRhzDtvZqkr6BEhxgIWtMtz2020yD8ZJSw/r2w=="], "sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.86.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-tYq5rywR53Qtc+0KI6pPipOvW7a47ETY69VxfqI9BR2RKw2hBbaz0bIw6OaOgEBv2/XNwcWb7a4sr7TqgkqKAA=="],
"sass-embedded-linux-ia32": ["sass-embedded-linux-ia32@1.85.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-7HlYY90d9mitDtNi5s+S+5wYZrTVbkBH2/kf7ixrzh2BFfT0YM81UHLJRnGX93y9aOMBL6DSZAIfkt1RsV9bkQ=="], "sass-embedded-linux-ia32": ["sass-embedded-linux-ia32@1.86.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-CmQ5OkqnaeLdaF+bMqlYGooBuenqm3LvEN9H8BLhjkpWiFW8hnYMetiqMcJjhrXLvDw601KGqA5sr/Rsg5s45g=="],
"sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.85.1", "", { "os": "linux", "cpu": "arm" }, "sha512-5vcdEqE8QZnu6i6shZo7x2N36V7YUoFotWj2rGekII5ty7Nkaj+VtZhUEOp9tAzEOlaFuDp5CyO1kUCvweT64A=="], "sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.86.3", "", { "os": "linux", "cpu": "arm" }, "sha512-SEm65SQknI4pl+mH5Xf231hOkHJyrlgh5nj4qDbiBG6gFeutaNkNIeRgKEg3cflXchCr8iV/q/SyPgjhhzQb7w=="],
"sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.85.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-FLkIT0p18XOkR6wryJ13LqGBDsrYev2dRk9dtiU18NCpNXruKsdBQ1ZnWHVKB3h1dA9lFyEEisC0sooKdNfeOQ=="], "sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.86.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-4zOr2C/eW89rxb4ozTfn7lBzyyM5ZigA1ZSRTcAR26Qbg/t2UksLdGnVX9/yxga0d6aOi0IvO/7iM2DPPRRotg=="],
"sass-embedded-linux-musl-ia32": ["sass-embedded-linux-musl-ia32@1.85.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-N1093T84zQJor1yyIAdYScB5eAuQarGK1tKgZ4uTnxVlgA7Xi1lXV8Eh7ox9sDqKCaWkVQ3MjqU26vYRBeRWyw=="], "sass-embedded-linux-musl-ia32": ["sass-embedded-linux-musl-ia32@1.86.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-84Tcld32LB1loiqUvczWyVBQRCChm0wNLlkT59qF29nxh8njFIVf9yaPgXcSyyjpPoD9Tu0wnq3dvVzoMCh9AQ=="],
"sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.85.1", "", { "os": "linux", "cpu": "none" }, "sha512-WRsZS/7qlfYXsa93FBpSruieuURIu7ySfFhzYfF1IbKrNAGwmbduutkHZh2ddm5/vQMvQ0Rdosgv+CslaQHMcw=="], "sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.86.3", "", { "os": "linux", "cpu": "none" }, "sha512-IxEqoiD7vdNpiOwccybbV93NljBy64wSTkUOknGy21SyV43C8uqESOwTwW9ywa3KufImKm8L3uQAW/B0KhJMWg=="],
"sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.85.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+OlLIilA5TnP0YEqTQ8yZtkW+bJIQYvzoGoNLUEskeyeGuOiIyn2CwL6G4JQB4xZQFaxPHb7JD3EueFkQbH0Pw=="], "sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.86.3", "", { "os": "linux", "cpu": "x64" }, "sha512-ePeTPXUxPK6JgHcUfnrkIyDtyt+zlAvF22mVZv6y1g/PZFm1lSfX+Za7TYHg9KaYqaaXDiw6zICX4i44HhR8rA=="],
"sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.85.1", "", { "os": "linux", "cpu": "none" }, "sha512-mKKlOwMGLN7yP1p0gB5yG/HX4fYLnpWaqstNuOOXH+fOzTaNg0+1hALg0H0CDIqypPO74M5MS9T6FAJZGdT6dQ=="], "sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.86.3", "", { "os": "linux", "cpu": "none" }, "sha512-NuXQ72dwfNLe35E+RaXJ4Noq4EkFwM65eWwCwxEWyJO9qxOx1EXiCAJii6x8kkOh5daWuMU0VAI1B9RsJaqqQQ=="],
"sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.85.1", "", { "os": "linux", "cpu": "x64" }, "sha512-uKRTv0z8NgtHV7xSren78+yoWB79sNi7TMqI7Bxd8fcRNIgHQSA8QBdF8led2ETC004hr8h71BrY60RPO+SSvA=="], "sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.86.3", "", { "os": "linux", "cpu": "x64" }, "sha512-t8be9zJ5B82+og9bQmIQ83yMGYZMTMrlGA+uGWtYacmwg6w3093dk91Fx0YzNSZBp3Tk60qVYjCZnEIwy60x0g=="],
"sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.85.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-/GMiZXBOc6AEMBC3g25Rp+x8fq9Z6Ql7037l5rajBPhZ+DdFwtdHY0Ou3oIU6XuWUwD06U3ii4XufXVFhsP6PA=="], "sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.86.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4ghuAzjX4q8Nksm0aifRz8hgXMMxS0SuymrFfkfJlrSx68pIgvAge6AOw0edoZoe0Tf5ZbsWUWamhkNyNxkTvw=="],
"sass-embedded-win32-ia32": ["sass-embedded-win32-ia32@1.85.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-L+4BWkKKBGFOKVQ2PQ5HwFfkM5FvTf1Xx2VSRvEWt9HxPXp6SPDho6zC8fqNQ3hSjoaoASEIJcSvgfdQYO0gdg=="], "sass-embedded-win32-ia32": ["sass-embedded-win32-ia32@1.86.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-tCaK4zIRq9mLRPxLzBAdYlfCuS/xLNpmjunYxeWkIwlJo+k53h1udyXH/FInnQ2GgEz0xMXyvH3buuPgzwWYsw=="],
"sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.85.1", "", { "os": "win32", "cpu": "x64" }, "sha512-/FO0AGKWxVfCk4GKsC0yXWBpUZdySe3YAAbQQL0lL6xUd1OiUY8Kow6g4Kc1TB/+z0iuQKKTqI/acJMEYl4iTQ=="], "sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.86.3", "", { "os": "win32", "cpu": "x64" }, "sha512-zS+YNKfTF4SnOfpC77VTb0qNZyTXrxnAezSoRV0xnw6HlY+1WawMSSB6PbWtmbvyfXNgpmJUttoTtsvJjRCucg=="],
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
@ -698,7 +725,7 @@
"sync-message-port": ["sync-message-port@1.1.3", "", {}, "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg=="], "sync-message-port": ["sync-message-port@1.1.3", "", {}, "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg=="],
"tailwindcss": ["tailwindcss@4.0.9", "", {}, "sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw=="], "tailwindcss": ["tailwindcss@4.1.3", "", {}, "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g=="],
"tailwindcss-primeui": ["tailwindcss-primeui@0.4.0", "", { "peerDependencies": { "tailwindcss": ">=3.1.0" } }, "sha512-YYC7B7Yyzm1/4pEGgpf1ABAhbrKY++LuPoUamnKE7fTPO5Ct/Qr/dT+Uq2yiVhQnaW1zHQpYnThxfksaxhlDfQ=="], "tailwindcss-primeui": ["tailwindcss-primeui@0.4.0", "", { "peerDependencies": { "tailwindcss": ">=3.1.0" } }, "sha512-YYC7B7Yyzm1/4pEGgpf1ABAhbrKY++LuPoUamnKE7fTPO5Ct/Qr/dT+Uq2yiVhQnaW1zHQpYnThxfksaxhlDfQ=="],
@ -714,13 +741,15 @@
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript-eslint": ["typescript-eslint@8.24.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/parser": "8.24.0", "@typescript-eslint/utils": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ=="], "typescript-eslint": ["typescript-eslint@8.29.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.29.0", "@typescript-eslint/parser": "8.29.0", "@typescript-eslint/utils": "8.29.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="], "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unplugin": ["unplugin@2.0.0-beta.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-2qzQo5LN2DmUZXkWDHvGKLF5BP0WN+KthD6aPnPJ8plRBIjv4lh5O07eYcSxgO2znNw9s4MNhEO1sB+JDllDbQ=="], "unplugin": ["unplugin@2.0.0-beta.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-2qzQo5LN2DmUZXkWDHvGKLF5BP0WN+KthD6aPnPJ8plRBIjv4lh5O07eYcSxgO2znNw9s4MNhEO1sB+JDllDbQ=="],
@ -736,19 +765,19 @@
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="], "varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
"vite": ["vite@6.2.0", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ=="], "vite": ["vite@6.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw=="],
"vite-plugin-vuetify": ["vite-plugin-vuetify@2.1.0", "", { "dependencies": { "@vuetify/loader-shared": "^2.1.0", "debug": "^4.3.3", "upath": "^2.0.1" }, "peerDependencies": { "vite": ">=5", "vue": "^3.0.0", "vuetify": "^3.0.0" } }, "sha512-4wEAQtZaigPpwbFcZbrKpYwutOsWwWdeXn22B9XHzDPQNxVsKT+K9lKcXZnI5JESO1Iaql48S9rOk8RZZEt+Mw=="], "vite-plugin-vuetify": ["vite-plugin-vuetify@2.1.1", "", { "dependencies": { "@vuetify/loader-shared": "^2.1.0", "debug": "^4.3.3", "upath": "^2.0.1" }, "peerDependencies": { "vite": ">=5", "vue": "^3.0.0", "vuetify": "^3.0.0" } }, "sha512-Pb7bKhQH8qPMzURmEGq2aIqCJkruFNsyf1NcrrtnjsOIkqJPMcBbiP0oJoO8/uAmyB5W/1JTbbUEsyXdMM0QHQ=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"vue": ["vue@3.5.13", "", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", "@vue/runtime-dom": "3.5.13", "@vue/server-renderer": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ=="], "vue": ["vue@3.5.13", "", { "dependencies": { "@vue/compiler-dom": "3.5.13", "@vue/compiler-sfc": "3.5.13", "@vue/runtime-dom": "3.5.13", "@vue/server-renderer": "3.5.13", "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ=="],
"vue-eslint-parser": ["vue-eslint-parser@9.4.3", "", { "dependencies": { "debug": "^4.3.4", "eslint-scope": "^7.1.1", "eslint-visitor-keys": "^3.3.0", "espree": "^9.3.1", "esquery": "^1.4.0", "lodash": "^4.17.21", "semver": "^7.3.6" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg=="], "vue-eslint-parser": ["vue-eslint-parser@10.1.2", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", "lodash": "^4.17.21", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-1guOfYgNlD7JH2popr/bt5vc7Mzt6quRCnEbqLgpMHvoHEGV1oImzdqrLd+oMD76cHt8ilBP4cda9WA72TLFDQ=="],
"vue-tsc": ["vue-tsc@2.2.8", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.8" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ=="], "vue-tsc": ["vue-tsc@2.2.8", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.8" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ=="],
"vuetify": ["vuetify@3.7.15", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=1.0.0", "vue": "^3.3.0", "webpack-plugin-vuetify": ">=2.0.0" }, "optionalPeers": ["typescript", "vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-kBZzwXI5EcAMiW5TRMgK1reXQd0K/PpUt+ekX4Alvm7n09uzJ1my1TLNbX1sQ8/0KYgoxOf17C8qOJzBGkT+PA=="], "vuetify": ["vuetify@3.8.1", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=2.1.0", "vue": "^3.5.0", "webpack-plugin-vuetify": ">=3.1.0" }, "optionalPeers": ["typescript", "vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-3qReKBBWIIdJJmwnFU1blVIKHDtnLfIP7kk0MwUrrfjYkWmsDpsymtDnsukkTCnlJ1WvhLr64eQFosr0RVbj9w=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
@ -776,13 +805,7 @@
"@primeuix/forms/@primeuix/utils": ["@primeuix/utils@0.4.1", "", {}, "sha512-5+1NLfyna+gLRPeFTo+xlR0tfPVLuVdidbeahAMLkQga5Rw0LxyUBCyD2/Zv2JkV69o2T+hpEDyddl3VdnYoBw=="], "@primeuix/forms/@primeuix/utils": ["@primeuix/utils@0.4.1", "", {}, "sha512-5+1NLfyna+gLRPeFTo+xlR0tfPVLuVdidbeahAMLkQga5Rw0LxyUBCyD2/Zv2JkV69o2T+hpEDyddl3VdnYoBw=="],
"@tauri-apps/plugin-deep-link/@tauri-apps/api": ["@tauri-apps/api@2.2.0", "", {}, "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg=="], "@tauri-apps/plugin-opener/@tauri-apps/api": ["@tauri-apps/api@2.3.0", "", {}, "sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA=="],
"@tauri-apps/plugin-dialog/@tauri-apps/api": ["@tauri-apps/api@2.2.0", "", {}, "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg=="],
"@tauri-apps/plugin-fs/@tauri-apps/api": ["@tauri-apps/api@2.2.0", "", {}, "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg=="],
"@tauri-apps/plugin-shell/@tauri-apps/api": ["@tauri-apps/api@2.2.0", "", {}, "sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg=="],
"@vue/compiler-sfc/postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="], "@vue/compiler-sfc/postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="],
@ -798,6 +821,8 @@
"eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"eslint-plugin-vue/vue-eslint-parser": ["vue-eslint-parser@9.4.3", "", { "dependencies": { "debug": "^4.3.4", "eslint-scope": "^7.1.1", "eslint-visitor-keys": "^3.3.0", "espree": "^9.3.1", "esquery": "^1.4.0", "lodash": "^4.17.21", "semver": "^7.3.6" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@ -808,18 +833,18 @@
"unplugin-vue-components/unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="], "unplugin-vue-components/unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="],
"vue-eslint-parser/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
"vue-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"vue-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"eslint-plugin-vue/vue-eslint-parser/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
"eslint-plugin-vue/vue-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"eslint-plugin-vue/vue-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
} }
} }

View File

@ -10,43 +10,46 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@f3ve/vue-markdown-it": "^0.2.3",
"@mdi/font": "7.4.47", "@mdi/font": "7.4.47",
"@primevue/forms": "^4.3.1", "@primevue/forms": "^4.3.3",
"@primevue/themes": "^4.3.1", "@primevue/themes": "^4.3.3",
"@tailwindcss/vite": "^4.0.9", "@tailwindcss/vite": "^4.1.3",
"@tauri-apps/api": "^2.3.0", "@tauri-apps/api": "^2.4.1",
"@tauri-apps/plugin-cli": "^2.2.0", "@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "~2.2.0", "@tauri-apps/plugin-deep-link": "~2.2.1",
"@tauri-apps/plugin-dialog": "~2.2.0", "@tauri-apps/plugin-dialog": "~2.2.1",
"@tauri-apps/plugin-fs": "^2.2.0", "@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.6", "@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-shell": "~2.2.0", "@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.6.1", "@tauri-apps/plugin-updater": "^2.7.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"pinia": "^3.0.1", "@types/markdown-it": "^14.1.2",
"markdown-it": "^14.1.0",
"pinia": "^3.0.2",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.3.1", "primevue": "^4.3.3",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"tailwindcss": "^4.0.9", "tailwindcss": "^4.1.3",
"tailwindcss-primeui": "^0.4.0", "tailwindcss-primeui": "^0.4.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vuetify": "^3.7.15" "vuetify": "^3.8.1"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.3.1", "@tauri-apps/cli": "^2.4.1",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.1",
"@types/node": "^22.13.9", "@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.3",
"@vue/eslint-config-typescript": "^14.4.0", "@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^7.0.2", "npm-run-all2": "^7.0.2",
"sass": "1.77.8", "sass": "1.77.8",
"sass-embedded": "^1.85.1", "sass-embedded": "^1.86.3",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"unplugin-fonts": "^1.3.1", "unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^0.27.5", "unplugin-vue-components": "^0.27.5",
"vite": "^6.2.0", "vite": "^6.2.6",
"vite-plugin-vuetify": "^2.1.0", "vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8" "vue-tsc": "^2.2.8"
} }
} }

View File

@ -0,0 +1,3 @@
If you're stuck on this screen, restart the game.
If the problem persists, <a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ#game-is-stuck-at-checking-distribution-server" target="_blank">check your network configuration</a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

8
public/help-finale.md Normal file
View File

@ -0,0 +1,8 @@
You can access this page any time by right-clicking the START button.
Additional resources:
- <a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ" target="_blank">SEGAguide</a>
- <a href="https://two-torial.xyz/" target="_blank">two-torial</a>
## Have fun

View File

@ -0,0 +1,3 @@
You also have to calibrate the lever, or you may get the error 3301.
Go to lever settings (<span class="bg-black text-white">レバー設定</span>), move the lever to both edges, then press "end" (<span class="bg-black text-white">終了</span>) and "save" (<span class="bg-black text-white">保存する</span>).

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -0,0 +1,3 @@
You might get stuck on this screen for several minutes. _This is normal_. The game just takes a long time to load data.
If you install <code>7EVENDAYSHOLIDAYS/LoadBoost</code>, subsequent launches will be much faster.

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

7
public/help-standard.md Normal file
View File

@ -0,0 +1,7 @@
You might get stuck on the following screen:
<div class="p-2 mt-1 mb-1 bg-black text-white">Aグループの基準機から設定を取得</div>
In which case, you should go to the test menu, and in game settings <span class="bg-black text-white">ゲーム設定</span> switch from "follow the standard machine" <span class="bg-black text-white">基準機に従う</span> to "standard machine" <span class="bg-black text-white">基準機</span>.
The test menu can be accessed with %TESTMENU%.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 43 KiB

BIN
res/cfg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
res/icon-chunithm.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
res/icon-ongeki.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 70 KiB

BIN
res/store.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

1161
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,6 @@ futures = "0.3.31"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
directories = "6.0.0" directories = "6.0.0"
rust-ini = "0.21.1" rust-ini = "0.21.1"
simple_logger = "5.0.0"
log = "0.4.25" log = "0.4.25"
regex = "1.11.1" regex = "1.11.1"
zip = "2.2.2" zip = "2.2.2"
@ -42,6 +41,11 @@ junction = "1.2.0"
tauri-plugin-fs = "2" tauri-plugin-fs = "2"
yaml-rust2 = "0.10.0" yaml-rust2 = "0.10.0"
enumflags2 = { version = "0.7.11", features = ["serde"] } enumflags2 = { version = "0.7.11", features = ["serde"] }
sha256 = "1.6.0"
serialport = "4.7.1"
fern = { version ="0.7.1", features = ["colored"] }
humantime = "2.2.0"
open = "5.3.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-cli = "2" tauri-plugin-cli = "2"
@ -49,5 +53,5 @@ tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
tauri-plugin-updater = "2" tauri-plugin-updater = "2"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winsafe = { version = "0.0.23", features = ["user"] } winsafe = { version = "0.0.23", features = ["user", "ole", "shell"] }
displayz = "^0.2.0" displayz = "^0.2.0"

View File

@ -12,6 +12,9 @@
"core:window:allow-set-focus", "core:window:allow-set-focus",
"core:window:allow-hide", "core:window:allow-hide",
"core:window:allow-show", "core:window:allow-show",
"core:window:allow-set-size",
"core:window:allow-inner-size",
"core:window:allow-set-min-size",
"core:app:allow-app-hide", "core:app:allow-app-hide",
"shell:default", "shell:default",
"dialog:default", "dialog:default",
@ -20,6 +23,7 @@
"fs:allow-data-read-recursive", "fs:allow-data-read-recursive",
"fs:allow-data-write-recursive", "fs:allow-data-write-recursive",
"fs:allow-config-read-recursive", "fs:allow-config-read-recursive",
"fs:allow-config-write-recursive" "fs:allow-config-write-recursive",
"shell:allow-open"
] ]
} }

BIN
rust/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
rust/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@ -1,11 +1,14 @@
use std::hash::{DefaultHasher, Hash, Hasher}; use std::hash::{DefaultHasher, Hash, Hasher};
use std::time::SystemTime;
use crate::model::config::GlobalConfig; use crate::model::config::GlobalConfig;
use crate::model::patch::PatchFileVec;
use crate::pkg::{Feature, Status}; use crate::pkg::{Feature, Status};
use crate::profiles::Profile; use crate::profiles::types::Profile;
use crate::{model::misc::Game, pkg::PkgKey}; use crate::{model::misc::Game, pkg::PkgKey};
use crate::pkg_store::PackageStore; use crate::pkg_store::PackageStore;
use crate::util; use crate::util;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use fern::colors::{Color, ColoredLevelConfig};
use tauri::AppHandle; use tauri::AppHandle;
pub struct GlobalState { pub struct GlobalState {
@ -17,6 +20,7 @@ pub struct AppData {
pub pkgs: PackageStore, pub pkgs: PackageStore,
pub cfg: GlobalConfig, pub cfg: GlobalConfig,
pub state: GlobalState, pub state: GlobalState,
pub patch_vec: PatchFileVec,
} }
#[derive(PartialEq, Debug, Copy, Clone)] #[derive(PartialEq, Debug, Copy, Clone)]
@ -32,18 +36,25 @@ impl AppData {
.and_then(|s| Ok(serde_json::from_str::<GlobalConfig>(&s)?)) .and_then(|s| Ok(serde_json::from_str::<GlobalConfig>(&s)?))
.unwrap_or_default(); .unwrap_or_default();
Self::init_logger(&cfg);
let profile = match cfg.recent_profile { let profile = match cfg.recent_profile {
Some((game, ref name)) => Profile::load(game, name.clone()).ok(), Some((game, ref name)) => Profile::load(game, name.clone()).ok(),
None => None None => None
}; };
log::debug!("Recent profile: {:?}", profile); let patch_vec = PatchFileVec::new(util::config_dir())
.map_err(|e| log::error!("unable to load patch set: {e}"))
.unwrap_or_default();
log::info!("recent profile: {:?}", profile);
AppData { AppData {
profile: profile, profile: profile,
pkgs: PackageStore::new(apph.clone()), pkgs: PackageStore::new(apph.clone()),
cfg, cfg,
state: GlobalState { remain_open: true } state: GlobalState { remain_open: true },
patch_vec
} }
} }
@ -78,8 +89,7 @@ impl AppData {
.clone() .clone()
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?; .ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
if let Status::OK(feature_set) = loc.status { if let Status::OK(feature_set, _) = loc.status {
log::debug!("{:?}", feature_set);
if feature_set.contains(Feature::Mod) { if feature_set.contains(Feature::Mod) {
profile.mod_pkgs_mut().insert(key); profile.mod_pkgs_mut().insert(key);
} }
@ -121,4 +131,38 @@ impl AppData {
p.fix(&self.pkgs); p.fix(&self.pkgs);
} }
} }
fn init_logger(cfg: &GlobalConfig) {
_ = std::fs::create_dir_all(util::data_dir());
let mut fern_builder;
let colors = ColoredLevelConfig::new()
.debug(Color::Green)
.info(Color::Blue)
.warn(Color::Yellow)
.error(Color::Red);
fern_builder = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
colors.color(record.level()),
record.target(),
message
))
})
.chain(std::io::stdout())
.chain(fern::log_file(util::data_dir().join("log.txt")).expect("unable to initialize the logger"));
if cfg.verbose == true {
fern_builder = fern_builder.level(log::LevelFilter::Debug);
} else {
fern_builder = fern_builder.level(log::LevelFilter::Info);
}
if let Err(e) = fern_builder.apply() {
panic!("unable to initialize the logger? {:?}", e);
}
}
} }

View File

@ -1,12 +1,17 @@
use ini::Ini;
use log; use log;
use std::collections::HashMap; use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::fs; use tokio::fs;
use tauri::{AppHandle, Manager, State}; use tauri::{AppHandle, Manager, State};
use crate::model::config::GlobalConfigField;
use crate::model::misc::Game; use crate::model::misc::Game;
use crate::model::patch::Patch;
use crate::modules::package::prepare_dlls;
use crate::pkg::{Package, PkgKey}; use crate::pkg::{Package, PkgKey};
use crate::pkg_store::{InstallResult, PackageStore}; use crate::pkg_store::{InstallResult, PackageStore};
use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths}; use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use crate::appdata::{AppData, ToggleAction}; use crate::appdata::{AppData, ToggleAction};
use crate::model::misc::StartCheckError; use crate::model::misc::StartCheckError;
use crate::util; use crate::util;
@ -55,18 +60,40 @@ pub async fn startline(app: AppHandle, refresh: bool) -> Result<(), String> {
let state = app.state::<Mutex<AppData>>(); let state = app.state::<Mutex<AppData>>();
let mut hash = "".to_owned(); let mut hash = "".to_owned();
let mut appd = state.lock().await; let appd = state.lock().await;
let mut game_dlls = Vec::new();
let mut amd_dlls = Vec::new();
if let Some(p) = &appd.profile { if let Some(p) = &appd.profile {
hash = appd.sum_packages(p); hash = appd.sum_packages(p);
(game_dlls, amd_dlls) = prepare_dlls(p.mod_pkgs(), &appd.pkgs).map_err(|e| e.to_string())?
} }
if let Some(p) = &mut appd.profile { if let Some(p) = &appd.profile {
log::debug!("{}", hash); log::debug!("{}", hash);
p.line_up(hash, refresh, app.clone()).await let info = p.prepare_display()
.map_err(|e| format!("Lineup failed:\n{}", e))?; .map_err(|e| e.to_string())?;
let lineup_res = p.line_up(hash, refresh, &appd.patch_vec).await
.map_err(|e| e.to_string());
#[cfg(target_os = "windows")]
if let Some(info) = info {
use crate::model::profile::Display;
if lineup_res.is_ok() {
Display::wait_for_exit(app.clone(), info);
} else {
Display::clean_up(&info).map_err(|e| e.to_string())?;
}
}
lineup_res?;
let app_clone = app.clone(); let app_clone = app.clone();
let p_clone = p.clone(); let p_clone = p.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
if let Err(e) = p_clone.start(app_clone).await { if let Err(e) = p_clone.start(StartPayload {
app: app_clone,
game_dlls,
amd_dlls
}).await {
log::error!("Startup failed:\n{}", e); log::error!("Startup failed:\n{}", e);
} }
}); });
@ -148,6 +175,10 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
let appd = state.lock().await; let appd = state.lock().await;
let pkgs_all = appd.pkgs.get_all();
log::debug!("pkgs_all: {:?}", pkgs_all);
Ok(appd.pkgs.get_all()) Ok(appd.pkgs.get_all())
} }
@ -292,9 +323,10 @@ pub async fn duplicate_profile(profile: ProfileMeta) -> Result<(), String> {
pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMeta) -> Result<(), String> { pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMeta) -> Result<(), String> {
log::debug!("invoke: delete_profile({:?})", profile); log::debug!("invoke: delete_profile({:?})", profile);
std::fs::remove_dir_all(profile.config_dir()) util::remove_dir_all(profile.config_dir())
.await
.map_err(|e| format!("Unable to delete {:?}: {}", profile.config_dir(), e))?; .map_err(|e| format!("Unable to delete {:?}: {}", profile.config_dir(), e))?;
if let Err(e) = std::fs::remove_dir_all(profile.data_dir()) { if let Err(e) = util::remove_dir_all(profile.data_dir()).await {
log::warn!("Unable to delete: {:?} {}", profile.data_dir(), e); log::warn!("Unable to delete: {:?} {}", profile.data_dir(), e);
} }
@ -318,7 +350,7 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
#[tauri::command] #[tauri::command]
pub async fn sync_current_profile(state: State<'_, Mutex<AppData>>, data: ProfileData) -> Result<(), String> { pub async fn sync_current_profile(state: State<'_, Mutex<AppData>>, data: ProfileData) -> Result<(), String> {
log::debug!("invoke: sync_current_profile"); log::debug!("invoke: sync_current_profile {:?}", data);
let mut appd = state.lock().await; let mut appd = state.lock().await;
if let Some(p) = &mut appd.profile { if let Some(p) = &mut appd.profile {
@ -344,6 +376,34 @@ pub async fn save_current_profile(state: State<'_, Mutex<AppData>>) -> Result<()
} }
} }
#[tauri::command]
pub async fn load_segatools_ini(state: State<'_, Mutex<AppData>>, path: PathBuf) -> Result<(), String> {
log::debug!("invoke: load_segatools_ini({:?})", path);
let mut appd = state.lock().await;
if let Some(p) = &mut appd.profile {
let str = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
// Stupid path escape hack for the ini reader
let str = str.replace("\\", "\\\\").replace("\\\\\\\\", "\\\\");
let ini = Ini::load_from_str(&str).map_err(|e| e.to_string())?;
p.data.sgt.load_from_ini(&ini, p.config_dir()).map_err(|e| e.to_string())?;
p.data.network.load_from_ini(&ini).map_err(|e| e.to_string())?;
if let Some(kb) = &mut p.data.keyboard {
kb.load_from_ini(&ini).map_err(|e| e.to_string())?;
}
p.save().map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
pub async fn create_shortcut(app: AppHandle, profile_meta: ProfileMeta) -> Result<(), String> {
log::debug!("invoke: create_shortcut({:?})", profile_meta);
util::create_shortcut(app, &profile_meta).map_err(|e| e.to_string())
}
#[tauri::command] #[tauri::command]
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> { pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
log::debug!("invoke: list_platform_capabilities"); log::debug!("invoke: list_platform_capabilities");
@ -355,6 +415,31 @@ pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
return Ok(vec!["wine".to_owned()]); return Ok(vec!["wine".to_owned()]);
} }
#[tauri::command]
pub async fn get_global_config(state: State<'_, Mutex<AppData>>, field: GlobalConfigField) -> Result<bool, ()> {
log::debug!("invoke: get_global_config({field:?})");
let appd = state.lock().await;
match field {
GlobalConfigField::OfflineMode => Ok(appd.cfg.offline_mode),
GlobalConfigField::EnableAutoupdates => Ok(appd.cfg.enable_autoupdates),
GlobalConfigField::Verbose => Ok(appd.cfg.verbose)
}
}
#[tauri::command]
pub async fn set_global_config(state: State<'_, Mutex<AppData>>, field: GlobalConfigField, value: bool) -> Result<(), String> {
log::debug!("invoke: set_global_config({field:?}, {value})");
let mut appd = state.lock().await;
match field {
GlobalConfigField::OfflineMode => appd.cfg.offline_mode = value,
GlobalConfigField::EnableAutoupdates => appd.cfg.enable_autoupdates = value,
GlobalConfigField::Verbose => appd.cfg.verbose = value,
};
appd.write().map_err(|e| e.to_string())
}
#[tauri::command] #[tauri::command]
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn list_displays() -> Result<Vec<(String, String)>, String> { pub async fn list_displays() -> Result<Vec<(String, String)>, String> {
@ -389,4 +474,48 @@ pub async fn list_directories() -> Result<util::Dirs, ()> {
log::debug!("invoke: list_directores"); log::debug!("invoke: list_directores");
Ok(util::all_dirs().clone()) Ok(util::all_dirs().clone())
}
// Tauri fs api is useless
#[tauri::command]
pub async fn file_exists(path: String) -> Result<bool, ()> {
Ok(std::fs::exists(path).unwrap_or(false))
}
// Easier than trying to get the barely-documented tauri permissions system to work
#[tauri::command]
pub async fn open_file(path: String) -> Result<(), String> {
open::that(path).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn get_changelog() -> Result<String, ()> {
Ok(include_str!("../../CHANGELOG.md").to_owned())
}
#[tauri::command]
pub async fn list_com_ports() -> Result<BTreeMap<String, i32>, String> {
let ports = serialport::available_ports().unwrap_or(Vec::new());
let mut res = BTreeMap::new();
for p in ports {
log::debug!("port {}", p.port_name);
if p.port_name.starts_with("COM") {
if let Ok(parsed) = (p.port_name[3..]).parse() {
res.insert(p.port_name, parsed);
}
}
}
Ok(res)
}
#[tauri::command]
pub async fn list_patches(state: State<'_, Mutex<AppData>>, target: String) -> Result<Vec<Patch>, String> {
log::debug!("invoke: list_patches({})", target);
let mut appd = state.lock().await;
appd.fix();
let list = appd.patch_vec.find_patches(target).map_err(|e| e.to_string())?;
Ok(list)
} }

View File

@ -1,4 +1,5 @@
use std::{collections::HashSet, path::PathBuf}; use std::{collections::HashSet, path::PathBuf};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
use tokio::fs::File; use tokio::fs::File;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
@ -6,14 +7,20 @@ use anyhow::{anyhow, Result};
use crate::pkg::{Package, PkgKey, Remote}; use crate::pkg::{Package, PkgKey, Remote};
pub struct DownloadHandler { pub struct DownloadHandler {
set: HashSet<String>, paths: HashSet<PathBuf>,
app: AppHandle app: AppHandle
} }
#[derive(Serialize, Deserialize, Clone)]
pub struct DownloadTick {
pkg_key: PkgKey,
ratio: f32,
}
impl DownloadHandler { impl DownloadHandler {
pub fn new(app: AppHandle) -> DownloadHandler { pub fn new(app: AppHandle) -> DownloadHandler {
DownloadHandler { DownloadHandler {
set: HashSet::new(), paths: HashSet::new(),
app app
} }
} }
@ -22,11 +29,11 @@ impl DownloadHandler {
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();
if self.set.contains(zip_path.to_string_lossy().as_ref()) { if self.paths.contains(zip_path) {
// Todo when there is a clear cache button, it should clear the set Ok(())
Err(anyhow!("Already downloading"))
} else { } else {
self.set.insert(zip_path.to_string_lossy().to_string()); // TODO clear cache button should clear this
self.paths.insert(zip_path.clone());
tauri::async_runtime::spawn(Self::download_zip_proc(self.app.clone(), zip_path.clone(), pkg.key(), rmt)); tauri::async_runtime::spawn(Self::download_zip_proc(self.app.clone(), zip_path.clone(), pkg.key(), rmt));
Ok(()) Ok(())
} }
@ -42,16 +49,22 @@ impl DownloadHandler {
let mut cache_file_w = File::create(&zip_path_part).await?; let mut cache_file_w = File::create(&zip_path_part).await?;
let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream(); let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream();
let mut total_bytes = 0;
log::info!("Downloading: {}", rmt.download_url); log::info!("downloading: {}", rmt.download_url);
while let Some(item) = byte_stream.next().await { while let Some(item) = byte_stream.next().await {
let i = item?; let i = item?;
total_bytes += i.len();
_ = app.emit("download-progress", DownloadTick {
pkg_key: pkg_key.clone(),
ratio: (total_bytes as f32) / (rmt.file_size as f32),
})?;
cache_file_w.write_all(&mut i.as_ref()).await?; cache_file_w.write_all(&mut i.as_ref()).await?;
} }
cache_file_w.sync_all().await?; cache_file_w.sync_all().await?;
tokio::fs::rename(&zip_path_part, &zip_path).await?; tokio::fs::rename(&zip_path_part, &zip_path).await?;
log::debug!("Downloaded to {:?}", zip_path); log::debug!("downloaded to {:?}", zip_path);
app.emit("download-end", pkg_key)?; app.emit("download-end", pkg_key)?;

View File

@ -7,6 +7,7 @@ mod download_handler;
mod appdata; mod appdata;
mod modules; mod modules;
mod profiles; mod profiles;
mod patcher;
use std::sync::OnceLock; use std::sync::OnceLock;
use anyhow::anyhow; use anyhow::anyhow;
@ -15,26 +16,15 @@ use appdata::{AppData, ToggleAction};
use model::misc::Game; use model::misc::Game;
use pkg::PkgKey; use pkg::PkgKey;
use pkg_store::Payload; use pkg_store::Payload;
use tauri::{AppHandle, Listener, Manager, RunEvent}; use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent};
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_cli::CliExt; use tauri_plugin_cli::CliExt;
use tauri_plugin_updater::UpdaterExt;
use tokio::{fs, sync::Mutex, try_join}; use tokio::{fs, sync::Mutex, try_join};
static EXIT_REQUESTED: OnceLock<()> = OnceLock::new(); static EXIT_REQUESTED: OnceLock<()> = OnceLock::new();
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub async fn run(_args: Vec<String>) { pub async fn run(_args: Vec<String>) {
simple_logger::init_with_env().expect("Unable to initialize the logger");
log::info!(
"Running from {}",
std::env::current_dir()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
);
let tauri = tauri::Builder::default() let tauri = tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
@ -58,6 +48,15 @@ pub async fn run(_args: Vec<String>) {
util::init_dirs(&apph); util::init_dirs(&apph);
let mut app_data = AppData::new(app.handle().clone()); let mut app_data = AppData::new(app.handle().clone());
log::info!(
"running from {}",
std::env::current_dir()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
);
let start_immediately; let start_immediately;
if let Ok(matches) = app.cli().matches() { if let Ok(matches) = app.cli().matches() {
@ -67,13 +66,8 @@ pub async fn run(_args: Vec<String>) {
log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg); log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg);
if start_arg.occurrences > 0 { if start_arg.occurrences > 0 {
start_immediately = true; start_immediately = true;
app_data.state.remain_open = false;
} else { } else {
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into())) open_window(apph.clone())?;
.title("STARTLINER")
.inner_size(720f64, 480f64)
.min_inner_size(720f64, 480f64)
.build()?;
start_immediately = false; start_immediately = false;
} }
@ -108,14 +102,15 @@ pub async fn run(_args: Vec<String>) {
}); });
app.listen("download-end", closure!(clone apph, |ev| { app.listen("download-end", closure!(clone apph, |ev| {
log::debug!("download-end triggered: {}", ev.payload());
let raw = ev.payload(); let raw = ev.payload();
log::debug!("download-end triggered: {}", raw);
let key = PkgKey(raw[1..raw.len()-1].to_owned()); let key = PkgKey(raw[1..raw.len()-1].to_owned());
let apph = apph.clone(); let apph = apph.clone();
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;
log::debug!("download-end install {:?}", appd.pkgs.install_package(&key, true, false).await); let res = appd.pkgs.install_package(&key, true, false).await;
log::debug!("download-end install {:?}", res);
}); });
})); }));
@ -132,19 +127,21 @@ pub async fn run(_args: Vec<String>) {
})); }));
app.listen("install-end-prelude", closure!(clone apph, |ev| { app.listen("install-end-prelude", closure!(clone apph, |ev| {
log::debug!("install-end-prelude triggered: {}", ev.payload());
let payload = serde_json::from_str::<Payload>(ev.payload()); let payload = serde_json::from_str::<Payload>(ev.payload());
log::debug!("install-end-prelude triggered: {:?}", payload);
let apph = apph.clone(); let apph = apph.clone();
if let Ok(payload) = payload { if let Ok(payload) = payload {
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);
log::debug!( log::debug!(
"install-end-prelude toggle {:?}", "install-end-prelude toggle {:?}",
appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf) res
); );
use tauri::Emitter; use tauri::Emitter;
log::debug!("install-end {:?}", apph.emit("install-end", payload)); let res = apph.emit("install-end", payload);
log::debug!("install-end {:?}", res);
}); });
} else { } else {
log::error!("install-end-prelude: invalid payload: {}", ev.payload()); log::error!("install-end-prelude: invalid payload: {}", ev.payload());
@ -158,13 +155,20 @@ pub async fn run(_args: Vec<String>) {
{ {
let mut appd = mtx.lock().await; let mut appd = mtx.lock().await;
if let Err(e) = appd.pkgs.reload_all().await { if let Err(e) = appd.pkgs.reload_all().await {
log::error!("Unable to reload packages: {}", e); log::error!("unable to reload packages: {}", e);
apph.exit(1); apph.exit(1);
} }
} }
if let Err(e) = cmd::startline(apph.clone(), false).await { if let Err(e) = cmd::startline(apph.clone(), false).await {
log::error!("Unable to launch: {}", e); log::error!("unable to launch: {}", e);
apph.exit(1); _ = open_window(apph.clone());
// stupid but effective
std::thread::sleep(std::time::Duration::from_secs(3));
_ = apph.emit("launch-error", e.to_string());
} else {
let mut appd = mtx.lock().await;
appd.state.remain_open = false;
log::info!("started quietly");
} }
}); });
} else { } else {
@ -199,10 +203,22 @@ pub async fn run(_args: Vec<String>) {
cmd::get_current_profile, cmd::get_current_profile,
cmd::sync_current_profile, cmd::sync_current_profile,
cmd::save_current_profile, cmd::save_current_profile,
cmd::load_segatools_ini,
cmd::create_shortcut,
cmd::get_global_config,
cmd::set_global_config,
cmd::list_displays, cmd::list_displays,
cmd::list_platform_capabilities, cmd::list_platform_capabilities,
cmd::list_directories, cmd::list_directories,
cmd::file_exists,
cmd::open_file,
cmd::get_changelog,
cmd::list_com_ports,
cmd::list_patches,
]) ])
.build(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while building tauri application"); .expect("error while building tauri application");
@ -219,7 +235,8 @@ pub async fn run(_args: Vec<String>) {
let mutex = app.state::<Mutex<AppData>>(); let mutex = app.state::<Mutex<AppData>>();
let appd = mutex.lock().await; let appd = mutex.lock().await;
if let Some(p) = &appd.profile { if let Some(p) = &appd.profile {
log::debug!("save: {:?}", p.save()); let res = p.save();
log::debug!("save: {:?}", res);
app.exit(0); app.exit(0);
} }
}); });
@ -237,7 +254,7 @@ fn deep_link(app: AppHandle, args: Vec<String>) {
let url = &args[1]; let url = &args[1];
let proto = "rainycolor://"; let proto = "rainycolor://";
if &url[..proto.len()] == proto { if &url[..proto.len()] == proto {
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/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
@ -262,23 +279,64 @@ fn deep_link(app: AppHandle, args: Vec<String>) {
} }
async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> { async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
if let Some(update) = app.updater()?.check().await? { let mutex = app.state::<Mutex<AppData>>();
let mut downloaded = 0; {
update let appd = mutex.lock().await;
.download_and_install( if !appd.cfg.enable_autoupdates {
|chunk_length, content_length| { log::info!("skipping auto-update");
downloaded += chunk_length;
log::debug!("downloaded {downloaded} from {content_length:?}");
},
|| {
log::info!("download finished");
},
)
.await?;
log::info!("update installed"); return Ok(());
app.restart(); }
} }
#[cfg(not(debug_assertions))]
{
use tauri_plugin_updater::UpdaterExt;
use tauri::Emitter;
if let Some(update) = app.updater()?.check().await? {
let mut downloaded = 0;
update.download_and_install(
|chunk_length, content_length| {
downloaded += chunk_length;
_ = app.emit("update-progress", (downloaded as f64) / (content_length.unwrap_or(u64::MAX) as f64));
},
|| {
log::info!("download finished");
},
)
.await?;
log::info!("update installed");
app.restart();
}
}
// One day I will write proper tests
// #[cfg(debug_assertions)]
// {
// use tauri::Emitter;
// std::thread::sleep(std::time::Duration::from_millis(5000));
// let mut downloaded = 0;
// while downloaded < 500 {
// std::thread::sleep(std::time::Duration::from_millis(10));
// downloaded += 1;
// _ = app.emit("update-progress", (downloaded as f32) / 500f32);
// }
// app.restart();
// }
log::info!("ending auto-update check");
Ok(())
}
fn open_window(apph: AppHandle) -> anyhow::Result<()> {
let config = apph.config().clone();
tauri::WebviewWindowBuilder::new(&apph, "main", tauri::WebviewUrl::App("index.html".into()))
.title(format!("STARTLINER {}", config.version.unwrap_or_default()))
.inner_size(900f64, 600f64)
.min_inner_size(900f64, 600f64)
.build()?;
Ok(()) Ok(())
} }

View File

@ -1,10 +1,30 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::misc::Game; use super::misc::Game;
#[derive(Serialize, Deserialize, Clone, Default)] #[derive(Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct GlobalConfig { pub struct GlobalConfig {
pub recent_profile: Option<(Game, String)>, pub recent_profile: Option<(Game, String)>,
#[serde(default)]
pub offline_mode: bool, pub offline_mode: bool,
pub enable_autoupdates: bool,
pub verbose: bool,
}
impl Default for GlobalConfig {
fn default() -> Self {
Self {
recent_profile: Default::default(),
offline_mode: false,
enable_autoupdates: true,
verbose: false,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum GlobalConfigField {
OfflineMode,
EnableAutoupdates,
Verbose
} }

View File

@ -1,6 +1,6 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::pkg::{Status, PkgKey, PkgKeyVersion}; use crate::pkg::{PkgKey, PkgKeyVersion};
use super::misc::Game; use super::misc::Game;
@ -14,7 +14,10 @@ pub struct PackageManifest {
pub dependencies: BTreeSet<PkgKeyVersion>, pub dependencies: BTreeSet<PkgKeyVersion>,
#[serde(default)] #[serde(default)]
pub installers: Vec<BTreeMap<String, serde_json::Value>> pub installers: Vec<BTreeMap<String, serde_json::Value>>,
#[serde(default)]
pub games: Option<Vec<Game>>,
} }
pub type PackageList = BTreeMap<PkgKey, PackageListEntry>; pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
@ -22,6 +25,5 @@ pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct PackageListEntry { pub struct PackageListEntry {
pub version: String, pub version: String,
pub status: Status,
pub games: Vec<Game>, pub games: Vec<Game>,
} }

View File

@ -5,10 +5,9 @@ use crate::pkg::PkgKey;
use super::profile::ProfileModule; use super::profile::ProfileModule;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Game { pub enum Game {
#[serde(rename = "ongeki")]
Ongeki, Ongeki,
#[serde(rename = "chunithm")]
Chunithm, Chunithm,
} }
@ -21,6 +20,13 @@ impl Game {
} }
} }
pub fn print(&self) -> &'static str {
match self {
Game::Ongeki => "O.N.G.E.K.I.",
Game::Chunithm => "CHUNITHM"
}
}
pub fn hook_exe(&self) -> &'static str { pub fn hook_exe(&self) -> &'static str {
match self { match self {
Game::Ongeki => "mu3hook.dll", Game::Ongeki => "mu3hook.dll",
@ -59,14 +65,14 @@ impl Game {
pub fn amd_args(&self) -> Vec<&'static str> { pub fn amd_args(&self) -> Vec<&'static str> {
match self { match self {
Game::Ongeki => vec!["-f", "-c", "config_common.json", "config_server.json", "config_client.json"], Game::Ongeki => vec!["-f", "-c", "config_common.json", "config_server.json", "config_client.json"],
Game::Chunithm => vec!["-c", "config_common.json", "config_server.json", "config_client.json", "config_cvt.json", "config_sp.json", "config_hook.json"] Game::Chunithm => vec!["-c", "config_common.json", "config_server.json", "config_client.json", "config_cvt.json", "config_sp.json"]
} }
} }
pub fn has_module(&self, module: ProfileModule) -> bool { pub fn has_module(&self, module: ProfileModule) -> bool {
match self { match self {
Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini}), Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini | Keyboard}),
Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Network}), Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Display | Network | Keyboard | Mempatcher}),
}.contains(module) }.contains(module)
} }
} }
@ -86,4 +92,28 @@ pub enum StartCheckError {
MissingLocalPackage(PkgKey), MissingLocalPackage(PkgKey),
MissingDependency(PkgKey, PkgKey), MissingDependency(PkgKey, PkgKey),
MissingTool(PkgKey), MissingTool(PkgKey),
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct ConfigHook {
#[serde(skip_serializing_if = "Option::is_none")]
pub allnet_auth: Option<ConfigHookAuth>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aime: Option<ConfigHookAime>,
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct ConfigHookAuth {
pub r#type: String
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct ConfigHookAime {
pub unit: Vec<ConfigHookAimeUnit>
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct ConfigHookAimeUnit {
pub port: i32,
pub id: i32
} }

View File

@ -3,4 +3,4 @@ pub mod misc;
pub mod rainy; pub mod rainy;
pub mod profile; pub mod profile;
pub mod config; pub mod config;
pub mod segatools_base; pub mod patch;

162
rust/src/model/patch.rs Normal file
View File

@ -0,0 +1,162 @@
use std::collections::BTreeMap;
use serde::{de, Deserialize, Deserializer, Serialize};
use serde::ser::{Serializer, SerializeStruct};
use serde_json::Value;
use anyhow::Result;
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct PatchSelection(pub BTreeMap<String, PatchSelectionData>);
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum PatchSelectionData {
Enabled,
Number(i8),
Hex(Vec<u8>)
}
#[derive(Default)]
pub struct PatchFileVec(pub Vec<PatchFile>);
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct PatchFile(pub Vec<PatchList>);
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct PatchList {
pub filename: String,
pub version: String,
pub sha256: String,
pub patches: Vec<Patch>
}
#[derive(Clone, Debug)]
pub struct Patch {
pub id: String,
pub name: String,
pub tooltip: Option<String>,
pub data: PatchData
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(untagged)]
pub enum PatchData {
Normal(NormalPatch),
Number(NumberPatch),
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct NormalPatch {
pub patches: Vec<NormalPatchField>
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct NormalPatchField {
pub offset: u64,
pub off: Vec<u8>,
pub on: Vec<u8>
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct NumberPatch {
pub offset: u64,
pub default: i32,
pub size: i64,
pub min: i32,
pub max: i32
}
impl Serialize for Patch {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
let mut state = serializer.serialize_struct("Patch", 7)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("name", &self.name)?;
state.serialize_field("tooltip", &self.tooltip)?;
match &self.data {
PatchData::Normal(patch) => {
state.serialize_field("patches", &patch.patches)?;
}
PatchData::Number(patch) => {
state.serialize_field("type", "number")?;
state.serialize_field("offset", &patch.offset)?;
state.serialize_field("default", &patch.default)?;
state.serialize_field("size", &patch.size)?;
state.serialize_field("min", &patch.min)?;
state.serialize_field("max", &patch.max)?;
}
}
state.end()
}
}
impl<'de> serde::Deserialize<'de> for Patch {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let value = Value::deserialize(d)?;
let data = Ok(match value.get("type").and_then(Value::as_str) {
Some("number") => PatchData::Number(NumberPatch {
offset: value.get("offset")
.and_then(Value::as_u64)
.ok_or_else(|| de::Error::missing_field("offset"))?,
default: i32::try_from(value.get("default")
.and_then(Value::as_i64)
.ok_or_else(|| de::Error::missing_field("default"))?
).map_err(|_| de::Error::missing_field("default"))?,
size: value.get("size")
.and_then(Value::as_i64)
.ok_or_else(|| de::Error::missing_field("size"))?,
min: i32::try_from(value.get("min")
.and_then(Value::as_i64)
.ok_or_else(|| de::Error::missing_field("min"))?
).map_err(|_| de::Error::missing_field("min"))?,
max: i32::try_from(value.get("max")
.and_then(Value::as_i64)
.ok_or_else(|| de::Error::missing_field("max"))?
).map_err(|_| de::Error::missing_field("max"))?
}),
None => {
let mut patches = vec![];
for patch in value.get("patches").and_then(Value::as_array).unwrap() {
let mut off_list: Vec<u8> = Vec::new();
let mut on_list: Vec<u8> = Vec::new();
for off in patch.get("off").and_then(Value::as_array).unwrap() {
off_list.push(u8::try_from(
off.as_u64().ok_or_else(|| de::Error::missing_field("off"))?
).map_err(|_| de::Error::missing_field("off"))?);
}
for on in patch.get("on").and_then(Value::as_array).unwrap() {
on_list.push(u8::try_from(
on.as_u64().ok_or_else(|| de::Error::missing_field("on"))?
).map_err(|_| de::Error::missing_field("on"))?);
}
patches.push(NormalPatchField {
offset: patch.get("offset")
.and_then(Value::as_u64)
.ok_or_else(|| de::Error::missing_field("offset"))?,
off: off_list,
on: on_list
})
}
PatchData::Normal(NormalPatch {
patches
})
},
Some(&_) => return Err(de::Error::custom("unsupported type"))
});
Ok(Patch {
id: value.get("id")
.and_then(Value::as_str)
.ok_or_else(|| de::Error::missing_field("id"))?
.to_owned(),
name: value.get("name")
.and_then(Value::as_str)
.ok_or_else(|| de::Error::missing_field("name"))?
.to_owned(),
tooltip: value.get("tooltip")
.and_then(Value::as_str)
.and_then(|s| Some(s.to_owned())),
data: data?
})
}
}

View File

@ -13,7 +13,16 @@ pub enum Aime {
Other(PkgKey), Other(PkgKey),
} }
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
#[serde(rename_all = "snake_case")]
pub enum IOSelection {
Hardware,
#[default] SegatoolsBuiltIn,
Custom(PkgKey)
}
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct AMNet { pub struct AMNet {
pub name: String, pub name: String,
pub addr: String, pub addr: String,
@ -26,19 +35,21 @@ impl Default for AMNet {
} }
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug, Default )]
#[serde(default)]
pub struct Segatools { pub struct Segatools {
pub target: PathBuf, pub target: PathBuf,
pub hook: Option<PkgKey>, pub hook: Option<PkgKey>,
#[serde(skip_serializing_if = "Option::is_none")]
pub io: Option<PkgKey>, pub io: Option<PkgKey>,
#[serde(default)] pub io2: IOSelection,
pub aime: Aime, pub aime: Aime,
pub amfs: PathBuf, pub amfs: PathBuf,
pub option: PathBuf, pub option: PathBuf,
pub appdata: PathBuf, pub appdata: PathBuf,
pub intel: bool, pub intel: bool,
#[serde(default)]
pub amnet: AMNet, pub amnet: AMNet,
pub aime_port: Option<i32>,
} }
impl Segatools { impl Segatools {
@ -50,12 +61,14 @@ impl Segatools {
Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned())) Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned()))
}, },
io: None, io: None,
io2: IOSelection::SegatoolsBuiltIn,
amfs: PathBuf::default(), amfs: PathBuf::default(),
option: PathBuf::default(), option: PathBuf::default(),
appdata: PathBuf::from("appdata"), appdata: PathBuf::from("appdata"),
aime: Aime::default(), aime: Aime::default(),
intel: false, intel: false,
amnet: AMNet::default(), amnet: AMNet::default(),
aime_port: None
} }
} }
} }
@ -67,14 +80,17 @@ pub enum DisplayMode {
Fullscreen Fullscreen
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(default)]
pub struct Display { pub struct Display {
pub target: String, pub target: String,
pub rez: (i32, i32), pub rez: (i32, i32),
pub mode: DisplayMode, pub mode: DisplayMode,
pub rotation: i32, pub rotation: Option<i32>,
pub frequency: i32, pub frequency: i32,
pub borderless_fullscreen: bool, pub borderless_fullscreen: bool,
pub dont_switch_primary: bool,
pub monitor_index_override: Option<i32>,
} }
impl Display { impl Display {
@ -86,12 +102,14 @@ impl Display {
Game::Ongeki => (1080, 1920), Game::Ongeki => (1080, 1920),
}, },
mode: DisplayMode::Borderless, mode: DisplayMode::Borderless,
rotation: 0, rotation: None,
frequency: match game { frequency: match game {
Game::Chunithm => 120, Game::Chunithm => 120,
Game::Ongeki => 60, Game::Ongeki => 60,
}, },
borderless_fullscreen: true, borderless_fullscreen: true,
dont_switch_primary: false,
monitor_index_override: None,
} }
} }
} }
@ -103,6 +121,7 @@ pub enum NetworkType {
} }
#[derive(Deserialize, Serialize, Clone, Default, Debug)] #[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(default)]
pub struct Network { pub struct Network {
pub network_type: NetworkType, pub network_type: NetworkType,
@ -117,11 +136,13 @@ pub struct Network {
} }
#[derive(Deserialize, Serialize, Clone, Default, Debug)] #[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(default)]
pub struct BepInEx { pub struct BepInEx {
pub console: bool, pub console: bool,
} }
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
#[serde(default)]
pub struct Wine { pub struct Wine {
pub runtime: PathBuf, pub runtime: PathBuf,
pub prefix: PathBuf, pub prefix: PathBuf,
@ -146,21 +167,109 @@ pub enum Mu3Audio {
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct Mu3Ini { pub struct Mu3Ini {
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<Mu3Audio>, pub audio: Option<Mu3Audio>,
pub sample_rate: i32,
#[serde(skip_serializing_if = "Option::is_none")]
pub blacklist: Option<(i32, i32)>, pub blacklist: Option<(i32, i32)>,
pub gp: i32,
pub enable_bonus_tracks: bool,
}
impl Default for Mu3Ini {
fn default() -> Self {
Self {
audio: Some(Mu3Audio::Shared),
sample_rate: 48_000,
blacklist: Some((10000, 19999)),
gp: 999,
enable_bonus_tracks: true
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct OngekiKeyboard {
pub enabled: bool,
pub use_mouse: bool,
pub coin: i32,
pub svc: i32,
pub test: i32,
pub lmenu: i32,
pub rmenu: i32,
pub l1: i32,
pub l2: i32,
pub l3: i32,
pub r1: i32,
pub r2: i32,
pub r3: i32,
pub lwad: i32,
pub rwad: i32,
}
impl Default for OngekiKeyboard {
fn default() -> Self {
Self {
enabled: true,
use_mouse: true,
test: 0x70,
svc: 0x71,
coin: 0x72,
lmenu: 0x55,
rmenu: 0x4F,
lwad: 0x01,
rwad: 0x02,
l1: 0x41,
l2: 0x53,
l3: 0x44,
r1: 0x4A,
r2: 0x4B,
r3: 0x4C
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct ChunithmKeyboard {
pub enabled: bool,
pub coin: i32,
pub svc: i32,
pub test: i32,
pub cell: [i32; 32],
pub ir: [i32; 6],
}
impl Default for ChunithmKeyboard {
fn default() -> Self {
Self {
enabled: true,
test: 0x70,
svc: 0x71,
coin: 0x72,
cell: Default::default(),
ir: Default::default(),
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(tag = "game", content = "data")]
pub enum Keyboard {
Ongeki(OngekiKeyboard),
Chunithm(ChunithmKeyboard),
} }
#[bitflags] #[bitflags]
#[repr(u8)] #[repr(u16)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ProfileModule { pub enum ProfileModule {
Segatools, Segatools,
Network, Network,
Display, Display,
BepInEx, BepInEx,
Mu3Ini Mu3Ini,
Keyboard,
Mempatcher
} }

View File

@ -22,4 +22,5 @@ pub struct V1Version {
pub icon: String, pub icon: String,
pub dependencies: BTreeSet<PkgKeyVersion>, pub dependencies: BTreeSet<PkgKeyVersion>,
pub download_url: String, pub download_url: String,
pub file_size: i64,
} }

View File

@ -1,298 +0,0 @@
use super::misc::Game;
pub fn segatools_base(game: Game) -> String {
match game {
Game::Ongeki =>
"[vfd]
; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly.
enable=1
[system]
; Enable ALLS system settings.
enable=1
; Enable freeplay mode. This will disable the coin slot and set the game to
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
; allow you to start a game in freeplay mode.
freeplay=0
; LAN Install: Set this to 1 on all machines.
dipsw1=1
[gfx]
; Enables the graphics hook.
enable=1
[led15093]
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
; RGBs and the rear LED panel (billboard) on the cabinet.
enable=1
[led]
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\ongeki_led\"
cabLedOutputPipe=1
; Output billboard LED strip data to serial
cabLedOutputSerial=0
; Output slider LED data to the named pipe
controllerLedOutputPipe=1
; Output slider LED data to the serial port
controllerLedOutputSerial=0
[io4]
; Test button virtual-key code. Default is the F1 key.
test=0x70
; Service button virtual-key code. Default is the F2 key.
service=0x71
; Keyboard button to increment coin counter. Default is the F3 key.
coin=0x72
; Set \"1\" to enable mouse lever emulation, \"0\" to use XInput
mouse=1
; XInput input bindings
;
; Left Stick Lever
; Left Trigger Lever (move to the left)
; Right Trigger Lever (move to the right)
; Left Left red button
; Up Left green button
; Right Left blue button
; Left Shoulder Left side button
; Right Shoulder Right side button
; X Right red button
; Y Right green button
; A Right blue button
; Back Left menu button
; Start Right menu button
; Keyboard input bindings
left1=0x41 ; A
left2=0x53 ; S
left3=0x44 ; D
leftSide=0x01 ; Mouse Left
rightSide=0x02 ; Mouse Right
right1=0x4A ; J
right2=0x4B ; K
right3=0x4C ; L
leftMenu=0x55 ; U
rightMenu=0x4F ; O".to_owned(),
Game::Chunithm => "
[vfd]
; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly.
enable=1
[system]
; Enable ALLS system settings.
enable=1
; Enable freeplay mode. This will disable the coin slot and set the game to
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
; allow you to start a game in freeplay mode.
freeplay=0
; LAN Install: If multiple machines are present on the same LAN then set
; this to 1 on exactly one machine and set this to 0 on all others.
dipsw1=1
; Monitor type: 0 = 120FPS, 1 = 60FPS
dipsw2=1
; Cab type: 0 = SP, 1 = CVT. SP will enable VFD and eMoney. This setting will switch
; the LED 837-15093-06 COM port and the AiMe reder hardware generation as well.
dipsw3=1
; -----------------------------------------------------------------------------
; Misc. hooks settings
; -----------------------------------------------------------------------------
[gfx]
; Enables the graphics hook.
enable=1
; Force the game to run windowed.
windowed=1
; Add a frame to the game window if running windowed.
framed=0
; Select the monitor to run the game on. (Fullscreen only, 0 =primary screen)
monitor=0
; -----------------------------------------------------------------------------
; LED settings
; -----------------------------------------------------------------------------
[led15093]
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
; RGBs and the rear LED panel (billboard) on the cabinet.
enable=1
[led]
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\chuni_led\"
cabLedOutputPipe=1
; Output billboard LED strip data to serial
cabLedOutputSerial=0
; Output slider LED data to the named pipe
controllerLedOutputPipe=1
; Output slider LED data to the serial port
controllerLedOutputSerial=0
; Use the OpeNITHM protocol for serial LED output
controllerLedOutputOpeNITHM=0
; Serial port to send data to if using serial output. Default is COM5.
;serialPort=COM5
; Baud rate for serial data (set to 115200 if using OpeNITHM)
;serialBaud=921600
; Data output a sequence of bytes, with JVS-like framing.
; Each \"packet\" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere,
; 0xD0 is used as an escape character -- if you receive D0 in the output, ignore
; it and use the next sent byte plus one instead.
;
; After the sync is one byte for the board number that was updated, followed by
; the red, green and blue values for each LED.
;
; Board 0 has 53 LEDs:
; [0]-[49]: snakes through left half of billboard (first column starts at top)
; [50]-[52]: left side partition LEDs
;
; Board 1 has 63 LEDs:
; [0]-[59]: right half of billboard (first column starts at bottom)
; [60]-[62]: right side partition LEDs
;
; Board 2 is the slider and has 31 LEDs:
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers
; -----------------------------------------------------------------------------
; Custom IO settings
; -----------------------------------------------------------------------------
[chuniio]
; Uncomment this if you have custom chuniio implementation comprised of a single 32bit DLL.
; (will use chu2to3 engine internally)
;path=
; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs.
; x86 chuniio to path32, x64 to path64. Both are necessary.
;path32=
;path64=
; -----------------------------------------------------------------------------
; Input settings
; -----------------------------------------------------------------------------
; Keyboard bindings are specified as hexadecimal (prefixed with 0x) or decimal
; (not prefixed with 0x) virtual-key codes, a list of which can be found here:
;
; https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
;
; This is, admittedly, not the most user-friendly configuration method in the
; world. An improved solution will be provided later.
[io3]
test=0x31
service=0x32
coin=0x33
ir=0x00
ir6=0x39
ir5=0x38
ir4=0x37
ir3=0x36
ir2=0x35
ir1=0x34
[ir]
ir6=0x39
ir5=0x38
ir4=0x37
ir3=0x36
ir2=0x35
ir1=0x34
[slider]
cell32=0x51
cell30=0x5A
cell28=0x53
cell26=0x45
cell24=0x43
cell22=0x46
cell20=0x54
cell18=0x42
cell16=0x48
cell14=0x55
cell12=0x4D
cell10=0x4B
cell8=0x4F
cell6=190
cell4=186
cell2=219
cell31=0x41
cell29=0x57
cell27=0x58
cell25=0x44
cell23=0x52
cell21=0x56
cell19=0x47
cell17=0x59
cell15=0x4E
cell13=0x4A
cell11=0x49
cell9=188
cell7=0x4C
cell5=0x50
cell3=191
cell1=222
".to_owned()
}
}

View File

@ -1,20 +1,20 @@
use crate::{model::{misc::Game, profile::{Display, DisplayMode}}, util::bool_to_01};
use crate::model::profile::{Display, DisplayMode};
use anyhow::Result; use anyhow::Result;
use displayz::{query_displays, DisplaySet}; use displayz::{query_displays, DisplaySet};
use ini::Ini;
use tauri::{AppHandle, Listener}; use tauri::{AppHandle, Listener};
#[derive(Clone)] #[derive(Clone)]
pub struct DisplayInfo { pub struct DisplayInfo {
pub primary: String, pub primary: Option<String>,
pub set: Option<DisplaySet> pub set: Option<DisplaySet>,
} }
impl Default for DisplayInfo { impl Default for DisplayInfo {
fn default() -> Self { fn default() -> Self {
DisplayInfo { DisplayInfo {
primary: "default".to_owned(), primary: None,
set: query_displays().ok() set: query_displays().ok(),
} }
} }
} }
@ -31,7 +31,7 @@ impl Display {
}); });
} }
pub fn line_up(&self) -> Result<Option<DisplayInfo>> { pub fn prepare(&self) -> Result<Option<DisplayInfo>> {
use anyhow::anyhow; use anyhow::anyhow;
use displayz::{query_displays, Orientation, Resolution, Frequency}; use displayz::{query_displays, Orientation, Resolution, Frequency};
@ -52,21 +52,35 @@ impl Display {
.find(|display| display.name() == self.target) .find(|display| display.name() == self.target)
.ok_or_else(|| anyhow!("Display {} not found", self.target))?; .ok_or_else(|| anyhow!("Display {} not found", self.target))?;
target.set_primary()?; if !self.dont_switch_primary {
target.set_primary()?;
}
let settings = target.settings() let settings = target.settings()
.as_ref() .as_ref()
.ok_or_else(|| anyhow!("Unable to query display settings"))?; .ok_or_else(|| anyhow!("Unable to query display settings"))?;
let res = DisplayInfo { let res = DisplayInfo {
primary: primary.name().to_owned(), primary: if self.dont_switch_primary { None } else { Some(primary.name().to_owned()) },
set: Some(display_set.clone()) set: Some(display_set.clone()),
}; };
if self.rotation == 90 || self.rotation == 270 { if let Some(rotation) = self.rotation {
let rez = settings.borrow_mut().resolution; let rez = settings.borrow_mut().resolution;
settings.borrow_mut().orientation = if self.rotation == 90 { Orientation::PortraitFlipped } else { Orientation::Portrait }; settings.borrow_mut().orientation = match rotation {
0 => Orientation::Landscape,
90 => Orientation::PortraitFlipped,
180 => Orientation::LandscapeFlipped,
270 => Orientation::Portrait,
_ => panic!("Invalid display rotation")
};
if rez.height < rez.width { if rez.height < rez.width {
settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width); if rotation == 90 || rotation == 270 {
settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width);
}
} else {
if rotation == 0 || rotation == 180 {
settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width);
}
} }
} }
@ -88,7 +102,9 @@ impl Display {
settings.borrow_mut().resolution = Resolution::new(width, height); settings.borrow_mut().resolution = Resolution::new(width, height);
} }
display_set.apply()?; display_set.apply().map_err(
|_| anyhow!("The selected monitor has been disconnected or doesn't support the chosen display mode")
)?;
displayz::refresh()?; displayz::refresh()?;
log::debug!("prepare display: done"); log::debug!("prepare display: done");
@ -96,18 +112,34 @@ impl Display {
Ok(Some(res)) Ok(Some(res))
} }
pub fn line_up(&self, game: Game, ini: &mut Ini) {
if game == Game::Chunithm {
let autism = self.monitor_index_override.unwrap_or(0).to_string();
ini.with_section(Some("gfx"))
.set("enable", "1")
.set("windowed", bool_to_01(self.mode != DisplayMode::Fullscreen))
.set("framed", bool_to_01(self.mode == DisplayMode::Window))
.set("monitor", if self.dont_switch_primary { &autism } else { "0" });
ini.with_section(Some("system"))
.set("dipsw2", bool_to_01(self.frequency == 60))
.set("dipsw3", bool_to_01(self.frequency == 60));
}
}
pub fn clean_up(info: &DisplayInfo) -> Result<()> { pub fn clean_up(info: &DisplayInfo) -> Result<()> {
use anyhow::anyhow; use anyhow::anyhow;
let display_set = info.set.as_ref() let display_set = info.set.as_ref()
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?; .ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
let primary = display_set if let Some(info_primary) = &info.primary {
.displays() let primary = display_set
.find(|display| display.name() == info.primary) .displays()
.ok_or_else(|| anyhow!("Display {} not found", info.primary))?; .find(|display| display.name() == info_primary)
.ok_or_else(|| anyhow!("Display {} not found", info_primary))?;
primary.set_primary()?; primary.set_primary()?;
}
display_set.apply()?; display_set.apply()?;
displayz::refresh()?; displayz::refresh()?;

View File

@ -0,0 +1,150 @@
use ini::Ini;
use anyhow::Result;
use crate::model::profile::Keyboard;
macro_rules! parse_int_field {
($section:expr,$sgt:expr,$sl:expr) => {
if let Some(field) = $section.get($sgt) {
let field = &field[0..field.chars().position(|c| c == ';').unwrap_or(field.len())].trim();
log::debug!("loading {}={}", $sgt, field);
let res = if field.starts_with("0x") {
i32::from_str_radix(&field.trim()[2..], 16)
} else {
field.trim().parse::<i32>()
};
match res {
Ok(v) => $sl = v,
Err(e) => log::warn!("unable to read a segatools.ini field key={} value={}: {:?}", $sgt, field.trim(), e)
};
} else {
log::debug!("unable to load {}", $sgt);
}
}
}
impl Keyboard {
pub fn load_from_ini(&mut self, ini: &Ini) -> Result<()> {
log::debug!("loading kb");
match self {
Keyboard::Ongeki(kb) => {
if let Some(s) = ini.section(Some("io4")) {
parse_int_field!(s, "test", kb.test);
parse_int_field!(s, "service", kb.svc);
parse_int_field!(s, "coin", kb.coin);
parse_int_field!(s, "left1", kb.l1);
parse_int_field!(s, "left2", kb.l2);
parse_int_field!(s, "left3", kb.l3);
parse_int_field!(s, "right1", kb.r1);
parse_int_field!(s, "right2", kb.r2);
parse_int_field!(s, "right3", kb.r3);
parse_int_field!(s, "leftMenu", kb.lmenu);
parse_int_field!(s, "rightMenu", kb.rmenu);
parse_int_field!(s, "leftSide", kb.lwad);
parse_int_field!(s, "rightSide", kb.rwad);
let mut mouse: i32 = 1;
parse_int_field!(s, "mouse", mouse);
kb.use_mouse = if mouse == 1 { true } else { false };
}
}
Keyboard::Chunithm(kb) => {
if let Some(s) = ini.section(Some("io3")) {
parse_int_field!(s, "test", kb.test);
parse_int_field!(s, "service", kb.svc);
parse_int_field!(s, "coin", kb.coin);
}
if let Some(s) = ini.section(Some("slider")) {
for i in 0..kb.cell.len() {
parse_int_field!(s, format!("cell{}", i + 1), kb.cell[i]);
}
}
if let Some(s) = ini.section(Some("ir")) {
for i in 0..kb.ir.len() {
parse_int_field!(s, format!("ir{}", i + 1), kb.ir[i]);
}
}
}
}
Ok(())
}
// This is assumed to run in sync after the segatools module
pub fn line_up(&self, ini: &mut Ini) -> Result<()> {
if let Some(enable) = ini.section(Some("io4")).and_then(|s| s.get("enable")) {
// io4 was disabled by the Segatools module -> abort
if enable == "0" {
return Ok(());
}
}
match self {
Keyboard::Ongeki(kb) => {
if kb.enabled {
ini.with_section(Some("io4"))
.set("test", kb.test.to_string())
.set("service", kb.svc.to_string())
.set("coin", kb.coin.to_string())
.set("left1", kb.l1.to_string())
.set("left2", kb.l2.to_string())
.set("left3", kb.l3.to_string())
.set("right1", kb.r1.to_string())
.set("right2", kb.r2.to_string())
.set("right3", kb.r3.to_string())
.set("leftSide", kb.lwad.to_string())
.set("rightSide", kb.rwad.to_string())
.set("leftMenu", kb.lmenu.to_string())
.set("rightMenu", kb.rmenu.to_string())
.set("mouse", if kb.use_mouse { "1" } else { "0" });
} else {
ini.with_section(Some("io4"))
.set("test", "0")
.set("service", "0")
.set("coin", "0")
.set("left1", "0")
.set("left2", "0")
.set("left3", "0")
.set("right1", "0")
.set("right2", "0")
.set("right3", "0")
.set("leftSide", "0")
.set("rightSide", "0")
.set("leftMenu", "0")
.set("rightMenu", "0");
}
}
Keyboard::Chunithm(kb) => {
if kb.enabled {
for (i, cell) in kb.cell.iter().enumerate() {
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string());
}
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("io3"))
.set("test", kb.test.to_string())
.set("service", kb.svc.to_string())
.set("coin", kb.coin.to_string());
} else {
for (i, _) in kb.cell.iter().enumerate() {
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), "0");
}
for (i, _) in kb.ir.iter().enumerate() {
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), "0");
}
ini.with_section(Some("io3"))
.set("test", "0")
.set("service", "0")
.set("coin", "0");
}
ini.with_section(Some("io3"))
.set("ir", "0");
}
}
Ok(())
}
}

View File

@ -0,0 +1,59 @@
use std::path::Path;
use anyhow::Result;
use crate::model::patch::{Patch, PatchData, PatchFileVec, PatchSelection, PatchSelectionData};
impl PatchSelection {
pub async fn render_to_file(
&self,
filename: &str,
patches: &PatchFileVec,
path: impl AsRef<Path>
) -> Result<()> {
let mut res = "".to_owned();
for file in &patches.0 {
for list in &file.0 {
if list.filename != filename {
continue;
}
for patch in &list.patches {
if let Some(selection) = self.0.get(&patch.id) {
res += &Self::render(filename, patch, selection);
}
}
}
}
tokio::fs::write(path, res).await?;
Ok(())
}
fn render(filename: &str, patch: &Patch, sel: &PatchSelectionData) -> String {
let mut res = "".to_owned();
match &patch.data {
PatchData::Normal(data) => {
for p in &data.patches {
res += &format!("{} F+{:X} ", filename, p.offset);
for on in &p.on {
res += &format!("{:02X}", on);
}
res += " ";
for off in &p.off {
res += &format!("{:02X}", off);
}
res += "\n";
}
},
PatchData::Number(data) => {
if let PatchSelectionData::Number(val) = sel {
let width = (data.size as usize) * 2usize;
res += &format!("{} F+{:X} {:0width$X} {:0width$X}", filename, data.offset, val, data.default, width = width);
} else {
log::error!("invalid number patch {:?}", patch);
}
}
}
format!("{}\n", res)
}
}

View File

@ -3,6 +3,8 @@ pub mod segatools;
pub mod network; pub mod network;
pub mod bepinex; pub mod bepinex;
pub mod mu3ini; pub mod mu3ini;
pub mod keyboard;
pub mod mempatcher;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub mod display_windows; pub mod display_windows;

View File

@ -1,11 +1,11 @@
use std::path::Path; use std::path::Path;
use anyhow::Result; use anyhow::{anyhow, Result};
use ini::Ini; use ini::Ini;
use crate::model::profile::{Mu3Audio, Mu3Ini}; use crate::model::profile::{Mu3Audio, Mu3Ini};
impl Mu3Ini { impl Mu3Ini {
pub fn line_up(&self, game_path: impl AsRef<Path>) -> Result<()> { pub fn line_up(&self, data_dir: impl AsRef<Path>, cfg_dir: impl AsRef<Path>) -> Result<()> {
let file = game_path.as_ref().join("mu3.ini"); let file = cfg_dir.as_ref().join("mu3.ini");
if !file.exists() { if !file.exists() {
std::fs::write(&file, "")?; std::fs::write(&file, "")?;
@ -20,9 +20,26 @@ impl Mu3Ini {
Mu3Audio::Excl2Ch => "2", Mu3Audio::Excl2Ch => "2",
}; };
ini.with_section(Some("Sound")).set("WasapiExclusive", value); ini.with_section(Some("Sound"))
.set("WasapiExclusive", value)
.set("SampleRate", self.sample_rate.to_string());
} }
if let Some(blacklist) = self.blacklist {
ini.with_section(Some("Extra"))
.set("BlacklistMin", blacklist.0.to_string())
.set("BlacklistMax", blacklist.1.to_string());
}
let cache_path = data_dir.as_ref().join("mu3-mods-cache");
let cache_path = cache_path.to_str()
.ok_or_else(|| anyhow!("Invalid cache path"))?;
ini.with_section(Some("Extra"))
.set("GP", self.gp.to_string())
.set("CacheDir", cache_path)
.set("UnlockBonusTracks", crate::util::bool_to_01(self.enable_bonus_tracks));
ini.write_to_file(file)?; ini.write_to_file(file)?;
Ok(()) Ok(())

View File

@ -5,6 +5,32 @@ use ini::Ini;
use crate::model::profile::{Network, NetworkType}; use crate::model::profile::{Network, NetworkType};
impl Network { impl Network {
pub fn load_from_ini(&mut self, ini: &Ini) -> Result<()> {
log::debug!("loading network");
if let Some(s) = ini.section(Some("dns")) {
if let Some(default) = s.get("default") {
if default.starts_with("192.") || default.starts_with("127.") {
self.network_type = NetworkType::Artemis;
} else {
self.network_type = NetworkType::Remote;
self.remote_address = default.to_owned();
}
}
}
if let Some(s) = ini.section(Some("netenv")) {
s.get("addrSuffix").map(|v|
self.suffix = v.parse::<i32>().ok()
);
}
if let Some(s) = ini.section(Some("keychip")) {
s.get("subnet").map(|v| self.subnet = v.to_owned());
s.get("id").map(|v| self.keychip = v.to_owned());
}
Ok(())
}
pub fn line_up(&self, ini: &mut Ini) -> Result<()> { pub fn line_up(&self, ini: &mut Ini) -> Result<()> {
log::debug!("begin line-up: network"); log::debug!("begin line-up: network");

View File

@ -1,8 +1,10 @@
use anyhow::Result; use anyhow::Result;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use crate::pkg::PkgKey; use std::path::PathBuf;
use crate::pkg::{PkgKey, Status};
use crate::pkg_store::PackageStore;
use crate::util; use crate::util;
use crate::profiles::ProfilePaths; use crate::profiles::types::ProfilePaths;
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> { pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> {
log::debug!("begin prepare packages"); log::debug!("begin prepare packages");
@ -12,7 +14,10 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
if redo_bepinex { if redo_bepinex {
if pfx_dir.join("BepInEx").exists() { if pfx_dir.join("BepInEx").exists() {
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?; util::remove_dir_all(pfx_dir.join("BepInEx")).await?;
}
if pfx_dir.join("lang").exists() {
util::remove_dir_all(pfx_dir.join("lang")).await?;
} }
} }
@ -22,18 +27,25 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
for m in pkgs { for m in pkgs {
log::debug!("preparing {}", m); log::debug!("preparing {}", m);
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition")); let (namespace, name) = m.split()?;
if redo_bepinex { if redo_bepinex {
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen let bpx_dir = util::pkg_dir_of(&namespace, &name)
.join("app") .join("app")
.join("BepInEx"); .join("BepInEx");
if bpx_dir.exists() { if bpx_dir.exists() {
util::copy_directory(&bpx_dir, &pfx_dir.join("BepInEx"), true)?; util::copy_directory(&bpx_dir, &pfx_dir.join("BepInEx"), true)?;
} }
let lang_dir = util::pkg_dir_of(&namespace, &name)
.join("app")
.join("lang");
if lang_dir.exists() {
util::copy_directory(&lang_dir, &pfx_dir.join("lang"), true)?;
}
} }
let opt_dir = util::pkg_dir_of(namespace, &name[1..]).join("option"); let opt_dir = util::pkg_dir_of(&namespace, &name).join("option");
if opt_dir.exists() { if opt_dir.exists() {
let x = opt_dir.read_dir().unwrap().next().unwrap()?; let x = opt_dir.read_dir().unwrap().next().unwrap()?;
if x.metadata()?.is_dir() { if x.metadata()?.is_dir() {
@ -45,4 +57,27 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
log::debug!("end prepare packages"); log::debug!("end prepare packages");
Ok(()) Ok(())
}
pub fn prepare_dlls(
enabled_pkgs: &BTreeSet<PkgKey>,
store: &PackageStore,
) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
let mut res_game = Vec::new();
let mut res_amd = Vec::new();
for pkg in enabled_pkgs {
if let Ok(pkg) = store.get(&pkg) {
if let Some(loc) = &pkg.loc {
if let Status::OK(_, dlls) = &loc.status {
if let Some(game_dll) = &dlls.game {
res_game.push(pkg.path().join(game_dll.clone()));
}
if let Some(amd_dll) = &dlls.amd {
res_amd.push(pkg.path().join(amd_dll.clone()));
}
}
}
}
}
Ok((res_game, res_amd))
} }

View File

@ -1,35 +1,59 @@
use std::path::PathBuf; use std::path::{PathBuf, Path};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use ini::Ini; use ini::Ini;
use crate::{model::{misc::Game, profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}}; use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, ConfigHookAuth, Game}, profile::{Aime, IOSelection, Segatools}}, profiles::ProfilePaths, util::{self, PathStr}};
use crate::pkg_store::PackageStore; use crate::pkg_store::PackageStore;
impl Segatools { impl Segatools {
pub fn fix(&mut self, store: &PackageStore) { pub fn fix(&mut self, _store: &PackageStore) {
macro_rules! remove_if_nonpresent { // macro_rules! remove_if_nonpresent {
($item:expr,$key:expr,$emptyval:expr,$store:expr) => { // ($item:expr,$key:expr,$emptyval:expr,$store:expr) => {
if let Ok(pkg) = $store.get($key) { // if let Ok(pkg) = $store.get($key) {
if pkg.loc.is_none() { // if pkg.loc.is_none() {
$item = $emptyval; // $item = $emptyval;
// }
// } else {
// $item = $emptyval;
// }
// }
// }
// if let Some(key) = &self.hook {
// remove_if_nonpresent!(self.hook, key, None, store);
// }
// if let IOSelection::Custom(key) = &self.io2 {
// remove_if_nonpresent!(self.io2, key, IOSelection::default(), store);
// }
// match &self.aime {
// Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
// Aime::Other(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
// _ => {},
// }
}
pub fn load_from_ini(&mut self, ini: &Ini, config_dir: impl AsRef<Path>) -> Result<()> {
log::debug!("loading sgt");
if let Some(s) = ini.section(Some("vfs")) {
s.get("amfs").map(|v| self.amfs = PathBuf::from(v));
s.get("appdata").map(|v| self.appdata = PathBuf::from(v));
s.get("option").map(|v| self.option = PathBuf::from(v));
}
if let Some(s) = ini.section(Some("aime")) {
if s.get("enable").unwrap_or("0") == "1" {
if let Some(aime_path) = s.get("aimePath") {
if let Some(game_dir) = self.target.parent() {
let target = game_dir.join(aime_path);
std::fs::copy(target, config_dir.as_ref().join("aime.txt"))?;
} else {
log::error!("profile doesn't have a game directory");
} }
} else { } else {
$item = $emptyval; log::warn!("aime emulation is enabled, but no aimePath specified");
} }
} }
} }
if let Some(key) = &self.hook { Ok(())
remove_if_nonpresent!(self.hook, key, None, store);
}
if let Some(key) = &self.io {
remove_if_nonpresent!(self.io, key, None, store);
}
match &self.aime {
Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
Aime::Other(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
_ => {},
}
} }
pub async fn line_up(&self, p: &impl ProfilePaths, game: Game) -> Result<Ini> { pub async fn line_up(&self, p: &impl ProfilePaths, game: Game) -> Result<Ini> {
log::debug!("begin line-up: segatools"); log::debug!("begin line-up: segatools");
@ -42,8 +66,12 @@ impl Segatools {
let ini_path = p.config_dir().join("segatools-base.ini"); let ini_path = p.config_dir().join("segatools-base.ini");
if !ini_path.exists() { if !ini_path.exists() {
tokio::fs::write(&ini_path, segatools_base(game)).await match game {
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?; Game::Ongeki => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-ongeki.ini"))
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?,
Game::Chunithm => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-chunithm.ini"))
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?
}
} }
if !pfx_dir.exists() { if !pfx_dir.exists() {
tokio::fs::create_dir(&pfx_dir).await tokio::fs::create_dir(&pfx_dir).await
@ -106,6 +134,9 @@ impl Segatools {
if self.amnet.name.len() > 0 { if self.amnet.name.len() > 0 {
aimeio.set("serverName", &self.amnet.name); aimeio.set("serverName", &self.amnet.name);
} }
} else if let Aime::Other(key) = &self.aime {
ini_out.with_section(Some("aimeio"))
.set("path", util::pkg_dir().join(key.to_string()).join("segatools").join("aimeio.dll").stringify()?);
} }
} else { } else {
ini_out.with_section(Some("aime")) ini_out.with_section(Some("aime"))
@ -113,7 +144,7 @@ impl Segatools {
} }
if game == Game::Ongeki { if game == Game::Ongeki {
if let Some(io) = &self.io { if let IOSelection::Custom(io) = &self.io2 {
ini_out.with_section(Some("mu3io")) ini_out.with_section(Some("mu3io"))
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?); .set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
} else { } else {
@ -121,6 +152,44 @@ impl Segatools {
.set("path", ""); .set("path", "");
} }
} }
match game {
Game::Ongeki => {
match &self.io2 {
IOSelection::Custom(io) => {
ini_out.with_section(Some("mu3io"))
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
}
IOSelection::SegatoolsBuiltIn => {
ini_out.with_section(Some("mu3io"))
.set("path", "");
}
IOSelection::Hardware => {
ini_out.with_section(Some("io4"))
.set("enable", "0");
}
}
},
Game::Chunithm => {
match &self.io2 {
IOSelection::Custom(io) => {
ini_out.with_section(Some("chuniio"))
.set("path32", util::pkg_dir().join(io.to_string()).join("segatools").join("chuniio32.dll").stringify()?)
.set("path64", util::pkg_dir().join(io.to_string()).join("segatools").join("chuniio64.dll").stringify()?);
}
IOSelection::SegatoolsBuiltIn => {
ini_out.with_section(Some("chuniio"))
.set("path32", "")
.set("path64", "");
}
IOSelection::Hardware => {
ini_out.with_section(Some("io4"))
.set("enable", "0");
ini_out.with_section(Some("slider"))
.set("enable", "0");
}
}
}
};
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out); log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
@ -143,6 +212,31 @@ impl Segatools {
)?; )?;
} }
let mut cfg_hook = ConfigHook::default();
if game == Game::Chunithm {
cfg_hook.allnet_auth = Some({
ConfigHookAuth {
r#type: "1.0".to_owned()
}
})
}
if let Some(port) = self.aime_port {
if self.aime == Aime::Disabled {
cfg_hook.aime = Some({
ConfigHookAime {
unit: vec![
ConfigHookAimeUnit {
port,
id: 1
}
]
}
})
}
}
std::fs::write(pfx_dir.join("config_hook.json"), serde_json::to_string(&cfg_hook)?)?;
log::debug!("end line-up: segatools"); log::debug!("end line-up: segatools");
Ok(ini_out) Ok(ini_out)

49
rust/src/patcher.rs Normal file
View File

@ -0,0 +1,49 @@
use std::path::Path;
use anyhow::Result;
use sha256::try_digest;
use crate::model::patch::{Patch, PatchFile, PatchFileVec};
impl PatchFileVec {
pub fn new(config_path: impl AsRef<Path>) -> Result<PatchFileVec> {
let path = config_path.as_ref().join("patches");
if !path.exists() {
std::fs::create_dir(&path)?;
}
std::fs::write(path.join("builtin-chunithm.json5"), include_bytes!("../static/standard-chunithm.json5"))?;
let mut res = Vec::new();
for f in std::fs::read_dir(path)? {
let f = f?;
let f = &f.path();
match serde_json5::from_str::<PatchFile>(&std::fs::read_to_string(f)?) {
Ok(parsed) => res.push(parsed),
Err(e) => {
log::error!("Error parsing {f:?}: {e}");
anyhow::bail!("Error parsing {f:?}: {e}");
}
}
}
Ok(PatchFileVec(res))
}
pub fn find_patches(&self, target: impl AsRef<Path>) -> Result<Vec<Patch>> {
let checksum = try_digest(target.as_ref())?;
let mut res = Vec::new();
for pfile in &self.0 {
for plist in &pfile.0 {
log::debug!("checking {}", plist.sha256.to_ascii_lowercase());
if plist.sha256.to_ascii_lowercase() == checksum {
let mut cloned = plist.clone().patches;
res.append(&mut cloned);
}
}
}
if res.len() == 0 {
log::warn!("no matching patchset for {:?} ({})", target.as_ref(), checksum);
}
Ok(res)
}
}

View File

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use std::{collections::BTreeSet, path::{Path, PathBuf}}; use std::{collections::BTreeSet, path::{Path, PathBuf}};
use tokio::fs; use tokio::fs;
use enumflags2::{bitflags, make_bitflags, BitFlags}; use enumflags2::{bitflags, make_bitflags, BitFlags};
use crate::{model::{local::{self, PackageManifest}, rainy}, util}; use crate::{model::{local::{self, PackageManifest}, misc::Game, rainy}, util};
// {namespace}-{name} // {namespace}-{name}
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)] #[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
@ -14,25 +14,38 @@ pub struct PkgKey(pub String);
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)] #[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
pub struct PkgKeyVersion(String); pub struct PkgKeyVersion(String);
#[derive(Clone, Default, Serialize, Deserialize)] #[derive(Copy, Clone, Display, Debug, Serialize, Deserialize, Default)]
pub enum PackageSource {
#[default] Rainy,
Local(Game)
}
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct Package { pub struct Package {
pub namespace: String, pub namespace: String,
pub name: String, pub name: String,
pub description: String, pub description: String,
pub loc: Option<Local>, pub loc: Option<Local>,
pub rmt: Option<Remote> pub rmt: Option<Remote>,
pub source: PackageSource,
} }
#[derive(Clone, PartialEq, Serialize, Deserialize)] #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum Status { pub enum Status {
Unchecked, Unchecked,
Unsupported, Unsupported,
OK(BitFlags<Feature>) OK(BitFlags<Feature>, DLLs),
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct DLLs {
pub game: Option<String>,
pub amd: Option<String>
} }
#[bitflags] #[bitflags]
#[repr(u8)] #[repr(u16)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Feature { pub enum Feature {
Mod, Mod,
@ -41,9 +54,13 @@ pub enum Feature {
Mu3Hook, Mu3Hook,
Mu3IO, Mu3IO,
ChusanHook, ChusanHook,
ChuniIO,
Mempatcher,
GameDLL,
AmdDLL
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize, Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct Local { pub struct Local {
pub version: String, pub version: String,
@ -53,7 +70,7 @@ pub struct Local {
pub icon: String, pub icon: String,
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize, Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub struct Remote { pub struct Remote {
pub version: String, pub version: String,
@ -64,6 +81,15 @@ pub struct Remote {
pub nsfw: bool, pub nsfw: bool,
pub categories: Vec<String>, pub categories: Vec<String>,
pub dependencies: BTreeSet<PkgKey>, pub dependencies: BTreeSet<PkgKey>,
pub file_size: i64,
}
impl PkgKey {
pub fn split(&self) -> Result<(String, String)> {
let (namespace, name) = self.0
.split_at(self.0.find("-").ok_or_else(|| anyhow!("Invalid package key"))?);
Ok((namespace.to_owned(), name[1..].to_owned())) // cut the hyphen
}
} }
impl Package { impl Package {
@ -87,12 +113,14 @@ impl Package {
nsfw: p.has_nsfw_content, nsfw: p.has_nsfw_content,
version: v.version_number, version: v.version_number,
categories: p.categories, categories: p.categories,
dependencies: Self::sanitize_deps(v.dependencies) dependencies: Self::sanitize_deps(v.dependencies),
}) file_size: v.file_size
}),
source: PackageSource::Rainy,
}) })
} }
pub async fn from_dir(dir: PathBuf) -> Result<Package> { pub async fn from_dir(dir: PathBuf, source: PackageSource) -> Result<(Package, Option<Vec<Game>>)> {
let str = fs::read_to_string(dir.join("manifest.json")).await?; let str = fs::read_to_string(dir.join("manifest.json")).await?;
let mft: local::PackageManifest = serde_json::from_str(&str)?; let mft: local::PackageManifest = serde_json::from_str(&str)?;
@ -102,10 +130,10 @@ impl Package {
.unwrap() .unwrap()
.to_owned(); .to_owned();
let status = Self::parse_status(&mft); let status = Self::parse_status(&mft, &dir);
let dependencies = Self::sanitize_deps(mft.dependencies); let dependencies = Self::sanitize_deps(mft.dependencies);
Ok(Package { Ok((Package {
namespace: Self::dir_to_namespace(&dir)?, namespace: Self::dir_to_namespace(&dir)?,
name: mft.name.clone(), name: mft.name.clone(),
description: mft.description.clone(), description: mft.description.clone(),
@ -116,8 +144,9 @@ impl Package {
status, status,
dependencies dependencies
}), }),
rmt: None rmt: None,
}) source
}, mft.games))
} }
pub fn key(&self) -> PkgKey { pub fn key(&self) -> PkgKey {
@ -125,7 +154,15 @@ impl Package {
} }
pub fn path(&self) -> PathBuf { pub fn path(&self) -> PathBuf {
util::pkg_dir().join(self.key().0) match self.source {
PackageSource::Rainy => util::pkg_dir().join(self.key().0),
PackageSource::Local(game) =>
util::pkg_dir()
.parent()
.unwrap()
.join(format!("pkg-{game}"))
.join(&self.name),
}
} }
pub fn _dir_to_key(dir: &Path) -> Result<String> { pub fn _dir_to_key(dir: &Path) -> Result<String> {
@ -186,33 +223,69 @@ impl Package {
res res
} }
fn parse_status(mft: &PackageManifest) -> Status { fn parse_status(mft: &PackageManifest, dir: impl AsRef<Path>) -> Status {
if mft.installers.len() == 0 { if mft.installers.len() == 0 {
return Status::OK(make_bitflags!(Feature::Mod));//Unchecked if dir.as_ref().join("post_load.ps1").exists() {
} else if mft.installers.len() == 1 { return Status::Unsupported;
if let Some(serde_json::Value::String(id)) = &mft.installers[0].get("identifier") { }
if id == "rainycolor" { if dir.as_ref().join("app").join("data").exists() {
return Status::OK(make_bitflags!(Feature::Mod)); return Status::Unsupported;
} else if id == "segatools" { }
// Multiple features in the same dll (yubideck etc.) should be supported at some point return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None });
let mut flags = BitFlags::default(); } else {
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") { let mut flags = BitFlags::default();
if module == "mu3hook" { let mut game_dll = None;
flags |= Feature::Mu3Hook; let mut amd_dll = None;
} else if module == "chusanhook" { for installer in &mft.installers {
flags |= Feature::ChusanHook; if let Some(serde_json::Value::String(id)) = installer.get("identifier") {
} else if module == "amnet" { if id == "rainycolor" {
flags |= Feature::AMNet | Feature::Aime; flags |= Feature::Mod;
} else if module == "aimeio" { } else if id == "segatools" {
flags |= Feature::Aime; if let Some(serde_json::Value::String(module)) = installer.get("module") {
} else if module == "mu3io" { flags |= Self::parse_segatools_module(&module);
flags |= Feature::Mu3IO;
} }
if let Some(serde_json::Value::Array(arr)) = installer.get("module") {
for elem in arr {
if let serde_json::Value::String(module) = elem {
flags |= Self::parse_segatools_module(module);
}
}
}
} else if id == "native_mod" {
if let Some(serde_json::Value::String(path)) = installer.get("dll-game") {
flags |= Feature::GameDLL;
flags |= Feature::Mod;
game_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll_game") {
flags |= Feature::GameDLL;
flags |= Feature::Mod;
game_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll-amdaemon") {
flags |= Feature::AmdDLL;
flags |= Feature::Mod;
amd_dll = Some(path.to_owned());
}
} else {
return Status::Unsupported;
} }
return Status::OK(flags);
} }
} }
log::debug!("{} parse result: {:?} {:?} {:?}", mft.name, flags, game_dll, amd_dll);
Status::OK(flags, DLLs { game: game_dll, amd: amd_dll })
}
}
fn parse_segatools_module(module: &str) -> BitFlags<Feature, u16> {
match module {
"mu3hook" => make_bitflags!(Feature::Mu3Hook),
"chusanhook" => make_bitflags!(Feature::ChusanHook),
"amnet" => make_bitflags!(Feature::{AMNet | Aime}),
"aimeio" => make_bitflags!(Feature::Aime),
"mu3io" => make_bitflags!(Feature::Mu3IO),
"chuniio" => make_bitflags!(Feature::ChuniIO),
_ => BitFlags::default()
} }
Status::Unsupported
} }
} }

View File

@ -1,5 +1,4 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::Path;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
@ -8,7 +7,7 @@ use tokio::task::JoinSet;
use crate::model::local::{PackageList, PackageListEntry}; use crate::model::local::{PackageList, PackageListEntry};
use crate::model::misc::Game; use crate::model::misc::Game;
use crate::model::rainy; use crate::model::rainy;
use crate::pkg::{Package, PkgKey, Remote, Status}; use crate::pkg::{Feature, Package, PackageSource, PkgKey, Remote, Status};
use crate::util; use crate::util;
use crate::download_handler::DownloadHandler; use crate::download_handler::DownloadHandler;
@ -22,7 +21,7 @@ pub struct PackageStore {
offline: bool, offline: bool,
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Payload { pub struct Payload {
pub pkg: PkgKey pub pkg: PkgKey
} }
@ -67,9 +66,24 @@ impl PackageStore {
.collect() .collect()
} }
#[allow(dead_code)]
pub fn get_by_feature(&self, feature: Feature) -> Vec<PkgKey> {
self.store.iter()
.filter(|(_, v)| {
if let Some(loc) = &v.loc {
if let Status::OK(flags, _) = loc.status {
return flags.contains(feature);
}
}
return false
})
.map(|(k, _)| k.clone())
.collect()
}
pub async fn reload_package(&mut self, key: PkgKey) { pub async fn reload_package(&mut self, key: PkgKey) {
let dir = util::pkg_dir().join(&key.0); let dir = util::pkg_dir().join(&key.0);
if let Ok(pkg) = Package::from_dir(dir).await { if let Ok((pkg, _)) = Package::from_dir(dir, PackageSource::Rainy).await {
self.update_nonremote(key, pkg); self.update_nonremote(key, pkg);
} else { } else {
log::error!("couldn't reload {}", key); log::error!("couldn't reload {}", key);
@ -83,12 +97,18 @@ impl PackageStore {
for dir in dirents { for dir in dirents {
if let Ok(dir) = dir { if let Ok(dir) = dir {
let path = dir.path(); let path = dir.path();
futures.spawn(Package::from_dir(path)); futures.spawn(Package::from_dir(path, PackageSource::Rainy));
} }
} }
while let Some(res) = futures.join_next().await { while let Some(res) = futures.join_next().await {
if let Ok(Ok(pkg)) = res { if let Ok(Ok((pkg, locally_declared_games))) = res {
if let Some(games) = locally_declared_games {
self.meta_list.insert(pkg.key(), PackageListEntry {
version: pkg.loc.as_ref().unwrap().version.clone(),
games
});
}
self.update_nonremote(pkg.key(), pkg); self.update_nonremote(pkg.key(), pkg);
} }
} }
@ -138,7 +158,6 @@ impl PackageStore {
PackageListEntry { PackageListEntry {
// from_rainy() is guaranteed to include rmt // from_rainy() is guaranteed to include rmt
version: r.rmt.as_ref().unwrap().version.clone(), version: r.rmt.as_ref().unwrap().version.clone(),
status: Status::Unchecked,
games: vec![ game ], games: vec![ game ],
} }
}); });
@ -192,8 +211,9 @@ impl PackageStore {
"{}-{}-{}.zip", "{}-{}-{}.zip",
pkg.namespace, pkg.name, rmt.version pkg.namespace, pkg.name, rmt.version
)); ));
let part_path = zip_path.join(".part");
if !zip_path.exists() { if !zip_path.exists() && !part_path.exists() {
self.dlh.download_zip(&zip_path, &pkg)?; self.dlh.download_zip(&zip_path, &pkg)?;
log::debug!("deferring {}", key); log::debug!("deferring {}", key);
return Ok(InstallResult::Deferred); return Ok(InstallResult::Deferred);
@ -228,7 +248,7 @@ impl PackageStore {
if path.exists() && path.join("manifest.json").exists() { if path.exists() && path.join("manifest.json").exists() {
pkg.loc = None; pkg.loc = None;
let rv = Self::clean_up_package(&path).await; let rv = util::remove_dir_all(&path).await;
if rv.is_ok() { if rv.is_ok() {
self.app.emit("install-end-prelude", Payload { self.app.emit("install-end-prelude", Payload {
@ -254,44 +274,6 @@ impl PackageStore {
self.store.insert(key, new); self.store.insert(key, new);
} }
async fn clean_up_dir(path: impl AsRef<Path>, name: &str) -> Result<()> {
let path = path.as_ref().join(name);
if path.exists() {
tokio::fs::remove_dir_all(path)
.await
.map_err(|e| anyhow!("could not delete {}: {}", name, e))?;
}
Ok(())
}
async fn clean_up_file(path: impl AsRef<Path>, name: &str, force: bool) -> Result<()> {
let path = path.as_ref().join(name);
if force || path.exists() {
tokio::fs::remove_file(path).await
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
}
Ok(())
}
async fn clean_up_package(path: impl AsRef<Path>) -> Result<()> {
// todo case sensitivity for linux
Self::clean_up_dir(&path, "app").await?;
Self::clean_up_dir(&path, "option").await?;
Self::clean_up_dir(&path, "segatools").await?;
Self::clean_up_file(&path, "icon.png", true).await?;
Self::clean_up_file(&path, "manifest.json", true).await?;
Self::clean_up_file(&path, "README.md", true).await?;
Self::clean_up_file(&path, "post_load.ps1", false).await?;
tokio::fs::remove_dir(path.as_ref())
.await
.map_err(|e| anyhow!("Could not delete {}: {}", path.as_ref().to_string_lossy(), e))?;
Ok(())
}
fn resolve_deps(&self, rmt: Remote, set: &mut HashSet<PkgKey>) -> Result<()> { fn resolve_deps(&self, rmt: Remote, set: &mut HashSet<PkgKey>) -> Result<()> {
for d in rmt.dependencies { for d in rmt.dependencies {
set.insert(d.clone()); set.insert(d.clone());

View File

@ -1,61 +1,16 @@
use serde::{Deserialize, Serialize}; pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use tauri::AppHandle; use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
use std::{collections::BTreeSet, path::{Path, PathBuf}}; use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
use crate::{model::{misc::Game, profile::{Aime, Mu3Ini, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
use tauri::Emitter; use tauri::Emitter;
use std::process::Stdio; use std::process::Stdio;
use crate::model::profile::BepInEx; use crate::model::profile::BepInEx;
use crate::model::{profile::{Display, DisplayMode, Network, Segatools}, segatools_base::segatools_base}; use crate::model::profile::{Display, DisplayMode, Network, Segatools};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use std::fs::File; use std::fs::File;
use tokio::process::Command; use tokio::process::Command;
use tokio::task::JoinSet; use tokio::task::JoinSet;
pub trait ProfilePaths { pub mod types;
fn config_dir(&self) -> PathBuf;
fn data_dir(&self) -> PathBuf;
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct ProfileMeta {
pub game: Game,
pub name: String
}
impl ProfilePaths for ProfileMeta {
fn config_dir(&self) -> PathBuf {
util::profile_config_dir(self.game, &self.name)
}
fn data_dir(&self) -> PathBuf {
util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name))
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Profile {
pub meta: ProfileMeta,
pub data: ProfileData,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ProfileData {
pub mods: BTreeSet<PkgKey>,
pub sgt: Segatools,
pub network: Network,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bepinex: Option<BepInEx>,
#[cfg(not(target_os = "windows"))]
pub wine: crate::model::profile::Wine,
#[serde(skip_serializing_if = "Option::is_none")]
pub mu3_ini: Option<Mu3Ini>
}
impl Profile { impl Profile {
pub fn new(mut meta: ProfileMeta) -> Result<Self> { pub fn new(mut meta: ProfileMeta) -> Result<Self> {
@ -66,32 +21,83 @@ impl Profile {
mods: BTreeSet::new(), mods: BTreeSet::new(),
sgt: Segatools::default_for(meta.game), sgt: Segatools::default_for(meta.game),
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
display: if meta.game == Game::Ongeki { Some(Display::default_for(meta.game)) } else { None }, display: Some(Display::default_for(meta.game)),
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
display: None, display: None,
network: Network::default(), network: Network::default(),
bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None }, bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None },
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
wine: crate::model::profile::Wine::default(), wine: crate::model::profile::Wine::default(),
mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini { audio: None, blacklist: None }) } else { None }, mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini::default()) } else { None },
keyboard:
if meta.game == Game::Ongeki {
Some(Keyboard::Ongeki(OngekiKeyboard::default()))
} else {
Some(Keyboard::Chunithm(ChunithmKeyboard::default()))
},
patches: if meta.game == Game::Chunithm { Some(PatchSelection(BTreeMap::new())) } else { None }
}, },
meta: meta.clone() meta: meta.clone()
}; };
p.save()?; p.save()?;
std::fs::create_dir_all(p.config_dir())?; std::fs::create_dir_all(p.config_dir())?;
std::fs::create_dir_all(p.data_dir())?; std::fs::create_dir_all(p.data_dir())?;
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base(meta.game))?;
if meta.game == Game::Ongeki {
if let Err(e) = Self::load_existing_mu3_ini(&p.data, &p.meta) {
log::error!("unable to load existing mu3.ini: {e}");
}
}
match meta.game {
Game::Ongeki => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-ongeki.ini"))?,
Game::Chunithm => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-chunithm.ini"))?,
};
Ok(p) Ok(p)
} }
pub fn load(game: Game, name: String) -> Result<Self> { pub fn load(game: Game, name: String) -> Result<Self> {
let path = util::profile_config_dir(game, &name).join("profile.json"); let path = util::profile_config_dir(game, &name).join("profile.json");
if let Ok(s) = std::fs::read_to_string(&path) { if let Ok(s) = std::fs::read_to_string(&path) {
let data = serde_json::from_str::<ProfileData>(&s) let mut data = serde_json::from_str::<ProfileData>(&s)
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?; .map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
log::debug!("{:?}", data); log::debug!("{:?}", data);
// Backwards compat
if game == Game::Ongeki {
if data.keyboard.is_none() {
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default()));
}
if let Some(io) = data.sgt.io {
data.sgt.io2 = IOSelection::Custom(io);
data.sgt.io = None;
}
if let Some(ini) = &mut data.mu3_ini {
if ini.audio.is_none() {
ini.audio = Some(crate::model::profile::Mu3Audio::Shared);
}
if ini.blacklist.is_none() {
ini.blacklist = Some((10000, 19999));
}
} else {
data.mu3_ini = Some(Mu3Ini::default());
}
Self::load_existing_mu3_ini(&data, &ProfileMeta { game, name: name.clone() })?;
}
if game == Game::Chunithm {
if data.keyboard.is_none() {
data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default()));
}
if data.patches.is_none() {
data.patches = Some(PatchSelection(BTreeMap::new()));
}
if data.display.is_none() {
data.display = Some(Display::default_for(Game::Chunithm));
}
}
Ok(Profile { Ok(Profile {
meta: ProfileMeta { meta: ProfileMeta {
game, name game, name
@ -112,7 +118,7 @@ impl Profile {
} }
std::fs::write(&path, s) std::fs::write(&path, s)
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?; .map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
log::info!("Written to {:?}", path); log::info!("profile saved to {:?}", path);
Ok(()) Ok(())
} }
@ -130,7 +136,7 @@ impl Profile {
if let Some(hook) = &self.data.sgt.hook { if let Some(hook) = &self.data.sgt.hook {
res.push(hook.clone()); res.push(hook.clone());
} }
if let Some(io) = &self.data.sgt.io { if let IOSelection::Custom(io) = &self.data.sgt.io2 {
res.push(io.clone()); res.push(io.clone());
} }
if let Aime::AMNet(aime) = &self.data.sgt.aime { if let Aime::AMNet(aime) = &self.data.sgt.aime {
@ -163,28 +169,24 @@ impl Profile {
if self.meta.game.has_module(ProfileModule::Mu3Ini) && source.mu3_ini.is_some() { if self.meta.game.has_module(ProfileModule::Mu3Ini) && source.mu3_ini.is_some() {
self.data.mu3_ini = source.mu3_ini; self.data.mu3_ini = source.mu3_ini;
} }
}
pub async fn line_up(&self, pkg_hash: String, refresh: bool, _app: AppHandle) -> Result<()> {
let info = match &self.data.display {
None => None,
Some(display) => display.line_up()?
};
let res = self.line_up_the_rest(pkg_hash, refresh).await; if self.meta.game.has_module(ProfileModule::Keyboard) && source.keyboard.is_some() {
self.data.keyboard = source.keyboard;
#[cfg(target_os = "windows")]
if let Some(info) = info {
use crate::model::profile::Display;
if res.is_ok() {
Display::wait_for_exit(_app, info);
} else {
Display::clean_up(&info)?;
}
} }
res if self.data.patches.is_some() && source.patches.is_some() {
self.data.patches = source.patches;
}
} }
async fn line_up_the_rest(&self, pkg_hash: String, refresh: bool) -> Result<()> { pub fn prepare_display(&self) -> Result<Option<DisplayInfo>> {
let info = match &self.data.display {
None => None,
Some(display) => display.prepare()?
};
Ok(info)
}
pub async fn line_up(&self, pkg_hash: String, refresh: bool, patch_files: &PatchFileVec) -> Result<()> {
if !self.data_dir().exists() { if !self.data_dir().exists() {
tokio::fs::create_dir(self.data_dir()).await?; tokio::fs::create_dir(self.data_dir()).await?;
} }
@ -194,12 +196,23 @@ impl Profile {
util::clean_up_opts(self.data_dir().join("option"))?; util::clean_up_opts(self.data_dir().join("option"))?;
let hash_check = Self::hash_check(&hash_path, &pkg_hash).await? || refresh; let hash_check = Self::hash_check(&hash_path, &pkg_hash).await? || refresh;
prepare_packages(&self.meta, &self.data.mods, hash_check).await prepare_packages(&self.meta, &self.data.mods, hash_check).await
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?; .map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?; .map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
self.data.network.line_up(&mut ini)?; self.data.network.line_up(&mut ini)?;
if let Some(display) = &self.data.display {
display.line_up(self.meta.game, &mut ini);
}
if let Some(keyboard) = &self.data.keyboard {
keyboard.line_up(&mut ini)?;
}
ini.write_to_file(self.data_dir().join("segatools.ini")) ini.write_to_file(self.data_dir().join("segatools.ini"))
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?; .map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
@ -208,13 +221,21 @@ impl Profile {
} }
if let Some(mu3ini) = &self.data.mu3_ini { if let Some(mu3ini) = &self.data.mu3_ini {
mu3ini.line_up(&self.data.sgt.target.parent().unwrap())?; mu3ini.line_up(&self.data_dir(), &self.config_dir())?;
} }
if let Some(patches) = &self.data.patches {
futures::try_join!(
patches.render_to_file("amdaemon.exe", patch_files, self.data_dir().join("patch-amd.mph")),
patches.render_to_file("chusanApp.exe", patch_files, self.data_dir().join("patch-game.mph"))
)?;
}
Ok(()) Ok(())
} }
pub async fn start(&self, app: AppHandle) -> Result<()> { pub async fn start(&self, payload: StartPayload) -> Result<()> {
let ini_path = self.data_dir().join("segatools.ini"); let ini_path = self.data_dir().join("segatools.ini");
log::debug!("With path {:?}", ini_path); log::debug!("With path {:?}", ini_path);
@ -245,12 +266,24 @@ impl Profile {
&ini_path, &ini_path,
) )
.current_dir(&exe_dir) .current_dir(&exe_dir)
.arg("/C") .raw_arg("/C")
.arg(&sgt_dir.join(self.meta.game.inject_amd())) .arg(&sgt_dir.join(self.meta.game.inject_amd()))
.args(["-d", "-k"]) .raw_arg("-d");
for dll in payload.amd_dlls {
amd_builder.raw_arg("-k");
amd_builder.arg(dll);
}
amd_builder
.raw_arg("-k")
.arg(sgt_dir.join(self.meta.game.hook_amd())) .arg(sgt_dir.join(self.meta.game.hook_amd()))
.arg("amdaemon.exe") .arg("amdaemon.exe")
.args(self.meta.game.amd_args()); .args(self.meta.game.amd_args());
amd_builder.arg(self.data_dir().join("config_hook.json"));
game_builder game_builder
.env( .env(
"SEGATOOLS_CONFIG_PATH", "SEGATOOLS_CONFIG_PATH",
@ -260,23 +293,62 @@ impl Profile {
"INOHARA_CONFIG_PATH", "INOHARA_CONFIG_PATH",
self.config_dir().join("inohara.cfg"), self.config_dir().join("inohara.cfg"),
) )
.env(
"SAEKAWA_CONFIG_PATH",
self.config_dir().join("saekawa.toml"),
)
.env(
"ONGEKI_LANG_PATH",
self.data_dir().join("lang"),
)
.env(
"MU3_MODS_CONFIG_PATH",
self.config_dir().join("mu3.ini"),
)
.env(
"STARTLINER",
"1"
)
.current_dir(&exe_dir) .current_dir(&exe_dir)
.args(["-d", "-k"]) .raw_arg("-d")
.arg(sgt_dir.join(self.meta.game.hook_exe())) .raw_arg("-k")
.arg(self.meta.game.exe()); .arg(sgt_dir.join(self.meta.game.hook_exe()));
if let Some(display) = &self.data.display { for dll in payload.game_dlls {
game_builder.args([ game_builder.raw_arg("-k");
"-monitor 1", game_builder.arg(dll);
"-screen-width", &display.rez.0.to_string(), }
"-screen-height", &display.rez.1.to_string(),
"-screen-fullscreen", if display.mode == DisplayMode::Fullscreen { "1" } else { "0" } game_builder.arg(self.meta.game.exe());
]);
if display.mode == DisplayMode::Borderless { if self.meta.game.has_module(ProfileModule::BepInEx) {
game_builder.arg("-popupwindow"); if let Some(display) = &self.data.display {
if display.dont_switch_primary && display.target != "default" {
game_builder.args(["-monitor", &display.monitor_index_override.unwrap_or_else(|| 1).to_string()]);
} else {
game_builder.args(["-monitor", "1"]);
}
game_builder.args([
"-screen-width", &display.rez.0.to_string(),
"-screen-height", &display.rez.1.to_string(),
"-screen-fullscreen", if display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
]);
if display.mode == DisplayMode::Borderless {
game_builder.arg("-popupwindow");
}
} }
} }
if self.meta.game.has_module(ProfileModule::Mempatcher) {
amd_builder
.env("MEMPATCHER_PATCH_PATH", self.data_dir().join("patch-amd.mph"))
.env("MEMPATCHER_LOG_PATH", self.data_dir().join("mempatcher-amdaemon.log"));
game_builder
.raw_arg("--mempatch")
.arg(self.data_dir().join("patch-game.mph"))
.env("MEMPATCHER_LOG_PATH", self.data_dir().join("mempatcher-game.log"));
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
amd_builder.env("WINEPREFIX", &self.wine.prefix); amd_builder.env("WINEPREFIX", &self.wine.prefix);
@ -305,8 +377,8 @@ impl Profile {
util::pkill("amdaemon.exe").await; util::pkill("amdaemon.exe").await;
log::info!("Launching amdaemon: {:?}", amd_builder); log::info!("launching amdaemon: {:?}", amd_builder);
log::info!("Launching {}: {:?}", self.meta.game, game_builder); log::info!("launching {}: {:?}", self.meta.game, game_builder);
let mut amd = amd_builder.spawn()?; let mut amd = amd_builder.spawn()?;
let mut game = game_builder.spawn()?; let mut game = game_builder.spawn()?;
@ -321,7 +393,7 @@ impl Profile {
(game.wait().await.expect("game failed to run"), "game") (game.wait().await.expect("game failed to run"), "game")
}); });
if let Err(e) = app.emit("launch-start", "") { if let Err(e) = payload.app.emit("launch-start", "") {
log::warn!("Unable to emit launch-start: {}", e); log::warn!("Unable to emit launch-start: {}", e);
} }
@ -339,7 +411,7 @@ impl Profile {
log::debug!("Fin"); log::debug!("Fin");
if let Err(e) = app.emit("launch-end", "") { if let Err(e) = payload.app.emit("launch-end", "") {
log::warn!("Unable to emit launch-end: {}", e); log::warn!("Unable to emit launch-end: {}", e);
} }
@ -357,6 +429,17 @@ impl Profile {
Ok(false) Ok(false)
} }
} }
fn load_existing_mu3_ini(data: &ProfileData, meta: &ProfileMeta) -> Result<()> {
let mu3_ini_target_path = data.sgt.target.parent().ok_or_else(|| anyhow!("invalid target directory"))?.join("mu3.ini");
let mu3_ini_profile_path = util::profile_config_dir(meta.game, &meta.name).join("mu3.ini");
log::debug!("mu3.ini paths: {:?} {:?}", mu3_ini_target_path, mu3_ini_profile_path);
if mu3_ini_target_path.exists() && !mu3_ini_profile_path.exists() {
std::fs::copy(&mu3_ini_target_path, &mu3_ini_profile_path)?;
log::info!("copied mu3.ini from {:?}", &mu3_ini_target_path);
}
Ok(())
}
} }
impl ProfilePaths for Profile { impl ProfilePaths for Profile {

View File

@ -0,0 +1,64 @@
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use std::{collections::BTreeSet, path::PathBuf};
use crate::{model::{misc::Game, patch::PatchSelection, profile::{Keyboard, Mu3Ini}}, pkg::PkgKey, util};
use crate::model::profile::BepInEx;
use crate::model::profile::{Display, Network, Segatools};
pub trait ProfilePaths {
fn config_dir(&self) -> PathBuf;
fn data_dir(&self) -> PathBuf;
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct ProfileMeta {
pub game: Game,
pub name: String
}
impl ProfilePaths for ProfileMeta {
fn config_dir(&self) -> PathBuf {
util::profile_config_dir(self.game, &self.name)
}
fn data_dir(&self) -> PathBuf {
util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name))
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Profile {
pub meta: ProfileMeta,
pub data: ProfileData,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ProfileData {
pub mods: BTreeSet<PkgKey>,
pub sgt: Segatools,
pub network: Network,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bepinex: Option<BepInEx>,
#[cfg(not(target_os = "windows"))]
pub wine: crate::model::profile::Wine,
#[serde(skip_serializing_if = "Option::is_none")]
pub mu3_ini: Option<Mu3Ini>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard>,
#[serde(skip_serializing_if = "Option::is_none")]
pub patches: Option<PatchSelection>,
}
pub struct StartPayload {
pub app: AppHandle,
pub game_dlls: Vec<PathBuf>,
pub amd_dlls: Vec<PathBuf>,
}

View File

@ -150,4 +150,67 @@ impl PathStr for PathBuf {
fn stringify(&self) -> Result<String> { fn stringify(&self) -> Result<String> {
path_to_str(&self) path_to_str(&self)
} }
}
pub fn bool_to_01(val: bool) -> &'static str {
return if val { "1" } else { "0" }
}
// rm -r with checks
pub async fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
let canon = path.as_ref().canonicalize()?;
if canon.to_string_lossy().len() < 10 {
return Err(anyhow!("invalid remove_dir_all target: too short"));
}
if canon.starts_with(data_dir().canonicalize()?)
|| canon.starts_with(config_dir().canonicalize()?)
|| canon.starts_with(cache_dir().canonicalize()?) {
tokio::fs::remove_dir_all(path).await
.map_err(|e| anyhow!("invalid remove_dir_all target: {:?}", e))?;
Ok(())
} else {
Err(anyhow!("invalid remove_dir_all target: not in a data directory"))
}
}
#[cfg(target_os = "windows")]
pub fn create_shortcut(
apph: AppHandle,
meta: &crate::profiles::ProfileMeta
) -> Result<()> {
use winsafe::{co, prelude::{ole_IPersistFile, ole_IUnknown, shell_IShellLink}, CoCreateInstance, CoInitializeEx, IPersistFile};
let _com_guard = CoInitializeEx(
co::COINIT::APARTMENTTHREADED
| co::COINIT::DISABLE_OLE1DDE,
)?;
let obj = CoCreateInstance::<winsafe::IShellLink>(
&co::CLSID::ShellLink,
None,
co::CLSCTX::INPROC_SERVER,
)?;
let target_dir = apph.path().cache_dir()?.join(NAME);
let target_path = target_dir.join("startliner.exe");
let lnk_path = apph.path().desktop_dir()?.join(format!("{} {}.lnk", &meta.game.print(), &meta.name));
obj.SetPath(target_path.to_str().ok_or_else(|| anyhow!("Illegal target path"))?)?;
obj.SetDescription(&format!("{} {} (STARTLINER)", &meta.game.print(), &meta.name))?;
obj.SetArguments(&format!("--start --game {} --profile {}", &meta.game, &meta.name))?;
obj.SetIconLocation(
target_dir.join(format!("icon-{}.ico", &meta.game)).to_str().ok_or_else(|| anyhow!("Illegal icon path"))?,
0
)?;
match meta.game {
Game::Ongeki => std::fs::write(target_dir.join("icon-ongeki.ico"), include_bytes!("../../res/icon-ongeki.ico")),
Game::Chunithm => std::fs::write(target_dir.join("icon-chunithm.ico"), include_bytes!("../../res/icon-chunithm.ico"))
}?;
let file = obj.QueryInterface::<IPersistFile>()?;
file.Save(Some(lnk_path.to_str().ok_or_else(|| anyhow!("Illegal shortcut path"))?), true)?;
Ok(())
} }

View File

@ -0,0 +1,82 @@
[vfd]
; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly.
enable=1
[system]
; Enable ALLS system settings.
enable=1
; Enable freeplay mode. This will disable the coin slot and set the game to
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
; allow you to start a game in freeplay mode.
freeplay=0
; LAN Install: If multiple machines are present on the same LAN then set
; this to 1 on exactly one machine and set this to 0 on all others.
dipsw1=1
; -----------------------------------------------------------------------------
; Misc. hooks settings
; -----------------------------------------------------------------------------
; -----------------------------------------------------------------------------
; LED settings
; -----------------------------------------------------------------------------
[led15093]
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
; RGBs and the rear LED panel (billboard) on the cabinet.
enable=1
[led]
; Output billboard LED strip data to a named pipe called "\\.\pipe\chuni_led"
cabLedOutputPipe=1
; Output billboard LED strip data to serial
cabLedOutputSerial=0
; Output slider LED data to the named pipe
controllerLedOutputPipe=1
; Output slider LED data to the serial port
controllerLedOutputSerial=0
; Use the OpeNITHM protocol for serial LED output
controllerLedOutputOpeNITHM=0
; Serial port to send data to if using serial output. Default is COM5.
;serialPort=COM5
; Baud rate for serial data (set to 115200 if using OpeNITHM)
;serialBaud=921600
; Data output a sequence of bytes, with JVS-like framing.
; Each "packet" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere,
; 0xD0 is used as an escape character -- if you receive D0 in the output, ignore
; it and use the next sent byte plus one instead.
;
; After the sync is one byte for the board number that was updated, followed by
; the red, green and blue values for each LED.
;
; Board 0 has 53 LEDs:
; [0]-[49]: snakes through left half of billboard (first column starts at top)
; [50]-[52]: left side partition LEDs
;
; Board 1 has 63 LEDs:
; [0]-[59]: right half of billboard (first column starts at bottom)
; [60]-[62]: right side partition LEDs
;
; Board 2 is the slider and has 31 LEDs:
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers
; -----------------------------------------------------------------------------
; Custom IO settings
; -----------------------------------------------------------------------------
[chuniio]
; Uncomment this if you have custom chuniio implementation comprised of a single 32bit DLL.
; (will use chu2to3 engine internally)
;path=
; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs.
; x86 chuniio to path32, x64 to path64. Both are necessary.
;path32=
;path64=

View File

@ -0,0 +1,36 @@
[vfd]
; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly.
enable=1
[system]
; Enable ALLS system settings.
enable=1
; Enable freeplay mode. This will disable the coin slot and set the game to
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
; allow you to start a game in freeplay mode.
freeplay=0
; LAN Install: Set this to 1 on all machines.
dipsw1=1
[gfx]
; Enables the graphics hook.
enable=1
[led15093]
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
; RGBs and the rear LED panel (billboard) on the cabinet.
enable=1
[led]
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\ongeki_led\"
cabLedOutputPipe=1
; Output billboard LED strip data to serial
cabLedOutputSerial=0
; Output slider LED data to the named pipe
controllerLedOutputPipe=1
; Output slider LED data to the serial port
controllerLedOutputSerial=0

View File

@ -0,0 +1,369 @@
[
{
filename: 'chusanApp.exe',
version: '2.26.00',
sha256: 'AD2DCC02CE52B3FFF24A2919F8617854581DD2E2C0378EA13D84438FCCA2D522',
patches: [
{
id: 'standard-shared-audio',
name: "Force shared audio mode, system audio sample rate must be 48000Hz",
tooltip: "Improves compatibility, but may increase latency",
patches: [
{offset: 0xF233DA, off: [0x01], on: [0x00]}
]
},
{
id: 'standard-2ch',
name: "Force 2 channel audio output",
tooltip: "May cause bass overload",
patches: [
{offset: 0xF234B1, off: [0x75, 0x3f], on: [0x90, 0x90]}
]
},
{
id: 'standard-song-timer',
name: "Disable song select timer",
patches: [
{offset: 0xA03916, off: [0x74], on: [0xeb]}
]
},
{
id: 'standard-map-timer',
name: "Map selection timer",
tooltip: "If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)",
type: "number",
offset: 0x965B37,
default: 30,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-ticket-timer',
name: "Ticket selection timer",
tooltip: "If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)",
type: "number",
offset: 0x9592C2,
default: 60,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-course-timer',
name: "Course selection timer",
tooltip: "If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)",
type: "number",
offset: 0xA0EADB,
default: 30,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-unlimited-tracks',
name: "Unlimited maximum tracks",
tooltip: "Must check to play more than 7 tracks per credit",
patches: [
{offset: 0x71E2E0, off: [0xf0], on: [0xc0]}
]
},
{
id: 'standard-maximum-tracks',
type: "number",
name: "Maximum tracks",
offset: 0x3980C1,
default: 3,
size: 1,
min: 3,
max: 12
},
{
id: 'standard-no-encryption',
name: "No encryption",
tooltip: "Will also disable TLS",
patches: [
{offset: 0x1DE29E8, off: [0xE1], on: [0x00]},
{offset: 0x1DE29EC, off: [0xE1], on: [0x00]}
]
},
{
id: 'standard-no-tls',
name: "No TLS",
tooltip: "Title server workaround",
patches: [
{offset: 0xF06447, off: [0x80], on: [0x00]}
]
},
{
id: 'standard-head-to-head',
name: "Patch for head-to-head play",
tooltip: "Fix infinite sync while trying to connect to head to head play",
patches: [
{offset: 0x6533A3, off: [0x01], on: [0x00]}
]
},
{
id: 'standard-bypass-1080p',
name: "Bypass 1080p monitor check",
patches: [
{offset: 0x1CCBF, off: [0x81, 0xbc, 0x24, 0xb8, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x75, 0x1f, 0x81, 0xbc, 0x24, 0xbc, 0x02, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x75, 0x12], on: [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90]}
]
},
{
id: 'standard-bypass-120hz',
name: "Bypass 120Hz monitor check",
patches: [
{offset: 0x1CCB1, off: [0x85, 0xc0], on: [0xeb, 0x30]}
]
},
{
id: 'standard-force-free-play-text',
name: "Force FREE PLAY credit text",
tooltip: "Replaces the credit count with FREE PLAY",
patches: [
{offset: 0x3875A4, off: [0x3c, 0x01], on: [0x38, 0xc0]}
]
},
],
},
{
filename: 'amdaemon.exe',
version: '2.25.00',
sha256: '00FB867D1EE821033101B8773FAC116A45DF1939D23C38E9DAFC9B86CD5A3777',
patches: [
{
id: 'standard-localhost',
name: "Allow 127.0.0.1/localhost as the network server",
patches: [
{ offset: 0x6E28A4, off: [0x31, 0x32, 0x37, 0x2F], on: [0x30, 0x2F, 0x38, 0x00] },
{ offset: 0x3C94C4, off: [0xFF, 0x15, 0xC6, 0x2F, 0x1B, 0x00, 0x8B], on: [0x33, 0xC0, 0x48, 0x83, 0xC4, 0x28, 0xC3] }
]
},
{
id: 'standard-credit-freeze',
name: "Infinite credits",
patches: [
{ offset: 0x2BBBC8, off: [0x28], on: [0x08] }
]
}
]
},
{
filename: 'chusanApp.exe',
version: '2.30.00',
sha256: 'd624da8a397c2885b3937e7b8bd0de6fc4e8da4beaf5c229569b29bb2847d694',
patches: [
{
id: 'standard-shared-audio',
name: 'Force shared audio mode, system audio sample rate must be 48000Hz',
tooltip: 'Improves compatibility, but may increase latency',
patches: [
{
offset: 16181386,
off: [1],
on: [0],
},
],
},
{
id: 'standard-2ch',
name: 'Force 2 channel audio output',
tooltip: 'May cause bass overload',
patches: [
{
offset: 16181601,
off: [117, 63],
on: [144, 144],
},
],
},
{
id: 'standard-song-timer',
name: 'Disable song select timer',
patches: [
{
offset: 10766682,
off: [116],
on: [235],
},
],
},
{
id: 'standard-map-timer',
name: 'Map selection timer',
tooltip: 'If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)',
type: 'number',
default: 30,
offset: 10111639,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-ticket-timer',
name: 'Ticket selection timer',
tooltip: 'If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)',
type: 'number',
default: 60,
offset: 10060322,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-course-timer',
name: 'Course selection timer',
tooltip: 'If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)',
type: 'number',
default: 30,
offset: 10812315,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-unlimited-tracks',
name: 'Unlimited maximum tracks',
tooltip: 'Must check to play more than 7 tracks per credit',
patches: [
{
offset: 7635328,
off: [240],
on: [192],
},
],
},
{
id: 'standard-maximum-tracks',
name: 'Maximum tracks',
type: 'number',
default: 3,
offset: 3768513,
size: 1,
min: 1,
max: 12,
},
{
id: 'standard-no-encryption',
name: 'No encryption',
tooltip: 'Will also disable TLS',
patches: [
{
offset: 31812584,
off: [230],
on: [0],
},
{
offset: 31812588,
off: [230],
on: [0],
},
],
},
{
id: 'standard-no-tls',
name: 'No TLS',
tooltip: 'Title server workaround',
patches: [
{
offset: 16062679,
off: [128],
on: [0],
},
],
},
{
id: 'standard-head-to-head',
name: 'Patch for head-to-head play',
tooltip: 'Fix infinite sync while trying to connect to head to head play',
patches: [
{
offset: 6795139,
off: [1],
on: [0],
},
],
},
{
id: 'standard-bypass-1080p',
name: 'Bypass 1080p monitor check',
patches: [
{
offset: 117951,
off: [
129, 188, 36, 184, 2, 0, 0, 128, 7, 0, 0, 117, 31,
129, 188, 36, 188, 2, 0, 0, 56, 4, 0, 0, 117, 18,
],
on: [
144, 144, 144, 144, 144, 144, 144, 144, 144, 144,
144, 144, 144, 144, 144, 144, 144, 144, 144, 144,
144, 144, 144, 144, 144, 144,
],
},
],
},
{
id: 'standard-bypass-120hz',
name: 'Bypass 120Hz monitor check',
patches: [
{
offset: 117937,
off: [133, 192, 116, 63],
on: [235, 48, 235, 46],
},
],
},
{
id: 'standard-force-free-play-text',
name: 'Force FREE PLAY credit text',
tooltip: 'Replaces the credit count with FREE PLAY',
patches: [
{
offset: 3700132,
off: [60, 1],
on: [56, 192],
},
],
},
],
},
{
filename: 'amdaemon.exe',
version: '2.30.00',
sha256: 'd4809220578374865370e31c541ed6e406b854d8c26cfe7464c2c15145113bfd',
patches: [
{
id: 'standard-localhost',
name: 'Allow 127.0.0.1/localhost as the network server',
patches: [
{
offset: 0x6e1ca4,
off: [0x31, 0x32, 0x37, 0x2f],
on: [0x30, 0x2f, 0x38, 0x00],
},
{
offset: 0x3c88c4,
off: [0xff, 0x15, 0xc6, 0x2f, 0x1b, 0x00, 0x8b],
on: [0x33, 0xc0, 0x48, 0x83, 0xc4, 0x28, 0xc3],
},
],
},
{
id: 'standard-credit-freeze',
name: 'Credit freeze',
tooltip: 'Prevents credits from being used. At least one credit must be available to start the game or purchase premium tickets.',
patches: [{ offset: 0x2bafc8, off: [0x28], on: [0x08] }],
},
{
id: 'standard-openssl-fix',
name: 'OpenSSL SHA crash bug fix',
tooltip: 'Fix crashes on 10th generation and newer Intel CPUs',
patches: [
{ offset: 0x4d4a43, off: [0x48], on: [0x4c] },
{ offset: 0x4d4a4b, off: [0x48], on: [0x49] },
],
},
],
},
]

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.2.0", "version": "0.12.0",
"identifier": "zip.patafour.startliner", "identifier": "zip.patafour.startliner",
"build": { "build": {
"beforeDevCommand": "bun run dev", "beforeDevCommand": "bun run dev",
@ -66,7 +66,7 @@
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",
"icon": ["icons/slow.png", "icons/slow.ico"], "icon": ["icons/icon.png", "icons/icon.ico"],
"createUpdaterArtifacts": true "createUpdaterArtifacts": true
} }
} }

View File

@ -1,36 +1,64 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, computed, onMounted, ref } from 'vue'; import { Ref, computed, onMounted, ref } from 'vue';
import Button from 'primevue/button'; import Button from 'primevue/button';
import ConfirmDialog from 'primevue/confirmdialog';
import Dialog from 'primevue/dialog';
import InputIcon from 'primevue/inputicon'; import InputIcon from 'primevue/inputicon';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import ProgressBar from 'primevue/progressbar';
import ScrollPanel from 'primevue/scrollpanel';
import Tab from 'primevue/tab'; import Tab from 'primevue/tab';
import TabList from 'primevue/tablist'; import TabList from 'primevue/tablist';
import TabPanel from 'primevue/tabpanel'; import TabPanel from 'primevue/tabpanel';
import TabPanels from 'primevue/tabpanels'; import TabPanels from 'primevue/tabpanels';
import Tabs from 'primevue/tabs'; import Tabs from 'primevue/tabs';
import { listen } from '@tauri-apps/api/event';
import InfoPage from './InfoPage.vue';
import ModList from './ModList.vue'; import ModList from './ModList.vue';
import ModStore from './ModStore.vue'; import ModStore from './ModStore.vue';
import OptionList from './OptionList.vue'; import OptionList from './OptionList.vue';
import PatchList from './PatchList.vue';
import ProfileList from './ProfileList.vue'; import ProfileList from './ProfileList.vue';
import StartButton from './StartButton.vue'; import StartButton from './StartButton.vue';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { useGeneralStore, usePkgStore, usePrfStore } from '../stores'; import {
useClientStore,
useGeneralStore,
usePkgStore,
usePrfStore,
} from '../stores';
import { Dirs } from '../types'; import { Dirs } from '../types';
import { messageSplit, shouldPreferDark } from '../util';
document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark());
const pkg = usePkgStore(); const pkg = usePkgStore();
const prf = usePrfStore(); const prf = usePrfStore();
const general = useGeneralStore(); const general = useGeneralStore();
const client = useClientStore();
pkg.setupListeners(); pkg.setupListeners();
const currentTab: Ref<string | number> = ref(3); const currentTab: Ref<'users' | 'loc' | 'patches' | 'rmt' | 'cfg' | 'info'> =
ref('users');
const pkgSearchTerm = ref(''); const pkgSearchTerm = ref('');
const isProfileDisabled = computed(() => prf.current === null); const isProfileDisabled = computed(() => prf.current === null);
const updateProgress: Ref<number | null> = ref(null);
listen<number>('update-progress', (ev) => {
updateProgress.value = Math.floor(ev.payload * 100);
});
listen<undefined>('update-end', (_) => {
updateProgress.value = null;
});
onMounted(async () => { onMounted(async () => {
invoke('list_directories').then((d) => { invoke('list_directories').then((d) => {
general.dirs = d as Dirs; general.dirs = d as Dirs;
client.load();
}); });
const fetch_promise = pkg.fetch(true); const fetch_promise = pkg.fetch(true);
@ -38,61 +66,199 @@ onMounted(async () => {
await Promise.all([prf.reloadList(), prf.reload()]); await Promise.all([prf.reloadList(), prf.reload()]);
if (prf.current !== null) { if (prf.current !== null) {
currentTab.value = 0; currentTab.value = 'loc';
await pkg.reloadAll(); await pkg.reloadAll();
} }
fetch_promise.then(async () => { await fetch_promise;
await invoke('install_package', {
key: 'segatools-mu3hook',
force: false,
});
await invoke('install_package', {
key: 'segatools-chusanhook',
force: false,
});
});
}); });
const errorVisible = ref(false);
const errorMessage = ref('No error');
const errorHeader = ref('No header');
listen<{ message: string; header: string }>('invoke-error', (event) => {
errorVisible.value = true;
errorMessage.value = event.payload.message;
errorHeader.value = event.payload.header;
});
listen<string>('launch-error', (event) => {
errorVisible.value = true;
errorMessage.value = event.payload;
errorHeader.value = 'Launch error';
});
interface DownloadingStatus {
ratio: number;
pkg_key: string;
}
const downloading_status: Ref<DownloadingStatus[]> = ref([]);
const download_value = computed(() => {
return (
downloading_status.value.map((v) => v.ratio).reduce((a, v) => a * v) *
100
);
});
const downloadProgressText = computed(() => {
if (download_value.value < 7) {
return '';
}
let pkgs = `${downloading_status.value.length} package${downloading_status.value.length === 1 ? '' : 's'}`;
if (download_value.value < 14) {
return pkgs;
} else {
return `${pkgs} (${Math.floor(download_value.value)}%)`;
}
});
listen<DownloadingStatus>('download-progress', (event) => {
let status = downloading_status.value.find(
(v) => v.pkg_key === event.payload.pkg_key
);
if (status === undefined) {
status = {
ratio: 0,
pkg_key: event.payload.pkg_key,
};
downloading_status.value.push(status);
}
status.ratio = event.payload.ratio;
const remove = () => {
if (status !== undefined) {
downloading_status.value = downloading_status.value.filter(
(v) => v.pkg_key !== event.payload.pkg_key
);
}
};
if (status.ratio === 1.0) {
remove();
}
setTimeout(() => remove, 10_000);
});
</script> </script>
<template> <template>
<main> <main
:class="
client.scaleFactor === 's'
? 'main-scale-s'
: client.scaleFactor === 'm'
? 'main-scale-m'
: client.scaleFactor === 'l'
? 'main-scale-l'
: 'main-scale-xl'
"
>
<div
v-if="downloading_status.length > 0"
class="download-progress-bg"
></div>
<ProgressBar
v-if="downloading_status.length > 0"
:value="download_value"
class="download-progress"
>{{ downloadProgressText }}</ProgressBar
>
<ConfirmDialog>
<template #message="{ message }">
<ScrollPanel
v-if="messageSplit(message).length > 5"
style="width: 100%; height: 40vh"
>
<p v-for="m in messageSplit(message)">
{{ m }}
</p></ScrollPanel
>
<div v-else>
<p v-for="m in messageSplit(message)">
{{ m }}
</p>
</div>
</template>
</ConfirmDialog>
<Dialog
modal
:visible="errorVisible"
:closable="false /*this shit doesn't work */"
:header="errorHeader"
:style="{ width: '50vw' }"
>
<div class="flex flex-col gap-4">
{{ errorMessage }}
<Button
class="m-auto"
label="OK"
@click="errorVisible = false"
/>
</div>
</Dialog>
<Dialog
modal
:visible="updateProgress !== null"
:closable="false"
header="Updating"
:style="{ width: '200px' }"
>
<ProgressBar :value="updateProgress ?? undefined" />
</Dialog>
<Tabs <Tabs
lazy lazy
:value="currentTab" :value="currentTab"
v-on:update:value="(value) => (currentTab = value)" v-on:update:value="
(value) => {
currentTab = value as any;
}
"
class="h-screen" class="h-screen"
> >
<div class="fixed w-full flex z-100"> <div class="fixed w-full flex z-100">
<TabList class="grow"> <TabList class="grow" :show-navigators="false">
<Tab :value="3" <Tab value="users"><div class="pi pi-users"></div></Tab>
><div class="pi pi-question-circle"></div <Tab :disabled="isProfileDisabled" value="loc"
></Tab>
<Tab :disabled="isProfileDisabled" :value="0"
><div class="pi pi-box"></div ><div class="pi pi-box"></div
></Tab> ></Tab>
<Tab v-if="prf.current?.meta.game === 'chunithm'" :value="4" <Tab
v-if="
prf.current?.meta.game === 'chunithm' &&
prf.current.data.sgt.target.length > 0
"
value="patches"
><div class="pi pi-ticket"></div ><div class="pi pi-ticket"></div
></Tab> ></Tab>
<Tab <Tab
v-if="pkg.networkStatus === 'online'" v-if="pkg.networkStatus === 'online'"
:disabled="isProfileDisabled" :disabled="isProfileDisabled"
:value="1" value="rmt"
><div class="pi pi-download"></div ><div class="pi pi-download"></div
></Tab> ></Tab>
<Tab :disabled="isProfileDisabled" :value="2" <Tab :disabled="isProfileDisabled" value="cfg"
><div class="pi pi-cog"></div ><div class="pi pi-cog"></div
></Tab> ></Tab>
<Tab value="info"
><div class="pi pi-info-circle"></div
></Tab>
<div class="grow"></div> <div class="grow"></div>
<div class="flex gap-4"> <div class="flex gap-4">
<div class="flex" v-if="currentTab !== 3"> <div
class="flex"
v-if="['loc', 'rmt', 'cfg'].includes(currentTab)"
>
<InputIcon class="self-center mr-2"> <InputIcon class="self-center mr-2">
<i class="pi pi-search" /> <i class="pi pi-search" />
</InputIcon> </InputIcon>
<InputText <InputText
v-if="currentTab === 2" v-if="currentTab === 'cfg'"
style="min-width: 0; width: 25dvw"
class="self-center" class="self-center"
size="small" size="small"
placeholder="Search" placeholder="Search"
@ -100,6 +266,7 @@ onMounted(async () => {
/> />
<InputText <InputText
v-else v-else
style="min-width: 0; width: 25dvw"
class="self-center" class="self-center"
size="small" size="small"
placeholder="Search" placeholder="Search"
@ -114,48 +281,76 @@ onMounted(async () => {
:disabled="true" :disabled="true"
/> />
<Button <Button
v-if="pkg.networkStatus === 'offline'" v-if="
pkg.networkStatus === 'offline' &&
!client.offlineMode
"
class="shrink self-center" class="shrink self-center"
icon="pi pi-sync" icon="pi pi-sync"
size="small" size="small"
@click="pkg.fetch(false)" @click="pkg.fetch(false)"
/> />
<Button
v-if="
pkg.networkStatus === 'online' &&
pkg.hasAvailableUpdates
"
icon="pi pi-download"
label="UPDATE ALL"
size="small"
class="mr-4 m-2.5"
@click="pkg.updateAll()"
></Button>
</div> </div>
<div class="grow"></div> <div class="grow"></div>
<StartButton /> <StartButton />
</TabList> </TabList>
</div> </div>
<TabPanels class="w-full grow mt-[3rem]"> <TabPanels class="w-full grow mt-[3rem]">
<TabPanel :value="0"> <TabPanel value="loc">
<ModList :search="pkgSearchTerm" /> <ModList :search="pkgSearchTerm" />
</TabPanel> </TabPanel>
<TabPanel :value="1"> <TabPanel value="rmt">
<ModStore :search="pkgSearchTerm" /> <ModStore :search="pkgSearchTerm" />
</TabPanel> </TabPanel>
<TabPanel :value="2"> <TabPanel value="cfg">
<OptionList /> <OptionList />
</TabPanel> </TabPanel>
<TabPanel :value="3"> <TabPanel value="users">
<strong>UNDER CONSTRUCTION</strong><br />Some features are
missing.<br />Existing features are expected to break
sometimes.
<ProfileList /> <ProfileList />
<br /><br /><br />
<footer>
<Button
icon="pi pi-discord"
as="a"
target="_blank"
href="https://discord.gg/jxvzHjjEmc"
/>
</footer>
</TabPanel> </TabPanel>
<TabPanel :value="4"> <TabPanel value="patches">
CHUNITHM patches are not yet implemented.<br />Use <PatchList
<a href=https://patcher.two-torial.xyz/ target="_blank" style="text-decoration: underline;">patcher.two-torial.xyz</a> v-if="
pkg.hasLocal('mempatcher-mempatcher') &&
prf.isPkgKeyEnabled('mempatcher-mempatcher')
.value === true
"
/>
<div v-else>
Patches require <code>mempatcher</code> to be installed
and enabled.
<div>
<Button
label="Add mempatcher"
icon="pi pi-plus"
class="mt-3"
@click="
() =>
pkg.installFromKey(
'mempatcher-mempatcher'
)
"
/>
</div>
</div>
</TabPanel>
<TabPanel value="info">
<InfoPage />
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
<div v-if="currentTab === 5 || currentTab === 3"> <div v-if="currentTab === 'users' || currentTab === 'info'">
<img <img
v-if="prf.current?.meta.game === 'ongeki'" v-if="prf.current?.meta.game === 'ongeki'"
src="/sticker-ongeki.svg" src="/sticker-ongeki.svg"
@ -184,4 +379,61 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.main-scale-s {
zoom: 1;
}
.main-scale-m {
zoom: 1.25;
}
.main-scale-l {
zoom: 1.4;
}
.main-scale-xl {
zoom: 1.7;
}
.p-tablist {
background-color: #ffffff !important;
border-bottom: white 10px !important;
}
.p-tab {
border-bottom: none !important;
}
.p-tablist-active-bar {
display: none !important;
}
.p-tooltip {
min-width: 300px;
}
.p-progressbar,
.p-progressbar-value,
.p-progressbar-label {
transition-duration: 0s !important;
}
.download-progress {
position: fixed !important;
bottom: 0;
left: 5vw;
width: 90vw;
z-index: 10000 !important;
margin: 20px auto;
}
.download-progress-bg {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 60px;
background-color: var(--p-surface-900);
border-top: 1px solid var(--p-surface-600);
z-index: 998;
}
</style> </style>

View File

@ -65,7 +65,7 @@ const filePick = async () => {
<template> <template>
<Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" /> <Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" />
<div v-else> <div class="primitive-base" v-else>
<Button <Button
v-if="exists" v-if="exists"
icon="pi pi-pen-to-square" icon="pi pi-pen-to-square"
@ -102,12 +102,20 @@ const filePick = async () => {
font-family: monospace; font-family: monospace;
white-space: nowrap; white-space: nowrap;
position: fixed; position: fixed;
top: 10vh; top: 50%;
left: 10vw; left: 50%;
height: 80vh; height: 500px;
width: 80vw; width: 800px;
margin-left: -400px;
margin-top: -250px;
z-index: 1000; z-index: 1000;
padding: 20px;
border-radius: 20px;
background-color: #151515; background-color: #151515;
color: #ddd; color: #ddd;
} }
.primitive-base ::-webkit-scrollbar {
display: none;
}
</style> </style>

View File

@ -1,7 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button'; import Button from 'primevue/button';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import * as path from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog'; import { open } from '@tauri-apps/plugin-dialog';
import { usePrfStore } from '../stores';
const props = defineProps({ const props = defineProps({
placeholder: String, placeholder: String,
@ -12,10 +14,25 @@ const props = defineProps({
callback: Function, callback: Function,
}); });
const prf = usePrfStore();
const filePick = async () => { const filePick = async () => {
const exePath = prf.current?.data.sgt.target;
let defaultPath: string | undefined;
if (
exePath !== undefined &&
exePath.length > 0 &&
props.value !== undefined &&
!(await path.isAbsolute(props.value))
) {
defaultPath = await path.join(exePath, '..');
defaultPath = await path.join(defaultPath, props.value);
defaultPath = await path.join(defaultPath, '..');
}
const res = await open({ const res = await open({
multiple: false, multiple: false,
directory: props.directory, directory: props.directory,
defaultPath,
filters: filters:
props.promptname && props.extension props.promptname && props.extension
? [ ? [
@ -28,7 +45,7 @@ const filePick = async () => {
}); });
if (res != null && props.callback !== undefined) { if (res != null && props.callback !== undefined) {
props.callback(res); props.callback(res);
/*path.relative(cfgs.current?.data.exe_dir ?? '', res) */ /*path.relative(prf.current?.data.sgt.target ?? '', res) */
} }
}; };
</script> </script>

View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import { ref } from 'vue';
import Button from 'primevue/button';
import ScrollPanel from 'primevue/scrollpanel';
import { invoke } from '../invoke';
import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
const changelog = ref('');
invoke('get_changelog').then((s) => (changelog.value = s as string));
</script>
<template>
<h1>About</h1>
STARTLINER is a simple launcher, configuration tool and mod manager for
O.N.G.E.K.I. and CHUNITHM.
<h1>Changelog</h1>
<ScrollPanel style="height: 200px">
<div class="markdown">
<vue-markdown-it
:source="changelog"
:options="{ typographer: true, breaks: true }"
/>
</div>
</ScrollPanel>
<footer class="mt-10 flex gap-3">
<Button
icon="pi pi-discord"
as="a"
target="_blank"
href="https://discord.gg/jxvzHjjEmc"
/>
<Button
icon="pi pi-github"
as="a"
target="_blank"
href="https://gitea.tendokyu.moe/akanyan/STARTLINER"
/>
</footer>
</template>
<style lang="css">
h1 {
font-size: 1.7rem;
}
.markdown h3 {
font-size: 1.2rem;
}
.markdown h2 {
font-size: 1.4rem;
}
.markdown ul {
list-style-type: circle;
}
.markdown li {
margin-left: 40px;
}
.markdown a {
text-decoration: underline;
}
</style>

View File

@ -1,47 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import Button from 'primevue/button'; import Button from 'primevue/button';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { usePkgStore } from '../stores';
import { Package } from '../types'; import { Package } from '../types';
import { pkgKey } from '../util'; import { pkgKey } from '../util';
const pkgs = usePkgStore();
const props = defineProps({ const props = defineProps({
pkg: Object as () => Package, pkg: Object as () => Package,
}); });
const install = async () => { const deleting = ref(false);
if (props.pkg === undefined) {
return;
}
try {
await invoke('install_package', {
key: pkgKey(props.pkg),
force: true,
});
} catch (err) {
console.error(err);
if (props.pkg !== undefined) {
props.pkg.js.busy = false;
}
}
//if (rv === 'Deferred') { /* download progress */ }
};
const remove = async () => { const remove = async () => {
if (props.pkg === undefined) { if (props.pkg === undefined) {
return; return;
} }
deleting.value = true;
await invoke('delete_package', { await invoke('delete_package', {
key: pkgKey(props.pkg), key: pkgKey(props.pkg),
}); });
deleting.value = false;
}; };
</script> </script>
<template> <template>
<Button <Button
v-if="pkg?.loc" v-if="pkg?.loc && !pkg?.js.downloading"
rounded rounded
icon="pi pi-trash" icon="pi pi-trash"
severity="danger" severity="danger"
@ -49,7 +39,7 @@ const remove = async () => {
size="small" size="small"
class="self-center ml-4" class="self-center ml-4"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
:loading="pkg?.js.busy" :loading="deleting"
v-on:click="remove()" v-on:click="remove()"
/> />
@ -62,7 +52,7 @@ const remove = async () => {
size="small" size="small"
class="self-center ml-4" class="self-center ml-4"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
:loading="pkg?.js.busy" :loading="pkg?.js.downloading"
v-on:click="install()" v-on:click="async () => await pkgs.install(pkg)"
/> />
</template> </template>

View File

@ -0,0 +1,188 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import InputText from 'primevue/inputtext';
import { fromKeycode, toKeycode } from '../keyboard';
import { usePrfStore } from '../stores';
import { OngekiButtons } from '../types';
const prf = usePrfStore();
const hasClickedM1Once = ref(false);
const handleKey = (
button: string | undefined,
event: KeyboardEvent,
index?: number
) => {
event.preventDefault();
let keycode = toKeycode(event.code);
if (keycode !== null && button !== undefined) {
const data = prf.current!.data.keyboard!.data as any;
if (event.getModifierState('NumLock') === false) {
switch (event.code) {
case 'NumpadDecimal':
keycode = toKeycode('Delete');
break;
case 'Numpad0':
keycode = toKeycode('Insert');
break;
case 'Numpad1':
keycode = toKeycode('End');
break;
case 'Numpad2':
keycode = toKeycode('ArrowDown');
break;
case 'Numpad3':
keycode = toKeycode('PageDown');
break;
case 'Numpad4':
keycode = toKeycode('ArrowLeft');
break;
case 'Numpad5':
keycode = toKeycode('Clear');
break;
case 'Numpad6':
keycode = toKeycode('ArrowRight');
break;
case 'Numpad7':
keycode = toKeycode('Home');
break;
case 'Numpad8':
keycode = toKeycode('ArrowUp');
break;
case 'Numpad9':
keycode = toKeycode('PageUp');
break;
default:
break;
}
}
if (index !== undefined) {
data[button][index] = keycode;
} else {
data[button] = keycode;
}
}
};
const handleMouse = (
button: string | undefined,
event: MouseEvent,
index?: number
) => {
if (button === undefined || button == 'use_mouse') {
return;
}
if (event.button === 0) {
if (hasClickedM1Once.value === false) {
hasClickedM1Once.value = true;
return;
}
} else {
event.preventDefault();
}
let keycode;
switch (event.button) {
case 0:
keycode = 1;
break;
case 1:
keycode = 4;
break;
case 2:
keycode = 2;
break;
case 3:
keycode = 5;
break;
case 4:
keycode = 6;
break;
default:
break;
}
if (keycode !== undefined) {
const data = prf.current!.data.keyboard!.data as any;
if (index !== undefined) {
data[button][index] = keycode;
} else {
data[button] = keycode;
}
}
};
const getKey = (key: keyof OngekiButtons, index?: number): any =>
computed(() => {
const data = prf.current!.data.keyboard?.data as any;
const keycode =
index === undefined
? (data[key] as number | undefined)
: (data[key]?.[index] as number | undefined);
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '';
});
const props = defineProps({
small: Boolean,
tall: Boolean,
tooltip: String,
button: String,
color: String,
index: Number,
});
const modelValue = computed(() => {
return getKey(props.button as keyof OngekiButtons, props.index).value;
});
const fontSize = computed(() => {
if (!props.small) {
return '1rem';
}
const len = modelValue.value.length;
if (len < 5) {
return '1rem';
}
if (len < 7) {
return '0.75rem';
}
return '0.5rem';
});
</script>
<template>
<InputText
:style="{
width: small ? '2.8rem' : '5rem',
height: small ? '2.8rem' : tall ? '10rem' : '5rem',
fontSize,
backgroundColor: color,
}"
unstyled
class="text-center buttoninputtext"
v-tooltip="tooltip ? `${tooltip}: ${modelValue}` : undefined"
@contextmenu.prevent="() => {}"
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
@mousedown="
(ev: MouseEvent) =>
handleMouse(button as keyof OngekiButtons, ev, index)
"
@focusout="() => (hasClickedM1Once = false)"
:model-value="modelValue"
/>
</template>
<style scoped lang="css">
.buttoninputtext {
border-radius: 6px;
border: 1px solid rgba(200, 200, 200, 0.3);
overflow: scroll !important;
text-align: center !important;
}
</style>

View File

@ -15,7 +15,7 @@ const props = defineProps({
const pkgs = usePkgStore(); const pkgs = usePkgStore();
const prf = usePrfStore(); const prf = usePrfStore();
const empty = ref(true); const empty = ref(false);
const gameSublist: Ref<string[]> = ref([]); const gameSublist: Ref<string[]> = ref([]);
invoke('get_game_packages', { invoke('get_game_packages', {
@ -81,5 +81,5 @@ const missing = computed(() => {
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()"> <Fieldset v-for="(namespace, key) in group" :legend="key.toString()">
<ModListEntry v-for="p in namespace" :pkg="p" /> <ModListEntry v-for="p in namespace" :pkg="p" />
</Fieldset> </Fieldset>
<div v-if="empty" class="text-3xl"></div> <div v-if="empty === true" class="text-3xl fadein"></div>
</template> </template>

View File

@ -2,11 +2,11 @@
import { computed } from 'vue'; import { computed } from 'vue';
import Button from 'primevue/button'; import Button from 'primevue/button';
import ToggleSwitch from 'primevue/toggleswitch'; import ToggleSwitch from 'primevue/toggleswitch';
import { open } from '@tauri-apps/plugin-shell';
import InstallButton from './InstallButton.vue'; import InstallButton from './InstallButton.vue';
import LinkButton from './LinkButton.vue'; 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 { usePkgStore, usePrfStore } from '../stores'; import { usePkgStore, usePrfStore } from '../stores';
import { Feature, Package } from '../types'; import { Feature, Package } from '../types';
import { hasFeature } from '../util'; import { hasFeature } from '../util';
@ -26,19 +26,32 @@ const model = computed({
await prf.togglePkg(props.pkg, value); await prf.togglePkg(props.pkg, value);
}, },
}); });
const unsupported = computed(() => props.pkg!.loc!.status === 'Unsupported');
if (unsupported.value === true && model.value === true) {
prf.togglePkg(props.pkg, false);
}
</script> </script>
<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 :pkg="pkg" />
<UpdateButton :pkg="pkg" /> <UpdateButton :pkg="pkg" />
<ToggleSwitch <span
v-if="hasFeature(pkg, Feature.Mod)" v-tooltip="
class="scale-[1.33] shrink-0" unsupported &&
inputId="switch" 'This package is currently incompatible with STARTLINER.'
:disabled="pkg!.loc!.status === 'Unsupported'" "
v-model="model" >
/> <ToggleSwitch
v-if="hasFeature(pkg, Feature.Mod) || unsupported === true"
class="scale-[1.33] shrink-0"
inputId="switch"
:disabled="unsupported === true"
v-model="model"
/>
</span>
<InstallButton :pkg="pkg" /> <InstallButton :pkg="pkg" />
<Button <Button
rounded rounded
@ -48,7 +61,9 @@ const model = computed({
size="small" size="small"
class="ml-2 shrink-0" class="ml-2 shrink-0"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
v-on:click="pkg?.loc && open(pkg.loc.path ?? '')" v-on:click="
pkg?.loc?.path && invoke('open_file', { path: pkg.loc.path })
"
/> />
<LinkButton v-if="pkgs.networkStatus === 'online'" :pkg="pkg" /> <LinkButton v-if="pkgs.networkStatus === 'online'" :pkg="pkg" />
</div> </div>

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, ref } from 'vue'; import { Ref, computed, ref } from 'vue';
import Button from 'primevue/button';
import Divider from 'primevue/divider'; import Divider from 'primevue/divider';
import MultiSelect from 'primevue/multiselect'; import MultiSelect from 'primevue/multiselect';
import ToggleSwitch from 'primevue/toggleswitch'; import ToggleSwitch from 'primevue/toggleswitch';
@ -40,6 +41,39 @@ const list = () => {
empty.value = res.length === 0; empty.value = res.length === 0;
return res; return res;
}; };
const shouldShowRecommended = computed(() => {
if (prf.current!.meta.game === 'ongeki') {
return !pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-mu3hook');
}
if (prf.current!.meta.game === 'chunithm') {
return (
!pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-chusanhook') ||
!pkgs.allLocal.some((p) => pkgKey(p) === 'mempatcher-mempatcher')
);
}
return false;
});
const getRecommendedTooltip = () => {
if (prf.current!.meta.game === 'ongeki') {
return 'segatools-mu3hook';
}
if (prf.current!.meta.game === 'chunithm') {
return 'segatools-chusanhook and mempatcher';
}
return '';
};
const installRecommended = () => {
if (prf.current!.meta.game === 'ongeki') {
pkgs.installFromKey('segatools-mu3hook');
}
if (prf.current!.meta.game === 'chunithm') {
pkgs.installFromKey('segatools-chusanhook');
pkgs.installFromKey('mempatcher-mempatcher');
}
};
</script> </script>
<template> <template>
@ -78,6 +112,14 @@ const list = () => {
</div> </div>
</div> </div>
<Divider /> <Divider />
<Button
v-if="shouldShowRecommended"
label="Install recommended packages"
v-tooltip="getRecommendedTooltip"
icon="pi pi-plus"
class="mb-3"
@click="installRecommended"
/>
<div v-for="p in list()" class="flex flex-row"> <div v-for="p in list()" class="flex flex-row">
<ModStoreEntry :pkg="p" /> <ModStoreEntry :pkg="p" />
</div> </div>

View File

@ -38,7 +38,7 @@ const iconSrc = computed(() => {
<label class="m-3 align-middle text grow z-5 h-50px"> <label class="m-3 align-middle text grow z-5 h-50px">
<div> <div>
<span class="text-lg"> <span class="text-lg">
{{ pkg?.name ?? 'Untitled' }} {{ pkg?.name.replaceAll('_', ' ') ?? 'Untitled' }}
</span> </span>
<span <span
v-if="pkg?.rmt?.deprecated" v-if="pkg?.rmt?.deprecated"
@ -62,7 +62,10 @@ const iconSrc = computed(() => {
> >
</span> </span>
<span <span
v-if="hasFeature(pkg, Feature.Mu3IO)" v-if="
hasFeature(pkg, Feature.Mu3IO) ||
hasFeature(pkg, Feature.ChuniIO)
"
v-tooltip="'IO'" v-tooltip="'IO'"
class="pi pi-wrench ml-1 text-green-400" class="pi pi-wrench ml-1 text-green-400"
> >
@ -73,6 +76,15 @@ const iconSrc = computed(() => {
class="pi pi-credit-card ml-1 text-purple-400" class="pi pi-credit-card ml-1 text-purple-400"
> >
</span> </span>
<span
v-if="
hasFeature(pkg, Feature.GameDLL) ||
hasFeature(pkg, Feature.AmdDLL)
"
v-tooltip="'DLL'"
class="pi pi-cog ml-1 text-red-400"
>
</span>
<span <span
v-if="showNamespace && pkg?.namespace" v-if="showNamespace && pkg?.namespace"
class="text-sm opacity-75" class="text-sm opacity-75"

View File

@ -0,0 +1,175 @@
<script setup lang="ts">
import { ComputedRef, computed, onMounted, ref } from 'vue';
import Button from 'primevue/button';
import Carousel from 'primevue/carousel';
import Dialog from 'primevue/dialog';
import { fromKeycode } from '../keyboard';
import { useClientStore, usePrfStore } from '../stores';
import { prettyPrint } from '../util';
import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
const prf = usePrfStore();
const client = useClientStore();
const props = defineProps({
visible: Boolean,
firstTime: Boolean,
onFinish: Function,
});
interface Datum {
text: string;
image: string;
}
const game = computed(() => prf.current?.meta.game);
const processText = (s: string) => {
if (prf.current!.data.keyboard?.data.enabled) {
const testKey = prf.current!.data.keyboard?.data.test;
const readable = fromKeycode(testKey);
if (readable !== null) {
return s.replace(
'%TESTMENU%',
`${readable} or a button on the back of the controller`
);
}
}
return s.replace('%TESTMENU%', 'a button on the back of the controller');
};
const loadPage = async (title: string) => {
return {
text: await (await fetch(`/help-${title}.md`)).text(),
image: `help-${title}.png`,
};
};
let systemProcessing: Datum;
let standardOngeki: Datum;
let standardChunithm: Datum;
let lever: Datum;
let server: Datum;
let finaleOngeki: Datum;
let finaleChunithm: Datum;
const data: ComputedRef<Datum[]> = computed(() => {
const res = [];
switch (prf.current?.meta.game) {
case 'ongeki':
res.push(systemProcessing);
res.push(standardOngeki);
res.push(lever);
res.push(finaleOngeki);
break;
case 'chunithm':
res.push(standardChunithm);
res.push(server);
res.push(finaleChunithm);
break;
default:
break;
}
return res;
});
onMounted(async () => {
[standardOngeki, systemProcessing, lever, server, finaleOngeki] =
await Promise.all([
loadPage('standard'),
loadPage('ongeki-system-processing'),
loadPage('ongeki-lever'),
loadPage('chunithm-server'),
loadPage('finale'),
]);
standardOngeki = {
...standardOngeki,
image: '/help-standard-ongeki.png',
};
standardChunithm = {
...standardOngeki,
image: '/help-standard-chunithm.png',
};
finaleOngeki = {
...finaleOngeki,
image: '/help-finale-ongeki.png',
};
finaleChunithm = {
...finaleOngeki,
image: '/help-finale-chunithm.png',
};
});
const counter = ref(0);
const exitLabel = computed(() => {
return props.firstTime === true && counter.value < data.value.length - 1
? 'Skip'
: 'Close';
});
</script>
<template>
<Dialog
modal
:visible="visible"
:closable="false"
:header="
firstTime
? `It looks like you're running ${game ? prettyPrint(game) : '<game>'} for the first time`
: `${game ? prettyPrint(game) : '<game>'} help`
"
:style="{ width: '760px', scale: client.scaleValue }"
>
<Carousel
:value="data"
:num-visible="1"
:num-scroll="1"
:page="counter"
v-on:update:page="(p) => (counter = p)"
>
<template #item="slotProps">
<div class="md-container markdown">
<vue-markdown-it
:source="processText(slotProps.data?.text)"
:options="{
typographer: true,
breaks: true,
html: true,
}"
/>
</div>
<div
class="border border-surface-200 dark:border-surface-700 rounded m-2"
>
<img :src="slotProps.data.image" />
</div>
</template>
</Carousel>
<div style="width: 100%; text-align: center">
<Button
v-if="counter < data.length - 1"
class="m-auto mr-4"
label="Next"
@click="() => (counter += 1)"
/>
<Button
class="m-auto"
:label="exitLabel"
@click="() => onFinish && onFinish()"
/>
</div>
</Dialog>
</template>
<style lang="css">
.p-dialog ::-webkit-scrollbar {
display: none;
}
.md-container {
height: 9.5rem;
}
</style>

View File

@ -6,6 +6,8 @@ const general = useGeneralStore();
defineProps({ defineProps({
title: String, title: String,
collapsed: Boolean,
alwaysFound: Boolean,
}); });
</script> </script>
@ -13,7 +15,8 @@ defineProps({
<Fieldset <Fieldset
:legend="title" :legend="title"
:toggleable="true" :toggleable="true"
v-show="general.cfgCategories.has(title ?? '')" v-show="general.cfgCategories.has(title ?? '') || alwaysFound"
:collapsed="collapsed"
> >
<div class="flex w-full flex-col gap-1"> <div class="flex w-full flex-col gap-1">
<slot /> <slot />

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import InputNumber from 'primevue/inputnumber';
import SelectButton from 'primevue/selectbutton'; import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch'; import ToggleSwitch from 'primevue/toggleswitch';
import FileEditor from './FileEditor.vue'; import FileEditor from './FileEditor.vue';
@ -7,70 +8,65 @@ import OptionCategory from './OptionCategory.vue';
import OptionRow from './OptionRow.vue'; import OptionRow from './OptionRow.vue';
import AimeOptions from './options/Aime.vue'; import AimeOptions from './options/Aime.vue';
import DisplayOptions from './options/Display.vue'; import DisplayOptions from './options/Display.vue';
import KeyboardOptions from './options/Keyboard.vue';
import MiscOptions from './options/Misc.vue'; import MiscOptions from './options/Misc.vue';
import NetworkOptions from './options/Network.vue'; import NetworkOptions from './options/Network.vue';
import SegatoolsOptions from './options/Segatools.vue'; import SegatoolsOptions from './options/Segatools.vue';
import StartlinerOptions from './options/Startliner.vue';
import { usePrfStore } from '../stores'; import { usePrfStore } from '../stores';
const prf = usePrfStore(); const prf = usePrfStore();
const audioModel = computed({ const blacklistMinModel = computed({
get() { get() {
return prf.current?.data.mu3_ini?.audio ?? null; if (prf.current?.data.mu3_ini?.blacklist === undefined) {
}, return null;
set(value: 'Shared' | 'Excl6Ch' | 'Excl2Ch') {
if (prf.current!.data.mu3_ini === undefined) {
prf.current!.data.mu3_ini = {};
} }
prf.current!.data.mu3_ini!.audio = value; return prf.current?.data.mu3_ini?.blacklist[0];
},
set(value: number) {
prf.current!.data.mu3_ini!.blacklist = [
value,
prf.current!.data.mu3_ini!.blacklist?.[1] ?? 19999,
];
}, },
}); });
// const blacklistMinModel = computed({ const blacklistMaxModel = computed({
// get() { get() {
// if (prf.current?.data.mu3_ini?.blacklist === undefined) { if (prf.current?.data.mu3_ini?.blacklist === undefined) {
// return null; return null;
// } }
// return prf.current?.data.mu3_ini?.blacklist[0]; return prf.current?.data.mu3_ini.blacklist[1];
// }, },
// set(value: number) { set(value: number) {
// if (prf.current!.data.mu3_ini === undefined) { prf.current!.data.mu3_ini!.blacklist = [
// prf.current!.data.mu3_ini = {}; prf.current!.data.mu3_ini!.blacklist?.[0] ?? 10000,
// } value,
// prf.current!.data.mu3_ini!.blacklist = [ ];
// value, },
// prf.current!.data.mu3_ini!.blacklist?.[1] ?? 19999, });
// ];
// },
// });
// const blacklistMaxModel = computed({
// get() {
// if (prf.current?.data.mu3_ini?.blacklist === undefined) {
// return null;
// }
// return prf.current?.data.mu3_ini?.blacklist[1];
// },
// set(value: number) {
// if (prf.current!.data.mu3_ini === undefined) {
// prf.current!.data.mu3_ini = {};
// }
// prf.current!.data.mu3_ini!.blacklist = [
// prf.current!.data.mu3_ini!.blacklist?.[0] ?? 10000,
// value,
// ];
// },
// });
prf.reload(); prf.reload();
</script> </script>
<template> <template>
<SegatoolsOptions /> <SegatoolsOptions />
<DisplayOptions v-if="prf.current!.meta.game === 'ongeki'" /> <DisplayOptions />
<NetworkOptions /> <NetworkOptions />
<AimeOptions /> <AimeOptions />
<MiscOptions /> <MiscOptions />
<OptionCategory
title="Extensions"
v-if="prf.current!.meta.game === 'chunithm'"
>
<OptionRow title="Saekawa config">
<FileEditor
filename="saekawa.toml"
promptname="saekawa config file"
extension="toml"
/> </OptionRow
></OptionCategory>
<OptionCategory <OptionCategory
title="Extensions" title="Extensions"
v-if="prf.current!.meta.game === 'ongeki'" v-if="prf.current!.meta.game === 'ongeki'"
@ -89,34 +85,59 @@ prf.reload();
<OptionRow <OptionRow
title="Audio mode" title="Audio mode"
tooltip="Exclusive 2-channel mode requires a patch" tooltip="Exclusive 2-channel mode requires 7EVENDAYSHOLIDAYS-ExclusiveAudio"
> >
<SelectButton <SelectButton
v-model="audioModel" v-model="prf.current!.data.mu3_ini!.audio"
:options="[ :options="[
{ title: 'Shared', value: 'Shared' }, { title: 'Shared', value: 'Shared' },
{ title: 'Exclusive 6-channel', value: 'Excl6Ch' }, { title: 'Exclusive 6-channel', value: 'Excl6Ch' },
{ title: 'Exclusive 2-channel', value: 'Excl2Ch' }, { title: 'Exclusive 2-channel', value: 'Excl2Ch' },
]" ]"
:allow-empty="true" :allow-empty="false"
option-label="title" option-label="title"
option-value="value" option-value="value"
/></OptionRow> /></OptionRow>
<!-- <OptionRow <OptionRow
title="Sample rate"
v-if="
prf.current?.data.mods.includes(
'7EVENDAYSHOLIDAYS-ExclusiveAudio'
)
"
>
<SelectButton
v-model="prf.current!.data.mu3_ini!.sample_rate"
:disabled="prf.current!.data.mu3_ini!.audio === 'Shared'"
:options="[
{ title: '44.1KHz', value: 44100 },
{ title: '48KHz', value: 48000 },
{ title: '96KHz', value: 96000 },
{ title: '192KHz', value: 192000 },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/></OptionRow>
<OptionRow
v-if="
prf.current?.data.mods.includes('7EVENDAYSHOLIDAYS-Blacklist')
"
class="number-input" class="number-input"
title="Song ID Blacklist" title="Song ID Blacklist"
tooltip="Requires a patch" tooltip="Scores on charts within this ID range will not be saved nor uploaded"
><InputNumber ><InputNumber
class="shrink" class="shrink"
size="small" size="small"
:min="10000" :min="9000"
:max="99999" :max="99999"
placeholder="10000" placeholder="10000"
:use-grouping="false" :use-grouping="false"
:allow-empty="false" :allow-empty="false"
v-model="blacklistMinModel" /> v-model="blacklistMinModel" />
x ~
<InputNumber <InputNumber
class="shrink" class="shrink"
size="small" size="small"
@ -126,8 +147,39 @@ prf.reload();
:use-grouping="false" :use-grouping="false"
:allow-empty="false" :allow-empty="false"
v-model="blacklistMaxModel" v-model="blacklistMaxModel"
/></OptionRow> --> /></OptionRow>
<OptionRow
class="number-input"
title="GP"
v-if="
prf.current?.data.mods.includes('7EVENDAYSHOLIDAYS-DisableGP')
"
><InputNumber
class="shrink"
size="small"
:min="0"
:max="9999"
:use-grouping="false"
:allow-empty="false"
v-model="prf.current!.data.mu3_ini!.gp"
/>
</OptionRow>
<OptionRow
title="Unlock Bonus Tracks"
tooltip="Disabling this option can help declutter the song list"
v-if="
prf.current?.data.mods.includes(
'7EVENDAYSHOLIDAYS-UnlockAllMusic'
)
"
>
<ToggleSwitch
v-model="prf.current!.data.mu3_ini!.enable_bonus_tracks"
/>
</OptionRow>
</OptionCategory> </OptionCategory>
<KeyboardOptions />
<StartlinerOptions />
</template> </template>
<style> <style>

View File

@ -8,6 +8,8 @@ const category = getCurrentInstance()?.parent?.parent?.parent?.parent; // yes in
const props = defineProps({ const props = defineProps({
title: String, title: String,
tooltip: String, tooltip: String,
dangerousTooltip: String,
greytext: String,
}); });
const searched = computed(() => { const searched = computed(() => {
@ -32,6 +34,17 @@ const searched = computed(() => {
class="pi pi-question-circle ml-2" class="pi pi-question-circle ml-2"
v-tooltip="tooltip" v-tooltip="tooltip"
></span> ></span>
<span
v-if="dangerousTooltip"
class="pi pi-exclamation-circle ml-2 text-red-500"
v-tooltip="dangerousTooltip"
></span>
<span
v-if="greytext"
style="font-size: 0.65rem"
class="ml-2 text-gray-400"
>{{ greytext }}</span
>
</div> </div>
<slot /> <slot />

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import InputNumber from 'primevue/inputnumber';
import ToggleSwitch from 'primevue/toggleswitch';
import OptionRow from './OptionRow.vue';
import { usePrfStore } from '../stores';
import { Patch } from '../types';
const prf = usePrfStore();
const toggleUnary = (key: string, val: boolean) => {
if (val) {
prf.current!.data.patches[key] = 'enabled';
} else {
delete prf.current!.data.patches[key];
}
};
const setNumber = (key: string, val: number) => {
if (val) {
prf.current!.data.patches[key] = { number: val };
} else {
delete prf.current!.data.patches[key];
}
};
defineProps({
patch: Object as () => Patch,
});
</script>
<template>
<OptionRow
:title="patch?.name"
:tooltip="patch?.tooltip"
:greytext="patch?.id"
>
<ToggleSwitch
v-if="patch?.type === undefined"
:model-value="prf.current!.data.patches[patch!.id!] !== undefined"
@update:model-value="(v: boolean) => toggleUnary(patch!.id!, v)"
/>
<InputNumber
v-else-if="patch?.type === 'number'"
class="number-input"
:model-value="
(prf.current!.data.patches[patch!.id!] as any)?.number
"
@update:model-value="(v: number) => setNumber(patch!.id!, v)"
:min="patch?.min"
:max="patch?.max"
:placeholder="(patch?.default ?? 0).toString()"
/>
</OptionRow>
</template>

View File

@ -0,0 +1,60 @@
<script setup lang="ts">
import { Ref, ref } from 'vue';
import * as path from '@tauri-apps/api/path';
import OptionCategory from './OptionCategory.vue';
import PatchEntry from './PatchEntry.vue';
import { invoke } from '../invoke';
import { usePrfStore } from '../stores';
import { Patch } from '../types';
const prf = usePrfStore();
prf.reload();
const gamePatches: Ref<Patch[] | null> = ref(null);
const amdPatches: Ref<Patch[] | null> = ref(null);
invoke('list_patches', { target: prf.current!.data.sgt.target }).then(
(patches) => {
gamePatches.value = patches as Patch[];
}
);
(async () => {
const amd = await path.join(
prf.current!.data.sgt.target,
'../amdaemon.exe'
);
amdPatches.value = (await invoke('list_patches', {
target: amd,
})) as Patch[];
})();
const errorMessage =
"No compatible patches found. Make sure you're using unpacked and unpatched files.";
</script>
<template>
<OptionCategory title="chusanApp.exe" always-found>
<PatchEntry
v-if="gamePatches !== null"
v-for="p in gamePatches"
:patch="p"
/>
<div v-if="gamePatches === null">Loading...</div>
<div v-if="gamePatches !== null && gamePatches.length === 0">
{{ errorMessage }}
</div>
</OptionCategory>
<OptionCategory title="amdaemon.exe" always-found>
<PatchEntry
v-if="amdPatches !== null"
v-for="p in amdPatches"
:patch="p"
/>
<div v-if="gamePatches === null">Loading...</div>
<div v-if="amdPatches !== null && amdPatches.length === 0">
{{ errorMessage }}
</div>
</OptionCategory>
</template>

View File

@ -7,6 +7,9 @@ const prf = usePrfStore();
</script> </script>
<template> <template>
<div v-if="prf.list.length === 0">
Welcome to STARTLINER! Start by creating a profile.
</div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4"> <div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button <Button
label="O.N.G.E.K.I. profile" label="O.N.G.E.K.I. profile"
@ -19,7 +22,6 @@ const prf = usePrfStore();
icon="pi pi-plus" icon="pi pi-plus"
class="chunithm-button profile-button" class="chunithm-button profile-button"
@click="() => prf.create('chunithm')" @click="() => prf.create('chunithm')"
v-tooltip="'!!! Experimental !!!'"
/> />
</div> </div>
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4"> <div class="mt-12 flex flex-col flex-wrap align-middle gap-4">

View File

@ -2,14 +2,16 @@
import { ref } from 'vue'; import { ref } from 'vue';
import Button from 'primevue/button'; import Button from 'primevue/button';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import { useConfirm } from 'primevue/useconfirm';
import * as path from '@tauri-apps/api/path'; import * as path from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-shell';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { useGeneralStore, usePrfStore } from '../stores'; import { useGeneralStore, usePrfStore } from '../stores';
import { ProfileMeta } from '../types'; import { ProfileMeta } from '../types';
const prf = usePrfStore();
const general = useGeneralStore(); const general = useGeneralStore();
const prf = usePrfStore();
const confirmDialog = useConfirm();
const isEditing = ref(false); const isEditing = ref(false);
const props = defineProps({ const props = defineProps({
@ -55,6 +57,22 @@ const deleteProfile = async () => {
await prf.reloadList(); await prf.reloadList();
await prf.reload(); await prf.reload();
}; };
const promptDeleteProfile = async () => {
confirmDialog.require({
message: `Are you sure you want to delete ${props.p?.game}-${props.p?.name}?`,
header: 'Delete profile',
accept: deleteProfile,
});
};
const dataExists = ref(false);
path.join(general.dataDir, `profile-${props.p!.game}-${props.p!.name}`).then(
async (p) => {
dataExists.value = await invoke('file_exists', { path: p });
}
);
</script> </script>
<template> <template>
@ -74,6 +92,8 @@ const deleteProfile = async () => {
<div v-if="!isEditing">{{ p!.name }}</div> <div v-if="!isEditing">{{ p!.name }}</div>
<div v-else> <div v-else>
<InputText <InputText
unstyled
class="text-center"
:model-value="p!.name" :model-value="p!.name"
@vue:mounted="$event?.el?.focus()" @vue:mounted="$event?.el?.focus()"
@keyup="renameProfile" @keyup="renameProfile"
@ -89,7 +109,7 @@ const deleteProfile = async () => {
size="small" size="small"
class="self-center ml-2" class="self-center ml-2"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
@click="deleteProfile" @click="promptDeleteProfile"
/> />
<Button <Button
rounded rounded
@ -112,18 +132,45 @@ const deleteProfile = async () => {
@click="isEditing = true" @click="isEditing = true"
/> />
<Button <Button
rounded
icon="pi pi-cog"
severity="help"
aria-label="open-config-directory"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
@click="
path
.join(general.configDir, `profile-${p!.game}-${p!.name}`)
.then(async (path) => {
await invoke('open_file', { path });
})
"
/>
<Button
v-if="dataExists"
rounded rounded
icon="pi pi-folder" icon="pi pi-folder"
severity="help" severity="help"
aria-label="open-directory" aria-label="open-data-directory"
size="small" size="small"
class="self-center" class="self-center"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
@click=" @click="
path path
.join(general.dataDir, `profile-${p!.game}-${p!.name}`) .join(general.dataDir, `profile-${p!.game}-${p!.name}`)
.then(open) .then(async (path) => {
await invoke('open_file', { path });
})
" "
/> />
</div> </div>
</template> </template>
<style lang="css">
.p-tablist-tab-list {
border: none !important;
border-color: transparent !important;
border-radius: 0 !important;
}
</style>

View File

@ -1,16 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, computed, ref } from 'vue'; import { Ref, computed, ref } from 'vue';
import Button from 'primevue/button'; import Button from 'primevue/button';
import ConfirmDialog from 'primevue/confirmdialog';
import ContextMenu from 'primevue/contextmenu'; import ContextMenu from 'primevue/contextmenu';
import ScrollPanel from 'primevue/scrollpanel';
import { useConfirm } from 'primevue/useconfirm'; import { useConfirm } from 'primevue/useconfirm';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window'; import { getCurrentWindow } from '@tauri-apps/api/window';
import Onboarding from './Onboarding.vue';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { usePrfStore } from '../stores'; import { useClientStore, usePrfStore } from '../stores';
const prf = usePrfStore(); const prf = usePrfStore();
const client = useClientStore();
const confirmDialog = useConfirm(); const confirmDialog = useConfirm();
type StartStatus = 'ready' | 'preparing' | 'running'; type StartStatus = 'ready' | 'preparing' | 'running';
@ -28,7 +28,7 @@ const startline = async (force: boolean, refresh: boolean) => {
} else if ('MissingLocalPackage' in o) { } else if ('MissingLocalPackage' in o) {
return `Package missing: ${o.MissingLocalPackage}`; return `Package missing: ${o.MissingLocalPackage}`;
} else if ('MissingDependency' in o) { } else if ('MissingDependency' in o) {
return `Dependency missing: ${o.MissingDependency}`; return `Dependency missing: ${(o.MissingDependency as string[]).join(' ')}`;
} else if ('MissingTool' in o) { } else if ('MissingTool' in o) {
return `Tool missing: ${o.MissingTool}`; return `Tool missing: ${o.MissingTool}`;
} else { } else {
@ -38,6 +38,8 @@ const startline = async (force: boolean, refresh: boolean) => {
confirmDialog.require({ confirmDialog.require({
message: message.join('\n'), message: message.join('\n'),
header: 'Start check failed', header: 'Start check failed',
acceptLabel: 'Run anyway',
rejectLabel: 'Cancel',
accept: () => { accept: () => {
startline(true, refresh); startline(true, refresh);
}, },
@ -85,14 +87,20 @@ listen('launch-end', () => {
getCurrentWindow().setFocus(); getCurrentWindow().setFocus();
}); });
const messageSplit = (message: any) => { const createShortcut = async () => {
return message.message?.split('\n'); const current = prf.current;
if (current !== null) {
await invoke('create_shortcut', {
profileMeta: current.meta,
});
}
}; };
const menuItems = [ const menuItems = [
{ {
label: 'Refresh and start', label: 'Refresh and start',
icon: 'pi pi-sync', icon: 'pi pi-sync',
tooltip: 'test',
command: async () => await startline(false, true), command: async () => await startline(false, true),
}, },
{ {
@ -100,6 +108,19 @@ const menuItems = [
icon: 'pi pi-exclamation-circle', icon: 'pi pi-exclamation-circle',
command: async () => await startline(true, false), command: async () => await startline(true, false),
}, },
{
label: 'Create desktop shortcut',
icon: 'pi pi-link',
command: createShortcut,
},
{
label: 'Help',
icon: 'pi pi-question-circle',
command: () => {
onboardingFirstTime.value = false;
onboardingVisible.value = true;
},
},
]; ];
const menu = ref(); const menu = ref();
@ -107,49 +128,39 @@ const showContextMenu = (event: Event) => {
event.preventDefault(); event.preventDefault();
menu.value.show(event); menu.value.show(event);
}; };
const onboardingVisible = ref(false);
const onboardingFirstTime = ref(false);
const tryStart = () => {
const game = prf.current?.meta.game;
if (game !== undefined) {
if (client.onboarded.includes(game)) {
startline(false, false);
} else {
onboardingVisible.value = true;
onboardingFirstTime.value = true;
client.setOnboarded(game);
}
}
};
</script> </script>
<template> <template>
<Onboarding
:visible="onboardingVisible"
:first-time="onboardingFirstTime"
:on-finish="
() => {
onboardingVisible = false;
if (onboardingFirstTime === true) {
startline(false, false);
}
}
"
/>
<ContextMenu ref="menu" :model="menuItems" /> <ContextMenu ref="menu" :model="menuItems" />
<ConfirmDialog>
<template #container="{ message, acceptCallback, rejectCallback }">
<div
class="flex flex-col p-8 bg-surface-0 dark:bg-surface-900 rounded"
>
<span class="font-bold self-center text-2xl block mb-2 mt-2">{{
message.header
}}</span>
<ScrollPanel
v-if="messageSplit(message).length > 5"
style="width: 100%; height: 40vh"
>
<p v-for="m in messageSplit(message)">
{{ m }}
</p></ScrollPanel
>
<div v-else>
<p v-for="m in messageSplit(message)">
{{ m }}
</p>
</div>
<div class="flex self-center items-center gap-2 mt-6">
<Button
label="Run anyway"
@click="acceptCallback"
size="small"
class="w-32"
></Button>
<Button
label="Cancel"
outlined
size="small"
@click="rejectCallback"
class="w-32"
></Button>
</div>
</div>
</template>
</ConfirmDialog>
<Button <Button
v-if="startStatus === 'ready'" v-if="startStatus === 'ready'"
v-tooltip="disabledTooltip" v-tooltip="disabledTooltip"
@ -159,7 +170,7 @@ const showContextMenu = (event: Event) => {
aria-label="start" aria-label="start"
size="small" size="small"
class="m-2.5" class="m-2.5"
@click="startline(false, false)" @click="tryStart"
@contextmenu="showContextMenu" @contextmenu="showContextMenu"
/> />
<Button <Button

View File

@ -20,19 +20,17 @@ const install = async () => {
}); });
} catch (err) { } catch (err) {
if (props.pkg !== undefined) { if (props.pkg !== undefined) {
props.pkg.js.busy = false; props.pkg.js.downloading = false;
} }
} }
//if (rv === 'Deferred') { /* download progress */ }
}; };
</script> </script>
<template> <template>
<Button <Button
v-if="needsUpdate(pkg) && !pkg?.js.busy" v-if="needsUpdate(pkg) && !pkg?.js.downloading"
rounded rounded
icon="pi pi-sync" icon="pi pi-download"
severity="success" severity="success"
aria-label="remove" aria-label="remove"
size="small" size="small"

View File

@ -1,12 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { Ref, computed, ref } from 'vue';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import Select from 'primevue/select'; import Select from 'primevue/select';
import ToggleSwitch from 'primevue/toggleswitch'; import ToggleSwitch from 'primevue/toggleswitch';
import { listen } from '@tauri-apps/api/event';
import * as path from '@tauri-apps/api/path'; import * as path from '@tauri-apps/api/path';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs'; import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
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 { usePkgStore, usePrfStore } from '../../stores'; import { usePkgStore, usePrfStore } from '../../stores';
import { Feature } from '../../types'; import { Feature } from '../../types';
import { hasFeature, pkgKey } from '../../util'; import { hasFeature, pkgKey } from '../../util';
@ -15,6 +17,7 @@ const pkgs = usePkgStore();
const prf = usePrfStore(); const prf = usePrfStore();
const aimeCode = ref(''); const aimeCode = ref('');
const coms: Ref<{ [key: string]: number }> = ref({});
prf.reload(); prf.reload();
@ -40,20 +43,31 @@ const aimeCodePaste = (ev: ClipboardEvent) => {
.join('') ?? ''; .join('') ?? '';
}; };
(async () => { const load = async () => {
const aime_path = await path.join(await prf.configDir, 'aime.txt'); const aime_path = await path.join(await prf.configDir, 'aime.txt');
aimeCode.value = await readTextFile(aime_path).catch(() => ''); aimeCode.value = await readTextFile(aime_path).catch(() => '');
})(); };
invoke('list_com_ports').then((newComs) => {
coms.value = newComs as typeof coms.value;
});
listen('reload-aime-code', load);
load();
</script> </script>
<template> <template>
<OptionCategory title="Aime"> <OptionCategory title="Aime">
<OptionRow title="Aime emulation"> <OptionRow
title="Aime type"
tooltip="Additional Aime plugins can be downloaded from the package store."
>
<Select <Select
v-model="prf.current!.data.sgt.aime" v-model="prf.current!.data.sgt.aime"
:options="[ :options="[
{ title: 'none', value: 'Disabled' }, { title: 'hardware', value: 'Disabled' },
{ title: 'segatools built-in', value: 'BuiltIn' }, { title: 'segatools built-in emulation', value: 'BuiltIn' },
...pkgs.byFeature(Feature.Aime).map((p) => { ...pkgs.byFeature(Feature.Aime).map((p) => {
return { return {
title: pkgKey(p), title: pkgKey(p),
@ -68,11 +82,14 @@ const aimeCodePaste = (ev: ClipboardEvent) => {
option-value="value" option-value="value"
></Select> ></Select>
</OptionRow> </OptionRow>
<OptionRow title="Aime code"> <OptionRow
title="Aime code"
tooltip="Only applicable with the segatools built-in emulation or with compatible third-party packages"
v-if="prf.current!.data.sgt.aime !== 'Disabled'"
>
<InputText <InputText
class="shrink" class="shrink"
size="small" size="small"
:disabled="prf.current!.data.sgt.aime === 'Disabled'"
:maxlength="20" :maxlength="20"
placeholder="00000000000000000000" placeholder="00000000000000000000"
v-model="aimeCodeModel" v-model="aimeCodeModel"
@ -105,5 +122,27 @@ const aimeCodePaste = (ev: ClipboardEvent) => {
<ToggleSwitch v-model="prf.current!.data.sgt.amnet.physical" /> <ToggleSwitch v-model="prf.current!.data.sgt.amnet.physical" />
</OptionRow> </OptionRow>
</div> </div>
<OptionRow
title="Aime serial port"
tooltip="Ports can be checked in Devices and Printers or at googlechromelabs.github.io/serial-terminal
For AIC Pico, the AIME port should be selected."
v-if="prf.current!.data.sgt.aime === 'Disabled'"
>
<Select
v-model="prf.current!.data.sgt.aime_port"
:options="[
{ title: 'default', value: null },
...Object.entries(coms ?? {}).map(([title, value]) => {
return {
title,
value,
};
}),
]"
placeholder="default"
option-label="title"
option-value="value"
></Select>
</OptionRow>
</OptionCategory> </OptionCategory>
</template> </template>

View File

@ -63,6 +63,11 @@ const loadDisplays = () => {
}; };
loadDisplays(); loadDisplays();
const game = prf.current!.meta.game;
const isVertical = game === 'ongeki';
const adjustableRez = game === 'ongeki';
const canSkipPrimarySwitch = game === 'ongeki';
</script> </script>
<template> <template>
@ -80,7 +85,11 @@ loadDisplays();
@show="loadDisplays" @show="loadDisplays"
></Select> ></Select>
</OptionRow> </OptionRow>
<OptionRow class="number-input" title="Game resolution"> <OptionRow
class="number-input"
title="Game resolution"
v-if="adjustableRez"
>
<InputNumber <InputNumber
class="shrink" class="shrink"
size="small" size="small"
@ -118,12 +127,18 @@ loadDisplays();
> >
<SelectButton <SelectButton
v-model="prf.current!.data.display.rotation" v-model="prf.current!.data.display.rotation"
:options="[ :options="
{ title: 'Unchanged', value: 0 }, isVertical
{ title: 'Portrait', value: 90 }, ? [
{ title: 'Portrait (flipped)', value: 270 }, { title: 'Portrait', value: 90 },
]" { title: 'Portrait (flipped)', value: 270 },
:allow-empty="false" ]
: [
{ title: 'Landscape', value: 0 },
{ title: 'Landscape (flipped)', value: 180 },
]
"
:allow-empty="true"
option-label="title" option-label="title"
option-value="value" option-value="value"
:disabled="extraDisplayOptionsDisabled" :disabled="extraDisplayOptionsDisabled"
@ -135,6 +150,7 @@ loadDisplays();
title="Refresh Rate" title="Refresh Rate"
> >
<InputNumber <InputNumber
v-if="game === 'ongeki'"
class="shrink" class="shrink"
size="small" size="small"
:min="60" :min="60"
@ -143,6 +159,18 @@ loadDisplays();
v-model="prf.current!.data.display.frequency" v-model="prf.current!.data.display.frequency"
:disabled="extraDisplayOptionsDisabled" :disabled="extraDisplayOptionsDisabled"
/> />
<SelectButton
v-if="game === 'chunithm'"
v-model="prf.current!.data.display.frequency"
:options="[
{ title: '60Hz (CVT)', value: 60 },
{ title: '120Hz (SP)', value: 120 },
]"
:allow-empty="false"
option-label="title"
option-value="value"
:disabled="extraDisplayOptionsDisabled"
/>
</OptionRow> </OptionRow>
<OptionRow <OptionRow
title="Borderless fullscreen" title="Borderless fullscreen"
@ -157,5 +185,41 @@ loadDisplays();
v-model="prf.current!.data.display.borderless_fullscreen" v-model="prf.current!.data.display.borderless_fullscreen"
/> />
</OptionRow> </OptionRow>
<OptionRow
title="Skip switching primary display"
v-if="
capabilities.includes('display') &&
prf.current?.data.display.target !== 'default' &&
(prf.current!.data.display.dont_switch_primary ||
displayList.length > 2) &&
canSkipPrimarySwitch
"
dangerous-tooltip="Only enable this option if switching the primary display causes issues. The monitors must have a matching refresh rate."
>
<ToggleSwitch
:disabled="extraDisplayOptionsDisabled"
v-model="prf.current!.data.display.dont_switch_primary"
/>
</OptionRow>
<OptionRow
title="Display index"
class="number-input"
v-if="
capabilities.includes('display') &&
prf.current?.data.display.target !== 'default' &&
prf.current!.data.display.dont_switch_primary
"
>
<InputNumber
class="shrink"
size="small"
:min="game === 'chunithm' ? 0 : 1"
:max="32"
:use-grouping="false"
v-model="prf.current!.data.display.monitor_index_override"
:disabled="extraDisplayOptionsDisabled"
:allow-empty="true"
/>
</OptionRow>
</OptionCategory> </OptionCategory>
</template> </template>

View File

@ -0,0 +1,160 @@
<script setup lang="ts">
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import KeyboardKey from '../KeyboardKey.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Keyboard">
<OptionRow
title="Enable"
tooltip="Only applicable if the IO module is set to segatools built-in (keyboard) or a compatible third-party module (like mu3io.NET)"
>
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" />
</OptionRow>
<OptionRow
title="Lever mode"
v-if="prf.current!.data.keyboard!.game === 'Ongeki'"
>
<SelectButton
v-model="prf.current!.data.keyboard!.data.use_mouse"
:options="[
{ title: 'XInput', value: false },
{ title: 'Mouse', value: true },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</OptionRow>
<div
v-if="prf.current!.data.keyboard!.data.enabled"
:style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`"
>
<div
class="absolute left-1/6 top-1/10"
style="transform: translateX(-30%) translateY(-50%)"
>
<div class="flex flex-row gap-2 self-center w-full">
<KeyboardKey button="test" small tooltip="Test" />
<KeyboardKey button="svc" small tooltip="Service" />
<KeyboardKey button="coin" small tooltip="Coin" />
</div>
</div>
<div v-if="prf.current?.meta.game === 'ongeki'">
<div
class="absolute left-1/2 top-1/2"
style="transform: translateX(-540%) translateY(-200%)"
>
<KeyboardKey
button="lmenu"
small
color="rgba(255, 0, 0, 0.2)"
/>
</div>
<div
class="absolute right-1/2 top-1/2"
style="transform: translateX(540%) translateY(-200%)"
>
<KeyboardKey
button="rmenu"
small
color="rgba(255, 255, 0, 0.2)"
/>
</div>
<div
class="absolute left-1/2 top-1/2"
style="transform: translateX(-50%) translateY(-20%)"
>
<div class="flex flex-row gap-2 self-center w-full">
<KeyboardKey
button="lwad"
tall
color="rgba(180, 0, 255, 0.2)"
/>
<div style="width: 0.7em"></div>
<KeyboardKey button="l1" color="rgba(255, 0, 0, 0.2)" />
<KeyboardKey button="l2" color="rgba(0, 255, 0, 0.2)" />
<KeyboardKey button="l3" color="rgba(0, 0, 255, 0.2)" />
<div style="width: 0.7em"></div>
<KeyboardKey button="r1" color="rgba(255, 0, 0, 0.2)" />
<KeyboardKey button="r2" color="rgba(0, 255, 0, 0.2)" />
<KeyboardKey button="r3" color="rgba(0, 0, 255, 0.2)" />
<div style="width: 0.7em"></div>
<KeyboardKey
button="rwad"
tall
color="rgba(255, 0, 180, 0.2)"
/>
</div>
</div>
</div>
<div v-if="prf.current?.meta.game === 'chunithm'">
<div class="absolute left-1/2 top-1/5">
<div
class="flex flex-row flex-nowrap gap-2 self-center w-full"
>
<div
v-for="idx in Array(6)
.fill(0)
.map((_, i) => i + 1)"
>
<KeyboardKey
button="ir"
:index="idx - 1"
:tooltip="`ir${idx}`"
small
color="rgba(0, 255, 0, 0.2)"
/>
</div>
</div>
</div>
<div
class="absolute left-1/2 top-1/2"
style="transform: translateX(-50%) translateY(-5%)"
>
<div
class="flex flex-row flex-nowrap gap-2 self-center w-full"
>
<div
v-for="idx in Array(16)
.fill(0)
.map((_, i) => 32 - 2 * i - 1)"
>
<KeyboardKey
button="cell"
:index="idx - 1"
:tooltip="`cell${idx}`"
small
color="rgba(255, 255, 0, 0.2)"
/>
</div>
</div>
<div style="height: 0.6em"></div>
<div
class="flex flex-row flex-nowrap gap-2 self-center w-full"
>
<div
v-for="idx in Array(16)
.fill(0)
.map((_, i) => 32 - 2 * i)"
>
<KeyboardKey
button="cell"
:index="idx - 1"
:tooltip="`cell${idx}`"
small
color="rgba(255, 255, 0, 0.2)"
/>
</div>
</div>
</div>
</div>
</div>
</OptionCategory>
</template>

View File

@ -13,7 +13,11 @@ const prf = usePrfStore();
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen"> <OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
<ToggleSwitch v-model="prf.current!.data.sgt.intel" /> <ToggleSwitch v-model="prf.current!.data.sgt.intel" />
</OptionRow> </OptionRow>
<OptionRow title="More segatools options"> <OptionRow
title="More segatools options"
tooltip="Advanced options not covered by STARTLINER"
>
<!-- <Button icon="pi pi-refresh" size="small" /> -->
<FileEditor filename="segatools-base.ini" /> <FileEditor filename="segatools-base.ini" />
</OptionRow> </OptionRow>
</OptionCategory> </OptionCategory>

View File

@ -1,15 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import Select from 'primevue/select'; import Select from 'primevue/select';
import { useConfirm } from 'primevue/useconfirm';
import { emit } from '@tauri-apps/api/event';
import * as path from '@tauri-apps/api/path';
import FilePicker from '../FilePicker.vue'; import FilePicker from '../FilePicker.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 { usePkgStore, usePrfStore } from '../../stores'; import { usePkgStore, usePrfStore } from '../../stores';
import { Feature } from '../../types'; import { Feature } from '../../types';
import { pkgKey } from '../../util'; import { pkgKey } from '../../util';
const prf = usePrfStore(); const prf = usePrfStore();
const pkgs = usePkgStore(); const pkgs = usePkgStore();
const confirmDialog = useConfirm();
const names = computed(() => { const names = computed(() => {
switch (prf.current?.meta.game) { switch (prf.current?.meta.game) {
@ -31,18 +36,39 @@ const names = computed(() => {
throw new Error('Option tab without a profile'); throw new Error('Option tab without a profile');
} }
}); });
const checkSegatoolsIni = async (target: string) => {
const iniPath = await path.join(target, '../segatools.ini');
if (await invoke('file_exists', { path: iniPath })) {
confirmDialog.require({
message: 'Would you like to load the existing configuration data?',
header: 'segatools.ini found',
accept: async () => {
await invoke('load_segatools_ini', { path: iniPath });
await prf.reload();
await emit('reload-aime-code');
},
});
}
};
</script> </script>
<template> <template>
<OptionCategory title="General"> <OptionCategory title="General">
<OptionRow :title="names.exe"> <OptionRow
:title="names.exe"
tooltip="STARTLINER expects unpacked executables put into otherwise clean data."
>
<FilePicker <FilePicker
:directory="false" :directory="false"
:promptname="names.exe" :promptname="names.exe"
extension="exe" extension="exe"
:value="prf.current!.data.sgt.target" :value="prf.current!.data.sgt.target"
:callback=" :callback="
(value: string) => (prf.current!.data.sgt.target = value) (value: string) => (
(prf.current!.data.sgt.target = value),
checkSegatoolsIni(value)
)
" "
></FilePicker> ></FilePicker>
</OptionRow> </OptionRow>
@ -76,7 +102,10 @@ const names = computed(() => {
" "
></FilePicker> ></FilePicker>
</OptionRow> </OptionRow>
<OptionRow :title="names.hook"> <OptionRow
:title="names.hook"
tooltip="Hooks can be downloaded from the package store."
>
<Select <Select
v-model="prf.current!.data.sgt.hook" v-model="prf.current!.data.sgt.hook"
:options=" :options="
@ -90,19 +119,35 @@ const names = computed(() => {
return { title: pkgKey(p), value: pkgKey(p) }; return { title: pkgKey(p), value: pkgKey(p) };
}) })
" "
placeholder="none"
option-label="title" option-label="title"
option-value="value" option-value="value"
></Select> ></Select>
</OptionRow> </OptionRow>
<OptionRow :title="names.io" v-if="prf.current?.meta.game === 'ongeki'"> <OptionRow
:title="names.io"
tooltip="IO plugins can be downloaded from the package store."
>
<Select <Select
v-model="prf.current!.data.sgt.io" v-model="prf.current!.data.sgt.io2"
placeholder="segatools built-in"
:options="[ :options="[
{ title: 'segatools built-in', value: null }, { title: 'native io4', value: 'hardware' },
...pkgs.byFeature(Feature.Mu3IO).map((p) => { {
return { title: pkgKey(p), value: pkgKey(p) }; title: 'segatools built-in (keyboard)',
}), value: 'segatools_built_in',
},
...pkgs
.byFeature(
prf.current?.meta.game === 'ongeki'
? Feature.Mu3IO
: Feature.ChuniIO
)
.map((p) => {
return {
title: pkgKey(p),
value: { custom: pkgKey(p) },
};
}),
]" ]"
option-label="title" option-label="title"
option-value="value" option-value="value"

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import { computed } from 'vue';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { useClientStore } from '../../stores';
const client = useClientStore();
const offlineModel = computed({
get() {
return client.offlineMode;
},
async set(value: boolean) {
await client.setOfflineMode(value);
},
});
const updatesModel = computed({
get() {
return client.enableAutoupdates;
},
async set(value: boolean) {
await client.setAutoupdates(value);
},
});
const verboseModel = computed({
get() {
return client.verbose;
},
async set(value: boolean) {
await client.setVerbose(value);
},
});
const themeModel = computed({
get() {
return client.theme;
},
async set(value: 'light' | 'dark' | 'system') {
await client.setTheme(value);
},
});
</script>
<template>
<OptionCategory title="STARTLINER">
<OptionRow title="UI scaling">
<SelectButton
v-model="client.scaleModel"
:options="[
{ title: 'S', value: 's' },
{ title: 'M', value: 'm' },
{ title: 'L', value: 'l' },
{ title: 'XL', value: 'xl' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</OptionRow>
<OptionRow
title="Offline mode"
tooltip="Disables the package store. Applies after a restart."
>
<ToggleSwitch v-model="offlineModel" />
</OptionRow>
<OptionRow title="Enable automatic updates">
<ToggleSwitch v-model="updatesModel" />
</OptionRow>
<OptionRow
title="Enable detailed logs"
tooltip="Applies after a restart."
>
<ToggleSwitch v-model="verboseModel" />
</OptionRow>
<OptionRow title="Theme">
<SelectButton
v-model="themeModel"
:options="[
{ title: 'System', value: 'system' },
{ title: 'Light', value: 'light' },
{ title: 'Dark', value: 'dark' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</OptionRow>
</OptionCategory>
</template>

View File

@ -3,7 +3,7 @@ import {
InvokeOptions, InvokeOptions,
invoke as real_invoke, invoke as real_invoke,
} from '@tauri-apps/api/core'; } from '@tauri-apps/api/core';
import { message } from '@tauri-apps/plugin-dialog'; import { emit } from '@tauri-apps/api/event';
export const invoke = async <T>( export const invoke = async <T>(
cmd: string, cmd: string,
@ -14,9 +14,9 @@ export const invoke = async <T>(
return await real_invoke(cmd, args, options); return await real_invoke(cmd, args, options);
} catch (e: unknown) { } catch (e: unknown) {
if (typeof e === 'string') { if (typeof e === 'string') {
await message(`${cmd}: ${e}`, { emit('invoke-error', {
title: `Error`, message: e,
kind: 'error', header: `${cmd} failed`,
}); });
} else { } else {
console.error(`Unresolved error: ${e}`); console.error(`Unresolved error: ${e}`);

119
src/keyboard.ts Normal file
View File

@ -0,0 +1,119 @@
const KEY_MAP: { [key: number]: string } = {
1: 'M1',
2: 'M2',
4: 'M3',
5: 'M4',
6: 'M5',
8: 'Backspace',
9: 'Tab',
12: 'Clear',
13: 'Enter',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: 'Space',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
45: 'Insert',
46: 'Delete',
48: 'Digit0',
49: 'Digit1',
50: 'Digit2',
51: 'Digit3',
52: 'Digit4',
53: 'Digit5',
54: 'Digit6',
55: 'Digit7',
56: 'Digit8',
57: 'Digit9',
65: 'KeyA',
66: 'KeyB',
67: 'KeyC',
68: 'KeyD',
69: 'KeyE',
70: 'KeyF',
71: 'KeyG',
72: 'KeyH',
73: 'KeyI',
74: 'KeyJ',
75: 'KeyK',
76: 'KeyL',
77: 'KeyM',
78: 'KeyN',
79: 'KeyO',
80: 'KeyP',
81: 'KeyQ',
82: 'KeyR',
83: 'KeyS',
84: 'KeyT',
85: 'KeyU',
86: 'KeyV',
87: 'KeyW',
88: 'KeyX',
89: 'KeyY',
90: 'KeyZ',
91: 'MetaLeft',
92: 'MetaRight',
93: 'ContextMenu',
96: 'Numpad0',
97: 'Numpad1',
98: 'Numpad2',
99: 'Numpad3',
100: 'Numpad4',
101: 'Numpad5',
102: 'Numpad6',
103: 'Numpad7',
104: 'Numpad8',
105: 'Numpad9',
106: 'NumpadMultiply',
107: 'NumpadAdd',
109: 'NumpadSubtract',
110: 'NumpadDecimal',
111: 'NumpadDivide',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
160: 'ShiftLeft',
161: 'ShiftRight',
162: 'ControlLeft',
163: 'ControlRight',
164: 'AltLeft',
165: 'AltRight',
186: 'Semicolon',
187: 'Equal',
188: 'Comma',
189: 'Minus',
190: 'Period',
191: 'Slash',
192: 'Backquote',
219: 'BracketLeft',
220: 'Backslash',
221: 'BracketRight',
222: 'Quote',
};
export const fromKeycode = (keyCode: number): string | null => {
return KEY_MAP[keyCode] ?? null;
};
export const toKeycode = (key: string): number | null => {
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
return res ? parseInt(res) : null;
};

View File

@ -17,6 +17,9 @@ app.use(pinia);
app.use(PrimeVue, { app.use(PrimeVue, {
theme: { theme: {
preset: Preset, preset: Preset,
options: {
darkModeSelector: '.use-dark-mode',
},
}, },
}); });
app.use(ConfirmationService); app.use(ConfirmationService);

View File

@ -2,9 +2,16 @@ import { Ref, computed, ref, watchEffect } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import * as path from '@tauri-apps/api/path'; import * as path from '@tauri-apps/api/path';
import { PhysicalSize, getCurrentWindow } from '@tauri-apps/api/window';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
import { invoke, invoke_nopopup } from './invoke'; import { invoke, invoke_nopopup } from './invoke';
import { Dirs, Feature, Game, Package, Profile, ProfileMeta } from './types'; import { Dirs, Feature, Game, Package, Profile, ProfileMeta } from './types';
import { changePrimaryColor, hasFeature, pkgKey } from './util'; import {
changePrimaryColor,
hasFeature,
pkgKey,
shouldPreferDark,
} from './util';
type InstallStatus = { type InstallStatus = {
pkg: string; pkg: string;
@ -102,19 +109,23 @@ export const usePkgStore = defineStore('pkg', {
), ),
byFeature: (state) => (feature: Feature) => byFeature: (state) => (feature: Feature) =>
Object.values(state.pkg).filter((p) => hasFeature(p, feature)), Object.values(state.pkg).filter((p) => hasFeature(p, feature)),
hasAvailableUpdates: (state) =>
Object.values(state.pkg).some(
(p) => p.loc && (p.rmt?.version ?? 0) > p.loc.version
),
}, },
actions: { actions: {
setupListeners() { setupListeners() {
listen<InstallStatus>('install-start', async (ev) => { listen<InstallStatus>('install-start', async (ev) => {
const key = ev.payload.pkg; const key = ev.payload.pkg;
await this.reload(key); await this.reload(key);
this.pkg[key].js.busy = true; this.pkg[key].js.downloading = true;
}); });
listen<InstallStatus>('install-end', async (ev) => { listen<InstallStatus>('install-end', async (ev) => {
const key = ev.payload.pkg; const key = ev.payload.pkg;
await this.reload(key); await this.reload(key);
this.pkg[key].js.busy = false; this.pkg[key].js.downloading = false;
}); });
}, },
@ -141,17 +152,22 @@ export const usePkgStore = defineStore('pkg', {
async reloadWith(key: string, pkg: Package) { async reloadWith(key: string, pkg: Package) {
if (this.pkg[key] === undefined) { if (this.pkg[key] === undefined) {
this.pkg[key] = { js: { busy: false } } as Package; this.pkg[key] = { js: { downloading: false } } as Package;
} else { } else {
this.pkg[key].loc = null; this.pkg[key].loc = null;
this.pkg[key].rmt = null; this.pkg[key].rmt = null;
} }
Object.assign(this.pkg[key], pkg); Object.assign(this.pkg[key], pkg);
if (!pkg.js) {
pkg.js = { downloading: false };
}
if (pkg.rmt !== null) { if (pkg.rmt !== null) {
pkg.rmt.categories.forEach((c) => pkg.rmt.categories.forEach((c) =>
this.availableCategories.add(c) this.availableCategories.add(c)
); );
pkg.js.downloading = false;
} }
}, },
@ -170,6 +186,44 @@ export const usePkgStore = defineStore('pkg', {
} }
await this.reloadAll(); await this.reloadAll();
}, },
async install(pkg: Package | undefined) {
if (pkg === undefined) {
return;
}
try {
await invoke('install_package', {
key: pkgKey(pkg),
force: true,
});
} catch (err) {
if (pkg !== undefined) {
pkg.js.downloading = false;
}
}
},
async installFromKey(key: string) {
try {
await invoke('install_package', {
key,
force: true,
});
} catch (err) {
console.error(err);
}
},
async updateAll() {
const list = [];
for (const pkg of this.allLocal) {
if (pkg.rmt && pkg.rmt.version > pkg.loc!.version) {
list.push(this.install(pkg));
}
}
await Promise.all(list);
},
}, },
}); });
@ -186,6 +240,12 @@ export const usePrfStore = defineStore('prf', () => {
current.value?.data.mods.includes(pkgKey(pkg)) current.value?.data.mods.includes(pkgKey(pkg))
); );
const isPkgKeyEnabled = (pkg: string) =>
computed(
() =>
current.value !== null && current.value?.data.mods.includes(pkg)
);
const reload = async () => { const reload = async () => {
const p = (await invoke('get_current_profile')) as Profile; const p = (await invoke('get_current_profile')) as Profile;
current.value = p; current.value = p;
@ -278,7 +338,7 @@ export const usePrfStore = defineStore('prf', () => {
if (timeout !== null) { if (timeout !== null) {
clearTimeout(timeout); clearTimeout(timeout);
} }
timeout = setTimeout(() => invoke('save_current_profile'), 2000); timeout = setTimeout(() => invoke('save_current_profile'), 600);
} }
}); });
@ -286,6 +346,7 @@ export const usePrfStore = defineStore('prf', () => {
current, current,
list, list,
isPkgEnabled, isPkgEnabled,
isPkgKeyEnabled,
reload, reload,
create, create,
rename, rename,
@ -295,3 +356,193 @@ export const usePrfStore = defineStore('prf', () => {
configDir, configDir,
}; };
}); });
export enum ClientData {
Onboarded,
}
export const useClientStore = defineStore('client', () => {
type ScaleType = 's' | 'm' | 'l' | 'xl';
const scaleFactor: Ref<ScaleType> = ref('s');
const timeout: Ref<NodeJS.Timeout | null> = ref(null);
const offlineMode = ref(false);
const enableAutoupdates = ref(true);
const verbose = ref(false);
const theme: Ref<'light' | 'dark' | 'system'> = ref('system');
const onboarded: Ref<Game[]> = ref([]);
const _scaleValue = (value: ScaleType) =>
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
const scaleValue = computed(() => {
return _scaleValue(scaleFactor.value);
});
const setScaleFactor = async (value: ScaleType) => {
scaleFactor.value = value;
const window = getCurrentWindow();
const w = Math.floor(_scaleValue(value) * 900);
const h = Math.floor(_scaleValue(value) * 600);
let size = await window.innerSize();
window.setMinSize(new PhysicalSize(w, h));
window.setSize(
new PhysicalSize(Math.max(w, size.width), Math.max(h, size.height))
);
};
const scaleModel = computed({
get() {
return scaleFactor;
},
async set(value: ScaleType) {
await setScaleFactor(value);
await save();
},
});
const load = async () => {
const generalStore = useGeneralStore();
try {
const input = JSON.parse(
await readTextFile(
await path.join(
generalStore.configDir,
'client-options.json'
)
)
);
if (input.windowSize) {
getCurrentWindow().setSize(
new PhysicalSize(input.windowSize.w, input.windowSize.h)
);
}
if (input.scaleFactor) {
await setScaleFactor(input.scaleFactor);
}
if (input.theme) {
theme.value = input.theme;
}
if (input.onboarded) {
onboarded.value = input.onboarded;
}
await setTheme(theme.value);
} catch (e) {
console.error(`Error reading client options: ${e}`);
}
offlineMode.value = await invoke('get_global_config', {
field: 'offline_mode',
});
enableAutoupdates.value = await invoke('get_global_config', {
field: 'enable_autoupdates',
});
verbose.value = await invoke('get_global_config', {
field: 'verbose',
});
};
const save = async () => {
const generalStore = useGeneralStore();
const w = getCurrentWindow();
const size = await w.innerSize();
await writeTextFile(
await path.join(generalStore.configDir, 'client-options.json'),
JSON.stringify({
scaleFactor: scaleFactor.value,
windowSize: {
w: Math.floor(size.width),
h: Math.floor(size.height),
},
theme: theme.value,
onboarded: onboarded.value,
})
);
};
const queueSave = async () => {
if (timeout.value !== null) {
clearTimeout(timeout.value);
}
timeout.value = setTimeout(async () => {
timeout.value = null;
await save();
}, 1000);
};
const setOfflineMode = async (value: boolean) => {
offlineMode.value = value;
await invoke('set_global_config', { field: 'offline_mode', value });
};
const setAutoupdates = async (value: boolean) => {
enableAutoupdates.value = value;
await invoke('set_global_config', {
field: 'enable_autoupdates',
value,
});
};
const setVerbose = async (value: boolean) => {
verbose.value = value;
await invoke('set_global_config', { field: 'verbose', value });
};
const setTheme = async (value: 'light' | 'dark' | 'system') => {
if (value === 'dark') {
document.documentElement.classList.add('use-dark-mode');
} else if (value === 'light') {
document.documentElement.classList.remove('use-dark-mode');
} else {
document.documentElement.classList.toggle(
'use-dark-mode',
shouldPreferDark()
);
}
theme.value = value;
await save();
};
const setOnboarded = async (game: Game) => {
onboarded.value = [...onboarded.value, game];
await save();
};
getCurrentWindow().onResized(async ({ payload }) => {
// For whatever reason this is 0 when minimized
if (payload.width > 0) {
await queueSave();
}
});
return {
scaleFactor,
offlineMode,
enableAutoupdates,
verbose,
theme,
onboarded,
timeout,
scaleModel,
_scaleValue,
scaleValue,
load,
save,
queueSave,
setOfflineMode,
setAutoupdates,
setVerbose,
setTheme,
setOnboarded,
};
});

View File

@ -19,7 +19,7 @@ export interface Package {
icon: string; icon: string;
} | null; } | null;
js: { js: {
busy: boolean; downloading: boolean;
}; };
} }
@ -30,13 +30,17 @@ export enum Feature {
Mu3Hook = 1 << 3, Mu3Hook = 1 << 3,
Mu3IO = 1 << 4, Mu3IO = 1 << 4,
ChusanHook = 1 << 5, ChusanHook = 1 << 5,
ChuniIO = 1 << 6,
Mempatcher = 1 << 7,
GameDLL = 1 << 8,
AmdDLL = 1 << 9,
} }
export type Status = export type Status =
| 'Unchecked' | 'Unchecked'
| 'Unsupported' | 'Unsupported'
| { | {
OK: Feature; OK: [Feature, String, String];
}; };
export type Game = 'ongeki' | 'chunithm'; export type Game = 'ongeki' | 'chunithm';
@ -53,12 +57,16 @@ export interface ProfileData {
network: NetworkConfig; network: NetworkConfig;
bepinex: BepInExConfig; bepinex: BepInExConfig;
mu3_ini: Mu3IniConfig | undefined; mu3_ini: Mu3IniConfig | undefined;
keyboard: KeyboardConfig | undefined;
patches: {
[key: string]: 'enabled' | { number: number } | { hex: Int8Array };
};
} }
export interface SegatoolsConfig { export interface SegatoolsConfig {
target: string; target: string;
hook: string | null; hook: string | null;
io: string | null; io2: 'segatools_built_in' | 'hardware' | { custom: string };
amfs: string; amfs: string;
option: string; option: string;
appdata: string; appdata: string;
@ -69,15 +77,18 @@ export interface SegatoolsConfig {
addr: string; addr: string;
physical: boolean; physical: boolean;
}; };
aime_port: number;
} }
export interface DisplayConfig { export interface DisplayConfig {
target: String; target: String;
rez: [number, number]; rez: [number, number];
mode: 'Window' | 'Borderless' | 'Fullscreen'; mode: 'Window' | 'Borderless' | 'Fullscreen';
rotation: number; rotation: number | null;
frequency: number; frequency: number;
borderless_fullscreen: boolean; borderless_fullscreen: boolean;
dont_switch_primary: boolean;
monitor_index_override: number | null;
} }
export interface NetworkConfig { export interface NetworkConfig {
@ -96,9 +107,49 @@ export interface BepInExConfig {
export interface Mu3IniConfig { export interface Mu3IniConfig {
audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch'; audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch';
// blacklist?: [number, number]; sample_rate: number;
blacklist?: [number, number];
gp: number;
enable_bonus_tracks: boolean;
} }
export interface OngekiButtons {
use_mouse: boolean;
enabled: boolean;
coin: number;
svc: number;
test: number;
lmenu: number;
rmenu: number;
l1: number;
l2: number;
l3: number;
r1: number;
r2: number;
r3: number;
lwad: number;
rwad: number;
}
export interface ChunithmButtons {
enabled: boolean;
coin: number;
svc: number;
test: number;
cell: number[];
ir: number[];
}
export type KeyboardConfig =
| {
game: 'Ongeki';
data: OngekiButtons;
}
| {
game: 'Chunithm';
data: ChunithmButtons;
};
export interface Profile { export interface Profile {
meta: ProfileMeta; meta: ProfileMeta;
data: ProfileData; data: ProfileData;
@ -111,3 +162,13 @@ export interface Dirs {
data_dir: string; data_dir: string;
cache_dir: string; cache_dir: string;
} }
export interface Patch {
id: string;
name: string;
tooltip: string;
type: undefined | 'number';
default: number;
min: number;
max: number;
}

View File

@ -52,6 +52,23 @@ export const hasFeature = (pkg: Package | undefined, feature: Feature) => {
pkg.loc !== null && pkg.loc !== null &&
pkg.loc !== undefined && pkg.loc !== undefined &&
typeof pkg.loc?.status !== 'string' && typeof pkg.loc?.status !== 'string' &&
pkg.loc.status.OK & feature pkg.loc.status.OK[0] & feature
); );
}; };
export const messageSplit = (message: any) => {
return message.message?.split('\n');
};
export const shouldPreferDark = () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
export const prettyPrint = (game: Game) => {
switch (game) {
case 'ongeki':
return 'O.N.G.E.K.I.';
case 'chunithm':
return 'CHUNITHM';
}
};

View File

@ -1,6 +1,6 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
// @ts-expect-error process is a nodejs global // @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST; const host = process.env.TAURI_DEV_HOST;
@ -30,4 +30,7 @@ export default defineConfig(async () => ({
ignored: ['**/rust/**'], ignored: ['**/rust/**'],
}, },
}, },
build: {
chunkSizeWarningLimit: 1024,
},
})); }));