13 Commits
0.5.0 ... 0.7.1

Author SHA1 Message Date
f588892b05 feat: verbose toggle, info tab, many misc fixes 2025-04-14 19:20:08 +00:00
37df371006 fix: support for translationengine 2025-04-13 23:24:32 +00:00
7ea31c7a87 fix: misc fixes for 0.6.0 2025-04-13 21:02:04 +00:00
073ff8cfb8 fix: native-mod -> native_mod 2025-04-13 20:25:22 +00:00
d93118683d fix: release build 2025-04-13 19:53:12 +00:00
7f68b8d28b feat: amdaemon.exe patching 2025-04-13 19:28:06 +00:00
4247e19996 feat: chusanApp.exe patching 2025-04-13 18:15:41 +00:00
6270fce05f feat: display module for chunithm
Also make the progress bar all shiny
2025-04-12 17:33:39 +00:00
7db36b7bc0 fix: remove update check from the ui
it is no longer necessary
2025-04-12 11:59:02 +00:00
f3016eb029 feat: autoupdate ui 2025-04-12 10:53:06 +00:00
28269c5d75 feat: hardware aime 2025-04-11 23:07:45 +00:00
1a68eda8c1 feat: partial support for patches 2025-04-11 15:27:13 +00:00
b9a40d44a8 fix: remove split_ir 2025-04-10 19:33:54 +00:00
55 changed files with 2459 additions and 1034 deletions

52
CHANGELOG.md Normal file
View File

@ -0,0 +1,52 @@
## 0.7.0
- Hopefully fixed issues with the download button
- Added a verbose logging option
- Added an info tab
- Instead of auto-installing segatools & mempatcher at launch, the package store now shows a "install recommended" button
## 0.6.1
- Added support for O.N.G.E.K.I. English Translation
- Disabled the icon buttons as they broke at some point
## 0.6.0
- Chunithm: added support for DLLs (saekawa, mempatcher)
- Chunithm: added a patch interface
- Chunithm: added display settings
- Chunithm: removed split IR
- Both games: added hardware aime reader support
- Added an update progress bar
## 0.5.0
- Added a keyboard configuration UI (for both games)
- Added a prompt after selecting `mu3.exe`/`chusanApp.exe` in the file picker, allowing you to copy much of the data from `segatools.ini`, if it already exists.
- Added an option to not switch the primary monitor.
- This is not recommended to use but it may help when the primary monitor switcher doesn't work correctly.
## 0.4.0
- New error dialog.
- Added tooltips for IO, Aime, Hook.
- Added a welcome message.
- Added a separate auto-update toggle.
- Fixed a display bug with the offline mode toggle.
## 0.3.0
- Added UI scaling, offline mode, and an 'update all' button
- First public release
## 0.2.0
- Added a context menu for the start button with additional launch options
- Added an audio mode button for ongeki
- Fixed the launcher freezing while the game is running
- Probably added auto-updates
## 0.1.0
⚠️ this release is incomplete and potentially cursed
it's more of a preview of a preview

View File

