Compare commits

...

66 Commits

Author SHA1 Message Date
SpikeHD
6db219e32e Merge pull request #109 from Drevoed/chore/bump-deps
chore: bump rust deps
2022-11-17 20:09:08 -08:00
Kirill
5bd4f3ff6e chore: bump rust deps
Bump rust dependencies and apply cargo fmt/clippy fixes
2022-11-17 23:41:08 +03:00
SpikeHD
6206b6762a cleanup 2022-10-01 16:12:32 -07:00
SpikeHD
258c656815 Merge pull request #97 from pfyy/main
make passwordKey.txt use CRLF
2022-10-01 15:55:02 -07:00
pfyy
68404f5e39 make passwordKey.txt use CRLF 2022-10-01 11:28:00 +08:00
SpikeHD
8c75541939 Merge pull request #95 from pfyy/main
fix proxy uri (add query)
2022-09-30 14:36:33 -07:00
pfyy
aa32782179 fix proxy uri (add query) 2022-09-27 19:36:39 +08:00
Magix
2cda891f02 THE GREATEST CULTIVATION UPDATE OF ALL TIME
part one of removing deprecated code
2022-09-24 00:08:12 -04:00
SpikeHD
9c8ed852d1 Merge branch 'main' of github.com:Grasscutters/Cultivation 2022-09-18 17:54:20 -07:00
SpikeHD
e75f65ab56 --no-admin and fix migoto setting 2022-09-18 17:54:05 -07:00
SpikeHD
bdeda36043 Merge pull request #92 from Grasscutters/dependabot/cargo/src-tauri/tauri-1.0.6
Bump tauri from 1.0.4 to 1.0.6 in /src-tauri
2022-09-16 23:57:04 -07:00
dependabot[bot]
151e7c3919 Bump tauri from 1.0.4 to 1.0.6 in /src-tauri
Bumps [tauri](https://github.com/tauri-apps/tauri) from 1.0.4 to 1.0.6.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v1.0.4...tauri-v1.0.6)

---
updated-dependencies:
- dependency-name: tauri
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-16 19:33:19 +00:00
SpikeHD
e6365b5592 Merge pull request #91 from trollerr/patch-1
Vietnamese Translations Update
2022-09-12 09:14:46 -07:00
Trollerr
ed76a086b6 Vietnamese Translations Update 2022-09-12 13:09:15 +07:00
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
37 changed files with 1273 additions and 1009 deletions

4
.gitattributes vendored
View File

@@ -1 +1,3 @@
* text=auto eol=lf
* text=auto eol=lf
src-tauri/keys/* binary

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

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@
# production
/build
.idea
# misc
.DS_Store

View File

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

8
.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

14
.idea/cultivation.iml generated
View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src-tauri/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/src-tauri/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

7
.idea/discord.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

View File

@@ -1,40 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="HtmlUnknownBooleanAttribute" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredUrls">
<list>
<option value="http://localhost" />
<option value="http://127.0.0.1" />
<option value="http://0.0.0.0" />
<option value="http://www.w3.org/" />
<option value="http://json-schema.org/draft" />
<option value="http://java.sun.com/" />
<option value="http://xmlns.jcp.org/" />
<option value="http://javafx.com/javafx/" />
<option value="http://javafx.com/fxml" />
<option value="http://maven.apache.org/xsd/" />
<option value="http://maven.apache.org/POM/" />
<option value="http://www.springframework.org/schema/" />
<option value="http://www.springframework.org/tags" />
<option value="http://www.springframework.org/security/tags" />
<option value="http://www.thymeleaf.org" />
<option value="http://www.jboss.org/j2ee/schema/" />
<option value="http://www.jboss.com/xml/ns/" />
<option value="http://www.ibm.com/webservices/xsd" />
<option value="http://activemq.apache.org/schema/" />
<option value="http://schema.cloudfoundry.org/spring/" />
<option value="http://schemas.xmlsoap.org/" />
<option value="http://cxf.apache.org/schemas/" />
<option value="http://primefaces.org/ui" />
<option value="http://tiles.apache.org/" />
<option value="http://api.grasscutter.io" />
<option value="http://api.grasscutters.xyz" />
</list>
</option>
</inspection_tool>
<inspection_tool class="JSIgnoredPromiseFromCall" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
</profile>
</component>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cultivation.iml" filepath="$PROJECT_DIR$/.idea/cultivation.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,19 +1,14 @@
EN | [简中](README_zh-CN.md) | [繁中](README_zh-TW.md) |
# 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.
# Cultivation
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
- [Client Patching Notice](#client-patching-notice)
- [Download](#download)
- [Setup](#setup)
- [Developer Quick-start](#developer-quickstart)
- [Setup](#setup)
- [Building](#building)
@@ -23,11 +18,45 @@ Please do **NOT install, download, or use pre-compiled versions of Cultivation f
- [Screenshots](#screenshots)
- [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
[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

View File

@@ -1,6 +1,6 @@
{
"name": "cultivation",
"version": "1.0.8",
"version": "1.0.10",
"private": true,
"dependencies": {
"@tauri-apps/api": "^1.0.0-rc.5",
@@ -20,14 +20,14 @@
"scripts": {
"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:linux": "cp -r \".\\src-tauri\\lang\" \".\\lang\"",
"postbuild:linux": "cp -r \"./src-tauri/lang\" \"./lang\"",
"build:windows": "yarn tauri build",
"build:linux": "yarn tauri build",
"build": "react-scripts build && run-script-os",
"test": "react-scripts test",
"eject": "react-scripts eject",
"tauri": "tauri",
"start:dev": "tauri dev",
"start:dev": "tauri dev -- -- --no-admin",
"format": "cargo fmt --manifest-path ./src-tauri/Cargo.toml --all && yarn prettier --write --cache --loglevel warn .",
"lint": "cargo clippy --manifest-path ./src-tauri/Cargo.toml --no-default-features && yarn tsc --noEmit && yarn eslint src",
"lint:fix": "cargo clippy --manifest-path ./src-tauri/Cargo.toml --no-default-features --fix --allow-dirty && yarn tsc --noEmit && yarn eslint --fix src",

1388
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,33 +12,33 @@ rust-version = "1.57"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.0.0-rc.8", features = [] }
tauri-build = { version = "1.2.0", features = [] }
cc = "1.0"
[target.'cfg(windows)'.dependencies]
is_elevated = "0.1.2"
registry = "1.2.1"
registry = "1.2.2"
[target.'cfg(unix)'.dependencies]
sudo = "0.6.0"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-rc.9", features = ["api-all"] }
tauri = { version = "1.2.0", features = ["api-all"] }
# Access system process info.
sysinfo = "0.24.6"
sysinfo = "0.26.7"
# ZIP-archive library.
zip-extract = "0.1.1"
unrar = "0.4.4"
zip = "0.6.2"
zip = "0.6.3"
# For creating a "global" downloads list.
once_cell = "1.13.0"
once_cell = "1.16.0"
# Program opener.
open = "3.0.2"
open = "3.0.3"
duct = "0.13.5"
# Serialization.
@@ -46,15 +46,15 @@ serde_json = "1"
# Dependencies for the HTTP(S) proxy.
http = "0.2"
hudsucker = "0.17.2"
tracing = "0.1.21"
tokio-rustls = "0.23.0"
tokio-tungstenite = "0.17.0"
tokio = { version = "1.18.2", features = ["signal"] }
rustls-pemfile = "1.0.0"
reqwest = { version = "0.11.3", features = ["stream"] }
futures-util = "0.3.14"
rcgen = { version = "0.9", features = ["x509-parser"] }
hudsucker = "0.18.0"
tracing = "0.1.37"
tokio-rustls = "0.23.4"
tokio-tungstenite = "0.17.2"
tokio = { version = "1.21.2", features = ["signal"] }
rustls-pemfile = "1.0.1"
reqwest = { version = "0.11.13", features = ["stream"] }
futures-util = "0.3.25"
rcgen = { version = "0.10", features = ["x509-parser"] }
# metadata stuff
regex = "1"

View File

@@ -1,7 +1,7 @@
<RSAKeyValue>
<Exponent>AQAB</Exponent>
<Modulus>yytg/H9lz7Lm0XcA8LMqIyXPVNApYTcSepT4VDLB4qqqFC3s
/Huv8vN7zA/P4uoREIu8KMenADFk7uwrZSxoMWwJgn6A7sbAt1cqAaUXB
9J4NzhL0x3AFTiHEQbw86hRvm2VGkbA5sWnr0NZw8SGBBY+EODwNIt51G
dBA7eoUQU=</Modulus>
<RSAKeyValue>
<Exponent>AQAB</Exponent>
<Modulus>yytg/H9lz7Lm0XcA8LMqIyXPVNApYTcSepT4VDLB4qqqFC3s
/Huv8vN7zA/P4uoREIu8KMenADFk7uwrZSxoMWwJgn6A7sbAt1cqAaUXB
9J4NzhL0x3AFTiHEQbw86hRvm2VGkbA5sWnr0NZw8SGBBY+EODwNIt51G
dBA7eoUQU=</Modulus>
</RSAKeyValue>

View File

@@ -14,6 +14,7 @@
"enabled": "Enabled",
"disabled": "Disabled",
"game_path": "Set Game Install Path",
"game_command": "Game Launch Command",
"game_executable": "Set Game Executable",
"recover_metadata": "Emergency Metadata Recovery",
"grasscutter_jar": "Set Grasscutter JAR",
@@ -26,7 +27,8 @@
"theme": "Set Theme",
"patch_metadata": "Automatically Patch Metadata",
"use_proxy": "Use Internal Proxy",
"wipe_login": "Wipe Login Cache"
"wipe_login": "Wipe Login Cache",
"horny_mode": "Horny Mode"
},
"downloads": {
"grasscutter_stable_data": "Download Grasscutter Stable Data",

View File

@@ -14,18 +14,21 @@
"enabled": "Bật",
"disabled": "Tắt",
"game_path": "Đường dẫn cài game",
"game_command": "Lệnh khởi chạy game",
"game_executable": "Tập tin thực thi game",
"recover_metadata": "Khôi phục Metadata khẩn cấp",
"recover_metadata": "Khôi phục metadata khẩn cấp",
"grasscutter_jar": "Tập tin JAR Grasscutter",
"toggle_encryption": "Bật/tắt mã hóa",
"install_certificate": "Cài chứng chỉ proxy",
"java_path": "Đường dẫn Java tùy chỉnh",
"grasscutter_with_game": "Tự động chạy Grasscutter cùng với game",
"language": "Chọn ngôn ngữ",
"language": "Ngôn ngữ",
"background": "Hình nền tùy chỉnh (liên kết hoặc tập tin hình ảnh)",
"theme": "Giao diện",
"patch_metadata": "Tự động sửa Metadata",
"use_proxy": "Sử dụng proxy nội bộ"
"patch_metadata": "Tự động vá metadata",
"use_proxy": "Sử dụng proxy nội bộ",
"wipe_login": "Tẩy sạch cache đăng nhập",
"horny_mode": "Chế độ hứng tình"
},
"downloads": {
"grasscutter_stable_data": "Tải dữ liệu Grasscutter bản ổn định",
@@ -40,17 +43,17 @@
"game": "Tải game"
},
"download_status": {
"downloading": "Đang tải xuống",
"downloading": "Đang tải",
"extracting": "Đang giải nén",
"error": "Lỗi",
"finished": "Hoàn thành",
"finished": "Đã hoàn thành",
"stopped": "Đã dừng"
},
"components": {
"select_file": "Chọn tập tin hoặc thư mục...",
"select_folder": "Chọn thư mục...",
"download": "Tải xuống",
"install": "Cài đặt"
"download": "Tải",
"install": "Cài"
},
"news": {
"latest_commits": "Thay Đổi Gần Đây",
@@ -62,11 +65,19 @@
"gc_stable_jar": "Tải xuống phiên bản ổn định của Grasscutter, bao gồm tập tin jar và các tệp dữ liệu.",
"gc_dev_jar": "Tải xuống phiên bản phát triển mới nhất của Grasscutter, bao gồm tập tin jar và các tệp dữ liệu.",
"gc_stable_data": "Tải xuống tệp dữ liệu phiên bản ổn định hiện hành của Grasscutter. Bản này không đi kèm với tập tin jar, hữu dụng khi muốn cập nhật.",
"gc_dev_data": "Tải xuống tệp dữ liệu phiên bản mới nhất của Grasscutter. Bản này không đi kèm với tập tin jar, hữu dụng khi muốn cập nhật.",
"resources": "Chúng được yêu cầu để chạy máy chủ Grasscutter. Nút này sẽ có màu xám nếu bạn đã có sẵn một thư mục tài nguyên có nội dung bên trong"
"gc_dev_data": "Tải xuống tệp dữ liệu phiên bản mới nhất của Grasscutter. Bản này không đi kèm với tập tin jar, hữu dụng khi muốn cập nhật.",
"encryption": "Mục này nên được tắt.",
"resources": "Tài nguyên được yêu cầu để chạy máy chủ Grasscutter. Nút này sẽ có màu xám nếu bạn đã có sẵn một thư mục tài nguyên (resources) có nội dung bên trong",
"emergency_metadata": "Trong trường hợp gặp vấn đề, khôi phục lại metadata về phiên bản chính thức mới nhất.",
"use_proxy": "Sử dụng proxy nội bộ của Cultivation. Nên bật tùy chọn này trừ khi bạn đang sử dụng một ứng dụng khác, như Fiddler",
"patch_metadata": "Tự động vá và sửa lại metadata của game. Tùy chọn này nên được bật trừ khi bạn đang sử dụng phiên bản cũ, phiên bản không chính thức hoặc bạn đã tự vá metadata rồi."
},
"swag": {
"akebi_name": "Akebi",
"migoto_name": "Migoto",
"reshade_name": "Reshade",
"akebi": "Tập tin thực thi Akebi",
"migoto": "Tập tin thực thi 3dMigoto"
"migoto": "Tập tin thực thi 3dMigoto",
"reshade": "Tập tin inject Reshade"
}
}

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

@@ -0,0 +1,20 @@
#[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 std::fs;
use std::io::{Read, Write};
use std::path::PathBuf;
#[tauri::command]
pub fn rename(path: String, new_name: String) {
@@ -16,9 +17,9 @@ pub fn rename(path: String, new_name: String) {
new_path = path.replace('\\', "/");
}
let path_replaced = &path.replace(&new_path.split('/').last().unwrap(), &new_name);
let path_replaced = &path.replace(new_path.split('/').last().unwrap(), &new_name);
match fs::rename(&path, &path_replaced) {
match fs::rename(&path, path_replaced) {
Ok(_) => {
println!("Renamed {} to {}", &path, path_replaced);
}
@@ -35,19 +36,19 @@ pub fn dir_create(path: String) {
#[tauri::command]
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()
}
#[tauri::command]
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
}
#[tauri::command]
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();
}
@@ -59,16 +60,15 @@ pub fn are_files_identical(path1: &str, path2: &str) -> bool {
#[tauri::command]
pub fn copy_file(path: String, new_path: String) -> bool {
let filename = &path.split('/').last().unwrap();
let mut new_path_buf = std::path::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 !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();
}
// Copy old to new
match std::fs::copy(&path_buf, format!("{}/{}", new_path, filename)) {
match std::fs::copy(path_buf, format!("{}/{}", new_path, filename)) {
Ok(_) => true,
Err(e) => {
println!("Failed to copy file: {}", e);
@@ -81,11 +81,11 @@ pub fn copy_file(path: String, new_path: String) -> bool {
#[tauri::command]
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 path_buf = std::path::PathBuf::from(&path);
let mut new_path_buf = PathBuf::from(&new_path);
let path_buf = PathBuf::from(&path);
// 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) {
Ok(_) => {}
Err(e) => {
@@ -95,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
match std::fs::copy(&path_buf, format!("{}/{}", new_path, new_name)) {
match std::fs::copy(path_buf, &new_path_buf) {
Ok(_) => true,
Err(e) => {
println!("Failed to copy file: {}", e);
@@ -109,7 +111,7 @@ pub fn copy_file_with_new_name(path: String, new_path: String, new_name: String)
#[tauri::command]
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) {
Ok(_) => true,
@@ -124,7 +126,7 @@ pub fn delete_file(path: String) -> bool {
#[tauri::command]
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) {
Ok(file) => file,
@@ -142,10 +144,10 @@ pub fn read_file(path: String) -> String {
#[tauri::command]
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
let mut file = match fs::File::create(&path_buf) {
let mut file = match fs::File::create(path_buf) {
Ok(file) => file,
Err(e) => {
println!("Failed to open file: {}", e);

View File

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

View File

@@ -9,7 +9,7 @@ pub async fn get_lang(window: tauri::Window, lang: String) -> String {
let lang_path: PathBuf = [&install_location(), "lang", &format!("{}.json", lang)]
.iter()
.collect();
match std::fs::read_to_string(&lang_path) {
match std::fs::read_to_string(lang_path) {
Ok(x) => x,
Err(e) => {
emit_lang_err(window, format!("Failed to read language file: {}", e));

View File

@@ -2,23 +2,30 @@
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#![deny(clippy::all, unused)]
use file_helpers::dir_exists;
use once_cell::sync::Lazy;
use std::fs;
use std::io::Write;
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 structs::APIQuery;
use sysinfo::{System, SystemExt};
#[cfg(windows)]
use crate::admin::reopen_as_admin;
mod admin;
mod downloader;
mod file_helpers;
mod gamebanana;
mod lang;
mod metadata_patcher;
mod proxy;
mod structs;
mod system_helpers;
mod unzip;
mod web;
@@ -29,13 +36,13 @@ fn try_flush() {
std::io::stdout().flush().unwrap_or(())
}
fn has_arg(args: &Vec<String>, arg: &str) -> bool {
fn has_arg(args: &[String], arg: &str) -> bool {
args.contains(&arg.to_string())
}
async fn arg_handler(args: &Vec<String>) {
async fn arg_handler(args: &[String]) {
if has_arg(args, "--proxy") {
let mut pathbuf = tauri::api::path::data_dir().unwrap();
let mut pathbuf = data_dir().unwrap();
pathbuf.push("cultivation");
pathbuf.push("ca");
@@ -44,17 +51,32 @@ async fn arg_handler(args: &Vec<String>) {
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if !is_elevated() && !has_arg(&args, "--no-admin") {
println!("===============================================================================");
println!("You running as a non-elevated user. Some stuff will almost definitely not work.");
println!("===============================================================================");
#[cfg(windows)]
reopen_as_admin();
}
// Setup datadir/cultivation just in case something went funky and it wasn't made
if !dir_exists(data_dir().unwrap().join("cultivation").to_str().unwrap()) {
fs::create_dir_all(data_dir().unwrap().join("cultivation")).unwrap();
}
// Always set CWD to the location of the executable.
let mut exe_path = std::env::current_exe().unwrap();
exe_path.pop();
std::env::set_current_dir(&exe_path).unwrap();
let args: Vec<String> = std::env::args().collect();
block_on(arg_handler(&args));
// For disabled GUI
ctrlc::set_handler(|| {
disconnect();
std::process::exit(0);
})
.unwrap_or(());
@@ -66,7 +88,6 @@ fn main() {
connect,
disconnect,
req_get,
get_bg_file,
is_game_running,
get_theme_list,
system_helpers::run_command,
@@ -78,6 +99,7 @@ fn main() {
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,
@@ -229,54 +251,3 @@ async fn get_theme_list(data_dir: String) -> Vec<HashMap<String, String>> {
themes
}
#[tauri::command]
// TODO: Replace with downloading the background file & saving it.
async fn get_bg_file(bg_path: String, appdata: String) -> String {
let copy_loc = appdata;
let query = web::query("https://api.grasscutter.io/cultivation/query").await;
let response_data: APIQuery = match serde_json::from_str(&query) {
Ok(data) => data,
Err(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();
}
};
let file_name = response_data.bg_file.to_string();
// First we see if the file already exists in our local bg folder.
if file_helpers::dir_exists(format!("{}\\bg\\{}", copy_loc, file_name).as_str()) {
return format!("{}\\{}", copy_loc, response_data.bg_file.as_str());
}
// Now we check if the bg folder, which is one directory above the game_path, exists.
let bg_img_path = format!("{}\\{}", &bg_path, &file_name);
// If it doesn't, then we do not have backgrounds to grab.
if !file_helpers::dir_exists(&bg_path) {
return "".to_string();
}
// BG folder does exist, lets see if the image exists.
if !file_helpers::dir_exists(&bg_img_path) {
// Image doesn't exist
return "".to_string();
}
// The image exists, lets copy it to our local '\bg' folder.
let bg_img_path_local = format!("{}\\bg\\{}", copy_loc, file_name.as_str());
match std::fs::copy(bg_img_path, bg_img_path_local) {
Ok(_) => {
// Copy was successful, lets return true.
format!("{}\\{}", copy_loc, response_data.bg_file)
}
Err(e) => {
// Copy failed, lets return false
println!("Failed to copy background image: {}", e);
"".to_string()
}
}
}

View File

@@ -1,8 +1,5 @@
use regex::Regex;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Write;
use std::{fs, fs::File, fs::OpenOptions, io::Read, io::Write, path::Path};
// For these two functions, a non-zero return value indicates failure.
extern "C" {
@@ -13,6 +10,12 @@ extern "C" {
#[tauri::command]
pub fn patch_metadata(metadata_folder: &str) -> bool {
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);
let decrypted = decrypt_metadata(metadata_file);
if do_vecs_match(&decrypted, &Vec::new()) {
@@ -36,7 +39,7 @@ pub fn patch_metadata(metadata_folder: &str) -> bool {
let mut file = match OpenOptions::new()
.create(true)
.write(true)
.open(&(metadata_folder.to_owned() + "\\global-metadata-patched.dat"))
.open(metadata_folder.to_owned() + "\\global-metadata-patched.dat")
{
Ok(file) => file,
Err(e) => {
@@ -111,20 +114,20 @@ fn replace_keys(data: &[u8]) -> Vec<u8> {
fn replace_rsa_key(old_data: &str, to_replace: &str, file_name: &str) -> String {
// Read dispatch key file
unsafe {
// Get key folder from exe path
let mut exe_path = std::env::current_exe().unwrap();
exe_path.pop();
// Get key path from current directory
let key_file_path = std::env::current_dir()
.unwrap()
.join("keys")
.join(file_name);
let key_folder = exe_path.to_str().unwrap().to_string();
let mut new_key_file = match File::open(format!("{}/keys/{}", key_folder, file_name)) {
let key_data = match fs::read(&key_file_path) {
Ok(file) => file,
Err(e) => {
println!("Failed to open keys/{}: {}", file_name, e);
println!("Failed to open {}: {}", key_file_path.to_str().unwrap(), e);
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());
// Replace old key with new key
@@ -134,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> {
let mut data = old_data.to_vec();
let success = unsafe { encrypt_global_metadata(data.as_mut_ptr(), data.len()) } == 0;
if success {
println!("Successfully encrypted global-metadata");

View File

@@ -3,8 +3,11 @@
* 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 std::{str::FromStr, sync::Mutex};
use std::{path::PathBuf, str::FromStr, sync::Mutex};
use hudsucker::{
async_trait::async_trait,
@@ -19,7 +22,7 @@ use std::net::SocketAddr;
use std::path::Path;
use rustls_pemfile as pemfile;
use tauri::http::Uri;
use tauri::{api::path::data_dir, http::Uri};
#[cfg(windows)]
use registry::{Data, Hive, Security};
@@ -49,12 +52,13 @@ impl HttpHandler for ProxyHandler {
mut request: Request<Body>,
) -> RequestOrResponse {
let uri = request.uri().to_string();
let uri_path = request.uri().path();
let uri_path_and_query = request.uri().path_and_query().unwrap().as_str();
if uri.contains("hoyoverse.com") || uri.contains("mihoyo.com") || uri.contains("yuanshen.com") {
// Create new URI.
let new_uri =
Uri::from_str(format!("{}{}", SERVER.lock().unwrap(), uri_path).as_str()).unwrap();
Uri::from_str(format!("{}{}", SERVER.lock().unwrap(), uri_path_and_query).as_str())
.unwrap();
// Set request URI to the new one.
*request.uri_mut() = new_uri;
}
@@ -75,11 +79,32 @@ impl HttpHandler for ProxyHandler {
* Starts an HTTP(S) proxy server.
*/
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.
let mut private_key_bytes: &[u8] =
&fs::read(format!("{}\\private.key", certificate_path)).expect("Could not read private key");
let mut ca_cert_bytes: &[u8] =
&fs::read(format!("{}\\cert.crt", certificate_path)).expect("Could not read certificate");
let mut private_key_bytes: &[u8] = &match fs::read(&pk_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(&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.
let private_key = rustls::PrivateKey(
@@ -138,9 +163,29 @@ pub fn connect_to_proxy(proxy_port: u16) {
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) {
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 +207,26 @@ pub fn disconnect_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() {}
/*
@@ -260,11 +324,27 @@ pub fn install_ca_files(cert_path: &Path) {
"/Library/Keychains/System.keychain",
cert_path.to_str().unwrap(),
],
None,
);
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) {
println!("Certificate installation is not supported on this platform.");
}

View File

@@ -1,8 +0,0 @@
#![allow(non_snake_case)]
use serde::Deserialize;
#[derive(Deserialize)]
pub(crate) struct APIQuery {
pub bg_file: String,
}

View File

@@ -8,7 +8,7 @@ use registry::{Data, Hive, Security};
#[tauri::command]
pub fn run_program(path: String, args: Option<String>) {
// 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]
@@ -24,10 +24,10 @@ pub fn run_program_relative(path: String, args: Option<String>) {
std::env::set_current_dir(&path_buf).unwrap();
// 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
std::env::set_current_dir(&cwd).unwrap();
std::env::set_current_dir(cwd).unwrap();
}
#[tauri::command]
@@ -52,7 +52,7 @@ pub fn run_command(program: &str, args: Vec<&str>, relative: Option<bool>) {
cmd(prog, args).run().unwrap();
// Restore the original working directory
std::env::set_current_dir(&cwd).unwrap();
std::env::set_current_dir(cwd).unwrap();
});
}
@@ -115,7 +115,7 @@ pub fn set_migoto_target(path: String, migoto_path: String) -> bool {
// Set options
conf
.with_section(Some("Loader"))
.set("target", pathbuf.to_str().unwrap());
.set("target", pathbuf.file_name().unwrap().to_str().unwrap());
// Write file
match conf.write_to_file(&migoto_pathbuf) {
@@ -130,6 +130,7 @@ pub fn set_migoto_target(path: String, migoto_path: String) -> bool {
}
}
#[cfg(windows)]
#[tauri::command]
pub fn wipe_registry(exec_name: String) {
// Fetch the 'Internet Settings' registry key.
@@ -152,6 +153,10 @@ pub fn wipe_registry(exec_name: String) {
}
}
#[cfg(unix)]
#[tauri::command]
pub fn wipe_registry(_exec_name: String) {}
#[cfg(windows)]
#[tauri::command]
pub fn is_elevated() -> bool {
@@ -163,3 +168,8 @@ pub fn is_elevated() -> bool {
pub fn is_elevated() -> bool {
sudo::check() == sudo::RunningAs::Root
}
#[tauri::command]
pub fn get_platform() -> &'static str {
std::env::consts::OS
}

View File

@@ -58,7 +58,7 @@ pub fn unzip(
}
};
full_path = new_path.clone();
full_path = new_path;
}
println!("Is rar file? {}", zipfile.ends_with(".rar"));
@@ -78,7 +78,7 @@ pub fn unzip(
// Get the name of the inenr file in the zip file
let mut zip = zip::ZipArchive::new(&f).unwrap();
let file = zip.by_index(0).unwrap();
name = file.name().to_string().clone();
name = file.name().to_string();
}
if !success {
@@ -111,23 +111,21 @@ pub fn unzip(
for entry in read_dir(&write_path).unwrap() {
let entry = entry.unwrap();
let entry_path = entry.path();
if entry_path.is_dir() {
if !dirs.contains(&entry_path) {
new_dir = entry_path.to_str().unwrap().to_string();
}
if entry_path.is_dir() && !dirs.contains(&entry_path) {
new_dir = entry_path.to_str().unwrap().to_string();
}
}
let mut res_hash = std::collections::HashMap::new();
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();
});
}
fn extract_rar(rarfile: &String, _f: &File, full_path: &path::PathBuf, _top_level: bool) -> bool {
let archive = Archive::new(rarfile.clone());
fn extract_rar(rarfile: &str, _f: &File, full_path: &path::Path, _top_level: bool) -> bool {
let archive = Archive::new(rarfile.to_string());
let mut open_archive = archive
.extract_to(full_path.to_str().unwrap().to_string())
@@ -149,7 +147,7 @@ fn extract_rar(rarfile: &String, _f: &File, full_path: &path::PathBuf, _top_leve
}
}
fn extract_zip(_zipfile: &String, f: &File, full_path: &path::PathBuf, top_level: bool) -> bool {
fn extract_zip(_zipfile: &str, f: &File, full_path: &path::Path, top_level: bool) -> bool {
match zip_extract::extract(f, full_path, top_level) {
Ok(_) => {
println!(

View File

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

View File

@@ -5,7 +5,6 @@ import DownloadHandler from '../utils/download'
import { getConfigOption } from '../utils/configuration'
import { getTheme, loadTheme } from '../utils/themes'
import { convertFileSrc, invoke } from '@tauri-apps/api/tauri'
import { dataDir } from '@tauri-apps/api/path'
import { Main } from './Main'
import { Mods } from './Mods'
@@ -28,10 +27,6 @@ class App extends React.Component<Readonly<unknown>, IState> {
}
async componentDidMount() {
const game_exe = await getConfigOption('game_install_path')
const game_path = game_exe?.substring(0, game_exe.replace(/\\/g, '/').lastIndexOf('/')) || ''
const root_path = game_path?.substring(0, game_path.replace(/\\/g, '/').lastIndexOf('/')) || ''
// Load a theme if it exists
const theme = await getConfigOption('theme')
if (theme && theme !== 'default') {
@@ -42,23 +37,7 @@ class App extends React.Component<Readonly<unknown>, IState> {
// Get custom bg AFTER theme is loaded !! important !!
const custom_bg = await getConfigOption('customBackground')
if (!custom_bg || !/png|jpg|jpeg$/.test(custom_bg)) {
if (game_path) {
// Get the bg by invoking, then set the background to that bg.
const bgLoc: string = await invoke('get_bg_file', {
bgPath: root_path,
appdata: await dataDir(),
})
bgLoc &&
this.setState(
{
bgFile: bgLoc,
},
this.forceUpdate
)
}
} else {
if (custom_bg) {
const isUrl = /^http(s)?:\/\//gm.test(custom_bg)
if (!isUrl) {

View File

@@ -12,7 +12,7 @@ import Plus from '../../resources/icons/plus.svg'
import './ServerLaunchSection.css'
import { dataDir } from '@tauri-apps/api/path'
import { getGameExecutable } from '../../utils/game'
import { getGameExecutable, getGameVersion } from '../../utils/game'
import { patchGame, unpatchGame } from '../../utils/metadata'
interface IProps {
@@ -110,6 +110,30 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
// Connect to proxy
if (config.toggle_grasscutter) {
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()
if (!patched) {
@@ -161,11 +185,13 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
}
}
// 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'),
})
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
const gameExists = await invoke('dir_exists', {

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 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 RESOURCES_DOWNLOAD = 'https://gitlab.com/yukiz/GrasscutterResources/-/archive/3.0/GrasscutterResources-3.0.zip'
const RESOURCES_DOWNLOAD = 'https://github.com/tamilpp25/Grasscutter_Resources/archive/refs/heads/3.0.zip'
interface IProps {
closeFn: () => void

View File

@@ -4,7 +4,7 @@ import { dataDir } from '@tauri-apps/api/path'
import DirInput from '../common/DirInput'
import Menu from './Menu'
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 Divider from './Divider'
import { getThemeList } from '../../../utils/themes'
@@ -15,6 +15,7 @@ import BigButton from '../common/BigButton'
import DownloadHandler from '../../../utils/download'
import * as meta from '../../../utils/metadata'
import HelpButton from '../common/HelpButton'
import TextInput from '../common/TextInput'
interface IProps {
closeFn: () => void
@@ -35,7 +36,9 @@ interface IState {
patch_metadata: boolean
use_internal_proxy: boolean
wipe_login: boolean
horny_mode: boolean
swag: boolean
platform: string
// Swag stuff
akebi_path: string
@@ -61,7 +64,9 @@ export default class Options extends React.Component<IProps, IState> {
patch_metadata: false,
use_internal_proxy: false,
wipe_login: false,
horny_mode: false,
swag: false,
platform: '',
// Swag stuff
akebi_path: '',
@@ -78,20 +83,20 @@ export default class Options extends React.Component<IProps, IState> {
this.setCustomBackground = this.setCustomBackground.bind(this)
this.toggleEncryption = this.toggleEncryption.bind(this)
this.restoreMetadata = this.restoreMetadata.bind(this)
this.toggleMetadata = this.toggleMetadata.bind(this)
this.toggleProxy = this.toggleProxy.bind(this)
this.toggleLoginWipe = this.toggleLoginWipe.bind(this)
}
async componentDidMount() {
const config = await getConfig()
const languages = await getLanguages()
const platform: string = await invoke('get_platform')
// Remove jar from path
const path = config.grasscutter_path.replace(/\\/g, '/')
const folderPath = path.substring(0, path.lastIndexOf('/'))
const encEnabled = await server.encryptionEnabled(folderPath + '/config.json')
console.log(platform)
this.setState({
game_install_path: config.game_install_path || '',
grasscutter_path: config.grasscutter_path || '',
@@ -106,7 +111,9 @@ export default class Options extends React.Component<IProps, IState> {
patch_metadata: config.patch_metadata || 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,
platform,
// Swag stuff
akebi_path: config.akebi_path || '',
@@ -120,6 +127,17 @@ export default class Options extends React.Component<IProps, IState> {
setGameExecutable(value: string) {
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({
game_install_path: value,
})
@@ -246,47 +264,39 @@ export default class Options extends React.Component<IProps, IState> {
})
}
async toggleMetadata() {
const changedVal = !(await getConfigOption('patch_metadata'))
async toggleOption(opt: keyof Configuration) {
const changedVal = !(await getConfigOption(opt))
await setConfigOption('patch_metadata', changedVal)
await setConfigOption(opt, changedVal)
// @ts-expect-error shut up bitch
this.setState({
patch_metadata: changedVal,
})
}
async toggleProxy() {
const changedVal = !(await getConfigOption('use_internal_proxy'))
await setConfigOption('use_internal_proxy', changedVal)
this.setState({
use_internal_proxy: changedVal,
})
}
async toggleLoginWipe() {
const changedVal = !(await getConfigOption('wipe_login'))
await setConfigOption('wipe_login', changedVal)
this.setState({
wipe_login: changedVal,
[opt]: changedVal,
})
}
render() {
return (
<Menu closeFn={this.props.closeFn} className="Options" heading="Options">
<div className="OptionSection" id="menuOptionsContainerGamePath">
<div className="OptionLabel" id="menuOptionsLabelGamePath">
<Tr text="options.game_path" />
{!this.state.platform || this.state.platform === 'windows' ? (
<div className="OptionSection" id="menuOptionsContainerGamePath">
<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 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 className="OptionSection" id="menuOptionsContainermetaDownload">
<div className="OptionLabel" id="menuOptionsLabelmetaDownload">
<Tr text="options.recover_metadata" />
@@ -304,7 +314,11 @@ export default class Options extends React.Component<IProps, IState> {
<HelpButton contents="help.patch_metadata" />
</div>
<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 className="OptionSection" id="menuOptionsContainerUseProxy">
@@ -313,7 +327,11 @@ export default class Options extends React.Component<IProps, IState> {
<HelpButton contents="help.use_proxy" />
</div>
<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">
@@ -321,7 +339,11 @@ export default class Options extends React.Component<IProps, IState> {
<Tr text="options.wipe_login" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxWipeLogin">
<Checkbox onChange={this.toggleLoginWipe} checked={this.state?.wipe_login} id="wipeLogin" />
<Checkbox
onChange={() => this.toggleOption('wipe_login')}
checked={this.state?.wipe_login}
id="wipeLogin"
/>
</div>
</div>
@@ -394,12 +416,26 @@ export default class Options extends React.Component<IProps, IState> {
</div>
<div className="OptionValue" id="menuOptionsCheckboxGCWGame">
<Checkbox
onChange={this.toggleGrasscutterWithGame}
onChange={() => this.toggleOption('grasscutter_with_game')}
checked={this.state?.grasscutter_with_game}
id="gcWithGame"
/>
</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 />

View File

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

View File

@@ -12,6 +12,7 @@ import { disableMod, enableMod, modIsEnabled } from '../../../utils/mods'
interface IProps {
mod: ModData | PartialModData
horny?: boolean
path?: string
onClick: (mod: ModData) => void
}
@@ -107,7 +108,9 @@ export class ModTile extends React.Component<IProps, IState> {
))}
<img
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 className="ModInner">

View File

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

View File

@@ -1,3 +1,4 @@
import { invoke } from '@tauri-apps/api'
import { getConfig } from './configuration'
export async function getGameExecutable() {
@@ -25,3 +26,36 @@ export async function getGameFolder() {
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

@@ -1,7 +1,7 @@
import { invoke } from '@tauri-apps/api'
import { dataDir } from '@tauri-apps/api/path'
import DownloadHandler from './download'
import { getGameExecutable, getGameFolder } from './game'
import { getGameDataFolder } from './game'
export async function patchMetadata() {
const metadataExists = await invoke('dir_exists', {
@@ -35,6 +35,11 @@ export async function patchMetadata() {
})
if (!patchedMeta) {
// Remove metadata backup, in case it invalid or something
await invoke('delete_file', {
path: (await getBackupMetadataPath()) + '\\global-metadata-unpatched.dat',
})
return false
}
@@ -166,16 +171,13 @@ export async function unpatchGame() {
}
export async function getGameMetadataPath() {
const gameExec = await getGameExecutable()
const gameData = await getGameDataFolder()
if (!gameExec) {
if (!gameData) {
return null
}
return ((await getGameFolder()) + '\\' + gameExec.replace('.exe', '_Data') + '\\Managed\\Metadata').replace(
/\\/g,
'/'
)
return (gameData + '\\Managed\\Metadata').replace(/\\/g, '/')
}
export async function getBackupMetadataPath() {
@@ -183,6 +185,7 @@ export async function getBackupMetadataPath() {
}
export async function globalMetadataLink() {
// TODO: Get metadata based on current game version.
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'