Compare commits

..

98 Commits

Author SHA1 Message Date
SpikeHD
c4526e2ff6 version bump 2022-09-09 17:02:43 -07:00
SpikeHD
80c28669e1 conditionally wipe reg 2022-09-09 16:58:53 -07:00
SpikeHD
5e48a4772e update resources url 2022-09-09 16:58:11 -07:00
SpikeHD
10234bed5a upload MSI bundle 2022-09-04 18:55:13 -07:00
SpikeHD
22d8a23386 version bump 2022-09-04 18:35:38 -07:00
SpikeHD
6ef3e86820 build conditions 2022-09-04 18:13:57 -07:00
SpikeHD
0087801f83 fix linux check 2022-09-04 18:12:31 -07:00
SpikeHD
55be3ebc2b remove cmd requirement 2022-09-04 18:08:02 -07:00
SpikeHD
7cf1b198ff open as admin (windows only) 2022-09-04 18:05:19 -07:00
SpikeHD
fb231acaa6 easy-zip is not easy 2022-09-04 16:55:38 -07:00
SpikeHD
827b8942c9 action fixes 2022-09-04 15:24:37 -07:00
SpikeHD
0fd4376e0d change artifact names 2022-09-04 14:59:05 -07:00
SpikeHD
d6c5463619 include appimage 2022-09-04 14:40:56 -07:00
SpikeHD
9ade56dc6f fix file copy linux 2022-09-04 14:38:52 -07:00
SpikeHD
484cd36565 format 2022-09-04 14:20:36 -07:00
SpikeHD
1c27ae172e lint fixes 2022-09-04 14:18:18 -07:00
SpikeHD
541352c3fd Merge pull request #83 from 4Benj/main
Fix some filesystem related bugs
2022-09-04 14:09:03 -07:00
SpikeHD
bfbf3e77a2 prettier fix 2022-09-04 14:05:24 -07:00
SpikeHD
404e946e7c change path/command option based on platform 2022-09-04 14:00:27 -07:00
Benj
8077285e79 Game version checking 2022-09-04 20:15:05 +08:00
SpikeHD
94c1bfd104 build on push/pr 2022-09-03 22:29:31 -07:00
SpikeHD
ed473ad659 run_command for ubuntu build 2022-09-03 22:22:01 -07:00
SpikeHD
b624ef693e sudo 2022-09-03 19:11:53 -07:00
SpikeHD
c10a5cd82f get libs ubuntu 2022-09-03 19:01:21 -07:00
SpikeHD
25c6a70dc0 action name 2022-09-03 18:47:03 -07:00
SpikeHD
86c595abda ubuntu build 2022-09-03 18:46:13 -07:00
SpikeHD
35e6144733 use node 16 2022-09-03 17:57:56 -07:00
SpikeHD
565f229dac fix toolchain install 2022-09-03 17:55:01 -07:00
SpikeHD
9bd4b9ccaf clippy stuff 2022-09-03 17:52:57 -07:00
SpikeHD
6750787bf9 build action 2022-09-03 17:51:47 -07:00
SpikeHD
c0770606ae lint fixes 2022-09-03 17:22:00 -07:00
SpikeHD
4e03fec2a0 Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-09-02 20:21:24 -07:00
SpikeHD
e7809be97c only show horny mode when swag mode is set 2022-09-02 20:20:49 -07:00
SpikeHD
782e350ae5 unblur when horny mode is on 2022-09-02 20:17:48 -07:00
SpikeHD
6eab66032b horny mode option 2022-09-02 20:11:14 -07:00
SpikeHD
c1842722b4 make toggles less dogshit 2022-09-02 19:56:33 -07:00
SpikeHD
31aef02d5f Update README.md 2022-08-31 11:06:46 -07:00
SpikeHD
bcdbb2ba06 Update README.md 2022-08-31 11:06:21 -07:00
Benj
ff8f35c52a Stop popping the directory when we need it later 2022-08-31 18:28:28 +08:00
Benj
a7188828fa Fix os error 123 while reading key files 2022-08-31 17:18:53 +08:00
jseniuk
db6917df5d fix merge conflict 2022-08-30 19:06:49 -07:00
jseniuk
8268c127a9 linux proxyworking probably 2022-08-30 19:02:20 -07:00
jseniuk
8fd5b895af 1-time cert retry 2022-08-30 18:02:48 -07:00
SpikeHD
10b9141815 fix compile errors 2022-08-28 21:59:40 -07:00
SpikeHD
d7783c5936 fix compile issues 2022-08-28 21:57:35 -07:00
SpikeHD
27122cd399 Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-08-28 21:08:09 -07:00
SpikeHD
49740470ac wip linux proxy stuff 2022-08-28 21:07:22 -07:00
SpikeHD
7b693d7758 Update README.md 2022-08-28 19:15:59 -07:00
SpikeHD
03439c3757 launcher.exe alert 2022-08-27 19:47:28 -07:00
SpikeHD
e83ae64714 webview disclaimer for win7 users 2022-08-27 14:47:23 -07:00
SpikeHD
3ecd13d1c3 Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-08-27 14:45:18 -07:00
SpikeHD
28701ba007 delete backed up meta when patch is unsuccessful against it 2022-08-27 14:43:39 -07:00
SpikeHD
3a84825cf9 Merge pull request #60 from Kawaa-qwq/main
Translation languages are supported in the README file and all matched files use Prettier code style!
2022-08-27 01:01:14 -07:00
SpikeHD
4de8a43c3a begin CLI options 2022-08-27 00:54:14 -07:00
SpikeHD
d28e0a1bc8 Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-08-26 17:54:26 -07:00
SpikeHD
bf8de40caa wipe registry option 2022-08-26 17:53:45 -07:00
SpikeHD
8e7d6ee420 Merge pull request #75 from Pikachubolk/patch-1
Dutch Cultivation Translation
2022-08-26 12:08:53 -07:00
Pikachubolk
d6e9bb100b Rename nl.json to src-tauri/lang/nl.json 2022-08-26 20:55:25 +02:00
Pikachubolk
374c6abbfc Create nl.json 2022-08-26 20:51:10 +02:00
SpikeHD
a728e8ba2c Merge pull request #74 from Arikatsu/main
Fix "visual bug" (again)
2022-08-26 10:32:56 -07:00
Scald
4371804429 Update download.ts 2022-08-26 21:15:04 +05:30
SpikeHD
fa38a22117 version bump 2022-08-25 20:19:44 -07:00
SpikeHD
a7914406b4 get ALL mods at once 2022-08-25 20:18:58 -07:00
SpikeHD
57c1a7800c write game path to migoto config autmatically 2022-08-25 20:08:44 -07:00
SpikeHD
9426937a62 cleanup 2022-08-25 19:05:51 -07:00
SpikeHD
96d69d9ff6 bump initial wait for game to 10 seconds 2022-08-25 19:04:08 -07:00
SpikeHD
defc1b43bd fix all sorts of zip stuff 2022-08-25 18:57:24 -07:00
SpikeHD
01fce477ef Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-08-23 23:52:21 -07:00
SpikeHD
2f610ce0af version bump 2022-08-23 23:52:11 -07:00
SpikeHD
d9772d9ffb resource link update 2022-08-23 23:47:52 -07:00
Kawaa-qwq
569c465a50 Update 2022-08-22 13:51:22 +08:00
Kawaa
8a5e66be48 更新日志 2022-08-06 10:47:32 +08:00
Kawaa
e633f7dad7 Updated instructions 2022-08-05 21:38:10 +08:00
Kawaa
bffd089192 Updated instructions 2022-08-05 21:29:47 +08:00
Kawaa
bf3d892af5 Updated instructions 2022-08-05 21:28:15 +08:00
Kawaa
424dd4ff1c Updated instructions 2022-08-05 21:22:40 +08:00
SpikeHD
93097db5cc Merge pull request #58 from Untitled/main
Chinese Simplified Translations Update
2022-08-04 10:55:53 -07:00
SpikeHD
c06aa9f885 Merge pull request #57 from Kimi898246/patch-3
Traditional Chinese | Translation Patches
2022-08-04 10:55:21 -07:00
ChapterII
f7bcad0a90 Update chs.json
Updated translations
2022-08-04 15:04:27 +08:00
Kimi
7120f846f7 Traditional Chinese | Translation Patches
hi
2022-08-04 02:18:21 +08:00
SpikeHD
f6ed37d2e4 Merge pull request #53 from DasIschBims/main
Update de.json
2022-08-02 17:21:54 -07:00
DasIschBims
99b210b3be Update de.json 2022-07-30 13:49:13 +02:00
SpikeHD
7d52766c07 Merge pull request #51 from TotalyNotOndre/patch-1
Update ru.json
2022-07-28 12:18:13 -07:00
Abdul
212d974ac6 Fixed missing separators 2022-07-28 20:23:55 +03:00
SpikeHD
a6e06e3005 revert to 50 per page 2022-07-27 21:52:26 -07:00
Abdul
56c1f2dcc2 Update ru.json
- Added and translated missing localization strings
2022-07-28 03:33:22 +03:00
SpikeHD
a843888cb8 cleanup 2022-07-27 17:01:55 -07:00
SpikeHD
281bfb5cea cheeky key path fix 2022-07-26 22:30:25 -07:00
SpikeHD
79891238b6 fix reshade 2022-07-26 20:59:33 -07:00
SpikeHD
0971f5b826 progress bar 'fix' 2022-07-26 20:49:20 -07:00
SpikeHD
7e5f3be4fa cleanup 2022-07-26 20:41:55 -07:00
SpikeHD
e29e269c4c Merge branch 'mod_management' 2022-07-26 20:39:54 -07:00
SpikeHD
75f1eef587 help text for some options 2022-07-25 18:45:48 -07:00
SpikeHD
43a6348b7a cleanup 2022-07-25 18:28:06 -07:00
SpikeHD
cc74107dfe move help buttons to alerts 2022-07-25 18:26:04 -07:00
SpikeHD
afa40f437f english fallback when using other languages 2022-07-25 18:13:37 -07:00
SpikeHD
a06a8af7df Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-07-24 16:43:59 -07:00
SpikeHD
cd628b4f3d open with cwd, restore akebi option stuff 2022-07-21 19:49:51 -07:00
43 changed files with 1247 additions and 307 deletions

85
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
name: Build
on:
push:
paths:
- '.github/workflows/build.yml'
- 'src-tauri/**/*'
- 'src/**/*'
pull_request:
paths:
- '.github/workflows/build.yml'
- 'src-tauri/**/*'
- 'src/**/*'
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
jobs:
build-win:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: setup node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install deps and build
run: yarn && yarn build --debug
- name: Compress build
uses: vimtor/action-zip@v1
with:
files: src-tauri/target/debug/lang/ src-tauri/target/debug/keys/ src-tauri/target/debug/Cultivation.exe src-tauri/target/debug/bundle/msi/
recursive: true
dest: Cultivation.zip
- name: Upload build
uses: actions/upload-artifact@v3
with:
name: CultivationWin
path: Cultivation.zip
build-ubuntu:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: setup node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install libraries
run: sudo apt install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
- name: Install deps and build
run: yarn && yarn build --debug
- name: Compress build
uses: vimtor/action-zip@v1
with:
files: src-tauri/target/debug/lang/ src-tauri/target/debug/keys/ src-tauri/target/debug/cultivation
recursive: true
dest: Cultivation.zip
- name: Upload build
uses: actions/upload-artifact@v3
with:
name: CultivationLinux
path: Cultivation.zip

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged yarn lint-staged --allow-empty

View File