@ -16,7 +16,7 @@ Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome
## Usage
Download a prebuilt binary from [Modding Re:Fresh](https://discord.gg/jxvzHjjEmc) or build it yourself:
Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:
```sh
bun install

View File

@ -1,11 +1,9 @@
### 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)

View File

@ -4,10 +4,11 @@
"": {
"name": "startliner",
"dependencies": {
"@f3ve/vue-markdown-it": "^0.2.3",
"@mdi/font": "7.4.47",
"@primevue/forms": "^4.3.3",
"@primevue/themes": "^4.3.3",
"@tailwindcss/vite": "^4.1.2",
"@tailwindcss/vite": "^4.1.3",
"@tauri-apps/api": "^2.4.1",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "~2.2.1",
@ -17,29 +18,31 @@
"@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"pinia": "^3.0.1",
"@types/markdown-it": "^14.1.2",
"markdown-it": "^14.1.0",
"pinia": "^3.0.2",
"primeicons": "^7.0.0",
"primevue": "^4.3.3",
"roboto-fontface": "^0.10.0",
"tailwindcss": "^4.1.2",
"tailwindcss": "^4.1.3",
"tailwindcss-primeui": "^0.4.0",
"vue": "^3.5.13",
"vuetify": "^3.8.0",
"vuetify": "^3.8.1",
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tsconfig/node22": "^22.0.1",
"@types/node": "^22.14.0",
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3",
"@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^7.0.2",
"sass": "1.77.8",
"sass-embedded": "^1.86.3",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^0.27.5",
"vite": "^6.2.5",
"vite": "^6.2.6",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8",
},
@ -132,6 +135,8 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
"@f3ve/vue-markdown-it": ["@f3ve/vue-markdown-it@0.2.3", "", { "dependencies": { "markdown-it": "^14.1.0" }, "peerDependencies": { "vue": "^3.3.4" } }, "sha512-v0VNd7wb55kwsUUy3n6DLI9+0FYSG0PrCTD3bWuSRo6WS3OHD5wghh/aHzebVdsVkSBXfVpiEUlMA3DrxLs7Lw=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
@ -216,33 +221,33 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.6", "", { "os": "win32", "cpu": "x64" }, "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.2", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.2" } }, "sha512-ZwFnxH+1z8Ehh8bNTMX3YFrYdzAv7JLY5X5X7XSFY+G9QGJVce/P9xb2mh+j5hKt8NceuHmdtllJvAHWKtsNrQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.3", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.3" } }, "sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.2", "@tailwindcss/oxide-darwin-arm64": "4.1.2", "@tailwindcss/oxide-darwin-x64": "4.1.2", "@tailwindcss/oxide-freebsd-x64": "4.1.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.2", "@tailwindcss/oxide-linux-arm64-musl": "4.1.2", "@tailwindcss/oxide-linux-x64-gnu": "4.1.2", "@tailwindcss/oxide-linux-x64-musl": "4.1.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.2", "@tailwindcss/oxide-win32-x64-msvc": "4.1.2" } }, "sha512-Zwz//1QKo6+KqnCKMT7lA4bspGfwEgcPAHlSthmahtgrpKDfwRGk8PKQrW8Zg/ofCDIlg6EtjSTKSxxSufC+CQ=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.3", "@tailwindcss/oxide-darwin-arm64": "4.1.3", "@tailwindcss/oxide-darwin-x64": "4.1.3", "@tailwindcss/oxide-freebsd-x64": "4.1.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.3", "@tailwindcss/oxide-linux-arm64-musl": "4.1.3", "@tailwindcss/oxide-linux-x64-gnu": "4.1.3", "@tailwindcss/oxide-linux-x64-musl": "4.1.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.3", "@tailwindcss/oxide-win32-x64-msvc": "4.1.3" } }, "sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.2", "", { "os": "android", "cpu": "arm64" }, "sha512-IxkXbntHX8lwGmwURUj4xTr6nezHhLYqeiJeqa179eihGv99pRlKV1W69WByPJDQgSf4qfmwx904H6MkQqTA8w=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.3", "", { "os": "android", "cpu": "arm64" }, "sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZRtiHSnFYHb4jHKIdzxlFm6EDfijTCOT4qwUhJ3GWxfDoW2yT3z/y8xg0nE7e72unsmSj6dtfZ9Y5r75FIrlpA=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BiKUNZf1A0pBNzndBvnPnBxonCY49mgbOsPfILhcCE5RM7pQlRoOgN7QnwNhY284bDbfQSEOWnFR0zbPo6IDTw=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Z30VcpUfRGkiddj4l5NRCpzbSGjhmmklVoqkVQdkEC0MOelpY+fJrVhzSaXHmWrmSvnX8yiaEqAbdDScjVujYQ=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.2", "", { "os": "linux", "cpu": "arm" }, "sha512-w3wsK1ChOLeQ3gFOiwabtWU5e8fY3P1Ss8jR3IFIn/V0va3ir//hZ8AwURveS4oK1Pu6b8i+yxesT4qWnLVUow=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3", "", { "os": "linux", "cpu": "arm" }, "sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oY/u+xJHpndTj7B5XwtmXGk8mQ1KALMfhjWMMpE8pdVAznjJsF5KkCceJ4Fmn5lS1nHMCwZum5M3/KzdmwDMdw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-k7G6vcRK/D+JOWqnKzKN/yQq1q4dCkI49fMoLcfs2pVcaUAXEqCP9NmA8Jv+XahBv5DtDjSAY3HJbjosEdKczg=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-fLL+c678TkYKgkDLLNxSjPPK/SzTec7q/E5pTwvpTqrth867dftV4ezRyhPM5PaiCqX651Y8Yk0wRQMcWUGnmQ=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0tU1Vjd1WucZ2ooq6y4nI9xyTSaH2g338bhrqk+2yzkMHskBm+pMsOCfY7nEIvALkA1PKPOycR4YVdlV7Czo+A=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-r8QaMo3QKiHqUcn+vXYCypCEha+R0sfYxmaZSgZshx9NfkY+CHz91aS2xwNV/E4dmUDkTPUag7sSdiCHPzFVTg=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-lYCdkPxh9JRHXoBsPE8Pu/mppUsC2xihYArNAESub41PKhHTnvn6++5RpmFM+GLSt3ewyS8fwCVvht7ulWm6cw=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.2", "", { "dependencies": { "@tailwindcss/node": "4.1.2", "@tailwindcss/oxide": "4.1.2", "tailwindcss": "4.1.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-3r/ZdMW0gxY8uOx1To0lpYa4coq4CzINcCX4laM1rS340Kcn0ac4A/MMFfHN8qba51aorZMYwMcOxYk4wJ9FYg=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.3", "", { "dependencies": { "@tailwindcss/node": "4.1.3", "@tailwindcss/oxide": "4.1.3", "tailwindcss": "4.1.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ=="],
"@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
@ -292,7 +297,13 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="],
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.29.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/type-utils": "8.29.0", "@typescript-eslint/utils": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ=="],
@ -540,6 +551,8 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
@ -550,6 +563,10 @@
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
@ -602,7 +619,7 @@
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
"pinia": ["pinia@3.0.1", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg=="],
"pinia": ["pinia@3.0.2", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
@ -620,6 +637,8 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
@ -706,7 +725,7 @@
"sync-message-port": ["sync-message-port@1.1.3", "", {}, "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg=="],
"tailwindcss": ["tailwindcss@4.1.2", "", {}, "sha512-VCsK+fitIbQF7JlxXaibFhxrPq4E2hDcG8apzHUdWFMCQWD8uLdlHg4iSkZ53cgLCCcZ+FZK7vG8VjvLcnBgKw=="],
"tailwindcss": ["tailwindcss@4.1.3", "", {}, "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g=="],
"tailwindcss-primeui": ["tailwindcss-primeui@0.4.0", "", { "peerDependencies": { "tailwindcss": ">=3.1.0" } }, "sha512-YYC7B7Yyzm1/4pEGgpf1ABAhbrKY++LuPoUamnKE7fTPO5Ct/Qr/dT+Uq2yiVhQnaW1zHQpYnThxfksaxhlDfQ=="],
@ -722,10 +741,12 @@
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript-eslint": ["typescript-eslint@8.29.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.29.0", "@typescript-eslint/parser": "8.29.0", "@typescript-eslint/utils": "8.29.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
@ -744,7 +765,7 @@
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
"vite": ["vite@6.2.5", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA=="],
"vite": ["vite@6.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw=="],
"vite-plugin-vuetify": ["vite-plugin-vuetify@2.1.1", "", { "dependencies": { "@vuetify/loader-shared": "^2.1.0", "debug": "^4.3.3", "upath": "^2.0.1" }, "peerDependencies": { "vite": ">=5", "vue": "^3.0.0", "vuetify": "^3.0.0" } }, "sha512-Pb7bKhQH8qPMzURmEGq2aIqCJkruFNsyf1NcrrtnjsOIkqJPMcBbiP0oJoO8/uAmyB5W/1JTbbUEsyXdMM0QHQ=="],
@ -756,7 +777,7 @@
"vue-tsc": ["vue-tsc@2.2.8", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.8" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ=="],
"vuetify": ["vuetify@3.8.0", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=2.1.0", "vue": "^3.5.0", "webpack-plugin-vuetify": ">=3.1.0" }, "optionalPeers": ["typescript", "vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-ROC0Xq2G/25ZyUpQMhaynMyXZBJY1WbOGlqOB810yubp8hfY8RlrOw+mzXJonOq6jylCY32muQ9xiJF1JPTLVA=="],
"vuetify": ["vuetify@3.8.1", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=2.1.0", "vue": "^3.5.0", "webpack-plugin-vuetify": ">=3.1.0" }, "optionalPeers": ["typescript", "vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-3qReKBBWIIdJJmwnFU1blVIKHDtnLfIP7kk0MwUrrfjYkWmsDpsymtDnsukkTCnlJ1WvhLr64eQFosr0RVbj9w=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],

View File

@ -10,10 +10,11 @@
"tauri": "tauri"
},
"dependencies": {
"@f3ve/vue-markdown-it": "^0.2.3",
"@mdi/font": "7.4.47",
"@primevue/forms": "^4.3.3",
"@primevue/themes": "^4.3.3",
"@tailwindcss/vite": "^4.1.2",
"@tailwindcss/vite": "^4.1.3",
"@tauri-apps/api": "^2.4.1",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "~2.2.1",
@ -23,29 +24,31 @@
"@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"pinia": "^3.0.1",
"@types/markdown-it": "^14.1.2",
"markdown-it": "^14.1.0",
"pinia": "^3.0.2",
"primeicons": "^7.0.0",
"primevue": "^4.3.3",
"roboto-fontface": "^0.10.0",
"tailwindcss": "^4.1.2",
"tailwindcss": "^4.1.3",
"tailwindcss-primeui": "^0.4.0",
"vue": "^3.5.13",
"vuetify": "^3.8.0"
"vuetify": "^3.8.1"
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tsconfig/node22": "^22.0.1",
"@types/node": "^22.14.0",
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3",
"@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^7.0.2",
"sass": "1.77.8",
"sass-embedded": "^1.86.3",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^0.27.5",
"vite": "^6.2.5",
"vite": "^6.2.6",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8"
}

1163
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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"
]
}

View File

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

View File

@ -1,15 +1,17 @@
use ini::Ini;
use log;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use tokio::sync::Mutex;
use tokio::fs;
use tauri::{AppHandle, Manager, State};
use crate::model::config::GlobalConfigField;
use crate::model::misc::Game;
use crate::model::patch::Patch;
use crate::modules::package::prepare_dlls;
use crate::pkg::{Package, PkgKey};
use crate::pkg_store::{InstallResult, PackageStore};
use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths};
use crate::profiles::{self, Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use crate::appdata::{AppData, ToggleAction};
use crate::model::misc::StartCheckError;
use crate::util;
@ -58,18 +60,40 @@ pub async fn startline(app: AppHandle, refresh: bool) -> Result<(), String> {
let state = app.state::<Mutex<AppData>>();
let mut hash = "".to_owned();
let mut appd = state.lock().await;
let appd = state.lock().await;
let mut game_dlls = Vec::new();
let mut amd_dlls = Vec::new();
if let Some(p) = &appd.profile {
hash = appd.sum_packages(p);
(game_dlls, amd_dlls) = prepare_dlls(p.mod_pkgs(), &appd.pkgs).map_err(|e| e.to_string())?
}
if let Some(p) = &mut appd.profile {
if let Some(p) = &appd.profile {
log::debug!("{}", hash);
p.line_up(hash, refresh, app.clone()).await
let info = p.prepare_display()
.map_err(|e| e.to_string())?;
let lineup_res = p.line_up(hash, refresh, &appd.patch_vec).await
.map_err(|e| e.to_string());
#[cfg(target_os = "windows")]
if let Some(info) = info {
use crate::model::profile::Display;
if lineup_res.is_ok() {
Display::wait_for_exit(app.clone(), info);
} else {
Display::clean_up(&info).map_err(|e| e.to_string())?;
}
}
lineup_res?;
let app_clone = app.clone();
let p_clone = p.clone();
tauri::async_runtime::spawn(async move {
if let Err(e) = p_clone.start(app_clone).await {
if let Err(e) = p_clone.start(StartPayload {
app: app_clone,
game_dlls,
amd_dlls
}).await {
log::error!("Startup failed:\n{}", e);
}
});
@ -151,6 +175,10 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
let appd = state.lock().await;
let pkgs_all = appd.pkgs.get_all();
log::debug!("pkgs_all: {:?}", pkgs_all);
Ok(appd.pkgs.get_all())
}
@ -295,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);
}
@ -386,7 +415,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)
}
}
@ -397,7 +427,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())
}
@ -442,4 +473,42 @@ pub async fn list_directories() -> Result<util::Dirs, ()> {
#[tauri::command]
pub async fn file_exists(path: String) -> Result<bool, ()> {
Ok(std::fs::exists(path).unwrap_or(false))
}
// Easier than trying to get the barely-documented tauri permissions system to work
#[tauri::command]
pub async fn open_file(path: String) -> Result<(), String> {
open::that(path).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn get_changelog() -> Result<String, ()> {
Ok(include_str!("../../CHANGELOG.md").to_owned())
}
#[tauri::command]
pub async fn list_com_ports() -> Result<BTreeMap<String, i32>, String> {
let ports = serialport::available_ports().unwrap_or(Vec::new());
let mut res = BTreeMap::new();
for p in ports {
log::debug!("port {}", p.port_name);
if p.port_name.starts_with("COM") {
if let Ok(parsed) = (p.port_name[3..]).parse() {
res.insert(p.port_name, parsed);
}
}
}
Ok(res)
}
#[tauri::command]
pub async fn list_patches(state: State<'_, Mutex<AppData>>, target: String) -> Result<Vec<Patch>, String> {
log::debug!("invoke: list_patches({})", target);
let mut appd = state.lock().await;
appd.fix();
let list = appd.patch_vec.find_patches(target).map_err(|e| e.to_string())?;
Ok(list)
}

View File

@ -1,4 +1,6 @@
use std::{collections::HashSet, path::PathBuf};
use futures::Stream;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use tokio::fs::File;
use anyhow::{anyhow, Result};
@ -6,14 +8,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 +30,11 @@ impl DownloadHandler {
let rmt = pkg.rmt.as_ref()
.ok_or_else(|| anyhow!("Attempted to download a package without remote data"))?
.clone();
if self.set.contains(zip_path.to_string_lossy().as_ref()) {
// Todo when there is a clear cache button, it should clear the set
Err(anyhow!("Already downloading"))
if self.paths.contains(zip_path) {
Ok(())
} else {
self.set.insert(zip_path.to_string_lossy().to_string());
// TODO clear cache button should clear this
self.paths.insert(zip_path.clone());
tauri::async_runtime::spawn(Self::download_zip_proc(self.app.clone(), zip_path.clone(), pkg.key(), rmt));
Ok(())
}
@ -42,16 +50,21 @@ 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 first_hint = byte_stream.size_hint().0 as f32;
log::info!("Downloading: {}", rmt.download_url);
log::info!("downloading: {}", rmt.download_url);
while let Some(item) = byte_stream.next().await {
let i = item?;
app.emit("download-tick", DownloadTick {
pkg_key: pkg_key.clone(),
ratio: 1.0f32 - (byte_stream.size_hint().0 as f32) / first_hint
})?;
cache_file_w.write_all(&mut i.as_ref()).await?;
}
cache_file_w.sync_all().await?;
tokio::fs::rename(&zip_path_part, &zip_path).await?;
log::debug!("Downloaded to {:?}", zip_path);
log::debug!("downloaded to {:?}", zip_path);
app.emit("download-end", pkg_key)?;

View File

@ -7,6 +7,7 @@ mod download_handler;
mod appdata;
mod modules;
mod profiles;
mod patcher;
use std::sync::OnceLock;
use anyhow::anyhow;
@ -18,23 +19,12 @@ use pkg_store::Payload;
use tauri::{AppHandle, Listener, Manager, RunEvent};
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_cli::CliExt;
use tauri_plugin_updater::UpdaterExt;
use tokio::{fs, sync::Mutex, try_join};
static EXIT_REQUESTED: OnceLock<()> = OnceLock::new();
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub async fn run(_args: Vec<String>) {
simple_logger::init_with_env().expect("Unable to initialize the logger");
log::info!(
"Running from {}",
std::env::current_dir()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
);
let tauri = tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
@ -58,6 +48,15 @@ pub async fn run(_args: Vec<String>) {
util::init_dirs(&apph);
let mut app_data = AppData::new(app.handle().clone());
log::info!(
"running from {}",
std::env::current_dir()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
);
let start_immediately;
if let Ok(matches) = app.cli().matches() {
@ -208,6 +207,12 @@ 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,
cmd::list_patches,
])
.build(tauri::generate_context!())
.expect("error while building tauri application");
@ -242,7 +247,7 @@ fn deep_link(app: AppHandle, args: Vec<String>) {
let url = &args[1];
let proto = "rainycolor://";
if &url[..proto.len()] == proto {
log::info!("Deep link: {}", url);
log::info!("deep link: {}", url);
let regex = regex::Regex::new(
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
@ -268,28 +273,52 @@ fn deep_link(app: AppHandle, args: Vec<String>) {
async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
let mutex = app.state::<Mutex<AppData>>();
let appd = mutex.lock().await;
if !appd.cfg.enable_autoupdates {
log::info!("skipping autoupdate");
return Ok(());
{
let appd = mutex.lock().await;
if !appd.cfg.enable_autoupdates {
log::info!("skipping auto-update");
return Ok(());
}
}
if let Some(update) = app.updater()?.check().await? {
let mut downloaded = 0;
update.download_and_install(
|chunk_length, content_length| {
downloaded += chunk_length;
log::debug!("downloaded {downloaded} from {content_length:?}");
},
|| {
log::info!("download finished");
},
)
.await?;
#[cfg(not(debug_assertions))]
{
use tauri_plugin_updater::UpdaterExt;
use tauri::Emitter;
if let Some(update) = app.updater()?.check().await? {
let mut downloaded = 0;
update.download_and_install(
|chunk_length, content_length| {
downloaded += chunk_length;
_ = app.emit("update-progress", (downloaded as f64) / (content_length.unwrap_or(u64::MAX) as f64));
},
|| {
log::info!("download finished");
},
)
.await?;
log::info!("update installed");
app.restart();
log::info!("update installed");
app.restart();
}
}
// One day I will write proper tests
// #[cfg(debug_assertions)]
// {
// use tauri::Emitter;
// std::thread::sleep(std::time::Duration::from_millis(5000));
// let mut downloaded = 0;
// while downloaded < 500 {
// std::thread::sleep(std::time::Duration::from_millis(10));
// downloaded += 1;
// _ = app.emit("update-progress", (downloaded as f32) / 500f32);
// }
// app.restart();
// }
log::info!("ending auto-update check");
Ok(())
}

View File

@ -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
}

View File

@ -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,
}
@ -59,14 +58,14 @@ impl Game {
pub fn amd_args(&self) -> Vec<&'static str> {
match self {
Game::Ongeki => vec!["-f", "-c", "config_common.json", "config_server.json", "config_client.json"],
Game::Chunithm => vec!["-c", "config_common.json", "config_server.json", "config_client.json", "config_cvt.json", "config_sp.json", "config_hook.json"]
Game::Chunithm => vec!["-c", "config_common.json", "config_server.json", "config_client.json", "config_cvt.json", "config_sp.json"]
}
}
pub fn has_module(&self, module: ProfileModule) -> bool {
match self {
Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini | Keyboard}),
Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Network | Keyboard}),
Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Display | Network | Keyboard | Mempatcher}),
}.contains(module)
}
}
@ -86,4 +85,27 @@ pub enum StartCheckError {
MissingLocalPackage(PkgKey),
MissingDependency(PkgKey, PkgKey),
MissingTool(PkgKey),
}
#[derive(Default, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct ConfigHook {
pub allnet_auth: Option<ConfigHookAuth>,
pub aime: Option<ConfigHookAime>,
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct ConfigHookAuth {
pub r#type: String
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct ConfigHookAime {
pub unit: Vec<ConfigHookAimeUnit>
}
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct ConfigHookAimeUnit {
pub port: i32,
pub id: i32
}

View File

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

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

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

View File

@ -14,6 +14,7 @@ pub enum Aime {
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct AMNet {
pub name: String,
pub addr: String,
@ -26,19 +27,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>,
pub io: Option<PkgKey>,
#[serde(default)]
pub aime: Aime,
pub amfs: PathBuf,
pub option: PathBuf,
pub appdata: PathBuf,
pub intel: bool,
#[serde(default)]
pub amnet: AMNet,
pub aime_port: Option<i32>,
}
impl Segatools {
@ -56,6 +57,7 @@ impl Segatools {
aime: Aime::default(),
intel: false,
amnet: AMNet::default(),
aime_port: None
}
}
}
@ -67,19 +69,16 @@ pub enum DisplayMode {
Fullscreen
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(default)]
pub struct Display {
pub target: String,
pub rez: (i32, i32),
pub mode: DisplayMode,
pub rotation: i32,
pub rotation: Option<i32>,
pub frequency: i32,
pub borderless_fullscreen: bool,
#[serde(default)]
pub dont_switch_primary: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub monitor_index_override: Option<i32>,
}
@ -92,7 +91,7 @@ impl Display {
Game::Ongeki => (1080, 1920),
},
mode: DisplayMode::Borderless,
rotation: 0,
rotation: None,
frequency: match game {
Game::Chunithm => 120,
Game::Ongeki => 60,
@ -111,6 +110,7 @@ pub enum NetworkType {
}
#[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(default)]
pub struct Network {
pub network_type: NetworkType,
@ -125,11 +125,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,
@ -153,17 +155,17 @@ pub enum Mu3Audio {
Excl2Ch,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(default)]
pub struct Mu3Ini {
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<Mu3Audio>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blacklist: Option<(i32, i32)>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct OngekiKeyboard {
pub enabled: bool,
pub use_mouse: bool,
pub coin: i32,
pub svc: i32,
@ -183,6 +185,7 @@ pub struct OngekiKeyboard {
impl Default for OngekiKeyboard {
fn default() -> Self {
Self {
enabled: true,
use_mouse: true,
test: 0x70,
svc: 0x71,
@ -202,8 +205,9 @@ impl Default for OngekiKeyboard {
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct ChunithmKeyboard {
pub split_ir: bool,
pub enabled: bool,
pub coin: i32,
pub svc: i32,
pub test: i32,
@ -214,7 +218,7 @@ pub struct ChunithmKeyboard {
impl Default for ChunithmKeyboard {
fn default() -> Self {
Self {
split_ir: false,
enabled: true,
test: 0x70,
svc: 0x71,
coin: 0x72,
@ -232,7 +236,7 @@ pub enum Keyboard {
}
#[bitflags]
#[repr(u8)]
#[repr(u16)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ProfileModule {
Segatools,
@ -241,4 +245,5 @@ pub enum ProfileModule {
BepInEx,
Mu3Ini,
Keyboard,
Mempatcher
}

View File

@ -1,6 +1,7 @@
use crate::model::profile::{Display, DisplayMode};
use crate::{model::{misc::Game, profile::{Display, DisplayMode}}, util::bool_to_01};
use anyhow::Result;
use displayz::{query_displays, DisplaySet};
use ini::Ini;
use tauri::{AppHandle, Listener};
#[derive(Clone)]
@ -30,7 +31,7 @@ impl Display {
});
}
pub fn line_up(&self) -> Result<Option<DisplayInfo>> {
pub fn prepare(&self) -> Result<Option<DisplayInfo>> {
use anyhow::anyhow;
use displayz::{query_displays, Orientation, Resolution, Frequency};
@ -63,11 +64,23 @@ impl Display {
set: Some(display_set.clone()),
};
if self.rotation == 90 || self.rotation == 270 {
if let Some(rotation) = self.rotation {
let rez = settings.borrow_mut().resolution;
settings.borrow_mut().orientation = if self.rotation == 90 { Orientation::PortraitFlipped } else { Orientation::Portrait };
settings.borrow_mut().orientation = match rotation {
0 => Orientation::Landscape,
90 => Orientation::PortraitFlipped,
180 => Orientation::LandscapeFlipped,
270 => Orientation::Portrait,
_ => panic!("Invalid display rotation")
};
if rez.height < rez.width {
settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width);
if rotation == 90 || rotation == 270 {
settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width);
}
} else {
if rotation == 0 || rotation == 180 {
settings.borrow_mut().resolution = Resolution::new(rez.height, rez.width);
}
}
}
@ -99,6 +112,20 @@ impl Display {
Ok(Some(res))
}
pub fn line_up(&self, game: Game, ini: &mut Ini) {
if game == Game::Chunithm {
let autism = self.monitor_index_override.unwrap_or(0).to_string();
ini.with_section(Some("gfx"))
.set("enable", "1")
.set("windowed", bool_to_01(self.mode != DisplayMode::Fullscreen))
.set("framed", bool_to_01(self.mode == DisplayMode::Window))
.set("monitor", if self.dont_switch_primary { &autism } else { "0" });
ini.with_section(Some("system"))
.set("dipsw2", bool_to_01(self.frequency == 60))
.set("dipsw3", bool_to_01(self.frequency == 60));
}
}
pub fn clean_up(info: &DisplayInfo) -> Result<()> {
use anyhow::anyhow;

View File

@ -54,10 +54,6 @@ impl Keyboard {
parse_int_field!(s, "test", kb.test);
parse_int_field!(s, "service", kb.svc);
parse_int_field!(s, "coin", kb.coin);
let mut ir: i32 = 1;
parse_int_field!(s, "ir", ir);
kb.split_ir = if ir == 0 { true } else { false };
}
if let Some(s) = ini.section(Some("slider")) {
@ -81,37 +77,46 @@ impl Keyboard {
pub fn line_up(&self, ini: &mut Ini) -> Result<()> {
match self {
Keyboard::Ongeki(kb) => {
ini.with_section(Some("io4"))
.set("test", kb.test.to_string())
.set("service", kb.svc.to_string())
.set("coin", kb.coin.to_string())
.set("left1", kb.l1.to_string())
.set("left2", kb.l2.to_string())
.set("left3", kb.l3.to_string())
.set("right1", kb.r1.to_string())
.set("right2", kb.r2.to_string())
.set("right3", kb.r3.to_string())
.set("leftSide", kb.lwad.to_string())
.set("rightSide", kb.rwad.to_string())
.set("leftMenu", kb.lmenu.to_string())
.set("rightMenu", kb.rmenu.to_string())
.set("mouse", if kb.use_mouse { "1" } else { "0" });
if kb.enabled {
ini.with_section(Some("io4"))
.set("test", kb.test.to_string())
.set("service", kb.svc.to_string())
.set("coin", kb.coin.to_string())
.set("left1", kb.l1.to_string())
.set("left2", kb.l2.to_string())
.set("left3", kb.l3.to_string())
.set("right1", kb.r1.to_string())
.set("right2", kb.r2.to_string())
.set("right3", kb.r3.to_string())
.set("leftSide", kb.lwad.to_string())
.set("rightSide", kb.rwad.to_string())
.set("leftMenu", kb.lmenu.to_string())
.set("rightMenu", kb.rmenu.to_string())
.set("mouse", if kb.use_mouse { "1" } else { "0" });
} else {
ini.with_section(Some("io4"))
.set("enable", "0");
}
}
Keyboard::Chunithm(kb) => {
for (i, cell) in kb.cell.iter().enumerate() {
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string());
}
if kb.split_ir {
if kb.enabled {
for (i, cell) in kb.cell.iter().enumerate() {
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string());
}
for (i, ir) in kb.ir.iter().enumerate() {
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), ir.to_string());
}
ini.with_section(Some("io3"))
.set("test", kb.test.to_string())
.set("service", kb.svc.to_string())
.set("coin", kb.coin.to_string())
.set("ir", "0");
} else {
ini.with_section(Some("io3")).set("ir", kb.ir[0].to_string());
ini.with_section(Some("io4"))
.set("enable", "0");
ini.with_section(Some("slider"))
.set("enable", "0");
}
ini.with_section(Some("io3"))
.set("test", kb.test.to_string())
.set("service", kb.svc.to_string())
.set("coin", kb.coin.to_string());
}
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
use std::path::{PathBuf, Path};
use anyhow::{anyhow, Result};
use ini::Ini;
use crate::{model::{misc::Game, profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}};
use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, ConfigHookAuth, Game}, profile::{Aime, Segatools}}, profiles::ProfilePaths, util::{self, PathStr}};
use crate::pkg_store::PackageStore;
impl Segatools {
@ -66,8 +66,12 @@ impl Segatools {
let ini_path = p.config_dir().join("segatools-base.ini");
if !ini_path.exists() {
tokio::fs::write(&ini_path, segatools_base(game)).await
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
match game {
Game::Ongeki => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-ongeki.ini"))
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?,
Game::Chunithm => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-chunithm.ini"))
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?
}
}
if !pfx_dir.exists() {
tokio::fs::create_dir(&pfx_dir).await
@ -167,6 +171,31 @@ impl Segatools {
)?;
}
let mut cfg_hook = ConfigHook::default();
if game == Game::Chunithm {
cfg_hook.allnet_auth = Some({
ConfigHookAuth {
r#type: "1.0".to_owned()
}
})
}
if let Some(port) = self.aime_port {
if self.aime == Aime::Disabled {
cfg_hook.aime = Some({
ConfigHookAime {
unit: vec![
ConfigHookAimeUnit {
port,
id: 1
}
]
}
})
}
}
std::fs::write(pfx_dir.join("config_hook.json"), serde_json::to_string(&cfg_hook)?)?;
log::debug!("end line-up: segatools");
Ok(ini_out)

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

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

View File

@ -20,7 +20,7 @@ pub enum PackageSource {
Local(Game)
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
#[allow(dead_code)]
pub struct Package {
pub namespace: String,
@ -31,15 +31,21 @@ pub struct Package {
pub source: PackageSource,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum Status {
Unchecked,
Unsupported,
OK(BitFlags<Feature>)
OK(BitFlags<Feature>, DLLs),
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct DLLs {
pub game: Option<String>,
pub amd: Option<String>
}
#[bitflags]
#[repr(u8)]
#[repr(u16)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Feature {
Mod,
@ -48,9 +54,13 @@ pub enum Feature {
Mu3Hook,
Mu3IO,
ChusanHook,
ChuniIO,
Mempatcher,
GameDLL,
AmdDLL
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Debug)]
#[allow(dead_code)]
pub struct Local {
pub version: String,
@ -60,7 +70,7 @@ pub struct Local {
pub icon: String,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Debug)]
#[allow(dead_code)]
pub struct Remote {
pub version: String,
@ -73,6 +83,14 @@ pub struct Remote {
pub dependencies: BTreeSet<PkgKey>,
}
impl PkgKey {
pub fn split(&self) -> Result<(String, String)> {
let (namespace, name) = self.0
.split_at(self.0.find("-").ok_or_else(|| anyhow!("Invalid package key"))?);
Ok((namespace.to_owned(), name[1..].to_owned())) // cut the hyphen
}
}
impl Package {
pub fn from_rainy(mut p: rainy::V1Package) -> Option<Package> {
if p.versions.len() == 0 {
@ -205,31 +223,55 @@ impl Package {
fn parse_status(mft: &PackageManifest) -> Status {
if mft.installers.len() == 0 {
return Status::OK(make_bitflags!(Feature::Mod));//Unchecked
} else if mft.installers.len() == 1 {
if let Some(serde_json::Value::String(id)) = &mft.installers[0].get("identifier") {
if id == "rainycolor" {
return Status::OK(make_bitflags!(Feature::Mod));
} else if id == "segatools" {
// Multiple features in the same dll (yubideck etc.) should be supported at some point
let mut flags = BitFlags::default();
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") {
if module == "mu3hook" {
flags |= Feature::Mu3Hook;
} else if module == "chusanhook" {
flags |= Feature::ChusanHook;
} else if module == "amnet" {
flags |= Feature::AMNet | Feature::Aime;
} else if module == "aimeio" {
flags |= Feature::Aime;
} else if module == "mu3io" {
flags |= Feature::Mu3IO;
return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None }); //Unchecked
} else {
let mut flags = BitFlags::default();
let mut game_dll = None;
let mut amd_dll = None;
for installer in &mft.installers {
if let Some(serde_json::Value::String(id)) = installer.get("identifier") {
if id == "rainycolor" {
flags |= Feature::Mod;
} else if id == "segatools" {
// 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;
}
}
} else if id == "native_mod" {
if let Some(serde_json::Value::String(path)) = installer.get("dll-game") {
flags |= Feature::GameDLL;
flags |= Feature::Mod;
game_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll_game") {
flags |= Feature::GameDLL;
flags |= Feature::Mod;
game_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll-amdaemon") {
flags |= Feature::AmdDLL;
flags |= Feature::Mod;
amd_dll = Some(path.to_owned());
}
} else {
return Status::Unsupported;
}
return Status::OK(flags);
}
}
log::debug!("{} parse result: {:?} {:?} {:?}", mft.name, flags, game_dll, amd_dll);
Status::OK(flags, DLLs { game: game_dll, amd: amd_dll })
}
Status::Unsupported
}
}

View File

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

View File

@ -1,64 +1,16 @@
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use std::{collections::BTreeSet, path::{Path, PathBuf}};
use crate::{model::{misc::Game, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
use tauri::Emitter;
use std::process::Stdio;
use crate::model::profile::BepInEx;
use crate::model::{profile::{Display, DisplayMode, Network, Segatools}, segatools_base::segatools_base};
use crate::model::profile::{Display, DisplayMode, Network, Segatools};
use anyhow::{anyhow, Result};
use std::fs::File;
use tokio::process::Command;
use tokio::task::JoinSet;
pub trait ProfilePaths {
fn config_dir(&self) -> PathBuf;
fn data_dir(&self) -> PathBuf;
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct ProfileMeta {
pub game: Game,
pub name: String
}
impl ProfilePaths for ProfileMeta {
fn config_dir(&self) -> PathBuf {
util::profile_config_dir(self.game, &self.name)
}
fn data_dir(&self) -> PathBuf {
util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name))
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Profile {
pub meta: ProfileMeta,
pub data: ProfileData,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ProfileData {
pub mods: BTreeSet<PkgKey>,
pub sgt: Segatools,
pub network: Network,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Display>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bepinex: Option<BepInEx>,
#[cfg(not(target_os = "windows"))]
pub wine: crate::model::profile::Wine,
#[serde(skip_serializing_if = "Option::is_none")]
pub mu3_ini: Option<Mu3Ini>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyboard: Option<Keyboard>
}
pub mod types;
impl Profile {
pub fn new(mut meta: ProfileMeta) -> Result<Self> {
@ -69,7 +21,7 @@ impl Profile {
mods: BTreeSet::new(),
sgt: Segatools::default_for(meta.game),
#[cfg(target_os = "windows")]
display: if meta.game == Game::Ongeki { Some(Display::default_for(meta.game)) } else { None },
display: Some(Display::default_for(meta.game)),
#[cfg(not(target_os = "windows"))]
display: None,
network: Network::default(),
@ -83,13 +35,18 @@ impl Profile {
} else {
Some(Keyboard::Chunithm(ChunithmKeyboard::default()))
},
patches: if meta.game == Game::Chunithm { Some(PatchSelection(BTreeMap::new())) } else { None }
},
meta: meta.clone()
};
p.save()?;
std::fs::create_dir_all(p.config_dir())?;
std::fs::create_dir_all(p.data_dir())?;
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base(meta.game))?;
match meta.game {
Game::Ongeki => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-ongeki.ini"))?,
Game::Chunithm => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-chunithm.ini"))?,
};
Ok(p)
}
@ -101,11 +58,20 @@ 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::Chunithm && data.keyboard.is_none() {
data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default()));
if game == Game::Chunithm {
if data.keyboard.is_none() {
data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default()));
}
if data.patches.is_none() {
data.patches = Some(PatchSelection(BTreeMap::new()));
}
if data.display.is_none() {
data.display = Some(Display::default_for(Game::Chunithm));
}
}
Ok(Profile {
@ -128,7 +94,7 @@ impl Profile {
}
std::fs::write(&path, s)
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
log::info!("Written to {:?}", path);
log::info!("profile saved to {:?}", path);
Ok(())
}
@ -183,28 +149,20 @@ impl Profile {
if self.meta.game.has_module(ProfileModule::Keyboard) && source.keyboard.is_some() {
self.data.keyboard = source.keyboard;
}
if self.data.patches.is_some() && source.patches.is_some() {
self.data.patches = source.patches;
}
}
pub async fn line_up(&self, pkg_hash: String, refresh: bool, _app: AppHandle) -> Result<()> {
pub fn prepare_display(&self) -> Result<Option<DisplayInfo>> {
let info = match &self.data.display {
None => None,
Some(display) => display.line_up()?
Some(display) => display.prepare()?
};
let res = self.line_up_the_rest(pkg_hash, refresh).await;
#[cfg(target_os = "windows")]
if let Some(info) = info {
use crate::model::profile::Display;
if res.is_ok() {
Display::wait_for_exit(_app, info);
} else {
Display::clean_up(&info)?;
}
}
res
Ok(info)
}
async fn line_up_the_rest(&self, pkg_hash: String, refresh: bool) -> Result<()> {
pub async fn line_up(&self, pkg_hash: String, refresh: bool, patch_files: &PatchFileVec) -> Result<()> {
if !self.data_dir().exists() {
tokio::fs::create_dir(self.data_dir()).await?;
}
@ -214,12 +172,19 @@ impl Profile {
util::clean_up_opts(self.data_dir().join("option"))?;
let hash_check = Self::hash_check(&hash_path, &pkg_hash).await? || refresh;
prepare_packages(&self.meta, &self.data.mods, hash_check).await
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
self.data.network.line_up(&mut ini)?;
if let Some(display) = &self.data.display {
display.line_up(self.meta.game, &mut ini);
}
if let Some(keyboard) = &self.data.keyboard {
keyboard.line_up(&mut ini)?;
}
@ -235,10 +200,18 @@ impl Profile {
mu3ini.line_up(&self.data.sgt.target.parent().unwrap())?;
}
if let Some(patches) = &self.data.patches {
futures::try_join!(
patches.render_to_file("amdaemon.exe", patch_files, self.data_dir().join("patch-amd.mph")),
patches.render_to_file("chusanApp.exe", patch_files, self.data_dir().join("patch-game.mph"))
)?;
}
Ok(())
}
pub async fn start(&self, app: AppHandle) -> Result<()> {
pub async fn start(&self, payload: StartPayload) -> Result<()> {
let ini_path = self.data_dir().join("segatools.ini");
log::debug!("With path {:?}", ini_path);
@ -269,12 +242,24 @@ impl Profile {
&ini_path,
)
.current_dir(&exe_dir)
.arg("/C")
.raw_arg("/C")
.arg(&sgt_dir.join(self.meta.game.inject_amd()))
.args(["-d", "-k"])
.raw_arg("-d");
for dll in payload.amd_dlls {
amd_builder.raw_arg("-k");
amd_builder.arg(dll);
}
amd_builder
.raw_arg("-k")
.arg(sgt_dir.join(self.meta.game.hook_amd()))
.arg("amdaemon.exe")
.args(self.meta.game.amd_args());
amd_builder.arg(self.data_dir().join("config_hook.json"));
game_builder
.env(
"SEGATOOLS_CONFIG_PATH",
@ -284,27 +269,54 @@ impl Profile {
"INOHARA_CONFIG_PATH",
self.config_dir().join("inohara.cfg"),
)
.env(
"SAEKAWA_CONFIG_PATH",
self.config_dir().join("saekawa.toml"),
)
.env(
"ONGEKI_LANG_PATH",
self.data_dir().join("lang"),
)
.current_dir(&exe_dir)
.args(["-d", "-k"])
.arg(sgt_dir.join(self.meta.game.hook_exe()))
.arg(self.meta.game.exe());
.raw_arg("-d")
.raw_arg("-k")
.arg(sgt_dir.join(self.meta.game.hook_exe()));
if let Some(display) = &self.data.display {
if display.dont_switch_primary && display.target != "default" {
game_builder.args(["-monitor", &display.monitor_index_override.unwrap_or_else(|| 1).to_string()]);
} else {
game_builder.args(["-monitor", "1"]);
}
game_builder.args([
"-screen-width", &display.rez.0.to_string(),
"-screen-height", &display.rez.1.to_string(),
"-screen-fullscreen", if display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
]);
if display.mode == DisplayMode::Borderless {
game_builder.arg("-popupwindow");
for dll in payload.game_dlls {
game_builder.raw_arg("-k");
game_builder.arg(dll);
}
game_builder.arg(self.meta.game.exe());
if self.meta.game.has_module(ProfileModule::BepInEx) {
if let Some(display) = &self.data.display {
if display.dont_switch_primary && display.target != "default" {
game_builder.args(["-monitor", &display.monitor_index_override.unwrap_or_else(|| 1).to_string()]);
} else {
game_builder.args(["-monitor", "1"]);
}
game_builder.args([
"-screen-width", &display.rez.0.to_string(),
"-screen-height", &display.rez.1.to_string(),
"-screen-fullscreen", if display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
]);
if display.mode == DisplayMode::Borderless {
game_builder.arg("-popupwindow");
}
}
}
if self.meta.game.has_module(ProfileModule::Mempatcher) {
amd_builder
.env("MEMPATCHER_PATCH_PATH", self.data_dir().join("patch-amd.mph"))
.env("MEMPATCHER_LOG_PATH", self.data_dir().join("mempatcher-amdaemon.log"));
game_builder
.raw_arg("--mempatch")
.arg(self.data_dir().join("patch-game.mph"))
.env("MEMPATCHER_LOG_PATH", self.data_dir().join("mempatcher-game.log"));
}
#[cfg(target_os = "linux")]
{
amd_builder.env("WINEPREFIX", &self.wine.prefix);
@ -333,8 +345,8 @@ impl Profile {
util::pkill("amdaemon.exe").await;
log::info!("Launching amdaemon: {:?}", amd_builder);
log::info!("Launching {}: {:?}", self.meta.game, game_builder);
log::info!("launching amdaemon: {:?}", amd_builder);
log::info!("launching {}: {:?}", self.meta.game, game_builder);
let mut amd = amd_builder.spawn()?;
let mut game = game_builder.spawn()?;
@ -349,7 +361,7 @@ impl Profile {
(game.wait().await.expect("game failed to run"), "game")
});
if let Err(e) = app.emit("launch-start", "") {
if let Err(e) = payload.app.emit("launch-start", "") {
log::warn!("Unable to emit launch-start: {}", e);
}
@ -367,7 +379,7 @@ impl Profile {
log::debug!("Fin");
if let Err(e) = app.emit("launch-end", "") {
if let Err(e) = payload.app.emit("launch-end", "") {
log::warn!("Unable to emit launch-end: {}", e);
}

View File

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

View File

@ -150,4 +150,27 @@ impl PathStr for PathBuf {
fn stringify(&self) -> Result<String> {
path_to_str(&self)
}
}
pub fn bool_to_01(val: bool) -> &'static str {
return if val { "1" } else { "0" }
}
// rm -r with checks
pub async fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
let canon = path.as_ref().canonicalize()?;
if canon.to_string_lossy().len() < 10 {
return Err(anyhow!("invalid remove_dir_all target: too short"));
}
if canon.starts_with(data_dir().canonicalize()?)
|| canon.starts_with(config_dir().canonicalize()?)
|| canon.starts_with(cache_dir().canonicalize()?) {
tokio::fs::remove_dir_all(path).await
.map_err(|e| anyhow!("invalid remove_dir_all target: {:?}", e))?;
Ok(())
} else {
Err(anyhow!("invalid remove_dir_all target: not in a data directory"))
}
}

View File

@ -1,45 +1,3 @@
use super::misc::Game;
pub fn segatools_base(game: Game) -> String {
match game {
Game::Ongeki =>
"[vfd]
; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly.
enable=1
[system]
; Enable ALLS system settings.
enable=1
; Enable freeplay mode. This will disable the coin slot and set the game to
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
; allow you to start a game in freeplay mode.
freeplay=0
; LAN Install: Set this to 1 on all machines.
dipsw1=1
[gfx]
; Enables the graphics hook.
enable=1
[led15093]
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
; RGBs and the rear LED panel (billboard) on the cabinet.
enable=1
[led]
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\ongeki_led\"
cabLedOutputPipe=1
; Output billboard LED strip data to serial
cabLedOutputSerial=0
; Output slider LED data to the named pipe
controllerLedOutputPipe=1
; Output slider LED data to the serial port
controllerLedOutputSerial=0".to_owned(),
Game::Chunithm => "
[vfd]
; Enable VFD emulation. Disable to use a real VFD
; GP1232A02A FUTABA assembly.
@ -57,26 +15,11 @@ freeplay=0
; LAN Install: If multiple machines are present on the same LAN then set
; this to 1 on exactly one machine and set this to 0 on all others.
dipsw1=1
; Monitor type: 0 = 120FPS, 1 = 60FPS
dipsw2=1
; Cab type: 0 = SP, 1 = CVT. SP will enable VFD and eMoney. This setting will switch
; the LED 837-15093-06 COM port and the AiMe reder hardware generation as well.
dipsw3=1
; -----------------------------------------------------------------------------
; Misc. hooks settings
; -----------------------------------------------------------------------------
[gfx]
; Enables the graphics hook.
enable=1
; Force the game to run windowed.
windowed=1
; Add a frame to the game window if running windowed.
framed=0
; Select the monitor to run the game on. (Fullscreen only, 0 =primary screen)
monitor=0
; -----------------------------------------------------------------------------
; LED settings
; -----------------------------------------------------------------------------
@ -87,7 +30,7 @@ monitor=0
enable=1
[led]
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\chuni_led\"
; Output billboard LED strip data to a named pipe called "\\.\pipe\chuni_led"
cabLedOutputPipe=1
; Output billboard LED strip data to serial
cabLedOutputSerial=0
@ -105,7 +48,7 @@ controllerLedOutputOpeNITHM=0
;serialBaud=921600
; Data output a sequence of bytes, with JVS-like framing.
; Each \"packet\" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere,
; Each "packet" starts with 0xE0 as a sync. To avoid E0 appearing elsewhere,
; 0xD0 is used as an escape character -- if you receive D0 in the output, ignore
; it and use the next sent byte plus one instead.
;
@ -136,7 +79,4 @@ controllerLedOutputOpeNITHM=0
; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs.
; x86 chuniio to path32, x64 to path64. Both are necessary.
;path32=
;path64=
".to_owned()
}
}
;path64=

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import ConfirmDialog from 'primevue/confirmdialog';
import Dialog from 'primevue/dialog';
import InputIcon from 'primevue/inputicon';
import InputText from 'primevue/inputtext';
import ProgressBar from 'primevue/progressbar';
import ScrollPanel from 'primevue/scrollpanel';
import Tab from 'primevue/tab';
import TabList from 'primevue/tablist';
@ -12,9 +13,11 @@ import TabPanel from 'primevue/tabpanel';
import TabPanels from 'primevue/tabpanels';
import Tabs from 'primevue/tabs';
import { listen } from '@tauri-apps/api/event';
import InfoPage from './InfoPage.vue';
import ModList from './ModList.vue';
import ModStore from './ModStore.vue';
import OptionList from './OptionList.vue';
import PatchList from './PatchList.vue';
import ProfileList from './ProfileList.vue';
import StartButton from './StartButton.vue';
import { invoke } from '../invoke';
@ -34,11 +37,22 @@ const client = useClientStore();
pkg.setupListeners();
const currentTab: Ref<string | number> = ref(3);
const currentTab: Ref<'users' | 'loc' | 'patches' | 'rmt' | 'cfg' | 'info'> =
ref('users');
const pkgSearchTerm = ref('');
const isProfileDisabled = computed(() => prf.current === null);
const updateProgress: Ref<number | null> = ref(null);
listen<number>('update-progress', (ev) => {
updateProgress.value = Math.floor(ev.payload * 100);
});
listen<undefined>('update-end', (_) => {
updateProgress.value = null;
});
onMounted(async () => {
invoke('list_directories').then((d) => {
general.dirs = d as Dirs;
@ -50,20 +64,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);
@ -122,53 +127,62 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
/>
</div>
</Dialog>
<Dialog
modal
:visible="updateProgress !== null"
:closable="false"
header="Updating"
:style="{ width: '200px' }"
>
<ProgressBar :value="updateProgress ?? undefined" />
</Dialog>
<Tabs
lazy
:value="currentTab"
v-on:update:value="
(value) => {
currentTab = value;
currentTab = value as any;
}
"
class="h-screen"
>
<div class="fixed w-full flex z-100">
<TabList class="grow" :show-navigators="false">
<Tab :value="3"
><div class="pi pi-users" v-tooltip="'Profiles'"></div
<Tab value="users"><div class="pi pi-users"></div></Tab>
<Tab :disabled="isProfileDisabled" value="loc"
><div class="pi pi-box"></div
></Tab>
<Tab :disabled="isProfileDisabled" :value="0"
><div
class="pi pi-box"
v-tooltip="'Installed packages'"
></div
></Tab>
<Tab v-if="prf.current?.meta.game === 'chunithm'" :value="4"
><div class="pi pi-ticket" v-tooltip="'Patches'"></div
<Tab
v-if="prf.current?.meta.game === 'chunithm'"
value="patches"
><div class="pi pi-ticket"></div
></Tab>
<Tab
v-if="pkg.networkStatus === 'online'"
:disabled="isProfileDisabled"
:value="1"
><div
class="pi pi-download"
v-tooltip="'Package store'"
></div
value="rmt"
><div class="pi pi-download"></div
></Tab>
<Tab :disabled="isProfileDisabled" :value="2"
><div class="pi pi-cog" v-tooltip="'Settings'"></div
<Tab :disabled="isProfileDisabled" value="cfg"
><div class="pi pi-cog"></div
></Tab>
<Tab value="info"
><div class="pi pi-info-circle"></div
></Tab>
<div class="grow"></div>
<div class="flex gap-4">
<div class="flex" v-if="currentTab !== 3">
<div
class="flex"
v-if="['loc', 'rmt', 'cfg'].includes(currentTab)"
>
<InputIcon class="self-center mr-2">
<i class="pi pi-search" />
</InputIcon>
<InputText
v-if="currentTab === 2"
v-if="currentTab === 'cfg'"
style="min-width: 0; width: 25dvw"
class="self-center"
size="small"
@ -218,38 +232,36 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
</TabList>
</div>
<TabPanels class="w-full grow mt-[3rem]">
<TabPanel :value="0">
<TabPanel value="loc">
<ModList :search="pkgSearchTerm" />
</TabPanel>
<TabPanel :value="1">
<TabPanel value="rmt">
<ModStore :search="pkgSearchTerm" />
</TabPanel>
<TabPanel :value="2">
<TabPanel value="cfg">
<OptionList />
</TabPanel>
<TabPanel :value="3">
<TabPanel value="users">
<ProfileList />
<br /><br /><br />
<footer>
<Button
icon="pi pi-discord"
as="a"
target="_blank"
href="https://discord.gg/jxvzHjjEmc"
/>
</footer>
</TabPanel>
<TabPanel :value="4">
CHUNITHM patches are not implemented yet.<br />Use
<a
href="https://patcher.two-torial.xyz/"
target="_blank"
style="text-decoration: underline"
>patcher.two-torial.xyz</a
>
<TabPanel value="patches">
<PatchList
v-if="
pkg.hasLocal('mempatcher-mempatcher') &&
prf.isPkgKeyEnabled('mempatcher-mempatcher')
.value === true
"
/>
<div v-else>
Patches require <code>mempatcher</code> to be installed
and enabled.
</div>
</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"
@ -306,4 +318,14 @@ body {
.p-tablist-active-bar {
display: none !important;
}
.p-tooltip {
min-width: 300px;
}
.p-progressbar,
.p-progressbar-value,
.p-progressbar-label {
transition-duration: 0s !important;
}
</style>

View File

@ -0,0 +1,56 @@
<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="changelog">
<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;
}
.changelog h2 {
font-size: 1.4rem;
}
.changelog ul {
list-style-type: circle;
}
.changelog li {
margin-left: 40px;
}
</style>

View File

@ -15,7 +15,8 @@ const props = defineProps({
const pkgs = usePkgStore();
const prf = usePrfStore();
const empty = ref(true);
const groupCallIndex = ref(0);
const empty = ref(false);
const gameSublist: Ref<string[]> = ref([]);
invoke('get_game_packages', {
@ -45,7 +46,10 @@ const group = computed(() => {
({ namespace }) => namespace
)
);
empty.value = Object.keys(res).length === 0;
if (groupCallIndex.value > 0) {
empty.value = Object.keys(res).length === 0;
}
groupCallIndex.value += 1;
return res;
});
@ -81,5 +85,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>

View File

@ -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';
@ -48,7 +48,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>

View File

@ -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>

View File

@ -62,7 +62,10 @@ const iconSrc = computed(() => {
>
</span>
<span
v-if="hasFeature(pkg, Feature.Mu3IO)"
v-if="
hasFeature(pkg, Feature.Mu3IO) ||
hasFeature(pkg, Feature.ChuniIO)
"
v-tooltip="'IO'"
class="pi pi-wrench ml-1 text-green-400"
>
@ -73,6 +76,15 @@ const iconSrc = computed(() => {
class="pi pi-credit-card ml-1 text-purple-400"
>
</span>
<span
v-if="
hasFeature(pkg, Feature.GameDLL) ||
hasFeature(pkg, Feature.AmdDLL)
"
v-tooltip="'DLL'"
class="pi pi-cog ml-1 text-red-400"
>
</span>
<span
v-if="showNamespace && pkg?.namespace"
class="text-sm opacity-75"

View File

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

View File

@ -69,10 +69,21 @@ prf.reload();
<template>
<SegatoolsOptions />
<DisplayOptions v-if="prf.current!.meta.game === 'ongeki'" />
<DisplayOptions />
<NetworkOptions />
<AimeOptions />
<MiscOptions />
<OptionCategory
title="Extensions"
v-if="prf.current!.meta.game === 'chunithm'"
>
<OptionRow title="Saekawa config">
<FileEditor
filename="saekawa.toml"
promptname="saekawa config file"
extension="toml"
/> </OptionRow
></OptionCategory>
<OptionCategory
title="Extensions"
v-if="prf.current!.meta.game === 'ongeki'"

View File

@ -9,6 +9,7 @@ const props = defineProps({
title: String,
tooltip: String,
dangerousTooltip: String,
greytext: String,
});
const searched = computed(() => {
@ -38,6 +39,12 @@ const searched = computed(() => {
class="pi pi-exclamation-circle ml-2 text-red-500"
v-tooltip="dangerousTooltip"
></span>
<span
v-if="greytext"
style="font-size: 0.65rem"
class="ml-2 text-gray-400"
>{{ greytext }}</span
>
</div>
<slot />

View File

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

View File

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

View File

@ -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">

View File

@ -3,13 +3,12 @@ import { ref } from 'vue';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
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 isEditing = ref(false);
const props = defineProps({
@ -124,7 +123,11 @@ const deleteProfile = async () => {
@click="
path
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
.then(open)
.then(async (path) => {
if (await invoke('file_exists', { path })) {
await invoke('open_file', { path });
}
})
"
/>
</div>

View File

@ -26,7 +26,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 {

View File

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

View File

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

View File

@ -5,15 +5,15 @@ import KeyboardKey from '../KeyboardKey.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
import { ChunithmButtons } from '@/types';
ToggleSwitch;
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Keyboard">
<OptionRow title="Enable">
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" />
</OptionRow>
<OptionRow
title="Lever mode"
v-if="prf.current!.data.keyboard!.game === 'Ongeki'"
@ -29,12 +29,6 @@ const prf = usePrfStore();
option-value="value"
/>
</OptionRow>
<OptionRow
title="Enable multiple IRs"
v-if="prf.current!.data.keyboard!.game === 'Chunithm'"
>
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.split_ir" />
</OptionRow>
<div
:style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`"
>
@ -102,12 +96,6 @@ const prf = usePrfStore();
class="flex flex-row flex-nowrap gap-2 self-center w-full"
>
<div
v-if="
(
prf.current!.data.keyboard!
.data as ChunithmButtons
).split_ir
"
v-for="idx in Array(6)
.fill(0)
.map((_, i) => i + 1)"
@ -120,15 +108,6 @@ const prf = usePrfStore();
color="rgba(0, 255, 0, 0.2)"
/>
</div>
<div v-else>
<KeyboardKey
button="ir"
:index="0"
:tooltip="`ir0`"
small
color="rgba(0, 255, 0, 0.2)"
/>
</div>
</div>
</div>
<div

View File

@ -119,6 +119,7 @@ const checkSegatoolsIni = async (target: string) => {
return { title: pkgKey(p), value: pkgKey(p) };
})
"
placeholder="none"
option-label="title"
option-value="value"
></Select>

