Compare commits

...

20 Commits
0.3.0 ... 0.6.0

Author SHA1 Message Date
7ea31c7a87 fix: misc fixes for 0.6.0 2025-04-13 21:02:04 +00:00
073ff8cfb8 fix: native-mod -> native_mod 2025-04-13 20:25:22 +00:00
d93118683d fix: release build 2025-04-13 19:53:12 +00:00
7f68b8d28b feat: amdaemon.exe patching 2025-04-13 19:28:06 +00:00
4247e19996 feat: chusanApp.exe patching 2025-04-13 18:15:41 +00:00
6270fce05f feat: display module for chunithm
Also make the progress bar all shiny
2025-04-12 17:33:39 +00:00
7db36b7bc0 fix: remove update check from the ui
it is no longer necessary
2025-04-12 11:59:02 +00:00
f3016eb029 feat: autoupdate ui 2025-04-12 10:53:06 +00:00
28269c5d75 feat: hardware aime 2025-04-11 23:07:45 +00:00
1a68eda8c1 feat: partial support for patches 2025-04-11 15:27:13 +00:00
b9a40d44a8 fix: remove split_ir 2025-04-10 19:33:54 +00:00
b10c797d52 docs: update README.md 2025-04-10 14:10:47 +00:00
ff0a37dfdc feat: skeleton of proper local package support 2025-04-10 14:07:44 +00:00
d63d81e349 feat: also copy aime.txt 2025-04-10 13:53:20 +00:00
9ea66dbeab feat: segatools.ini loading 2025-04-10 13:32:49 +00:00
4e795257ad feat: add dont_switch_primary 2025-04-08 21:49:15 +00:00
41fbef0260 chore: bump ver 2025-04-04 22:20:05 +00:00
93e0a7e313 feat: new error banner, qol 2025-04-04 22:14:09 +00:00
ca871f069f feat: autoupdate toggle 2025-04-04 19:41:38 +00:00
8c3f9762a4 feat: ui scaling, update all 2025-04-04 14:37:16 +00:00
57 changed files with 3580 additions and 1289 deletions

View File

