Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
890d26e883 | |||
2aff5834b9 | |||
69f2c83109 | |||
dbbd80c6c3 | |||
3479804dca | |||
aaeed669df | |||
7084f40404 | |||
f7e9d7d7db | |||
e87b661f08 | |||
5d2d407659 | |||
795e889bd0 | |||
7071f19877 | |||
a72ec25088 | |||
5893536daa | |||
e9550e8eee | |||
658a69a1e2 | |||
f3ee0d0068 | |||
43f885cffc | |||
d0ce3cddc7 | |||
9cbdf2a9c8 | |||
54a6476010 | |||
e4dc0b1f55 | |||
e6c21ef04a | |||
d3145bfc4e | |||
c7ddeb53e6 | |||
b82fcc942f | |||
ac18c34895 | |||
f588892b05 | |||
37df371006 | |||
7ea31c7a87 | |||
073ff8cfb8 | |||
d93118683d | |||
7f68b8d28b | |||
4247e19996 | |||
6270fce05f | |||
7db36b7bc0 | |||
f3016eb029 | |||
28269c5d75 | |||
1a68eda8c1 | |||
b9a40d44a8 | |||
b10c797d52 | |||
ff0a37dfdc | |||
d63d81e349 | |||
9ea66dbeab | |||
4e795257ad |
108
CHANGELOG.md
Normal file
@ -0,0 +1,108 @@
|
||||
## 0.12.1
|
||||
|
||||
- Chunithm: fixed crash when using mempatcher
|
||||
|
||||
## 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
|
63
README.md
@ -1,59 +1,44 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
|
||||

|
||||

