Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
104
CHANGELOG.md
Normal file
@ -0,0 +1,104 @@
|
||||
## 0.12.0
|
||||
|
||||
- Ongeki: cache and mu3.ini config are now split per-profile (requires mu3-mods 3.7+)
|
||||
- Ongeki: added the few config options of mu3.ini that aren't available in TestMenuConfig, or require a restart
|
||||
- Chunithm: added Lumi+ patches
|
||||
- Added support for a non-standard `games` manifest entry intended for local packages
|
||||
- Expected to be an array containing "ongeki", "chunithm" or both
|
||||
- Example: { "games": ["ongeki"] }
|
||||
- Added a button linking to the profile config folder
|
||||
- Fixed the button linking to the data folder showing up when the folder does not exist
|
||||
- Uninstalled tool packages are no longer automatically deselected, as that caused issues
|
||||
|
||||
## 0.11.1
|
||||
|
||||
- Improved help pages
|
||||
|
||||
## 0.11.0
|
||||
|
||||
- Added help pages
|
||||
|
||||
## 0.10.1
|
||||
|
||||
- Fixed the order of cells in the CHUNITHM keyboard
|
||||
- Fixed numpad bindings with numlock disabled
|
||||
- Disabled primary monitor cleanup when "don't switch primary monitor" is enabled
|
||||
|
||||
## 0.10.0
|
||||
|
||||
- Added a global progress bar
|
||||
- Fixed issues with downloading under certain conditions
|
||||
|
||||
## 0.9.0
|
||||
|
||||
- Added a light/dark theme switcher
|
||||
|
||||
## 0.8.1
|
||||
|
||||
- Hotfixed the program failing to launch if the data dir hadn't already been created
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- Added support for ChuniIO
|
||||
- CHUNITHM support is now complete
|
||||
- Added a context menu option to create a desktop shortcut
|
||||
- Added a confirmation prompt before deleting a profile
|
||||
- Removed Slow
|
||||
|
||||
## 0.7.1
|
||||
|
||||
- Hotfixed amdaemon crashing at launch
|
||||
- Greyed out packages currently incompatible with STARTLINER
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- Hopefully fixed issues with the download button
|
||||
- Added a verbose logging option
|
||||
- Added an info tab
|
||||
- Instead of auto-installing segatools & mempatcher at launch, the package store now shows a "install recommended" button
|
||||
|
||||
## 0.6.1
|
||||
|
||||
- Added support for O.N.G.E.K.I. English Translation
|
||||
- Disabled the icon buttons as they broke at some point
|
||||
|
||||
## 0.6.0
|
||||
|
||||
- Chunithm: added support for DLLs (saekawa, mempatcher)
|
||||
- Chunithm: added a patch interface
|
||||
- Chunithm: added display settings
|
||||
- Chunithm: removed split IR
|
||||
- Both games: added hardware aime reader support
|
||||
- Added an update progress bar
|
||||
|
||||
## 0.5.0
|
||||
|
||||
- Added a keyboard configuration UI (for both games)
|
||||
- Added a prompt after selecting `mu3.exe`/`chusanApp.exe` in the file picker, allowing you to copy much of the data from `segatools.ini`, if it already exists.
|
||||
- Added an option to not switch the primary monitor.
|
||||
- This is not recommended to use but it may help when the primary monitor switcher doesn't work correctly.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- New error dialog.
|
||||
- Added tooltips for IO, Aime, Hook.
|
||||
- Added a welcome message.
|
||||
- Added a separate auto-update toggle.
|
||||
- Fixed a display bug with the offline mode toggle.
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- Added UI scaling, offline mode, and an 'update all' button
|
||||
- First public release
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Added a context menu for the start button with additional launch options
|
||||
- Added an audio mode button for ongeki
|
||||
- Fixed the launcher freezing while the game is running
|
||||
- Probably added auto-updates
|
||||
|
||||
## 0.1.0
|
||||
|
||||
⚠️ this release is incomplete and potentially cursed
|
||||
it's more of a preview of a preview
|
61
README.md
@ -1,19 +1,19 @@
|
||||
# STARTLINER
|
||||
|
||||
A simple and easy to use launcher, configuration tool and mod manager for O.N.G.E.K.I. and CHUNITHM, 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 [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:
|
||||
@ -23,37 +23,22 @@ 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)
|
||||
|
35
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=="],
|
||||
@ -292,6 +297,12 @@
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@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=="],
|
||||
@ -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=="],
|
||||
@ -726,6 +745,8 @@
|
||||
|
||||
"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=="],
|
||||
|
@ -10,6 +10,7 @@
|
||||
"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",
|
||||
@ -23,6 +24,8 @@
|
||||
"@tauri-apps/plugin-shell": "~2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.2",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.3.3",
|
||||
|
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 |
3
rust/Cargo.lock
generated
@ -803,7 +803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -4730,6 +4730,7 @@ dependencies = [
|
||||
"humantime",
|
||||
"junction",
|
||||
"log",
|
||||
"open",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rust-ini",
|
||||
|
@ -45,6 +45,7 @@ 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"
|
||||
@ -52,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,4 +1,5 @@
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::time::SystemTime;
|
||||
use crate::model::config::GlobalConfig;
|
||||
use crate::model::patch::PatchFileVec;
|
||||
use crate::pkg::{Feature, Status};
|
||||
@ -7,6 +8,7 @@ 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 {
|
||||
@ -34,6 +36,8 @@ 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
|
||||
@ -127,4 +131,38 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -323,9 +323,10 @@ pub async fn duplicate_profile(profile: ProfileMeta) -> Result<(), String> {
|
||||
pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMeta) -> Result<(), String> {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -396,6 +397,13 @@ pub async fn load_segatools_ini(state: State<'_, Mutex<AppData>>, path: PathBuf)
|
||||
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");
|
||||
@ -414,7 +422,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -425,7 +434,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())
|
||||
}
|
||||
@ -472,6 +482,18 @@ 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());
|
||||
|
@ -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,10 +49,16 @@ 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);
|
||||
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?;
|
||||
|
@ -9,15 +9,14 @@ mod modules;
|
||||
mod profiles;
|
||||
mod patcher;
|
||||
|
||||
use std::{sync::OnceLock, time::SystemTime};
|
||||
use std::sync::OnceLock;
|
||||
use anyhow::anyhow;
|
||||
use closure::closure;
|
||||
use appdata::{AppData, ToggleAction};
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
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 tokio::{fs, sync::Mutex, try_join};
|
||||
@ -48,42 +47,7 @@ pub async fn run(_args: Vec<String>) {
|
||||
|
||||
util::init_dirs(&apph);
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
fern_builder = fern_builder.level(log::LevelFilter::Debug);
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
if std::env::var("DEBUG_LOG").is_ok() {
|
||||
fern_builder = fern_builder.level(log::LevelFilter::Debug);
|
||||
} else {
|
||||
fern_builder = fern_builder.level(log::LevelFilter::Info);
|
||||
}
|
||||
}
|
||||
|
||||
fern_builder.apply()?;
|
||||
let mut app_data = AppData::new(app.handle().clone());
|
||||
|
||||
log::info!(
|
||||
"running from {}",
|
||||
@ -93,7 +57,6 @@ pub async fn run(_args: Vec<String>) {
|
||||
.unwrap_or_default()
|
||||
);
|
||||
|
||||
let mut app_data = AppData::new(app.handle().clone());
|
||||
let start_immediately;
|
||||
|
||||
if let Ok(matches) = app.cli().matches() {
|
||||
@ -103,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(900f64, 480f64)
|
||||
.min_inner_size(900f64, 480f64)
|
||||
.build()?;
|
||||
open_window(apph.clone())?;
|
||||
start_immediately = false;
|
||||
}
|
||||
|
||||
@ -144,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);
|
||||
});
|
||||
}));
|
||||
|
||||
@ -168,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());
|
||||
@ -194,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 {
|
||||
@ -236,6 +204,7 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::sync_current_profile,
|
||||
cmd::save_current_profile,
|
||||
cmd::load_segatools_ini,
|
||||
cmd::create_shortcut,
|
||||
|
||||
cmd::get_global_config,
|
||||
cmd::set_global_config,
|
||||
@ -244,6 +213,8 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::list_platform_capabilities,
|
||||
cmd::list_directories,
|
||||
cmd::file_exists,
|
||||
cmd::open_file,
|
||||
cmd::get_changelog,
|
||||
|
||||
cmd::list_com_ports,
|
||||
|
||||
@ -264,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);
|
||||
}
|
||||
});
|
||||
@ -326,7 +298,7 @@ async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
|
||||
update.download_and_install(
|
||||
|chunk_length, content_length| {
|
||||
downloaded += chunk_length;
|
||||
_ = app.emit("update-progress", (chunk_length as f64) / (content_length.unwrap_or(u64::MAX) as f64));
|
||||
_ = app.emit("update-progress", (downloaded as f64) / (content_length.unwrap_or(u64::MAX) as f64));
|
||||
},
|
||||
|| {
|
||||
log::info!("download finished");
|
||||
@ -355,5 +327,16 @@ async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
|
||||
|
||||
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",
|
||||
|
@ -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,18 +35,19 @@ 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>,
|
||||
}
|
||||
@ -51,6 +61,7 @@ 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"),
|
||||
@ -69,7 +80,8 @@ 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),
|
||||
@ -77,11 +89,7 @@ pub struct Display {
|
||||
pub rotation: Option<i32>,
|
||||
pub frequency: i32,
|
||||
pub borderless_fullscreen: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub dont_switch_primary: bool,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub monitor_index_override: Option<i32>,
|
||||
}
|
||||
|
||||
@ -113,6 +121,7 @@ pub enum NetworkType {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Network {
|
||||
pub network_type: NetworkType,
|
||||
|
||||
@ -127,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,
|
||||
@ -156,19 +167,31 @@ 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,
|
||||
}
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
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 {
|
||||
#[serde(default = "default_true")] pub enabled: bool,
|
||||
pub enabled: bool,
|
||||
pub use_mouse: bool,
|
||||
pub coin: i32,
|
||||
pub svc: i32,
|
||||
@ -208,8 +231,9 @@ impl Default for OngekiKeyboard {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct ChunithmKeyboard {
|
||||
#[serde(default = "default_true")] pub enabled: bool,
|
||||
pub enabled: bool,
|
||||
pub coin: i32,
|
||||
pub svc: i32,
|
||||
pub test: i32,
|
||||
|
@ -22,4 +22,5 @@ pub struct V1Version {
|
||||
pub icon: String,
|
||||
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||
pub download_url: String,
|
||||
pub file_size: i64,
|
||||
}
|
@ -6,14 +6,14 @@ use tauri::{AppHandle, Listener};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DisplayInfo {
|
||||
pub primary: String,
|
||||
pub primary: Option<String>,
|
||||
pub set: Option<DisplaySet>,
|
||||
}
|
||||
|
||||
impl Default for DisplayInfo {
|
||||
fn default() -> Self {
|
||||
DisplayInfo {
|
||||
primary: "default".to_owned(),
|
||||
primary: None,
|
||||
set: query_displays().ok(),
|
||||
}
|
||||
}
|
||||
@ -60,7 +60,7 @@ impl Display {
|
||||
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
||||
|
||||
let res = DisplayInfo {
|
||||
primary: primary.name().to_owned(),
|
||||
primary: if self.dont_switch_primary { None } else { Some(primary.name().to_owned()) },
|
||||
set: Some(display_set.clone()),
|
||||
};
|
||||
|
||||
@ -132,12 +132,14 @@ impl Display {
|
||||
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()?;
|
||||
|
@ -75,6 +75,12 @@ impl Keyboard {
|
||||
|
||||
// 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 {
|
||||
@ -95,7 +101,19 @@ impl Keyboard {
|
||||
.set("mouse", if kb.use_mouse { "1" } else { "0" });
|
||||
} else {
|
||||
ini.with_section(Some("io4"))
|
||||
.set("enable", "0");
|
||||
.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) => {
|
||||
@ -109,14 +127,21 @@ impl Keyboard {
|
||||
ini.with_section(Some("io3"))
|
||||
.set("test", kb.test.to_string())
|
||||
.set("service", kb.svc.to_string())
|
||||
.set("coin", kb.coin.to_string())
|
||||
.set("ir", "0");
|
||||
.set("coin", kb.coin.to_string());
|
||||
} else {
|
||||
ini.with_section(Some("io4"))
|
||||
.set("enable", "0");
|
||||
ini.with_section(Some("slider"))
|
||||
.set("enable", "0");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(())
|
||||
|
@ -14,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?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +36,13 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
|
||||
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).join("option");
|
||||
|
@ -1,34 +1,34 @@
|
||||
use std::path::{PathBuf, Path};
|
||||
use anyhow::{anyhow, Result};
|
||||
use ini::Ini;
|
||||
use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, ConfigHookAuth, Game}, profile::{Aime, Segatools}}, 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;
|
||||
}
|
||||
} else {
|
||||
$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 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),
|
||||
_ => {},
|
||||
}
|
||||
// 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");
|
||||
@ -134,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"))
|
||||
@ -141,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 {
|
||||
@ -149,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);
|
||||
|
||||
|
@ -15,10 +15,14 @@ impl PatchFileVec {
|
||||
let mut res = Vec::new();
|
||||
for f in std::fs::read_dir(path)? {
|
||||
let f = f?;
|
||||
let f = f.path();
|
||||
res.push(
|
||||
serde_json5::from_str::<PatchFile>(&std::fs::read_to_string(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))
|
||||
}
|
||||
@ -29,8 +33,8 @@ impl PatchFileVec {
|
||||
let mut res = Vec::new();
|
||||
for pfile in &self.0 {
|
||||
for plist in &pfile.0 {
|
||||
log::debug!("checking {}", plist.sha256);
|
||||
if plist.sha256 == checksum {
|
||||
log::debug!("checking {}", plist.sha256.to_ascii_lowercase());
|
||||
if plist.sha256.to_ascii_lowercase() == checksum {
|
||||
let mut cloned = plist.clone().patches;
|
||||
res.append(&mut cloned);
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ pub struct Remote {
|
||||
pub nsfw: bool,
|
||||
pub categories: Vec<String>,
|
||||
pub dependencies: BTreeSet<PkgKey>,
|
||||
pub file_size: i64,
|
||||
}
|
||||
|
||||
impl PkgKey {
|
||||
@ -112,13 +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, source: PackageSource) -> 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)?;
|
||||
|
||||
@ -128,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(),
|
||||
@ -144,7 +146,7 @@ impl Package {
|
||||
}),
|
||||
rmt: None,
|
||||
source
|
||||
})
|
||||
}, mft.games))
|
||||
}
|
||||
|
||||
pub fn key(&self) -> PkgKey {
|
||||
@ -221,9 +223,15 @@ 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), DLLs { game: None, amd: None }); //Unchecked
|
||||
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;
|
||||
@ -233,20 +241,14 @@ impl Package {
|
||||
if id == "rainycolor" {
|
||||
flags |= Feature::Mod;
|
||||
} else if id == "segatools" {
|
||||
// Multiple features in the same dll (yubideck etc.) should be supported at some point
|
||||
if let Some(serde_json::Value::String(module)) = installer.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;
|
||||
} else if module == "chuniio" {
|
||||
flags |= Feature::ChuniIO;
|
||||
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" {
|
||||
@ -274,4 +276,16 @@ impl Package {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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};
|
||||
@ -22,7 +21,7 @@ pub struct PackageStore {
|
||||
offline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Payload {
|
||||
pub pkg: PkgKey
|
||||
}
|
||||
@ -84,7 +83,7 @@ impl PackageStore {
|
||||
|
||||
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, PackageSource::Rainy).await {
|
||||
if let Ok((pkg, _)) = Package::from_dir(dir, PackageSource::Rainy).await {
|
||||
self.update_nonremote(key, pkg);
|
||||
} else {
|
||||
log::error!("couldn't reload {}", key);
|
||||
@ -103,7 +102,13 @@ impl PackageStore {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -153,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 ],
|
||||
}
|
||||
});
|
||||
@ -207,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);
|
||||
@ -243,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 {
|
||||
@ -269,48 +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?;
|
||||
// todo search for the proper dll
|
||||
Self::clean_up_file(&path, "saekawa.dll", false).await?;
|
||||
Self::clean_up_file(&path, "mempatcher32.dll", false).await?;
|
||||
Self::clean_up_file(&path, "mempatcher64.dll", 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,6 +1,6 @@
|
||||
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
|
||||
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
|
||||
use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
use tauri::Emitter;
|
||||
use std::process::Stdio;
|
||||
use crate::model::profile::BepInEx;
|
||||
@ -28,7 +28,7 @@ impl Profile {
|
||||
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()))
|
||||
@ -43,6 +43,12 @@ impl Profile {
|
||||
std::fs::create_dir_all(p.config_dir())?;
|
||||
std::fs::create_dir_all(p.data_dir())?;
|
||||
|
||||
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"))?,
|
||||
@ -59,8 +65,26 @@ impl Profile {
|
||||
log::debug!("{:?}", data);
|
||||
|
||||
// Backwards compat
|
||||
if game == Game::Ongeki && data.keyboard.is_none() {
|
||||
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default()));
|
||||
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() {
|
||||
@ -94,7 +118,7 @@ impl Profile {
|
||||
}
|
||||
std::fs::write(&path, s)
|
||||
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
|
||||
log::info!("profile written to {:?}", path);
|
||||
log::info!("profile saved to {:?}", path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -112,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 {
|
||||
@ -197,7 +221,7 @@ 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 {
|
||||
@ -273,6 +297,18 @@ impl Profile {
|
||||
"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)
|
||||
.raw_arg("-d")
|
||||
.raw_arg("-k")
|
||||
@ -393,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 {
|
||||
|
@ -154,4 +154,63 @@ impl PathStr for PathBuf {
|
||||
|
||||
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(())
|
||||
}
|
@ -1,4 +1,154 @@
|
||||
[
|
||||
{
|
||||
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',
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "STARTLINER",
|
||||
"version": "0.6.0",
|
||||
"version": "0.12.0",
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ 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';
|
||||
@ -27,7 +28,9 @@ import {
|
||||
usePrfStore,
|
||||
} from '../stores';
|
||||
import { Dirs } from '../types';
|
||||
import { messageSplit } from '../util';
|
||||
import { messageSplit, shouldPreferDark } from '../util';
|
||||
|
||||
document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark());
|
||||
|
||||
const pkg = usePkgStore();
|
||||
const prf = usePrfStore();
|
||||
@ -36,7 +39,8 @@ 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);
|
||||
@ -62,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);
|
||||
@ -87,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>
|
||||
@ -101,6 +156,16 @@ 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
|
||||
@ -129,7 +194,7 @@ 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>
|
||||
@ -149,42 +214,50 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
||||
: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"></div></Tab>
|
||||
<Tab :disabled="isProfileDisabled" :value="0"
|
||||
<Tab value="users"><div class="pi pi-users"></div></Tab>
|
||||
<Tab :disabled="isProfileDisabled" value="loc"
|
||||
><div class="pi pi-box"></div
|
||||
></Tab>
|
||||
<Tab v-if="prf.current?.meta.game === 'chunithm'" :value="4"
|
||||
<Tab
|
||||
v-if="
|
||||
prf.current?.meta.game === 'chunithm' &&
|
||||
prf.current.data.sgt.target.length > 0
|
||||
"
|
||||
value="patches"
|
||||
><div class="pi pi-ticket"></div
|
||||
></Tab>
|
||||
<Tab
|
||||
v-if="pkg.networkStatus === 'online'"
|
||||
:disabled="isProfileDisabled"
|
||||
:value="1"
|
||||
value="rmt"
|
||||
><div class="pi pi-download"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isProfileDisabled" :value="2"
|
||||
<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="[0, 1, 2].includes(currentTab as number)"
|
||||
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"
|
||||
@ -234,28 +307,19 @@ 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">
|
||||
<TabPanel value="patches">
|
||||
<PatchList
|
||||
v-if="
|
||||
pkg.hasLocal('mempatcher-mempatcher') &&
|
||||
@ -266,10 +330,27 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
||||
<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"
|
||||
@ -336,4 +417,23 @@ body {
|
||||
.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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<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';
|
||||
|
||||
@ -15,9 +16,51 @@ const handleKey = (
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
const keycode = toKeycode(event.code);
|
||||
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 {
|
||||
@ -75,7 +118,7 @@ const handleMouse = (
|
||||
}
|
||||
};
|
||||
|
||||
const getKey = (key: keyof OngekiButtons, index?: number) =>
|
||||
const getKey = (key: keyof OngekiButtons, index?: number): any =>
|
||||
computed(() => {
|
||||
const data = prf.current!.data.keyboard?.data as any;
|
||||
const keycode =
|
||||
@ -85,147 +128,45 @@ const getKey = (key: keyof OngekiButtons, index?: number) =>
|
||||
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '–';
|
||||
});
|
||||
|
||||
const KEY_MAP: { [key: number]: string } = {
|
||||
1: 'M1',
|
||||
2: 'M2',
|
||||
4: 'M3',
|
||||
5: 'M4',
|
||||
6: 'M5',
|
||||
8: 'Backspace',
|
||||
9: 'Tab',
|
||||
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',
|
||||
};
|
||||
|
||||
const fromKeycode = (keyCode: number): string | null => {
|
||||
return KEY_MAP[keyCode] ?? null;
|
||||
};
|
||||
|
||||
const toKeycode = (key: string): number | null => {
|
||||
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
|
||||
return res ? parseInt(res) : null;
|
||||
};
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
small: Boolean,
|
||||
verySmall: 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 ? '3em' : '5em',
|
||||
height: small ? '3em' : tall ? '10em' : '5em',
|
||||
fontSize: small ? '0.9em' : '1em',
|
||||
width: small ? '2.8rem' : '5rem',
|
||||
height: small ? '2.8rem' : tall ? '10rem' : '5rem',
|
||||
fontSize,
|
||||
backgroundColor: color,
|
||||
}"
|
||||
unstyled
|
||||
class="text-center buttoninputtext"
|
||||
v-tooltip="tooltip"
|
||||
v-tooltip="tooltip ? `${tooltip}: ${modelValue}` : undefined"
|
||||
@contextmenu.prevent="() => {}"
|
||||
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
|
||||
@mousedown="
|
||||
@ -233,7 +174,7 @@ defineProps({
|
||||
handleMouse(button as keyof OngekiButtons, ev, index)
|
||||
"
|
||||
@focusout="() => (hasClickedM1Once = false)"
|
||||
:model-value="getKey(button as keyof OngekiButtons, index) as any"
|
||||
:model-value="modelValue"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -241,5 +182,7 @@ defineProps({
|
||||
.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"
|
||||
|
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>
|
@ -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';
|
||||
@ -16,53 +17,35 @@ 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>
|
||||
@ -102,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"
|
||||
@ -139,7 +147,36 @@ 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 />
|
||||
|
@ -3,7 +3,7 @@ import InputNumber from 'primevue/inputnumber';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import OptionRow from './OptionRow.vue';
|
||||
import { usePrfStore } from '../stores';
|
||||
import { Patch } from '@/types';
|
||||
import { Patch } from '../types';
|
||||
|
||||
const prf = usePrfStore();
|
||||
|
||||
|
@ -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>
|
||||
|
@ -5,10 +5,12 @@ import ContextMenu from 'primevue/contextmenu';
|
||||
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';
|
||||
@ -26,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 {
|
||||
@ -85,10 +87,20 @@ listen('launch-end', () => {
|
||||
getCurrentWindow().setFocus();
|
||||
});
|
||||
|
||||
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),
|
||||
},
|
||||
{
|
||||
@ -96,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();
|
||||
|
||||
@ -103,9 +128,38 @@ 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" />
|
||||
<Button
|
||||
v-if="startStatus === 'ready'"
|
||||
@ -116,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"
|
||||
|
@ -84,6 +84,7 @@ load();
|
||||
</OptionRow>
|
||||
<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
|
||||
|
@ -11,7 +11,10 @@ const prf = usePrfStore();
|
||||
|
||||
<template>
|
||||
<OptionCategory title="Keyboard">
|
||||
<OptionRow title="Enable">
|
||||
<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
|
||||
@ -30,6 +33,7 @@ const prf = usePrfStore();
|
||||
/>
|
||||
</OptionRow>
|
||||
<div
|
||||
v-if="prf.current!.data.keyboard!.data.enabled"
|
||||
:style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`"
|
||||
>
|
||||
<div
|
||||
@ -120,7 +124,7 @@ const prf = usePrfStore();
|
||||
<div
|
||||
v-for="idx in Array(16)
|
||||
.fill(0)
|
||||
.map((_, i) => 16 - i)"
|
||||
.map((_, i) => 32 - 2 * i - 1)"
|
||||
>
|
||||
<KeyboardKey
|
||||
button="cell"
|
||||
@ -138,7 +142,7 @@ const prf = usePrfStore();
|
||||
<div
|
||||
v-for="idx in Array(16)
|
||||
.fill(0)
|
||||
.map((_, i) => 32 - i)"
|
||||
.map((_, i) => 32 - 2 * i)"
|
||||
>
|
||||
<KeyboardKey
|
||||
button="cell"
|
||||
|
@ -119,23 +119,35 @@ const checkSegatoolsIni = async (target: string) => {
|
||||
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);
|
||||
|
107
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() {
|
||||
@ -320,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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -339,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');
|
||||
@ -346,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) * 900);
|
||||
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();
|
||||
|
||||
@ -396,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',
|
||||
});
|
||||
};
|
||||
|
||||
@ -422,6 +464,8 @@ export const useClientStore = defineStore('client', () => {
|
||||
w: Math.floor(size.width),
|
||||
h: Math.floor(size.height),
|
||||
},
|
||||
theme: theme.value,
|
||||
onboarded: onboarded.value,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -438,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) {
|
||||
@ -460,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,
|
||||
};
|
||||
});
|
||||
|
@ -19,7 +19,7 @@ export interface Package {
|
||||
icon: string;
|
||||
} | null;
|
||||
js: {
|
||||
busy: boolean;
|
||||
downloading: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ export interface ProfileData {
|
||||
export interface SegatoolsConfig {
|
||||
target: string;
|
||||
hook: string | null;
|
||||
io: string | null;
|
||||
io2: 'segatools_built_in' | 'hardware' | { custom: string };
|
||||
amfs: string;
|
||||
option: string;
|
||||
appdata: string;
|
||||
@ -107,7 +107,10 @@ 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 {
|
||||
|
13
src/util.ts
@ -59,3 +59,16 @@ export const hasFeature = (pkg: Package | undefined, feature: 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,
|
||||
},
|
||||
}));
|
||||
|