View File

@ -25,6 +25,15 @@ const updatesModel = computed({
await client.setAutoupdates(value);
},
});
const verboseModel = computed({
get() {
return client.verbose;
},
async set(value: boolean) {
await client.setVerbose(value);
},
});
</script>
<template>
@ -45,12 +54,18 @@ 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>
</OptionCategory>
</template>

View File

@ -193,8 +193,17 @@ export const usePkgStore = defineStore('pkg', {
pkg.js.busy = false;
}
}
},
//if (rv === 'Deferred') { /* download progress */ }
async installFromKey(key: string) {
try {
await invoke('install_package', {
key,
force: true,
});
} catch (err) {
console.error(err);
}
},
async updateAll() {
@ -222,6 +231,12 @@ export const usePrfStore = defineStore('prf', () => {
current.value?.data.mods.includes(pkgKey(pkg))
);
const isPkgKeyEnabled = (pkg: string) =>
computed(
() =>
current.value !== null && current.value?.data.mods.includes(pkg)
);
const reload = async () => {
const p = (await invoke('get_current_profile')) as Profile;
current.value = p;
@ -322,6 +337,7 @@ export const usePrfStore = defineStore('prf', () => {
current,
list,
isPkgEnabled,
isPkgKeyEnabled,
reload,
create,
rename,
@ -339,6 +355,7 @@ export const useClientStore = defineStore('client', () => {
const offlineMode = ref(false);
const enableAutoupdates = ref(true);
const verbose = ref(false);
const scaleValue = (value: ScaleType) =>
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
@ -394,11 +411,15 @@ export const useClientStore = defineStore('client', () => {
}
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',
});
};
@ -431,17 +452,22 @@ 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 });
};
getCurrentWindow().onResized(async ({ payload }) => {
// For whatever reason this is 0 when minimized
if (payload.width > 0) {
@ -453,6 +479,7 @@ export const useClientStore = defineStore('client', () => {
scaleFactor,
offlineMode,
enableAutoupdates,
verbose,
timeout,
scaleModel,
load,
@ -460,5 +487,6 @@ export const useClientStore = defineStore('client', () => {
queueSave,
setOfflineMode,
setAutoupdates,
setVerbose,
};
});

View File

@ -30,13 +30,17 @@ export enum Feature {
Mu3Hook = 1 << 3,
Mu3IO = 1 << 4,
ChusanHook = 1 << 5,
ChuniIO = 1 << 6,
Mempatcher = 1 << 7,
GameDLL = 1 << 8,
AmdDLL = 1 << 9,
}
export type Status =
| 'Unchecked'
| 'Unsupported'
| {
OK: Feature;
OK: [Feature, String, String];
};
export type Game = 'ongeki' | 'chunithm';
@ -54,6 +58,9 @@ export interface ProfileData {
bepinex: BepInExConfig;
mu3_ini: Mu3IniConfig | undefined;
keyboard: KeyboardConfig | undefined;
patches: {
[key: string]: 'enabled' | { number: number } | { hex: Int8Array };
};
}
export interface SegatoolsConfig {
@ -70,13 +77,14 @@ export interface SegatoolsConfig {
addr: string;
physical: boolean;
};
aime_port: number;
}
export interface DisplayConfig {
target: String;
rez: [number, number];
mode: 'Window' | 'Borderless' | 'Fullscreen';
rotation: number;
rotation: number | null;
frequency: number;
borderless_fullscreen: boolean;
dont_switch_primary: boolean;
@ -104,6 +112,7 @@ export interface Mu3IniConfig {
export interface OngekiButtons {
use_mouse: boolean;
enabled: boolean;
coin: number;
svc: number;
test: number;
@ -120,7 +129,7 @@ export interface OngekiButtons {
}
export interface ChunithmButtons {
split_ir: boolean;
enabled: boolean;
coin: number;
svc: number;
test: number;
@ -150,3 +159,13 @@ export interface Dirs {
data_dir: string;
cache_dir: string;
}
export interface Patch {
id: string;
name: string;
tooltip: string;
type: undefined | 'number';
default: number;
min: number;
max: number;
}

View File

@ -52,7 +52,7 @@ export const hasFeature = (pkg: Package | undefined, feature: Feature) => {
pkg.loc !== null &&
pkg.loc !== undefined &&
typeof pkg.loc?.status !== 'string' &&
pkg.loc.status.OK & feature
pkg.loc.status.OK[0] & feature
);
};