@ -1,6 +1,6 @@
# STARTLINER
A simple and easy to use launcher, configuration tool and mod manager for [many games](https://silentblue.remywiki.com/ONGEKI:bright_MEMORY) (more to come) using [Rainycolor Watercolor](https://rainy.patafour.zip).
A simple and easy to use launcher, configuration tool and mod manager for O.N.G.E.K.I. and CHUNITHM, using [Rainycolor Watercolor](https://rainy.patafour.zip).
Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome.
@ -16,14 +16,14 @@ 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
bun run tauri build
```
Create a profile, then click on things in the configuration tab (game path, `amfs` and network at the least). STARTLINER expects clean data with unpacked binaries. Anything else you can have in the game directory (segatools, BepInEx, etc.) can be present, but will not be used.
Create a profile, then click on things in the configuration tab (game path, `amfs` and network at the least). STARTLINER expects clean data with unpacked binaries. Anything else you may have in the game directory (segatools, BepInEx, etc.) can be present, but will not be used.
Once a profile has been set up, it is possible to bypass the GUI:

246
bun.lock
View File

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

View File

@ -11,42 +11,42 @@
},
"dependencies": {
"@mdi/font": "7.4.47",
"@primevue/forms": "^4.3.1",
"@primevue/themes": "^4.3.1",
"@tailwindcss/vite": "^4.0.9",
"@tauri-apps/api": "^2.3.0",
"@primevue/forms": "^4.3.3",
"@primevue/themes": "^4.3.3",
"@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.0",
"@tauri-apps/plugin-dialog": "~2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-deep-link": "~2.2.1",
"@tauri-apps/plugin-dialog": "~2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-shell": "~2.2.0",
"@tauri-apps/plugin-updater": "^2.6.1",
"@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",
"pinia": "^3.0.2",
"primeicons": "^7.0.0",
"primevue": "^4.3.1",
"primevue": "^4.3.3",
"roboto-fontface": "^0.10.0",
"tailwindcss": "^4.0.9",
"tailwindcss": "^4.1.3",
"tailwindcss-primeui": "^0.4.0",
"vue": "^3.5.13",
"vuetify": "^3.7.15"
"vuetify": "^3.8.1"
},
"devDependencies": {
"@tauri-apps/cli": "^2.3.1",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.13.9",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-typescript": "^14.4.0",
"@tauri-apps/cli": "^2.4.1",
"@tsconfig/node22": "^22.0.1",
"@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.85.1",
"typescript": "^5.8.2",
"sass-embedded": "^1.86.3",
"typescript": "^5.8.3",
"unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^0.27.5",
"vite": "^6.2.0",
"vite-plugin-vuetify": "^2.1.0",
"vite": "^6.2.6",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8"
}
}

1162
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,6 @@ futures = "0.3.31"
tauri-plugin-shell = "2"
directories = "6.0.0"
rust-ini = "0.21.1"
simple_logger = "5.0.0"
log = "0.4.25"
regex = "1.11.1"
zip = "2.2.2"
@ -42,6 +41,10 @@ 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"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-cli = "2"

View File

@ -12,6 +12,9 @@
"core:window:allow-set-focus",
"core:window:allow-hide",
"core:window:allow-show",
"core:window:allow-set-size",
"core:window:allow-inner-size",
"core:window:allow-set-min-size",
"core:app:allow-app-hide",
"shell:default",
"dialog:default",

View File

@ -1,7 +1,8 @@
use std::hash::{DefaultHasher, Hash, Hasher};
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;
@ -17,6 +18,7 @@ pub struct AppData {
pub pkgs: PackageStore,
pub cfg: GlobalConfig,
pub state: GlobalState,
pub patch_vec: PatchFileVec,
}
#[derive(PartialEq, Debug, Copy, Clone)]
@ -37,13 +39,18 @@ impl AppData {
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 +85,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);
}

View File

@ -1,12 +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;
@ -55,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
.map_err(|e| format!("Lineup failed:\n{}", e))?;
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);
}
});
@ -148,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())
}
@ -318,7 +349,7 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
#[tauri::command]
pub async fn sync_current_profile(state: State<'_, Mutex<AppData>>, data: ProfileData) -> Result<(), String> {
log::debug!("invoke: sync_current_profile");
log::debug!("invoke: sync_current_profile {:?}", data);
let mut appd = state.lock().await;
if let Some(p) = &mut appd.profile {
@ -344,6 +375,27 @@ pub async fn save_current_profile(state: State<'_, Mutex<AppData>>) -> Result<()
}
}
#[tauri::command]
pub async fn load_segatools_ini(state: State<'_, Mutex<AppData>>, path: PathBuf) -> Result<(), String> {
log::debug!("invoke: load_segatools_ini({:?})", path);
let mut appd = state.lock().await;
if let Some(p) = &mut appd.profile {
let str = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
// Stupid path escape hack for the ini reader
let str = str.replace("\\", "\\\\").replace("\\\\\\\\", "\\\\");
let ini = Ini::load_from_str(&str).map_err(|e| e.to_string())?;
p.data.sgt.load_from_ini(&ini, p.config_dir()).map_err(|e| e.to_string())?;
p.data.network.load_from_ini(&ini).map_err(|e| e.to_string())?;
if let Some(kb) = &mut p.data.keyboard {
kb.load_from_ini(&ini).map_err(|e| e.to_string())?;
}
p.save().map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
log::debug!("invoke: list_platform_capabilities");
@ -355,6 +407,29 @@ pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
return Ok(vec!["wine".to_owned()]);
}
#[tauri::command]
pub async fn get_global_config(state: State<'_, Mutex<AppData>>, field: GlobalConfigField) -> Result<bool, ()> {
log::debug!("invoke: get_global_config({field:?})");
let appd = state.lock().await;
match field {
GlobalConfigField::OfflineMode => Ok(appd.cfg.offline_mode),
GlobalConfigField::EnableAutoupdates => Ok(appd.cfg.enable_autoupdates)
}
}
#[tauri::command]
pub async fn set_global_config(state: State<'_, Mutex<AppData>>, field: GlobalConfigField, value: bool) -> Result<(), String> {
log::debug!("invoke: set_global_config({field:?}, {value})");
let mut appd = state.lock().await;
match field {
GlobalConfigField::OfflineMode => appd.cfg.offline_mode = value,
GlobalConfigField::EnableAutoupdates => appd.cfg.enable_autoupdates = value
};
appd.write().map_err(|e| e.to_string())
}
#[tauri::command]
#[cfg(target_os = "windows")]
pub async fn list_displays() -> Result<Vec<(String, String)>, String> {
@ -389,4 +464,36 @@ pub async fn list_directories() -> Result<util::Dirs, ()> {
log::debug!("invoke: list_directores");
Ok(util::all_dirs().clone())
}
// Tauri fs api is useless
#[tauri::command]
pub async fn file_exists(path: String) -> Result<bool, ()> {
Ok(std::fs::exists(path).unwrap_or(false))
}
#[tauri::command]
pub async fn list_com_ports() -> Result<BTreeMap<String, i32>, String> {
let ports = serialport::available_ports().unwrap_or(Vec::new());
let mut res = BTreeMap::new();
for p in ports {
log::debug!("port {}", p.port_name);
if p.port_name.starts_with("COM") {
if let Ok(parsed) = (p.port_name[3..]).parse() {
res.insert(p.port_name, parsed);
}
}
}
Ok(res)
}
#[tauri::command]
pub async fn list_patches(state: State<'_, Mutex<AppData>>, target: String) -> Result<Vec<Patch>, String> {
log::debug!("invoke: list_patches({})", target);
let mut appd = state.lock().await;
appd.fix();
let list = appd.patch_vec.find_patches(target).map_err(|e| e.to_string())?;
Ok(list)
}

View File

@ -43,7 +43,7 @@ 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();
log::info!("Downloading: {}", rmt.download_url);
log::info!("downloading: {}", rmt.download_url);
while let Some(item) = byte_stream.next().await {
let i = item?;
cache_file_w.write_all(&mut i.as_ref()).await?;
@ -51,7 +51,7 @@ impl DownloadHandler {
cache_file_w.sync_all().await?;
tokio::fs::rename(&zip_path_part, &zip_path).await?;
log::debug!("Downloaded to {:?}", zip_path);
log::debug!("downloaded to {:?}", zip_path);
app.emit("download-end", pkg_key)?;

View File

@ -7,34 +7,25 @@ mod download_handler;
mod appdata;
mod modules;
mod profiles;
mod patcher;
use std::sync::OnceLock;
use std::{sync::OnceLock, time::SystemTime};
use anyhow::anyhow;
use closure::closure;
use appdata::{AppData, ToggleAction};
use fern::colors::{Color, ColoredLevelConfig};
use model::misc::Game;
use pkg::PkgKey;
use pkg_store::Payload;
use tauri::{AppHandle, Listener, Manager, RunEvent};
use tauri_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| {
@ -57,6 +48,51 @@ pub async fn run(_args: Vec<String>) {
util::init_dirs(&apph);
let mut fern_builder;
{
let colors = ColoredLevelConfig::new()
.debug(Color::Green)
.info(Color::Blue)
.warn(Color::Yellow)
.error(Color::Red);
fern_builder = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
colors.color(record.level()),
record.target(),
message
))
})
.chain(std::io::stdout())
.chain(fern::log_file(util::data_dir().join("log.txt")).expect("unable to initialize the logger"));
}
#[cfg(debug_assertions)]
{
fern_builder = fern_builder.level(log::LevelFilter::Debug);
}
#[cfg(not(debug_assertions))]
{
if std::env::var("DEBUG_LOG").is_ok() {
fern_builder = fern_builder.level(log::LevelFilter::Debug);
} else {
fern_builder = fern_builder.level(log::LevelFilter::Info);
}
}
fern_builder.apply()?;
log::info!(
"running from {}",
std::env::current_dir()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
);
let mut app_data = AppData::new(app.handle().clone());
let start_immediately;
@ -71,8 +107,8 @@ pub async fn run(_args: Vec<String>) {
} else {
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into()))
.title("STARTLINER")
.inner_size(720f64, 480f64)
.min_inner_size(720f64, 480f64)
.inner_size(900f64, 480f64)
.min_inner_size(900f64, 480f64)
.build()?;
start_immediately = false;
}
@ -199,10 +235,19 @@ pub async fn run(_args: Vec<String>) {
cmd::get_current_profile,
cmd::sync_current_profile,
cmd::save_current_profile,
cmd::load_segatools_ini,
cmd::get_global_config,
cmd::set_global_config,
cmd::list_displays,
cmd::list_platform_capabilities,
cmd::list_directories,
cmd::file_exists,
cmd::list_com_ports,
cmd::list_patches,
])
.build(tauri::generate_context!())
.expect("error while building tauri application");
@ -237,7 +282,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]+/"
@ -262,23 +307,53 @@ fn deep_link(app: AppHandle, args: Vec<String>) {
}
async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
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?;
let mutex = app.state::<Mutex<AppData>>();
{
let appd = mutex.lock().await;
if !appd.cfg.enable_autoupdates {
log::info!("skipping auto-update");
log::info!("update installed");
app.restart();
return Ok(());
}
}
#[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", (chunk_length as f64) / (content_length.unwrap_or(u64::MAX) as f64));
},
|| {
log::info!("download finished");
},
)
.await?;
log::info!("update installed");
app.restart();
}
}
// One day I will write proper tests
// #[cfg(debug_assertions)]
// {
// use tauri::Emitter;
// std::thread::sleep(std::time::Duration::from_millis(5000));
// let mut downloaded = 0;
// while downloaded < 500 {
// std::thread::sleep(std::time::Duration::from_millis(10));
// downloaded += 1;
// _ = app.emit("update-progress", (downloaded as f32) / 500f32);
// }
// app.restart();
// }
log::info!("ending auto-update check");
Ok(())
}

View File

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

View File

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

View File

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

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

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

View File

@ -39,6 +39,7 @@ pub struct Segatools {
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
}
}
}
@ -72,9 +74,15 @@ 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>,
}
impl Display {
@ -86,12 +94,14 @@ impl Display {
Game::Ongeki => (1080, 1920),
},
mode: DisplayMode::Borderless,
rotation: 0,
rotation: None,
frequency: match game {
Game::Chunithm => 120,
Game::Ongeki => 60,
},
borderless_fullscreen: true,
dont_switch_primary: false,
monitor_index_override: None,
}
}
}
@ -154,13 +164,88 @@ pub struct Mu3Ini {
pub blacklist: Option<(i32, i32)>,
}
fn default_true() -> bool { true }
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct OngekiKeyboard {
#[serde(default = "default_true")] pub enabled: bool,
pub use_mouse: bool,
pub coin: i32,
pub svc: i32,
pub test: i32,
pub lmenu: i32,
pub rmenu: i32,
pub l1: i32,
pub l2: i32,
pub l3: i32,
pub r1: i32,
pub r2: i32,
pub r3: i32,
pub lwad: i32,
pub rwad: i32,
}
impl Default for OngekiKeyboard {
fn default() -> Self {
Self {
enabled: true,
use_mouse: true,
test: 0x70,
svc: 0x71,
coin: 0x72,
lmenu: 0x55,
rmenu: 0x4F,
lwad: 0x01,
rwad: 0x02,
l1: 0x41,
l2: 0x53,
l3: 0x44,
r1: 0x4A,
r2: 0x4B,
r3: 0x4C
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ChunithmKeyboard {
#[serde(default = "default_true")] pub enabled: bool,
pub coin: i32,
pub svc: i32,
pub test: i32,
pub cell: [i32; 32],
pub ir: [i32; 6],
}
impl Default for ChunithmKeyboard {
fn default() -> Self {
Self {
enabled: true,
test: 0x70,
svc: 0x71,
coin: 0x72,
cell: Default::default(),
ir: Default::default(),
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(tag = "game", content = "data")]
pub enum Keyboard {
Ongeki(OngekiKeyboard),
Chunithm(ChunithmKeyboard),
}
#[bitflags]
#[repr(u8)]
#[repr(u16)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ProfileModule {
Segatools,
Network,
Display,
BepInEx,
Mu3Ini
Mu3Ini,
Keyboard,
Mempatcher
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
use anyhow::Result;
use std::collections::BTreeSet;
use crate::pkg::PkgKey;
use std::path::PathBuf;
use crate::pkg::{PkgKey, Status};
use crate::pkg_store::PackageStore;
use crate::util;
use crate::profiles::ProfilePaths;
use crate::profiles::types::ProfilePaths;
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> {
log::debug!("begin prepare packages");
@ -22,10 +24,10 @@ 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() {
@ -33,7 +35,7 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
}
}
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 +47,27 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
log::debug!("end prepare packages");
Ok(())
}
pub fn prepare_dlls(
enabled_pkgs: &BTreeSet<PkgKey>,
store: &PackageStore,
) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
let mut res_game = Vec::new();
let mut res_amd = Vec::new();
for pkg in enabled_pkgs {
if let Ok(pkg) = store.get(&pkg) {
if let Some(loc) = &pkg.loc {
if let Status::OK(_, dlls) = &loc.status {
if let Some(game_dll) = &dlls.game {
res_game.push(pkg.path().join(game_dll.clone()));
}
if let Some(amd_dll) = &dlls.amd {
res_amd.push(pkg.path().join(amd_dll.clone()));
}
}
}
}
}
Ok((res_game, res_amd))
}

View File

@ -1,8 +1,7 @@
use std::path::PathBuf;
use std::path::{PathBuf, Path};
use anyhow::{anyhow, Result};
use ini::Ini;
use crate::{model::{misc::Game, profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}};
use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, ConfigHookAuth, Game}, profile::{Aime, Segatools}}, profiles::ProfilePaths, util::{self, PathStr}};
use crate::pkg_store::PackageStore;
impl Segatools {
@ -31,6 +30,31 @@ impl Segatools {
_ => {},
}
}
pub fn load_from_ini(&mut self, ini: &Ini, config_dir: impl AsRef<Path>) -> Result<()> {
log::debug!("loading sgt");
if let Some(s) = ini.section(Some("vfs")) {
s.get("amfs").map(|v| self.amfs = PathBuf::from(v));
s.get("appdata").map(|v| self.appdata = PathBuf::from(v));
s.get("option").map(|v| self.option = PathBuf::from(v));
}
if let Some(s) = ini.section(Some("aime")) {
if s.get("enable").unwrap_or("0") == "1" {
if let Some(aime_path) = s.get("aimePath") {
if let Some(game_dir) = self.target.parent() {
let target = game_dir.join(aime_path);
std::fs::copy(target, config_dir.as_ref().join("aime.txt"))?;
} else {
log::error!("profile doesn't have a game directory");
}
} else {
log::warn!("aime emulation is enabled, but no aimePath specified");
}
}
}
Ok(())
}
pub async fn line_up(&self, p: &impl ProfilePaths, game: Game) -> Result<Ini> {
log::debug!("begin line-up: segatools");
@ -42,8 +66,12 @@ impl Segatools {
let ini_path = p.config_dir().join("segatools-base.ini");
if !ini_path.exists() {
tokio::fs::write(&ini_path, segatools_base(game)).await
.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?;
match game {
Game::Ongeki => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-ongeki.ini"))
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?,
Game::Chunithm => tokio::fs::write(&ini_path, include_bytes!("../../static/segatools-chunithm.ini"))
.await.map_err(|e| anyhow!("Error creating {:?}: {}", ini_path, e))?
}
}
if !pfx_dir.exists() {
tokio::fs::create_dir(&pfx_dir).await
@ -143,6 +171,31 @@ impl Segatools {
)?;
}
let mut cfg_hook = ConfigHook::default();
if game == Game::Chunithm {
cfg_hook.allnet_auth = Some({
ConfigHookAuth {
r#type: "1.0".to_owned()
}
})
}
if let Some(port) = self.aime_port {
if self.aime == Aime::Disabled {
cfg_hook.aime = Some({
ConfigHookAime {
unit: vec![
ConfigHookAimeUnit {
port,
id: 1
}
]
}
})
}
}
std::fs::write(pfx_dir.join("config_hook.json"), serde_json::to_string(&cfg_hook)?)?;
log::debug!("end line-up: segatools");
Ok(ini_out)

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

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

View File

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use std::{collections::BTreeSet, path::{Path, PathBuf}};
use tokio::fs;
use enumflags2::{bitflags, make_bitflags, BitFlags};
use crate::{model::{local::{self, PackageManifest}, rainy}, util};
use crate::{model::{local::{self, PackageManifest}, misc::Game, rainy}, util};
// {namespace}-{name}
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
@ -14,25 +14,38 @@ pub struct PkgKey(pub String);
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
pub struct PkgKeyVersion(String);
#[derive(Clone, Default, Serialize, Deserialize)]
#[derive(Copy, Clone, Display, Debug, Serialize, Deserialize, Default)]
pub enum PackageSource {
#[default] Rainy,
Local(Game)
}
#[derive(Clone, Default, Serialize, Deserialize, Debug)]
#[allow(dead_code)]
pub struct Package {
pub namespace: String,
pub name: String,
pub description: String,
pub loc: Option<Local>,
pub rmt: Option<Remote>
pub rmt: Option<Remote>,
pub source: PackageSource,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum Status {
Unchecked,
Unsupported,
OK(BitFlags<Feature>)
OK(BitFlags<Feature>, DLLs),
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct DLLs {
pub game: Option<String>,
pub amd: Option<String>
}
#[bitflags]
#[repr(u8)]
#[repr(u16)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Feature {
Mod,
@ -41,9 +54,13 @@ pub enum Feature {
Mu3Hook,
Mu3IO,
ChusanHook,
ChuniIO,
Mempatcher,
GameDLL,
AmdDLL
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Debug)]
#[allow(dead_code)]
pub struct Local {
pub version: String,
@ -53,7 +70,7 @@ pub struct Local {
pub icon: String,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Debug)]
#[allow(dead_code)]
pub struct Remote {
pub version: String,
@ -66,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 {
@ -88,11 +113,12 @@ impl Package {
version: v.version_number,
categories: p.categories,
dependencies: Self::sanitize_deps(v.dependencies)
})
}),
source: PackageSource::Rainy,
})
}
pub async fn from_dir(dir: PathBuf) -> Result<Package> {
pub async fn from_dir(dir: PathBuf, source: PackageSource) -> Result<Package> {
let str = fs::read_to_string(dir.join("manifest.json")).await?;
let mft: local::PackageManifest = serde_json::from_str(&str)?;
@ -116,7 +142,8 @@ impl Package {
status,
dependencies
}),
rmt: None
rmt: None,
source
})
}
@ -125,7 +152,15 @@ impl Package {
}
pub fn path(&self) -> PathBuf {
util::pkg_dir().join(self.key().0)
match self.source {
PackageSource::Rainy => util::pkg_dir().join(self.key().0),
PackageSource::Local(game) =>
util::pkg_dir()
.parent()
.unwrap()
.join(format!("pkg-{game}"))
.join(&self.name),
}
}
pub fn _dir_to_key(dir: &Path) -> Result<String> {
@ -188,31 +223,55 @@ impl Package {
fn parse_status(mft: &PackageManifest) -> Status {
if mft.installers.len() == 0 {
return Status::OK(make_bitflags!(Feature::Mod));//Unchecked
} else if mft.installers.len() == 1 {
if let Some(serde_json::Value::String(id)) = &mft.installers[0].get("identifier") {
if id == "rainycolor" {
return Status::OK(make_bitflags!(Feature::Mod));
} else if id == "segatools" {
// Multiple features in the same dll (yubideck etc.) should be supported at some point
let mut flags = BitFlags::default();
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") {
if module == "mu3hook" {
flags |= Feature::Mu3Hook;
} else if module == "chusanhook" {
flags |= Feature::ChusanHook;
} else if module == "amnet" {
flags |= Feature::AMNet | Feature::Aime;
} else if module == "aimeio" {
flags |= Feature::Aime;
} else if module == "mu3io" {
flags |= Feature::Mu3IO;
return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None }); //Unchecked
} else {
let mut flags = BitFlags::default();
let mut game_dll = None;
let mut amd_dll = None;
for installer in &mft.installers {
if let Some(serde_json::Value::String(id)) = installer.get("identifier") {
if id == "rainycolor" {
flags |= Feature::Mod;
} else if id == "segatools" {
// Multiple features in the same dll (yubideck etc.) should be supported at some point
if let Some(serde_json::Value::String(module)) = installer.get("module") {
if module == "mu3hook" {
flags |= Feature::Mu3Hook;
} else if module == "chusanhook" {
flags |= Feature::ChusanHook;
} else if module == "amnet" {
flags |= Feature::AMNet | Feature::Aime;
} else if module == "aimeio" {
flags |= Feature::Aime;
} else if module == "mu3io" {
flags |= Feature::Mu3IO;
} else if module == "chuniio" {
flags |= Feature::ChuniIO;
}
}
} else if id == "native_mod" {
if let Some(serde_json::Value::String(path)) = installer.get("dll-game") {
flags |= Feature::GameDLL;
flags |= Feature::Mod;
game_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll_game") {
flags |= Feature::GameDLL;
flags |= Feature::Mod;
game_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll-amdaemon") {
flags |= Feature::AmdDLL;
flags |= Feature::Mod;
amd_dll = Some(path.to_owned());
}
} else {
return Status::Unsupported;
}
return Status::OK(flags);
}
}
log::debug!("{} parse result: {:?} {:?} {:?}", mft.name, flags, game_dll, amd_dll);
Status::OK(flags, DLLs { game: game_dll, amd: amd_dll })
}
Status::Unsupported
}
}

View File

@ -8,7 +8,7 @@ use tokio::task::JoinSet;
use crate::model::local::{PackageList, PackageListEntry};
use crate::model::misc::Game;
use crate::model::rainy;
use crate::pkg::{Package, PkgKey, Remote, Status};
use crate::pkg::{Feature, Package, PackageSource, PkgKey, Remote, Status};
use crate::util;
use crate::download_handler::DownloadHandler;
@ -67,9 +67,24 @@ impl PackageStore {
.collect()
}
#[allow(dead_code)]
pub fn get_by_feature(&self, feature: Feature) -> Vec<PkgKey> {
self.store.iter()
.filter(|(_, v)| {
if let Some(loc) = &v.loc {
if let Status::OK(flags, _) = loc.status {
return flags.contains(feature);
}
}
return false
})
.map(|(k, _)| k.clone())
.collect()
}
pub async fn reload_package(&mut self, key: PkgKey) {
let dir = util::pkg_dir().join(&key.0);
if let Ok(pkg) = Package::from_dir(dir).await {
if let Ok(pkg) = Package::from_dir(dir, PackageSource::Rainy).await {
self.update_nonremote(key, pkg);
} else {
log::error!("couldn't reload {}", key);
@ -83,7 +98,7 @@ impl PackageStore {
for dir in dirents {
if let Ok(dir) = dir {
let path = dir.path();
futures.spawn(Package::from_dir(path));
futures.spawn(Package::from_dir(path, PackageSource::Rainy));
}
}
@ -284,6 +299,10 @@ impl PackageStore {
Self::clean_up_file(&path, "manifest.json", true).await?;
Self::clean_up_file(&path, "README.md", true).await?;
Self::clean_up_file(&path, "post_load.ps1", false).await?;
// todo search for the proper dll
Self::clean_up_file(&path, "saekawa.dll", false).await?;
Self::clean_up_file(&path, "mempatcher32.dll", false).await?;
Self::clean_up_file(&path, "mempatcher64.dll", false).await?;
tokio::fs::remove_dir(path.as_ref())
.await

View File

@ -1,61 +1,16 @@
use serde::{Deserialize, Serialize};
use tauri::AppHandle;
use std::{collections::BTreeSet, path::{Path, PathBuf}};
use crate::{model::{misc::Game, profile::{Aime, Mu3Ini, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
use crate::{model::{misc::Game, patch::{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>
}
pub mod types;
impl Profile {
pub fn new(mut meta: ProfileMeta) -> Result<Self> {
@ -66,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(),
@ -74,24 +29,51 @@ impl Profile {
#[cfg(not(target_os = "windows"))]
wine: crate::model::profile::Wine::default(),
mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini { audio: None, blacklist: None }) } else { None },
keyboard:
if meta.game == Game::Ongeki {
Some(Keyboard::Ongeki(OngekiKeyboard::default()))
} else {
Some(Keyboard::Chunithm(ChunithmKeyboard::default()))
},
patches: if meta.game == Game::Chunithm { Some(PatchSelection(BTreeMap::new())) } else { None }
},
meta: meta.clone()
};
p.save()?;
std::fs::create_dir_all(p.config_dir())?;
std::fs::create_dir_all(p.data_dir())?;
std::fs::write(p.config_dir().join("segatools-base.ini"), segatools_base(meta.game))?;
match meta.game {
Game::Ongeki => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-ongeki.ini"))?,
Game::Chunithm => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-chunithm.ini"))?,
};
Ok(p)
}
pub fn load(game: Game, name: String) -> Result<Self> {
let path = util::profile_config_dir(game, &name).join("profile.json");
if let Ok(s) = std::fs::read_to_string(&path) {
let data = serde_json::from_str::<ProfileData>(&s)
let mut data = serde_json::from_str::<ProfileData>(&s)
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
log::debug!("{:?}", data);
// Backwards compat
if game == Game::Ongeki && data.keyboard.is_none() {
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::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 {
meta: ProfileMeta {
game, name
@ -112,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 written to {:?}", path);
Ok(())
}
@ -163,28 +145,24 @@ impl Profile {
if self.meta.game.has_module(ProfileModule::Mu3Ini) && source.mu3_ini.is_some() {
self.data.mu3_ini = source.mu3_ini;
}
}
pub async fn line_up(&self, pkg_hash: String, refresh: bool, _app: AppHandle) -> Result<()> {
let info = match &self.data.display {
None => None,
Some(display) => display.line_up()?
};
let res = self.line_up_the_rest(pkg_hash, refresh).await;
#[cfg(target_os = "windows")]
if let Some(info) = info {
use crate::model::profile::Display;
if res.is_ok() {
Display::wait_for_exit(_app, info);
} else {
Display::clean_up(&info)?;
}
if self.meta.game.has_module(ProfileModule::Keyboard) && source.keyboard.is_some() {
self.data.keyboard = source.keyboard;
}
res
if self.data.patches.is_some() && source.patches.is_some() {
self.data.patches = source.patches;
}
}
async fn line_up_the_rest(&self, pkg_hash: String, refresh: bool) -> Result<()> {
pub fn prepare_display(&self) -> Result<Option<DisplayInfo>> {
let info = match &self.data.display {
None => None,
Some(display) => display.prepare()?
};
Ok(info)
}
pub async fn line_up(&self, pkg_hash: String, refresh: bool, patch_files: &PatchFileVec) -> Result<()> {
if !self.data_dir().exists() {
tokio::fs::create_dir(self.data_dir()).await?;
}
@ -194,12 +172,23 @@ impl Profile {
util::clean_up_opts(self.data_dir().join("option"))?;
let hash_check = Self::hash_check(&hash_path, &pkg_hash).await? || refresh;
prepare_packages(&self.meta, &self.data.mods, hash_check).await
.map_err(|e| anyhow!("package configuration failed:\n{:?}", e))?;
let mut ini = self.data.sgt.line_up(&self.meta, self.meta.game).await
.map_err(|e| anyhow!("segatools configuration failed:\n{:?}", e))?;
self.data.network.line_up(&mut ini)?;
if let Some(display) = &self.data.display {
display.line_up(self.meta.game, &mut ini);
}
if let Some(keyboard) = &self.data.keyboard {
keyboard.line_up(&mut ini)?;
}
ini.write_to_file(self.data_dir().join("segatools.ini"))
.map_err(|e| anyhow!("Error writing segatools.ini: {}", e))?;
@ -211,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);
@ -245,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",
@ -260,23 +269,50 @@ impl Profile {
"INOHARA_CONFIG_PATH",
self.config_dir().join("inohara.cfg"),
)
.env(
"SAEKAWA_CONFIG_PATH",
self.config_dir().join("saekawa.toml"),
)
.current_dir(&exe_dir)
.args(["-d", "-k"])
.arg(sgt_dir.join(self.meta.game.hook_exe()))
.arg(self.meta.game.exe());
.raw_arg("-d")
.raw_arg("-k")
.arg(sgt_dir.join(self.meta.game.hook_exe()));
if let Some(display) = &self.data.display {
game_builder.args([
"-monitor 1",
"-screen-width", &display.rez.0.to_string(),
"-screen-height", &display.rez.1.to_string(),
"-screen-fullscreen", if display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
]);
if display.mode == DisplayMode::Borderless {
game_builder.arg("-popupwindow");
for dll in payload.game_dlls {
game_builder.raw_arg("-k");
game_builder.arg(dll);
}
game_builder.arg(self.meta.game.exe());
if self.meta.game.has_module(ProfileModule::BepInEx) {
if let Some(display) = &self.data.display {
if display.dont_switch_primary && display.target != "default" {
game_builder.args(["-monitor", &display.monitor_index_override.unwrap_or_else(|| 1).to_string()]);
} else {
game_builder.args(["-monitor", "1"]);
}
game_builder.args([
"-screen-width", &display.rez.0.to_string(),
"-screen-height", &display.rez.1.to_string(),
"-screen-fullscreen", if display.mode == DisplayMode::Fullscreen { "1" } else { "0" }
]);
if display.mode == DisplayMode::Borderless {
game_builder.arg("-popupwindow");
}
}
}
if self.meta.game.has_module(ProfileModule::Mempatcher) {
amd_builder
.env("MEMPATCHER_PATCH_PATH", self.data_dir().join("patch-amd.mph"))
.env("MEMPATCHER_LOG_PATH", self.data_dir().join("mempatcher-amdaemon.log"));
game_builder
.raw_arg("--mempatch")
.arg(self.data_dir().join("patch-game.mph"))
.env("MEMPATCHER_LOG_PATH", self.data_dir().join("mempatcher-game.log"));
}
#[cfg(target_os = "linux")]
{
amd_builder.env("WINEPREFIX", &self.wine.prefix);
@ -305,8 +341,8 @@ impl Profile {
util::pkill("amdaemon.exe").await;
log::info!("Launching amdaemon: {:?}", amd_builder);
log::info!("Launching {}: {:?}", self.meta.game, game_builder);
log::info!("launching amdaemon: {:?}", amd_builder);
log::info!("launching {}: {:?}", self.meta.game, game_builder);
let mut amd = amd_builder.spawn()?;
let mut game = game_builder.spawn()?;
@ -321,7 +357,7 @@ impl Profile {
(game.wait().await.expect("game failed to run"), "game")
});
if let Err(e) = app.emit("launch-start", "") {
if let Err(e) = payload.app.emit("launch-start", "") {
log::warn!("Unable to emit launch-start: {}", e);
}
@ -339,7 +375,7 @@ impl Profile {
log::debug!("Fin");
if let Err(e) = app.emit("launch-end", "") {
if let Err(e) = payload.app.emit("launch-end", "") {
log::warn!("Unable to emit launch-end: {}", e);
}

View File

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

View File

@ -150,4 +150,8 @@ 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" }
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,38 @@
<script setup lang="ts">
import { Ref, computed, onMounted, ref } from 'vue';
import Button from 'primevue/button';
import ConfirmDialog from 'primevue/confirmdialog';
import Dialog from 'primevue/dialog';
import InputIcon from 'primevue/inputicon';
import InputText from 'primevue/inputtext';
import ProgressBar from 'primevue/progressbar';
import ScrollPanel from 'primevue/scrollpanel';
import Tab from 'primevue/tab';
import TabList from 'primevue/tablist';
import TabPanel from 'primevue/tabpanel';
import TabPanels from 'primevue/tabpanels';
import Tabs from 'primevue/tabs';
import { listen } from '@tauri-apps/api/event';
import 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';
import { useGeneralStore, usePkgStore, usePrfStore } from '../stores';
import {
useClientStore,
useGeneralStore,
usePkgStore,
usePrfStore,
} from '../stores';
import { Dirs } from '../types';
import { messageSplit } from '../util';
const pkg = usePkgStore();
const prf = usePrfStore();
const general = useGeneralStore();
const client = useClientStore();
pkg.setupListeners();
@ -28,9 +41,20 @@ 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;
client.load();
});
const fetch_promise = pkg.fetch(true);
@ -54,21 +78,85 @@ onMounted(async () => {
});
});
const errorVisible = ref(false);
const errorMessage = ref('No error');
const errorHeader = ref('No header');
listen<{ message: string; header: string }>('invoke-error', (event) => {
errorVisible.value = true;
errorMessage.value = event.payload.message;
errorHeader.value = event.payload.header;
});
</script>
<template>
<main>
<main
:class="
client.scaleFactor === 's'
? 'main-scale-s'
: client.scaleFactor === 'm'
? 'main-scale-m'
: client.scaleFactor === 'l'
? 'main-scale-l'
: 'main-scale-xl'
"
>
<ConfirmDialog>
<template #message="{ message }">
<ScrollPanel
v-if="messageSplit(message).length > 5"
style="width: 100%; height: 40vh"
>
<p v-for="m in messageSplit(message)">
{{ m }}
</p></ScrollPanel
>
<div v-else>
<p v-for="m in messageSplit(message)">
{{ m }}
</p>
</div>
</template>
</ConfirmDialog>
<Dialog
modal
:visible="errorVisible"
:closable="false /*this shit doesn't work */"
:header="errorHeader"
:style="{ width: '50vw' }"
>
<div class="flex flex-col gap-4">
{{ errorMessage }}
<Button
class="m-auto"
label="A sad state of affairs"
@click="errorVisible = false"
/>
</div>
</Dialog>
<Dialog
modal
:visible="updateProgress !== null"
:closable="false"
header="Updating"
:style="{ width: '200px' }"
>
<ProgressBar :value="updateProgress ?? undefined" />
</Dialog>
<Tabs
lazy
:value="currentTab"
v-on:update:value="(value) => (currentTab = value)"
v-on:update:value="
(value) => {
currentTab = value;
}
"
class="h-screen"
>
<div class="fixed w-full flex z-100">
<TabList class="grow">
<Tab :value="3"
><div class="pi pi-question-circle"></div
></Tab>
<TabList class="grow" :show-navigators="false">
<Tab :value="3"><div class="pi pi-users"></div></Tab>
<Tab :disabled="isProfileDisabled" :value="0"
><div class="pi pi-box"></div
></Tab>
@ -86,13 +174,18 @@ onMounted(async () => {
></Tab>
<div class="grow"></div>
<div class="flex gap-4">
<div class="flex" v-if="currentTab !== 3">
<div
class="flex"
v-if="[0, 1, 2].includes(currentTab as number)"
>
<InputIcon class="self-center mr-2">
<i class="pi pi-search" />
</InputIcon>
<InputText
v-if="currentTab === 2"
style="min-width: 0; width: 25dvw"
class="self-center"
size="small"
placeholder="Search"
@ -100,6 +193,7 @@ onMounted(async () => {
/>
<InputText
v-else
style="min-width: 0; width: 25dvw"
class="self-center"
size="small"
placeholder="Search"
@ -114,12 +208,26 @@ onMounted(async () => {
:disabled="true"
/>
<Button
v-if="pkg.networkStatus === 'offline'"
v-if="
pkg.networkStatus === 'offline' &&
!client.offlineMode
"
class="shrink self-center"
icon="pi pi-sync"
size="small"
@click="pkg.fetch(false)"
/>
<Button
v-if="
pkg.networkStatus === 'online' &&
pkg.hasAvailableUpdates
"
icon="pi pi-download"
label="UPDATE ALL"
size="small"
class="mr-4 m-2.5"
@click="pkg.updateAll()"
></Button>
</div>
<div class="grow"></div>
<StartButton />
@ -136,9 +244,6 @@ onMounted(async () => {
<OptionList />
</TabPanel>
<TabPanel :value="3">
<strong>UNDER CONSTRUCTION</strong><br />Some features are
missing.<br />Existing features are expected to break
sometimes.
<ProfileList />
<br /><br /><br />
<footer>
@ -151,8 +256,17 @@ onMounted(async () => {
</footer>
</TabPanel>
<TabPanel :value="4">
CHUNITHM patches are not yet implemented.<br />Use
<a href=https://patcher.two-torial.xyz/ target="_blank" style="text-decoration: underline;">patcher.two-torial.xyz</a>
<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>
</TabPanels>
<div v-if="currentTab === 5 || currentTab === 3">
@ -184,4 +298,42 @@ body {
margin: 0;
padding: 0;
}
.main-scale-s {
zoom: 1;
}
.main-scale-m {
zoom: 1.25;
}
.main-scale-l {
zoom: 1.4;
}
.main-scale-xl {
zoom: 1.7;
}
.p-tablist {
background-color: #ffffff !important;
border-bottom: white 10px !important;
}
.p-tab {
border-bottom: none !important;
}
.p-tablist-active-bar {
display: none !important;
}
.p-tooltip {
min-width: 300px;
}
.p-progressbar,
.p-progressbar-value,
.p-progressbar-label {
transition-duration: 0s !important;
}
</style>

View File

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

View File

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

View File

@ -0,0 +1,245 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import InputText from 'primevue/inputtext';
import { usePrfStore } from '../stores';
import { OngekiButtons } from '../types';
const prf = usePrfStore();
const hasClickedM1Once = ref(false);
const handleKey = (
button: string | undefined,
event: KeyboardEvent,
index?: number
) => {
event.preventDefault();
const keycode = toKeycode(event.code);
if (keycode !== null && button !== undefined) {
const data = prf.current!.data.keyboard!.data as any;
if (index !== undefined) {
data[button][index] = keycode;
} else {
data[button] = keycode;
}
}
};
const handleMouse = (
button: string | undefined,
event: MouseEvent,
index?: number
) => {
if (button === undefined || button == 'use_mouse') {
return;
}
if (event.button === 0) {
if (hasClickedM1Once.value === false) {
hasClickedM1Once.value = true;
return;
}
} else {
event.preventDefault();
}
let keycode;
switch (event.button) {
case 0:
keycode = 1;
break;
case 1:
keycode = 4;
break;
case 2:
keycode = 2;
break;
case 3:
keycode = 5;
break;
case 4:
keycode = 6;
break;
default:
break;
}
if (keycode !== undefined) {
const data = prf.current!.data.keyboard!.data as any;
if (index !== undefined) {
data[button][index] = keycode;
} else {
data[button] = keycode;
}
}
};
const getKey = (key: keyof OngekiButtons, index?: number) =>
computed(() => {
const data = prf.current!.data.keyboard?.data as any;
const keycode =
index === undefined
? (data[key] as number | undefined)
: (data[key]?.[index] as number | undefined);
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '';
});
const KEY_MAP: { [key: number]: string } = {
1: 'M1',
2: 'M2',
4: 'M3',
5: 'M4',
6: 'M5',
8: 'Backspace',
9: 'Tab',
13: 'Enter',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: 'Space',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
45: 'Insert',
46: 'Delete',
48: 'Digit0',
49: 'Digit1',
50: 'Digit2',
51: 'Digit3',
52: 'Digit4',
53: 'Digit5',
54: 'Digit6',
55: 'Digit7',
56: 'Digit8',
57: 'Digit9',
65: 'KeyA',
66: 'KeyB',
67: 'KeyC',
68: 'KeyD',
69: 'KeyE',
70: 'KeyF',
71: 'KeyG',
72: 'KeyH',
73: 'KeyI',
74: 'KeyJ',
75: 'KeyK',
76: 'KeyL',
77: 'KeyM',
78: 'KeyN',
79: 'KeyO',
80: 'KeyP',
81: 'KeyQ',
82: 'KeyR',
83: 'KeyS',
84: 'KeyT',
85: 'KeyU',
86: 'KeyV',
87: 'KeyW',
88: 'KeyX',
89: 'KeyY',
90: 'KeyZ',
91: 'MetaLeft',
92: 'MetaRight',
93: 'ContextMenu',
96: 'Numpad0',
97: 'Numpad1',
98: 'Numpad2',
99: 'Numpad3',
100: 'Numpad4',
101: 'Numpad5',
102: 'Numpad6',
103: 'Numpad7',
104: 'Numpad8',
105: 'Numpad9',
106: 'NumpadMultiply',
107: 'NumpadAdd',
109: 'NumpadSubtract',
110: 'NumpadDecimal',
111: 'NumpadDivide',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
160: 'ShiftLeft',
161: 'ShiftRight',
162: 'ControlLeft',
163: 'ControlRight',
164: 'AltLeft',
165: 'AltRight',
186: 'Semicolon',
187: 'Equal',
188: 'Comma',
189: 'Minus',
190: 'Period',
191: 'Slash',
192: 'Backquote',
219: 'BracketLeft',
220: 'Backslash',
221: 'BracketRight',
222: 'Quote',
};
const fromKeycode = (keyCode: number): string | null => {
return KEY_MAP[keyCode] ?? null;
};
const toKeycode = (key: string): number | null => {
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
return res ? parseInt(res) : null;
};
defineProps({
small: Boolean,
verySmall: Boolean,
tall: Boolean,
tooltip: String,
button: String,
color: String,
index: Number,
});
</script>
<template>
<InputText
:style="{
width: small ? '3em' : '5em',
height: small ? '3em' : tall ? '10em' : '5em',
fontSize: small ? '0.9em' : '1em',
backgroundColor: color,
}"
unstyled
class="text-center buttoninputtext"
v-tooltip="tooltip"
@contextmenu.prevent="() => {}"
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
@mousedown="
(ev: MouseEvent) =>
handleMouse(button as keyof OngekiButtons, ev, index)
"
@focusout="() => (hasClickedM1Once = false)"
:model-value="getKey(button as keyof OngekiButtons, index) as any"
/>
</template>
<style scoped lang="css">
.buttoninputtext {
border-radius: 6px;
border: 1px solid rgba(200, 200, 200, 0.3);
}
</style>

View File

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

View File

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

View File

@ -7,9 +7,11 @@ import OptionCategory from './OptionCategory.vue';
import OptionRow from './OptionRow.vue';
import AimeOptions from './options/Aime.vue';
import DisplayOptions from './options/Display.vue';
import KeyboardOptions from './options/Keyboard.vue';
import MiscOptions from './options/Misc.vue';
import NetworkOptions from './options/Network.vue';
import SegatoolsOptions from './options/Segatools.vue';
import StartlinerOptions from './options/Startliner.vue';
import { usePrfStore } from '../stores';
const prf = usePrfStore();
@ -67,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'"
@ -128,6 +141,8 @@ prf.reload();
v-model="blacklistMaxModel"
/></OptionRow> -->
</OptionCategory>
<KeyboardOptions />
<StartlinerOptions />
</template>
<style>

View File

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

View File

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

View File

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

View File

@ -7,6 +7,9 @@ const prf = usePrfStore();
</script>
<template>
<div v-if="prf.list.length === 0">
Welcome to STARTLINER! Start by creating a profile.
</div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button
label="O.N.G.E.K.I. profile"

View File

@ -74,6 +74,8 @@ const deleteProfile = async () => {
<div v-if="!isEditing">{{ p!.name }}</div>
<div v-else>
<InputText
unstyled
class="text-center"
:model-value="p!.name"
@vue:mounted="$event?.el?.focus()"
@keyup="renameProfile"
@ -127,3 +129,11 @@ const deleteProfile = async () => {
/>
</div>
</template>
<style lang="css">
.p-tablist-tab-list {
border: none !important;
border-color: transparent !important;
border-radius: 0 !important;
}
</style>

View File

@ -1,9 +1,7 @@
<script setup lang="ts">
import { Ref, computed, ref } from 'vue';
import Button from 'primevue/button';
import ConfirmDialog from 'primevue/confirmdialog';
import ContextMenu from 'primevue/contextmenu';
import ScrollPanel from 'primevue/scrollpanel';
import { useConfirm } from 'primevue/useconfirm';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
@ -38,6 +36,8 @@ const startline = async (force: boolean, refresh: boolean) => {
confirmDialog.require({
message: message.join('\n'),
header: 'Start check failed',
acceptLabel: 'Run anyway',
rejectLabel: 'Cancel',
accept: () => {
startline(true, refresh);
},
@ -85,10 +85,6 @@ listen('launch-end', () => {
getCurrentWindow().setFocus();
});
const messageSplit = (message: any) => {
return message.message?.split('\n');
};
const menuItems = [
{
label: 'Refresh and start',
@ -111,45 +107,6 @@ const showContextMenu = (event: Event) => {
<template>
<ContextMenu ref="menu" :model="menuItems" />
<ConfirmDialog>
<template #container="{ message, acceptCallback, rejectCallback }">
<div
class="flex flex-col p-8 bg-surface-0 dark:bg-surface-900 rounded"
>
<span class="font-bold self-center text-2xl block mb-2 mt-2">{{
message.header
}}</span>
<ScrollPanel
v-if="messageSplit(message).length > 5"
style="width: 100%; height: 40vh"
>
<p v-for="m in messageSplit(message)">
{{ m }}
</p></ScrollPanel
>
<div v-else>
<p v-for="m in messageSplit(message)">
{{ m }}
</p>
</div>
<div class="flex self-center items-center gap-2 mt-6">
<Button
label="Run anyway"
@click="acceptCallback"
size="small"
class="w-32"
></Button>
<Button
label="Cancel"
outlined
size="small"
@click="rejectCallback"
class="w-32"
></Button>
</div>
</div>
</template>
</ConfirmDialog>
<Button
v-if="startStatus === 'ready'"
v-tooltip="disabledTooltip"

View File

@ -32,7 +32,7 @@ const install = async () => {
<Button
v-if="needsUpdate(pkg) && !pkg?.js.busy"
rounded
icon="pi pi-sync"
icon="pi pi-download"
severity="success"
aria-label="remove"
size="small"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,8 @@ import { Ref, computed, ref, watchEffect } from 'vue';
import { defineStore } from 'pinia';
import { listen } from '@tauri-apps/api/event';
import * as path from '@tauri-apps/api/path';
import { PhysicalSize, getCurrentWindow } from '@tauri-apps/api/window';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
import { invoke, invoke_nopopup } from './invoke';
import { Dirs, Feature, Game, Package, Profile, ProfileMeta } from './types';
import { changePrimaryColor, hasFeature, pkgKey } from './util';
@ -102,6 +104,10 @@ export const usePkgStore = defineStore('pkg', {
),
byFeature: (state) => (feature: Feature) =>
Object.values(state.pkg).filter((p) => hasFeature(p, feature)),
hasAvailableUpdates: (state) =>
Object.values(state.pkg).some(
(p) => p.loc && (p.rmt?.version ?? 0) > p.loc.version
),
},
actions: {
setupListeners() {
@ -170,6 +176,36 @@ export const usePkgStore = defineStore('pkg', {
}
await this.reloadAll();
},
async install(pkg: Package | undefined) {
if (pkg === undefined) {
return;
}
try {
await invoke('install_package', {
key: pkgKey(pkg),
force: true,
});
} catch (err) {
console.error(err);
if (pkg !== undefined) {
pkg.js.busy = false;
}
}
//if (rv === 'Deferred') { /* download progress */ }
},
async updateAll() {
const list = [];
for (const pkg of this.allLocal) {
if (pkg.rmt && pkg.rmt.version > pkg.loc!.version) {
list.push(this.install(pkg));
}
}
await Promise.all(list);
},
},
});
@ -186,6 +222,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;
@ -286,6 +328,7 @@ export const usePrfStore = defineStore('prf', () => {
current,
list,
isPkgEnabled,
isPkgKeyEnabled,
reload,
create,
rename,
@ -295,3 +338,134 @@ export const usePrfStore = defineStore('prf', () => {
configDir,
};
});
export const useClientStore = defineStore('client', () => {
type ScaleType = 's' | 'm' | 'l' | 'xl';
const scaleFactor: Ref<ScaleType> = ref('s');
const timeout: Ref<NodeJS.Timeout | null> = ref(null);
const offlineMode = ref(false);
const enableAutoupdates = ref(true);
const scaleValue = (value: ScaleType) =>
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
const setScaleFactor = async (value: ScaleType) => {
scaleFactor.value = value;
const window = getCurrentWindow();
const w = Math.floor(scaleValue(value) * 900);
const h = Math.floor(scaleValue(value) * 480);
let size = await window.innerSize();
window.setMinSize(new PhysicalSize(w, h));
window.setSize(
new PhysicalSize(Math.max(w, size.width), Math.max(h, size.height))
);
};
const scaleModel = computed({
get() {
return scaleFactor;
},
async set(value: ScaleType) {
await setScaleFactor(value);
await save();
},
});
const load = async () => {
const generalStore = useGeneralStore();
try {
const input = JSON.parse(
await readTextFile(
await path.join(
generalStore.configDir,
'client-options.json'
)
)
);
if (input.windowSize) {
getCurrentWindow().setSize(
new PhysicalSize(input.windowSize.w, input.windowSize.h)
);
}
if (input.scaleFactor) {
await setScaleFactor(input.scaleFactor);
}
} catch (e) {
console.error(`Error reading client options: ${e}`);
}
offlineMode.value = await invoke('get_global_config', {
field: 'OfflineMode',
});
enableAutoupdates.value = await invoke('get_global_config', {
field: 'EnableAutoupdates',
});
};
const save = async () => {
const generalStore = useGeneralStore();
const w = getCurrentWindow();
const size = await w.innerSize();
await writeTextFile(
await path.join(generalStore.configDir, 'client-options.json'),
JSON.stringify({
scaleFactor: scaleFactor.value,
windowSize: {
w: Math.floor(size.width),
h: Math.floor(size.height),
},
})
);
};
const queueSave = async () => {
if (timeout.value !== null) {
clearTimeout(timeout.value);
}
timeout.value = setTimeout(async () => {
timeout.value = null;
await save();
}, 1000);
};
const setOfflineMode = async (value: boolean) => {
offlineMode.value = value;
await invoke('set_global_config', { field: 'OfflineMode', value });
};
const setAutoupdates = async (value: boolean) => {
enableAutoupdates.value = value;
await invoke('set_global_config', {
field: 'EnableAutoupdates',
value,
});
};
getCurrentWindow().onResized(async ({ payload }) => {
// For whatever reason this is 0 when minimized
if (payload.width > 0) {
await queueSave();
}
});
return {
scaleFactor,
offlineMode,
enableAutoupdates,
timeout,
scaleModel,
load,
save,
queueSave,
setOfflineMode,
setAutoupdates,
};
});

View File

@ -30,13 +30,17 @@ export enum Feature {
Mu3Hook = 1 << 3,
Mu3IO = 1 << 4,
ChusanHook = 1 << 5,
ChuniIO = 1 << 6,
Mempatcher = 1 << 7,
GameDLL = 1 << 8,
AmdDLL = 1 << 9,
}
export type Status =
| 'Unchecked'
| 'Unsupported'
| {
OK: Feature;
OK: [Feature, String, String];
};
export type Game = 'ongeki' | 'chunithm';
@ -53,6 +57,10 @@ export interface ProfileData {
network: NetworkConfig;
bepinex: BepInExConfig;
mu3_ini: Mu3IniConfig | undefined;
keyboard: KeyboardConfig | undefined;
patches: {
[key: string]: 'enabled' | { number: number } | { hex: Int8Array };
};
}
export interface SegatoolsConfig {
@ -69,15 +77,18 @@ export interface SegatoolsConfig {
addr: string;
physical: boolean;
};
aime_port: number;
}
export interface DisplayConfig {
target: String;
rez: [number, number];
mode: 'Window' | 'Borderless' | 'Fullscreen';
rotation: number;
rotation: number | null;
frequency: number;
borderless_fullscreen: boolean;
dont_switch_primary: boolean;
monitor_index_override: number | null;
}
export interface NetworkConfig {
@ -99,6 +110,43 @@ export interface Mu3IniConfig {
// blacklist?: [number, number];
}
export interface OngekiButtons {
use_mouse: boolean;
enabled: boolean;
coin: number;
svc: number;
test: number;
lmenu: number;
rmenu: number;
l1: number;
l2: number;
l3: number;
r1: number;
r2: number;
r3: number;
lwad: number;
rwad: number;
}
export interface ChunithmButtons {
enabled: boolean;
coin: number;
svc: number;
test: number;
cell: number[];
ir: number[];
}
export type KeyboardConfig =
| {
game: 'Ongeki';
data: OngekiButtons;
}
| {
game: 'Chunithm';
data: ChunithmButtons;
};
export interface Profile {
meta: ProfileMeta;
data: ProfileData;
@ -111,3 +159,13 @@ export interface Dirs {
data_dir: string;
cache_dir: string;
}
export interface Patch {
id: string;
name: string;
tooltip: string;
type: undefined | 'number';
default: number;
min: number;
max: number;
}

View File

@ -52,6 +52,10 @@ export const hasFeature = (pkg: Package | undefined, feature: Feature) => {
pkg.loc !== null &&
pkg.loc !== undefined &&
typeof pkg.loc?.status !== 'string' &&
pkg.loc.status.OK & feature
pkg.loc.status.OK[0] & feature
);
};
export const messageSplit = (message: any) => {
return message.message?.split('\n');
};