Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
f588892b05 | |||
37df371006 | |||
7ea31c7a87 | |||
073ff8cfb8 | |||
d93118683d | |||
7f68b8d28b | |||
4247e19996 | |||
6270fce05f | |||
7db36b7bc0 | |||
f3016eb029 | |||
28269c5d75 | |||
1a68eda8c1 | |||
b9a40d44a8 |
52
CHANGELOG.md
Normal file
52
CHANGELOG.md
Normal 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
|
@ -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
|
||||
|
4
TODO.md
4
TODO.md
@ -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)
|
||||
|
75
bun.lock
75
bun.lock
@ -4,10 +4,11 @@
|
||||
"": {
|
||||
"name": "startliner",
|
||||
"dependencies": {
|
||||
"@f3ve/vue-markdown-it": "^0.2.3",
|
||||
"@mdi/font": "7.4.47",
|
||||
"@primevue/forms": "^4.3.3",
|
||||
"@primevue/themes": "^4.3.3",
|
||||
"@tailwindcss/vite": "^4.1.2",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@tauri-apps/api": "^2.4.1",
|
||||
"@tauri-apps/plugin-cli": "^2.2.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2.2.1",
|
||||
@ -17,29 +18,31 @@
|
||||
"@tauri-apps/plugin-shell": "~2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"pinia": "^3.0.1",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.2",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.3.3",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"tailwindcss": "^4.1.2",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"tailwindcss-primeui": "^0.4.0",
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "^3.8.0",
|
||||
"vuetify": "^3.8.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.4.1",
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/node": "^22.14.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"sass": "1.77.8",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-fonts": "^1.3.1",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"vite": "^6.2.5",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-vuetify": "^2.1.1",
|
||||
"vue-tsc": "^2.2.8",
|
||||
},
|
||||
@ -132,6 +135,8 @@
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
|
||||
|
||||
"@f3ve/vue-markdown-it": ["@f3ve/vue-markdown-it@0.2.3", "", { "dependencies": { "markdown-it": "^14.1.0" }, "peerDependencies": { "vue": "^3.3.4" } }, "sha512-v0VNd7wb55kwsUUy3n6DLI9+0FYSG0PrCTD3bWuSRo6WS3OHD5wghh/aHzebVdsVkSBXfVpiEUlMA3DrxLs7Lw=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
@ -216,33 +221,33 @@
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.6", "", { "os": "win32", "cpu": "x64" }, "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.2", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.2" } }, "sha512-ZwFnxH+1z8Ehh8bNTMX3YFrYdzAv7JLY5X5X7XSFY+G9QGJVce/P9xb2mh+j5hKt8NceuHmdtllJvAHWKtsNrQ=="],
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.3", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.3" } }, "sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.2", "@tailwindcss/oxide-darwin-arm64": "4.1.2", "@tailwindcss/oxide-darwin-x64": "4.1.2", "@tailwindcss/oxide-freebsd-x64": "4.1.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.2", "@tailwindcss/oxide-linux-arm64-musl": "4.1.2", "@tailwindcss/oxide-linux-x64-gnu": "4.1.2", "@tailwindcss/oxide-linux-x64-musl": "4.1.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.2", "@tailwindcss/oxide-win32-x64-msvc": "4.1.2" } }, "sha512-Zwz//1QKo6+KqnCKMT7lA4bspGfwEgcPAHlSthmahtgrpKDfwRGk8PKQrW8Zg/ofCDIlg6EtjSTKSxxSufC+CQ=="],
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.3", "@tailwindcss/oxide-darwin-arm64": "4.1.3", "@tailwindcss/oxide-darwin-x64": "4.1.3", "@tailwindcss/oxide-freebsd-x64": "4.1.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.3", "@tailwindcss/oxide-linux-arm64-musl": "4.1.3", "@tailwindcss/oxide-linux-x64-gnu": "4.1.3", "@tailwindcss/oxide-linux-x64-musl": "4.1.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.3", "@tailwindcss/oxide-win32-x64-msvc": "4.1.3" } }, "sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.2", "", { "os": "android", "cpu": "arm64" }, "sha512-IxkXbntHX8lwGmwURUj4xTr6nezHhLYqeiJeqa179eihGv99pRlKV1W69WByPJDQgSf4qfmwx904H6MkQqTA8w=="],
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.3", "", { "os": "android", "cpu": "arm64" }, "sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZRtiHSnFYHb4jHKIdzxlFm6EDfijTCOT4qwUhJ3GWxfDoW2yT3z/y8xg0nE7e72unsmSj6dtfZ9Y5r75FIrlpA=="],
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BiKUNZf1A0pBNzndBvnPnBxonCY49mgbOsPfILhcCE5RM7pQlRoOgN7QnwNhY284bDbfQSEOWnFR0zbPo6IDTw=="],
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Z30VcpUfRGkiddj4l5NRCpzbSGjhmmklVoqkVQdkEC0MOelpY+fJrVhzSaXHmWrmSvnX8yiaEqAbdDScjVujYQ=="],
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.2", "", { "os": "linux", "cpu": "arm" }, "sha512-w3wsK1ChOLeQ3gFOiwabtWU5e8fY3P1Ss8jR3IFIn/V0va3ir//hZ8AwURveS4oK1Pu6b8i+yxesT4qWnLVUow=="],
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3", "", { "os": "linux", "cpu": "arm" }, "sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oY/u+xJHpndTj7B5XwtmXGk8mQ1KALMfhjWMMpE8pdVAznjJsF5KkCceJ4Fmn5lS1nHMCwZum5M3/KzdmwDMdw=="],
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-k7G6vcRK/D+JOWqnKzKN/yQq1q4dCkI49fMoLcfs2pVcaUAXEqCP9NmA8Jv+XahBv5DtDjSAY3HJbjosEdKczg=="],
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-fLL+c678TkYKgkDLLNxSjPPK/SzTec7q/E5pTwvpTqrth867dftV4ezRyhPM5PaiCqX651Y8Yk0wRQMcWUGnmQ=="],
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0tU1Vjd1WucZ2ooq6y4nI9xyTSaH2g338bhrqk+2yzkMHskBm+pMsOCfY7nEIvALkA1PKPOycR4YVdlV7Czo+A=="],
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-r8QaMo3QKiHqUcn+vXYCypCEha+R0sfYxmaZSgZshx9NfkY+CHz91aS2xwNV/E4dmUDkTPUag7sSdiCHPzFVTg=="],
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-lYCdkPxh9JRHXoBsPE8Pu/mppUsC2xihYArNAESub41PKhHTnvn6++5RpmFM+GLSt3ewyS8fwCVvht7ulWm6cw=="],
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.2", "", { "dependencies": { "@tailwindcss/node": "4.1.2", "@tailwindcss/oxide": "4.1.2", "tailwindcss": "4.1.2" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-3r/ZdMW0gxY8uOx1To0lpYa4coq4CzINcCX4laM1rS340Kcn0ac4A/MMFfHN8qba51aorZMYwMcOxYk4wJ9FYg=="],
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.3", "", { "dependencies": { "@tailwindcss/node": "4.1.3", "@tailwindcss/oxide": "4.1.3", "tailwindcss": "4.1.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ=="],
|
||||
|
||||
"@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
|
||||
|
||||
@ -292,7 +297,13 @@
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="],
|
||||
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
|
||||
|
||||
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
|
||||
|
||||
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
|
||||
|
||||
"@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.29.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/type-utils": "8.29.0", "@typescript-eslint/utils": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ=="],
|
||||
|
||||
@ -540,6 +551,8 @@
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="],
|
||||
|
||||
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
|
||||
|
||||
"local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
@ -550,6 +563,10 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
|
||||
|
||||
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
|
||||
|
||||
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
@ -602,7 +619,7 @@
|
||||
|
||||
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
|
||||
|
||||
"pinia": ["pinia@3.0.1", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg=="],
|
||||
"pinia": ["pinia@3.0.2", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g=="],
|
||||
|
||||
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||
|
||||
@ -620,6 +637,8 @@
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
|
||||
@ -706,7 +725,7 @@
|
||||
|
||||
"sync-message-port": ["sync-message-port@1.1.3", "", {}, "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.2", "", {}, "sha512-VCsK+fitIbQF7JlxXaibFhxrPq4E2hDcG8apzHUdWFMCQWD8uLdlHg4iSkZ53cgLCCcZ+FZK7vG8VjvLcnBgKw=="],
|
||||
"tailwindcss": ["tailwindcss@4.1.3", "", {}, "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g=="],
|
||||
|
||||
"tailwindcss-primeui": ["tailwindcss-primeui@0.4.0", "", { "peerDependencies": { "tailwindcss": ">=3.1.0" } }, "sha512-YYC7B7Yyzm1/4pEGgpf1ABAhbrKY++LuPoUamnKE7fTPO5Ct/Qr/dT+Uq2yiVhQnaW1zHQpYnThxfksaxhlDfQ=="],
|
||||
|
||||
@ -722,10 +741,12 @@
|
||||
|
||||
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||
|
||||
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.29.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.29.0", "@typescript-eslint/parser": "8.29.0", "@typescript-eslint/utils": "8.29.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg=="],
|
||||
|
||||
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
|
||||
|
||||
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
@ -744,7 +765,7 @@
|
||||
|
||||
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
|
||||
|
||||
"vite": ["vite@6.2.5", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA=="],
|
||||
"vite": ["vite@6.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw=="],
|
||||
|
||||
"vite-plugin-vuetify": ["vite-plugin-vuetify@2.1.1", "", { "dependencies": { "@vuetify/loader-shared": "^2.1.0", "debug": "^4.3.3", "upath": "^2.0.1" }, "peerDependencies": { "vite": ">=5", "vue": "^3.0.0", "vuetify": "^3.0.0" } }, "sha512-Pb7bKhQH8qPMzURmEGq2aIqCJkruFNsyf1NcrrtnjsOIkqJPMcBbiP0oJoO8/uAmyB5W/1JTbbUEsyXdMM0QHQ=="],
|
||||
|
||||
@ -756,7 +777,7 @@
|
||||
|
||||
"vue-tsc": ["vue-tsc@2.2.8", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.8" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ=="],
|
||||
|
||||
"vuetify": ["vuetify@3.8.0", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=2.1.0", "vue": "^3.5.0", "webpack-plugin-vuetify": ">=3.1.0" }, "optionalPeers": ["typescript", "vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-ROC0Xq2G/25ZyUpQMhaynMyXZBJY1WbOGlqOB810yubp8hfY8RlrOw+mzXJonOq6jylCY32muQ9xiJF1JPTLVA=="],
|
||||
"vuetify": ["vuetify@3.8.1", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=2.1.0", "vue": "^3.5.0", "webpack-plugin-vuetify": ">=3.1.0" }, "optionalPeers": ["typescript", "vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-3qReKBBWIIdJJmwnFU1blVIKHDtnLfIP7kk0MwUrrfjYkWmsDpsymtDnsukkTCnlJ1WvhLr64eQFosr0RVbj9w=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
|
||||
|
17
package.json
17
package.json
@ -10,10 +10,11 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@f3ve/vue-markdown-it": "^0.2.3",
|
||||
"@mdi/font": "7.4.47",
|
||||
"@primevue/forms": "^4.3.3",
|
||||
"@primevue/themes": "^4.3.3",
|
||||
"@tailwindcss/vite": "^4.1.2",
|
||||
"@tailwindcss/vite": "^4.1.3",
|
||||
"@tauri-apps/api": "^2.4.1",
|
||||
"@tauri-apps/plugin-cli": "^2.2.0",
|
||||
"@tauri-apps/plugin-deep-link": "~2.2.1",
|
||||
@ -23,29 +24,31 @@
|
||||
"@tauri-apps/plugin-shell": "~2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"pinia": "^3.0.1",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.2",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.3.3",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"tailwindcss": "^4.1.2",
|
||||
"tailwindcss": "^4.1.3",
|
||||
"tailwindcss-primeui": "^0.4.0",
|
||||
"vue": "^3.5.13",
|
||||
"vuetify": "^3.8.0"
|
||||
"vuetify": "^3.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.4.1",
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/node": "^22.14.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"sass": "1.77.8",
|
||||
"sass-embedded": "^1.86.3",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-fonts": "^1.3.1",
|
||||
"unplugin-vue-components": "^0.27.5",
|
||||
"vite": "^6.2.5",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-vuetify": "^2.1.1",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
|
1163
rust/Cargo.lock
generated
1163
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)?;
|
||||
|
||||
|
@ -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(())
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
162
rust/src/model/patch.rs
Normal file
@ -0,0 +1,162 @@
|
||||
use std::collections::BTreeMap;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use serde::ser::{Serializer, SerializeStruct};
|
||||
use serde_json::Value;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct PatchSelection(pub BTreeMap<String, PatchSelectionData>);
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PatchSelectionData {
|
||||
Enabled,
|
||||
Number(i8),
|
||||
Hex(Vec<u8>)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PatchFileVec(pub Vec<PatchFile>);
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct PatchFile(pub Vec<PatchList>);
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct PatchList {
|
||||
pub filename: String,
|
||||
pub version: String,
|
||||
pub sha256: String,
|
||||
pub patches: Vec<Patch>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Patch {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub tooltip: Option<String>,
|
||||
pub data: PatchData
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum PatchData {
|
||||
Normal(NormalPatch),
|
||||
Number(NumberPatch),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct NormalPatch {
|
||||
pub patches: Vec<NormalPatchField>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct NormalPatchField {
|
||||
pub offset: u64,
|
||||
pub off: Vec<u8>,
|
||||
pub on: Vec<u8>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct NumberPatch {
|
||||
pub offset: u64,
|
||||
pub default: i32,
|
||||
pub size: i64,
|
||||
pub min: i32,
|
||||
pub max: i32
|
||||
}
|
||||
|
||||
impl Serialize for Patch {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer {
|
||||
let mut state = serializer.serialize_struct("Patch", 7)?;
|
||||
state.serialize_field("id", &self.id)?;
|
||||
state.serialize_field("name", &self.name)?;
|
||||
state.serialize_field("tooltip", &self.tooltip)?;
|
||||
match &self.data {
|
||||
PatchData::Normal(patch) => {
|
||||
state.serialize_field("patches", &patch.patches)?;
|
||||
}
|
||||
PatchData::Number(patch) => {
|
||||
state.serialize_field("type", "number")?;
|
||||
state.serialize_field("offset", &patch.offset)?;
|
||||
state.serialize_field("default", &patch.default)?;
|
||||
state.serialize_field("size", &patch.size)?;
|
||||
state.serialize_field("min", &patch.min)?;
|
||||
state.serialize_field("max", &patch.max)?;
|
||||
}
|
||||
}
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for Patch {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let value = Value::deserialize(d)?;
|
||||
|
||||
let data = Ok(match value.get("type").and_then(Value::as_str) {
|
||||
Some("number") => PatchData::Number(NumberPatch {
|
||||
offset: value.get("offset")
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| de::Error::missing_field("offset"))?,
|
||||
default: i32::try_from(value.get("default")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| de::Error::missing_field("default"))?
|
||||
).map_err(|_| de::Error::missing_field("default"))?,
|
||||
size: value.get("size")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| de::Error::missing_field("size"))?,
|
||||
min: i32::try_from(value.get("min")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| de::Error::missing_field("min"))?
|
||||
).map_err(|_| de::Error::missing_field("min"))?,
|
||||
max: i32::try_from(value.get("max")
|
||||
.and_then(Value::as_i64)
|
||||
.ok_or_else(|| de::Error::missing_field("max"))?
|
||||
).map_err(|_| de::Error::missing_field("max"))?
|
||||
}),
|
||||
None => {
|
||||
let mut patches = vec![];
|
||||
for patch in value.get("patches").and_then(Value::as_array).unwrap() {
|
||||
let mut off_list: Vec<u8> = Vec::new();
|
||||
let mut on_list: Vec<u8> = Vec::new();
|
||||
for off in patch.get("off").and_then(Value::as_array).unwrap() {
|
||||
off_list.push(u8::try_from(
|
||||
off.as_u64().ok_or_else(|| de::Error::missing_field("off"))?
|
||||
).map_err(|_| de::Error::missing_field("off"))?);
|
||||
}
|
||||
for on in patch.get("on").and_then(Value::as_array).unwrap() {
|
||||
on_list.push(u8::try_from(
|
||||
on.as_u64().ok_or_else(|| de::Error::missing_field("on"))?
|
||||
).map_err(|_| de::Error::missing_field("on"))?);
|
||||
}
|
||||
patches.push(NormalPatchField {
|
||||
offset: patch.get("offset")
|
||||
.and_then(Value::as_u64)
|
||||
.ok_or_else(|| de::Error::missing_field("offset"))?,
|
||||
off: off_list,
|
||||
on: on_list
|
||||
})
|
||||
}
|
||||
PatchData::Normal(NormalPatch {
|
||||
patches
|
||||
})
|
||||
},
|
||||
Some(&_) => return Err(de::Error::custom("unsupported type"))
|
||||
});
|
||||
|
||||
Ok(Patch {
|
||||
id: value.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| de::Error::missing_field("id"))?
|
||||
.to_owned(),
|
||||
name: value.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| de::Error::missing_field("name"))?
|
||||
.to_owned(),
|
||||
tooltip: value.get("tooltip")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|s| Some(s.to_owned())),
|
||||
data: data?
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
59
rust/src/modules/mempatcher.rs
Normal file
59
rust/src/modules/mempatcher.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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;
|
@ -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))
|
||||
}
|
@ -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
45
rust/src/patcher.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
64
rust/src/profiles/types.rs
Normal file
64
rust/src/profiles/types.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::AppHandle;
|
||||
use std::{collections::BTreeSet, path::PathBuf};
|
||||
use crate::{model::{misc::Game, patch::PatchSelection, profile::{Keyboard, Mu3Ini}}, pkg::PkgKey, util};
|
||||
use crate::model::profile::BepInEx;
|
||||
use crate::model::profile::{Display, Network, Segatools};
|
||||
|
||||
pub trait ProfilePaths {
|
||||
fn config_dir(&self) -> PathBuf;
|
||||
fn data_dir(&self) -> PathBuf;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub struct ProfileMeta {
|
||||
pub game: Game,
|
||||
pub name: String
|
||||
}
|
||||
|
||||
impl ProfilePaths for ProfileMeta {
|
||||
fn config_dir(&self) -> PathBuf {
|
||||
util::profile_config_dir(self.game, &self.name)
|
||||
}
|
||||
|
||||
fn data_dir(&self) -> PathBuf {
|
||||
util::data_dir().join(format!("profile-{}-{}", &self.game, &self.name))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
pub struct Profile {
|
||||
pub meta: ProfileMeta,
|
||||
pub data: ProfileData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct ProfileData {
|
||||
pub mods: BTreeSet<PkgKey>,
|
||||
pub sgt: Segatools,
|
||||
pub network: Network,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display: Option<Display>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bepinex: Option<BepInEx>,
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub wine: crate::model::profile::Wine,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mu3_ini: Option<Mu3Ini>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub keyboard: Option<Keyboard>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub patches: Option<PatchSelection>,
|
||||
}
|
||||
|
||||
pub struct StartPayload {
|
||||
pub app: AppHandle,
|
||||
pub game_dlls: Vec<PathBuf>,
|
||||
pub amd_dlls: Vec<PathBuf>,
|
||||
}
|
@ -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"))
|
||||
}
|
||||
}
|
@ -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=
|
36
rust/static/segatools-ongeki.ini
Normal file
36
rust/static/segatools-ongeki.ini
Normal file
@ -0,0 +1,36 @@
|
||||
[vfd]
|
||||
; Enable VFD emulation. Disable to use a real VFD
|
||||
; GP1232A02A FUTABA assembly.
|
||||
enable=1
|
||||
|
||||
[system]
|
||||
; Enable ALLS system settings.
|
||||
enable=1
|
||||
|
||||
; Enable freeplay mode. This will disable the coin slot and set the game to
|
||||
; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not
|
||||
; allow you to start a game in freeplay mode.
|
||||
freeplay=0
|
||||
|
||||
; LAN Install: Set this to 1 on all machines.
|
||||
dipsw1=1
|
||||
|
||||
[gfx]
|
||||
; Enables the graphics hook.
|
||||
enable=1
|
||||
|
||||
[led15093]
|
||||
; Enable emulation of the 15093-06 controlled lights, which handle the air tower
|
||||
; RGBs and the rear LED panel (billboard) on the cabinet.
|
||||
enable=1
|
||||
|
||||
[led]
|
||||
; Output billboard LED strip data to a named pipe called \"\\\\.\\pipe\\ongeki_led\"
|
||||
cabLedOutputPipe=1
|
||||
; Output billboard LED strip data to serial
|
||||
cabLedOutputSerial=0
|
||||
|
||||
; Output slider LED data to the named pipe
|
||||
controllerLedOutputPipe=1
|
||||
; Output slider LED data to the serial port
|
||||
controllerLedOutputSerial=0
|
219
rust/static/standard-chunithm.json5
Normal file
219
rust/static/standard-chunithm.json5
Normal 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] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
@ -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",
|
||||
|
@ -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>
|
||||
|
56
src/components/InfoPage.vue
Normal file
56
src/components/InfoPage.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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">
|
||||
|
@ -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'"
|
||||
|
@ -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 />
|
||||
|
54
src/components/PatchEntry.vue
Normal file
54
src/components/PatchEntry.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import OptionRow from './OptionRow.vue';
|
||||
import { usePrfStore } from '../stores';
|
||||
import { Patch } from '@/types';
|
||||
|
||||
const prf = usePrfStore();
|
||||
|
||||
const toggleUnary = (key: string, val: boolean) => {
|
||||
if (val) {
|
||||
prf.current!.data.patches[key] = 'enabled';
|
||||
} else {
|
||||
delete prf.current!.data.patches[key];
|
||||
}
|
||||
};
|
||||
|
||||
const setNumber = (key: string, val: number) => {
|
||||
if (val) {
|
||||
prf.current!.data.patches[key] = { number: val };
|
||||
} else {
|
||||
delete prf.current!.data.patches[key];
|
||||
}
|
||||
};
|
||||
|
||||
defineProps({
|
||||
patch: Object as () => Patch,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionRow
|
||||
:title="patch?.name"
|
||||
:tooltip="patch?.tooltip"
|
||||
:greytext="patch?.id"
|
||||
>
|
||||
<ToggleSwitch
|
||||
v-if="patch?.type === undefined"
|
||||
:model-value="prf.current!.data.patches[patch!.id!] !== undefined"
|
||||
@update:model-value="(v: boolean) => toggleUnary(patch!.id!, v)"
|
||||
/>
|
||||
<InputNumber
|
||||
v-else-if="patch?.type === 'number'"
|
||||
class="number-input"
|
||||
:model-value="
|
||||
(prf.current!.data.patches[patch!.id!] as any)?.number
|
||||
"
|
||||
@update:model-value="(v: number) => setNumber(patch!.id!, v)"
|
||||
:min="patch?.min"
|
||||
:max="patch?.max"
|
||||
:placeholder="(patch?.default ?? 0).toString()"
|
||||
/>
|
||||
</OptionRow>
|
||||
</template>
|
60
src/components/PatchList.vue
Normal file
60
src/components/PatchList.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from 'vue';
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
import OptionCategory from './OptionCategory.vue';
|
||||
import PatchEntry from './PatchEntry.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePrfStore } from '../stores';
|
||||
import { Patch } from '../types';
|
||||
|
||||
const prf = usePrfStore();
|
||||
|
||||
prf.reload();
|
||||
|
||||
const gamePatches: Ref<Patch[] | null> = ref(null);
|
||||
const amdPatches: Ref<Patch[] | null> = ref(null);
|
||||
|
||||
invoke('list_patches', { target: prf.current!.data.sgt.target }).then(
|
||||
(patches) => {
|
||||
gamePatches.value = patches as Patch[];
|
||||
}
|
||||
);
|
||||
|
||||
(async () => {
|
||||
const amd = await path.join(
|
||||
prf.current!.data.sgt.target,
|
||||
'../amdaemon.exe'
|
||||
);
|
||||
amdPatches.value = (await invoke('list_patches', {
|
||||
target: amd,
|
||||
})) as Patch[];
|
||||
})();
|
||||
|
||||
const errorMessage =
|
||||
"No compatible patches found. Make sure you're using unpacked and unpatched files.";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="chusanApp.exe" always-found>
|
||||
<PatchEntry
|
||||
v-if="gamePatches !== null"
|
||||
v-for="p in gamePatches"
|
||||
:patch="p"
|
||||
/>
|
||||
<div v-if="gamePatches === null">Loading...</div>
|
||||
<div v-if="gamePatches !== null && gamePatches.length === 0">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</OptionCategory>
|
||||
<OptionCategory title="amdaemon.exe" always-found>
|
||||
<PatchEntry
|
||||
v-if="amdPatches !== null"
|
||||
v-for="p in amdPatches"
|
||||
:patch="p"
|
||||
/>
|
||||
<div v-if="gamePatches === null">Loading...</div>
|
||||
<div v-if="amdPatches !== null && amdPatches.length === 0">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</OptionCategory>
|
||||
</template>
|
@ -22,7 +22,6 @@ const prf = usePrfStore();
|
||||
icon="pi pi-plus"
|
||||
class="chunithm-button profile-button"
|
||||
@click="() => prf.create('chunithm')"
|
||||
v-tooltip="'!!! Experimental !!!'"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
25
src/types.ts
25
src/types.ts
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user