|
||||
|
||||
## 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
|
||||
bun install
|
||||
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 may 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:
|
||||
|
||||
```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`.
|
||||
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.
|
||||
|
||||
## Package format
|
||||
|
||||
- [Package format requirements](https://rainy.patafour.zip/package/create/docs/)
|
||||
- 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.
|
||||
Refer to [the wiki](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Package-format).
|
||||
|
||||
## 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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
5
TODO.md
@ -1,11 +1,8 @@
|
||||
### Short-term
|
||||
|
||||
- CHUNITHM support
|
||||
- https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63
|
||||
|
||||
### Long-term
|
||||
|
||||
- Auto-updates
|
||||
- Progress bars and other GUI sugar
|
||||
- IO DLLs and artemis as special packages
|
||||
- artemis as a special package
|
||||
- Other arcade games (if there is demand)
|
||||
|
75
bun.lock
@ -4,10 +4,11 @@
|
||||
"": {
|
||||
"name": "startliner",
|
||||
"dependencies": {
|
||||
"@f3ve/vue-markdown-it": "^0.2.3",
|
||||
"@mdi/font": "7.4.47",
|
||||
"@primevue/forms": "^4.3.3",
|
||||
"@primevue/themes": "^4.3.3",
|
||||
"@tailwindcss/vite": "^4.1.2",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@tauri-apps/api": "^2.4.1",
|
||||
"@tauri-apps/plugin-cli": "^2.2.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2.2.1",
|
||||
@ -17,29 +18,31 @@
|
||||
"@tauri-apps/plugin-shell": "~2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.0",
|
||||
"@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",
|
||||
"primevue": "^4.3.3",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"tailwindcss": "^4.1.2",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"tailwindcss-primeui": "^0.4.0",
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "^3.8.0",
|
||||
"vuetify": "^3.8.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.4.1",
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/node": "^22.14.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"sass": "1.77.8",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-fonts": "^1.3.1",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"vite": "^6.2.5",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-vuetify": "^2.1.1",
|
||||
"vue-tsc": "^2.2.8",
|
||||
},
|
||||
@ -132,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=="],
|
||||
|
||||
"@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/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
@ -216,33 +221,33 @@
|
||||
|
||||
"@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.1.2", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.2" } }, "sha512-ZwFnxH+1z8Ehh8bNTMX3YFrYdzAv7JLY5X5X7XSFY+G9QGJVce/P9xb2mh+j5hKt8NceuHmdtllJvAHWKtsNrQ=="],
|
||||
"@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.1.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.2", "@tailwindcss/oxide-darwin-arm64": "4.1.2", "@tailwindcss/oxide-darwin-x64": "4.1.2", "@tailwindcss/oxide-freebsd-x64": "4.1.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.2", "@tailwindcss/oxide-linux-arm64-musl": "4.1.2", "@tailwindcss/oxide-linux-x64-gnu": "4.1.2", "@tailwindcss/oxide-linux-x64-musl": "4.1.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.2", "@tailwindcss/oxide-win32-x64-msvc": "4.1.2" } }, "sha512-Zwz//1QKo6+KqnCKMT7lA4bspGfwEgcPAHlSthmahtgrpKDfwRGk8PKQrW8Zg/ofCDIlg6EtjSTKSxxSufC+CQ=="],
|
||||
"@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.1.2", "", { "os": "android", "cpu": "arm64" }, "sha512-IxkXbntHX8lwGmwURUj4xTr6nezHhLYqeiJeqa179eihGv99pRlKV1W69WByPJDQgSf4qfmwx904H6MkQqTA8w=="],
|
||||
"@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.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZRtiHSnFYHb4jHKIdzxlFm6EDfijTCOT4qwUhJ3GWxfDoW2yT3z/y8xg0nE7e72unsmSj6dtfZ9Y5r75FIrlpA=="],
|
||||
"@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.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BiKUNZf1A0pBNzndBvnPnBxonCY49mgbOsPfILhcCE5RM7pQlRoOgN7QnwNhY284bDbfQSEOWnFR0zbPo6IDTw=="],
|
||||
"@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.1.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Z30VcpUfRGkiddj4l5NRCpzbSGjhmmklVoqkVQdkEC0MOelpY+fJrVhzSaXHmWrmSvnX8yiaEqAbdDScjVujYQ=="],
|
||||
"@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.1.2", "", { "os": "linux", "cpu": "arm" }, "sha512-w3wsK1ChOLeQ3gFOiwabtWU5e8fY3P1Ss8jR3IFIn/V0va3ir//hZ8AwURveS4oK1Pu6b8i+yxesT4qWnLVUow=="],
|
||||
"@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.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oY/u+xJHpndTj7B5XwtmXGk8mQ1KALMfhjWMMpE8pdVAznjJsF5KkCceJ4Fmn5lS1nHMCwZum5M3/KzdmwDMdw=="],
|
||||
"@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.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-k7G6vcRK/D+JOWqnKzKN/yQq1q4dCkI49fMoLcfs2pVcaUAXEqCP9NmA8Jv+XahBv5DtDjSAY3HJbjosEdKczg=="],
|
||||
"@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.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-fLL+c678TkYKgkDLLNxSjPPK/SzTec7q/E5pTwvpTqrth867dftV4ezRyhPM5PaiCqX651Y8Yk0wRQMcWUGnmQ=="],
|
||||
"@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.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0tU1Vjd1WucZ2ooq6y4nI9xyTSaH2g338bhrqk+2yzkMHskBm+pMsOCfY7nEIvALkA1PKPOycR4YVdlV7Czo+A=="],
|
||||
"@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.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-r8QaMo3QKiHqUcn+vXYCypCEha+R0sfYxmaZSgZshx9NfkY+CHz91aS2xwNV/E4dmUDkTPUag7sSdiCHPzFVTg=="],
|
||||
"@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.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-lYCdkPxh9JRHXoBsPE8Pu/mppUsC2xihYArNAESub41PKhHTnvn6++5RpmFM+GLSt3ewyS8fwCVvht7ulWm6cw=="],
|
||||
"@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.1.2", "", { "dependencies": { "@tailwindcss/node": "4.1.2", "@tailwindcss/oxide": "4.1.2", "tailwindcss": "4.1.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-3r/ZdMW0gxY8uOx1To0lpYa4coq4CzINcCX4laM1rS340Kcn0ac4A/MMFfHN8qba51aorZMYwMcOxYk4wJ9FYg=="],
|
||||
"@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.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
|
||||
|
||||
@ -292,7 +297,13 @@
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="],
|
||||
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
|
||||
|
||||
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
|
||||
|
||||
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
|
||||
|
||||
"@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@ -540,6 +551,8 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
@ -550,6 +563,10 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
@ -602,7 +619,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@ -620,6 +637,8 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
@ -706,7 +725,7 @@
|
||||
|
||||
"sync-message-port": ["sync-message-port@1.1.3", "", {}, "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.2", "", {}, "sha512-VCsK+fitIbQF7JlxXaibFhxrPq4E2hDcG8apzHUdWFMCQWD8uLdlHg4iSkZ53cgLCCcZ+FZK7vG8VjvLcnBgKw=="],
|
||||
"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=="],
|
||||
|
||||
@ -722,10 +741,12 @@
|
||||
|
||||
"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.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=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
@ -744,7 +765,7 @@
|
||||
|
||||
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
|
||||
|
||||
"vite": ["vite@6.2.5", "", { "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-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA=="],
|
||||
"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.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=="],
|
||||
|
||||
@ -756,7 +777,7 @@
|
||||
|
||||
"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.8.0", "", { "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-ROC0Xq2G/25ZyUpQMhaynMyXZBJY1WbOGlqOB810yubp8hfY8RlrOw+mzXJonOq6jylCY32muQ9xiJF1JPTLVA=="],
|
||||
"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=="],
|
||||
|
||||
|
17
package.json
@ -10,10 +10,11 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@f3ve/vue-markdown-it": "^0.2.3",
|
||||
"@mdi/font": "7.4.47",
|
||||
"@primevue/forms": "^4.3.3",
|
||||
"@primevue/themes": "^4.3.3",
|
||||
"@tailwindcss/vite": "^4.1.2",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@tauri-apps/api": "^2.4.1",
|
||||
"@tauri-apps/plugin-cli": "^2.2.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2.2.1",
|
||||
@ -23,29 +24,31 @@
|
||||
"@tauri-apps/plugin-shell": "~2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.0",
|
||||
"@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",
|
||||
"primevue": "^4.3.3",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"tailwindcss": "^4.1.2",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"tailwindcss-primeui": "^0.4.0",
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "^3.8.0"
|
||||
"vuetify": "^3.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.4.1",
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/node": "^22.14.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"sass": "1.77.8",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-fonts": "^1.3.1",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"vite": "^6.2.5",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-vuetify": "^2.1.1",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
|
3
public/help-chunithm-server.md
Normal 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>
|
BIN
public/help-chunithm-server.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
public/help-finale-chunithm.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/help-finale-ongeki.png
Normal file
After Width: | Height: | Size: 12 KiB |
8
public/help-finale.md
Normal 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
|
3
public/help-ongeki-lever.md
Normal 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>).
|
BIN
public/help-ongeki-lever.png
Normal file
After Width: | Height: | Size: 90 KiB |
3
public/help-ongeki-system-processing.md
Normal 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.
|
BIN
public/help-ongeki-system-processing.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
public/help-standard-chunithm.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
public/help-standard-ongeki.png
Normal file
After Width: | Height: | Size: 129 KiB |
7
public/help-standard.md
Normal 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%.
|
BIN
res/cfg.png
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 43 KiB |
BIN
res/cfg2.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
res/icon-chunithm.ico
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
res/icon-ongeki.ico
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
res/list.png
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 70 KiB |
BIN
res/store.png
Normal file
After Width: | Height: | Size: 63 KiB |
1161
rust/Cargo.lock
generated
@ -28,7 +28,6 @@ futures = "0.3.31"
|
||||
tauri-plugin-shell = "2"
|
||||
directories = "6.0.0"
|
||||
rust-ini = "0.21.1"
|
||||
simple_logger = "5.0.0"
|
||||
log = "0.4.25"
|
||||
regex = "1.11.1"
|
||||
zip = "2.2.2"
|
||||
@ -42,6 +41,11 @@ junction = "1.2.0"
|
||||
tauri-plugin-fs = "2"
|
||||
yaml-rust2 = "0.10.0"
|
||||
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]
|
||||
tauri-plugin-cli = "2"
|
||||
@ -49,5 +53,5 @@ tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
[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"
|
||||
|
@ -23,6 +23,7 @@
|
||||
"fs:allow-data-read-recursive",
|
||||
"fs:allow-data-write-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
After Width: | Height: | Size: 40 KiB |
BIN
rust/icons/icon.png
Normal file
After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 23 KiB |
@ -1,11 +1,15 @@
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
use crate::model::config::GlobalConfig;
|
||||
use crate::model::patch::{PatchFileVec, PatchList};
|
||||
use crate::pkg::{Feature, Status};
|
||||
use crate::profiles::Profile;
|
||||
use crate::profiles::types::Profile;
|
||||
use crate::{model::misc::Game, pkg::PkgKey};
|
||||
use crate::pkg_store::PackageStore;
|
||||
use crate::util;
|
||||
use anyhow::{anyhow, Result};
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub struct GlobalState {
|
||||
@ -17,6 +21,7 @@ pub struct AppData {
|
||||
pub pkgs: PackageStore,
|
||||
pub cfg: GlobalConfig,
|
||||
pub state: GlobalState,
|
||||
pub patch_vec: PatchFileVec,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||
@ -32,18 +37,25 @@ impl AppData {
|
||||
.and_then(|s| Ok(serde_json::from_str::<GlobalConfig>(&s)?))
|
||||
.unwrap_or_default();
|
||||
|
||||
Self::init_logger(&cfg);
|
||||
|
||||
let profile = match cfg.recent_profile {
|
||||
Some((game, ref name)) => Profile::load(game, name.clone()).ok(),
|
||||
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 {
|
||||
profile: profile,
|
||||
pkgs: PackageStore::new(apph.clone()),
|
||||
cfg,
|
||||
state: GlobalState { remain_open: true }
|
||||
state: GlobalState { remain_open: true },
|
||||
patch_vec
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,8 +90,7 @@ impl AppData {
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
||||
|
||||
if let Status::OK(feature_set) = loc.status {
|
||||
log::debug!("{:?}", feature_set);
|
||||
if let Status::OK(feature_set, _) = loc.status {
|
||||
if feature_set.contains(Feature::Mod) {
|
||||
profile.mod_pkgs_mut().insert(key);
|
||||
}
|
||||
@ -121,4 +132,56 @@ impl AppData {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn patches_enabled(&self, game_target: impl AsRef<Path>, amd_target: impl AsRef<Path>) -> Result<Vec<&PatchList>> {
|
||||
let ch1 = sha256::try_digest(game_target.as_ref())?;
|
||||
let ch2 = sha256::try_digest(amd_target.as_ref())?;
|
||||
|
||||
let mut res = Vec::new();
|
||||
for pfile in &self.patch_vec.0 {
|
||||
for plist in &pfile.0 {
|
||||
let this_hash = plist.sha256.to_ascii_lowercase();
|
||||
log::debug!("checking {}", this_hash);
|
||||
if this_hash == ch1 || this_hash == ch2 {
|
||||
log::debug!("enabling {this_hash}");
|
||||
res.push(plist);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
133
rust/src/cmd.rs
@ -1,13 +1,17 @@
|
||||
use ini::Ini;
|
||||
use log;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::fs;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use crate::model::config::GlobalConfigField;
|
||||
use crate::model::misc::Game;
|
||||
use crate::model::patch::Patch;
|
||||
use crate::modules::package::prepare_dlls;
|
||||
use crate::pkg::{Package, PkgKey};
|
||||
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::model::misc::StartCheckError;
|
||||
use crate::util;
|
||||
@ -56,18 +60,46 @@ pub async fn startline(app: AppHandle, refresh: bool) -> Result<(), String> {
|
||||
let state = app.state::<Mutex<AppData>>();
|
||||
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 {
|
||||
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);
|
||||
p.line_up(hash, refresh, app.clone()).await
|
||||
|
||||
let patches_enabled = appd.patches_enabled(
|
||||
&p.data.sgt.target,
|
||||
&p.data.sgt.target.parent().unwrap().join("amdaemon.exe")
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
let info = p.prepare_display()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let lineup_res = p.line_up(hash, refresh, patches_enabled).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 p_clone = p.clone();
|
||||
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);
|
||||
}
|
||||
});
|
||||
@ -149,6 +181,10 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
|
||||
|
||||
let appd = state.lock().await;
|
||||
|
||||
let pkgs_all = appd.pkgs.get_all();
|
||||
|
||||
log::debug!("pkgs_all: {:?}", pkgs_all);
|
||||
|
||||
Ok(appd.pkgs.get_all())
|
||||
}
|
||||
|
||||
@ -293,9 +329,10 @@ pub async fn duplicate_profile(profile: ProfileMeta) -> Result<(), String> {
|
||||
pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMeta) -> Result<(), String> {
|
||||
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))?;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -319,7 +356,7 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
|
||||
|
||||
#[tauri::command]
|
||||
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;
|
||||
if let Some(p) = &mut appd.profile {
|
||||
@ -345,6 +382,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]
|
||||
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
|
||||
log::debug!("invoke: list_platform_capabilities");
|
||||
@ -363,7 +428,8 @@ pub async fn get_global_config(state: State<'_, Mutex<AppData>>, field: GlobalCo
|
||||
let appd = state.lock().await;
|
||||
match field {
|
||||
GlobalConfigField::OfflineMode => Ok(appd.cfg.offline_mode),
|
||||
GlobalConfigField::EnableAutoupdates => Ok(appd.cfg.enable_autoupdates)
|
||||
GlobalConfigField::EnableAutoupdates => Ok(appd.cfg.enable_autoupdates),
|
||||
GlobalConfigField::Verbose => Ok(appd.cfg.verbose)
|
||||
}
|
||||
}
|
||||
|
||||
@ -374,7 +440,8 @@ pub async fn set_global_config(state: State<'_, Mutex<AppData>>, field: GlobalCo
|
||||
let mut appd = state.lock().await;
|
||||
match field {
|
||||
GlobalConfigField::OfflineMode => appd.cfg.offline_mode = value,
|
||||
GlobalConfigField::EnableAutoupdates => appd.cfg.enable_autoupdates = value
|
||||
GlobalConfigField::EnableAutoupdates => appd.cfg.enable_autoupdates = value,
|
||||
GlobalConfigField::Verbose => appd.cfg.verbose = value,
|
||||
};
|
||||
appd.write().map_err(|e| e.to_string())
|
||||
}
|
||||
@ -414,3 +481,47 @@ pub async fn list_directories() -> Result<util::Dirs, ()> {
|
||||
|
||||
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)
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
use std::{collections::HashSet, path::PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::fs::File;
|
||||
use anyhow::{anyhow, Result};
|
||||
@ -6,14 +7,20 @@ use anyhow::{anyhow, Result};
|
||||
use crate::pkg::{Package, PkgKey, Remote};
|
||||
|
||||
pub struct DownloadHandler {
|
||||
set: HashSet<String>,
|
||||
paths: HashSet<PathBuf>,
|
||||
app: AppHandle
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DownloadTick {
|
||||
pkg_key: PkgKey,
|
||||
ratio: f32,
|
||||
}
|
||||
|
||||
impl DownloadHandler {
|
||||
pub fn new(app: AppHandle) -> DownloadHandler {
|
||||
DownloadHandler {
|
||||
set: HashSet::new(),
|
||||
paths: HashSet::new(),
|
||||
app
|
||||
}
|
||||
}
|
||||
@ -22,11 +29,11 @@ impl DownloadHandler {
|
||||
let rmt = pkg.rmt.as_ref()
|
||||
.ok_or_else(|| anyhow!("Attempted to download a package without remote data"))?
|
||||
.clone();
|
||||
if self.set.contains(zip_path.to_string_lossy().as_ref()) {
|
||||
// Todo when there is a clear cache button, it should clear the set
|
||||
Err(anyhow!("Already downloading"))
|
||||
if self.paths.contains(zip_path) {
|
||||
Ok(())
|
||||
} 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));
|
||||
Ok(())
|
||||
}
|
||||
@ -42,16 +49,22 @@ impl DownloadHandler {
|
||||
|
||||
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 total_bytes = 0;
|
||||
|
||||
log::info!("Downloading: {}", rmt.download_url);
|
||||
log::info!("downloading: {}", rmt.download_url);
|
||||
while let Some(item) = byte_stream.next().await {
|
||||
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.sync_all().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)?;
|
||||
|
||||
|
141
rust/src/lib.rs
@ -7,6 +7,7 @@ mod download_handler;
|
||||
mod appdata;
|
||||
mod modules;
|
||||
mod profiles;
|
||||
mod patcher;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use anyhow::anyhow;
|
||||
@ -15,26 +16,15 @@ use appdata::{AppData, ToggleAction};
|
||||
use model::misc::Game;
|
||||
use pkg::PkgKey;
|
||||
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_cli::CliExt;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
use tokio::{fs, sync::Mutex, try_join};
|
||||
|
||||
static EXIT_REQUESTED: OnceLock<()> = OnceLock::new();
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
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()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
@ -58,6 +48,15 @@ pub async fn run(_args: Vec<String>) {
|
||||
util::init_dirs(&apph);
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
if start_arg.occurrences > 0 {
|
||||
start_immediately = true;
|
||||
app_data.state.remain_open = false;
|
||||
} else {
|
||||
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into()))
|
||||
.title("STARTLINER")
|
||||
.inner_size(760f64, 480f64)
|
||||
.min_inner_size(760f64, 480f64)
|
||||
.build()?;
|
||||
open_window(apph.clone())?;
|
||||
start_immediately = false;
|
||||
}
|
||||
|
||||
@ -108,14 +102,15 @@ pub async fn run(_args: Vec<String>) {
|
||||
});
|
||||
|
||||
app.listen("download-end", closure!(clone apph, |ev| {
|
||||
log::debug!("download-end triggered: {}", ev.payload());
|
||||
let raw = ev.payload();
|
||||
log::debug!("download-end triggered: {}", raw);
|
||||
let key = PkgKey(raw[1..raw.len()-1].to_owned());
|
||||
let apph = apph.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mutex = apph.state::<Mutex<AppData>>();
|
||||
let mut appd = mutex.lock().await;
|
||||
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| {
|
||||
log::debug!("install-end-prelude triggered: {}", ev.payload());
|
||||
let payload = serde_json::from_str::<Payload>(ev.payload());
|
||||
log::debug!("install-end-prelude triggered: {:?}", payload);
|
||||
let apph = apph.clone();
|
||||
if let Ok(payload) = payload {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mutex = apph.state::<Mutex<AppData>>();
|
||||
let mut appd = mutex.lock().await;
|
||||
let res = appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf);
|
||||
log::debug!(
|
||||
"install-end-prelude toggle {:?}",
|
||||
appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf)
|
||||
res
|
||||
);
|
||||
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 {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
if let Err(e) = cmd::startline(apph.clone(), false).await {
|
||||
log::error!("Unable to launch: {}", e);
|
||||
apph.exit(1);
|
||||
log::error!("unable to launch: {}", e);
|
||||
_ = 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 {
|
||||
@ -199,6 +203,8 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::get_current_profile,
|
||||
cmd::sync_current_profile,
|
||||
cmd::save_current_profile,
|
||||
cmd::load_segatools_ini,
|
||||
cmd::create_shortcut,
|
||||
|
||||
cmd::get_global_config,
|
||||
cmd::set_global_config,
|
||||
@ -206,6 +212,13 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::list_displays,
|
||||
cmd::list_platform_capabilities,
|
||||
cmd::list_directories,
|
||||
cmd::file_exists,
|
||||
cmd::open_file,
|
||||
cmd::get_changelog,
|
||||
|
||||
cmd::list_com_ports,
|
||||
|
||||
cmd::list_patches,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application");
|
||||
@ -222,7 +235,8 @@ pub async fn run(_args: Vec<String>) {
|
||||
let mutex = app.state::<Mutex<AppData>>();
|
||||
let appd = mutex.lock().await;
|
||||
if let Some(p) = &appd.profile {
|
||||
log::debug!("save: {:?}", p.save());
|
||||
let res = p.save();
|
||||
log::debug!("save: {:?}", res);
|
||||
app.exit(0);
|
||||
}
|
||||
});
|
||||
@ -240,7 +254,7 @@ fn deep_link(app: AppHandle, args: Vec<String>) {
|
||||
let url = &args[1];
|
||||
let proto = "rainycolor://";
|
||||
if &url[..proto.len()] == proto {
|
||||
log::info!("Deep link: {}", url);
|
||||
log::info!("deep link: {}", url);
|
||||
|
||||
let regex = regex::Regex::new(
|
||||
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
|
||||
@ -266,28 +280,63 @@ fn deep_link(app: AppHandle, args: Vec<String>) {
|
||||
|
||||
async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
|
||||
let mutex = app.state::<Mutex<AppData>>();
|
||||
let appd = mutex.lock().await;
|
||||
if !appd.cfg.enable_autoupdates {
|
||||
log::info!("skipping autoupdate");
|
||||
return Ok(());
|
||||
{
|
||||
let appd = mutex.lock().await;
|
||||
if !appd.cfg.enable_autoupdates {
|
||||
log::info!("skipping auto-update");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(update) = app.updater()?.check().await? {
|
||||
let mut downloaded = 0;
|
||||
update.download_and_install(
|
||||
|chunk_length, content_length| {
|
||||
downloaded += chunk_length;
|
||||
log::debug!("downloaded {downloaded} from {content_length:?}");
|
||||
},
|
||||
|| {
|
||||
log::info!("download finished");
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
#[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();
|
||||
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(())
|
||||
}
|
@ -2,10 +2,12 @@ use serde::{Deserialize, Serialize};
|
||||
use super::misc::Game;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct GlobalConfig {
|
||||
pub recent_profile: Option<(Game, String)>,
|
||||
pub offline_mode: bool,
|
||||
pub enable_autoupdates: bool,
|
||||
pub verbose: bool,
|
||||
}
|
||||
|
||||
impl Default for GlobalConfig {
|
||||
@ -13,13 +15,16 @@ impl Default for GlobalConfig {
|
||||
Self {
|
||||
recent_profile: Default::default(),
|
||||
offline_mode: false,
|
||||
enable_autoupdates: true
|
||||
enable_autoupdates: true,
|
||||
verbose: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GlobalConfigField {
|
||||
OfflineMode,
|
||||
EnableAutoupdates
|
||||
EnableAutoupdates,
|
||||
Verbose
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::pkg::{Status, PkgKey, PkgKeyVersion};
|
||||
use crate::pkg::{PkgKey, PkgKeyVersion};
|
||||
|
||||
use super::misc::Game;
|
||||
|
||||
@ -14,7 +14,10 @@ pub struct PackageManifest {
|
||||
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||
|
||||
#[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>;
|
||||
@ -22,6 +25,5 @@ pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct PackageListEntry {
|
||||
pub version: String,
|
||||
pub status: Status,
|
||||
pub games: Vec<Game>,
|
||||
}
|
@ -5,10 +5,9 @@ use crate::pkg::PkgKey;
|
||||
use super::profile::ProfileModule;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Game {
|
||||
#[serde(rename = "ongeki")]
|
||||
Ongeki,
|
||||
#[serde(rename = "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 {
|
||||
match self {
|
||||
Game::Ongeki => "mu3hook.dll",
|
||||
@ -59,14 +65,14 @@ impl Game {
|
||||
pub fn amd_args(&self) -> Vec<&'static str> {
|
||||
match self {
|
||||
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 {
|
||||
match self {
|
||||
Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini}),
|
||||
Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Network}),
|
||||
Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini | Keyboard}),
|
||||
Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Display | Network | Keyboard | Mempatcher}),
|
||||
}.contains(module)
|
||||
}
|
||||
}
|
||||
@ -87,3 +93,27 @@ pub enum StartCheckError {
|
||||
MissingDependency(PkgKey, 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
|
||||
}
|
@ -3,4 +3,4 @@ pub mod misc;
|
||||
pub mod rainy;
|
||||
pub mod profile;
|
||||
pub mod config;
|
||||
pub mod segatools_base;
|
||||
pub mod patch;
|
162
rust/src/model/patch.rs
Normal 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?
|
||||
})
|
||||
}
|
||||
}
|
@ -13,7 +13,16 @@ pub enum Aime {
|
||||
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)]
|
||||
#[serde(default)]
|
||||
pub struct AMNet {
|
||||
pub name: 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 target: PathBuf,
|
||||
pub hook: Option<PkgKey>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub io: Option<PkgKey>,
|
||||
#[serde(default)]
|
||||
pub io2: IOSelection,
|
||||
pub aime: Aime,
|
||||
pub amfs: PathBuf,
|
||||
pub option: PathBuf,
|
||||
pub appdata: PathBuf,
|
||||
pub intel: bool,
|
||||
#[serde(default)]
|
||||
pub amnet: AMNet,
|
||||
pub aime_port: Option<i32>,
|
||||
}
|
||||
|
||||
impl Segatools {
|
||||
@ -50,12 +61,14 @@ impl Segatools {
|
||||
Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned()))
|
||||
},
|
||||
io: None,
|
||||
io2: IOSelection::SegatoolsBuiltIn,
|
||||
amfs: PathBuf::default(),
|
||||
option: PathBuf::default(),
|
||||
appdata: PathBuf::from("appdata"),
|
||||
aime: Aime::default(),
|
||||
intel: false,
|
||||
amnet: AMNet::default(),
|
||||
aime_port: None
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,14 +80,17 @@ pub enum DisplayMode {
|
||||
Fullscreen
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||
#[serde(default)]
|
||||
pub struct Display {
|
||||
pub target: String,
|
||||
pub rez: (i32, i32),
|
||||
pub mode: DisplayMode,
|
||||
pub rotation: i32,
|
||||
pub rotation: Option<i32>,
|
||||
pub frequency: i32,
|
||||
pub borderless_fullscreen: bool,
|
||||
pub dont_switch_primary: bool,
|
||||
pub monitor_index_override: Option<i32>,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
@ -86,12 +102,14 @@ impl Display {
|
||||
Game::Ongeki => (1080, 1920),
|
||||
},
|
||||
mode: DisplayMode::Borderless,
|
||||
rotation: 0,
|
||||
rotation: None,
|
||||
frequency: match game {
|
||||
Game::Chunithm => 120,
|
||||
Game::Ongeki => 60,
|
||||
},
|
||||
borderless_fullscreen: true,
|
||||
dont_switch_primary: false,
|
||||
monitor_index_override: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,6 +121,7 @@ pub enum NetworkType {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Network {
|
||||
pub network_type: NetworkType,
|
||||
|
||||
@ -117,11 +136,13 @@ pub struct Network {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct BepInEx {
|
||||
pub console: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Wine {
|
||||
pub runtime: PathBuf,
|
||||
pub prefix: PathBuf,
|
||||
@ -146,21 +167,109 @@ pub enum Mu3Audio {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Mu3Ini {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audio: Option<Mu3Audio>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sample_rate: 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]
|
||||
#[repr(u8)]
|
||||
#[repr(u16)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ProfileModule {
|
||||
Segatools,
|
||||
Network,
|
||||
Display,
|
||||
BepInEx,
|
||||
Mu3Ini
|
||||
Mu3Ini,
|
||||
Keyboard,
|
||||
Mempatcher
|
||||
}
|
@ -22,4 +22,5 @@ pub struct V1Version {
|
||||
pub icon: String,
|
||||
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||
pub download_url: String,
|
||||
pub file_size: i64,
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,20 +1,20 @@
|
||||
|
||||
use crate::model::profile::{Display, DisplayMode};
|
||||
use crate::{model::{misc::Game, profile::{Display, DisplayMode}}, util::bool_to_01};
|
||||
use anyhow::Result;
|
||||
use displayz::{query_displays, DisplaySet};
|
||||
use ini::Ini;
|
||||
use tauri::{AppHandle, Listener};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DisplayInfo {
|
||||
pub primary: String,
|
||||
pub set: Option<DisplaySet>
|
||||
pub primary: Option<String>,
|
||||
pub set: Option<DisplaySet>,
|
||||
}
|
||||
|
||||
impl Default for DisplayInfo {
|
||||
fn default() -> Self {
|
||||
DisplayInfo {
|
||||
primary: "default".to_owned(),
|
||||
set: query_displays().ok()
|
||||
primary: None,
|
||||
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 displayz::{query_displays, Orientation, Resolution, Frequency};
|
||||
|
||||
@ -52,21 +52,35 @@ impl Display {
|
||||
.find(|display| display.name() == 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()
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
||||
|
||||
let res = DisplayInfo {
|
||||
primary: primary.name().to_owned(),
|
||||
set: Some(display_set.clone())
|
||||
primary: if self.dont_switch_primary { None } else { Some(primary.name().to_owned()) },
|
||||
set: Some(display_set.clone()),
|
||||
};
|
||||
|
||||
if self.rotation == 90 || self.rotation == 270 {
|
||||
if let Some(rotation) = self.rotation {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,18 +112,34 @@ impl Display {
|
||||
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<()> {
|
||||
use anyhow::anyhow;
|
||||
|
||||
let display_set = info.set.as_ref()
|
||||
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
|
||||
|
||||
let primary = display_set
|
||||
.displays()
|
||||
.find(|display| display.name() == info.primary)
|
||||
.ok_or_else(|| anyhow!("Display {} not found", info.primary))?;
|
||||
if let Some(info_primary) = &info.primary {
|
||||
let primary = display_set
|
||||
.displays()
|
||||
.find(|display| display.name() == info_primary)
|
||||
.ok_or_else(|| anyhow!("Display {} not found", info_primary))?;
|
||||
|
||||
primary.set_primary()?;
|
||||
primary.set_primary()?;
|
||||
}
|
||||
|
||||
display_set.apply()?;
|
||||
displayz::refresh()?;
|
||||
|
150
rust/src/modules/keyboard.rs
Normal 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(())
|
||||
}
|
||||
}
|
57
rust/src/modules/mempatcher.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use std::path::Path;
|
||||
use anyhow::Result;
|
||||
use crate::model::patch::{Patch, PatchData, PatchList, PatchSelection, PatchSelectionData};
|
||||
|
||||
impl PatchSelection {
|
||||
pub async fn render_to_file(
|
||||
&self,
|
||||
filename: &str,
|
||||
patch_lists: &Vec<&PatchList>,
|
||||
path: impl AsRef<Path>
|
||||
) -> Result<()> {
|
||||
let mut res = "".to_owned();
|
||||
|
||||
for list in patch_lists {
|
||||
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)
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ pub mod segatools;
|
||||
pub mod network;
|
||||
pub mod bepinex;
|
||||
pub mod mu3ini;
|
||||
pub mod keyboard;
|
||||
pub mod mempatcher;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod display_windows;
|
@ -1,11 +1,11 @@
|
||||
use std::path::Path;
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use ini::Ini;
|
||||
use crate::model::profile::{Mu3Audio, Mu3Ini};
|
||||
|
||||
impl Mu3Ini {
|
||||
pub fn line_up(&self, game_path: impl AsRef<Path>) -> Result<()> {
|
||||
let file = game_path.as_ref().join("mu3.ini");
|
||||
pub fn line_up(&self, data_dir: impl AsRef<Path>, cfg_dir: impl AsRef<Path>) -> Result<()> {
|
||||
let file = cfg_dir.as_ref().join("mu3.ini");
|
||||
|
||||
if !file.exists() {
|
||||
std::fs::write(&file, "")?;
|
||||
@ -20,9 +20,26 @@ impl Mu3Ini {
|
||||
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)?;
|
||||
|
||||
Ok(())
|
||||
|
@ -5,6 +5,32 @@ use ini::Ini;
|
||||
use crate::model::profile::{Network, NetworkType};
|
||||
|
||||
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<()> {
|
||||
log::debug!("begin line-up: network");
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
use anyhow::Result;
|
||||
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::profiles::ProfilePaths;
|
||||
use crate::profiles::types::ProfilePaths;
|
||||
|
||||
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> {
|
||||
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 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 {
|
||||
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 {
|
||||
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("BepInEx");
|
||||
if bpx_dir.exists() {
|
||||
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() {
|
||||
let x = opt_dir.read_dir().unwrap().next().unwrap()?;
|
||||
if x.metadata()?.is_dir() {
|
||||
@ -46,3 +58,26 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
|
||||
|
||||
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))
|
||||
}
|
@ -1,35 +1,59 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::path::{PathBuf, Path};
|
||||
use anyhow::{anyhow, Result};
|
||||
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;
|
||||
|
||||
impl Segatools {
|
||||
pub fn fix(&mut self, store: &PackageStore) {
|
||||
macro_rules! remove_if_nonpresent {
|
||||
($item:expr,$key:expr,$emptyval:expr,$store:expr) => {
|
||||
if let Ok(pkg) = $store.get($key) {
|
||||
if pkg.loc.is_none() {
|
||||
$item = $emptyval;
|
||||
pub fn fix(&mut self, _store: &PackageStore) {
|
||||
// macro_rules! remove_if_nonpresent {
|
||||
// ($item:expr,$key:expr,$emptyval:expr,$store:expr) => {
|
||||
// if let Ok(pkg) = $store.get($key) {
|
||||
// if pkg.loc.is_none() {
|
||||
// $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 {
|
||||
$item = $emptyval;
|
||||
log::warn!("aime emulation is enabled, but no aimePath specified");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key) = &self.hook {
|
||||
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),
|
||||
_ => {},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub async fn line_up(&self, p: &impl ProfilePaths, game: Game) -> Result<Ini> {
|
||||
log::debug!("begin line-up: segatools");
|
||||
@ -42,8 +66,12 @@ impl Segatools {
|
||||
let ini_path = p.config_dir().join("segatools-base.ini");
|
||||
|
||||
if !ini_path.exists() {
|
||||
tokio::fs::write(&ini_path, segatools_base(game)).await
|
||||
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
|
||||
match game {
|
||||
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() {
|
||||
tokio::fs::create_dir(&pfx_dir).await
|
||||
@ -106,6 +134,9 @@ impl Segatools {
|
||||
if self.amnet.name.len() > 0 {
|
||||
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 {
|
||||
ini_out.with_section(Some("aime"))
|
||||
@ -113,7 +144,7 @@ impl Segatools {
|
||||
}
|
||||
|
||||
if game == Game::Ongeki {
|
||||
if let Some(io) = &self.io {
|
||||
if let IOSelection::Custom(io) = &self.io2 {
|
||||
ini_out.with_section(Some("mu3io"))
|
||||
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
|
||||
} else {
|
||||
@ -121,6 +152,44 @@ impl Segatools {
|
||||
.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);
|
||||
|
||||
@ -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");
|
||||
|
||||
Ok(ini_out)
|
||||
|
50
rust/src/patcher.rs
Normal file
@ -0,0 +1,50 @@
|
||||
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_patches = Vec::new();
|
||||
for pfile in &self.0 {
|
||||
for plist in &pfile.0 {
|
||||
let this_hash = plist.sha256.to_ascii_lowercase();
|
||||
log::debug!("checking {}", this_hash);
|
||||
if this_hash == checksum {
|
||||
let mut cloned = plist.clone().patches;
|
||||
res_patches.append(&mut cloned);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if res_patches.len() == 0 {
|
||||
log::warn!("no matching patchset for {:?} ({})", target.as_ref(), checksum);
|
||||
}
|
||||
Ok(res_patches)
|
||||
}
|
||||
}
|
149
rust/src/pkg.rs
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
||||
use tokio::fs;
|
||||
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}
|
||||
#[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)]
|
||||
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)]
|
||||
pub struct Package {
|
||||
pub namespace: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
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 {
|
||||
Unchecked,
|
||||
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]
|
||||
#[repr(u8)]
|
||||
#[repr(u16)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Feature {
|
||||
Mod,
|
||||
@ -41,9 +54,13 @@ pub enum Feature {
|
||||
Mu3Hook,
|
||||
Mu3IO,
|
||||
ChusanHook,
|
||||
ChuniIO,
|
||||
Mempatcher,
|
||||
GameDLL,
|
||||
AmdDLL
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Local {
|
||||
pub version: String,
|
||||
@ -53,7 +70,7 @@ pub struct Local {
|
||||
pub icon: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Remote {
|
||||
pub version: String,
|
||||
@ -64,6 +81,15 @@ pub struct Remote {
|
||||
pub nsfw: bool,
|
||||
pub categories: Vec<String>,
|
||||
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 {
|
||||
@ -87,12 +113,14 @@ impl Package {
|
||||
nsfw: p.has_nsfw_content,
|
||||
version: v.version_number,
|
||||
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 mft: local::PackageManifest = serde_json::from_str(&str)?;
|
||||
|
||||
@ -102,10 +130,10 @@ impl Package {
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
|
||||
let status = Self::parse_status(&mft);
|
||||
let status = Self::parse_status(&mft, &dir);
|
||||
let dependencies = Self::sanitize_deps(mft.dependencies);
|
||||
|
||||
Ok(Package {
|
||||
Ok((Package {
|
||||
namespace: Self::dir_to_namespace(&dir)?,
|
||||
name: mft.name.clone(),
|
||||
description: mft.description.clone(),
|
||||
@ -116,8 +144,9 @@ impl Package {
|
||||
status,
|
||||
dependencies
|
||||
}),
|
||||
rmt: None
|
||||
})
|
||||
rmt: None,
|
||||
source
|
||||
}, mft.games))
|
||||
}
|
||||
|
||||
pub fn key(&self) -> PkgKey {
|
||||
@ -125,7 +154,15 @@ impl Package {
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -186,33 +223,69 @@ impl Package {
|
||||
res
|
||||
}
|
||||
|
||||
fn parse_status(mft: &PackageManifest) -> Status {
|
||||
fn parse_status(mft: &PackageManifest, dir: impl AsRef<Path>) -> Status {
|
||||
if mft.installers.len() == 0 {
|
||||
return Status::OK(make_bitflags!(Feature::Mod));//Unchecked
|
||||
} else if mft.installers.len() == 1 {
|
||||
if let Some(serde_json::Value::String(id)) = &mft.installers[0].get("identifier") {
|
||||
if id == "rainycolor" {
|
||||
return Status::OK(make_bitflags!(Feature::Mod));
|
||||
} else if id == "segatools" {
|
||||
// Multiple features in the same dll (yubideck etc.) should be supported at some point
|
||||
let mut flags = BitFlags::default();
|
||||
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") {
|
||||
if module == "mu3hook" {
|
||||
flags |= Feature::Mu3Hook;
|
||||
} else if module == "chusanhook" {
|
||||
flags |= Feature::ChusanHook;
|
||||
} else if module == "amnet" {
|
||||
flags |= Feature::AMNet | Feature::Aime;
|
||||
} else if module == "aimeio" {
|
||||
flags |= Feature::Aime;
|
||||
} else if module == "mu3io" {
|
||||
flags |= Feature::Mu3IO;
|
||||
if dir.as_ref().join("post_load.ps1").exists() {
|
||||
return Status::Unsupported;
|
||||
}
|
||||
if dir.as_ref().join("app").join("data").exists() {
|
||||
return Status::Unsupported;
|
||||
}
|
||||
return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None });
|
||||
} else {
|
||||
let mut flags = BitFlags::default();
|
||||
let mut game_dll = None;
|
||||
let mut amd_dll = None;
|
||||
for installer in &mft.installers {
|
||||
if let Some(serde_json::Value::String(id)) = installer.get("identifier") {
|
||||
if id == "rainycolor" {
|
||||
flags |= Feature::Mod;
|
||||
} else if id == "segatools" {
|
||||
if let Some(serde_json::Value::String(module)) = installer.get("module") {
|
||||
flags |= Self::parse_segatools_module(&module);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
@ -8,7 +7,7 @@ use tokio::task::JoinSet;
|
||||
use crate::model::local::{PackageList, PackageListEntry};
|
||||
use crate::model::misc::Game;
|
||||
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::download_handler::DownloadHandler;
|
||||
|
||||
@ -22,7 +21,7 @@ pub struct PackageStore {
|
||||
offline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Payload {
|
||||
pub pkg: PkgKey
|
||||
}
|
||||
@ -67,9 +66,24 @@ impl PackageStore {
|
||||
.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) {
|
||||
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);
|
||||
} else {
|
||||
log::error!("couldn't reload {}", key);
|
||||
@ -83,12 +97,18 @@ impl PackageStore {
|
||||
for dir in dirents {
|
||||
if let Ok(dir) = dir {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -138,7 +158,6 @@ impl PackageStore {
|
||||
PackageListEntry {
|
||||
// from_rainy() is guaranteed to include rmt
|
||||
version: r.rmt.as_ref().unwrap().version.clone(),
|
||||
status: Status::Unchecked,
|
||||
games: vec![ game ],
|
||||
}
|
||||
});
|
||||
@ -192,8 +211,9 @@ impl PackageStore {
|
||||
"{}-{}-{}.zip",
|
||||
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)?;
|
||||
log::debug!("deferring {}", key);
|
||||
return Ok(InstallResult::Deferred);
|
||||
@ -228,7 +248,7 @@ impl PackageStore {
|
||||
if path.exists() && path.join("manifest.json").exists() {
|
||||
pkg.loc = None;
|
||||
|
||||
let rv = Self::clean_up_package(&path).await;
|
||||
let rv = util::remove_dir_all(&path).await;
|
||||
|
||||
if rv.is_ok() {
|
||||
self.app.emit("install-end-prelude", Payload {
|
||||
@ -254,44 +274,6 @@ impl PackageStore {
|
||||
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<()> {
|
||||
for d in rmt.dependencies {
|
||||
set.insert(d.clone());
|
||||
|
@ -1,61 +1,16 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use std::{collections::BTreeSet, path::{Path, PathBuf}};
|
||||
use crate::{model::{misc::Game, profile::{Aime, Mu3Ini, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
|
||||
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
|
||||
use crate::{model::{misc::Game, patch::{PatchList, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
use tauri::Emitter;
|
||||
use std::process::Stdio;
|
||||
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 std::fs::File;
|
||||
use tokio::process::Command;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
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>
|
||||
}
|
||||
pub mod types;
|
||||
|
||||
impl Profile {
|
||||
pub fn new(mut meta: ProfileMeta) -> Result<Self> {
|
||||
@ -66,32 +21,83 @@ impl Profile {
|
||||
mods: BTreeSet::new(),
|
||||
sgt: Segatools::default_for(meta.game),
|
||||
#[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"))]
|
||||
display: None,
|
||||
network: Network::default(),
|
||||
bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None },
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
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()
|
||||
};
|
||||
p.save()?;
|
||||
std::fs::create_dir_all(p.config_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)
|
||||
}
|
||||
pub fn load(game: Game, name: String) -> Result<Self> {
|
||||
let path = util::profile_config_dir(game, &name).join("profile.json");
|
||||
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))?;
|
||||
|
||||
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 {
|
||||
meta: ProfileMeta {
|
||||
game, name
|
||||
@ -112,7 +118,7 @@ impl Profile {
|
||||
}
|
||||
std::fs::write(&path, s)
|
||||
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
|
||||
log::info!("Written to {:?}", path);
|
||||
log::info!("profile saved to {:?}", path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -130,7 +136,7 @@ impl Profile {
|
||||
if let Some(hook) = &self.data.sgt.hook {
|
||||
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());
|
||||
}
|
||||
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() {
|
||||
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;
|
||||
|
||||
#[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)?;
|
||||
}
|
||||
if self.meta.game.has_module(ProfileModule::Keyboard) && source.keyboard.is_some() {
|
||||
self.data.keyboard = source.keyboard;
|
||||
}
|
||||
|
||||
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, patchlists_enabled: Vec<&PatchList>) -> Result<()> {
|
||||
if !self.data_dir().exists() {
|
||||
tokio::fs::create_dir(self.data_dir()).await?;
|
||||
}
|
||||
@ -194,12 +196,23 @@ impl Profile {
|
||||
util::clean_up_opts(self.data_dir().join("option"))?;
|
||||
|
||||
let hash_check = Self::hash_check(&hash_path, &pkg_hash).await? || refresh;
|
||||
|
||||
prepare_packages(&self.meta, &self.data.mods, hash_check).await
|
||||
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
|
||||
|
||||
let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await
|
||||
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
|
||||
|
||||
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"))
|
||||
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
|
||||
|
||||
@ -208,13 +221,21 @@ impl Profile {
|
||||
}
|
||||
|
||||
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", &patchlists_enabled, self.data_dir().join("patch-amd.mph")),
|
||||
patches.render_to_file("chusanApp.exe", &patchlists_enabled, self.data_dir().join("patch-game.mph"))
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
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");
|
||||
|
||||
log::debug!("With path {:?}", ini_path);
|
||||
@ -245,12 +266,24 @@ impl Profile {
|
||||
&ini_path,
|
||||
)
|
||||
.current_dir(&exe_dir)
|
||||
.arg("/C")
|
||||
.raw_arg("/C")
|
||||
.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("amdaemon.exe")
|
||||
.args(self.meta.game.amd_args());
|
||||
|
||||
amd_builder.arg(self.data_dir().join("config_hook.json"));
|
||||
|
||||
game_builder
|
||||
.env(
|
||||
"SEGATOOLS_CONFIG_PATH",
|
||||
@ -260,23 +293,62 @@ impl Profile {
|
||||
"INOHARA_CONFIG_PATH",
|
||||
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)
|
||||
.args(["-d", "-k"])
|
||||
.arg(sgt_dir.join(self.meta.game.hook_exe()))
|
||||
.arg(self.meta.game.exe());
|
||||
.raw_arg("-d")
|
||||
.raw_arg("-k")
|
||||
.arg(sgt_dir.join(self.meta.game.hook_exe()));
|
||||
|
||||
if let Some(display) = &self.data.display {
|
||||
game_builder.args([
|
||||
"-monitor 1",
|
||||
"-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");
|
||||
for dll in payload.game_dlls {
|
||||
game_builder.raw_arg("-k");
|
||||
game_builder.arg(dll);
|
||||
}
|
||||
|
||||
game_builder.arg(self.meta.game.exe());
|
||||
|
||||
if self.meta.game.has_module(ProfileModule::BepInEx) {
|
||||
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")]
|
||||
{
|
||||
amd_builder.env("WINEPREFIX", &self.wine.prefix);
|
||||
@ -305,8 +377,8 @@ impl Profile {
|
||||
|
||||
util::pkill("amdaemon.exe").await;
|
||||
|
||||
log::info!("Launching amdaemon: {:?}", amd_builder);
|
||||
log::info!("Launching {}: {:?}", self.meta.game, game_builder);
|
||||
log::info!("launching amdaemon: {:?}", amd_builder);
|
||||
log::info!("launching {}: {:?}", self.meta.game, game_builder);
|
||||
|
||||
let mut amd = amd_builder.spawn()?;
|
||||
let mut game = game_builder.spawn()?;
|
||||
@ -321,7 +393,7 @@ impl Profile {
|
||||
(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);
|
||||
}
|
||||
|
||||
@ -339,7 +411,7 @@ impl Profile {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -357,6 +429,17 @@ impl Profile {
|
||||
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 {
|
||||
|
64
rust/src/profiles/types.rs
Normal 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>,
|
||||
}
|
@ -151,3 +151,66 @@ impl PathStr for PathBuf {
|
||||
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(())
|
||||
}
|
82
rust/static/segatools-chunithm.ini
Normal 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=
|
36
rust/static/segatools-ongeki.ini
Normal 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
|
369
rust/static/standard-chunithm.json5
Normal 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] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "STARTLINER",
|
||||
"version": "0.4.0",
|
||||
"version": "0.12.1",
|
||||
"identifier": "zip.patafour.startliner",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
@ -66,7 +66,7 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": ["icons/slow.png", "icons/slow.ico"],
|
||||
"icon": ["icons/icon.png", "icons/icon.ico"],
|
||||
"createUpdaterArtifacts": true
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, onMounted, ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputIcon from 'primevue/inputicon';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import ProgressBar from 'primevue/progressbar';
|
||||
import ScrollPanel from 'primevue/scrollpanel';
|
||||
import Tab from 'primevue/tab';
|
||||
import TabList from 'primevue/tablist';
|
||||
import TabPanel from 'primevue/tabpanel';
|
||||
import TabPanels from 'primevue/tabpanels';
|
||||
import Tabs from 'primevue/tabs';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import InfoPage from './InfoPage.vue';
|
||||
import ModList from './ModList.vue';
|
||||
import ModStore from './ModStore.vue';
|
||||
import OptionList from './OptionList.vue';
|
||||
import PatchList from './PatchList.vue';
|
||||
import ProfileList from './ProfileList.vue';
|
||||
import StartButton from './StartButton.vue';
|
||||
import { invoke } from '../invoke';
|
||||
@ -23,6 +28,9 @@ import {
|
||||
usePrfStore,
|
||||
} from '../stores';
|
||||
import { Dirs } from '../types';
|
||||
import { messageSplit, shouldPreferDark } from '../util';
|
||||
|
||||
document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark());
|
||||
|
||||
const pkg = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
@ -31,11 +39,22 @@ const client = useClientStore();
|
||||
|
||||
pkg.setupListeners();
|
||||
|
||||
const currentTab: Ref<string | number> = ref(3);
|
||||
const currentTab: Ref<'users' | 'loc' | 'patches' | 'rmt' | 'cfg' | 'info'> =
|
||||
ref('users');
|
||||
const pkgSearchTerm = ref('');
|
||||
|
||||
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 () => {
|
||||
invoke('list_directories').then((d) => {
|
||||
general.dirs = d as Dirs;
|
||||
@ -47,20 +66,11 @@ onMounted(async () => {
|
||||
await Promise.all([prf.reloadList(), prf.reload()]);
|
||||
|
||||
if (prf.current !== null) {
|
||||
currentTab.value = 0;
|
||||
currentTab.value = 'loc';
|
||||
await pkg.reloadAll();
|
||||
}
|
||||
|
||||
fetch_promise.then(async () => {
|
||||
await invoke('install_package', {
|
||||
key: 'segatools-mu3hook',
|
||||
force: false,
|
||||
});
|
||||
await invoke('install_package', {
|
||||
key: 'segatools-chusanhook',
|
||||
force: false,
|
||||
});
|
||||
});
|
||||
await fetch_promise;
|
||||
});
|
||||
|
||||
const errorVisible = ref(false);
|
||||
@ -72,6 +82,66 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -86,6 +156,33 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
||||
: '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"
|
||||
@ -97,58 +194,70 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
||||
{{ errorMessage }}
|
||||
<Button
|
||||
class="m-auto"
|
||||
label="A sad state of affairs"
|
||||
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
|
||||
lazy
|
||||
:value="currentTab"
|
||||
v-on:update:value="
|
||||
(value) => {
|
||||
currentTab = value;
|
||||
currentTab = value as any;
|
||||
}
|
||||
"
|
||||
class="h-screen"
|
||||
>
|
||||
<div class="fixed w-full flex z-100">
|
||||
<TabList class="grow" :show-navigators="false">
|
||||
<Tab :value="3"
|
||||
><div class="pi pi-users" v-tooltip="'Profiles'"></div
|
||||
<Tab value="users"><div class="pi pi-users"></div></Tab>
|
||||
<Tab :disabled="isProfileDisabled" value="loc"
|
||||
><div class="pi pi-box"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isProfileDisabled" :value="0"
|
||||
><div
|
||||
class="pi pi-box"
|
||||
v-tooltip="'Installed packages'"
|
||||
></div
|
||||
></Tab>
|
||||
<Tab v-if="prf.current?.meta.game === 'chunithm'" :value="4"
|
||||
><div class="pi pi-ticket" v-tooltip="'Patches'"></div
|
||||
<Tab
|
||||
v-if="
|
||||
prf.current?.meta.game === 'chunithm' &&
|
||||
prf.current.data.sgt.target.length > 0
|
||||
"
|
||||
value="patches"
|
||||
><div class="pi pi-ticket"></div
|
||||
></Tab>
|
||||
<Tab
|
||||
v-if="pkg.networkStatus === 'online'"
|
||||
:disabled="isProfileDisabled"
|
||||
:value="1"
|
||||
><div
|
||||
class="pi pi-download"
|
||||
v-tooltip="'Package store'"
|
||||
></div
|
||||
value="rmt"
|
||||
><div class="pi pi-download"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isProfileDisabled" :value="2"
|
||||
><div class="pi pi-cog" v-tooltip="'Settings'"></div
|
||||
<Tab :disabled="isProfileDisabled" value="cfg"
|
||||
><div class="pi pi-cog"></div
|
||||
></Tab>
|
||||
<Tab value="info"
|
||||
><div class="pi pi-info-circle"></div
|
||||
></Tab>
|
||||
|
||||
<div class="grow"></div>
|
||||
|
||||
<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">
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText
|
||||
v-if="currentTab === 2"
|
||||
v-if="currentTab === 'cfg'"
|
||||
style="min-width: 0; width: 25dvw"
|
||||
class="self-center"
|
||||
size="small"
|
||||
@ -198,38 +307,50 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
||||
</TabList>
|
||||
</div>
|
||||
<TabPanels class="w-full grow mt-[3rem]">
|
||||
<TabPanel :value="0">
|
||||
<TabPanel value="loc">
|
||||
<ModList :search="pkgSearchTerm" />
|
||||
</TabPanel>
|
||||
<TabPanel :value="1">
|
||||
<TabPanel value="rmt">
|
||||
<ModStore :search="pkgSearchTerm" />
|
||||
</TabPanel>
|
||||
<TabPanel :value="2">
|
||||
<TabPanel value="cfg">
|
||||
<OptionList />
|
||||
</TabPanel>
|
||||
<TabPanel :value="3">
|
||||
<TabPanel value="users">
|
||||
<ProfileList />
|
||||
<br /><br /><br />
|
||||
<footer>
|
||||
<Button
|
||||
icon="pi pi-discord"
|
||||
as="a"
|
||||
target="_blank"
|
||||
href="https://discord.gg/jxvzHjjEmc"
|
||||
/>
|
||||
</footer>
|
||||
</TabPanel>
|
||||
<TabPanel :value="4">
|
||||
CHUNITHM patches are not implemented yet.<br />Use
|
||||
<a
|
||||
href="https://patcher.two-torial.xyz/"
|
||||
target="_blank"
|
||||
style="text-decoration: underline"
|
||||
>patcher.two-torial.xyz</a
|
||||
>
|
||||
<TabPanel value="patches">
|
||||
<PatchList
|
||||
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>
|
||||
</TabPanels>
|
||||
<div v-if="currentTab === 5 || currentTab === 3">
|
||||
<div v-if="currentTab === 'users' || currentTab === 'info'">
|
||||
<img
|
||||
v-if="prf.current?.meta.game === 'ongeki'"
|
||||
src="/sticker-ongeki.svg"
|
||||
@ -286,4 +407,33 @@ body {
|
||||
.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>
|
||||
|
@ -65,7 +65,7 @@ const filePick = async () => {
|
||||
|
||||
<template>
|
||||
<Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" />
|
||||
<div v-else>
|
||||
<div class="primitive-base" v-else>
|
||||
<Button
|
||||
v-if="exists"
|
||||
icon="pi pi-pen-to-square"
|
||||
@ -102,12 +102,20 @@ const filePick = async () => {
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
position: fixed;
|
||||
top: 10vh;
|
||||
left: 10vw;
|
||||
height: 80vh;
|
||||
width: 80vw;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
height: 500px;
|
||||
width: 800px;
|
||||
margin-left: -400px;
|
||||
margin-top: -250px;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
background-color: #151515;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.primitive-base ::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
63
src/components/InfoPage.vue
Normal 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>
|
@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore } from '../stores';
|
||||
@ -11,20 +12,26 @@ const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
});
|
||||
|
||||
const deleting = ref(false);
|
||||
|
||||
const remove = async () => {
|
||||
if (props.pkg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleting.value = true;
|
||||
|
||||
await invoke('delete_package', {
|
||||
key: pkgKey(props.pkg),
|
||||
});
|
||||
|
||||
deleting.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
v-if="pkg?.loc && !pkg?.js.busy"
|
||||
v-if="pkg?.loc && !pkg?.js.downloading"
|
||||
rounded
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
@ -32,7 +39,7 @@ const remove = async () => {
|
||||
size="small"
|
||||
class="self-center ml-4"
|
||||
style="width: 2rem; height: 2rem"
|
||||
:loading="pkg?.js.busy"
|
||||
:loading="deleting"
|
||||
v-on:click="remove()"
|
||||
/>
|
||||
|
||||
@ -45,7 +52,7 @@ const remove = async () => {
|
||||
size="small"
|
||||
class="self-center ml-4"
|
||||
style="width: 2rem; height: 2rem"
|
||||
:loading="pkg?.js.busy"
|
||||
:loading="pkg?.js.downloading"
|
||||
v-on:click="async () => await pkgs.install(pkg)"
|
||||
/>
|
||||
</template>
|
||||
|
188
src/components/KeyboardKey.vue
Normal 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>
|
@ -15,7 +15,7 @@ const props = defineProps({
|
||||
|
||||
const pkgs = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
const empty = ref(true);
|
||||
const empty = ref(false);
|
||||
const gameSublist: Ref<string[]> = ref([]);
|
||||
|
||||
invoke('get_game_packages', {
|
||||
@ -81,5 +81,5 @@ const missing = computed(() => {
|
||||
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()">
|
||||
<ModListEntry v-for="p in namespace" :pkg="p" />
|
||||
</Fieldset>
|
||||
<div v-if="empty" class="text-3xl">∅</div>
|
||||
<div v-if="empty === true" class="text-3xl fadein">∅</div>
|
||||
</template>
|
||||
|
@ -2,11 +2,11 @@
|
||||
import { computed } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import InstallButton from './InstallButton.vue';
|
||||
import LinkButton from './LinkButton.vue';
|
||||
import ModTitlecard from './ModTitlecard.vue';
|
||||
import UpdateButton from './UpdateButton.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore, usePrfStore } from '../stores';
|
||||
import { Feature, Package } from '../types';
|
||||
import { hasFeature } from '../util';
|
||||
@ -26,19 +26,32 @@ const model = computed({
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
|
||||
<UpdateButton :pkg="pkg" />
|
||||
<ToggleSwitch
|
||||
v-if="hasFeature(pkg, Feature.Mod)"
|
||||
class="scale-[1.33] shrink-0"
|
||||
inputId="switch"
|
||||
:disabled="pkg!.loc!.status === 'Unsupported'"
|
||||
v-model="model"
|
||||
/>
|
||||
<span
|
||||
v-tooltip="
|
||||
unsupported &&
|
||||
'This package is currently incompatible with STARTLINER.'
|
||||
"
|
||||
>
|
||||
<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" />
|
||||
<Button
|
||||
rounded
|
||||
@ -48,7 +61,9 @@ const model = computed({
|
||||
size="small"
|
||||
class="ml-2 shrink-0"
|
||||
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" />
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<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 MultiSelect from 'primevue/multiselect';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
@ -40,6 +41,39 @@ const list = () => {
|
||||
empty.value = res.length === 0;
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -78,6 +112,14 @@ const list = () => {
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<ModStoreEntry :pkg="p" />
|
||||
</div>
|
||||
|
@ -38,7 +38,7 @@ const iconSrc = computed(() => {
|
||||
<label class="m-3 align-middle text grow z-5 h-50px">
|
||||
<div>
|
||||
<span class="text-lg">
|
||||
{{ pkg?.name ?? 'Untitled' }}
|
||||
{{ pkg?.name.replaceAll('_', ' ') ?? 'Untitled' }}
|
||||
</span>
|
||||
<span
|
||||
v-if="pkg?.rmt?.deprecated"
|
||||
@ -62,7 +62,10 @@ const iconSrc = computed(() => {
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
v-if="hasFeature(pkg, Feature.Mu3IO)"
|
||||
v-if="
|
||||
hasFeature(pkg, Feature.Mu3IO) ||
|
||||
hasFeature(pkg, Feature.ChuniIO)
|
||||
"
|
||||
v-tooltip="'IO'"
|
||||
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"
|
||||
>
|
||||
</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
|
||||
v-if="showNamespace && pkg?.namespace"
|
||||
class="text-sm opacity-75"
|
||||
|
175
src/components/Onboarding.vue
Normal 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>
|
@ -6,6 +6,8 @@ const general = useGeneralStore();
|
||||
|
||||
defineProps({
|
||||
title: String,
|
||||
collapsed: Boolean,
|
||||
alwaysFound: Boolean,
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -13,7 +15,8 @@ defineProps({
|
||||
<Fieldset
|
||||
:legend="title"
|
||||
: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">
|
||||
<slot />
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import FileEditor from './FileEditor.vue';
|
||||
@ -7,6 +8,7 @@ import OptionCategory from './OptionCategory.vue';
|
||||
import OptionRow from './OptionRow.vue';
|
||||
import AimeOptions from './options/Aime.vue';
|
||||
import DisplayOptions from './options/Display.vue';
|
||||
import KeyboardOptions from './options/Keyboard.vue';
|
||||
import MiscOptions from './options/Misc.vue';
|
||||
import NetworkOptions from './options/Network.vue';
|
||||
import SegatoolsOptions from './options/Segatools.vue';
|
||||
@ -15,63 +17,56 @@ import { usePrfStore } from '../stores';
|
||||
|
||||
const prf = usePrfStore();
|
||||
|
||||
const audioModel = computed({
|
||||
const blacklistMinModel = computed({
|
||||
get() {
|
||||
return prf.current?.data.mu3_ini?.audio ?? null;
|
||||
},
|
||||
set(value: 'Shared' | 'Excl6Ch' | 'Excl2Ch') {
|
||||
if (prf.current!.data.mu3_ini === undefined) {
|
||||
prf.current!.data.mu3_ini = {};
|
||||
if (prf.current?.data.mu3_ini?.blacklist === undefined) {
|
||||
return null;
|
||||
}
|
||||
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({
|
||||
// get() {
|
||||
// if (prf.current?.data.mu3_ini?.blacklist === undefined) {
|
||||
// return null;
|
||||
// }
|
||||
// return prf.current?.data.mu3_ini?.blacklist[0];
|
||||
// },
|
||||
// set(value: number) {
|
||||
// if (prf.current!.data.mu3_ini === undefined) {
|
||||
// prf.current!.data.mu3_ini = {};
|
||||
// }
|
||||
// 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,
|
||||
// ];
|
||||
// },
|
||||
// });
|
||||
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) {
|
||||
prf.current!.data.mu3_ini!.blacklist = [
|
||||
prf.current!.data.mu3_ini!.blacklist?.[0] ?? 10000,
|
||||
value,
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
prf.reload();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SegatoolsOptions />
|
||||
<DisplayOptions v-if="prf.current!.meta.game === 'ongeki'" />
|
||||
<DisplayOptions />
|
||||
<NetworkOptions />
|
||||
<AimeOptions />
|
||||
<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
|
||||
title="Extensions"
|
||||
v-if="prf.current!.meta.game === 'ongeki'"
|
||||
@ -90,34 +85,59 @@ prf.reload();
|
||||
|
||||
<OptionRow
|
||||
title="Audio mode"
|
||||
tooltip="Exclusive 2-channel mode requires a patch"
|
||||
tooltip="Exclusive 2-channel mode requires 7EVENDAYSHOLIDAYS-ExclusiveAudio"
|
||||
>
|
||||
<SelectButton
|
||||
v-model="audioModel"
|
||||
v-model="prf.current!.data.mu3_ini!.audio"
|
||||
:options="[
|
||||
{ title: 'Shared', value: 'Shared' },
|
||||
{ title: 'Exclusive 6-channel', value: 'Excl6Ch' },
|
||||
{ title: 'Exclusive 2-channel', value: 'Excl2Ch' },
|
||||
]"
|
||||
:allow-empty="true"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
/></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"
|
||||
title="Song ID Blacklist"
|
||||
tooltip="Requires a patch"
|
||||
tooltip="Scores on charts within this ID range will not be saved nor uploaded"
|
||||
><InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
:min="10000"
|
||||
:min="9000"
|
||||
:max="99999"
|
||||
placeholder="10000"
|
||||
:use-grouping="false"
|
||||
:allow-empty="false"
|
||||
v-model="blacklistMinModel" />
|
||||
x
|
||||
~
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
@ -127,8 +147,38 @@ prf.reload();
|
||||
:use-grouping="false"
|
||||
:allow-empty="false"
|
||||
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>
|
||||
<KeyboardOptions />
|
||||
<StartlinerOptions />
|
||||
</template>
|
||||
|
||||
|
@ -8,6 +8,8 @@ const category = getCurrentInstance()?.parent?.parent?.parent?.parent; // yes in
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
tooltip: String,
|
||||
dangerousTooltip: String,
|
||||
greytext: String,
|
||||
});
|
||||
|
||||
const searched = computed(() => {
|
||||
@ -32,6 +34,17 @@ const searched = computed(() => {
|
||||
class="pi pi-question-circle ml-2"
|
||||
v-tooltip="tooltip"
|
||||
></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>
|
||||
|
||||
<slot />
|
||||
|
54
src/components/PatchEntry.vue
Normal 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>
|
60
src/components/PatchList.vue
Normal 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>
|
@ -22,7 +22,6 @@ const prf = usePrfStore();
|
||||
icon="pi pi-plus"
|
||||
class="chunithm-button profile-button"
|
||||
@click="() => prf.create('chunithm')"
|
||||
v-tooltip="'!!! Experimental !!!'"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
|
||||
|
@ -2,14 +2,16 @@
|
||||
import { ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { invoke } from '../invoke';
|
||||
import { useGeneralStore, usePrfStore } from '../stores';
|
||||
import { ProfileMeta } from '../types';
|
||||
|
||||
const prf = usePrfStore();
|
||||
const general = useGeneralStore();
|
||||
const prf = usePrfStore();
|
||||
const confirmDialog = useConfirm();
|
||||
|
||||
const isEditing = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
@ -55,6 +57,22 @@ const deleteProfile = async () => {
|
||||
await prf.reloadList();
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -91,7 +109,7 @@ const deleteProfile = async () => {
|
||||
size="small"
|
||||
class="self-center ml-2"
|
||||
style="width: 2rem; height: 2rem"
|
||||
@click="deleteProfile"
|
||||
@click="promptDeleteProfile"
|
||||
/>
|
||||
<Button
|
||||
rounded
|
||||
@ -114,17 +132,36 @@ const deleteProfile = async () => {
|
||||
@click="isEditing = true"
|
||||
/>
|
||||
<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
|
||||
icon="pi pi-folder"
|
||||
severity="help"
|
||||
aria-label="open-directory"
|
||||
aria-label="open-data-directory"
|
||||
size="small"
|
||||
class="self-center"
|
||||
style="width: 2rem; height: 2rem"
|
||||
@click="
|
||||
path
|
||||
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
|
||||
.then(open)
|
||||
.then(async (path) => {
|
||||
await invoke('open_file', { path });
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import ContextMenu from 'primevue/contextmenu';
|
||||
import ScrollPanel from 'primevue/scrollpanel';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import Onboarding from './Onboarding.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePrfStore } from '../stores';
|
||||
import { useClientStore, usePrfStore } from '../stores';
|
||||
|
||||
const prf = usePrfStore();
|
||||
const client = useClientStore();
|
||||
const confirmDialog = useConfirm();
|
||||
|
||||
type StartStatus = 'ready' | 'preparing' | 'running';
|
||||
@ -28,7 +28,7 @@ const startline = async (force: boolean, refresh: boolean) => {
|
||||
} else if ('MissingLocalPackage' in o) {
|
||||
return `Package missing: ${o.MissingLocalPackage}`;
|
||||
} else if ('MissingDependency' in o) {
|
||||
return `Dependency missing: ${o.MissingDependency}`;
|
||||
return `Dependency missing: ${(o.MissingDependency as string[]).join(' ')}`;
|
||||
} else if ('MissingTool' in o) {
|
||||
return `Tool missing: ${o.MissingTool}`;
|
||||
} else {
|
||||
@ -38,6 +38,8 @@ const startline = async (force: boolean, refresh: boolean) => {
|
||||
confirmDialog.require({
|
||||
message: message.join('\n'),
|
||||
header: 'Start check failed',
|
||||
acceptLabel: 'Run anyway',
|
||||
rejectLabel: 'Cancel',
|
||||
accept: () => {
|
||||
startline(true, refresh);
|
||||
},
|
||||
@ -85,14 +87,20 @@ listen('launch-end', () => {
|
||||
getCurrentWindow().setFocus();
|
||||
});
|
||||
|
||||
const messageSplit = (message: any) => {
|
||||
return message.message?.split('\n');
|
||||
const createShortcut = async () => {
|
||||
const current = prf.current;
|
||||
if (current !== null) {
|
||||
await invoke('create_shortcut', {
|
||||
profileMeta: current.meta,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: 'Refresh and start',
|
||||
icon: 'pi pi-sync',
|
||||
tooltip: 'test',
|
||||
command: async () => await startline(false, true),
|
||||
},
|
||||
{
|
||||
@ -100,6 +108,19 @@ const menuItems = [
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
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();
|
||||
|
||||
@ -107,49 +128,39 @@ const showContextMenu = (event: Event) => {
|
||||
event.preventDefault();
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Onboarding
|
||||
:visible="onboardingVisible"
|
||||
:first-time="onboardingFirstTime"
|
||||
:on-finish="
|
||||
() => {
|
||||
onboardingVisible = false;
|
||||
if (onboardingFirstTime === true) {
|
||||
startline(false, false);
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<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
|
||||
v-if="startStatus === 'ready'"
|
||||
v-tooltip="disabledTooltip"
|
||||
@ -159,7 +170,7 @@ const showContextMenu = (event: Event) => {
|
||||
aria-label="start"
|
||||
size="small"
|
||||
class="m-2.5"
|
||||
@click="startline(false, false)"
|
||||
@click="tryStart"
|
||||
@contextmenu="showContextMenu"
|
||||
/>
|
||||
<Button
|
||||
|
@ -20,17 +20,15 @@ const install = async () => {
|
||||
});
|
||||
} catch (err) {
|
||||
if (props.pkg !== undefined) {
|
||||
props.pkg.js.busy = false;
|
||||
props.pkg.js.downloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
//if (rv === 'Deferred') { /* download progress */ }
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
v-if="needsUpdate(pkg) && !pkg?.js.busy"
|
||||
v-if="needsUpdate(pkg) && !pkg?.js.downloading"
|
||||
rounded
|
||||
icon="pi pi-download"
|
||||
severity="success"
|
||||
|
@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Select from 'primevue/select';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import OptionCategory from '../OptionCategory.vue';
|
||||
import OptionRow from '../OptionRow.vue';
|
||||
import { invoke } from '../../invoke';
|
||||
import { usePkgStore, usePrfStore } from '../../stores';
|
||||
import { Feature } from '../../types';
|
||||
import { hasFeature, pkgKey } from '../../util';
|
||||
@ -15,6 +17,7 @@ const pkgs = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
|
||||
const aimeCode = ref('');
|
||||
const coms: Ref<{ [key: string]: number }> = ref({});
|
||||
|
||||
prf.reload();
|
||||
|
||||
@ -40,23 +43,31 @@ const aimeCodePaste = (ev: ClipboardEvent) => {
|
||||
.join('') ?? '';
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const load = async () => {
|
||||
const aime_path = await path.join(await prf.configDir, 'aime.txt');
|
||||
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>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="Aime">
|
||||
<OptionRow
|
||||
title="Aime emulation"
|
||||
tooltip="Aime plugins can be downloaded from the package store."
|
||||
title="Aime type"
|
||||
tooltip="Additional Aime plugins can be downloaded from the package store."
|
||||
>
|
||||
<Select
|
||||
v-model="prf.current!.data.sgt.aime"
|
||||
:options="[
|
||||
{ title: 'none', value: 'Disabled' },
|
||||
{ title: 'segatools built-in', value: 'BuiltIn' },
|
||||
{ title: 'hardware', value: 'Disabled' },
|
||||
{ title: 'segatools built-in emulation', value: 'BuiltIn' },
|
||||
...pkgs.byFeature(Feature.Aime).map((p) => {
|
||||
return {
|
||||
title: pkgKey(p),
|
||||
@ -71,11 +82,14 @@ const aimeCodePaste = (ev: ClipboardEvent) => {
|
||||
option-value="value"
|
||||
></Select>
|
||||
</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
|
||||
class="shrink"
|
||||
size="small"
|
||||
:disabled="prf.current!.data.sgt.aime === 'Disabled'"
|
||||
:maxlength="20"
|
||||
placeholder="00000000000000000000"
|
||||
v-model="aimeCodeModel"
|
||||
@ -108,5 +122,27 @@ const aimeCodePaste = (ev: ClipboardEvent) => {
|
||||
<ToggleSwitch v-model="prf.current!.data.sgt.amnet.physical" />
|
||||
</OptionRow>
|
||||
</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>
|
||||
</template>
|
||||
|
@ -63,6 +63,11 @@ const loadDisplays = () => {
|
||||
};
|
||||
|
||||
loadDisplays();
|
||||
|
||||
const game = prf.current!.meta.game;
|
||||
const isVertical = game === 'ongeki';
|
||||
const adjustableRez = game === 'ongeki';
|
||||
const canSkipPrimarySwitch = game === 'ongeki';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -80,7 +85,11 @@ loadDisplays();
|
||||
@show="loadDisplays"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow class="number-input" title="Game resolution">
|
||||
<OptionRow
|
||||
class="number-input"
|
||||
title="Game resolution"
|
||||
v-if="adjustableRez"
|
||||
>
|
||||
<InputNumber
|
||||
class="shrink"
|
||||
size="small"
|
||||
@ -118,12 +127,18 @@ loadDisplays();
|
||||
>
|
||||
<SelectButton
|
||||
v-model="prf.current!.data.display.rotation"
|
||||
:options="[
|
||||
{ title: 'Unchanged', value: 0 },
|
||||
{ title: 'Portrait', value: 90 },
|
||||
{ title: 'Portrait (flipped)', value: 270 },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
:options="
|
||||
isVertical
|
||||
? [
|
||||
{ title: 'Portrait', value: 90 },
|
||||
{ title: 'Portrait (flipped)', value: 270 },
|
||||
]
|
||||
: [
|
||||
{ title: 'Landscape', value: 0 },
|
||||
{ title: 'Landscape (flipped)', value: 180 },
|
||||
]
|
||||
"
|
||||
:allow-empty="true"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
:disabled="extraDisplayOptionsDisabled"
|
||||
@ -135,6 +150,7 @@ loadDisplays();
|
||||
title="Refresh Rate"
|
||||
>
|
||||
<InputNumber
|
||||
v-if="game === 'ongeki'"
|
||||
class="shrink"
|
||||
size="small"
|
||||
:min="60"
|
||||
@ -143,6 +159,18 @@ loadDisplays();
|
||||
v-model="prf.current!.data.display.frequency"
|
||||
: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
|
||||
title="Borderless fullscreen"
|
||||
@ -157,5 +185,41 @@ loadDisplays();
|
||||
v-model="prf.current!.data.display.borderless_fullscreen"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
160
src/components/options/Keyboard.vue
Normal 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>
|
@ -17,6 +17,7 @@ const prf = usePrfStore();
|
||||
title="More segatools options"
|
||||
tooltip="Advanced options not covered by STARTLINER"
|
||||
>
|
||||
<!-- <Button icon="pi pi-refresh" size="small" /> -->
|
||||
<FileEditor filename="segatools-base.ini" />
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
|
@ -1,15 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
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 OptionCategory from '../OptionCategory.vue';
|
||||
import OptionRow from '../OptionRow.vue';
|
||||
import { invoke } from '../../invoke';
|
||||
import { usePkgStore, usePrfStore } from '../../stores';
|
||||
import { Feature } from '../../types';
|
||||
import { pkgKey } from '../../util';
|
||||
|
||||
const prf = usePrfStore();
|
||||
const pkgs = usePkgStore();
|
||||
const confirmDialog = useConfirm();
|
||||
|
||||
const names = computed(() => {
|
||||
switch (prf.current?.meta.game) {
|
||||
@ -31,6 +36,21 @@ const names = computed(() => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -45,7 +65,10 @@ const names = computed(() => {
|
||||
extension="exe"
|
||||
:value="prf.current!.data.sgt.target"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.data.sgt.target = value)
|
||||
(value: string) => (
|
||||
(prf.current!.data.sgt.target = value),
|
||||
checkSegatoolsIni(value)
|
||||
)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
@ -96,23 +119,35 @@ const names = computed(() => {
|
||||
return { title: pkgKey(p), value: pkgKey(p) };
|
||||
})
|
||||
"
|
||||
placeholder="none"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
:title="names.io"
|
||||
v-if="prf.current?.meta.game === 'ongeki'"
|
||||
tooltip="IO plugins can be downloaded from the package store."
|
||||
>
|
||||
<Select
|
||||
v-model="prf.current!.data.sgt.io"
|
||||
placeholder="segatools built-in"
|
||||
v-model="prf.current!.data.sgt.io2"
|
||||
:options="[
|
||||
{ title: 'segatools built-in', value: null },
|
||||
...pkgs.byFeature(Feature.Mu3IO).map((p) => {
|
||||
return { title: pkgKey(p), value: pkgKey(p) };
|
||||
}),
|
||||
{ title: 'native io4', value: 'hardware' },
|
||||
{
|
||||
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-value="value"
|
||||
|
@ -25,6 +25,24 @@ const updatesModel = computed({
|
||||
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>
|
||||
@ -45,12 +63,31 @@ const updatesModel = computed({
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
title="Offline mode"
|
||||
tooltip="Disables the package store. Requires a restart."
|
||||
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>
|
||||
|
119
src/keyboard.ts
Normal 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;
|
||||
};
|
@ -17,6 +17,9 @@ app.use(pinia);
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Preset,
|
||||
options: {
|
||||
darkModeSelector: '.use-dark-mode',
|
||||
},
|
||||
},
|
||||
});
|
||||
app.use(ConfirmationService);
|
||||
|
114
src/stores.ts
@ -6,7 +6,12 @@ import { PhysicalSize, getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { invoke, invoke_nopopup } from './invoke';
|
||||
import { Dirs, Feature, Game, Package, Profile, ProfileMeta } from './types';
|
||||
import { changePrimaryColor, hasFeature, pkgKey } from './util';
|
||||
import {
|
||||
changePrimaryColor,
|
||||
hasFeature,
|
||||
pkgKey,
|
||||
shouldPreferDark,
|
||||
} from './util';
|
||||
|
||||
type InstallStatus = {
|
||||
pkg: string;
|
||||
@ -114,13 +119,13 @@ export const usePkgStore = defineStore('pkg', {
|
||||
listen<InstallStatus>('install-start', async (ev) => {
|
||||
const key = ev.payload.pkg;
|
||||
await this.reload(key);
|
||||
this.pkg[key].js.busy = true;
|
||||
this.pkg[key].js.downloading = true;
|
||||
});
|
||||
|
||||
listen<InstallStatus>('install-end', async (ev) => {
|
||||
const key = ev.payload.pkg;
|
||||
await this.reload(key);
|
||||
this.pkg[key].js.busy = false;
|
||||
this.pkg[key].js.downloading = false;
|
||||
});
|
||||
},
|
||||
|
||||
@ -147,17 +152,22 @@ export const usePkgStore = defineStore('pkg', {
|
||||
|
||||
async reloadWith(key: string, pkg: Package) {
|
||||
if (this.pkg[key] === undefined) {
|
||||
this.pkg[key] = { js: { busy: false } } as Package;
|
||||
this.pkg[key] = { js: { downloading: false } } as Package;
|
||||
} else {
|
||||
this.pkg[key].loc = null;
|
||||
this.pkg[key].rmt = null;
|
||||
}
|
||||
Object.assign(this.pkg[key], pkg);
|
||||
|
||||
if (!pkg.js) {
|
||||
pkg.js = { downloading: false };
|
||||
}
|
||||
|
||||
if (pkg.rmt !== null) {
|
||||
pkg.rmt.categories.forEach((c) =>
|
||||
this.availableCategories.add(c)
|
||||
);
|
||||
pkg.js.downloading = false;
|
||||
}
|
||||
},
|
||||
|
||||
@ -188,13 +198,21 @@ export const usePkgStore = defineStore('pkg', {
|
||||
force: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (pkg !== undefined) {
|
||||
pkg.js.busy = false;
|
||||
pkg.js.downloading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//if (rv === 'Deferred') { /* download progress */ }
|
||||
async installFromKey(key: string) {
|
||||
try {
|
||||
await invoke('install_package', {
|
||||
key,
|
||||
force: true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
||||
async updateAll() {
|
||||
@ -222,6 +240,12 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
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 p = (await invoke('get_current_profile')) as Profile;
|
||||
current.value = p;
|
||||
@ -314,7 +338,7 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
if (timeout !== null) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => invoke('save_current_profile'), 2000);
|
||||
timeout = setTimeout(() => invoke('save_current_profile'), 600);
|
||||
}
|
||||
});
|
||||
|
||||
@ -322,6 +346,7 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
current,
|
||||
list,
|
||||
isPkgEnabled,
|
||||
isPkgKeyEnabled,
|
||||
reload,
|
||||
create,
|
||||
rename,
|
||||
@ -332,6 +357,10 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
};
|
||||
});
|
||||
|
||||
export enum ClientData {
|
||||
Onboarded,
|
||||
}
|
||||
|
||||
export const useClientStore = defineStore('client', () => {
|
||||
type ScaleType = 's' | 'm' | 'l' | 'xl';
|
||||
const scaleFactor: Ref<ScaleType> = ref('s');
|
||||
@ -339,16 +368,23 @@ export const useClientStore = defineStore('client', () => {
|
||||
|
||||
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) =>
|
||||
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) * 760);
|
||||
const h = Math.floor(scaleValue(value) * 480);
|
||||
const w = Math.floor(_scaleValue(value) * 900);
|
||||
const h = Math.floor(_scaleValue(value) * 600);
|
||||
|
||||
let size = await window.innerSize();
|
||||
|
||||
@ -389,16 +425,29 @@ export const useClientStore = defineStore('client', () => {
|
||||
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: 'OfflineMode',
|
||||
field: 'offline_mode',
|
||||
});
|
||||
|
||||
enableAutoupdates.value = await invoke('get_global_config', {
|
||||
field: 'EnableAutoupdates',
|
||||
field: 'enable_autoupdates',
|
||||
});
|
||||
|
||||
verbose.value = await invoke('get_global_config', {
|
||||
field: 'verbose',
|
||||
});
|
||||
};
|
||||
|
||||
@ -415,6 +464,8 @@ export const useClientStore = defineStore('client', () => {
|
||||
w: Math.floor(size.width),
|
||||
h: Math.floor(size.height),
|
||||
},
|
||||
theme: theme.value,
|
||||
onboarded: onboarded.value,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -431,17 +482,42 @@ export const useClientStore = defineStore('client', () => {
|
||||
|
||||
const setOfflineMode = async (value: boolean) => {
|
||||
offlineMode.value = value;
|
||||
await invoke('set_global_config', { field: 'OfflineMode', value });
|
||||
await invoke('set_global_config', { field: 'offline_mode', value });
|
||||
};
|
||||
|
||||
const setAutoupdates = async (value: boolean) => {
|
||||
enableAutoupdates.value = value;
|
||||
await invoke('set_global_config', {
|
||||
field: 'EnableAutoupdates',
|
||||
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) {
|
||||
@ -453,12 +529,20 @@ export const useClientStore = defineStore('client', () => {
|
||||
scaleFactor,
|
||||
offlineMode,
|
||||
enableAutoupdates,
|
||||
verbose,
|
||||
theme,
|
||||
onboarded,
|
||||
timeout,
|
||||
scaleModel,
|
||||
_scaleValue,
|
||||
scaleValue,
|
||||
load,
|
||||
save,
|
||||
queueSave,
|
||||
setOfflineMode,
|
||||
setAutoupdates,
|
||||
setVerbose,
|
||||
setTheme,
|
||||
setOnboarded,
|
||||
};
|
||||
});
|
||||
|
71
src/types.ts
@ -19,7 +19,7 @@ export interface Package {
|
||||
icon: string;
|
||||
} | null;
|
||||
js: {
|
||||
busy: boolean;
|
||||
downloading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -30,13 +30,17 @@ export enum Feature {
|
||||
Mu3Hook = 1 << 3,
|
||||
Mu3IO = 1 << 4,
|
||||
ChusanHook = 1 << 5,
|
||||
ChuniIO = 1 << 6,
|
||||
Mempatcher = 1 << 7,
|
||||
GameDLL = 1 << 8,
|
||||
AmdDLL = 1 << 9,
|
||||
}
|
||||
|
||||
export type Status =
|
||||
| 'Unchecked'
|
||||
| 'Unsupported'
|
||||
| {
|
||||
OK: Feature;
|
||||
OK: [Feature, String, String];
|
||||
};
|
||||
|
||||
export type Game = 'ongeki' | 'chunithm';
|
||||
@ -53,12 +57,16 @@ export interface ProfileData {
|
||||
network: NetworkConfig;
|
||||
bepinex: BepInExConfig;
|
||||
mu3_ini: Mu3IniConfig | undefined;
|
||||
keyboard: KeyboardConfig | undefined;
|
||||
patches: {
|
||||
[key: string]: 'enabled' | { number: number } | { hex: Int8Array };
|
||||
};
|
||||
}
|
||||
|
||||
export interface SegatoolsConfig {
|
||||
target: string;
|
||||
hook: string | null;
|
||||
io: string | null;
|
||||
io2: 'segatools_built_in' | 'hardware' | { custom: string };
|
||||
amfs: string;
|
||||
option: string;
|
||||
appdata: string;
|
||||
@ -69,15 +77,18 @@ export interface SegatoolsConfig {
|
||||
addr: string;
|
||||
physical: boolean;
|
||||
};
|
||||
aime_port: number;
|
||||
}
|
||||
|
||||
export interface DisplayConfig {
|
||||
target: String;
|
||||
rez: [number, number];
|
||||
mode: 'Window' | 'Borderless' | 'Fullscreen';
|
||||
rotation: number;
|
||||
rotation: number | null;
|
||||
frequency: number;
|
||||
borderless_fullscreen: boolean;
|
||||
dont_switch_primary: boolean;
|
||||
monitor_index_override: number | null;
|
||||
}
|
||||
|
||||
export interface NetworkConfig {
|
||||
@ -96,9 +107,49 @@ export interface BepInExConfig {
|
||||
|
||||
export interface Mu3IniConfig {
|
||||
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 {
|
||||
meta: ProfileMeta;
|
||||
data: ProfileData;
|
||||
@ -111,3 +162,13 @@ export interface Dirs {
|
||||
data_dir: string;
|
||||
cache_dir: string;
|
||||
}
|
||||
|
||||
export interface Patch {
|
||||
id: string;
|
||||
name: string;
|
||||
tooltip: string;
|
||||
type: undefined | 'number';
|
||||
default: number;
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
19
src/util.ts
@ -52,6 +52,23 @@ export const hasFeature = (pkg: Package | undefined, feature: Feature) => {
|
||||
pkg.loc !== null &&
|
||||
pkg.loc !== undefined &&
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
@ -30,4 +30,7 @@ export default defineConfig(async () => ({
|
||||
ignored: ['**/rust/**'],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
chunkSizeWarningLimit: 1024,
|
||||
},
|
||||
}));
|
||||
|