@@ -1,17 +1,14 @@
# Client Patching Notice EN | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) |
For game versions 2.8 and above, Cultivation automatically makes a small patch to your game client when launching using Grasscutter, and restores it upon closing the game. In theory, you should still be totally safe, however it would be dishonest to not explicitly state that **modifying the game client could, theoretically, lead to a ban if you connect to official servers with it**. It is extremely unlikely AND there are no instances known of it happening, but the possibility exists.
# Cultivation # Cultivation
A game launcher designed to easily proxy traffic from anime game to private servers. A game launcher designed to easily proxy traffic from anime game to private servers.
While the Cultivation repository is **open**. This does **not** mean it has released.
Please do **NOT install, download, or use pre-compiled versions of Cultivation found elsewhere**. Only use releases from this GitHub repository.
# Table Of Contents # Table Of Contents
- [Client Patching Notice](#client-patching-notice)
- [Download](#download) - [Download](#download)
- [Setup](#setup)
- [Developer Quick-start](#developer-quickstart) - [Developer Quick-start](#developer-quickstart)
- [Setup](#setup) - [Setup](#setup)
- [Building](#building) - [Building](#building)
@@ -21,11 +18,45 @@ Please do **NOT install, download, or use pre-compiled versions of Cultivation f
- [Screenshots](#screenshots) - [Screenshots](#screenshots)
- [Credits](#credits) - [Credits](#credits)
# Client Patching Notice
For game versions 2.8 and above, Cultivation automatically makes a small patch to your game client when launching using Grasscutter, and restores it upon closing the game. In theory, you should still be totally safe, however it would be dishonest to not explicitly state that **modifying the game client could, theoretically, lead to a ban if you connect to official servers with it**. It is extremely unlikely AND there are no instances known of it happening, but the possibility exists.
# Download # Download
[Find release builds here!](https://github.com/Grasscutters/Cultivation/releases) [Find release builds here!](https://github.com/Grasscutters/Cultivation/releases)
Once downloaded, extract somewhere and open as administrator. Download and open the MSI, and once installed, run Cultivation as administrator. Refer below for more [detailed setup instructions](#setup).
**Windows 7 Users:** You will need to download [WebView](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) manually, and download the `.zip` instead of the `.msi`.
# Setup
5-minute video for those who don't like to/cannot read: https://youtu.be/e0irOYbQe7I
- Download Cultivation
- If you are on Windows 10 or 11, use the MSI
- If you are on Windows 7, or the MSI doesn't work, use the zip and download [WebView](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)
- If you are on Linux or MacOS, [help us port Windows-specific system calls to Linux/MacOS!](https://github.com/Grasscutters/Cultivation/issues/7)
- Install or extract Cultivation
- Open Cultivation **_as administrator_**
- Before clicking randomly on stuff, in options (top right cog icon), set your Game Install Path.
- If you are using an existing server installation from somewhere else, you can set the `.jar` file in settings as well. All downloads made through Culti will automatically use that path, no additional config needed.
- If you use multiple Java versions, you can set the Java path to your Java 17 installation (only required if you are running your own server)
- Decide if you want to download your own server, or just join a public one
- If joining a public one, you're done. Just click "Connect with Grasscutter" and input the address and port. You do not have to continue these instructions.
- If you are getting System Error, or 4214, ask the [Discord support channels](https://discord.gg/grasscutter)
- Open the "Downloads" menu (top right)
- Download "latest grasscutter" (second from the top)
- Download "resources" (very bottom)
- Once all of that is done, click the icon next to "Launch"
- To play on your new server:
- Click "Connect with Grasscutter"
- Input `localhost` as the address, and `443` as the port
- Ensure HTTPS is disabled
- Any generic "I am getting XYZ error!" should go in the [Discord support channels](https://discord.gg/grasscutter)
- Any specific Cultivation issues should go in [the issues section](/issues)
- Any Grasscutter server related issues should go in [the Grasscutter issues section](https://github.com/Grasscutters/Grasscutter)
# Developer Quickstart # Developer Quickstart

88
README_zh-CN.md Normal file
View File

@@ -0,0 +1,88 @@
[EN](README.md) | 简中 | [繁中](README_zh-TW.md)
# 客户端修补通知
对于游戏版本为 2.8 及以上时,使用 Grasscutter 启动时Cultivation 会自动为您的游戏客户端制作一个小补丁,并在关闭游戏时恢复它。 从理论上讲,你应该是完全安全的,但是不明确**如果您使用它连接到官方服务器,修改游戏客户端可能会导致封号**,但可能性是非常小的,并且从未接到发生过此类情况的问题,但存在这种可能性!
# Cultivation
一个游戏启动器,旨在轻松将某动漫游戏的流量代理到私人服务器。
虽然此存储库是**开放的**。 但这**并不**意味着它已经发布。
请不要**安装、下载或使用在其他地方找到的预编译版本的 Cultivation**。 仅使用此 GitHub 存储库中的版本。
# 目录
- [下载](#下载)
- [开发人员快速入门](#开发人员快速入门)
- [安装](#安装)
- [编译](#编译)
- [代码格式化与纠错](#代码格式化与纠错)
- [生成更新项目](#生成更新项目)
- [启动器主题](#启动器主题)
- [画面](#画面)
- [成员](#成员)
# 下载
[在此处查找发布版本!](https://github.com/Grasscutters/Cultivation/releases)
下载后,从某个位置解压缩并以管理员身份打开。
# 开发人员快速入门
### 安装
- 安装 [NodeJS >12](https://nodejs.org/en/)
- 安装 [yarn](https://classic.yarnpkg.com/lang/en/docs/install)
- 安装 [Rust](https://www.rust-lang.org/tools/install)
- `yarn install`
- `yarn start:dev`
### 编译
发布版本,
- `yarn build`
调试版本,
- `yarn build --debug`
### 代码格式化与纠错
格式化:
- `yarn format`
纠错, 修复(一些)错误:
- `yarn lint`, `yarn lint:fix`
### 生成更新项目
-`TAURI_PRIVATE_KEY` 添加到环境变量,其中包含私钥的路径。
-`TAURI_KEY_PASSWORD` 添加到环境变量,其中包含私钥的密码。
- `yarn build`
更新将生成在 `src-tauri/target/(release|debug)/msi/Cultivation_X.X.X_x64_xx-XX.msi.zip`
# 启动器主题
完整的主题参考可以[在这里找到!](/THEMES.md)
# 画面
![image](https://user-images.githubusercontent.com/25207995/173211603-e5e85df7-7fd3-430b-9246-749ebbc1e483.png)
![image](https://user-images.githubusercontent.com/25207995/173211543-b7e88943-cfd2-418b-ac48-7f856868129b.png)
![image](https://user-images.githubusercontent.com/25207995/173211561-a1778fdc-5cfe-4687-9a00-44500d29e470.png)
![image](https://user-images.githubusercontent.com/25207995/173211573-8cedfa9a-51c9-4670-a4f7-a334a2fabec5.png)
![image](https://user-images.githubusercontent.com/25207995/173211590-6a2242b5-1e8f-4db9-a5c7-06284688b131.png)
## 成员
- [SpikeHD](https://github.com/SpikeHD): For originally creating **GrassClipper** and creating the amazing UI of Cultivation.
- [KingRainbow44](https://github.com/KingRainbow44): For building a proxy daemon from scratch and integrating it with Cultivation.
- [Benj](https://github.com/4Benj): For assistance in client patching.
- [lilmayofuksu](https://github.com/lilmayofuksu): For assistance in client patching.
- [Tauri](https://tauri.app): For providing an amazing, efficient, and simple desktop application framework/library.

88
README_zh-TW.md Normal file
View File

@@ -0,0 +1,88 @@
[EN](README.md) | [简中](README_zh-CN.md) | 繁中
# 客戶端修補通知
對於遊戲版本為 2.8 及以上時,使用 Grasscutter 啟動時Cultivation 會自動為您的遊戲客戶端製作一個小修補,並在關閉遊戲時恢復它。 從理論上講,你應該是完全安全的,但是不明確**如果您使用它連接到官方伺服器,修改遊戲客戶端可能會導致封號**,但可能性是非常小的,並且從未接到發生過此類情況的問題,但存在這種可能性!
# Cultivation
一個遊戲啟動器,旨在輕松將某動漫遊戲的流量代理到私人伺服器。
雖然此存儲庫是**開放的**。 但這**並不**意味著它已經發布。
請不要**安裝、下載或使用在其他地方找到的預編譯版本的 Cultivation**。 僅使用此 GitHub 存儲庫中的版本。
# 目錄
- [下載](#下載)
- [開發人員快速入門](#開發人員快速入門)
- [安裝](#安裝)
- [編譯](#編譯)
- [代碼格式化與糾錯](#代碼格式化與糾錯)
- [生成更新項目](#生成更新項目)
- [啟動器主題](#啟動器主題)
- [畫面](#畫面)
- [成員](#成員)
# 下載
[在此處查找發布版本!](https://github.com/Grasscutters/Cultivation/releases)
下載後,從某個位置解壓縮並以管理員身份打開。
# 開發人員快速入門
### 安裝
- 安裝 [NodeJS >12](https://nodejs.org/en/)
- 安裝 [yarn](https://classic.yarnpkg.com/lang/en/docs/install) (`npm`愛好者去哭吧!(滑稽))
- 安裝 [Rust](https://www.rust-lang.org/tools/install)
- `yarn install`
- `yarn start:dev`
### 編譯
發布版本,
- `yarn build`
調試版本,
- `yarn build --debug`
### 代碼格式化與糾錯
格式化:
- `yarn format`
糾錯, 修復(一些)錯誤:
- `yarn lint`, `yarn lint:fix`
### 生成更新項目
-`TAURI_PRIVATE_KEY` 添加到環境變數,其中包含私鑰的路徑。
-`TAURI_KEY_PASSWORD` 添加到環境變數,其中包含私鑰的密碼。
- `yarn build`
更新將生成在 `src-tauri/target/(release|debug)/msi/Cultivation_X.X.X_x64_xx-XX.msi.zip`
# 啟動器主題
完整的主題參考可以[在這裏找到!](/THEMES.md)
# 畫面
![image](https://user-images.githubusercontent.com/25207995/173211603-e5e85df7-7fd3-430b-9246-749ebbc1e483.png)
![image](https://user-images.githubusercontent.com/25207995/173211543-b7e88943-cfd2-418b-ac48-7f856868129b.png)
![image](https://user-images.githubusercontent.com/25207995/173211561-a1778fdc-5cfe-4687-9a00-44500d29e470.png)
![image](https://user-images.githubusercontent.com/25207995/173211573-8cedfa9a-51c9-4670-a4f7-a334a2fabec5.png)
![image](https://user-images.githubusercontent.com/25207995/173211590-6a2242b5-1e8f-4db9-a5c7-06284688b131.png)
## 成員
- [SpikeHD](https://github.com/SpikeHD): For originally creating **GrassClipper** and creating the amazing UI of Cultivation.
- [KingRainbow44](https://github.com/KingRainbow44): For building a proxy daemon from scratch and integrating it with Cultivation.
- [Benj](https://github.com/4Benj): For assistance in client patching.
- [lilmayofuksu](https://github.com/lilmayofuksu): For assistance in client patching.
- [Tauri](https://tauri.app): For providing an amazing, efficient, and simple desktop application framework/library.

View File

@@ -1,6 +1,6 @@
{ {
"name": "cultivation", "name": "cultivation",
"version": "1.0.4", "version": "1.0.10",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.0.0-rc.5", "@tauri-apps/api": "^1.0.0-rc.5",
@@ -20,7 +20,7 @@
"scripts": { "scripts": {
"start": "cross-env BROWSER=none react-scripts start", "start": "cross-env BROWSER=none react-scripts start",
"postbuild:windows": "xcopy /E /H /C /I /Y \".\\src-tauri\\lang\" \".\\src-tauri\\target\\release\\lang\"", "postbuild:windows": "xcopy /E /H /C /I /Y \".\\src-tauri\\lang\" \".\\src-tauri\\target\\release\\lang\"",
"postbuild:linux": "cp -r \".\\src-tauri\\lang\" \".\\lang\"", "postbuild:linux": "cp -r \"./src-tauri/lang\" \"./lang\"",
"build:windows": "yarn tauri build", "build:windows": "yarn tauri build",
"build:linux": "yarn tauri build", "build:linux": "yarn tauri build",
"build": "react-scripts build && run-script-os", "build": "react-scripts build && run-script-os",

68
src-tauri/Cargo.lock generated
View File

@@ -26,6 +26,17 @@ dependencies = [
"opaque-debug", "opaque-debug",
] ]
[[package]]
name = "ahash"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.7",
"once_cell",
"version_check",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.18" version = "0.7.18"
@@ -702,6 +713,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "ctrlc"
version = "3.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173"
dependencies = [
"nix",
"winapi",
]
[[package]] [[package]]
name = "cty" name = "cty"
version = "0.2.2" version = "0.2.2"
@@ -713,6 +734,7 @@ name = "cultivation"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cc", "cc",
"ctrlc",
"duct", "duct",
"file_diff", "file_diff",
"futures-util", "futures-util",
@@ -725,6 +747,7 @@ dependencies = [
"regex", "regex",
"registry", "registry",
"reqwest", "reqwest",
"rust-ini",
"rustls-pemfile", "rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
@@ -888,6 +911,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dlv-list"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]] [[package]]
name = "dtoa" name = "dtoa"
version = "0.4.8" version = "0.4.8"
@@ -1481,6 +1510,9 @@ name = "hashbrown"
version = "0.12.3" version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [
"ahash",
]
[[package]] [[package]]
name = "heck" name = "heck"
@@ -1889,9 +1921,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.126" version = "0.2.132"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
[[package]] [[package]]
name = "libdbus-sys" name = "libdbus-sys"
@@ -2151,6 +2183,18 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nix"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
dependencies = [
"autocfg",
"bitflags",
"cfg-if 1.0.0",
"libc",
]
[[package]] [[package]]
name = "nodrop" name = "nodrop"
version = "0.1.14" version = "0.1.14"
@@ -2451,6 +2495,16 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "ordered-multimap"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
dependencies = [
"dlv-list",
"hashbrown",
]
[[package]] [[package]]
name = "os_info" name = "os_info"
version = "3.4.0" version = "3.4.0"
@@ -3223,6 +3277,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "rust-ini"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
dependencies = [
"cfg-if 1.0.0",
"ordered-multimap",
]
[[package]] [[package]]
name = "rustc-serialize" name = "rustc-serialize"
version = "0.3.24" version = "0.3.24"

View File

@@ -61,6 +61,8 @@ regex = "1"
# other # other
file_diff = "1.0.0" file_diff = "1.0.0"
rust-ini = "0.18.0"
ctrlc = "3.2.3"
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode

View File

@@ -11,13 +11,21 @@
"files_extracting": "文件解压中:" "files_extracting": "文件解压中:"
}, },
"options": { "options": {
"enabled": "已启用",
"disabled": "已禁用",
"game_path": "选择游戏安装路径",
"game_executable": "选择游戏可执行文件", "game_executable": "选择游戏可执行文件",
"recover_metadata": "紧急情况下恢复元数据文件",
"grasscutter_jar": "选择 Grasscutter JAR 文件", "grasscutter_jar": "选择 Grasscutter JAR 文件",
"java_path": "设置自定义 Java 路径", "toggle_encryption": "启用加密",
"install_certificate": "安装代理证书",
"java_path": "选择自定义 Java 路径",
"grasscutter_with_game": "随游戏自动启动 Grasscutter", "grasscutter_with_game": "随游戏自动启动 Grasscutter",
"language": "语言", "language": "选择语言",
"background": "设置自定义背景(链接或文件)", "background": "设置自定义背景(链接或文件)",
"theme": "设置主题" "theme": "设置主题",
"patch_metadata": "自动修改元数据",
"use_proxy": "使用内置代理"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "下载 Grasscutter 稳定版数据", "grasscutter_stable_data": "下载 Grasscutter 稳定版数据",
@@ -28,7 +36,8 @@
"grasscutter_latest": "下载 Grasscutter 开发版", "grasscutter_latest": "下载 Grasscutter 开发版",
"grasscutter_stable_update": "更新 Grasscutter 稳定版", "grasscutter_stable_update": "更新 Grasscutter 稳定版",
"grasscutter_latest_update": "更新 Grasscutter 开发版", "grasscutter_latest_update": "更新 Grasscutter 开发版",
"resources": "下载 Grasscutter 资源" "resources": "下载 Grasscutter 资源",
"game": "下载游戏"
}, },
"download_status": { "download_status": {
"downloading": "下载中", "downloading": "下载中",
@@ -40,10 +49,11 @@
"components": { "components": {
"select_file": "选择文件或文件夹...", "select_file": "选择文件或文件夹...",
"select_folder": "选择文件夹...", "select_folder": "选择文件夹...",
"download": "下载" "download": "下载",
"install": "安装"
}, },
"news": { "news": {
"latest_commits": "最近的PR", "latest_commits": "最近提交",
"latest_version": "最新版本" "latest_version": "最新版本"
}, },
"help": { "help": {
@@ -53,6 +63,17 @@
"gc_dev_jar": "下载最新的 Grasscutter 开发版,包括 JAR 文件和数据。", "gc_dev_jar": "下载最新的 Grasscutter 开发版,包括 JAR 文件和数据。",
"gc_stable_data": "下载当前的 Grasscutter 稳定版数据,不包括 JAR 文件。此选项在更新时有帮助。", "gc_stable_data": "下载当前的 Grasscutter 稳定版数据,不包括 JAR 文件。此选项在更新时有帮助。",
"gc_dev_data": "下载最新的 Grasscutter 开发版数据,不包括 JAR 文件。此选项在更新时有帮助。", "gc_dev_data": "下载最新的 Grasscutter 开发版数据,不包括 JAR 文件。此选项在更新时有帮助。",
"resources": "资源文件在运行 Grasscutter 服务器时是必要的。此选项在已经存在资源文件时不可选。" "resources": "资源文件在运行 Grasscutter 服务器时是必要的。此选项在已经存在资源文件时不可选。",
"emergency_metadata": "在出现意外情况时,自动将元数据恢复至原始版本",
"use_proxy": "使用 Cultivation 的内置代理。除非你使用 Fiddler 等软件,否则应启用此项。",
"patch_metadata": "自动修改和恢复游戏元数据。除非要游玩旧版本/非官方版本,抑或你已经手动修改了元数据,否则应启用此项。"
},
"swag": {
"akebi_name": "Akebi",
"migoto_name": "Migoto",
"reshade_name": "Reshade",
"akebi": "选择 Akebi 可执行文件",
"migoto": "选择 3DMigoto 可执行文件",
"reshade": "选择 Reshade 注入器"
} }
} }

View File

@@ -13,14 +13,19 @@
"options": { "options": {
"enabled": "已啟用", "enabled": "已啟用",
"disabled": "未啟用", "disabled": "未啟用",
"game_path": "選擇遊戲安裝路徑",
"game_executable": "選擇遊戲執行檔", "game_executable": "選擇遊戲執行檔",
"recover_metadata": "緊急恢復Metadata",
"grasscutter_jar": "選擇伺服器JAR檔案", "grasscutter_jar": "選擇伺服器JAR檔案",
"toggle_encryption": "設定加密", "toggle_encryption": "設定加密",
"java_path": "設定自定義Java路徑", "install_certificate": "安裝代理憑證",
"java_path": "選擇自定義Java路徑",
"grasscutter_with_game": "伴隨遊戲一起啟動Grasscutter", "grasscutter_with_game": "伴隨遊戲一起啟動Grasscutter",
"language": "語言", "language": "語言",
"background": "設定自定義背景(網址或檔案)", "background": "選擇自定義背景(網址或檔案)",
"theme": "設定主題" "theme": "選擇主題",
"patch_metadata": "自動修補Metadata",
"use_proxy": "使用內建代理伺服器"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "下載Grasscutter穩定版數據Data", "grasscutter_stable_data": "下載Grasscutter穩定版數據Data",
@@ -31,7 +36,8 @@
"grasscutter_latest": "下載Grasscutter開發板", "grasscutter_latest": "下載Grasscutter開發板",
"grasscutter_stable_update": "更新Grasscutter穩定版", "grasscutter_stable_update": "更新Grasscutter穩定版",
"grasscutter_latest_update": "更新Grasscutter開發板", "grasscutter_latest_update": "更新Grasscutter開發板",
"resources": "下載Grasscutter資源Resources" "resources": "下載Grasscutter資源Resources",
"game": "下載遊戲"
}, },
"download_status": { "download_status": {
"downloading": "下載中", "downloading": "下載中",
@@ -43,7 +49,8 @@
"components": { "components": {
"select_file": "選擇檔案或資料夾...", "select_file": "選擇檔案或資料夾...",
"select_folder": "選擇資料夾...", "select_folder": "選擇資料夾...",
"download": "下載" "download": "下載",
"install": "安裝"
}, },
"news": { "news": {
"latest_commits": "最近的PR", "latest_commits": "最近的PR",
@@ -51,11 +58,23 @@
}, },
"help": { "help": {
"port_help_text": "確保這是Dispatch伺服器端口不是遊戲伺服器端口。 大部分伺服器的端口都是443。", "port_help_text": "確保這是Dispatch伺服器端口不是遊戲伺服器端口。 大部分伺服器的端口都是443。",
"game_help_text": "不需要另外一個遊戲備份來使用Grasscutter。這是給想要降級到2.6或者還沒安裝遊戲的人使用的。", "game_help_text": "不需要另外一個遊戲備份來使用Grasscutter。這是給想要降級到2.6或者還沒安裝遊戲的人使用的。",
"gc_stable_jar": "下載當前的Grasscutter穩定版本包括JAR答案還有資料文件。", "gc_stable_jar": "下載當前的Grasscutter穩定版本包括JAR答案還有資料文件。",
"gc_dev_jar": "下載當前的Grasscutter穩定版本資料文件其中不會附帶JAR文件。這個選項在更新時很有用。", "gc_dev_jar": "下載當前的Grasscutter穩定版本資料文件其中不會附帶JAR文件。這個選項在更新時很有用。",
"gc_stable_data": "下載當前最新的Grasscutter開發版本資料文件其中不會附帶JAR文件。這個選項在更新時很有用。", "gc_stable_data": "下載當前最新的Grasscutter開發版本資料文件其中不會附帶JAR文件。這個選項在更新時很有用。",
"gc_dev_data": "下載當前最新的Grasscutter開發版本的資料文件其中不會附帶JAR文件。這個選項在更新時很有用。", "gc_dev_data": "下載當前最新的Grasscutter開發版本的資料文件其中不會附帶JAR文件。這個選項在更新時很有用。",
"resources": "資源文件在架設一個Grasscutter伺服器時是必要的。 這個選項會在你已經有裡面有檔案的資源資料夾時不可選。" "encryption": "在正常情況下,此選項應該被關閉。",
"resources": "資源文件在架設一個Grasscutter伺服器時是必要的。 這個選項會在您已經有裡面有檔案的資源資料夾時不可選。",
"emergency_metadata": "一旦有東西出了問題此選項可以把您的Metadata恢復成官方版本。",
"use_proxy": "使用Cultivation內建的代理伺服器。此選項應該被啟用除非你使用其他的代理伺服器。",
"patch_metadata": "自動修補和恢復Metadata。除非您的遊戲版本是舊的或者是非官方的此選項應該被啟用。"
},
"swag": {
"akebi_name": "Akebi",
"migoto_name": "Migoto",
"reshade_name": "Reshade",
"akebi": "選擇Akebi執行檔",
"migoto": "選擇3DMigoto執行檔",
"reshade": "選擇Reshade注入器"
} }
} }

View File

@@ -13,14 +13,17 @@
"options": { "options": {
"enabled": "Aktiviert", "enabled": "Aktiviert",
"disabled": "Deaktiviert", "disabled": "Deaktiviert",
"game_path": "Spielpfad",
"game_executable": "Spiel Datei auswählen", "game_executable": "Spiel Datei auswählen",
"recover_metadata": "Notfall Wiederherstellung der Metadaten",
"grasscutter_jar": "Grasscuter JAR auswählen", "grasscutter_jar": "Grasscuter JAR auswählen",
"toggle_encryption": "Verschlüsselung umschalten", "toggle_encryption": "Verschlüsselung umschalten",
"java_path": "Benutzerdefinierten Java Pfad setzen", "java_path": "Benutzerdefinierten Java Pfad setzen",
"grasscutter_with_game": "Grasscutter automatisch mit dem Spiel starten", "grasscutter_with_game": "Grasscutter automatisch mit dem Spiel starten",
"language": "Sprache auswählen", "language": "Sprache auswählen",
"background": "Benutzerdefinierten Hintergrund festlegen (link oder bild)", "background": "Benutzerdefinierten Hintergrund festlegen (link oder bild)",
"theme": "Theme auswählen" "theme": "Theme auswählen",
"patch_metadata": "Metadaten automatisch patchen"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "Stabile Grasscutter Daten herunterladen", "grasscutter_stable_data": "Stabile Grasscutter Daten herunterladen",
@@ -31,7 +34,8 @@
"grasscutter_latest": "Aktuellste Grasscutter Version herunterladen", "grasscutter_latest": "Aktuellste Grasscutter Version herunterladen",
"grasscutter_stable_update": "Stabile Grasscutter Version aktualisieren", "grasscutter_stable_update": "Stabile Grasscutter Version aktualisieren",
"grasscutter_latest_update": "Aktuellste Grasscutter Version aktualisieren", "grasscutter_latest_update": "Aktuellste Grasscutter Version aktualisieren",
"resources": "Grasscutter Ressourcen herunterladen" "resources": "Grasscutter Ressourcen herunterladen",
"game": "Spiel herunterladen"
}, },
"download_status": { "download_status": {
"downloading": "Lädt herunter", "downloading": "Lädt herunter",
@@ -43,7 +47,8 @@
"components": { "components": {
"select_file": "Datei oder Ordner auswählen...", "select_file": "Datei oder Ordner auswählen...",
"select_folder": "Ordner auswählen...", "select_folder": "Ordner auswählen...",
"download": "Herunterladen" "download": "Herunterladen",
"install": "Installieren"
}, },
"news": { "news": {
"latest_commits": "Letzte Commits", "latest_commits": "Letzte Commits",
@@ -56,6 +61,17 @@
"gc_dev_jar": "Laden Sie die neueste Grasscutter-Entwicklungsversion herunter, welche eine Jar-Datei und Datendateien enthält.", "gc_dev_jar": "Laden Sie die neueste Grasscutter-Entwicklungsversion herunter, welche eine Jar-Datei und Datendateien enthält.",
"gc_stable_data": "Laden Sie die stabilen Grasscutter Daten herunter, welche keine Jar-Datei enthalten. Dies ist nützlich zum Aktualisieren.", "gc_stable_data": "Laden Sie die stabilen Grasscutter Daten herunter, welche keine Jar-Datei enthalten. Dies ist nützlich zum Aktualisieren.",
"gc_dev_data": "Laden Sie die neuesten Grasscutter-Entwicklungsdateien herunter, welche keine Jar-Datei enthält. Dies ist nützlich zum Aktualisieren.", "gc_dev_data": "Laden Sie die neuesten Grasscutter-Entwicklungsdateien herunter, welche keine Jar-Datei enthält. Dies ist nützlich zum Aktualisieren.",
"resources": "Diese werden auch benötigt, um einen Grasscutter-Server auszuführen. Diese Schaltfläche ist grau, wenn Sie einen bestehenden Ressourcenordner mit Inhalten haben" "resources": "Diese werden auch benötigt, um einen Grasscutter-Server auszuführen. Diese Schaltfläche ist grau, wenn Sie einen bestehenden Ressourcenordner mit Inhalten haben",
"emergency_metadata": "Im Fall, dass etwas schief laufen sollte, kannst du deine Metadaten auf die letzte offizielle Version zurücksetzen",
"use_proxy": "Nutze den internen Proxy von Cultivation. Du solltest dies aktivieren, es sei denn du nutzt Programme wie Fiddler",
"patch_metadata": "Patche und aktualisiere deine Metadaten automatisch. Solange du nicht mit einer alten/nicht offiziellen Version spielst oder deine Metadaten manuell gepatcht hast, sollte dies aktiviert sein."
},
"swag": {
"akebi_name": "Akebi",
"migoto_name": "Migoto",
"reshade_name": "Reshade",
"akebi": "Akebi.exe festlegen",
"migoto": "Migoto.exe festlegen",
"reshade": "Reshade injector festlegen"
} }
} }

View File

@@ -14,6 +14,7 @@
"enabled": "Enabled", "enabled": "Enabled",
"disabled": "Disabled", "disabled": "Disabled",
"game_path": "Set Game Install Path", "game_path": "Set Game Install Path",
"game_command": "Game Launch Command",
"game_executable": "Set Game Executable", "game_executable": "Set Game Executable",
"recover_metadata": "Emergency Metadata Recovery", "recover_metadata": "Emergency Metadata Recovery",
"grasscutter_jar": "Set Grasscutter JAR", "grasscutter_jar": "Set Grasscutter JAR",
@@ -25,7 +26,9 @@
"background": "Set Custom Background (link or image file)", "background": "Set Custom Background (link or image file)",
"theme": "Set Theme", "theme": "Set Theme",
"patch_metadata": "Automatically Patch Metadata", "patch_metadata": "Automatically Patch Metadata",
"use_proxy": "Use Internal Proxy" "use_proxy": "Use Internal Proxy",
"wipe_login": "Wipe Login Cache",
"horny_mode": "Horny Mode"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "Download Grasscutter Stable Data", "grasscutter_stable_data": "Download Grasscutter Stable Data",
@@ -63,7 +66,11 @@
"gc_dev_jar": "Download the latest development Grasscutter build, which includes jar file and data files.", "gc_dev_jar": "Download the latest development Grasscutter build, which includes jar file and data files.",
"gc_stable_data": "Download the current stable Grasscutter data files, which does not come with a jar file. This is useful for updating.", "gc_stable_data": "Download the current stable Grasscutter data files, which does not come with a jar file. This is useful for updating.",
"gc_dev_data": "Download the latest development Grasscutter data files, which does not come with a jar file. This is useful for updating.", "gc_dev_data": "Download the latest development Grasscutter data files, which does not come with a jar file. This is useful for updating.",
"resources": "These are also required to run a Grasscutter server. This button will be grey if you have an existing resources folder with contents inside" "encryption": "This should usually be disabled.",
"resources": "These are also required to run a Grasscutter server. This button will be grey if you have an existing resources folder with contents inside",
"emergency_metadata": "In case something went wrong, restore your metadata to the latest official versions metadata.",
"use_proxy": "Use the Cultivation internal proxy. You should have this enabled unless you use something like Fiddler",
"patch_metadata": "Patch and unpatch your game metadata automatically. Unless playing with old/non-official versions, or you have manually patched your metadata, this should be enabled."
}, },
"swag": { "swag": {
"akebi_name": "Akebi", "akebi_name": "Akebi",

80
src-tauri/lang/nl.json Normal file
View File

@@ -0,0 +1,80 @@
{
"lang_name": "Nederlands",
"main": {
"title": "Cultivation",
"launch_button": "Start",
"gc_enable": "Verbind Met Grasscutter",
"https_enable": "Gebruik HTTPS",
"ip_placeholder": "Server Address...",
"port_placeholder": "Poort...",
"files_downloading": "Bestanden Aan Downloaden: ",
"files_extracting": "Bestanden Uitpakken: "
},
"options": {
"enabled": "Ingeschakeld",
"disabled": "Uitgeschakeld",
"game_path": "Spel Installatie Pad Instellen",
"game_executable": "Stel De Exe Van Het Spel In",
"recover_metadata": "Noodherstel Van De Metadata",
"grasscutter_jar": "Stel De Grasscutter JAR In",
"toggle_encryption": "Versleuteling Inschakelen",
"install_certificate": "Proxy-certificaat installeren",
"java_path": "Aangepast Java-pad Instellen",
"grasscutter_with_game": "Start Automatisch Grasscutter Met Spel",
"language": "Selecteer Taal",
"background": "Aangepaste Achtergrond Instellen (link of afbeeldingsbestand)",
"theme": "Thema instellen",
"patch_metadata": "Metadata Automatisch Bijwerken",
"use_proxy": "Gebruik Interne Proxy"
},
"downloads": {
"grasscutter_stable_data": "Download Stabiele Gegevens Van Grasscutter",
"grasscutter_latest_data": "Download De Nieuwste Gegevens Van Grasscutter",
"grasscutter_stable_data_update": "Stabiele gegevens Van Grasscutter bijwerken",
"grasscutter_latest_data_update": "Nieuwste gegevens Van Grasscutter bijwerken",
"grasscutter_stable": "Download Stabiele Versie Van Grasscutter",
"grasscutter_latest": "Download De Nieuwste Versie Van Grasscutter",
"grasscutter_stable_update": "Update Grasscutter Naar De Stabiele Versie",
"grasscutter_latest_update": "Update Grasscutter Naar De Nieuwste Versie",
"resources": "Download Grasscutter bronnen",
"game": "Download Spel"
},
"download_status": {
"downloading": "Aan Het Downloading",
"extracting": "Uitpakken",
"error": "Fout",
"finished": "Voltooid",
"stopped": "Gestopt"
},
"components": {
"select_file": "Select file or folder...",
"select_folder": "Select folder...",
"download": "Download",
"install": "Install"
},
"news": {
"latest_commits": "Recente Opdrachten",
"latest_version": "Nieuwste Versie"
},
"help": {
"port_help_text": "Zorg ervoor dat dit de Dispatch server poort is, niet de Game server poort. Dit is bijna altijd '443'.",
"game_help_text": "U hoeft geen aparte kopie te gebruiken om met Grasscutter te spelen. Dit is voor downgraden naar 2.6 of als u het spel niet geinstalleerd heeft.",
"gc_stable_jar": "Download de huidige stabiele Grasscutter build, die jar file en data bestanden bevat.",
"gc_dev_jar": "Download de laatste ontwikkeling Grasscutter build, die jar file en data bestanden bevat.",
"gc_stable_data": "Download de huidige stabiele versie van de Grasscutter data bestanden, die niet met een jar file komen. Dit is handig voor het bijwerken.",
"gc_dev_data": "Download de nieuwste versie van de Grasscutter data bestanden, die niet met een jar file komen. Dit is handig voor het bijwerken.",
"encryption": "Dit wordt meestal uitgeschakeld.",
"resources": "Deze zijn ook nodig om een Grasscutter server te draaien. Deze knop zal grijs zijn als u een bestaande resources map heeft met inhoud erin",
"emergency_metadata": "Voor het geval er iets fout is gegaan, herstel uw metadata naar de laatste offici<63>le versies metadata.",
"use_proxy": "Gebruik de Cultivation interne proxy. U zou dit ingeschakeld moeten hebben, tenzij u iets als Fiddler gebruikt",
"patch_metadata": "Patch en unpatch je spel metadata automatisch. Tenzij je met oude/niet-offici<63>le versies speelt, of je hebt je metadata handmatig gepatcht, zou dit ingeschakeld moeten zijn."
},
"swag": {
"akebi_name": "Akebi",
"migoto_name": "Migoto",
"reshade_name": "Reshade",
"akebi": "Stel Akebi Exe Bestand In",
"migoto": "Stel 3DMigoto Exe Bestand In",
"reshade": "Stel Reshade Injector In"
}
}

View File

@@ -13,14 +13,19 @@
"options": { "options": {
"enabled": "Включено", "enabled": "Включено",
"disabled": "Выключено", "disabled": "Выключено",
"game_path": "Установить путь к файлам игры",
"game_executable": "Установить исполняемый файл игры", "game_executable": "Установить исполняемый файл игры",
"recover_metadata": "Принудительное восстановление Метаданных",
"grasscutter_jar": "Установить Grasscutter JAR", "grasscutter_jar": "Установить Grasscutter JAR",
"toggle_encryption": "Переключить шифрование", "toggle_encryption": "Переключить шифрование",
"install_certificate": "Установить сертификат для работы Прокси",
"java_path": "Установить пользовательский путь Java", "java_path": "Установить пользовательский путь Java",
"grasscutter_with_game": "Автоматически запускать Grasscutter вместе с игрой", "grasscutter_with_game": "Автоматически запускать Grasscutter вместе с игрой",
"language": "Установить язык", "language": "Установить язык",
"background": "Установить свой фон (ссылка или файл)", "background": "Установить свой фон (ссылка или файл)",
"theme": "Установить тему" "theme": "Установить тему",
"patch_metadata": "Автоматический патч Метаданных при запуске",
"use_proxy": "Использовать встроенный Прокси"
}, },
"downloads": { "downloads": {
"grasscutter_stable_data": "Скачать стабильные данные Grasscutter", "grasscutter_stable_data": "Скачать стабильные данные Grasscutter",
@@ -31,7 +36,8 @@
"grasscutter_latest": "Скачать последнюю версию Grasscutter", "grasscutter_latest": "Скачать последнюю версию Grasscutter",
"grasscutter_stable_update": "Обновить стабильную версию Grasscutter", "grasscutter_stable_update": "Обновить стабильную версию Grasscutter",
"grasscutter_latest_update": "Обновить последнюю версию Grasscutter", "grasscutter_latest_update": "Обновить последнюю версию Grasscutter",
"resources": "Скачать ресурсы Grasscutter" "resources": "Скачать ресурсы Grasscutter",
"game": "Скачать Игру"
}, },
"download_status": { "download_status": {
"downloading": "Скачивание", "downloading": "Скачивание",
@@ -43,7 +49,8 @@
"components": { "components": {
"select_file": "Выберите файл или папку...", "select_file": "Выберите файл или папку...",
"select_folder": "Выберите папку...", "select_folder": "Выберите папку...",
"download": "Скачать" "download": "Скачать",
"install": "Установить"
}, },
"news": { "news": {
"latest_commits": "Последние коммиты", "latest_commits": "Последние коммиты",
@@ -56,6 +63,18 @@
"gc_dev_jar": "Скачать последнюю версию для разработки Grasscutter, которая содержит jar файл и данные.", "gc_dev_jar": "Скачать последнюю версию для разработки Grasscutter, которая содержит jar файл и данные.",
"gc_stable_data": "Скачать стабильные данные Grasscutter, в которой нету jar файла. Это полезно для обновления.", "gc_stable_data": "Скачать стабильные данные Grasscutter, в которой нету jar файла. Это полезно для обновления.",
"gc_dev_data": "Скачать последнюю версию для разработки Grasscutter, в которой нету jar файла. Это полезно для обновления.", "gc_dev_data": "Скачать последнюю версию для разработки Grasscutter, в которой нету jar файла. Это полезно для обновления.",
"resources": "Это необходимо для запуска сервера Grasscutter. Эта кнопка будет серой, если у Вас уже есть не пустая папка с ресурсами." "encryption": "Обычно это должно быть выключено.",
"resources": "Это необходимо для запуска сервера Grasscutter. Эта кнопка будет серой, если у Вас уже есть не пустая папка с ресурсами.",
"emergency_metadata": "Если что-то пошло не так, восстановит Метаданные до последней официальной версии.",
"use_proxy": "Использовать встроенный Прокси. Отключите если используете Fiddler или подобную программу",
"patch_metadata": "Патчит и восстанавливает Метаданные автоматически. Если вы не играете на старых/модифицированых версиях, или сами в ручную патчите Метаданные, эта опция должна быть включена."
},
"swag": {
"akebi_name": "Akebi",
"migoto_name": "Migoto",
"reshade_name": "Reshade",
"akebi": "Путь к исполняемому файлу Akebi",
"migoto": "Путь к исполняемому файлу 3DMigoto ",
"reshade": "Путь к инжектору Reshade"
} }
} }

23
src-tauri/src/admin.rs Normal file
View File

@@ -0,0 +1,23 @@
use std::process::exit;
use std::process::Command;
#[cfg(windows)]
pub fn reopen_as_admin() {
let install = std::env::current_exe().unwrap();
println!("Opening as admin: {}", install.to_str().unwrap());
Command::new("powershell.exe")
.arg("powershell")
.arg(format!(
"-command \"&{{Start-Process -filepath '{}' -verb runas}}\"",
install.to_str().unwrap()
))
.spawn()
.expect("Error starting exec as admin");
exit(0);
}
#[cfg(target_os = "linux")]
pub fn reopen_as_admin() {}

View File

@@ -1,6 +1,7 @@
use file_diff::diff; use file_diff::diff;
use std::fs; use std::fs;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::path::PathBuf;
#[tauri::command] #[tauri::command]
pub fn rename(path: String, new_name: String) { pub fn rename(path: String, new_name: String) {
@@ -18,7 +19,14 @@ pub fn rename(path: String, new_name: String) {
let path_replaced = &path.replace(&new_path.split('/').last().unwrap(), &new_name); let path_replaced = &path.replace(&new_path.split('/').last().unwrap(), &new_name);
fs::rename(path, &path_replaced).unwrap(); match fs::rename(&path, &path_replaced) {
Ok(_) => {
println!("Renamed {} to {}", &path, path_replaced);
}
Err(e) => {
println!("Error: {}", e);
}
};
} }
#[tauri::command] #[tauri::command]
@@ -28,19 +36,19 @@ pub fn dir_create(path: String) {
#[tauri::command] #[tauri::command]
pub fn dir_exists(path: &str) -> bool { pub fn dir_exists(path: &str) -> bool {
let path_buf = std::path::PathBuf::from(path); let path_buf = PathBuf::from(path);
fs::metadata(path_buf).is_ok() fs::metadata(path_buf).is_ok()
} }
#[tauri::command] #[tauri::command]
pub fn dir_is_empty(path: &str) -> bool { pub fn dir_is_empty(path: &str) -> bool {
let path_buf = std::path::PathBuf::from(path); let path_buf = PathBuf::from(path);
fs::read_dir(path_buf).unwrap().count() == 0 fs::read_dir(path_buf).unwrap().count() == 0
} }
#[tauri::command] #[tauri::command]
pub fn dir_delete(path: &str) { pub fn dir_delete(path: &str) {
let path_buf = std::path::PathBuf::from(path); let path_buf = PathBuf::from(path);
fs::remove_dir_all(path_buf).unwrap(); fs::remove_dir_all(path_buf).unwrap();
} }
@@ -52,11 +60,10 @@ pub fn are_files_identical(path1: &str, path2: &str) -> bool {
#[tauri::command] #[tauri::command]
pub fn copy_file(path: String, new_path: String) -> bool { pub fn copy_file(path: String, new_path: String) -> bool {
let filename = &path.split('/').last().unwrap(); let filename = &path.split('/').last().unwrap();
let mut new_path_buf = std::path::PathBuf::from(&new_path); let path_buf = PathBuf::from(&path);
let path_buf = std::path::PathBuf::from(&path);
// If the new path doesn't exist, create it. // If the new path doesn't exist, create it.
if !dir_exists(new_path_buf.pop().to_string().as_str()) { if !dir_exists(PathBuf::from(&new_path).pop().to_string().as_str()) {
std::fs::create_dir_all(&new_path).unwrap(); std::fs::create_dir_all(&new_path).unwrap();
} }
@@ -74,11 +81,11 @@ pub fn copy_file(path: String, new_path: String) -> bool {
#[tauri::command] #[tauri::command]
pub fn copy_file_with_new_name(path: String, new_path: String, new_name: String) -> bool { pub fn copy_file_with_new_name(path: String, new_path: String, new_name: String) -> bool {
let mut new_path_buf = std::path::PathBuf::from(&new_path); let mut new_path_buf = PathBuf::from(&new_path);
let path_buf = std::path::PathBuf::from(&path); let path_buf = PathBuf::from(&path);
// If the new path doesn't exist, create it. // If the new path doesn't exist, create it.
if !dir_exists(new_path_buf.pop().to_string().as_str()) { if !dir_exists(PathBuf::from(&new_path).pop().to_string().as_str()) {
match std::fs::create_dir_all(&new_path) { match std::fs::create_dir_all(&new_path) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
@@ -88,8 +95,10 @@ pub fn copy_file_with_new_name(path: String, new_path: String, new_name: String)
}; };
} }
new_path_buf.push(new_name);
// Copy old to new // Copy old to new
match std::fs::copy(&path_buf, format!("{}/{}", new_path, new_name)) { match std::fs::copy(&path_buf, &new_path_buf) {
Ok(_) => true, Ok(_) => true,
Err(e) => { Err(e) => {
println!("Failed to copy file: {}", e); println!("Failed to copy file: {}", e);
@@ -102,7 +111,7 @@ pub fn copy_file_with_new_name(path: String, new_path: String, new_name: String)
#[tauri::command] #[tauri::command]
pub fn delete_file(path: String) -> bool { pub fn delete_file(path: String) -> bool {
let path_buf = std::path::PathBuf::from(&path); let path_buf = PathBuf::from(&path);
match std::fs::remove_file(path_buf) { match std::fs::remove_file(path_buf) {
Ok(_) => true, Ok(_) => true,
@@ -117,7 +126,7 @@ pub fn delete_file(path: String) -> bool {
#[tauri::command] #[tauri::command]
pub fn read_file(path: String) -> String { pub fn read_file(path: String) -> String {
let path_buf = std::path::PathBuf::from(&path); let path_buf = PathBuf::from(&path);
let mut file = match fs::File::open(path_buf) { let mut file = match fs::File::open(path_buf) {
Ok(file) => file, Ok(file) => file,
@@ -135,7 +144,7 @@ pub fn read_file(path: String) -> String {
#[tauri::command] #[tauri::command]
pub fn write_file(path: String, contents: String) { pub fn write_file(path: String, contents: String) {
let path_buf = std::path::PathBuf::from(&path); let path_buf = PathBuf::from(&path);
// Create file if it exists, otherwise just open and rewrite // Create file if it exists, otherwise just open and rewrite
let mut file = match fs::File::create(&path_buf) { let mut file = match fs::File::create(&path_buf) {

View File

@@ -9,23 +9,19 @@ static SITE_URL: &str = "https://gamebanana.com";
#[tauri::command] #[tauri::command]
pub async fn get_download_links(mod_id: String) -> String { pub async fn get_download_links(mod_id: String) -> String {
let res = web::query(format!("{}/apiv9/Mod/{}/DownloadPage", SITE_URL, mod_id).as_str()).await; web::query(format!("{}/apiv9/Mod/{}/DownloadPage", SITE_URL, mod_id).as_str()).await
res
} }
#[tauri::command] #[tauri::command]
pub async fn list_submissions(mode: String) -> String { pub async fn list_submissions(mode: String, page: String) -> String {
let res = web::query( web::query(
format!( format!(
"{}/apiv9/Util/Game/Submissions?_idGameRow=8552&_nPage=1&_nPerpage=50&_sMode={}", "{}/apiv9/Util/Game/Submissions?_idGameRow=8552&_nPage={}&_nPerpage=50&_sMode={}",
SITE_URL, mode SITE_URL, page, mode
) )
.as_str(), .as_str(),
) )
.await; .await
res
} }
#[tauri::command] #[tauri::command]

View File

@@ -3,13 +3,22 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use file_helpers::dir_exists;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::fs;
use std::io::Write;
use std::{collections::HashMap, sync::Mutex}; use std::{collections::HashMap, sync::Mutex};
use system_helpers::is_elevated;
use tauri::api::path::data_dir;
use tauri::async_runtime::block_on;
use std::thread; use std::thread;
use structs::APIQuery; use structs::APIQuery;
use sysinfo::{System, SystemExt}; use sysinfo::{System, SystemExt};
use crate::admin::reopen_as_admin;
mod admin;
mod downloader; mod downloader;
mod file_helpers; mod file_helpers;
mod gamebanana; mod gamebanana;
@@ -23,50 +32,109 @@ mod web;
static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new())); static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
fn try_flush() {
std::io::stdout().flush().unwrap_or(())
}
fn has_arg(args: &[String], arg: &str) -> bool {
args.contains(&arg.to_string())
}
async fn arg_handler(args: &[String]) {
if has_arg(args, "--proxy") {
let mut pathbuf = tauri::api::path::data_dir().unwrap();
pathbuf.push("cultivation");
pathbuf.push("ca");
connect(8035, pathbuf.to_str().unwrap().to_string()).await;
}
}
fn main() { fn main() {
tauri::Builder::default() if !is_elevated() {
.invoke_handler(tauri::generate_handler![ println!("===============================================================================");
enable_process_watcher, println!("You running as a non-elevated user. Some stuff will almost definitely not work.");
connect, println!("===============================================================================");
disconnect,
req_get, reopen_as_admin();
get_bg_file, }
is_game_running,
get_theme_list, // Setup datadir/cultivation just in case something went funky and it wasn't made
system_helpers::run_command, if !dir_exists(data_dir().unwrap().join("cultivation").to_str().unwrap()) {
system_helpers::run_program, fs::create_dir_all(&data_dir().unwrap().join("cultivation")).unwrap();
system_helpers::run_program_relative, }
system_helpers::run_jar,
system_helpers::open_in_browser, // Always set CWD to the location of the executable.
system_helpers::install_location, let mut exe_path = std::env::current_exe().unwrap();
system_helpers::is_elevated, exe_path.pop();
proxy::set_proxy_addr, std::env::set_current_dir(&exe_path).unwrap();
proxy::generate_ca_files,
unzip::unzip, let args: Vec<String> = std::env::args().collect();
file_helpers::rename,
file_helpers::dir_create, block_on(arg_handler(&args));
file_helpers::dir_exists,
file_helpers::dir_is_empty, // For disabled GUI
file_helpers::dir_delete, ctrlc::set_handler(|| {
file_helpers::copy_file, disconnect();
file_helpers::copy_file_with_new_name, std::process::exit(0);
file_helpers::delete_file, })
file_helpers::are_files_identical, .unwrap_or(());
file_helpers::read_file,
file_helpers::write_file, if !has_arg(&args, "--no-gui") {
downloader::download_file, tauri::Builder::default()
downloader::stop_download, .invoke_handler(tauri::generate_handler![
lang::get_lang, enable_process_watcher,
lang::get_languages, connect,
web::valid_url, disconnect,
web::web_get, req_get,
gamebanana::get_download_links, get_bg_file,
gamebanana::list_submissions, is_game_running,
gamebanana::list_mods, get_theme_list,
metadata_patcher::patch_metadata system_helpers::run_command,
]) system_helpers::run_program,
.run(tauri::generate_context!()) system_helpers::run_program_relative,
.expect("error while running tauri application"); system_helpers::run_jar,
system_helpers::open_in_browser,
system_helpers::install_location,
system_helpers::is_elevated,
system_helpers::set_migoto_target,
system_helpers::wipe_registry,
system_helpers::get_platform,
proxy::set_proxy_addr,
proxy::generate_ca_files,
unzip::unzip,
file_helpers::rename,
file_helpers::dir_create,
file_helpers::dir_exists,
file_helpers::dir_is_empty,
file_helpers::dir_delete,
file_helpers::copy_file,
file_helpers::copy_file_with_new_name,
file_helpers::delete_file,
file_helpers::are_files_identical,
file_helpers::read_file,
file_helpers::write_file,
downloader::download_file,
downloader::stop_download,
lang::get_lang,
lang::get_languages,
web::valid_url,
web::web_get,
gamebanana::get_download_links,
gamebanana::list_submissions,
gamebanana::list_mods,
metadata_patcher::patch_metadata
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
} else {
try_flush();
println!("Press enter or CTRL-C twice to quit...");
std::io::stdin().read_line(&mut String::new()).unwrap();
}
// Always disconnect upon closing the program
disconnect();
} }
#[tauri::command] #[tauri::command]
@@ -88,6 +156,9 @@ fn enable_process_watcher(window: tauri::Window, process: String) {
println!("Starting process watcher..."); println!("Starting process watcher...");
thread::spawn(move || { thread::spawn(move || {
// Initial sleep for 8 seconds, since running 20 different injectors or whatever can take a while
std::thread::sleep(std::time::Duration::from_secs(10));
let mut system = System::new_all(); let mut system = System::new_all();
loop { loop {
@@ -189,7 +260,8 @@ async fn get_bg_file(bg_path: String, appdata: String) -> String {
let response_data: APIQuery = match serde_json::from_str(&query) { let response_data: APIQuery = match serde_json::from_str(&query) {
Ok(data) => data, Ok(data) => data,
Err(e) => { Err(e) => {
println!("Failed to parse response: {}", e); println!("Failed to get bg file response: {}", e);
println!("^ please stop reporting this as an error it's so annoying LMFAO");
return "".to_string(); return "".to_string();
} }
}; };

View File

@@ -1,8 +1,5 @@
use regex::Regex; use regex::Regex;
use std::fs::File; use std::{fs, fs::File, fs::OpenOptions, io::Read, io::Write, path::Path};
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Write;
// For these two functions, a non-zero return value indicates failure. // For these two functions, a non-zero return value indicates failure.
extern "C" { extern "C" {
@@ -13,6 +10,12 @@ extern "C" {
#[tauri::command] #[tauri::command]
pub fn patch_metadata(metadata_folder: &str) -> bool { pub fn patch_metadata(metadata_folder: &str) -> bool {
let metadata_file = &(metadata_folder.to_owned() + "\\global-metadata-unpatched.dat"); let metadata_file = &(metadata_folder.to_owned() + "\\global-metadata-unpatched.dat");
// check if metadata_file exists
if !Path::new(metadata_file).exists() {
println!("Metadata file not found");
return false;
}
println!("Patching metadata file: {}", metadata_file); println!("Patching metadata file: {}", metadata_file);
let decrypted = decrypt_metadata(metadata_file); let decrypted = decrypt_metadata(metadata_file);
if do_vecs_match(&decrypted, &Vec::new()) { if do_vecs_match(&decrypted, &Vec::new()) {
@@ -111,15 +114,20 @@ fn replace_keys(data: &[u8]) -> Vec<u8> {
fn replace_rsa_key(old_data: &str, to_replace: &str, file_name: &str) -> String { fn replace_rsa_key(old_data: &str, to_replace: &str, file_name: &str) -> String {
// Read dispatch key file // Read dispatch key file
unsafe { unsafe {
let mut new_key_file = match File::open(&("keys/".to_owned() + file_name)) { // Get key path from current directory
let key_file_path = std::env::current_dir()
.unwrap()
.join("keys")
.join(file_name);
let key_data = match fs::read(&key_file_path) {
Ok(file) => file, Ok(file) => file,
Err(e) => { Err(e) => {
println!("Failed to open keys/{}: {}", file_name, e); println!("Failed to open {}: {}", key_file_path.to_str().unwrap(), e);
return String::new(); return String::new();
} }
}; };
let mut key_data = Vec::new();
new_key_file.read_to_end(&mut key_data).unwrap();
let new_key = String::from_utf8_unchecked(key_data.to_vec()); let new_key = String::from_utf8_unchecked(key_data.to_vec());
// Replace old key with new key // Replace old key with new key
@@ -129,6 +137,7 @@ fn replace_rsa_key(old_data: &str, to_replace: &str, file_name: &str) -> String
fn encrypt_metadata(old_data: &[u8]) -> Vec<u8> { fn encrypt_metadata(old_data: &[u8]) -> Vec<u8> {
let mut data = old_data.to_vec(); let mut data = old_data.to_vec();
let success = unsafe { encrypt_global_metadata(data.as_mut_ptr(), data.len()) } == 0; let success = unsafe { encrypt_global_metadata(data.as_mut_ptr(), data.len()) } == 0;
if success { if success {
println!("Successfully encrypted global-metadata"); println!("Successfully encrypted global-metadata");
@@ -140,6 +149,5 @@ fn encrypt_metadata(old_data: &[u8]) -> Vec<u8> {
} }
fn do_vecs_match<T: PartialEq>(a: &Vec<T>, b: &Vec<T>) -> bool { fn do_vecs_match<T: PartialEq>(a: &Vec<T>, b: &Vec<T>) -> bool {
let matching = a.iter().zip(b.iter()).filter(|&(a, b)| a == b).count(); a == b
matching == a.len() && matching == b.len()
} }

View File

@@ -3,8 +3,11 @@
* https://github.com/omjadas/hudsucker/blob/main/examples/log.rs * https://github.com/omjadas/hudsucker/blob/main/examples/log.rs
*/ */
#[cfg(target_os = "linux")]
use crate::system_helpers::run_command;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{str::FromStr, sync::Mutex}; use std::{path::PathBuf, str::FromStr, sync::Mutex};
use hudsucker::{ use hudsucker::{
async_trait::async_trait, async_trait::async_trait,
@@ -19,7 +22,7 @@ use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use rustls_pemfile as pemfile; use rustls_pemfile as pemfile;
use tauri::http::Uri; use tauri::{api::path::data_dir, http::Uri};
#[cfg(windows)] #[cfg(windows)]
use registry::{Data, Hive, Security}; use registry::{Data, Hive, Security};
@@ -75,11 +78,32 @@ impl HttpHandler for ProxyHandler {
* Starts an HTTP(S) proxy server. * Starts an HTTP(S) proxy server.
*/ */
pub async fn create_proxy(proxy_port: u16, certificate_path: String) { pub async fn create_proxy(proxy_port: u16, certificate_path: String) {
let cert_path = PathBuf::from(certificate_path);
let pk_path = cert_path.join("private.key");
let ca_path = cert_path.join("cert.crt");
// Get the certificate and private key. // Get the certificate and private key.
let mut private_key_bytes: &[u8] = let mut private_key_bytes: &[u8] = &match fs::read(&pk_path) {
&fs::read(format!("{}\\private.key", certificate_path)).expect("Could not read private key"); // Try regenerating the CA stuff and read it again. If that doesn't work, quit.
let mut ca_cert_bytes: &[u8] = Ok(b) => b,
&fs::read(format!("{}\\cert.crt", certificate_path)).expect("Could not read certificate"); Err(e) => {
println!("Encountered {}. Regenerating CA cert and retrying...", e);
generate_ca_files(&data_dir().unwrap().join("cultivation"));
fs::read(&pk_path).expect("Could not read private key")
}
};
let mut ca_cert_bytes: &[u8] = &match fs::read(&ca_path) {
// Try regenerating the CA stuff and read it again. If that doesn't work, quit.
Ok(b) => b,
Err(e) => {
println!("Encountered {}. Regenerating CA cert and retrying...", e);
generate_ca_files(&data_dir().unwrap().join("cultivation"));
fs::read(&ca_path).expect("Could not read certificate")
}
};
// Parse the private key and certificate. // Parse the private key and certificate.
let private_key = rustls::PrivateKey( let private_key = rustls::PrivateKey(
@@ -138,9 +162,29 @@ pub fn connect_to_proxy(proxy_port: u16) {
println!("Connected to the proxy."); println!("Connected to the proxy.");
} }
#[cfg(not(windows))] #[cfg(unix)]
pub fn connect_to_proxy(proxy_port: u16) {
// Edit /etc/environment to set $http_proxy and $https_proxy
let mut env_file = match fs::read_to_string("/etc/environment") {
Ok(f) => f,
Err(e) => {
println!("Error opening /etc/environment: {}", e);
return;
}
};
// Append the proxy configuration.
// We will not remove the current proxy config if it exists, so we can just remove these last lines when we disconnect
env_file += format!("\nhttps_proxy=127.0.0.1:{}", proxy_port).as_str();
env_file += format!("\nhttp_proxy=127.0.0.1:{}", proxy_port).as_str();
// Save
fs::write("/etc/environment", env_file).unwrap();
}
#[cfg(target_od = "macos")]
pub fn connect_to_proxy(_proxy_port: u16) { pub fn connect_to_proxy(_proxy_port: u16) {
println!("Connecting to the proxy is not implemented on this platform."); println!("No Mac support yet. Someone mail me a Macbook and I will do it B)")
} }
/** /**
@@ -162,7 +206,26 @@ pub fn disconnect_from_proxy() {
println!("Disconnected from proxy."); println!("Disconnected from proxy.");
} }
#[cfg(not(windows))] #[cfg(target_os = "linux")]
pub fn disconnect_from_proxy() {
println!("Re-writing environment variables");
let regexp = regex::Regex::new(
// This has to be specific as possible or we risk fuckin up their environment LOL
r"(https|http)_proxy=.*127.0.0.1:.*",
)
.unwrap();
let environment = &fs::read_to_string("/etc/environment").expect("Failed to open environment");
let new_environment = regexp.replace_all(environment, "").to_string();
// Write new environment
fs::write("/etc/environment", new_environment.trim_end()).expect(
"Could not write environment, remove proxy declarations manually if they are still set",
);
}
#[cfg(target_os = "macos")]
pub fn disconnect_from_proxy() {} pub fn disconnect_from_proxy() {}
/* /*
@@ -242,6 +305,7 @@ pub fn install_ca_files(cert_path: &Path) {
crate::system_helpers::run_command( crate::system_helpers::run_command(
"certutil", "certutil",
vec!["-user", "-addstore", "Root", cert_path.to_str().unwrap()], vec!["-user", "-addstore", "Root", cert_path.to_str().unwrap()],
None,
); );
println!("Installed certificate."); println!("Installed certificate.");
} }
@@ -259,11 +323,27 @@ pub fn install_ca_files(cert_path: &Path) {
"/Library/Keychains/System.keychain", "/Library/Keychains/System.keychain",
cert_path.to_str().unwrap(), cert_path.to_str().unwrap(),
], ],
None,
); );
println!("Installed certificate."); println!("Installed certificate.");
} }
#[cfg(not(any(windows, target_os = "macos")))] // If this is borked on non-debian platforms, so be it
#[cfg(target_os = "linux")]
pub fn install_ca_files(cert_path: &Path) {
let usr_certs = PathBuf::from("/usr/local/share/ca-certificates");
let usr_cert_path = usr_certs.join("cultivation.crt");
// Create dir if it doesn't exist
fs::create_dir_all(&usr_certs).expect("Unable to create local certificate directory");
fs::copy(cert_path, &usr_cert_path).expect("Unable to copy cert to local certificate directory");
run_command("update-ca-certificates", vec![], None);
println!("Installed certificate.");
}
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
pub fn install_ca_files(_cert_path: &Path) { pub fn install_ca_files(_cert_path: &Path) {
println!("Certificate installation is not supported on this platform."); println!("Certificate installation is not supported on this platform.");
} }

View File

@@ -1,9 +1,14 @@
use duct::cmd; use duct::cmd;
use ini::Ini;
use std::path::PathBuf;
#[cfg(windows)]
use registry::{Data, Hive, Security};
#[tauri::command] #[tauri::command]
pub fn run_program(path: String, args: Option<String>) { pub fn run_program(path: String, args: Option<String>) {
// Without unwrap_or, this can crash when UAC prompt is denied // Without unwrap_or, this can crash when UAC prompt is denied
open::that(format!("{} {}", &path, &args.unwrap_or("".into()))).unwrap_or(()); open::that(format!("{} {}", &path, &args.unwrap_or_else(|| "".into()))).unwrap_or(());
} }
#[tauri::command] #[tauri::command]
@@ -18,23 +23,36 @@ pub fn run_program_relative(path: String, args: Option<String>) {
// Set new working directory // Set new working directory
std::env::set_current_dir(&path_buf).unwrap(); std::env::set_current_dir(&path_buf).unwrap();
println!("Opening {} {}", &path, args.clone().unwrap_or("".into()));
// Without unwrap_or, this can crash when UAC prompt is denied // Without unwrap_or, this can crash when UAC prompt is denied
open::that(format!("{} {}", &path, args.unwrap_or("".into()))).unwrap_or(()); open::that(format!("{} {}", &path, args.unwrap_or_else(|| "".into()))).unwrap_or(());
// Restore the original working directory // Restore the original working directory
std::env::set_current_dir(&cwd).unwrap(); std::env::set_current_dir(&cwd).unwrap();
} }
#[tauri::command] #[tauri::command]
pub fn run_command(program: &str, args: Vec<&str>) { pub fn run_command(program: &str, args: Vec<&str>, relative: Option<bool>) {
let prog = program.to_string(); let prog = program.to_string();
let args = args.iter().map(|s| s.to_string()).collect::<Vec<String>>(); let args = args.iter().map(|s| s.to_string()).collect::<Vec<String>>();
// Commands should not block (this is for the reshade injector mostly) // Commands should not block (this is for the reshade injector mostly)
std::thread::spawn(move || { std::thread::spawn(move || {
// Save the current working directory
let cwd = std::env::current_dir().unwrap();
if relative.unwrap_or(false) {
// Set the new working directory to the path before the executable
let mut path_buf = std::path::PathBuf::from(&prog);
path_buf.pop();
// Set new working directory
std::env::set_current_dir(&path_buf).unwrap();
}
cmd(prog, args).run().unwrap(); cmd(prog, args).run().unwrap();
// Restore the original working directory
std::env::set_current_dir(&cwd).unwrap();
}); });
} }
@@ -75,6 +93,70 @@ pub fn install_location() -> String {
return exe_path.to_str().unwrap().to_string(); return exe_path.to_str().unwrap().to_string();
} }
#[tauri::command]
pub fn set_migoto_target(path: String, migoto_path: String) -> bool {
let pathbuf = PathBuf::from(path);
let mut migoto_pathbuf = PathBuf::from(migoto_path);
migoto_pathbuf.pop();
migoto_pathbuf.push("d3dx.ini");
let mut conf = match Ini::load_from_file(&migoto_pathbuf) {
Ok(c) => {
println!("Loaded migoto ini");
c
}
Err(e) => {
println!("Error loading migoto config: {}", e);
return false;
}
};
// Set options
conf
.with_section(Some("Loader"))
.set("target", pathbuf.to_str().unwrap());
// Write file
match conf.write_to_file(&migoto_pathbuf) {
Ok(_) => {
println!("Wrote config!");
true
}
Err(e) => {
println!("Error writing config: {}", e);
false
}
}
}
#[cfg(windows)]
#[tauri::command]
pub fn wipe_registry(exec_name: String) {
// Fetch the 'Internet Settings' registry key.
let settings =
match Hive::CurrentUser.open(format!("Software\\miHoYo\\{}", exec_name), Security::Write) {
Ok(s) => s,
Err(e) => {
println!("Error getting registry setting: {}", e);
return;
}
};
// Wipe login cache
match settings.set_value(
"MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810",
&Data::String("".parse().unwrap()),
) {
Ok(_) => (),
Err(e) => println!("Error wiping registry: {}", e),
}
}
#[cfg(unix)]
#[tauri::command]
pub fn wipe_registry(_exec_name: String) {}
#[cfg(windows)] #[cfg(windows)]
#[tauri::command] #[tauri::command]
pub fn is_elevated() -> bool { pub fn is_elevated() -> bool {
@@ -86,3 +168,8 @@ pub fn is_elevated() -> bool {
pub fn is_elevated() -> bool { pub fn is_elevated() -> bool {
sudo::check() == sudo::RunningAs::Root sudo::check() == sudo::RunningAs::Root
} }
#[tauri::command]
pub fn get_platform() -> &'static str {
std::env::consts::OS
}

View File

@@ -1,7 +1,7 @@
use std::fs::{read_dir, File}; use std::fs::{read_dir, File};
use std::path; use std::path;
use std::thread; use std::thread;
use unrar::archive::{Archive, OpenArchive}; use unrar::archive::Archive;
#[tauri::command] #[tauri::command]
pub fn unzip( pub fn unzip(
@@ -58,38 +58,35 @@ pub fn unzip(
} }
}; };
full_path = new_path.clone(); full_path = new_path;
} }
println!("Is rar file? {}", zipfile.ends_with(".rar")); println!("Is rar file? {}", zipfile.ends_with(".rar"));
let mut name = "".into(); let name;
let success;
// If file ends in zip, OR is unknown, extract as zip, otherwise extract as rar // If file ends in zip, OR is unknown, extract as zip, otherwise extract as rar
if zipfile.ends_with(".rar") { if zipfile.ends_with(".rar") {
extract_rar( success = extract_rar(&zipfile, &f, &full_path, top_level.unwrap_or(true));
&window,
&zipfile,
&f,
&full_path,
top_level.unwrap_or(false),
);
let archive = Archive::new(zipfile.clone()); let archive = Archive::new(zipfile.clone());
name = archive.list().unwrap().next().unwrap().unwrap().filename; name = archive.list().unwrap().next().unwrap().unwrap().filename;
} else { } else {
extract_zip( success = extract_zip(&zipfile, &f, &full_path, top_level.unwrap_or(true));
&window,
&zipfile,
&f,
&full_path,
top_level.unwrap_or(false),
);
// Get the name of the inenr file in the zip file // Get the name of the inenr file in the zip file
let mut zip = zip::ZipArchive::new(&f).unwrap(); let mut zip = zip::ZipArchive::new(&f).unwrap();
let file = zip.by_index(0).unwrap(); let file = zip.by_index(0).unwrap();
name = file.name().to_string().clone(); name = file.name().to_string();
}
if !success {
let mut res_hash = std::collections::HashMap::new();
res_hash.insert("path".to_string(), zipfile.to_string());
window.emit("download_error", &res_hash).unwrap();
} }
// If the contents is a jar file, emit that we have extracted a new jar file // If the contents is a jar file, emit that we have extracted a new jar file
@@ -114,29 +111,21 @@ pub fn unzip(
for entry in read_dir(&write_path).unwrap() { for entry in read_dir(&write_path).unwrap() {
let entry = entry.unwrap(); let entry = entry.unwrap();
let entry_path = entry.path(); let entry_path = entry.path();
if entry_path.is_dir() { if entry_path.is_dir() && !dirs.contains(&entry_path) {
if !dirs.contains(&entry_path) { new_dir = entry_path.to_str().unwrap().to_string();
new_dir = entry_path.to_str().unwrap().to_string();
}
} }
} }
let mut res_hash = std::collections::HashMap::new(); let mut res_hash = std::collections::HashMap::new();
res_hash.insert("file", zipfile.to_string()); res_hash.insert("file", zipfile.to_string());
res_hash.insert("new_folder", new_dir.to_string()); res_hash.insert("new_folder", new_dir);
window.emit("extract_end", &res_hash).unwrap(); window.emit("extract_end", &res_hash).unwrap();
}); });
} }
fn extract_rar( fn extract_rar(rarfile: &str, _f: &File, full_path: &path::Path, _top_level: bool) -> bool {
window: &tauri::Window, let archive = Archive::new(rarfile.to_string());
rarfile: &String,
f: &File,
full_path: &path::PathBuf,
top_level: bool,
) {
let archive = Archive::new(rarfile.clone());
let mut open_archive = archive let mut open_archive = archive
.extract_to(full_path.to_str().unwrap().to_string()) .extract_to(full_path.to_str().unwrap().to_string())
@@ -148,41 +137,29 @@ fn extract_rar(
"Extracted rar file to: {}", "Extracted rar file to: {}",
full_path.to_str().unwrap_or("Error") full_path.to_str().unwrap_or("Error")
); );
true
} }
Err(e) => { Err(e) => {
println!("Failed to extract rar file: {}", e); println!("Failed to extract rar file: {}", e);
let mut res_hash = std::collections::HashMap::new(); false
res_hash.insert("error".to_string(), e.to_string());
res_hash.insert("path".to_string(), rarfile.to_string());
window.emit("download_error", &res_hash).unwrap();
} }
} }
} }
fn extract_zip( fn extract_zip(_zipfile: &str, f: &File, full_path: &path::Path, top_level: bool) -> bool {
window: &tauri::Window,
zipfile: &String,
f: &File,
full_path: &path::PathBuf,
top_level: bool,
) {
match zip_extract::extract(f, full_path, top_level) { match zip_extract::extract(f, full_path, top_level) {
Ok(_) => { Ok(_) => {
println!( println!(
"Extracted zip file to: {}", "Extracted zip file to: {}",
full_path.to_str().unwrap_or("Error") full_path.to_str().unwrap_or("Error")
); );
true
} }
Err(e) => { Err(e) => {
println!("Failed to extract zip file: {}", e); println!("Failed to extract zip file: {}", e);
let mut res_hash = std::collections::HashMap::new(); false
res_hash.insert("error".to_string(), e.to_string());
res_hash.insert("path".to_string(), zipfile.to_string());
window.emit("download_error", &res_hash).unwrap();
} }
}; }
} }

View File

@@ -7,7 +7,7 @@
}, },
"package": { "package": {
"productName": "Cultivation", "productName": "Cultivation",
"version": "1.0.4" "version": "1.0.10"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@@ -114,9 +114,6 @@ select:focus {
padding: 0; padding: 0;
} }
.ExtrasMenu {
}
@media (max-height: 580px) { @media (max-height: 580px) {
.BottomSection { .BottomSection {
height: 150px; height: 150px;

View File

@@ -36,10 +36,6 @@ async function generateInfo() {
alert('check your dev console and send that in #cultivation') alert('check your dev console and send that in #cultivation')
} }
function none() {
alert('none')
}
class Debug extends React.Component { class Debug extends React.Component {
render() { render() {
return ( return (

View File

@@ -24,7 +24,6 @@ import DownloadHandler from '../utils/download'
import cogBtn from '../resources/icons/cog.svg' import cogBtn from '../resources/icons/cog.svg'
import downBtn from '../resources/icons/download.svg' import downBtn from '../resources/icons/download.svg'
import wrenchBtn from '../resources/icons/wrench.svg' import wrenchBtn from '../resources/icons/wrench.svg'
import Menu from './components/menu/Menu'
import { ExtrasMenu } from './components/menu/ExtrasMenu' import { ExtrasMenu } from './components/menu/ExtrasMenu'
interface IProps { interface IProps {

View File

@@ -12,7 +12,7 @@ import Plus from '../../resources/icons/plus.svg'
import './ServerLaunchSection.css' import './ServerLaunchSection.css'
import { dataDir } from '@tauri-apps/api/path' import { dataDir } from '@tauri-apps/api/path'
import { getGameExecutable } from '../../utils/game' import { getGameExecutable, getGameVersion } from '../../utils/game'
import { patchGame, unpatchGame } from '../../utils/metadata' import { patchGame, unpatchGame } from '../../utils/metadata'
interface IProps { interface IProps {
@@ -110,6 +110,30 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
// Connect to proxy // Connect to proxy
if (config.toggle_grasscutter) { if (config.toggle_grasscutter) {
if (config.patch_metadata) { if (config.patch_metadata) {
const gameVersion = await getGameVersion()
console.log(gameVersion)
if (gameVersion == null) {
alert(
'Game version could not be determined. Please make sure you have the game correctly selected and try again.'
)
return
}
if (gameVersion?.major == 2 && gameVersion?.minor < 8) {
alert(
'Game version is too old for metadata patching. Please disable metadata patching in the settings and try again.'
)
return
}
if (gameVersion?.major == 3 && gameVersion?.minor >= 1) {
alert(
'Game version is too new for metadata patching. Please disable metadata patching in the settings to launch the game.\nNOTE: You will require a UA patch to play the game.'
)
return
}
const patched = await patchGame() const patched = await patchGame()
if (!patched) { if (!patched) {
@@ -161,6 +185,14 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
} }
} }
if (config.wipe_login) {
// First wipe registry if we have to
await invoke('wipe_registry', {
// The exe is always PascalCase so we can get the dir using regex
execName: (await getGameExecutable())?.split('.exe')[0].replace(/([a-z\d])([A-Z])/g, '$1 $2'),
})
}
// Launch the program // Launch the program
const gameExists = await invoke('dir_exists', { const gameExists = await invoke('dir_exists', {
path: exe || config.game_install_path, path: exe || config.game_install_path,

View File

@@ -2,7 +2,7 @@ import React from 'react'
import './HelpButton.css' import './HelpButton.css'
import Help from '../../../resources/icons/help.svg' import Help from '../../../resources/icons/help.svg'
import MiniDialog from '../MiniDialog' import { translate } from '../../../utils/language'
interface IProps { interface IProps {
children?: React.ReactNode[] | React.ReactNode children?: React.ReactNode[] | React.ReactNode
@@ -10,45 +10,23 @@ interface IProps {
id?: string id?: string
} }
interface IState { export default class HelpButton extends React.Component<IProps, never> {
opened: boolean
}
export default class HelpButton extends React.Component<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props) super(props)
this.state = { this.showAlert = this.showAlert.bind(this)
opened: false,
}
this.setOpen = this.setOpen.bind(this)
this.setClosed = this.setClosed.bind(this)
} }
setOpen() { async showAlert() {
this.setState({ opened: true }) if (this.props.contents) alert(await translate(this.props.contents))
}
setClosed() {
this.setState({ opened: false })
} }
render() { render() {
return ( return (
<div className="HelpSection"> <div className="HelpSection">
<div className="HelpButton" onMouseEnter={this.setOpen} onMouseLeave={this.setClosed}> <div className="HelpButton" onClick={this.showAlert}>
<img src={Help} /> <img src={Help} />
</div> </div>
<div
className="HelpContents"
style={{
display: this.state.opened ? 'block' : 'none',
}}
>
<MiniDialog closeFn={this.setClosed}>{this.props.contents || this.props.children}</MiniDialog>
</div>
</div> </div>
) )
} }

View File

@@ -57,7 +57,7 @@ export default class ProgressBar extends React.Component<IProps, IState> {
style={{ style={{
width: `${(() => { width: `${(() => {
// Handles no files downloading // Handles no files downloading
if (this.state.files === 0) { if (this.state.files === 0 || this.state.average >= 100) {
return '100' return '100'
} }

View File

@@ -17,7 +17,7 @@ const STABLE_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archiv
const DEV_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/development.zip' const DEV_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/development.zip'
const STABLE_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip' const STABLE_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip'
const DEV_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip' const DEV_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip'
const RESOURCES_DOWNLOAD = 'https://gitlab.com/yukiz/GrasscutterResources/-/archive/2.8/GrasscutterResources-2.8.zip' const RESOURCES_DOWNLOAD = 'https://github.com/tamilpp25/Grasscutter_Resources/archive/refs/heads/3.0.zip'
interface IProps { interface IProps {
closeFn: () => void closeFn: () => void
@@ -202,9 +202,7 @@ export default class Downloads extends React.Component<IProps, IState> {
<Tr <Tr
text={this.state.grasscutter_set ? 'downloads.grasscutter_stable' : 'downloads.grasscutter_stable_update'} text={this.state.grasscutter_set ? 'downloads.grasscutter_stable' : 'downloads.grasscutter_stable_update'}
/> />
<HelpButton> <HelpButton contents="help.gc_stable_jar" />
<Tr text="help.gc_stable_jar" />
</HelpButton>
</div> </div>
<div className="DownloadValue" id="downloadMenuButtonGCStable"> <div className="DownloadValue" id="downloadMenuButtonGCStable">
<BigButton <BigButton
@@ -221,9 +219,7 @@ export default class Downloads extends React.Component<IProps, IState> {
<Tr <Tr
text={this.state.grasscutter_set ? 'downloads.grasscutter_latest' : 'downloads.grasscutter_latest_update'} text={this.state.grasscutter_set ? 'downloads.grasscutter_latest' : 'downloads.grasscutter_latest_update'}
/> />
<HelpButton> <HelpButton contents="help.gc_dev_jar" />
<Tr text="help.gc_dev_jar" />
</HelpButton>
</div> </div>
<div className="DownloadValue" id="downloadMenuButtonGCDev"> <div className="DownloadValue" id="downloadMenuButtonGCDev">
<BigButton <BigButton
@@ -247,9 +243,7 @@ export default class Downloads extends React.Component<IProps, IState> {
: 'downloads.grasscutter_stable_data_update' : 'downloads.grasscutter_stable_data_update'
} }
/> />
<HelpButton> <HelpButton contents="help.gc_stable_data" />
<Tr text="help.gc_stable_data" />
</HelpButton>
</div> </div>
<div className="DownloadValue" id="downloadMenuButtonGCStableData"> <div className="DownloadValue" id="downloadMenuButtonGCStableData">
<BigButton <BigButton
@@ -270,9 +264,7 @@ export default class Downloads extends React.Component<IProps, IState> {
: 'downloads.grasscutter_latest_data_update' : 'downloads.grasscutter_latest_data_update'
} }
/> />
<HelpButton> <HelpButton contents="help.gc_dev_data" />
<Tr text="help.gc_dev_data" />
</HelpButton>
</div> </div>
<div className="DownloadValue" id="downloadMenuButtonGCDevData"> <div className="DownloadValue" id="downloadMenuButtonGCDevData">
<BigButton <BigButton
@@ -290,9 +282,7 @@ export default class Downloads extends React.Component<IProps, IState> {
<div className="DownloadMenuSection" id="downloadMenuContainerResources"> <div className="DownloadMenuSection" id="downloadMenuContainerResources">
<div className="DownloadLabel" id="downloadMenuLabelResources"> <div className="DownloadLabel" id="downloadMenuLabelResources">
<Tr text="downloads.resources" /> <Tr text="downloads.resources" />
<HelpButton> <HelpButton contents="help.resources" />
<Tr text="help.resources" />
</HelpButton>
</div> </div>
<div className="DownloadValue" id="downloadMenuButtonResources"> <div className="DownloadValue" id="downloadMenuButtonResources">
<BigButton <BigButton

View File

@@ -115,6 +115,7 @@ export class ExtrasMenu extends React.Component<IProps, IState> {
await invoke('run_command', { await invoke('run_command', {
program: config.reshade_path, program: config.reshade_path,
args: [await getGameExecutable()], args: [await getGameExecutable()],
relative: true,
}) })
} }

View File

@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import Menu from './Menu' import Menu from './Menu'
import Tr, { translate } from '../../../utils/language' import { translate } from '../../../utils/language'
import DownloadHandler from '../../../utils/download' import DownloadHandler from '../../../utils/download'
import './Game.css' import './Game.css'
@@ -70,9 +70,7 @@ export default class Downloads extends React.Component<IProps, IState> {
Download Game Download Game
</BigButton> </BigButton>
)} )}
<HelpButton> <HelpButton contents="main.game_help_text" />
<Tr text="main.game_help_text" />
</HelpButton>
</div> </div>
<div className="GameDownloadDir"> <div className="GameDownloadDir">

View File

@@ -16,3 +16,7 @@
.OptionSection .BigButtonText { .OptionSection .BigButtonText {
font-size: 12px; font-size: 12px;
} }
.OptionSection .HelpButton img {
filter: invert(0%) sepia(91%) saturate(7464%) hue-rotate(101deg) brightness(0%) contrast(107%);
}

View File

@@ -4,7 +4,7 @@ import { dataDir } from '@tauri-apps/api/path'
import DirInput from '../common/DirInput' import DirInput from '../common/DirInput'
import Menu from './Menu' import Menu from './Menu'
import Tr, { getLanguages, translate } from '../../../utils/language' import Tr, { getLanguages, translate } from '../../../utils/language'
import { setConfigOption, getConfig, getConfigOption } from '../../../utils/configuration' import { setConfigOption, getConfig, getConfigOption, Configuration } from '../../../utils/configuration'
import Checkbox from '../common/Checkbox' import Checkbox from '../common/Checkbox'
import Divider from './Divider' import Divider from './Divider'
import { getThemeList } from '../../../utils/themes' import { getThemeList } from '../../../utils/themes'
@@ -14,6 +14,8 @@ import './Options.css'
import BigButton from '../common/BigButton' import BigButton from '../common/BigButton'
import DownloadHandler from '../../../utils/download' import DownloadHandler from '../../../utils/download'
import * as meta from '../../../utils/metadata' import * as meta from '../../../utils/metadata'
import HelpButton from '../common/HelpButton'
import TextInput from '../common/TextInput'
interface IProps { interface IProps {
closeFn: () => void closeFn: () => void
@@ -33,7 +35,10 @@ interface IState {
encryption: boolean encryption: boolean
patch_metadata: boolean patch_metadata: boolean
use_internal_proxy: boolean use_internal_proxy: boolean
wipe_login: boolean
horny_mode: boolean
swag: boolean swag: boolean
platform: string
// Swag stuff // Swag stuff
akebi_path: string akebi_path: string
@@ -58,7 +63,10 @@ export default class Options extends React.Component<IProps, IState> {
encryption: false, encryption: false,
patch_metadata: false, patch_metadata: false,
use_internal_proxy: false, use_internal_proxy: false,
wipe_login: false,
horny_mode: false,
swag: false, swag: false,
platform: '',
// Swag stuff // Swag stuff
akebi_path: '', akebi_path: '',
@@ -75,19 +83,20 @@ export default class Options extends React.Component<IProps, IState> {
this.setCustomBackground = this.setCustomBackground.bind(this) this.setCustomBackground = this.setCustomBackground.bind(this)
this.toggleEncryption = this.toggleEncryption.bind(this) this.toggleEncryption = this.toggleEncryption.bind(this)
this.restoreMetadata = this.restoreMetadata.bind(this) this.restoreMetadata = this.restoreMetadata.bind(this)
this.toggleMetadata = this.toggleMetadata.bind(this)
this.toggleProxy = this.toggleProxy.bind(this)
} }
async componentDidMount() { async componentDidMount() {
const config = await getConfig() const config = await getConfig()
const languages = await getLanguages() const languages = await getLanguages()
const platform: string = await invoke('get_platform')
// Remove jar from path // Remove jar from path
const path = config.grasscutter_path.replace(/\\/g, '/') const path = config.grasscutter_path.replace(/\\/g, '/')
const folderPath = path.substring(0, path.lastIndexOf('/')) const folderPath = path.substring(0, path.lastIndexOf('/'))
const encEnabled = await server.encryptionEnabled(folderPath + '/config.json') const encEnabled = await server.encryptionEnabled(folderPath + '/config.json')
console.log(platform)
this.setState({ this.setState({
game_install_path: config.game_install_path || '', game_install_path: config.game_install_path || '',
grasscutter_path: config.grasscutter_path || '', grasscutter_path: config.grasscutter_path || '',
@@ -101,7 +110,10 @@ export default class Options extends React.Component<IProps, IState> {
encryption: await translate(encEnabled ? 'options.enabled' : 'options.disabled'), encryption: await translate(encEnabled ? 'options.enabled' : 'options.disabled'),
patch_metadata: config.patch_metadata || false, patch_metadata: config.patch_metadata || false,
use_internal_proxy: config.use_internal_proxy || false, use_internal_proxy: config.use_internal_proxy || false,
wipe_login: config.wipe_login || false,
horny_mode: config.horny_mode || false,
swag: config.swag_mode || false, swag: config.swag_mode || false,
platform,
// Swag stuff // Swag stuff
akebi_path: config.akebi_path || '', akebi_path: config.akebi_path || '',
@@ -115,6 +127,17 @@ export default class Options extends React.Component<IProps, IState> {
setGameExecutable(value: string) { setGameExecutable(value: string) {
setConfigOption('game_install_path', value) setConfigOption('game_install_path', value)
// I hope this stops people setting launcher.exe because oml it's annoying
if (value.endsWith('launcher.exe')) {
const pathArr = value.replace(/\\/g, '/').split('/')
pathArr.pop()
const path = pathArr.join('/') + '/Genshin Impact Game/'
alert(
`You have set your game execuatable to "launcher.exe". You should not do this. Your game executable is located in:\n\n${path}`
)
}
this.setState({ this.setState({
game_install_path: value, game_install_path: value,
}) })
@@ -150,6 +173,12 @@ export default class Options extends React.Component<IProps, IState> {
this.setState({ this.setState({
migoto_path: value, migoto_path: value,
}) })
// Set game exe in Migoto ini
invoke('set_migoto_target', {
path: this.state.game_install_path,
migotoPath: value,
})
} }
setReshade(value: string) { setReshade(value: string) {
@@ -235,40 +264,43 @@ export default class Options extends React.Component<IProps, IState> {
}) })
} }
async toggleMetadata() { async toggleOption(opt: keyof Configuration) {
const changedVal = !(await getConfigOption('patch_metadata')) const changedVal = !(await getConfigOption(opt))
await setConfigOption('patch_metadata', changedVal) await setConfigOption(opt, changedVal)
// @ts-expect-error shut up bitch
this.setState({ this.setState({
patch_metadata: changedVal, [opt]: changedVal,
})
}
async toggleProxy() {
const changedVal = !(await getConfigOption('use_internal_proxy'))
await setConfigOption('use_internal_proxy', changedVal)
this.setState({
use_internal_proxy: changedVal,
}) })
} }
render() { render() {
return ( return (
<Menu closeFn={this.props.closeFn} className="Options" heading="Options"> <Menu closeFn={this.props.closeFn} className="Options" heading="Options">
<div className="OptionSection" id="menuOptionsContainerGamePath"> {!this.state.platform || this.state.platform === 'windows' ? (
<div className="OptionLabel" id="menuOptionsLabelGamePath"> <div className="OptionSection" id="menuOptionsContainerGamePath">
<Tr text="options.game_path" /> <div className="OptionLabel" id="menuOptionsLabelGamePath">
<Tr text="options.game_path" />
</div>
<div className="OptionValue" id="menuOptionsDirGamePath">
<DirInput onChange={this.setGameExecutable} value={this.state?.game_install_path} extensions={['exe']} />
</div>
</div> </div>
<div className="OptionValue" id="menuOptionsDirGamePath"> ) : (
<DirInput onChange={this.setGameExecutable} value={this.state?.game_install_path} extensions={['exe']} /> <div className="OptionSection" id="menuOptionsContainerGameCommand">
<div className="OptionLabel" id="menuOptionsLabelGameCommand">
<Tr text="options.game_command" />
</div>
<div className="OptionValue" id="menuOptionsGameCommand">
<TextInput />
</div>
</div> </div>
</div> )}
<div className="OptionSection" id="menuOptionsContainermetaDownload"> <div className="OptionSection" id="menuOptionsContainermetaDownload">
<div className="OptionLabel" id="menuOptionsLabelmetaDownload"> <div className="OptionLabel" id="menuOptionsLabelmetaDownload">
<Tr text="options.recover_metadata" /> <Tr text="options.recover_metadata" />
<HelpButton contents="help.emergency_metadata" />
</div> </div>
<div className="OptionValue" id="menuOptionsButtonmetaDownload"> <div className="OptionValue" id="menuOptionsButtonmetaDownload">
<BigButton onClick={this.restoreMetadata} id="metaDownload"> <BigButton onClick={this.restoreMetadata} id="metaDownload">
@@ -279,17 +311,39 @@ export default class Options extends React.Component<IProps, IState> {
<div className="OptionSection" id="menuOptionsContainerPatchMeta"> <div className="OptionSection" id="menuOptionsContainerPatchMeta">
<div className="OptionLabel" id="menuOptionsLabelPatchMeta"> <div className="OptionLabel" id="menuOptionsLabelPatchMeta">
<Tr text="options.patch_metadata" /> <Tr text="options.patch_metadata" />
<HelpButton contents="help.patch_metadata" />
</div> </div>
<div className="OptionValue" id="menuOptionsCheckboxPatchMeta"> <div className="OptionValue" id="menuOptionsCheckboxPatchMeta">
<Checkbox onChange={this.toggleMetadata} checked={this.state?.patch_metadata} id="patchMeta" /> <Checkbox
onChange={() => this.toggleOption('patch_metadata')}
checked={this.state?.patch_metadata}
id="patchMeta"
/>
</div> </div>
</div> </div>
<div className="OptionSection" id="menuOptionsContainerUseProxy"> <div className="OptionSection" id="menuOptionsContainerUseProxy">
<div className="OptionLabel" id="menuOptionsLabelUseProxy"> <div className="OptionLabel" id="menuOptionsLabelUseProxy">
<Tr text="options.use_proxy" /> <Tr text="options.use_proxy" />
<HelpButton contents="help.use_proxy" />
</div> </div>
<div className="OptionValue" id="menuOptionsCheckboxUseProxy"> <div className="OptionValue" id="menuOptionsCheckboxUseProxy">
<Checkbox onChange={this.toggleProxy} checked={this.state?.use_internal_proxy} id="useProxy" /> <Checkbox
onChange={() => this.toggleOption('use_internal_proxy')}
checked={this.state?.use_internal_proxy}
id="useProxy"
/>
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerWipeLogin">
<div className="OptionLabel" id="menuOptionsLabelWipeLogin">
<Tr text="options.wipe_login" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxWipeLogin">
<Checkbox
onChange={() => this.toggleOption('wipe_login')}
checked={this.state?.wipe_login}
id="wipeLogin"
/>
</div> </div>
</div> </div>
@@ -306,6 +360,7 @@ export default class Options extends React.Component<IProps, IState> {
<div className="OptionSection" id="menuOptionsContainerToggleEnc"> <div className="OptionSection" id="menuOptionsContainerToggleEnc">
<div className="OptionLabel" id="menuOptionsLabelToggleEnc"> <div className="OptionLabel" id="menuOptionsLabelToggleEnc">
<Tr text="options.toggle_encryption" /> <Tr text="options.toggle_encryption" />
<HelpButton contents="help.encryption" />
</div> </div>
<div className="OptionValue" id="menuOptionsButtonToggleEnc"> <div className="OptionValue" id="menuOptionsButtonToggleEnc">
<BigButton onClick={this.toggleEncryption} id="toggleEnc"> <BigButton onClick={this.toggleEncryption} id="toggleEnc">
@@ -361,12 +416,26 @@ export default class Options extends React.Component<IProps, IState> {
</div> </div>
<div className="OptionValue" id="menuOptionsCheckboxGCWGame"> <div className="OptionValue" id="menuOptionsCheckboxGCWGame">
<Checkbox <Checkbox
onChange={this.toggleGrasscutterWithGame} onChange={() => this.toggleOption('grasscutter_with_game')}
checked={this.state?.grasscutter_with_game} checked={this.state?.grasscutter_with_game}
id="gcWithGame" id="gcWithGame"
/> />
</div> </div>
</div> </div>
{this.state.swag ? (
<div className="OptionSection" id="menuOptionsContainerHorny">
<div className="OptionLabel" id="menuOptionsLabelHorny">
<Tr text="options.horny_mode" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxHorny">
<Checkbox
onChange={() => this.toggleOption('horny_mode')}
checked={this.state?.horny_mode}
id="hornyMode"
/>
</div>
</div>
) : null}
<Divider /> <Divider />

View File

@@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { getConfigOption } from '../../../utils/configuration'
import { getInstalledMods, getMods, ModData, PartialModData } from '../../../utils/gamebanana' import { getInstalledMods, getMods, ModData, PartialModData } from '../../../utils/gamebanana'
import { LoadingCircle } from './LoadingCircle' import { LoadingCircle } from './LoadingCircle'
@@ -11,6 +12,7 @@ interface IProps {
} }
interface IState { interface IState {
horny: boolean
modList: ModData[] | null modList: ModData[] | null
installedList: installedList:
| { | {
@@ -25,6 +27,7 @@ export class ModList extends React.Component<IProps, IState> {
super(props) super(props)
this.state = { this.state = {
horny: false,
modList: null, modList: null,
installedList: null, installedList: null,
} }
@@ -60,8 +63,10 @@ export class ModList extends React.Component<IProps, IState> {
} }
const mods = await getMods(this.props.mode) const mods = await getMods(this.props.mode)
const horny = await getConfigOption('horny_mode')
this.setState({ this.setState({
horny,
modList: mods, modList: mods,
}) })
} }
@@ -81,7 +86,7 @@ export class ModList extends React.Component<IProps, IState> {
<ModTile path={mod.path} mod={mod.info} key={mod.info.name} onClick={this.downloadMod} /> <ModTile path={mod.path} mod={mod.info} key={mod.info.name} onClick={this.downloadMod} />
)) ))
: this.state.modList?.map((mod: ModData) => ( : this.state.modList?.map((mod: ModData) => (
<ModTile mod={mod} key={mod.id} onClick={this.downloadMod} /> <ModTile horny={this.state.horny} mod={mod} key={mod.id} onClick={this.downloadMod} />
))} ))}
</div> </div>
) : ( ) : (

View File

@@ -12,6 +12,7 @@ import { disableMod, enableMod, modIsEnabled } from '../../../utils/mods'
interface IProps { interface IProps {
mod: ModData | PartialModData mod: ModData | PartialModData
horny?: boolean
path?: string path?: string
onClick: (mod: ModData) => void onClick: (mod: ModData) => void
} }
@@ -107,7 +108,9 @@ export class ModTile extends React.Component<IProps, IState> {
))} ))}
<img <img
src={mod.images[0]} src={mod.images[0]}
className={`ModImageInner ${'id' in mod && mod.nsfw ? 'nsfw' : ''} ${this.state.hover ? 'blur' : ''}`} className={`ModImageInner ${'id' in mod && !this.props.horny && mod.nsfw ? 'nsfw' : ''} ${
this.state.hover ? 'blur' : ''
}`}
/> />
</div> </div>
<div className="ModInner"> <div className="ModInner">

View File

@@ -22,6 +22,8 @@ let defaultConfig: Configuration
debug_enabled: false, debug_enabled: false,
patch_metadata: true, patch_metadata: true,
use_internal_proxy: true, use_internal_proxy: true,
wipe_login: false,
horny_mode: false,
} }
})() })()
@@ -46,6 +48,8 @@ export interface Configuration {
debug_enabled: boolean debug_enabled: boolean
patch_metadata: boolean patch_metadata: boolean
use_internal_proxy: boolean use_internal_proxy: boolean
wipe_login: boolean
horny_mode: boolean
swag_mode?: boolean swag_mode?: boolean
// Swag stuff // Swag stuff

View File

@@ -77,14 +77,20 @@ export default class DownloadHandler {
// Extraction events // Extraction events
listen('extract_start', ({ payload }) => { listen('extract_start', ({ payload }) => {
// Find the download that is no extracting and set it's status as such // Find the download that is extracting and set it's status as such
const index = this.downloads.findIndex((download) => download.path === payload) const index = this.downloads.findIndex((download) => download.path === payload)
this.downloads[index].status = 'extracting' this.downloads[index].status = 'extracting'
}) })
listen('extract_end', ({ payload }) => { listen('extract_end', ({ payload }) => {
// Find the download that is no extracting and set it's status as such // @ts-expect-error shut up typescript
const index = this.downloads.findIndex((download) => download.path === payload) const obj: {
file: string
new_folder: string
} = payload
// Find the download that is not extracting and set it's status as such
const index = this.downloads.findIndex((download) => download.path === obj.file || obj.new_folder)
this.downloads[index].status = 'finished' this.downloads[index].status = 'finished'
}) })
} }

View File

@@ -1,3 +1,4 @@
import { invoke } from '@tauri-apps/api'
import { getConfig } from './configuration' import { getConfig } from './configuration'
export async function getGameExecutable() { export async function getGameExecutable() {
@@ -25,3 +26,36 @@ export async function getGameFolder() {
return path return path
} }
export async function getGameDataFolder() {
const gameExec = await getGameExecutable()
if (!gameExec) {
return null
}
return (await getGameFolder()) + '\\' + gameExec.replace('.exe', '_Data')
}
export async function getGameVersion() {
const GameData = await getGameDataFolder()
if (!GameData) {
return null
}
const settings = JSON.parse(
await invoke('read_file', {
path: GameData + '\\StreamingAssets\\asb_settings.json',
})
)
const versionRaw = settings.variance.split('.')
const version = {
major: parseInt(versionRaw[0]),
minor: parseInt(versionRaw[1].split('_')[0]),
release: parseInt(versionRaw[1].split('_')[1]),
}
return version
}

View File

@@ -118,13 +118,30 @@ interface ModDownload {
} }
export async function getMods(mode: string) { export async function getMods(mode: string) {
const resp = JSON.parse( let modList: GamebananaResponse[] = []
await invoke('list_submissions', { let hadMods = true
mode, let page = 1
})
)
return formatGamebananaData(resp) while (hadMods) {
const resp = JSON.parse(
await invoke('list_submissions', {
mode,
page: '' + page,
})
)
if (resp.length === 0) hadMods = false
modList = [...modList, ...resp]
page++
console.log(page)
console.log(resp)
}
console.log(modList)
return formatGamebananaData(modList)
} }
export async function formatGamebananaData(obj: GamebananaResponse[]) { export async function formatGamebananaData(obj: GamebananaResponse[]) {

View File

@@ -21,38 +21,53 @@ export default class Tr extends React.Component<IProps, IState> {
} }
} }
componentDidMount() { async componentDidMount() {
const { text } = this.props const { text } = this.props
getConfigOption('language').then((language: string) => { let language = await getConfigOption('language')
// Get translation file
if (!language) language = 'en'
invoke('get_lang', { lang: language }).then((response) => { // Get translation file
const translation_obj = JSON.parse((response as string) || '{}') if (!language) language = 'en'
// Traversal const response = await invoke('get_lang', { lang: language })
if (text.includes('.')) { const default_resp = await invoke('get_lang', { lang: 'en' })
const keys = text.split('.')
let translation: string | Record<string, string> = translation_obj
for (let i = 0; i < keys.length; i++) { const translation_obj = JSON.parse((response as string) || '{}')
if (!translation) { const default_obj = JSON.parse((default_resp as string) || '{}')
translation = ''
} else {
translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
}
}
this.setState({ // Traversal
translated_text: translation as string, if (text.includes('.')) {
}) const keys = text.split('.')
let translation: string | Record<string, string> = translation_obj
for (let i = 0; i < keys.length; i++) {
if (!translation) {
translation = ''
} else { } else {
this.setState({ translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
translated_text: translation_obj[text] || '',
})
} }
}
// If we could not find a translation, use the default one
if (!translation) {
translation = default_obj
for (let i = 0; i < keys.length; i++) {
if (!translation) {
translation = ''
} else {
translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
}
}
}
this.setState({
translated_text: translation as string,
}) })
}) } else {
this.setState({
translated_text: translation_obj[text] || '',
})
}
} }
render() { render() {
@@ -82,6 +97,7 @@ export async function getLanguages() {
export async function translate(text: string) { export async function translate(text: string) {
const language = (await getConfigOption('language')) || 'en' const language = (await getConfigOption('language')) || 'en'
const translation_json = JSON.parse((await invoke('get_lang', { lang: language })) || '{}') const translation_json = JSON.parse((await invoke('get_lang', { lang: language })) || '{}')
const default_json = JSON.parse(await invoke('get_lang', { lang: 'en' }))
// Traversal // Traversal
if (text.includes('.')) { if (text.includes('.')) {
@@ -96,6 +112,19 @@ export async function translate(text: string) {
} }
} }
// If we could not find a translation, use the default one
if (!translation) {
translation = default_json
for (let i = 0; i < keys.length; i++) {
if (!translation) {
translation = ''
} else {
translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
}
}
}
return translation return translation
} else { } else {
return translation_json[text] || '' return translation_json[text] || ''

View File

@@ -1,7 +1,7 @@
import { invoke } from '@tauri-apps/api' import { invoke } from '@tauri-apps/api'
import { dataDir } from '@tauri-apps/api/path' import { dataDir } from '@tauri-apps/api/path'
import DownloadHandler from './download' import DownloadHandler from './download'
import { getGameExecutable, getGameFolder } from './game' import { getGameDataFolder } from './game'
export async function patchMetadata() { export async function patchMetadata() {
const metadataExists = await invoke('dir_exists', { const metadataExists = await invoke('dir_exists', {
@@ -35,6 +35,11 @@ export async function patchMetadata() {
}) })
if (!patchedMeta) { if (!patchedMeta) {
// Remove metadata backup, in case it invalid or something
await invoke('delete_file', {
path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
})
return false return false
} }
@@ -166,16 +171,13 @@ export async function unpatchGame() {
} }
export async function getGameMetadataPath() { export async function getGameMetadataPath() {
const gameExec = await getGameExecutable() const gameData = await getGameDataFolder()
if (!gameExec) { if (!gameData) {
return null return null
} }
return ((await getGameFolder()) + '\\' + gameExec.replace('.exe', '_Data') + '\\Managed\\Metadata').replace( return (gameData + '\\Managed\\Metadata').replace(/\\/g, '/')
/\\/g,
'/'
)
} }
export async function getBackupMetadataPath() { export async function getBackupMetadataPath() {
@@ -183,6 +185,7 @@ export async function getBackupMetadataPath() {
} }
export async function globalMetadataLink() { export async function globalMetadataLink() {
// TODO: Get metadata based on current game version.
const versionAPIUrl = const versionAPIUrl =
'https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api/resource?channel_id=1&key=gcStgarh&launcher_id=10&sub_channel_id=0' 'https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api/resource?channel_id=1&key=gcStgarh&launcher_id=10&sub_channel_id=0'

View File

@@ -21,6 +21,9 @@ export function unzip(
}) })
listen('extract_end', ({ payload }) => { listen('extract_end', ({ payload }) => {
console.log(payload)
console.log(file)
// @ts-expect-error Payload is an object // @ts-expect-error Payload is an object
if (payload?.file === file) { if (payload?.file === file) {
resolve(payload as UnzipPayload) resolve(payload as UnzipPayload)