Compare commits

...

1 Commits

Author SHA1 Message Date
SpikeHD
d80e5c762e feat: the great purge 2023-10-01 12:29:16 -07:00
100 changed files with 10 additions and 19369 deletions

View File

@@ -75,37 +75,37 @@ Please allow the Cultivation window to pop back up once you have quit out of the
### Setup
- Install [NodeJS >12](https://nodejs.org/en/)
- Install [yarn](https://classic.yarnpkg.com/lang/en/docs/install) (cry about it `npm` lovers)
- Install [NodeJS >18](https://nodejs.org/en/)
- Install [pnpm](https://pnpm.io/installation) (cry about it `yarn` lovers)
- Install [Rust](https://www.rust-lang.org/tools/install)
- `yarn install`
- `yarn tauri dev`
- `pnpm install`
- `pnpm tauri dev`
### Building
For a release build,
- `yarn build`
- `pnpm build`
For a debug build,
- `yarn build --debug`
- `pnpm build --debug`
### Code Formatting and Linting
Formatting:
- `yarn format`
- `pnpm format`
Check Lints, fix (some) lints:
- `yarn lint`, `yarn lint:fix`
- `pnpm lint`, `pnpm lint:fix`
### Generating Update Artifacts
- Add the `TAURI_PRIVATE_KEY` as an environment variable with a path to your private key.
- Add the `TAURI_KEY_PASSWORD` as an environment variable with the password for your private key.
- `yarn build`
- `pnpm build`
The update will be at `src-tauri/target/(release|debug)/msi/Cultivation_X.X.X_x64_xx-XX.msi.zip`
@@ -115,10 +115,7 @@ A full theming reference can be found [here!](/THEMES.md)
# Screenshots
![image](https://user-images.githubusercontent.com/107363768/221495236-ca1e2f2e-0f85-4765-a5f3-8bdcea299612.png)
![image](https://user-images.githubusercontent.com/107363768/221495246-ea309640-f866-4f50-bda8-f9d916380f92.png)
![image](https://user-images.githubusercontent.com/107363768/221495249-5a1aac39-9e8a-4244-9642-72c2e7be8a69.png)
![image](https://user-images.githubusercontent.com/107363768/221495254-ffbfc24e-ef5d-4e72-9068-a02132381dcc.png)
TODO
## Credits

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -16,7 +16,6 @@ tauri-build = { version = "1.0.0-rc.8", features = [] }
cc = "1.0"
[target.'cfg(windows)'.dependencies]
is_elevated = "0.1.2"
registry = "1.2.1"
[target.'cfg(unix)'.dependencies]
@@ -45,9 +44,6 @@ unrar = "0.4.4"
zip = "0.6.2"
sevenz-rust = "0.2.9"
# For creating a "global" downloads list.
once_cell = "1.13.0"
# Program opener.
open = "3.0.2"
@@ -62,7 +58,6 @@ http = "0.2"
hudsucker = "0.19.2"
tracing = "0.1.21"
tokio-rustls = "0.23.0"
tokio-tungstenite = "0.17.0"
tokio = { version = "1.20.4", features = ["signal"] }
rustls-pemfile = "1.0.0"
reqwest = { version = "0.11.3", features = ["stream"] }
@@ -73,7 +68,6 @@ rcgen = { version = "0.9", features = ["x509-parser"] }
regex = "1"
# other
file_diff = "1.0.0"
rust-ini = "0.18.0"
ctrlc = "3.2.3"

View File

@@ -1,16 +1,3 @@
fn main() {
cc::Build::new()
.include("mhycrypto")
.cpp(true)
.file("mhycrypto/memecrypto.cpp")
.file("mhycrypto/metadata.cpp")
.file("mhycrypto/metadatastringdec.cpp")
.compile("mhycrypto");
cc::Build::new()
.include("mhycrypto")
.file("mhycrypto/aes.c")
.compile("mhycrypto-aes");
tauri_build::build()
}

View File

@@ -1,387 +0,0 @@
// Simple, thoroughly commented implementation of 128-bit AES / Rijndael using C
// Chris Hulbert - chris.hulbert@gmail.com - http://splinter.com.au/blog
// References:
// http://en.wikipedia.org/wiki/Advanced_Encryption_Standard
// http://en.wikipedia.org/wiki/Rijndael_key_schedule
// http://en.wikipedia.org/wiki/Rijndael_mix_columns
// http://en.wikipedia.org/wiki/Rijndael_S-box
// This code is public domain, or any OSI-approved license, your choice. No warranty.
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "aes.h"
typedef unsigned char byte;
// Here are all the lookup tables for the row shifts, rcon, s-boxes, and galois field multiplications
static const byte shift_rows_table[] = {0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, 1, 6, 11};
static const byte shift_rows_table_inv[] = {0, 13, 10, 7, 4, 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3};
static const byte lookup_rcon[] = {
0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a};
static const byte lookup_sbox[] = {
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16};
static const byte lookup_sbox_inv[] = {
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d};
static const byte lookup_g2[] = {
0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1a, 0x1c, 0x1e,
0x20, 0x22, 0x24, 0x26, 0x28, 0x2a, 0x2c, 0x2e, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3a, 0x3c, 0x3e,
0x40, 0x42, 0x44, 0x46, 0x48, 0x4a, 0x4c, 0x4e, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5a, 0x5c, 0x5e,
0x60, 0x62, 0x64, 0x66, 0x68, 0x6a, 0x6c, 0x6e, 0x70, 0x72, 0x74, 0x76, 0x78, 0x7a, 0x7c, 0x7e,
0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c, 0x8e, 0x90, 0x92, 0x94, 0x96, 0x98, 0x9a, 0x9c, 0x9e,
0xa0, 0xa2, 0xa4, 0xa6, 0xa8, 0xaa, 0xac, 0xae, 0xb0, 0xb2, 0xb4, 0xb6, 0xb8, 0xba, 0xbc, 0xbe,
0xc0, 0xc2, 0xc4, 0xc6, 0xc8, 0xca, 0xcc, 0xce, 0xd0, 0xd2, 0xd4, 0xd6, 0xd8, 0xda, 0xdc, 0xde,
0xe0, 0xe2, 0xe4, 0xe6, 0xe8, 0xea, 0xec, 0xee, 0xf0, 0xf2, 0xf4, 0xf6, 0xf8, 0xfa, 0xfc, 0xfe,
0x1b, 0x19, 0x1f, 0x1d, 0x13, 0x11, 0x17, 0x15, 0x0b, 0x09, 0x0f, 0x0d, 0x03, 0x01, 0x07, 0x05,
0x3b, 0x39, 0x3f, 0x3d, 0x33, 0x31, 0x37, 0x35, 0x2b, 0x29, 0x2f, 0x2d, 0x23, 0x21, 0x27, 0x25,
0x5b, 0x59, 0x5f, 0x5d, 0x53, 0x51, 0x57, 0x55, 0x4b, 0x49, 0x4f, 0x4d, 0x43, 0x41, 0x47, 0x45,
0x7b, 0x79, 0x7f, 0x7d, 0x73, 0x71, 0x77, 0x75, 0x6b, 0x69, 0x6f, 0x6d, 0x63, 0x61, 0x67, 0x65,
0x9b, 0x99, 0x9f, 0x9d, 0x93, 0x91, 0x97, 0x95, 0x8b, 0x89, 0x8f, 0x8d, 0x83, 0x81, 0x87, 0x85,
0xbb, 0xb9, 0xbf, 0xbd, 0xb3, 0xb1, 0xb7, 0xb5, 0xab, 0xa9, 0xaf, 0xad, 0xa3, 0xa1, 0xa7, 0xa5,
0xdb, 0xd9, 0xdf, 0xdd, 0xd3, 0xd1, 0xd7, 0xd5, 0xcb, 0xc9, 0xcf, 0xcd, 0xc3, 0xc1, 0xc7, 0xc5,
0xfb, 0xf9, 0xff, 0xfd, 0xf3, 0xf1, 0xf7, 0xf5, 0xeb, 0xe9, 0xef, 0xed, 0xe3, 0xe1, 0xe7, 0xe5};
static const byte lookup_g3[] = {
0x00, 0x03, 0x06, 0x05, 0x0c, 0x0f, 0x0a, 0x09, 0x18, 0x1b, 0x1e, 0x1d, 0x14, 0x17, 0x12, 0x11,
0x30, 0x33, 0x36, 0x35, 0x3c, 0x3f, 0x3a, 0x39, 0x28, 0x2b, 0x2e, 0x2d, 0x24, 0x27, 0x22, 0x21,
0x60, 0x63, 0x66, 0x65, 0x6c, 0x6f, 0x6a, 0x69, 0x78, 0x7b, 0x7e, 0x7d, 0x74, 0x77, 0x72, 0x71,
0x50, 0x53, 0x56, 0x55, 0x5c, 0x5f, 0x5a, 0x59, 0x48, 0x4b, 0x4e, 0x4d, 0x44, 0x47, 0x42, 0x41,
0xc0, 0xc3, 0xc6, 0xc5, 0xcc, 0xcf, 0xca, 0xc9, 0xd8, 0xdb, 0xde, 0xdd, 0xd4, 0xd7, 0xd2, 0xd1,
0xf0, 0xf3, 0xf6, 0xf5, 0xfc, 0xff, 0xfa, 0xf9, 0xe8, 0xeb, 0xee, 0xed, 0xe4, 0xe7, 0xe2, 0xe1,
0xa0, 0xa3, 0xa6, 0xa5, 0xac, 0xaf, 0xaa, 0xa9, 0xb8, 0xbb, 0xbe, 0xbd, 0xb4, 0xb7, 0xb2, 0xb1,
0x90, 0x93, 0x96, 0x95, 0x9c, 0x9f, 0x9a, 0x99, 0x88, 0x8b, 0x8e, 0x8d, 0x84, 0x87, 0x82, 0x81,
0x9b, 0x98, 0x9d, 0x9e, 0x97, 0x94, 0x91, 0x92, 0x83, 0x80, 0x85, 0x86, 0x8f, 0x8c, 0x89, 0x8a,
0xab, 0xa8, 0xad, 0xae, 0xa7, 0xa4, 0xa1, 0xa2, 0xb3, 0xb0, 0xb5, 0xb6, 0xbf, 0xbc, 0xb9, 0xba,
0xfb, 0xf8, 0xfd, 0xfe, 0xf7, 0xf4, 0xf1, 0xf2, 0xe3, 0xe0, 0xe5, 0xe6, 0xef, 0xec, 0xe9, 0xea,
0xcb, 0xc8, 0xcd, 0xce, 0xc7, 0xc4, 0xc1, 0xc2, 0xd3, 0xd0, 0xd5, 0xd6, 0xdf, 0xdc, 0xd9, 0xda,
0x5b, 0x58, 0x5d, 0x5e, 0x57, 0x54, 0x51, 0x52, 0x43, 0x40, 0x45, 0x46, 0x4f, 0x4c, 0x49, 0x4a,
0x6b, 0x68, 0x6d, 0x6e, 0x67, 0x64, 0x61, 0x62, 0x73, 0x70, 0x75, 0x76, 0x7f, 0x7c, 0x79, 0x7a,
0x3b, 0x38, 0x3d, 0x3e, 0x37, 0x34, 0x31, 0x32, 0x23, 0x20, 0x25, 0x26, 0x2f, 0x2c, 0x29, 0x2a,
0x0b, 0x08, 0x0d, 0x0e, 0x07, 0x04, 0x01, 0x02, 0x13, 0x10, 0x15, 0x16, 0x1f, 0x1c, 0x19, 0x1a};
static const byte lookup_g9[] = {
0x00, 0x09, 0x12, 0x1b, 0x24, 0x2d, 0x36, 0x3f, 0x48, 0x41, 0x5a, 0x53, 0x6c, 0x65, 0x7e, 0x77,
0x90, 0x99, 0x82, 0x8b, 0xb4, 0xbd, 0xa6, 0xaf, 0xd8, 0xd1, 0xca, 0xc3, 0xfc, 0xf5, 0xee, 0xe7,
0x3b, 0x32, 0x29, 0x20, 0x1f, 0x16, 0x0d, 0x04, 0x73, 0x7a, 0x61, 0x68, 0x57, 0x5e, 0x45, 0x4c,
0xab, 0xa2, 0xb9, 0xb0, 0x8f, 0x86, 0x9d, 0x94, 0xe3, 0xea, 0xf1, 0xf8, 0xc7, 0xce, 0xd5, 0xdc,
0x76, 0x7f, 0x64, 0x6d, 0x52, 0x5b, 0x40, 0x49, 0x3e, 0x37, 0x2c, 0x25, 0x1a, 0x13, 0x08, 0x01,
0xe6, 0xef, 0xf4, 0xfd, 0xc2, 0xcb, 0xd0, 0xd9, 0xae, 0xa7, 0xbc, 0xb5, 0x8a, 0x83, 0x98, 0x91,
0x4d, 0x44, 0x5f, 0x56, 0x69, 0x60, 0x7b, 0x72, 0x05, 0x0c, 0x17, 0x1e, 0x21, 0x28, 0x33, 0x3a,
0xdd, 0xd4, 0xcf, 0xc6, 0xf9, 0xf0, 0xeb, 0xe2, 0x95, 0x9c, 0x87, 0x8e, 0xb1, 0xb8, 0xa3, 0xaa,
0xec, 0xe5, 0xfe, 0xf7, 0xc8, 0xc1, 0xda, 0xd3, 0xa4, 0xad, 0xb6, 0xbf, 0x80, 0x89, 0x92, 0x9b,
0x7c, 0x75, 0x6e, 0x67, 0x58, 0x51, 0x4a, 0x43, 0x34, 0x3d, 0x26, 0x2f, 0x10, 0x19, 0x02, 0x0b,
0xd7, 0xde, 0xc5, 0xcc, 0xf3, 0xfa, 0xe1, 0xe8, 0x9f, 0x96, 0x8d, 0x84, 0xbb, 0xb2, 0xa9, 0xa0,
0x47, 0x4e, 0x55, 0x5c, 0x63, 0x6a, 0x71, 0x78, 0x0f, 0x06, 0x1d, 0x14, 0x2b, 0x22, 0x39, 0x30,
0x9a, 0x93, 0x88, 0x81, 0xbe, 0xb7, 0xac, 0xa5, 0xd2, 0xdb, 0xc0, 0xc9, 0xf6, 0xff, 0xe4, 0xed,
0x0a, 0x03, 0x18, 0x11, 0x2e, 0x27, 0x3c, 0x35, 0x42, 0x4b, 0x50, 0x59, 0x66, 0x6f, 0x74, 0x7d,
0xa1, 0xa8, 0xb3, 0xba, 0x85, 0x8c, 0x97, 0x9e, 0xe9, 0xe0, 0xfb, 0xf2, 0xcd, 0xc4, 0xdf, 0xd6,
0x31, 0x38, 0x23, 0x2a, 0x15, 0x1c, 0x07, 0x0e, 0x79, 0x70, 0x6b, 0x62, 0x5d, 0x54, 0x4f, 0x46};
static const byte lookup_g11[] = {
0x00, 0x0b, 0x16, 0x1d, 0x2c, 0x27, 0x3a, 0x31, 0x58, 0x53, 0x4e, 0x45, 0x74, 0x7f, 0x62, 0x69,
0xb0, 0xbb, 0xa6, 0xad, 0x9c, 0x97, 0x8a, 0x81, 0xe8, 0xe3, 0xfe, 0xf5, 0xc4, 0xcf, 0xd2, 0xd9,
0x7b, 0x70, 0x6d, 0x66, 0x57, 0x5c, 0x41, 0x4a, 0x23, 0x28, 0x35, 0x3e, 0x0f, 0x04, 0x19, 0x12,
0xcb, 0xc0, 0xdd, 0xd6, 0xe7, 0xec, 0xf1, 0xfa, 0x93, 0x98, 0x85, 0x8e, 0xbf, 0xb4, 0xa9, 0xa2,
0xf6, 0xfd, 0xe0, 0xeb, 0xda, 0xd1, 0xcc, 0xc7, 0xae, 0xa5, 0xb8, 0xb3, 0x82, 0x89, 0x94, 0x9f,
0x46, 0x4d, 0x50, 0x5b, 0x6a, 0x61, 0x7c, 0x77, 0x1e, 0x15, 0x08, 0x03, 0x32, 0x39, 0x24, 0x2f,
0x8d, 0x86, 0x9b, 0x90, 0xa1, 0xaa, 0xb7, 0xbc, 0xd5, 0xde, 0xc3, 0xc8, 0xf9, 0xf2, 0xef, 0xe4,
0x3d, 0x36, 0x2b, 0x20, 0x11, 0x1a, 0x07, 0x0c, 0x65, 0x6e, 0x73, 0x78, 0x49, 0x42, 0x5f, 0x54,
0xf7, 0xfc, 0xe1, 0xea, 0xdb, 0xd0, 0xcd, 0xc6, 0xaf, 0xa4, 0xb9, 0xb2, 0x83, 0x88, 0x95, 0x9e,
0x47, 0x4c, 0x51, 0x5a, 0x6b, 0x60, 0x7d, 0x76, 0x1f, 0x14, 0x09, 0x02, 0x33, 0x38, 0x25, 0x2e,
0x8c, 0x87, 0x9a, 0x91, 0xa0, 0xab, 0xb6, 0xbd, 0xd4, 0xdf, 0xc2, 0xc9, 0xf8, 0xf3, 0xee, 0xe5,
0x3c, 0x37, 0x2a, 0x21, 0x10, 0x1b, 0x06, 0x0d, 0x64, 0x6f, 0x72, 0x79, 0x48, 0x43, 0x5e, 0x55,
0x01, 0x0a, 0x17, 0x1c, 0x2d, 0x26, 0x3b, 0x30, 0x59, 0x52, 0x4f, 0x44, 0x75, 0x7e, 0x63, 0x68,
0xb1, 0xba, 0xa7, 0xac, 0x9d, 0x96, 0x8b, 0x80, 0xe9, 0xe2, 0xff, 0xf4, 0xc5, 0xce, 0xd3, 0xd8,
0x7a, 0x71, 0x6c, 0x67, 0x56, 0x5d, 0x40, 0x4b, 0x22, 0x29, 0x34, 0x3f, 0x0e, 0x05, 0x18, 0x13,
0xca, 0xc1, 0xdc, 0xd7, 0xe6, 0xed, 0xf0, 0xfb, 0x92, 0x99, 0x84, 0x8f, 0xbe, 0xb5, 0xa8, 0xa3};
static const byte lookup_g13[] = {
0x00, 0x0d, 0x1a, 0x17, 0x34, 0x39, 0x2e, 0x23, 0x68, 0x65, 0x72, 0x7f, 0x5c, 0x51, 0x46, 0x4b,
0xd0, 0xdd, 0xca, 0xc7, 0xe4, 0xe9, 0xfe, 0xf3, 0xb8, 0xb5, 0xa2, 0xaf, 0x8c, 0x81, 0x96, 0x9b,
0xbb, 0xb6, 0xa1, 0xac, 0x8f, 0x82, 0x95, 0x98, 0xd3, 0xde, 0xc9, 0xc4, 0xe7, 0xea, 0xfd, 0xf0,
0x6b, 0x66, 0x71, 0x7c, 0x5f, 0x52, 0x45, 0x48, 0x03, 0x0e, 0x19, 0x14, 0x37, 0x3a, 0x2d, 0x20,
0x6d, 0x60, 0x77, 0x7a, 0x59, 0x54, 0x43, 0x4e, 0x05, 0x08, 0x1f, 0x12, 0x31, 0x3c, 0x2b, 0x26,
0xbd, 0xb0, 0xa7, 0xaa, 0x89, 0x84, 0x93, 0x9e, 0xd5, 0xd8, 0xcf, 0xc2, 0xe1, 0xec, 0xfb, 0xf6,
0xd6, 0xdb, 0xcc, 0xc1, 0xe2, 0xef, 0xf8, 0xf5, 0xbe, 0xb3, 0xa4, 0xa9, 0x8a, 0x87, 0x90, 0x9d,
0x06, 0x0b, 0x1c, 0x11, 0x32, 0x3f, 0x28, 0x25, 0x6e, 0x63, 0x74, 0x79, 0x5a, 0x57, 0x40, 0x4d,
0xda, 0xd7, 0xc0, 0xcd, 0xee, 0xe3, 0xf4, 0xf9, 0xb2, 0xbf, 0xa8, 0xa5, 0x86, 0x8b, 0x9c, 0x91,
0x0a, 0x07, 0x10, 0x1d, 0x3e, 0x33, 0x24, 0x29, 0x62, 0x6f, 0x78, 0x75, 0x56, 0x5b, 0x4c, 0x41,
0x61, 0x6c, 0x7b, 0x76, 0x55, 0x58, 0x4f, 0x42, 0x09, 0x04, 0x13, 0x1e, 0x3d, 0x30, 0x27, 0x2a,
0xb1, 0xbc, 0xab, 0xa6, 0x85, 0x88, 0x9f, 0x92, 0xd9, 0xd4, 0xc3, 0xce, 0xed, 0xe0, 0xf7, 0xfa,
0xb7, 0xba, 0xad, 0xa0, 0x83, 0x8e, 0x99, 0x94, 0xdf, 0xd2, 0xc5, 0xc8, 0xeb, 0xe6, 0xf1, 0xfc,
0x67, 0x6a, 0x7d, 0x70, 0x53, 0x5e, 0x49, 0x44, 0x0f, 0x02, 0x15, 0x18, 0x3b, 0x36, 0x21, 0x2c,
0x0c, 0x01, 0x16, 0x1b, 0x38, 0x35, 0x22, 0x2f, 0x64, 0x69, 0x7e, 0x73, 0x50, 0x5d, 0x4a, 0x47,
0xdc, 0xd1, 0xc6, 0xcb, 0xe8, 0xe5, 0xf2, 0xff, 0xb4, 0xb9, 0xae, 0xa3, 0x80, 0x8d, 0x9a, 0x97};
static const byte lookup_g14[] = {
0x00, 0x0e, 0x1c, 0x12, 0x38, 0x36, 0x24, 0x2a, 0x70, 0x7e, 0x6c, 0x62, 0x48, 0x46, 0x54, 0x5a,
0xe0, 0xee, 0xfc, 0xf2, 0xd8, 0xd6, 0xc4, 0xca, 0x90, 0x9e, 0x8c, 0x82, 0xa8, 0xa6, 0xb4, 0xba,
0xdb, 0xd5, 0xc7, 0xc9, 0xe3, 0xed, 0xff, 0xf1, 0xab, 0xa5, 0xb7, 0xb9, 0x93, 0x9d, 0x8f, 0x81,
0x3b, 0x35, 0x27, 0x29, 0x03, 0x0d, 0x1f, 0x11, 0x4b, 0x45, 0x57, 0x59, 0x73, 0x7d, 0x6f, 0x61,
0xad, 0xa3, 0xb1, 0xbf, 0x95, 0x9b, 0x89, 0x87, 0xdd, 0xd3, 0xc1, 0xcf, 0xe5, 0xeb, 0xf9, 0xf7,
0x4d, 0x43, 0x51, 0x5f, 0x75, 0x7b, 0x69, 0x67, 0x3d, 0x33, 0x21, 0x2f, 0x05, 0x0b, 0x19, 0x17,
0x76, 0x78, 0x6a, 0x64, 0x4e, 0x40, 0x52, 0x5c, 0x06, 0x08, 0x1a, 0x14, 0x3e, 0x30, 0x22, 0x2c,
0x96, 0x98, 0x8a, 0x84, 0xae, 0xa0, 0xb2, 0xbc, 0xe6, 0xe8, 0xfa, 0xf4, 0xde, 0xd0, 0xc2, 0xcc,
0x41, 0x4f, 0x5d, 0x53, 0x79, 0x77, 0x65, 0x6b, 0x31, 0x3f, 0x2d, 0x23, 0x09, 0x07, 0x15, 0x1b,
0xa1, 0xaf, 0xbd, 0xb3, 0x99, 0x97, 0x85, 0x8b, 0xd1, 0xdf, 0xcd, 0xc3, 0xe9, 0xe7, 0xf5, 0xfb,
0x9a, 0x94, 0x86, 0x88, 0xa2, 0xac, 0xbe, 0xb0, 0xea, 0xe4, 0xf6, 0xf8, 0xd2, 0xdc, 0xce, 0xc0,
0x7a, 0x74, 0x66, 0x68, 0x42, 0x4c, 0x5e, 0x50, 0x0a, 0x04, 0x16, 0x18, 0x32, 0x3c, 0x2e, 0x20,
0xec, 0xe2, 0xf0, 0xfe, 0xd4, 0xda, 0xc8, 0xc6, 0x9c, 0x92, 0x80, 0x8e, 0xa4, 0xaa, 0xb8, 0xb6,
0x0c, 0x02, 0x10, 0x1e, 0x34, 0x3a, 0x28, 0x26, 0x7c, 0x72, 0x60, 0x6e, 0x44, 0x4a, 0x58, 0x56,
0x37, 0x39, 0x2b, 0x25, 0x0f, 0x01, 0x13, 0x1d, 0x47, 0x49, 0x5b, 0x55, 0x7f, 0x71, 0x63, 0x6d,
0xd7, 0xd9, 0xcb, 0xc5, 0xef, 0xe1, 0xf3, 0xfd, 0xa7, 0xa9, 0xbb, 0xb5, 0x9f, 0x91, 0x83, 0x8d};
// Xor's all elements in a n byte array a by b
static void xor_s(byte * a, const byte *b, int n) {
int i;
for (i = 0; i < n; i++) {
a[i] ^= b[i];
}
}
// Xor the current cipher state by a specific round key
static void xor_round_key(byte *state, const byte *keys, int round) {
xor_s(state, keys + round * 16, 16);
}
// Apply the rijndael s-box to all elements in an array
// http://en.wikipedia.org/wiki/Rijndael_S-box
static void sub_bytes(byte *a, int n) {
int i;
for (i = 0; i < n; i++) {
a[i] = lookup_sbox[a[i]];
}
}
static void sub_bytes_inv(byte *a, int n) {
int i;
for (i = 0; i < n; i++) {
a[i] = lookup_sbox_inv[a[i]];
}
}
// Perform the core key schedule transform on 4 bytes, as part of the key expansion process
// http://en.wikipedia.org/wiki/Rijndael_key_schedule#Key_schedule_core
static void key_schedule_core(byte *a, int i) {
byte temp = a[0]; // Rotate the output eight bits to the left
a[0] = a[1];
a[1] = a[2];
a[2] = a[3];
a[3] = temp;
sub_bytes(a, 4); // Apply Rijndael's S-box on all four individual bytes in the output word
a[0] ^= lookup_rcon[i]; // On just the first (leftmost) byte of the output word, perform the rcon operation with i
// as the input, and exclusive or the rcon output with the first byte of the output word
}
// Expand the 16-byte key to 11 round keys (176 bytes)
// http://en.wikipedia.org/wiki/Rijndael_key_schedule#The_key_schedule
void oqs_aes128_load_schedule_c(const uint8_t *key, void **_schedule) {
*_schedule = malloc(16 * 11);
assert(*_schedule != NULL);
uint8_t *schedule = (uint8_t *) *_schedule;
int bytes = 16; // The count of how many bytes we've created so far
int i = 1; // The rcon iteration value i is set to 1
int j; // For repeating the second stage 3 times
byte t[4]; // Temporary working area known as 't' in the Wiki article
memcpy(schedule, key, 16); // The first 16 bytes of the expanded key are simply the encryption key
while (bytes < 176) { // Until we have 176 bytes of expanded key, we do the following:
memcpy(t, schedule + bytes - 4, 4); // We assign the value of the previous four bytes in the expanded key to t
key_schedule_core(t, i); // We perform the key schedule core on t, with i as the rcon iteration value
i++; // We increment i by 1
xor_s(t, schedule + bytes - 16, 4); // We exclusive-or t with the four-byte block 16 bytes before the new expanded key.
memcpy(schedule + bytes, t, 4); // This becomes the next 4 bytes in the expanded key
bytes += 4; // Keep track of how many expanded key bytes we've added
// We then do the following three times to create the next twelve bytes
for (j = 0; j < 3; j++) {
memcpy(t, schedule + bytes - 4, 4); // We assign the value of the previous 4 bytes in the expanded key to t
xor_s(t, schedule + bytes - 16, 4); // We exclusive-or t with the four-byte block n bytes before
memcpy(schedule + bytes, t, 4); // This becomes the next 4 bytes in the expanded key
bytes += 4; // Keep track of how many expanded key bytes we've added
}
}
}
void oqs_aes128_free_schedule_c(void *schedule) {
if (schedule != NULL) {
free(schedule);
}
}
// Apply the shift rows step on the 16 byte cipher state
// http://en.wikipedia.org/wiki/Advanced_Encryption_Standard#The_ShiftRows_step
static void shift_rows(byte *state) {
int i;
byte temp[16];
memcpy(temp, state, 16);
for (i = 0; i < 16; i++) {
state[i] = temp[shift_rows_table[i]];
}
}
static void shift_rows_inv(byte *state) {
int i;
byte temp[16];
memcpy(temp, state, 16);
for (i = 0; i < 16; i++) {
state[i] = temp[shift_rows_table_inv[i]];
}
}
// Perform the mix columns matrix on one column of 4 bytes
// http://en.wikipedia.org/wiki/Rijndael_mix_columns
static void mix_col(byte *state) {
byte a0 = state[0];
byte a1 = state[1];
byte a2 = state[2];
byte a3 = state[3];
state[0] = lookup_g2[a0] ^ lookup_g3[a1] ^ a2 ^ a3;
state[1] = lookup_g2[a1] ^ lookup_g3[a2] ^ a3 ^ a0;
state[2] = lookup_g2[a2] ^ lookup_g3[a3] ^ a0 ^ a1;
state[3] = lookup_g2[a3] ^ lookup_g3[a0] ^ a1 ^ a2;
}
// Perform the mix columns matrix on each column of the 16 bytes
static void mix_cols(byte *state) {
mix_col(state);
mix_col(state + 4);
mix_col(state + 8);
mix_col(state + 12);
}
// Perform the inverse mix columns matrix on one column of 4 bytes
// http://en.wikipedia.org/wiki/Rijndael_mix_columns
static void mix_col_inv(byte *state) {
byte a0 = state[0];
byte a1 = state[1];
byte a2 = state[2];
byte a3 = state[3];
state[0] = lookup_g14[a0] ^ lookup_g9[a3] ^ lookup_g13[a2] ^ lookup_g11[a1];
state[1] = lookup_g14[a1] ^ lookup_g9[a0] ^ lookup_g13[a3] ^ lookup_g11[a2];
state[2] = lookup_g14[a2] ^ lookup_g9[a1] ^ lookup_g13[a0] ^ lookup_g11[a3];
state[3] = lookup_g14[a3] ^ lookup_g9[a2] ^ lookup_g13[a1] ^ lookup_g11[a0];
}
// Perform the inverse mix columns matrix on each column of the 16 bytes
static void mix_cols_inv(byte *state) {
mix_col_inv(state);
mix_col_inv(state + 4);
mix_col_inv(state + 8);
mix_col_inv(state + 12);
}
void oqs_aes128_enc_c(const uint8_t *plaintext, const void *_schedule, uint8_t *ciphertext) {
const uint8_t *schedule = (const uint8_t *) _schedule;
int i; // To count the rounds
// First Round
memcpy(ciphertext, plaintext, 16);
xor_round_key(ciphertext, schedule, 0);
// Middle rounds
for (i = 0; i < 9; i++) {
sub_bytes(ciphertext, 16);
shift_rows(ciphertext);
mix_cols(ciphertext);
xor_round_key(ciphertext, schedule, i + 1);
}
// Final Round
sub_bytes(ciphertext, 16);
shift_rows(ciphertext);
xor_round_key(ciphertext, schedule, 10);
}
void oqs_aes128_dec_c(const uint8_t *ciphertext, const void *_schedule, uint8_t *plaintext) {
const uint8_t *schedule = (const uint8_t *) _schedule;
int i; // To count the rounds
// Reverse the final Round
memcpy(plaintext, ciphertext, 16);
xor_round_key(plaintext, schedule, 10);
shift_rows_inv(plaintext);
sub_bytes_inv(plaintext, 16);
// Reverse the middle rounds
for (i = 0; i < 9; i++) {
xor_round_key(plaintext, schedule, 9 - i);
mix_cols_inv(plaintext);
shift_rows_inv(plaintext);
sub_bytes_inv(plaintext, 16);
}
// Reverse the first Round
xor_round_key(plaintext, schedule, 0);
}
// It's not enc nor dec, it's something in between
void oqs_mhy128_enc_c(const uint8_t *plaintext, const void *_schedule, uint8_t *ciphertext) {
const uint8_t *schedule = (const uint8_t *) _schedule;
int i; // To count the rounds
// First Round
memcpy(ciphertext, plaintext, 16);
xor_round_key(ciphertext, schedule, 0);
// Middle rounds
for (i = 0; i < 9; i++) {
sub_bytes_inv(ciphertext, 16);
shift_rows_inv(ciphertext);
mix_cols_inv(ciphertext);
xor_round_key(ciphertext, schedule, i + 1);
}
// Final Round
sub_bytes_inv(ciphertext, 16);
shift_rows_inv(ciphertext);
xor_round_key(ciphertext, schedule, 10);
}
void oqs_mhy128_dec_c(const uint8_t *ciphertext, const void *_schedule, uint8_t *plaintext) {
const uint8_t *schedule = (const uint8_t *) _schedule;
int i; // To count the rounds
// Reverse the final Round
memcpy(plaintext, ciphertext, 16);
xor_round_key(plaintext, schedule, 10);
shift_rows(plaintext);
sub_bytes(plaintext, 16);
// Reverse the middle rounds
for (i = 0; i < 9; i++) {
xor_round_key(plaintext, schedule, 9 - i);
mix_cols(plaintext);
shift_rows(plaintext);
sub_bytes(plaintext, 16);
}
// Reverse the first Round
xor_round_key(plaintext, schedule, 0);
}

View File

@@ -1,66 +0,0 @@
/**
* \file aes.h
* \brief Header defining the API for OQS AES
*/
#ifndef __OQS_AES_H
#define __OQS_AES_H
#include <stdint.h>
#include <stdlib.h>
/**
* Function to fill a key schedule given an initial key.
*
* @param key Initial Key.
* @param schedule Abstract data structure for a key schedule.
* @param forEncryption 1 if key schedule is for encryption, 0 if for decryption.
*/
void OQS_AES128_load_schedule(const uint8_t *key, void **schedule, int for_encryption);
/**
* Function to free a key schedule.
*
* @param schedule Schedule generated with OQS_AES128_load_schedule().
*/
void OQS_AES128_free_schedule(void *schedule);
/**
* Function to encrypt blocks of plaintext using ECB mode.
* A schedule based on the key is generated and used internally.
*
* @param plaintext Plaintext to be encrypted.
* @param plaintext_len Length on the plaintext in bytes. Must be a multiple of 16.
* @param key Key to be used for encryption.
* @param ciphertext Pointer to a block of memory which >= in size to the plaintext block. The result will be written here.
*/
void OQS_AES128_ECB_enc(const uint8_t *plaintext, const size_t plaintext_len, const uint8_t *key, uint8_t *ciphertext);
/**
* Function to decrypt blocks of plaintext using ECB mode.
* A schedule based on the key is generated and used internally.
*
* @param ciphertext Ciphertext to be decrypted.
* @param ciphertext_len Length on the ciphertext in bytes. Must be a multiple of 16.
* @param key Key to be used for encryption.
* @param ciphertext Pointer to a block of memory which >= in size to the ciphertext block. The result will be written here.
*/
void OQS_AES128_ECB_dec(const uint8_t *ciphertext, const size_t ciphertext_len, const uint8_t *key, uint8_t *plaintext);
/**
* Same as OQS_AES128_ECB_enc() except a schedule generated by
* OQS_AES128_load_schedule() is passed rather then a key. This is faster
* if the same schedule is used for multiple encryptions since it does
* not have to be regenerated from the key.
*/
void OQS_AES128_ECB_enc_sch(const uint8_t *plaintext, const size_t plaintext_len, const void *schedule, uint8_t *ciphertext);
/**
* Same as OQS_AES128_ECB_dec() except a schedule generated by
* OQS_AES128_load_schedule() is passed rather then a key. This is faster
* if the same schedule is used for multiple encryptions since it does
* not have to be regenerated from the key.
*/
void OQS_AES128_ECB_dec_sch(const uint8_t *ciphertext, const size_t ciphertext_len, const void *schedule, uint8_t *plaintext);
#endif

View File

@@ -1,31 +0,0 @@
#include "memecrypto.h"
#include <cstring>
#include <stdio.h>
extern "C" void oqs_mhy128_enc_c(const uint8_t *plaintext, const void *_schedule, uint8_t *ciphertext);
extern "C" void oqs_mhy128_dec_c(const uint8_t *ciphertext, const void *_schedule, uint8_t *plaintext);
static uint8_t dexor16(const uint8_t *c) {
uint8_t ret = 0;
for (int i = 0; i < 16; i++)
ret ^= c[i];
return ret;
}
void memecrypto_prepare_key(const uint8_t *in, uint8_t *out) {
for (int i = 0; i < 0xB0; i++)
out[i] = dexor16(&in[0x10 * i]);
}
void memecrypto_decrypt(const uint8_t *key, uint8_t *data) {
uint8_t plaintext[16];
oqs_mhy128_enc_c(data, key, plaintext);
memcpy(data, plaintext, 16);
}
void memecrypto_encrypt(const uint8_t *key, uint8_t *data) {
uint8_t ciphertext[16];
oqs_mhy128_dec_c(data, key, ciphertext);
memcpy(data, ciphertext, 16);
}

View File

@@ -1,12 +0,0 @@
#ifndef MEMECRYPTO_H
#define MEMECRYPTO_H
#include <cstdint>
void memecrypto_prepare_key(const uint8_t *in, uint8_t *out);
void memecrypto_decrypt(const uint8_t *key, uint8_t *data);
void memecrypto_encrypt(const uint8_t *key, uint8_t *data);
#endif //MEMECRYPTO_H

View File

@@ -1,146 +0,0 @@
#include "metadata.h"
#include <cstring>
#include <random>
#include <stdio.h>
#include "memecrypto.h"
#include "metadatastringdec.h"
unsigned char initial_prev_xor[] = { 0xad, 0x2f, 0x42, 0x30, 0x67, 0x04, 0xb0, 0x9c, 0x9d, 0x2a, 0xc0, 0xba, 0x0e, 0xbf, 0xa5, 0x68 };
bool get_global_metadata_keys(uint8_t *src, size_t srcn, uint8_t *longkey, uint8_t *shortkey) {
if (srcn != 0x4000)
return false;
if (*(uint16_t *) (src + 0xc8) != 0xfc2e || *(uint16_t *) (src + 0xca) != 0x2cfe)
return true;
auto offB00 = *(uint16_t *) (src + 0xd2);
for (size_t i = 0; i < 16; i++)
shortkey[i] = src[offB00 + i] ^ src[0x3000 + i];
for (size_t i = 0; i < 0xb00; i++)
longkey[i] = src[offB00 + 0x10 + i] ^ src[0x3000 + 0x10 + i] ^ shortkey[i % 16];
return true;
}
bool gen_global_metadata_key(uint8_t* src, size_t srcn) {
if (srcn != 0x4000)
return false;
#if 0
std::vector<uint8_t> read_file(const char* n);
auto data = read_file("xorpad.bin");
memcpy(src, data.data(), 0x4000);
return false;
#endif
std::mt19937_64 rand (0xDEADBEEF);
uint64_t* key = (uint64_t*)src;
for (size_t i = 0; i < srcn / sizeof(uint64_t); i++)
key[i] = rand();
*(uint16_t *) (src + 0xc8) = 0xfc2e; // Magic
*(uint16_t *) (src + 0xca) = 0x2cfe; // Magic
*(uint16_t *) (src + 0xd2) = rand() & 0x1FFFu; // Just some random value
return true;
}
void decrypt_global_metadata_inner(uint8_t *data, size_t size) {
uint8_t longkey[0xB00];
uint8_t longkeyp[0xB0];
uint8_t shortkey[16];
get_global_metadata_keys(data + size - 0x4000, 0x4000, longkey, shortkey);
for (int i = 0; i < 16; i++)
shortkey[i] ^= initial_prev_xor[i];
memecrypto_prepare_key(longkey, longkeyp);
auto perentry = (uint32_t) (size / 0x100 / 0x40);
for (int i = 0; i < 0x100; i++) {
auto off = (0x40u * perentry) * i;
uint8_t prev[16];
memcpy(prev, shortkey, 16);
for (int j = 0; j < 4; j++) {
uint8_t curr[16];
memcpy(curr, &data[off + j * 0x10], 16);
memecrypto_decrypt(longkeyp, curr);
for (int k = 0; k < 16; k++)
curr[k] ^= prev[k];
memcpy(prev, &data[off + j * 0x10], 16);
memcpy(&data[off + j * 0x10], curr, 16);
}
}
uint8_t literal_dec_key[0x5000];
recrypt_global_metadata_header_string_fields(data, size, literal_dec_key);
recrypt_global_metadata_header_string_literals(data, size, literal_dec_key);
}
extern "C" int decrypt_global_metadata(uint8_t *data, size_t size) {
try {
decrypt_global_metadata_inner(data, size);
return 0;
} catch (...) {
return -1;
}
}
void encrypt_global_metadata_inner(uint8_t* data, size_t size) {
uint8_t literal_dec_key[0x5000];
gen_global_metadata_key(data + size - 0x4000, 0x4000);
generate_key_for_global_metadata_header_string(data, size, literal_dec_key);
recrypt_global_metadata_header_string_literals(data, size, literal_dec_key);
recrypt_global_metadata_header_string_fields(data, size, literal_dec_key);
uint8_t longkey[0xB00];
uint8_t longkeyp[0xB0];
uint8_t shortkey[16];
get_global_metadata_keys(data + size - 0x4000, 0x4000, longkey, shortkey);
for (int i = 0; i < 16; i++)
shortkey[i] ^= initial_prev_xor[i];
memecrypto_prepare_key(longkey, longkeyp);
auto perentry = (uint32_t) (size / 0x100 / 0x40);
for (int i = 0; i < 0x100; i++) {
auto off = (0x40u * perentry) * i;
uint8_t prev[16];
memcpy(prev, shortkey, 16);
for (int j = 0; j < 4; j++) {
uint8_t curr[16];
memcpy(curr, &data[off + j * 0x10], 16);
for (int k = 0; k < 16; k++)
curr[k] ^= prev[k];
memecrypto_encrypt(longkeyp, curr);
memcpy(prev, curr, 16);
memcpy(&data[off + j * 0x10], curr, 16);
}
}
}
extern "C" int encrypt_global_metadata(uint8_t* data, size_t size) {
try {
encrypt_global_metadata_inner(data, size);
return 0;
} catch (...) {
return -1;
}
}

View File

@@ -1,10 +0,0 @@
#ifndef METADATA_H
#define METADATA_H
#include <cstdint>
#include <cstdlib>
extern "C" int decrypt_global_metadata(uint8_t *data, size_t size);
extern "C" int encrypt_global_metadata(uint8_t *data, size_t size);
#endif //METADATA_H

View File

@@ -1,121 +0,0 @@
#include "metadatastringdec.h"
#include <stdexcept>
#include <random>
#include <stdio.h>
struct m_header_fields {
char filler1[0x18];
uint32_t stringLiteralDataOffset; // 18
uint32_t stringLiteralDataCount; // 1c
uint32_t stringLiteralOffset; // 20
uint32_t stringLiteralCount; // 24
char filler2[0xd8 - 0x28];
uint32_t stringOffset, stringCount;
};
struct m_literal {
uint32_t offset, length;
};
void generate_key_for_global_metadata_header_string(uint8_t* data, size_t len, uint8_t* literal_dec_key) {
if (len < sizeof(m_header_fields))
throw std::out_of_range("data not big enough for global metadata header");
uint32_t values[0x12] = {
*(uint32_t *) (data + 0x60),
*(uint32_t *) (data + 0x64),
*(uint32_t *) (data + 0x68),
*(uint32_t *) (data + 0x6c),
*(uint32_t *) (data + 0x140),
*(uint32_t *) (data + 0x144),
*(uint32_t *) (data + 0x148),
*(uint32_t *) (data + 0x14c),
*(uint32_t *) (data + 0x100),
*(uint32_t *) (data + 0x104),
*(uint32_t *) (data + 0x108),
*(uint32_t *) (data + 0x10c),
*(uint32_t *) (data + 0xf0),
*(uint32_t *) (data + 0xf4),
*(uint32_t *) (data + 8),
*(uint32_t *) (data + 0xc),
*(uint32_t *) (data + 0x10),
*(uint32_t *) (data + 0x14)
};
uint64_t seed = ((uint64_t) values[values[0] & 0xfu] << 0x20u) | values[(values[0x11] & 0xf) + 2];
std::mt19937_64 rand (seed);
for (int i = 0; i < 6; i++) // Skip
rand();
auto key64 = (uint64_t *) literal_dec_key;
for (int i = 0; i < 0xa00; i++)
key64[i] = rand();
}
void recrypt_global_metadata_header_string_fields(uint8_t *data, size_t len, uint8_t *literal_dec_key) {
if (len < sizeof(m_header_fields))
throw std::out_of_range("data not big enough for global metadata header");
uint32_t values[0x12] = {
*(uint32_t *) (data + 0x60),
*(uint32_t *) (data + 0x64),
*(uint32_t *) (data + 0x68),
*(uint32_t *) (data + 0x6c),
*(uint32_t *) (data + 0x140),
*(uint32_t *) (data + 0x144),
*(uint32_t *) (data + 0x148),
*(uint32_t *) (data + 0x14c),
*(uint32_t *) (data + 0x100),
*(uint32_t *) (data + 0x104),
*(uint32_t *) (data + 0x108),
*(uint32_t *) (data + 0x10c),
*(uint32_t *) (data + 0xf0),
*(uint32_t *) (data + 0xf4),
*(uint32_t *) (data + 8),
*(uint32_t *) (data + 0xc),
*(uint32_t *) (data + 0x10),
*(uint32_t *) (data + 0x14)
};
uint64_t seed = ((uint64_t) values[values[0] & 0xfu] << 0x20u) | values[(values[0x11] & 0xf) + 2];
std::mt19937_64 rand (seed);
auto header = (m_header_fields *) data;
header->stringCount ^= (uint32_t) rand();
header->stringOffset ^= (uint32_t) rand();
rand();
header->stringLiteralOffset ^= (uint32_t) rand();
header->stringLiteralDataCount ^= (uint32_t) rand();
header->stringLiteralDataOffset ^= (uint32_t) rand();
auto key64 = (uint64_t *) literal_dec_key;
for (int i = 0; i < 0xa00; i++)
key64[i] = rand();
}
void recrypt_global_metadata_header_string_literals(uint8_t *data, size_t len, uint8_t *literal_dec_key) {
if (len < sizeof(m_header_fields))
throw std::out_of_range("data not big enough for global metadata header");
auto header = (m_header_fields *) data;
if ((size_t) header->stringLiteralCount + header->stringLiteralOffset > len)
throw std::out_of_range("file trimmed or string literal offset/count field invalid");
auto literals = (m_literal *) (data + header->stringLiteralOffset);
auto count = header->stringLiteralCount / sizeof(m_literal);
for (size_t i = 0; i < count; i++) {
auto slen = literals[i].length;
uint8_t *str = data + header->stringLiteralDataOffset + literals[i].offset;
uint8_t *okey = literal_dec_key + (i % 0x2800);
if ((size_t) header->stringLiteralDataOffset + literals[i].offset + slen > len)
throw std::out_of_range("file trimmed or contains invalid string entry");
for (size_t j = 0; j < slen; j++)
str[j] ^= literal_dec_key[(j + 0x1400u) % 0x5000u] ^ (okey[j % 0x2800u] + (uint8_t) j);
}
}

View File

@@ -1,13 +0,0 @@
#ifndef METADATASTRINGDEC_H
#define METADATASTRINGDEC_H
#include <cstdint>
#include <cstdlib>
void recrypt_global_metadata_header_string_fields(uint8_t *data, size_t len, uint8_t *literal_dec_key);
void recrypt_global_metadata_header_string_literals(uint8_t *data, size_t len, uint8_t *literal_dec_key);
void generate_key_for_global_metadata_header_string(uint8_t* data, size_t len, uint8_t* literal_dec_key);
#endif //METADATASTRINGDEC_H

View File

@@ -1,22 +0,0 @@
#[cfg(windows)]
pub fn reopen_as_admin() {
use std::process::{exit, Command};
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 = "macos")]
pub fn reopen_as_admin() {}

View File

@@ -1,49 +0,0 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::string::String;
// Config may not exist, or may be old, so it's okay if these are optional
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Configuration {
pub toggle_grasscutter: Option<bool>,
pub game_install_path: Option<String>,
pub grasscutter_with_game: Option<bool>,
pub grasscutter_path: Option<String>,
pub java_path: Option<String>,
pub close_action: Option<u64>,
pub startup_launch: Option<bool>,
pub last_ip: Option<String>,
pub last_port: Option<String>,
pub language: Option<String>,
pub custom_background: Option<String>,
pub use_theme_background: Option<bool>,
pub cert_generated: Option<bool>,
pub theme: Option<String>,
pub https_enabled: Option<bool>,
pub debug_enabled: Option<bool>,
pub patch_rsa: Option<bool>,
pub use_internal_proxy: Option<bool>,
pub wipe_login: Option<bool>,
pub horny_mode: Option<bool>,
pub auto_mongodb: Option<bool>,
pub un_elevated: Option<bool>,
pub redirect_more: Option<bool>,
pub launch_args: Option<String>,
pub offline_mode: Option<bool>,
}
pub fn config_path() -> PathBuf {
let mut path = tauri::api::path::data_dir().unwrap();
path.push("cultivation");
path.push("configuration.json");
path
}
pub fn get_config() -> Configuration {
let path = config_path();
let config = std::fs::read_to_string(path).unwrap_or("{}".to_string());
let config: Configuration = serde_json::from_str(&config).unwrap_or_default();
config
}

View File

@@ -1,117 +0,0 @@
use once_cell::sync::Lazy;
use std::cmp::min;
use std::fs::File;
use std::io::Write;
use std::sync::Mutex;
use futures_util::StreamExt;
// This will create a downloads list that will be used to check if we should continue downloading the file
static DOWNLOADS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
// Lots of help from: https://gist.github.com/giuliano-oliveira/4d11d6b3bb003dba3a1b53f43d81b30d
// and docs ofc
#[tauri::command]
pub async fn download_file(window: tauri::Window, url: &str, path: &str) -> Result<(), String> {
// Reqwest setup
let res = match reqwest::get(url).await {
Ok(r) => r,
Err(_e) => {
emit_download_err(window, format!("Failed to request {}", url), path);
return Err(format!("Failed to request {}", url));
}
};
let total_size = res.content_length().unwrap_or(0);
// Create file path
let mut file = match File::create(path) {
Ok(f) => f,
Err(_e) => {
emit_download_err(window, format!("Failed to create file '{}'", path), path);
return Err(format!("Failed to create file '{}'", path));
}
};
let mut downloaded: u64 = 0;
let mut total_downloaded: u64 = 0;
// File stream
let mut stream = res.bytes_stream();
// Assuming all goes well, add to the downloads list
DOWNLOADS.lock().unwrap().push(path.to_string());
// Await chunks
while let Some(item) = stream.next().await {
// Stop the loop if the download is removed from the list
if !DOWNLOADS.lock().unwrap().contains(&path.to_string()) {
break;
}
let chunk = match item {
Ok(itm) => itm,
Err(e) => {
emit_download_err(window, "Error while downloading file".to_string(), path);
return Err(format!("Error while downloading file: {}", e));
}
};
let vect = &chunk.to_vec()[..];
// Write bytes
match file.write_all(vect) {
Ok(x) => x,
Err(e) => {
emit_download_err(window, "Error while writing file".to_string(), path);
return Err(format!("Error while writing file: {}", e));
}
}
// New progress
let new = min(downloaded + (chunk.len() as u64), total_size);
downloaded = new;
total_downloaded += chunk.len() as u64;
let mut res_hash = std::collections::HashMap::new();
res_hash.insert("downloaded".to_string(), downloaded.to_string());
res_hash.insert("total".to_string(), total_size.to_string());
res_hash.insert("path".to_string(), path.to_string());
res_hash.insert("total_downloaded".to_string(), total_downloaded.to_string());
// Create event to send to frontend
window.emit("download_progress", &res_hash).unwrap();
}
// One more "finish" event
window.emit("download_finished", &path).unwrap();
// We are done
Ok(())
}
pub fn emit_download_err(window: tauri::Window, msg: String, path: &str) {
let mut res_hash = std::collections::HashMap::new();
res_hash.insert("error".to_string(), msg);
res_hash.insert("path".to_string(), path.to_string());
window.emit("download_error", &res_hash).unwrap();
}
#[tauri::command]
pub fn stop_download(path: String) {
// Check if the path is in the downloads list
let mut downloads = DOWNLOADS.lock().unwrap();
let index = downloads.iter().position(|x| x == &path);
// Remove from list
if let Some(i) = index {
downloads.remove(i);
}
// Delete the file from disk
if let Err(_e) = std::fs::remove_file(&path) {
// Do nothing
}
}

View File

@@ -1,169 +0,0 @@
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) {
let mut new_path = path.clone();
// Check if file/folder to replace exists
if fs::metadata(&path).is_err() {
return;
}
// Check if path uses forward or back slashes
if new_path.contains('\\') {
new_path = path.replace('\\', "/");
}
let path_replaced = &path.replace(new_path.split('/').last().unwrap(), &new_name);
match fs::rename(&path, path_replaced) {
Ok(_) => {
println!("Renamed {} to {}", &path, path_replaced);
}
Err(e) => {
println!("Error: {}", e);
}
};
}
#[tauri::command]
pub fn dir_create(path: String) {
fs::create_dir_all(path).unwrap();
}
#[tauri::command]
pub fn dir_exists(path: &str) -> bool {
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 = PathBuf::from(path);
fs::read_dir(path_buf).unwrap().count() == 0
}
#[tauri::command]
pub fn dir_delete(path: &str) {
let path_buf = PathBuf::from(path);
fs::remove_dir_all(path_buf).unwrap();
}
#[tauri::command]
pub fn are_files_identical(path1: &str, path2: &str) -> bool {
diff(path1, path2)
}
#[tauri::command]
pub fn copy_file(path: String, new_path: String) -> bool {
let filename = &path.split('/').last().unwrap();
let path_buf = PathBuf::from(&path);
// If the new path doesn't exist, create it.
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)) {
Ok(_) => true,
Err(e) => {
println!("Failed to copy file: {}", e);
println!("Path: {}", path);
println!("New Path: {}", new_path);
false
}
}
}
#[tauri::command]
pub fn copy_file_with_new_name(path: String, new_path: String, new_name: String) -> bool {
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(PathBuf::from(&new_path).pop().to_string().as_str()) {
match std::fs::create_dir_all(&new_path) {
Ok(_) => {}
Err(e) => {
println!("Failed to create directory: {}", e);
return false;
}
};
}
new_path_buf.push(new_name);
// Copy old to new
match std::fs::copy(path_buf, &new_path_buf) {
Ok(_) => true,
Err(e) => {
println!("Failed to copy file: {}", e);
println!("Path: {}", path);
println!("New Path: {}", new_path);
false
}
}
}
#[tauri::command]
pub fn delete_file(path: String) -> bool {
let path_buf = PathBuf::from(&path);
match std::fs::remove_file(path_buf) {
Ok(_) => true,
Err(e) => {
println!("Failed to delete file: {}", e);
false
}
};
false
}
#[tauri::command]
pub fn read_file(path: String) -> String {
let path_buf = PathBuf::from(&path);
let mut file = match fs::File::open(path_buf) {
Ok(file) => file,
Err(e) => {
println!("Failed to open file {}: {}", &path, e);
if path.contains("config") {
// Server.ts won't print the error so handle the message here for the user
println!("Server config not found or invalid. Be sure to run the server at least once to generate it before making edits.");
}
return String::new(); // Send back error for handling by the caller
}
};
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
contents
}
#[tauri::command]
pub fn write_file(path: String, contents: String) {
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) {
Ok(file) => file,
Err(e) => {
println!("Failed to open file: {}", e);
return;
}
};
// Write contents to file
match file.write_all(contents.as_bytes()) {
Ok(_) => (),
Err(e) => {
println!("Failed to write to file: {}", e);
}
}
}

View File

@@ -1,95 +0,0 @@
use crate::file_helpers;
use crate::web;
use std::collections::HashMap;
use std::fs::read_dir;
use std::io::Read;
use std::path::PathBuf;
static SITE_URL: &str = "https://gamebanana.com";
#[tauri::command]
pub async fn get_download_links(mod_id: String) -> String {
web::query(format!("{}/apiv9/Mod/{}/DownloadPage", SITE_URL, mod_id).as_str()).await
}
#[tauri::command]
pub async fn list_submissions(mode: String, page: String, search: String) -> String {
if search.is_empty() {
web::query(
format!(
"{}/apiv9/Util/Game/Submissions?_idGameRow=8552&_nPage={}&_nPerpage=50&_sMode={}",
SITE_URL, page, mode
)
.as_str(),
)
.await
} else {
web::query(
format!(
"{}/apiv11/Util/Search/Results?_nPage={}&_sOrder=best_match&_idGameRow=8552&_sSearchString={}&_csvFields=name,description,article,attribs,studio,owner,credits",
SITE_URL, page, search
)
.as_str()
)
.await
}
}
#[tauri::command]
pub async fn list_mods(path: String) -> HashMap<String, String> {
let mut path_buf = PathBuf::from(path);
// If the path includes a file, remove it
if path_buf.file_name().is_some() {
path_buf.pop();
}
// Ensure we are in the Mods folder
path_buf.push("Mods");
// Check if dir is empty
if file_helpers::dir_is_empty(path_buf.to_str().unwrap()) {
return HashMap::new();
}
let mut mod_info_files = vec![];
let mut mod_info_strings = HashMap::new();
for entry in read_dir(path_buf).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
// Check each dir for a modinfo.json file
if path.is_dir() {
let mut mod_info_path = path.clone();
mod_info_path.push("modinfo.json");
if mod_info_path.exists() {
// Push path AND file contents into the hashmap using path as key
mod_info_files.push(mod_info_path.to_str().unwrap().to_string());
} else {
// No modinfo, but we can still push a JSON obj with the folder name
mod_info_strings.insert(
path.to_str().unwrap().to_string(),
format!(
"{{ \"name\": \"{}\" }}",
path.file_name().unwrap().to_str().unwrap()
),
);
}
}
}
// Read each modinfo.json file
for mod_info_file in mod_info_files {
let mut mod_info_string = String::new();
// It is safe to unwrap here since we *know* that the file exists
let mut file = std::fs::File::open(&mod_info_file).unwrap();
file.read_to_string(&mut mod_info_string).unwrap();
// Push into hashmap using path as key
mod_info_strings.insert(mod_info_file, mod_info_string);
}
mod_info_strings
}

View File

@@ -1,58 +0,0 @@
use crate::system_helpers::*;
use std::path::{Path, PathBuf};
#[tauri::command]
pub async fn get_lang(window: tauri::Window, lang: String) -> String {
let lang = lang.to_lowercase();
// Send contents of language file back
let lang_path: PathBuf = [&install_location(), "lang", &format!("{}.json", lang)]
.iter()
.collect();
match std::fs::read_to_string(lang_path) {
Ok(x) => x,
Err(e) => {
emit_lang_err(window, format!("Failed to read language file: {}", e));
"".to_string()
}
}
}
#[tauri::command]
pub async fn get_languages() -> std::collections::HashMap<String, String> {
// for each lang file, set the key as the filename and the value as the lang_name contained in the file
let mut languages = std::collections::HashMap::new();
let lang_files = std::fs::read_dir(Path::new(&install_location()).join("lang")).unwrap();
for entry in lang_files {
let entry = entry.unwrap();
let path = entry.path();
let filename = path
.file_name()
.unwrap_or_else(|| panic!("Failed to get filename from path: {:?}", path))
.to_str()
.unwrap_or_else(|| panic!("Failed to convert filename to string: {:?}", path))
.to_string();
let content = match std::fs::read_to_string(&path) {
Ok(x) => x,
Err(e) => {
println!("Failed to read language file: {}", e);
break;
}
};
languages.insert(filename.to_string(), content);
}
languages
}
pub fn emit_lang_err(window: tauri::Window, msg: String) {
let mut res_hash = std::collections::HashMap::new();
res_hash.insert("error".to_string(), msg);
window.emit("lang_error", &res_hash).unwrap();
}

View File

@@ -1,538 +0,0 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use args::{Args, ArgsError};
use file_helpers::dir_exists;
use once_cell::sync::Lazy;
use proxy::set_proxy_addr;
use std::fs;
use std::io::Write;
use std::{collections::HashMap, sync::Mutex};
use tauri::api::path::data_dir;
use tauri::async_runtime::block_on;
use std::thread;
use sysinfo::{Pid, ProcessExt, System, SystemExt};
#[cfg(target_os = "windows")]
use crate::admin::reopen_as_admin;
#[cfg(target_os = "windows")]
use system_helpers::is_elevated;
#[cfg(target_os = "linux")]
use std::{
thread::{sleep, JoinHandle},
time::{Duration, Instant},
};
mod admin;
mod config;
mod downloader;
mod file_helpers;
mod gamebanana;
mod lang;
mod patch;
mod proxy;
mod release;
mod system_helpers;
mod unzip;
mod web;
static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
static WATCH_GRASSCUTTER_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
static GC_PID: std::sync::Mutex<usize> = Mutex::new(696969);
#[cfg(target_os = "linux")]
pub static AAGL_THREAD: Lazy<Mutex<Option<JoinHandle<()>>>> = Lazy::new(|| Mutex::new(None));
fn try_flush() {
std::io::stdout().flush().unwrap_or(())
}
async fn parse_args(inp: &Vec<String>) -> Result<Args, ArgsError> {
let mut args = Args::new(
"Cultivation",
"Private server helper program for an Anime Game",
);
args.flag("h", "help", "Print various CLI args");
args.flag("p", "proxy", "Start the proxy server");
args.flag("G", "launch-game", "Launch the game");
args.flag("o", "other-redirects", "Redirect other certain anime games");
args.flag(
"A",
"no-admin",
"Launch without requiring admin permissions",
);
args.flag(
"g",
"no-gui",
"Run in CLI mode. Requires -A to be passed as well.",
);
args.flag("s", "server", "Launch the configured GC server");
args.flag(
"P",
"patch",
"Patch your game before launching, with whatever your game version needs",
);
args.flag(
"N",
"non-elevated-game",
"Launch the game without admin permissions",
);
args.option(
"H",
"host",
"Set host to connect to (eg. 'localhost:443' or 'my.awesomeserver.com:6969)",
"SERVER_HOST",
getopts::Occur::Optional,
None,
);
args.option(
"a",
"game-args",
"Arguments to pass to the game process, if launching it",
r#""-opt-one -opt-two""#,
getopts::Occur::Optional,
None,
);
args.parse(inp).unwrap();
let config = config::get_config();
if args.value_of("help")? {
println!("{}", args.full_usage());
std::process::exit(0);
}
if args.value_of("launch-game")? {
let game_path = config.game_install_path;
let game_args: String = args.value_of("game-args").unwrap_or_default();
// Patch if needed
if args.value_of("patch")? {
patch::patch_game().await;
}
if game_path.is_some() {
if args.value_of("non-elevated-game")? {
system_helpers::run_un_elevated(game_path.unwrap(), Some(game_args))
} else {
system_helpers::run_program(game_path.unwrap(), Some(game_args))
}
}
}
if args.value_of("server")? && config.grasscutter_path.is_some() && config.java_path.is_some() {
let server_jar = config.grasscutter_path.unwrap();
let mut server_path = server_jar.clone();
// Strip jar name from path
if server_path.contains('/') {
// Can never panic because of if
let len = server_jar.rfind('/').unwrap();
server_path.truncate(len);
} else if server_path.contains('\\') {
let len = server_jar.rfind('\\').unwrap();
server_path.truncate(len);
}
let java_path = config.java_path.unwrap();
system_helpers::run_jar(server_jar, server_path.to_string(), java_path);
}
if args.value_of::<String>("host").is_ok() && !args.value_of::<String>("host")?.is_empty() {
let host = args.value_of::<String>("host")?;
set_proxy_addr(host);
}
if args.value_of("proxy")? {
println!("Starting proxy server...");
let mut pathbuf = tauri::api::path::data_dir().unwrap();
pathbuf.push("cultivation");
pathbuf.push("ca");
if args.value_of("other_redirects")? {
proxy::set_redirect_more();
}
connect(8035, pathbuf.to_str().unwrap().to_string()).await;
}
Ok(args)
}
fn main() -> Result<(), ArgsError> {
let args: Vec<String> = std::env::args().collect();
let parsed_args = block_on(parse_args(&args)).unwrap();
#[cfg(target_os = "windows")]
if !is_elevated() && !parsed_args.value_of("no-admin")? {
println!("===============================================================================");
println!("You running as a non-elevated user. Some stuff will almost definitely not work.");
println!("===============================================================================");
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();
// For disabled GUI
ctrlc::set_handler(|| {
disconnect();
block_on(patch::unpatch_game());
std::process::exit(0);
})
.unwrap_or(());
if !parsed_args.value_of("no-gui")? {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
enable_process_watcher,
enable_grasscutter_watcher,
connect,
disconnect,
req_get,
is_game_running,
is_grasscutter_running,
restart_grasscutter,
get_theme_list,
system_helpers::run_command,
system_helpers::run_program,
system_helpers::run_program_relative,
system_helpers::start_service,
system_helpers::service_status,
system_helpers::stop_service,
system_helpers::run_jar,
system_helpers::run_jar_root,
system_helpers::open_in_browser,
system_helpers::install_location,
system_helpers::is_elevated,
system_helpers::set_migoto_target,
system_helpers::set_migoto_delay,
system_helpers::wipe_registry,
system_helpers::get_platform,
system_helpers::run_un_elevated,
system_helpers::jvm_add_cap,
system_helpers::jvm_remove_cap,
patch::patch_game,
patch::unpatch_game,
proxy::set_proxy_addr,
proxy::generate_ca_files,
proxy::set_redirect_more,
release::get_latest_release,
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
])
.on_window_event(|event| {
if let tauri::WindowEvent::CloseRequested { .. } = event.event() {
// Ensure all proxy stuff is handled
disconnect();
}
})
.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();
// Always unpatch game upon closing the program
block_on(patch::unpatch_game());
Ok(())
}
#[tauri::command]
fn is_game_running() -> bool {
// Grab the game process name
let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string();
!proc.is_empty()
}
#[cfg(target_os = "windows")]
#[tauri::command]
fn enable_process_watcher(window: tauri::Window, process: String) {
*WATCH_GAME_PROCESS.lock().unwrap() = process;
window.listen("disable_process_watcher", |_e| {
*WATCH_GAME_PROCESS.lock().unwrap() = "".to_string();
});
println!("Starting process watcher...");
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();
loop {
// Shorten loop timer to avoid user closing Cultivation before unpatching/proxy disconnecting
thread::sleep(std::time::Duration::from_secs(2));
// Refresh system info
system.refresh_all();
// Grab the game process name
let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string();
if !proc.is_empty() {
let mut proc_with_name = system.processes_by_exact_name(&proc);
let exists = proc_with_name.next().is_some();
// If the game process closes, disable the proxy.
if !exists {
println!("Game closed");
*WATCH_GAME_PROCESS.lock().unwrap() = "".to_string();
disconnect();
window.emit("game_closed", &()).unwrap();
break;
}
}
}
});
}
// The library takes care of it
#[cfg(target_os = "linux")]
#[tauri::command]
fn enable_process_watcher(window: tauri::Window, process: String) {
drop(process);
thread::spawn(move || {
let end_time = Instant::now() + Duration::from_secs(60);
let game_thread = loop {
let mut lock = AAGL_THREAD.lock().unwrap();
if lock.is_some() {
break lock.take().unwrap();
}
drop(lock);
if end_time < Instant::now() {
// If more than 60 seconds pass something has gone wrong
println!("Waiting for game thread timed out");
return;
}
// Otherwhise wait in order to not use too many CPU cycles
sleep(Duration::from_millis(128));
};
game_thread.join().unwrap();
println!("Game closed");
*WATCH_GAME_PROCESS.lock().unwrap() = "".to_string();
disconnect();
window.emit("game_closed", &()).unwrap();
});
}
#[cfg(target_os = "macos")]
#[tauri::command]
fn enable_process_watcher(window: tauri::Window, process: String) {}
#[tauri::command]
fn is_grasscutter_running() -> bool {
// Grab the grasscutter process name
let proc = WATCH_GRASSCUTTER_PROCESS.lock().unwrap().to_string();
!proc.is_empty()
}
#[cfg(windows)]
#[tauri::command]
fn restart_grasscutter(window: tauri::Window) -> bool {
let pid: usize = *GC_PID.lock().unwrap();
let system = System::new_all();
// Get the process
if let Some(process) = system.process(Pid::from(pid)) {
// Kill it
if process.kill() {
// Also kill the cmd it was open in
if let Some(parent) = system.process(process.parent().unwrap()) {
parent.kill();
}
for process_gc in system.processes_by_name("java") {
if process_gc.cmd().last().unwrap().contains("grasscutter") {
process_gc.kill();
}
}
window.emit("disable_grasscutter_watcher", &()).unwrap();
thread::sleep(std::time::Duration::from_secs(2));
// Start again
window.emit("start_grasscutter", &()).unwrap();
true
} else {
false
}
} else {
false
}
}
#[cfg(unix)]
#[tauri::command]
fn restart_grasscutter(_window: tauri::Window) {
// Placeholder text for imports
let s = System::new();
if let Some(process) = s.process(Pid::from(1337)) {
println!("{}", process.name());
}
}
#[tauri::command]
fn enable_grasscutter_watcher(window: tauri::Window, process: String) {
let grasscutter_name = process.clone();
let mut gc_pid = Pid::from(696969);
*WATCH_GRASSCUTTER_PROCESS.lock().unwrap() = process;
window.listen("disable_grasscutter_watcher", |_e| {
*WATCH_GRASSCUTTER_PROCESS.lock().unwrap() = "".to_string();
});
println!("Starting grasscutter watcher...");
thread::spawn(move || {
// Initial sleep for 1 second while Grasscutter opens
std::thread::sleep(std::time::Duration::from_secs(3));
let mut system = System::new_all();
for process_gc in system.processes_by_name("java") {
if process_gc.cmd().last().unwrap().contains(&grasscutter_name) {
gc_pid = process_gc.pid();
*GC_PID.lock().unwrap() = gc_pid.into();
window
.emit("grasscutter_started", gc_pid.to_string())
.unwrap();
}
}
loop {
// Shorten loop timer to avoid user closing Cultivation before automatic stuff
thread::sleep(std::time::Duration::from_secs(2));
// Refresh system info
system.refresh_all();
// Grab the grasscutter process name
let proc = WATCH_GRASSCUTTER_PROCESS.lock().unwrap().to_string();
if !proc.is_empty() {
let mut exists = true;
if system.process(gc_pid).is_none() {
exists = false;
}
// If the grasscutter process closes.
if !exists {
println!("Grasscutter closed");
*WATCH_GRASSCUTTER_PROCESS.lock().unwrap() = "".to_string();
window.emit("grasscutter_closed", &()).unwrap();
break;
}
}
}
});
}
#[tauri::command]
async fn connect(port: u16, certificate_path: String) {
// Log message to console.
println!("Connecting to proxy...");
// Change proxy settings.
proxy::connect_to_proxy(port);
// Create and start a proxy.
proxy::create_proxy(port, certificate_path).await;
}
#[tauri::command]
fn disconnect() {
// Log message to console.
println!("Disconnecting from proxy...");
// Change proxy settings.
proxy::disconnect_from_proxy();
}
#[tauri::command]
async fn req_get(url: String) -> String {
// Send a GET request to the specified URL and send the response body back to the client.
web::query(&url.to_string()).await
}
#[tauri::command]
async fn get_theme_list(data_dir: String) -> Vec<HashMap<String, String>> {
let theme_loc = format!("{}/themes", data_dir);
// Ensure folder exists
if !std::path::Path::new(&theme_loc).exists() {
std::fs::create_dir_all(&theme_loc).unwrap();
}
// Read each index.json folder in each theme folder
let mut themes = Vec::new();
for entry in std::fs::read_dir(&theme_loc).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
let index_path = format!("{}/index.json", path.to_str().unwrap());
if std::path::Path::new(&index_path).exists() {
let theme_json = std::fs::read_to_string(&index_path).unwrap();
let mut map = HashMap::new();
map.insert("json".to_string(), theme_json);
map.insert("path".to_string(), path.to_str().unwrap().to_string());
// Push key-value pair containing "json" and "path"
themes.push(map);
}
}
}
themes
}

View File

@@ -1,230 +0,0 @@
use crate::config;
use crate::file_helpers;
use crate::system_helpers;
use std::path::PathBuf;
#[cfg(target_os = "linux")]
use once_cell::sync::Lazy;
#[cfg(target_os = "linux")]
use std::sync::Arc;
#[cfg(target_os = "linux")]
use tokio::sync::Mutex;
#[cfg(target_os = "linux")]
static PATCH_STATE: Lazy<Arc<Mutex<Option<PatchState>>>> = Lazy::new(|| Arc::new(Mutex::new(None)));
#[cfg(target_os = "linux")]
#[derive(Debug, Clone, Copy)]
enum PatchState {
NotExist,
Same,
BakNotExist,
BakExist,
}
#[cfg(target_os = "linux")]
use PatchState::*;
#[cfg(target_os = "linux")]
impl PatchState {
fn to_wta(self) -> WhatToUnpach {
let (mhyp_renamed, game_was_patched) = match self {
NotExist => (false, true),
Same => (false, true),
BakNotExist => (true, true),
BakExist => (false, false),
};
WhatToUnpach {
mhyp_renamed,
game_was_patched,
}
}
}
#[cfg(target_os = "linux")]
#[derive(Debug, Clone)]
struct WhatToUnpach {
mhyp_renamed: bool,
game_was_patched: bool,
}
#[cfg(windows)]
#[tauri::command]
pub async fn patch_game() -> bool {
let patch_path = PathBuf::from(system_helpers::install_location()).join("patch/version.dll");
// Are we already patched with mhypbase? If so, that's fine, just continue as normal
let game_is_patched = file_helpers::are_files_identical(
patch_path.clone().to_str().unwrap(),
PathBuf::from(get_game_rsa_path().await.unwrap())
.join("mhypbase.dll")
.to_str()
.unwrap(),
);
// Tell user they won't be unpatched with manual mhypbase patch
if game_is_patched {
println!(
"You are already patched using mhypbase, so you will not be auto patched and unpatched!"
);
return true;
}
// Copy the patch to game files
let replaced = file_helpers::copy_file_with_new_name(
patch_path.clone().to_str().unwrap().to_string(),
get_game_rsa_path().await.unwrap(),
String::from("version.dll"),
);
if !replaced {
return false;
}
true
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub async fn patch_game() -> bool {
let mut patch_state_mutex = PATCH_STATE.lock().await;
if patch_state_mutex.is_some() {
println!("Game already patched!");
}
let patch_path = PathBuf::from(system_helpers::install_location()).join("patch/version.dll");
let game_mhyp = PathBuf::from(get_game_rsa_path().await.unwrap()).join("mhypbase.dll");
let game_mhyp_bak = PathBuf::from(get_game_rsa_path().await.unwrap()).join("mhypbase.dll.bak");
let patch_state = if !game_mhyp.exists() {
NotExist
} else if file_helpers::are_files_identical(
patch_path.to_str().unwrap(),
game_mhyp.to_str().unwrap(),
) {
Same
} else if !game_mhyp_bak.exists() {
BakNotExist
} else {
BakExist
};
match patch_state {
NotExist => {
// No renaming needed.
// Copy version.dll as mhypbase.dll
file_helpers::copy_file_with_new_name(
patch_path.clone().to_str().unwrap().to_string(),
get_game_rsa_path().await.unwrap(),
String::from("mhypbase.dll"),
);
}
Same => {
// No renaming needed.
// No copying needed.
println!("The game is already patched.");
}
BakNotExist => {
// The current mhypbase.dll is most likely the original
// Rename mhypbase.dll to mhypbase.dll.bak
file_helpers::rename(
game_mhyp.to_str().unwrap().to_string(),
game_mhyp_bak
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string(),
);
// Copy version.dll as mhypbase.dll
file_helpers::copy_file_with_new_name(
patch_path.clone().to_str().unwrap().to_string(),
get_game_rsa_path().await.unwrap(),
String::from("mhypbase.dll"),
);
}
BakExist => {
// Can't rename. mhypbase.dll.bak already exists.
// Can't patch. mhypbase.dll exists.
// This SHOULD NOT HAPPEN
println!("The game directory contains a mhypbase.dll, but it's different from the patch.");
println!("Make sure you have the original mhypbase.dll.");
println!("Delete any other copy, and place the original copy in the game directory with the original name.");
}
}
patch_state_mutex.replace(patch_state);
patch_state.to_wta().game_was_patched
}
#[cfg(windows)]
#[tauri::command]
pub async fn unpatch_game() -> bool {
// Just delete patch since it's not replacing any existing file
let deleted = file_helpers::delete_file(
PathBuf::from(get_game_rsa_path().await.unwrap())
.join("version.dll")
.to_str()
.unwrap()
.to_string(),
);
deleted
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub async fn unpatch_game() -> bool {
// TODO: Prevent the launcher from unpatching the game two times
// This might be related to redirecting calls from the ts version of
// unpatchGame to the rust version
let mut patch_state_mutex = PATCH_STATE.lock().await;
let patch_state = patch_state_mutex.take();
if patch_state.is_none() {
println!("Game not patched!");
// NOTE: true is returned since otherwhise the launcher thinks unpatching failed
// NOTE: actually it should be false since delete_file always returns false
return false;
}
let patch_state = patch_state.unwrap();
let game_mhyp = PathBuf::from(get_game_rsa_path().await.unwrap()).join("mhypbase.dll");
let game_mhyp_bak = PathBuf::from(get_game_rsa_path().await.unwrap()).join("mhypbase.dll.bak");
let WhatToUnpach {
mhyp_renamed,
game_was_patched,
} = patch_state.to_wta();
// If the current mhypbase.dll is the patch, then delete it.
let deleted = if game_was_patched {
file_helpers::delete_file(game_mhyp.to_str().unwrap().to_string());
true
} else {
false
};
// If we renamed the original mhypbase.dll to mhypbase.dll.bak
// rename mhypbase.dll.bak back to mhypbase.dll
if mhyp_renamed {
file_helpers::rename(
game_mhyp_bak.to_str().unwrap().to_string(),
game_mhyp.to_str().unwrap().to_string(),
)
}
// NOTE: As mentioned in a note above, false should be returned if the function succeded
// and true if it failed
!deleted
}
pub async fn get_game_rsa_path() -> Option<String> {
let config = config::get_config();
config.game_install_path.as_ref()?;
let mut game_folder = PathBuf::from(config.game_install_path.unwrap());
game_folder.pop();
Some(format!("{}/", game_folder.to_str().unwrap()).replace('\\', "/"))
}

View File

@@ -1,521 +0,0 @@
/*
* Built on example code from:
* https://github.com/omjadas/hudsucker/blob/main/examples/log.rs
*/
use crate::config::get_config;
use once_cell::sync::Lazy;
use std::{path::PathBuf, str::FromStr, sync::Mutex};
use hudsucker::{
async_trait::async_trait,
certificate_authority::RcgenAuthority,
hyper::{Body, Request, Response, StatusCode},
*,
};
use rcgen::*;
use std::fs;
use std::net::SocketAddr;
use std::path::Path;
use rustls_pemfile as pemfile;
use tauri::{api::path::data_dir, http::Uri};
#[cfg(windows)]
use registry::{Data, Hive, Security};
#[cfg(target_os = "linux")]
use crate::system_helpers::{AsRoot, SpawnItsFineReally};
#[cfg(target_os = "linux")]
use anime_launcher_sdk::{config::ConfigExt, genshin::config::Config};
#[cfg(target_os = "linux")]
use std::{fs::File, io::Write, process::Command};
async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("Failed to install CTRL+C signal handler");
}
// Global ver for getting server address.
static SERVER: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new("http://localhost:443".to_string()));
static REDIRECT_MORE: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
#[derive(Clone)]
struct ProxyHandler;
#[tauri::command]
pub fn set_proxy_addr(addr: String) {
if addr.contains(' ') {
let addr2 = addr.replace(' ', "");
*SERVER.lock().unwrap() = addr2;
} else {
*SERVER.lock().unwrap() = addr;
}
println!("Set server to {}", SERVER.lock().unwrap());
}
#[tauri::command]
pub fn set_redirect_more() {
*REDIRECT_MORE.lock().unwrap() = true;
}
#[async_trait]
impl HttpHandler for ProxyHandler {
async fn handle_request(
&mut self,
_ctx: &HttpContext,
mut req: Request<Body>,
) -> RequestOrResponse {
let uri = req.uri().to_string();
let mut more = get_config().redirect_more;
if *REDIRECT_MORE.lock().unwrap() {
more = Some(true);
}
match more {
Some(true) => {
if uri.contains("hoyoverse.com")
|| uri.contains("mihoyo.com")
|| uri.contains("yuanshen.com")
|| uri.contains("starrails.com")
|| uri.contains("bhsr.com")
|| uri.contains("bh3.com")
|| uri.contains("honkaiimpact3.com")
|| uri.contains("zenlesszonezero.com")
{
// Handle CONNECTs
if req.method().as_str() == "CONNECT" {
let builder = Response::builder()
.header("DecryptEndpoint", "Created")
.status(StatusCode::OK);
let res = builder.body(()).unwrap();
// Respond to CONNECT
*res.body()
} else {
let uri_path_and_query = req.uri().path_and_query().unwrap().as_str();
// Create new URI.
let new_uri =
Uri::from_str(format!("{}{}", SERVER.lock().unwrap(), uri_path_and_query).as_str())
.unwrap();
// Set request URI to the new one.
*req.uri_mut() = new_uri;
}
}
}
Some(false) => {
if uri.contains("hoyoverse.com")
|| uri.contains("mihoyo.com")
|| uri.contains("yuanshen.com")
{
// Handle CONNECTs
if req.method().as_str() == "CONNECT" {
let builder = Response::builder()
.header("DecryptEndpoint", "Created")
.status(StatusCode::OK);
let res = builder.body(()).unwrap();
// Respond to CONNECT
*res.body()
} else {
let uri_path_and_query = req.uri().path_and_query().unwrap().as_str();
// Create new URI.
let new_uri =
Uri::from_str(format!("{}{}", SERVER.lock().unwrap(), uri_path_and_query).as_str())
.unwrap();
// Set request URI to the new one.
*req.uri_mut() = new_uri;
}
}
}
// Use default as fallback
None => {
if uri.contains("hoyoverse.com")
|| uri.contains("mihoyo.com")
|| uri.contains("yuanshen.com")
{
// Handle CONNECTs
if req.method().as_str() == "CONNECT" {
let builder = Response::builder()
.header("DecryptEndpoint", "Created")
.status(StatusCode::OK);
let res = builder.body(()).unwrap();
// Respond to CONNECT
*res.body()
} else {
let uri_path_and_query = req.uri().path_and_query().unwrap().as_str();
// Create new URI.
let new_uri =
Uri::from_str(format!("{}{}", SERVER.lock().unwrap(), uri_path_and_query).as_str())
.unwrap();
// Set request URI to the new one.
*req.uri_mut() = new_uri;
}
}
}
}
req.into()
}
async fn handle_response(
&mut self,
_context: &HttpContext,
response: Response<Body>,
) -> Response<Body> {
response
}
async fn should_intercept(&mut self, _ctx: &HttpContext, _req: &Request<Body>) -> bool {
let uri = _req.uri().to_string();
let more = get_config().redirect_more;
match more {
Some(true) => {
uri.contains("hoyoverse.com")
|| uri.contains("mihoyo.com")
|| uri.contains("yuanshen.com")
|| uri.contains("starrails.com")
|| uri.contains("bhsr.com")
|| uri.contains("bh3.com")
|| uri.contains("honkaiimpact3.com")
|| uri.contains("zenlesszonezero.com")
}
Some(false) => {
uri.contains("hoyoverse.com") || uri.contains("mihoyo.com") || uri.contains("yuanshen.com")
}
None => {
uri.contains("hoyoverse.com") || uri.contains("mihoyo.com") || uri.contains("yuanshen.com")
}
}
}
}
/**
* 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] = &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(
pemfile::pkcs8_private_keys(&mut private_key_bytes)
.expect("Failed to parse private key")
.remove(0),
);
let ca_cert = rustls::Certificate(
pemfile::certs(&mut ca_cert_bytes)
.expect("Failed to parse CA certificate")
.remove(0),
);
// Create the certificate authority.
let authority = RcgenAuthority::new(private_key, ca_cert, 1_000)
.expect("Failed to create Certificate Authority");
// Create an instance of the proxy.
let proxy = ProxyBuilder::new()
.with_addr(SocketAddr::from(([0, 0, 0, 0], proxy_port)))
.with_rustls_client()
.with_ca(authority)
.with_http_handler(ProxyHandler)
.build();
// Start the proxy.
tokio::spawn(proxy.start(shutdown_signal()));
}
/**
* Connects to the local HTTP(S) proxy server.
*/
#[cfg(windows)]
pub fn connect_to_proxy(proxy_port: u16) {
// Create 'ProxyServer' string.
let server_string: String = format!(
"http=127.0.0.1:{};https=127.0.0.1:{}",
proxy_port, proxy_port
);
// Fetch the 'Internet Settings' registry key.
let settings = Hive::CurrentUser
.open(
r"Software\Microsoft\Windows\CurrentVersion\Internet Settings",
// Only write should be needed but too many cases of Culti not being able to read/write proxy settings
Security::AllAccess,
)
.unwrap();
// Set registry values.
settings
.set_value("ProxyServer", &Data::String(server_string.parse().unwrap()))
.unwrap();
settings.set_value("ProxyEnable", &Data::U32(1)).unwrap();
println!("Connected to the proxy.");
}
#[cfg(target_os = "linux")]
pub fn connect_to_proxy(proxy_port: u16) {
let mut config = Config::get().unwrap();
let proxy_addr = format!("127.0.0.1:{}", proxy_port);
if !config.game.environment.contains_key("http_proxy") {
config
.game
.environment
.insert("http_proxy".to_string(), proxy_addr.clone());
}
if !config.game.environment.contains_key("https_proxy") {
config
.game
.environment
.insert("https_proxy".to_string(), proxy_addr);
}
Config::update(config);
}
#[cfg(target_od = "macos")]
pub fn connect_to_proxy(_proxy_port: u16) {
println!("No Mac support yet. Someone mail me a Macbook and I will do it B)")
}
/**
* Disconnects from the local HTTP(S) proxy server.
*/
#[cfg(windows)]
pub fn disconnect_from_proxy() {
// Fetch the 'Internet Settings' registry key.
let settings = Hive::CurrentUser
.open(
r"Software\Microsoft\Windows\CurrentVersion\Internet Settings",
Security::AllAccess,
)
.unwrap();
// Set registry values.
settings.set_value("ProxyEnable", &Data::U32(0)).unwrap();
println!("Disconnected from proxy.");
}
#[cfg(target_os = "linux")]
pub fn disconnect_from_proxy() {
let mut config = Config::get().unwrap();
if config.game.environment.contains_key("http_proxy") {
config.game.environment.remove("http_proxy");
}
if config.game.environment.contains_key("https_proxy") {
config.game.environment.remove("https_proxy");
}
Config::update(config);
}
#[cfg(target_os = "macos")]
pub fn disconnect_from_proxy() {}
/*
* Generates a private key and certificate used by the certificate authority.
* Additionally installs the certificate and private key in the Root CA store.
* Source: https://github.com/zu1k/good-mitm/raw/master/src/ca/gen.rs
*/
#[tauri::command]
pub fn generate_ca_files(path: &Path) {
let mut params = CertificateParams::default();
let mut details = DistinguishedName::new();
// Set certificate details.
details.push(DnType::CommonName, "Cultivation");
details.push(DnType::OrganizationName, "Grasscutters");
details.push(DnType::CountryName, "CN");
details.push(DnType::LocalityName, "CN");
// Set details in the parameter.
params.distinguished_name = details;
// Set other properties.
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::CrlSign,
];
// Create certificate.
let cert = Certificate::from_params(params).unwrap();
let cert_crt = cert.serialize_pem().unwrap();
let private_key = cert.serialize_private_key_pem();
// Make certificate directory.
let cert_dir = path.join("ca");
match fs::create_dir(&cert_dir) {
Ok(_) => {}
Err(e) => {
println!("{}", e);
}
};
// Write the certificate to a file.
let cert_path = cert_dir.join("cert.crt");
match fs::write(&cert_path, cert_crt) {
Ok(_) => println!("Wrote certificate to {}", cert_path.to_str().unwrap()),
Err(e) => println!(
"Error writing certificate to {}: {}",
cert_path.to_str().unwrap(),
e
),
}
// Write the private key to a file.
let private_key_path = cert_dir.join("private.key");
match fs::write(&private_key_path, private_key) {
Ok(_) => println!(
"Wrote private key to {}",
private_key_path.to_str().unwrap()
),
Err(e) => println!(
"Error writing private key to {}: {}",
private_key_path.to_str().unwrap(),
e
),
}
// Install certificate into the system's Root CA store.
install_ca_files(&cert_path);
}
/*
* Attempts to install the certificate authority's certificate into the Root CA store.
*/
#[cfg(windows)]
pub fn install_ca_files(cert_path: &Path) {
crate::system_helpers::run_command(
"certutil",
vec!["-user", "-addstore", "Root", cert_path.to_str().unwrap()],
None,
);
println!("Installed certificate.");
}
#[cfg(target_os = "macos")]
pub fn install_ca_files(cert_path: &Path) {
crate::system_helpers::run_command(
"security",
vec![
"add-trusted-cert",
"-d",
"-r",
"trustRoot",
"-k",
"/Library/Keychains/System.keychain",
cert_path.to_str().unwrap(),
],
None,
);
println!("Installed certificate.");
}
#[cfg(target_os = "linux")]
pub fn install_ca_files(cert_path: &Path) {
let platform = os_type::current_platform();
use os_type::OSType::*;
// TODO: Add more distros
match &platform.os_type {
// Debian-based
Debian | Ubuntu | Kali => {
let usr_certs = PathBuf::from("/usr/local/share/ca-certificates");
let usr_cert_path = usr_certs.join("cultivation.crt");
// We want to execute multiple commands, but we don't want multiple pkexec prompts
// so we have to use a script
let script = Path::new("/tmp/cultivation-inject-ca-cert.sh");
let mut scriptf = File::create(script).unwrap();
#[cfg(debug_assertions)]
let setflags = "xe";
#[cfg(not(debug_assertions))]
let setflags = "e";
write!(
scriptf,
r#"#!/usr/bin/env bash
set -{}
CERT="{}"
CERT_DIR="{}"
CERT_TARGET="{}"
# Create dir if it doesn't exist
if ! [[ -d "$CERT_DIR" ]]; then
mkdir -v "$CERT_DIR"
fi
cp -v "$CERT" "$CERT_TARGET"
update-ca-certificates
"#,
setflags,
cert_path.to_str().unwrap(),
usr_certs.to_str().unwrap(),
usr_cert_path.to_str().unwrap()
)
.unwrap();
scriptf.flush().unwrap();
drop(scriptf);
let _ = Command::new("bash")
.arg(script)
.as_root_gui()
.spawn_its_fine_really("Unable to install certificate");
if let Err(e) = fs::remove_file(script) {
println!("Unable to remove certificate install script: {}", e);
};
}
// RedHat-based
//Redhat | CentOS |
// Arch-based
Arch | Manjaro => {
let _ = Command::new("trust")
.arg("anchor")
.arg("--store")
.arg(cert_path)
.as_root_gui()
.spawn_its_fine_really("Unable to install certificate");
}
OSX => unreachable!(),
_ => {
println!("Unsupported Linux distribution.");
return;
}
}
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,33 +0,0 @@
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Release {
pub tag_name: String,
pub link: String,
}
#[tauri::command]
pub async fn get_latest_release() -> Release {
let url = "https://api.github.com/repos/Grasscutters/Cultivation/releases/latest";
let client = reqwest::Client::new();
let response = client
.get(url)
.header("User-Agent", "Cultivation")
.send()
.await
.unwrap();
let text = response.text().await.unwrap();
// This includes ip when github rate limits you, so avoid it for now to avoid leaks through screenshots
//println!("Response: {}", text);
// Parse "tag_name" from JSON
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
let tag_name = json["tag_name"].as_str().unwrap();
// Parse "html_url"
let link = json["html_url"].as_str().unwrap();
Release {
tag_name: tag_name.to_string(),
link: link.to_string(),
}
}

View File

@@ -1,709 +0,0 @@
use ini::Ini;
use std::path::PathBuf;
use std::process::Command;
#[cfg(windows)]
use std::ffi::OsStr;
#[cfg(windows)]
use {
registry::{Data, Hive, Security},
windows_service::service::{ServiceAccess, ServiceState::Stopped},
windows_service::service_manager::{ServiceManager, ServiceManagerAccess},
};
#[cfg(target_os = "linux")]
use crate::AAGL_THREAD;
#[cfg(target_os = "linux")]
use anime_launcher_sdk::{
config::ConfigExt, genshin::config::Config, genshin::game, genshin::states::LauncherState,
wincompatlib::prelude::*,
};
#[cfg(target_os = "linux")]
use std::{path::Path, process::Stdio, thread};
#[cfg(target_os = "linux")]
use term_detect::get_terminal;
#[cfg(target_os = "linux")]
fn guess_user_terminal() -> String {
if let Ok(term) = get_terminal() {
return term.0;
}
eprintln!("Could not guess default terminal. Try setting the $TERMINAL environment variable.");
// If everything fails, default to xterm
"xterm".to_string()
}
#[cfg(target_os = "linux")]
fn rawstrcmd(cmd: &Command) -> String {
format!("{:?}", cmd)
}
#[cfg(target_os = "linux")]
fn strcmd(cmd: &Command) -> String {
format!("bash -c {:?}", rawstrcmd(cmd))
}
#[cfg(target_os = "linux")]
pub trait AsRoot {
fn as_root(&self) -> Self;
fn as_root_gui(&self) -> Self;
}
#[cfg(target_os = "linux")]
impl AsRoot for Command {
fn as_root(&self) -> Self {
let mut cmd = Command::new("sudo");
cmd.arg("--").arg("bash").arg("-c").arg(rawstrcmd(self));
cmd
}
fn as_root_gui(&self) -> Self {
let mut cmd = Command::new("pkexec");
cmd.arg("bash").arg("-c").arg(rawstrcmd(self));
cmd
}
}
#[cfg(target_os = "linux")]
trait InTerminalEmulator {
fn in_terminal(&self) -> Self;
fn in_terminal_noclose(&self) -> Self;
}
#[cfg(target_os = "linux")]
impl InTerminalEmulator for Command {
fn in_terminal(&self) -> Self {
let mut cmd = Command::new(guess_user_terminal());
cmd.arg("-e").arg(strcmd(self));
cmd
}
fn in_terminal_noclose(&self) -> Self {
let mut cmd = Command::new(guess_user_terminal());
cmd.arg("--noclose");
cmd.arg("-e").arg(strcmd(self));
cmd
}
}
#[cfg(target_os = "linux")]
pub trait SpawnItsFineReally {
fn spawn_its_fine_really(&mut self, msg: &str) -> anyhow::Result<()>;
}
#[cfg(target_os = "linux")]
impl SpawnItsFineReally for Command {
fn spawn_its_fine_really(&mut self, msg: &str) -> anyhow::Result<()> {
let res = self.status();
let Ok(status) = res else {
let error = res.unwrap_err();
println!("{}: {}", msg, &error);
return Err(error.into());
};
if !status.success() {
println!("{}: {}", msg, status);
Err(anyhow::anyhow!("{}: {}", msg, status))
} else {
Ok(())
}
}
}
#[tauri::command]
pub fn run_program(path: String, args: Option<String>) {
// Without unwrap_or, this can crash when UAC prompt is denied
match open::with(
format!("{} {}", path, args.unwrap_or_else(|| "".into())),
path.clone(),
) {
Ok(_) => (),
Err(e) => println!("Failed to open program ({}): {}", &path, e),
};
}
#[cfg(target_os = "windows")]
#[tauri::command]
pub fn run_program_relative(path: String, args: Option<String>) {
// Save the current working directory
let cwd = std::env::current_dir().unwrap();
// Set the new working directory to the path before the executable
let mut path_buf = std::path::PathBuf::from(&path);
path_buf.pop();
// Set new working directory
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_else(|| "".into()))).unwrap_or(());
// Restore the original working directory
std::env::set_current_dir(cwd).unwrap();
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn run_program_relative(path: String, args: Option<String>) {
// This program should not run as root
run_un_elevated(path, args)
}
#[tauri::command]
pub fn run_command(program: &str, args: Vec<&str>, relative: Option<bool>) {
let prog = program.to_string();
let args = args.iter().map(|s| s.to_string()).collect::<Vec<String>>();
// Commands should not block (this is for the reshade injector mostly)
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();
}
// Run the command
let mut command = Command::new(&prog);
command.args(&args);
command.spawn().unwrap();
// Restore the original working directory
std::env::set_current_dir(cwd).unwrap();
});
}
#[tauri::command]
pub fn run_jar(path: String, execute_in: String, java_path: String) {
let command = if java_path.is_empty() {
format!("java -jar \"{}\"", path)
} else {
format!("\"{}\" -jar \"{}\"", java_path, path)
};
println!("Launching .jar with command: {}", &command);
// Open the program from the specified path.
#[cfg(not(target_os = "linux"))]
match open::with(
format!("/k cd /D \"{}\" & {}", &execute_in, &command),
"C:\\Windows\\System32\\cmd.exe",
) {
Ok(_) => (),
Err(e) => println!("Failed to open jar ({} from {}): {}", &path, &execute_in, e),
};
#[cfg(target_os = "linux")]
thread::spawn(move || {
match Command::new(guess_user_terminal())
.arg("-e")
.arg(command)
.current_dir(execute_in.clone())
.spawn()
{
Ok(mut handler) => {
// Prevent creation of zombie processes
handler
.wait()
.expect("Grasscutter exited with non-zero exit code");
}
Err(e) => println!("Failed to open jar ({} from {}): {}", &path, &execute_in, e),
}
});
}
#[cfg(not(target_os = "linux"))]
#[tauri::command]
pub fn run_jar_root(_path: String, _execute_in: String, _java_path: String) {
panic!("Not implemented");
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn run_jar_root(path: String, execute_in: String, java_path: String) {
let mut command = if java_path.is_empty() {
Command::new("java")
} else {
Command::new(java_path)
};
command.arg("-jar").arg(&path).current_dir(&execute_in);
println!("Launching .jar with command: {}", strcmd(&command));
// Open the program from the specified path.
thread::spawn(move || {
match command.as_root_gui().in_terminal().spawn() {
Ok(mut handler) => {
// Prevent creation of zombie processes
handler
.wait()
.expect("Grasscutter exited with non-zero exit code");
}
Err(e) => println!("Failed to open jar ({} from {}): {}", &path, &execute_in, e),
}
});
}
#[cfg(target_os = "windows")]
#[tauri::command]
pub fn run_un_elevated(path: String, args: Option<String>) {
// Open the program non-elevated.
match open::with(
format!(
"cmd /min /C \"set __COMPAT_LAYER=RUNASINVOKER && start \"\" \"{}\"\" {}",
path,
args.unwrap_or_else(|| "".into())
),
"C:\\Windows\\System32\\cmd.exe",
) {
Ok(_) => (),
Err(e) => println!("Failed to open program ({}): {}", &path, e),
};
}
#[cfg(target_os = "linux")]
fn aagl_wine_run<P: AsRef<Path>>(path: P, args: Option<String>) -> Command {
let config = Config::get().unwrap();
let wine = config.get_selected_wine().unwrap().unwrap();
let wine_run = wine
.to_wine(
config.components.path,
Some(config.game.wine.builds.join(&wine.name)),
)
.with_prefix(config.game.wine.prefix)
.with_loader(WineLoader::Current)
.with_arch(WineArch::Win64);
let env: Vec<(String, String)> = config
.game
.wine
.sync
.get_env_vars()
.clone()
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
use anime_launcher_sdk::components::wine::UnifiedWine::*;
let wined = match wine_run {
Default(wine) => wine,
Proton(proton) => proton.wine().clone(),
};
let mut cmd = Command::new(&wined.binary);
cmd.arg(path.as_ref()).envs(wined.get_envs()).envs(env);
if let Some(args) = args {
let mut args: Vec<String> = args.split(' ').map(|x| x.to_string()).collect();
cmd.args(&mut args);
};
cmd
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn run_un_elevated(path: String, args: Option<String>) {
let path = Path::new(&path);
let exec_name = path.file_name().unwrap().to_str().unwrap();
if exec_name == ["Yuan", "Shen", ".exe"].join("").as_str()
|| exec_name == ["Gen", "shin", "Impact", ".exe"].join("").as_str()
{
let game_thread = thread::spawn(|| {
'statechk: {
let state = LauncherState::get_from_config(|_| {});
let Ok(state) = state else {
println!("Failed to get state: {}", state.unwrap_err());
break 'statechk;
};
use anime_launcher_sdk::genshin::states::LauncherState::*;
match state {
FolderMigrationRequired { from, .. } => Err(format!(
"A folder migration is required ({:?} needs to be moved)",
from
)),
WineNotInstalled => Err("Wine is not installed".to_string()),
PrefixNotExists => Err("The Wine prefix does not exist".to_string()),
GameNotInstalled(_) => Err("The game is not installed".to_string()),
_ => Ok(()),
}
.expect("Can't launch game. Check the other launcher.");
}
if let Err(e) = game::run() {
println!("An error occurred while running the game: {}", e);
}
});
{
let mut game_thead_lock = AAGL_THREAD.lock().unwrap();
game_thead_lock.replace(game_thread);
}
return;
}
// Run exe with wine
if path.extension().unwrap() == "exe" {
let path = path.to_owned().clone();
thread::spawn(move || {
let _ = aagl_wine_run(&path, args)
.current_dir(path.parent().unwrap())
.in_terminal()
.spawn_its_fine_really(&format!(
"Failed to open program ({})",
path.to_str().unwrap()
));
});
}
println!(
"Can't run {:?}. Running this type of file is not supported yet.",
path
);
}
#[tauri::command]
pub fn open_in_browser(url: String) {
// Open the URL in the default browser.
match open::that(url) {
Ok(_) => (),
Err(e) => println!("Failed to open URL: {}", e),
};
}
#[tauri::command]
pub fn install_location() -> String {
let mut exe_path = std::env::current_exe().unwrap();
#[cfg(windows)]
{
// Get the path to the executable.
exe_path.pop();
return exe_path.to_str().unwrap().to_string();
}
#[cfg(target_os = "linux")]
{
let bin_name = exe_path.file_name().unwrap().to_str().unwrap().to_string();
exe_path.pop();
if exe_path.starts_with("/usr/bin") {
let mut path = PathBuf::from("/usr/lib");
path.push(bin_name);
path
} else {
exe_path
}
.to_str()
.unwrap()
.to_string()
}
}
#[tauri::command]
pub fn set_migoto_target(window: tauri::Window, migoto_path: String) -> bool {
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;
}
};
window.emit("migoto_set", &()).unwrap();
// Set options
conf
.with_section(Some("Loader"))
.set("target", "GenshinImpact.exe");
// Write file
match conf.write_to_file(&migoto_pathbuf) {
Ok(_) => {
println!("Wrote config!");
true
}
Err(e) => {
println!("Error writing config: {}", e);
false
}
}
}
#[tauri::command]
pub fn set_migoto_delay(migoto_path: String) -> bool {
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("delay", "20");
// Write file
match conf.write_to_file(&migoto_pathbuf) {
Ok(_) => {
println!("Wrote delay!");
true
}
Err(e) => {
println!("Error writing delay: {}", 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(windows)]
#[tauri::command]
pub fn service_status(service: String) -> bool {
let manager = match ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT) {
Ok(manager) => manager,
Err(_e) => return false,
};
let my_service = match manager.open_service(service.clone(), ServiceAccess::QUERY_STATUS) {
Ok(my_service) => my_service,
Err(_e) => {
println!("{} service not found! Not installed?", service);
return false;
}
};
let status_result = my_service.query_status();
if let Ok(..) = status_result {
let status = status_result.unwrap();
println!("{} service status: {:?}", service, status.current_state);
if status.current_state == Stopped {
// Start the service if it is stopped
start_service(service);
}
true
} else {
false
}
}
#[cfg(target_os = "linux")]
fn to_linux_service_name(service: &str) -> Option<String> {
Some(format!(
"{}.service",
match service {
"MongoDB" => "mongod",
_ => return None,
}
))
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn service_status(service: String) -> bool {
// Change Windows service name into Linux service name
let service_lnx = to_linux_service_name(&service);
if service_lnx.is_none() {
return false;
}
let service_lnx = service_lnx.unwrap();
let status = Command::new("systemctl")
.arg("is-active")
.arg(service_lnx)
.stdout(Stdio::null())
.status();
if status.is_err() {
return false;
}
let status = status.unwrap().success();
if status {
status
} else {
start_service(service)
}
}
#[cfg(windows)]
#[tauri::command]
pub fn start_service(service: String) -> bool {
println!("Starting service: {}", service);
let manager = match ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT) {
Ok(manager) => manager,
Err(_e) => return false,
};
let my_service = match manager.open_service(service, ServiceAccess::START) {
Ok(my_service) => my_service,
Err(_e) => return false,
};
match my_service.start(&[OsStr::new("Started service!")]) {
Ok(_s) => true,
Err(_e) => return false,
};
true
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn start_service(service: String) -> bool {
println!("Starting service: {}", service);
let service_lnx = to_linux_service_name(&service);
if service_lnx.is_none() {
return false;
}
let service_lnx = service_lnx.unwrap();
Command::new("systemctl")
.arg("start")
.arg(service_lnx)
.spawn_its_fine_really(&format!("Failed to stop service {}", service))
.is_ok()
}
#[cfg(windows)]
#[tauri::command]
pub fn stop_service(service: String) -> bool {
println!("Stopping service: {}", service);
let manager = match ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT) {
Ok(manager) => manager,
Err(_e) => return false,
};
let my_service = match manager.open_service(service, ServiceAccess::STOP) {
Ok(my_service) => my_service,
Err(_e) => return false,
};
match my_service.stop() {
Ok(_s) => true,
Err(_e) => return false,
};
true
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn stop_service(service: String) -> bool {
println!("Stopping service: {}", service);
let service_lnx = to_linux_service_name(&service);
if service_lnx.is_none() {
return false;
}
let service_lnx = service_lnx.unwrap();
Command::new("systemctl")
.arg("stop")
.arg(service_lnx)
.spawn_its_fine_really(&format!("Failed to start service {}", service))
.is_ok()
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub fn wipe_registry(exec_name: String) {
println!("Wiping registry");
let regpath = format!("HKCU\\Software\\miHoYo\\{}", exec_name);
let mut cmd = aagl_wine_run("reg", None);
cmd.args([
"DELETE",
&regpath,
"/f",
"/v",
"MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810",
]);
let _ = cmd.spawn_its_fine_really("Error wiping registry");
}
#[cfg(target_os = "macos")]
#[tauri::command]
pub fn wipe_registry(_exec_name: String) {}
#[cfg(windows)]
#[tauri::command]
pub fn is_elevated() -> bool {
is_elevated::is_elevated()
}
#[cfg(unix)]
#[tauri::command]
pub fn is_elevated() -> bool {
sudo::check() == sudo::RunningAs::Root
}
#[tauri::command]
pub fn get_platform() -> &'static str {
std::env::consts::OS
}
#[cfg(not(target_os = "linux"))]
#[tauri::command]
pub async fn jvm_add_cap(_java_path: String) -> bool {
panic!("Not implemented");
}
#[cfg(not(target_os = "linux"))]
#[tauri::command]
pub async fn jvm_remove_cap(_java_path: String) -> bool {
panic!("Not implemented");
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub async fn jvm_add_cap(java_path: String) -> bool {
let mut java_bin = if java_path.is_empty() {
which::which("java").expect("Java is not installed")
} else {
PathBuf::from(&java_path)
};
while java_bin.is_symlink() {
java_bin = java_bin.read_link().unwrap()
}
println!("Removing cap on {:?}", &java_bin);
Command::new("setcap")
.arg("CAP_NET_BIND_SERVICE=+eip")
.arg(java_bin)
.as_root_gui()
.spawn_its_fine_really(&format!("Failed to add cap to {}", java_path))
.is_ok()
}
#[cfg(target_os = "linux")]
#[tauri::command]
pub async fn jvm_remove_cap(java_path: String) -> bool {
let mut java_bin = if java_path.is_empty() {
which::which("java").expect("Java is not installed")
} else {
PathBuf::from(&java_path)
};
while java_bin.is_symlink() {
java_bin = java_bin.read_link().unwrap()
}
println!("Setting cap on {:?}", &java_bin);
Command::new("setcap")
.arg("-r")
.arg(java_bin)
.as_root_gui()
.spawn_its_fine_really(&format!("Failed to remove cap from {}", java_path))
.is_ok()
}

View File

@@ -1,207 +0,0 @@
use std::fs::{read_dir, File};
use std::path;
use std::thread;
use unrar::archive::Archive;
#[tauri::command]
pub fn unzip(
window: tauri::Window,
zipfile: String,
destpath: String,
top_level: Option<bool>,
folder_if_loose: Option<bool>,
) {
// Read file TODO: replace test file
let f = match File::open(&zipfile) {
Ok(f) => f,
Err(e) => {
println!("Failed to open zip file: {}", e);
return;
}
};
let write_path = path::PathBuf::from(&destpath);
// Get a list of all current directories
let mut dirs = vec![];
for entry in read_dir(&write_path).unwrap() {
let entry = entry.unwrap();
let entry_path = entry.path();
if entry_path.is_dir() {
dirs.push(entry_path);
}
}
// Run extraction in seperate thread
thread::spawn(move || {
let mut full_path = write_path.clone();
window.emit("extract_start", &zipfile).unwrap();
if folder_if_loose.unwrap_or(false) {
// Create a new folder with the same name as the zip file
let mut file_name = path::Path::new(&zipfile)
.file_name()
.unwrap()
.to_str()
.unwrap();
// remove ".zip" from the end of the file name
file_name = &file_name[..file_name.len() - 4];
let new_path = full_path.join(file_name);
match std::fs::create_dir_all(&new_path) {
Ok(_) => {}
Err(e) => {
println!("Failed to create directory: {}", e);
return;
}
};
full_path = new_path;
}
println!("Is rar file? {}", zipfile.ends_with(".rar"));
let name;
let success;
// If file ends in zip, OR is unknown, extract as zip, otherwise extract as rar
if zipfile.ends_with(".rar") {
success = extract_rar(&zipfile, &f, &full_path, top_level.unwrap_or(true));
let archive = Archive::new(zipfile.clone());
name = archive.list().unwrap().next().unwrap().unwrap().filename;
} else if zipfile.ends_with(".7z") {
success = extract_7z(&zipfile, &f, &full_path, top_level.unwrap_or(true));
name = String::from("banana");
} else {
success = extract_zip(&zipfile, &f, &full_path, top_level.unwrap_or(true));
// 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();
}
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 name.ends_with(".jar") {
window
.emit("jar_extracted", destpath.to_string() + name.as_str())
.unwrap();
}
// If downloading full build, emit that the jar was extracted with it
if zipfile.contains("GrasscutterCulti") {
window
.emit("jar_extracted", destpath.to_string() + "grasscutter.jar")
.unwrap();
}
if zipfile.contains("GrasscutterQuests") {
window
.emit("jar_extracted", destpath.to_string() + "grasscutter.jar")
.unwrap();
}
if zipfile.contains("GIMI") {
window
.emit(
"migoto_extracted",
destpath.to_string() + "3DMigoto Loader.exe",
)
.unwrap();
}
// Delete zip file
match std::fs::remove_file(&zipfile) {
Ok(_) => {
// Get any new directory that could have been created
let mut new_dir: String = String::new();
for entry in read_dir(&write_path).unwrap() {
let entry = entry.unwrap();
let entry_path = entry.path();
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);
window.emit("extract_end", &res_hash).unwrap();
println!("Deleted zip file: {}", zipfile);
}
Err(e) => {
println!("Failed to delete zip file: {}", e);
}
};
});
}
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())
.unwrap();
match open_archive.process() {
Ok(_) => {
println!(
"Extracted rar file to: {}",
full_path.to_str().unwrap_or("Error")
);
true
}
Err(e) => {
println!("Failed to extract rar file: {}", e);
false
}
}
}
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!(
"Extracted zip file to: {}",
full_path.to_str().unwrap_or("Error")
);
true
}
Err(e) => {
println!("Failed to extract zip file: {}", e);
false
}
}
}
fn extract_7z(sevenzfile: &str, _f: &File, full_path: &path::Path, _top_level: bool) -> bool {
match sevenz_rust::decompress_file(sevenzfile, full_path) {
Ok(_) => {
println!(
"Extracted 7zip file to: {}",
full_path.to_str().unwrap_or("Error")
);
true
}
Err(e) => {
println!("Failed to extract 7zip file: {}", e);
false
}
}
}

View File

@@ -1,50 +0,0 @@
use http::header;
use once_cell::sync::Lazy;
use reqwest::header::{CONTENT_TYPE, USER_AGENT};
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
let mut headers = header::HeaderMap::new();
headers.insert(USER_AGENT, header::HeaderValue::from_static("cultivation"));
headers.insert(
CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
let client = reqwest::Client::builder().default_headers(headers);
client.build().unwrap()
});
pub(crate) async fn query(site: &str) -> String {
CLIENT
.get(site)
.send()
.await
.expect("Failed to get web response")
.text()
.await
.unwrap()
}
#[tauri::command]
pub(crate) async fn valid_url(url: String) -> bool {
// Check if we get a 200 response
let client = reqwest::Client::new();
let response = client
.get(url)
.header(USER_AGENT, "cultivation")
.send()
.await
.ok();
if response.is_some() {
return response.unwrap().status().as_str() == "200";
} else {
false
}
}
#[tauri::command]
pub async fn web_get(url: String) -> String {
// Send a GET request to the specified URL and send the response body back to the client.
query(&url).await
}

View File

@@ -1,127 +0,0 @@
html,
body {
user-select: none;
height: 100vh;
overflow: hidden;
}
select {
width: 217px;
border: none;
border-bottom: 2px solid #cecece;
padding-right: 16px;
text-decoration-color: #000;
transition: border-bottom-color 0.1s ease-in-out;
}
select:focus {
outline: none;
border-bottom-color: #ffd326;
}
#root,
.App {
height: 100%;
}
.App {
background-size: cover !important;
background-position: center !important;
}
.TopButton {
height: 60%;
margin: 0px 10px;
color: #c5c5c5;
transition: color 0.1s ease-in-out;
}
.TopButton span {
display: inline-block;
line-height: normal;
border-bottom: 1px solid #c5c5c5;
}
.TopButton img {
filter: invert(95%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(88%) contrast(81%);
transition: filter 0.1s ease-in-out;
}
.TopButton:hover {
color: #fff;
cursor: pointer;
}
.TopButton:hover img {
filter: invert(100%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(100%) contrast(100%);
cursor: pointer;
}
.TopButton img {
height: 80%;
vertical-align: super;
}
#DownloadProgress {
position: absolute;
transform: translate(0%, -50%);
top: 50%;
left: 5%;
width: 50%;
}
.MiniDownloads {
position: absolute;
top: 40%;
left: 12%;
}
.arrow-down {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-top: 50px solid transparent;
/* Colored section */
border-top: 50px solid #fff;
position: fixed;
top: 73%;
left: 15%;
z-index: 99;
}
.BottomSection {
position: absolute;
bottom: 0%;
left: 50%;
transform: translate(-50%, 0%);
width: 100%;
height: 160px;
margin: 0;
padding: 0;
backdrop-filter: blur(10px);
box-shadow: inset 0px 5px 12px -3px rgb(0, 0, 0, 0.43);
}
@media (max-height: 580px) {
.BottomSection {
height: 150px;
}
}
@media (max-height: 500px) {
.BottomSection {
height: 140px;
}
}

View File

@@ -1,117 +0,0 @@
import React from 'react'
import './App.css'
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 { Main } from './Main'
import { Mods } from './Mods'
// From https://www.pixiv.net/en/artworks/93995273
import FALLBACK_BG from '../resources/icons/FALLBACK_BG.jpg'
interface IState {
page: string
bgFile: string
}
const downloadHandler = new DownloadHandler()
const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile'
class App extends React.Component<Readonly<unknown>, IState> {
constructor(props: Readonly<unknown>) {
super(props)
this.state = {
page: 'main',
bgFile: DEFAULT_BG,
}
}
async componentDidMount() {
// Load a theme if it exists
const theme = await getConfigOption('theme')
if (theme && theme !== 'default') {
const themeObj = await getTheme(theme)
await loadTheme(themeObj, document)
}
// Get custom bg AFTER theme is loaded !! important !!
const custom_bg = await getConfigOption('custom_background')
if (custom_bg) {
const isUrl = /^http(s)?:\/\//gm.test(custom_bg)
if (!isUrl) {
const isValid = await invoke('dir_exists', {
path: custom_bg,
})
this.setState(
{
bgFile: isValid ? convertFileSrc(custom_bg) : DEFAULT_BG,
},
this.forceUpdate
)
} else {
// Check if URL returns a valid image.
const isValid = await invoke('valid_url', {
url: custom_bg,
})
this.setState(
{
bgFile: isValid ? custom_bg : DEFAULT_BG,
},
this.forceUpdate
)
}
} else {
// Check if api bg is accessible
const isDefaultValid = await invoke('valid_url', {
url: DEFAULT_BG,
})
this.setState(
{
bgFile: isDefaultValid ? DEFAULT_BG : FALLBACK_BG,
},
this.forceUpdate
)
}
window.addEventListener('changePage', (e) => {
this.setState({
// @ts-expect-error - TS doesn't like our custom event
page: e.detail,
})
})
}
render() {
return (
<div
className="App"
style={
this.state.bgFile
? {
background: `url("${this.state.bgFile}") fixed`,
}
: {}
}
>
{(() => {
switch (this.state.page) {
case 'modding':
return <Mods downloadHandler={downloadHandler} />
default:
return <Main downloadHandler={downloadHandler} />
}
})()}
</div>
)
}
}
export default App

View File

@@ -1,54 +0,0 @@
import React from 'react'
import './App.css'
import TopBar from './components/TopBar'
import { invoke } from '@tauri-apps/api/tauri'
import { dataDir } from '@tauri-apps/api/path'
import TextInput from './components/common/TextInput'
let proxyAddress = ''
async function setProxyAddress(address: string) {
proxyAddress = address
await invoke('set_proxy_addr', { addr: address })
}
async function startProxy() {
await invoke('connect', { port: 2222, certificatePath: (await dataDir()) + 'cultivation/ca' })
await invoke('open_in_browser', { url: 'https://hoyoverse.com' })
}
async function stopProxy() {
await invoke('disconnect')
}
async function generateCertificates() {
await invoke('generate_ca_files', { path: (await dataDir()) + 'cultivation' })
}
async function generateInfo() {
console.log({
certificatePath: (await dataDir()) + 'cultivation/ca',
isAdmin: await invoke('is_elevated'),
connectingTo: proxyAddress,
})
alert('check your dev console and send that in #cultivation')
}
class Debug extends React.Component {
render() {
return (
<div className="App">
<TopBar />
<TextInput readOnly={false} initalValue={'change to set proxy address'} onChange={setProxyAddress} />
<button onClick={startProxy}>start proxy</button>
<button onClick={stopProxy}>stop proxy</button>
<button onClick={generateCertificates}>generate certificates</button>
<button onClick={generateInfo}>dump info</button>
</div>
)
}
}
export default Debug

View File

@@ -1,354 +0,0 @@
import React from 'react'
// Major Components
import TopBar from './components/TopBar'
import ServerLaunchSection from './components/ServerLaunchSection'
import MainProgressBar from './components/common/MainProgressBar'
import Options, { GrasscutterElevation } from './components/menu/Options'
import MiniDialog from './components/MiniDialog'
import DownloadList from './components/common/DownloadList'
import Downloads from './components/menu/Downloads'
import NewsSection from './components/news/NewsSection'
import Game from './components/menu/Game'
import RightBar from './components/RightBar'
import { ExtrasMenu } from './components/menu/ExtrasMenu'
import Notification from './components/common/Notification'
import GamePathNotify from './components/menu/GamePathNotify'
import { getConfig, getConfigOption, setConfigOption } from '../utils/configuration'
import { invoke } from '@tauri-apps/api'
import { getVersion } from '@tauri-apps/api/app'
import { listen } from '@tauri-apps/api/event'
import { dataDir } from '@tauri-apps/api/path'
import { appWindow } from '@tauri-apps/api/window'
import { unpatchGame } from '../utils/rsa'
import DownloadHandler from '../utils/download'
// Graphics
import cogBtn from '../resources/icons/cog.svg'
import downBtn from '../resources/icons/download.svg'
import wrenchBtn from '../resources/icons/wrench.svg'
interface IProps {
downloadHandler: DownloadHandler
}
interface IState {
isDownloading: boolean
optionsOpen: boolean
miniDownloadsOpen: boolean
downloadsOpen: boolean
gameDownloadsOpen: boolean
extrasOpen: boolean
migotoSet: boolean
playGame: (exe?: string, proc_name?: string) => void
notification: React.ReactElement | null
isGamePathSet: boolean
game_install_path: string
}
export class Main extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
isDownloading: false,
optionsOpen: false,
miniDownloadsOpen: false,
downloadsOpen: false,
gameDownloadsOpen: false,
extrasOpen: false,
migotoSet: false,
playGame: () => {
alert('Error launching game')
},
notification: null,
isGamePathSet: true,
game_install_path: '',
}
listen('lang_error', (payload) => {
console.log(payload)
})
listen('jar_extracted', ({ payload }: { payload: string }) => {
setConfigOption('grasscutter_path', payload)
})
listen('migoto_extracted', ({ payload }: { payload: string }) => {
setConfigOption('migoto_path', payload)
})
// Emitted for rsa replacing-purposes
listen('game_closed', async () => {
const wasPatched = await getConfigOption('patch_rsa')
if (wasPatched) {
const unpatched = await unpatchGame()
if (unpatched) {
alert(`Could not unpatch game! (Delete version.dll in your game folder)`)
}
}
})
listen('migoto_set', async () => {
this.setState({
migotoSet: !!(await getConfigOption('migoto_path')),
})
window.location.reload()
})
// Emitted for automatic processes
listen('grasscutter_closed', async () => {
const autoService = await getConfigOption('auto_mongodb')
const config = await getConfig()
if (autoService) {
await invoke('stop_service', { service: 'MongoDB' })
}
if ((await invoke('get_platform')) === 'linux') {
switch (config.grasscutter_elevation) {
case GrasscutterElevation.None:
break
case GrasscutterElevation.Capability:
await invoke('jvm_remove_cap', {
javaPath: config.java_path,
})
break
default:
console.error('Invalid grasscutter_elevation')
break
}
}
})
let min = false
// periodically check if we need to min/max based on whether the game is open
setInterval(async () => {
const gameOpen = await invoke('is_game_running')
if (gameOpen && !min) {
appWindow.minimize()
min = true
} else if (!gameOpen && min) {
appWindow.unminimize()
min = false
}
}, 1000)
this.openExtrasMenu = this.openExtrasMenu.bind(this)
}
async componentDidMount() {
const game_path = await getConfigOption('game_install_path')
const cert_generated = await getConfigOption('cert_generated')
this.setState({
game_install_path: game_path,
})
this.setState({
migotoSet: !!(await getConfigOption('migoto_path')),
})
if (!cert_generated) {
// Generate the certificate
await invoke('generate_ca_files', {
path: (await dataDir()) + 'cultivation',
})
await setConfigOption('cert_generated', true)
}
// Ensure old configs are updated to use RSA
const updatedConfig = await getConfigOption('patch_rsa')
await setConfigOption('patch_rsa', updatedConfig)
// Get latest version and compare to this version
const latestVersion: {
tag_name: string
link: string
} = await invoke('get_latest_release')
const tagName = latestVersion?.tag_name.replace(/[^\d.]/g, '')
// Check if tagName is different than current version
if (tagName && tagName !== (await getVersion())) {
// Display notification of new release
this.setState({
notification: (
<>
Cultivation{' '}
<a href="#" onClick={() => invoke('open_in_browser', { url: latestVersion.link })}>
{latestVersion?.tag_name}
</a>{' '}
is now available!
</>
),
})
setTimeout(() => {
this.setState({
notification: null,
})
}, 6000)
}
// Period check to only show progress bar when downloading files
setInterval(() => {
this.setState({
isDownloading: this.props.downloadHandler.getDownloads().filter((d) => d.status !== 'finished')?.length > 0,
})
}, 1000)
}
async openExtrasMenu(playGame: () => void) {
this.setState({
extrasOpen: true,
playGame,
})
}
async componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
const game_path = await getConfigOption('game_install_path')
// Check if game exists at set location
const game_exists: boolean = (await invoke('dir_exists', {
path: game_path,
})) as boolean
// Set no game path so the user understands it doesn't exist there
if (!game_exists) {
setConfigOption('game_install_path', '')
}
//if previous state is not equal the current one - update the game_install_path to be the current game path
if (prevState.game_install_path != game_path) {
this.setState({
game_install_path: game_path,
})
this.state.game_install_path === ''
? this.setState({ isGamePathSet: false })
: this.setState({ isGamePathSet: true })
}
}
render() {
return (
<>
<TopBar>
<div
id="settingsBtn"
onClick={() => this.setState({ optionsOpen: !this.state.optionsOpen })}
className="TopButton"
>
<img src={cogBtn} alt="settings" />
</div>
<div
id="downloadsBtn"
className="TopButton"
onClick={() => this.setState({ downloadsOpen: !this.state.downloadsOpen })}
>
<img src={downBtn} alt="downloads" />
</div>
{this.state.migotoSet && (
<div
id="modsBtn"
onClick={() => {
// Create and dispatch a custom "openMods" event
const event = new CustomEvent('changePage', { detail: 'modding' })
window.dispatchEvent(event)
}}
className="TopButton"
>
<img src={wrenchBtn} alt="mods" />
</div>
)}
{/* <div id="gameBtn" className="TopButton" onClick={() => this.setState({ gameDownloadsOpen: !this.state.gameDownloadsOpen })}>
<img src={gameBtn} alt="game" />
</div> */}
</TopBar>
<Notification show={!!this.state.notification}>{this.state.notification}</Notification>
{this.state.isGamePathSet ? <></> : <GamePathNotify />}
<RightBar />
<NewsSection />
{
// Extras section
this.state.extrasOpen && (
<ExtrasMenu closeFn={() => this.setState({ extrasOpen: false })} playGame={this.state.playGame}>
Yo
</ExtrasMenu>
)
}
{
// Mini downloads section
this.state.miniDownloadsOpen ? (
<div className="MiniDownloads" id="miniDownloadContainer">
<MiniDialog
title="Downloads"
closeFn={() => {
this.setState({ miniDownloadsOpen: false })
}}
>
<DownloadList downloadManager={this.props.downloadHandler} />
</MiniDialog>
<div className="arrow-down"></div>
</div>
) : null
}
{
// Download menu
this.state.downloadsOpen ? (
<Downloads
downloadManager={this.props.downloadHandler}
closeFn={() => this.setState({ downloadsOpen: false })}
/>
) : null
}
{
// Options menu
this.state.optionsOpen ? (
<Options
downloadManager={this.props.downloadHandler}
closeFn={() => this.setState({ optionsOpen: !this.state.optionsOpen })}
/>
) : null
}
{
// Game downloads menu
this.state.gameDownloadsOpen ? (
<Game
downloadManager={this.props.downloadHandler}
closeFn={() => this.setState({ gameDownloadsOpen: false })}
/>
) : null
}
<div className="BottomSection" id="bottomSectionContainer">
<ServerLaunchSection openExtras={this.openExtrasMenu} />
<div
id="DownloadProgress"
onClick={() => this.setState({ miniDownloadsOpen: !this.state.miniDownloadsOpen })}
>
{this.state.isDownloading ? <MainProgressBar downloadManager={this.props.downloadHandler} /> : null}
</div>
</div>
</>
)
}
}

View File

@@ -1,41 +0,0 @@
.Mods {
backdrop-filter: blur(10px);
height: 80%;
width: 100%;
}
/* Stuff for the top bar progress bar */
.TopDownloads {
position: absolute;
top: 10px;
left: 35.5%;
width: 30%;
}
.TopDownloads .ProgressBar {
width: 100%;
height: 10px;
}
.ModMenu {
width: 40%;
}
.ModMenu .BigButton {
font-size: 16px;
padding: 6px 30px;
}
.ModDownloadList {
width: 80%;
}
.ModDownloadItem {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 10px;
}

View File

@@ -1,235 +0,0 @@
import { invoke } from '@tauri-apps/api'
import React from 'react'
import DownloadHandler from '../utils/download'
import { getModDownload, ModData } from '../utils/gamebanana'
import { getModsFolder } from '../utils/mods'
import { unzip } from '../utils/zipUtils'
import ProgressBar from './components/common/MainProgressBar'
import { ModHeader } from './components/mods/ModHeader'
import { ModList } from './components/mods/ModList'
import TopBar from './components/TopBar'
import './Mods.css'
import Back from '../resources/icons/back.svg'
import Menu from './components/menu/Menu'
import BigButton from './components/common/BigButton'
import Tr from '../utils/language'
import { ModPages } from './components/mods/ModPages'
import TextInput from './components/common/TextInput'
interface IProps {
downloadHandler: DownloadHandler
}
interface IState {
isDownloading: boolean
category: string
downloadList: { name: string; url: string; mod: ModData }[] | null
page: number
search: string
}
const pages = [
{
name: -1,
title: '<',
},
{
name: 1,
title: '>',
},
]
const headers = [
{
name: 'ripe',
title: 'Hot',
},
{
name: 'new',
title: 'New',
},
{
name: 'installed',
title: 'Installed',
},
]
/**
* Mods currently install into folder labelled with their GB ID
*
* @TODO Categorizaiton/sorting (by likes, views, etc)
*/
export class Mods extends React.Component<IProps, IState> {
timeout: number
constructor(props: IProps) {
super(props)
this.timeout = 0
this.state = {
isDownloading: false,
category: '',
downloadList: null,
page: 1,
search: '',
}
this.setCategory = this.setCategory.bind(this)
this.addDownload = this.addDownload.bind(this)
this.setPage = this.setPage.bind(this)
}
async addDownload(mod: ModData) {
const dlLinks = await getModDownload(String(mod.id))
// Not gonna bother allowing sorting for now
const firstLink = dlLinks[0].downloadUrl
const fileExt = firstLink.split('.').pop()
const modName = `${mod.id}.${fileExt}`
if (dlLinks.length === 0) return
// If there is one download we don't care to choose
if (dlLinks.length === 1) {
this.downloadMod(firstLink, modName, mod)
return
}
this.setState({
downloadList: dlLinks.map((link) => ({
name: link.filename,
url: link.downloadUrl,
mod: mod,
})),
})
}
async downloadMod(link: string, modName: string, mod: ModData) {
const modFolder = await getModsFolder()
const path = `${modFolder}/${modName}`
if (!modFolder) return
this.props.downloadHandler.addDownload(link, path, async () => {
const unzipRes = await unzip(path, modFolder, false, true)
// Write a modinfo.json file
invoke('write_file', {
path: `${unzipRes.new_folder}/modinfo.json`,
contents: JSON.stringify(mod),
})
})
}
async setCategory(value: string) {
this.setState(
{
category: value,
},
this.forceUpdate
)
}
async setPage(value: number) {
const current = this.state.page
if (current + value == 0) return
this.setState(
{
page: current + value,
},
this.forceUpdate
)
}
async setSearch(text: string) {
if (this.timeout) clearTimeout(this.timeout)
this.timeout = window.setTimeout(() => {
this.setState(
{
search: text,
},
this.forceUpdate
)
}, 500)
}
render() {
return (
<div className="Mods">
<TopBar>
<div
id="backbtn"
className="TopButton"
onClick={() => {
// Create and dispatch a custom "changePage" event
const event = new CustomEvent('changePage', { detail: 'main' })
window.dispatchEvent(event)
}}
>
<img src={Back} alt="back" />
</div>
</TopBar>
{this.state.downloadList && (
<Menu className="ModMenu" heading="Links" closeFn={() => this.setState({ downloadList: null })}>
<div className="ModDownloadList">
{this.state.downloadList.map((o) => {
return (
<div className="ModDownloadItem" key={o.name}>
<div className="ModDownloadName">{o.name}</div>
<BigButton
id={o.url}
onClick={() => {
const fileExt = o.url.split('.').pop()
const modName = `${o.mod.id}.${fileExt}`
this.downloadMod(o.url, modName, o.mod)
this.setState({
downloadList: null,
})
}}
>
<Tr text="components.download" />
</BigButton>
</div>
)
})}
</div>
</Menu>
)}
<div className="TopDownloads">
<ProgressBar downloadManager={this.props.downloadHandler} withStats={false} />
</div>
<ModHeader onChange={this.setCategory} headers={headers} defaultHeader={'ripe'} />
{this.state.category != 'installed' && (
<>
<div className="ModPagesPage">
<TextInput
id="search"
key="search"
placeholder={this.state.page.toString()}
onChange={(text: string) => {
this.setSearch(text)
}}
initalValue={''}
/>
</div>
<ModPages onClick={this.setPage} headers={pages} defaultHeader={this.state.page} />
</>
)}
<ModList
key={`${this.state.category}_${this.state.page}_${this.state.search}`}
mode={this.state.category}
addDownload={this.addDownload}
page={this.state.page}
search={this.state.search}
/>
</div>
)
}
}

View File

@@ -1,35 +0,0 @@
.MiniDialog {
position: fixed;
z-index: 99;
/* Len and width */
height: 30%;
width: 30%;
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.2);
}
.MiniDialogTop {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 20px;
}
.MiniDialogTop img {
height: 100%;
}
.MiniDialogTop:hover {
cursor: pointer;
}
.MiniDialog .ProgressText {
color: #000;
}

View File

@@ -1,49 +0,0 @@
import React from 'react'
import Close from '../../resources/icons/close.svg'
import './MiniDialog.css'
interface IProps {
children: React.ReactNode[] | React.ReactNode
title?: string
closeable?: boolean
closeFn: () => void
}
export default class MiniDialog extends React.Component<IProps, never> {
constructor(props: IProps) {
super(props)
}
componentDidMount() {
document.addEventListener('mousedown', (evt) => {
const tgt = evt.target as HTMLElement
const isInside = tgt.closest('.MiniDialog') !== null
if (!isInside) {
this.props.closeFn()
}
})
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.props.closeFn)
}
render() {
return (
<div className="MiniDialog" id="miniDialogContainer">
{this.props.closeable !== undefined && this.props.closeable ? (
<div className="MiniDialogTop" id="miniDialogContainerTop" onClick={this.props.closeFn}>
<span>{this.props?.title}</span>
<img src={Close} className="MiniDialogClose" id="miniDialogButtonClose" />
</div>
) : null}
<div className="MiniDialogInner" id="miniDialogContent">
{this.props.children}
</div>
</div>
)
}
}

View File

@@ -1,61 +0,0 @@
.RightBar {
position: absolute;
transform: translate(0%, 0%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
height: 100vh;
width: 70px;
right: 0%;
z-index: 99;
background-color: rgba(0, 0, 0, 0.25);
}
.RightBarInner > div {
margin: 30px 0;
}
.RightBar img {
height: 30px;
filter: invert(100%) sepia(0%) saturate(11%) hue-rotate(227deg) brightness(103%) contrast(105%);
transition: filter 0.2s ease-in-out;
}
@media (max-height: 580px) {
.RightBar {
height: calc(100vh - 180px);
}
}
@media (max-height: 500px) {
.RightBar {
height: calc(100vh - 170px);
}
}
.BarImg {
transition: 0.2s;
border-radius: 50%;
width: 40px;
height: 40px;
background-color: rgba(0, 0, 0, 0.685);
display: flex;
justify-content: center;
align-items: center;
border: 2px solid transparent;
}
.BarImg:hover {
cursor: pointer;
border: 2px solid #ffc920;
}
.BarImg:hover img {
transition: 0.2s;
filter: invert(72%) sepia(68%) saturate(777%) hue-rotate(341deg) brightness(113%) contrast(101%);
}

View File

@@ -1,31 +0,0 @@
import { invoke } from '@tauri-apps/api'
import React from 'react'
import Discord from '../../resources/icons/discord.svg'
import Github from '../../resources/icons/github.svg'
import './RightBar.css'
const DISCORD = 'https://discord.gg/T5vZU6UyeG'
const GITHUB = 'https://github.com/Grasscutters/Grasscutter'
export default class RightBar extends React.Component {
openInBrowser(url: string) {
invoke('open_in_browser', { url })
}
render() {
return (
<div className="RightBar" id="rightBarContainer">
<div className="RightBarInner" id="rightBarContent">
<div className="BarDiscord BarImg" id="rightBarButtonDiscord" onClick={() => this.openInBrowser(DISCORD)}>
<img src={Discord} />
</div>
<div className="BarGithub BarImg" id="rightBarButtonGithub" onClick={() => this.openInBrowser(GITHUB)}>
<img src={Github} />
</div>
</div>
</div>
)
}
}

View File

@@ -1,132 +0,0 @@
#playButton {
display: flex;
flex-direction: column;
justify-content: space-evenly;
position: absolute;
transform: translate(0%, -50%);
right: 10%;
top: 50%;
width: 32%;
height: 80%;
min-width: 357px;
}
#playButton > div {
margin-bottom: 6px;
}
#playButton .BigButton {
height: 100%;
box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.2);
font-size: 24px;
font-weight: 500;
}
#serverControls {
color: white;
text-shadow: 1px 1px 8px black;
}
.BottomSection .CheckboxDisplay {
border-color: #c5c5c5;
background: #fff;
}
.BottomSection .CheckboxDisplay {
margin-right: 6px;
box-shadow: 0 0 5px 4px rgba(0, 0, 0, 0.2);
}
.BottomSection .Checkbox label {
color: #fff;
font-weight: bold;
text-shadow: #222222 1px 0 10px;
}
.BottomSection .Checkbox label span {
padding-left: 4px;
}
.ServerConfig {
margin-bottom: 6px;
}
.ServerConfig .Checkbox {
display: inline-grid;
vertical-align: middle;
margin-left: 8px;
}
.ServerLaunchButtons {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
height: 100%;
max-height: 60px;
}
.ServerLaunchButtons .BigButton {
margin-right: 8px;
}
#officialPlay {
max-width: 60%;
flex-grow: 1;
}
#akebiLaunch,
#serverLaunch {
width: 5%;
}
#ExtrasMenuButton {
width: 5%;
padding: 0 20px;
}
.ExtrasIcon,
.ServerIcon {
height: 20px;
filter: invert(28%) sepia(28%) saturate(1141%) hue-rotate(352deg) brightness(96%) contrast(88%);
}
.ServerConfig input {
padding: 6px;
border-radius: 6px;
height: 18px;
box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.2);
}
.ServerConfig .TextInputWrapper {
display: inline-block;
}
#ip {
margin-right: 6px;
}
#port {
width: 100%;
}
@media (max-width: 1040px) {
#playButton {
right: 5%;
}
}
@media (max-width: 870px) {
#playButton {
min-width: 235px;
}
#officialPlay {
width: 40%;
}
}

View File

@@ -1,376 +0,0 @@
import React from 'react'
import Checkbox from './common/Checkbox'
import BigButton from './common/BigButton'
import TextInput from './common/TextInput'
import HelpButton from './common/HelpButton'
import { getConfig, saveConfig, setConfigOption } from '../../utils/configuration'
import { translate } from '../../utils/language'
import { invoke } from '@tauri-apps/api/tauri'
import Server from '../../resources/icons/server.svg'
import Plus from '../../resources/icons/plus.svg'
import './ServerLaunchSection.css'
import { dataDir } from '@tauri-apps/api/path'
import { GrasscutterElevation } from './menu/Options'
import { getGameExecutable, getGameVersion, getGrasscutterJar } from '../../utils/game'
import { patchGame, unpatchGame } from '../../utils/rsa'
import { listen } from '@tauri-apps/api/event'
import { confirm } from '@tauri-apps/api/dialog'
interface IProps {
openExtras: (playGame: () => void) => void
}
interface IState {
grasscutterEnabled: boolean
buttonLabel: string
checkboxLabel: string
ip: string
port: string
launchServer: (proc_name?: string) => void
ipPlaceholder: string
portPlaceholder: string
httpsLabel: string
httpsEnabled: boolean
swag: boolean
akebiSet: boolean
migotoSet: boolean
unElevated: boolean
}
export default class ServerLaunchSection extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
grasscutterEnabled: false,
buttonLabel: '',
checkboxLabel: '',
ip: '',
port: '',
ipPlaceholder: '',
portPlaceholder: '',
httpsLabel: '',
httpsEnabled: false,
launchServer: () => {
alert('Error launching grasscutter')
},
swag: false,
akebiSet: false,
migotoSet: false,
unElevated: false,
}
this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
this.playGame = this.playGame.bind(this)
this.setIp = this.setIp.bind(this)
this.setPort = this.setPort.bind(this)
this.toggleHttps = this.toggleHttps.bind(this)
this.launchServer = this.launchServer.bind(this)
listen('start_grasscutter', async () => {
this.launchServer()
})
}
async componentDidMount() {
const config = await getConfig()
this.setState({
grasscutterEnabled: config.toggle_grasscutter || false,
buttonLabel: await translate('main.launch_button'),
checkboxLabel: await translate('main.gc_enable'),
ip: config.last_ip || '',
port: config.last_port || '',
ipPlaceholder: await translate('main.ip_placeholder'),
portPlaceholder: await translate('help.port_placeholder'),
httpsLabel: await translate('main.https_enable'),
httpsEnabled: config.https_enabled || false,
swag: config.swag_mode || false,
akebiSet: config.akebi_path !== '',
migotoSet: config.migoto_path !== '',
unElevated: config.un_elevated || false,
})
}
async toggleGrasscutter() {
const config = await getConfig()
config.toggle_grasscutter = !config.toggle_grasscutter
// Set state as well
this.setState({
grasscutterEnabled: config.toggle_grasscutter,
})
await saveConfig(config)
}
async playGame(exe?: string, proc_name?: string) {
const config = await getConfig()
if (!(await getGameExecutable())) {
alert('Game executable not set!')
return
}
// Check for HTTPS on local
if (this.state.httpsEnabled) {
if (this.state.ip == 'localhost') {
if (
await confirm(
"Oops! HTTPS is enabled but you're connecting to localhost! \nHTTPS MUST be disabled for localhost. \n\nWould you like to disable HTTPS and continue?",
{ title: 'WARNING!!', type: 'warning' }
)
) {
this.toggleHttps()
} else {
if (!(await confirm('You have chosen to keep HTTPS enabled! \n\nYOU WILL ERROR ON LOGIN.'))) {
return
}
}
}
}
// Connect to proxy
if (config.toggle_grasscutter) {
const game_exe = await getGameExecutable()
const patchable = game_exe?.toLowerCase().includes('genshin' || 'yuanshen')
if (config.patch_rsa && patchable) {
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 < 9) {
alert('Game version is too old for RSA patching. Please disable RSA patching in the settings and try again.')
return
}
if (gameVersion?.major == 3 && gameVersion?.minor < 1) {
alert('Game version is too old for RSA patching. Please disable RSA patching in the settings and try again.')
return
}
const patched = await patchGame()
if (!patched) {
alert('Could not patch! Try launching again, or patching manually.')
return
}
}
// Save last connected server and port
await setConfigOption('last_ip', this.state.ip)
await setConfigOption('last_port', this.state.port)
await invoke('enable_process_watcher', {
process: proc_name || game_exe,
})
if (config.use_internal_proxy) {
// Set IP
await invoke('set_proxy_addr', {
addr: (this.state.httpsEnabled ? 'https' : 'http') + '://' + this.state.ip + ':' + this.state.port,
})
// Connect to proxy
await invoke('connect', { port: 8365, certificatePath: (await dataDir()) + 'cultivation/ca' })
}
// Open server as well if the options are set
if (config.grasscutter_with_game) {
this.launchServer()
}
} else {
await unpatchGame()
}
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', {
path: exe || config.game_install_path,
})
if (gameExists)
if (config.un_elevated) {
await invoke('run_un_elevated', {
path: config.game_install_path,
args: config.launch_args,
})
} else {
if (config.launch_args.length < 1) {
// Run relative when there are no args
await invoke('run_program_relative', { path: exe || config.game_install_path })
} else {
// Run directly when there are args
await invoke('run_program', {
path: exe || config.game_install_path,
args: config.launch_args,
})
}
}
else alert('Game not found! At: ' + (exe || config.game_install_path))
}
async launchServer(proc_name?: string) {
if (await invoke('is_grasscutter_running')) {
alert('Grasscutter already running!')
return
}
const config = await getConfig()
if (!config.grasscutter_path) return alert('Grasscutter not installed or set!')
const grasscutter_jar = await getGrasscutterJar()
await invoke('enable_grasscutter_watcher', {
process: proc_name || grasscutter_jar,
})
if (config.auto_mongodb) {
// Check if MongoDB is running and start it if not
invoke('service_status', { service: 'MongoDB' })
}
let jarFolder = config.grasscutter_path
if (jarFolder.includes('/')) {
jarFolder = jarFolder.substring(0, config.grasscutter_path.lastIndexOf('/'))
} else {
jarFolder = jarFolder.substring(0, config.grasscutter_path.lastIndexOf('\\'))
}
let cmd = 'run_jar'
if ((await invoke('get_platform')) === 'linux') {
switch (config.grasscutter_elevation) {
case GrasscutterElevation.None:
break
case GrasscutterElevation.Capability:
await invoke('jvm_add_cap', {
javaPath: config.java_path,
})
break
case GrasscutterElevation.Root:
cmd = 'run_jar_root'
break
default:
console.error('Invalid grasscutter_elevation')
break
}
}
// Launch the jar
await invoke(cmd, {
path: config.grasscutter_path,
executeIn: jarFolder,
javaPath: config.java_path || '',
})
}
setIp(text: string) {
this.setState({
ip: text,
})
}
setPort(text: string) {
this.setState({
port: text,
})
}
async toggleHttps() {
const config = await getConfig()
config.https_enabled = !config.https_enabled
// Set state as well
this.setState({
httpsEnabled: config.https_enabled,
})
await saveConfig(config)
}
render() {
return (
<div id="playButton">
<div id="serverControls">
<Checkbox
id="enableGC"
label={this.state.checkboxLabel}
onChange={this.toggleGrasscutter}
checked={this.state.grasscutterEnabled}
/>
</div>
{this.state.grasscutterEnabled && (
<div>
<div className="ServerConfig" id="serverConfigContainer">
<TextInput
id="ip"
key="ip"
placeholder={this.state.ipPlaceholder}
onChange={this.setIp}
initalValue={this.state.ip}
/>
<TextInput
style={{
width: '10%',
}}
id="port"
key="port"
placeholder={this.state.portPlaceholder}
onChange={this.setPort}
initalValue={this.state.port}
/>
<HelpButton contents={'help.port_help_text'} />
<Checkbox
id="httpsEnable"
label={this.state.httpsLabel}
onChange={this.toggleHttps}
checked={this.state.httpsEnabled}
/>
</div>
</div>
)}
<div className="ServerLaunchButtons" id="serverLaunchContainer">
<BigButton onClick={this.playGame} id="officialPlay">
{this.state.buttonLabel}
</BigButton>
{this.state.swag && (
<BigButton onClick={() => this.props.openExtras(this.playGame)} id="ExtrasMenuButton">
<img className="ExtrasIcon" id="extrasIcon" src={Plus} />
</BigButton>
)}
<BigButton onClick={this.launchServer} id="serverLaunch">
<img className="ServerIcon" id="serverLaunchIcon" src={Server} />
</BigButton>
</div>
</div>
)
}
}

View File

@@ -1,54 +0,0 @@
.TopBar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
height: 30px;
background-color: #141414;
z-index: 1;
color: #fff;
}
.TopBtns {
display: flex;
flex-direction: row-reverse;
align-items: center;
height: 100%;
}
#title {
margin: 0px 12px;
color: #c5c5c5;
}
#version {
margin: 0px 6px;
color: #434343;
}
#unassumingButton {
font-weight: bold;
margin: 0px 6px;
color: #141414;
transition: color 0.2s ease-in-out;
}
#unassumingButton:hover {
color: #434343;
}
#unassumingButton.spin {
color: #fff;
animation: spin 0.5s ease-in-out;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,127 +0,0 @@
import React from 'react'
import { app, invoke } from '@tauri-apps/api'
import { appWindow } from '@tauri-apps/api/window'
import { getConfig, setConfigOption } from '../../utils/configuration'
import Tr from '../../utils/language'
import { confirm } from '@tauri-apps/api/dialog'
import './TopBar.css'
import closeIcon from '../../resources/icons/close.svg'
import minIcon from '../../resources/icons/min.svg'
import { unpatchGame } from '../../utils/rsa'
interface IProps {
children?: React.ReactNode | React.ReactNode[]
}
interface IState {
version: string
clicks: number
intv: NodeJS.Timeout | null
}
export default class TopBar extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
version: '0.0.0',
clicks: 0,
intv: null,
}
this.activateClick = this.activateClick.bind(this)
}
async componentDidMount() {
const version = await app.getVersion()
this.setState({ version })
}
async handleClose() {
if (await invoke('is_game_running')) {
const confirmed = await confirm(
'Game is running. You WILL NOT be unpatched. Would you like to exit?',
'WARNING!!'
)
if (!confirmed) {
return
}
}
await invoke('disconnect')
unpatchGame()
appWindow.close()
}
handleMinimize() {
appWindow.minimize()
}
async activateClick() {
const config = await getConfig()
// They already got it, no need to reactivate
if (config.swag_mode) return
if (this.state.clicks === 2) {
setTimeout(() => {
// Gotta clear it so it goes back to regular colors
this.setState({
clicks: 0,
})
}, 600)
// Activate... SWAG MODE
await setConfigOption('swag_mode', true)
// Reload the window
window.location.reload()
return
}
if (this.state.clicks < 3) {
this.setState({
clicks: this.state.clicks + 1,
intv: setTimeout(() => this.setState({ clicks: 0 }), 1500),
})
return
}
}
render() {
return (
<div className="TopBar" id="topBarContainer" data-tauri-drag-region>
<div id="title">
<span data-tauri-drag-region>
<Tr text="main.title" />
</span>
<span data-tauri-drag-region id="version">
{this.state?.version}
</span>
</div>
{/**
* HEY YOU
*
* If you're looking at the source code to find the swag mode thing, that's okay! If you're not, move along...
* Just do me a favor and don't go telling everyone about how you found it. If you are just helping someone who
* for some reason needs it, that's fine, but not EVERYONE needs it, which is why it exists in the first place.
*/}
<div id="unassumingButton" className={this.state.clicks === 2 ? 'spin' : ''} onClick={this.activateClick}>
?
</div>
<div className="TopBtns" id="topBarButtonContainer">
<div id="closeBtn" onClick={this.handleClose} className="TopButton">
<img src={closeIcon} alt="close" />
</div>
<div id="minBtn" onClick={this.handleMinimize} className="TopButton">
<img src={minIcon} alt="minimize" />
</div>
{this.props.children}
</div>
</div>
)
}
}

View File

@@ -1,30 +0,0 @@
.BigButton {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0 30px;
border-radius: 5px;
border: none;
background: linear-gradient(#ffcf0d, #fec004);
color: #704a1d;
font-weight: bold;
font-size: 20px;
}
.BigButton:hover {
cursor: pointer;
background: linear-gradient(#fdd841, #ffc517);
}
.BigButton.disabled {
background: linear-gradient(#9c9c9c, #949494);
color: rgb(226, 226, 226);
cursor: default;
}
.BigButton.disabled:hover {
background: linear-gradient(#949494, #9c9c9c);
}

View File

@@ -1,49 +0,0 @@
import React from 'react'
import './BigButton.css'
interface IProps {
children: React.ReactNode
onClick: () => unknown
id: string
disabled?: boolean
}
interface IState {
disabled?: boolean
}
export default class BigButton extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
disabled: this.props.disabled,
}
this.handleClick = this.handleClick.bind(this)
}
static getDerivedStateFromProps(props: IProps, _state: IState) {
return {
disabled: props.disabled,
}
}
handleClick() {
if (this.state.disabled) return
this.props.onClick()
}
render() {
return (
<div
className={'BigButton ' + (this.state.disabled ? 'disabled' : '')}
onClick={this.handleClick}
id={this.props.id}
>
<div className="BigButtonText">{this.props.children}</div>
</div>
)
}
}

View File

@@ -1,26 +0,0 @@
.Checkbox input[type='checkbox'] {
display: none;
}
.CheckboxDisplay {
height: 20px;
width: 20px;
border: 2px solid #cecece;
border-radius: 4px;
}
.CheckboxDisplay:hover {
cursor: pointer;
border-color: #aaaaaa;
}
.CheckboxDisplay img {
height: 100%;
filter: invert(60%);
}
.Checkbox label {
display: flex;
flex-direction: row;
}

View File

@@ -1,52 +0,0 @@
import React from 'react'
import checkmark from '../../../resources/icons/check.svg'
import './Checkbox.css'
interface IProps {
label?: string
checked: boolean
onChange: () => void
id?: string
}
interface IState {
checked: boolean
}
export default class Checkbox extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
checked: props.checked,
}
}
static getDerivedStateFromProps(props: IProps, state: IState) {
if (props.checked !== state.checked) {
return {
checked: props.checked,
}
}
return { checked: props.checked }
}
handleChange = () => {
this.setState({ checked: !this.state.checked })
this.props.onChange()
}
render() {
return (
<div className="Checkbox">
<input type="checkbox" id={this.props.id} checked={this.state.checked} onChange={this.handleChange} />
<label htmlFor={this.props.id}>
<div className="CheckboxDisplay">{this.state.checked ? <img src={checkmark} alt="Checkmark" /> : null}</div>
<span>{this.props.label || ''}</span>
</label>
</div>
)
}
}

View File

@@ -1,27 +0,0 @@
.DirInput {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.DirInput input {
text-overflow: ellipsis;
}
.FileSelectIcon {
height: 20px;
margin-left: 10px;
filter: invert(99%) sepia(0%) saturate(1188%) hue-rotate(186deg) brightness(97%) contrast(67%);
transition: filter 0.1s ease-in-out;
}
.FileSelectIcon:hover {
cursor: pointer;
filter: invert(73%) sepia(0%) saturate(380%) hue-rotate(224deg) brightness(94%) contrast(90%);
}
.FileSelectIcon img {
height: 100%;
}

View File

@@ -1,109 +0,0 @@
import React from 'react'
import { open } from '@tauri-apps/api/dialog'
import { translate } from '../../../utils/language'
import TextInput from './TextInput'
import File from '../../../resources/icons/folder.svg'
import './DirInput.css'
interface IProps {
value?: string
clearable?: boolean
onChange?: (value: string) => void
extensions?: string[]
readonly?: boolean
placeholder?: string
folder?: boolean
customClearBehaviour?: () => void
openFolder?: string
}
interface IState {
value: string
placeholder: string
folder: boolean
}
export default class DirInput extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
value: props.value || '',
placeholder: this.props.placeholder || 'Select file or folder...',
folder: this.props.folder || false,
}
this.handleIconClick = this.handleIconClick.bind(this)
}
static getDerivedStateFromProps(props: IProps, state: IState) {
const newState = state
if (props.value && state.value === '') {
newState.value = props.value || ''
}
if (props.placeholder) {
newState.placeholder = props.placeholder
}
return newState
}
async componentDidMount() {
if (!this.props.placeholder) {
const translation = await translate('components.select_file')
this.setState({
placeholder: translation,
})
}
}
async handleIconClick() {
let path
if (this.state.folder) {
path = await open({
directory: true,
})
} else {
path = await open({
filters: [{ name: 'Files', extensions: this.props.extensions || ['*'] }],
defaultPath: this.props.openFolder,
})
}
if (Array.isArray(path)) path = path[0]
if (!path) return
this.setState({
value: path,
})
if (this.props.onChange) this.props.onChange(path)
}
render() {
return (
<div className="DirInput">
<TextInput
value={this.state.value}
placeholder={this.state.placeholder}
clearable={this.props.clearable !== undefined ? this.props.clearable : true}
readOnly={this.props.readonly !== undefined ? this.props.readonly : true}
onChange={(text: string) => {
this.setState({ value: text })
if (this.props.onChange) this.props.onChange(text)
this.forceUpdate()
}}
customClearBehaviour={this.props.customClearBehaviour}
/>
<div className="FileSelectIcon" onClick={this.handleIconClick}>
<img src={File} />
</div>
</div>
)
}
}

View File

@@ -1,7 +0,0 @@
.DownloadList {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
padding: 10px;
}

View File

@@ -1,29 +0,0 @@
import React from 'react'
import DownloadHandler from '../../../utils/download'
import DownloadSection from './DownloadSection'
import './DownloadList.css'
interface IProps {
downloadManager: DownloadHandler
}
export default class DownloadList extends React.Component<IProps, never> {
constructor(props: IProps) {
super(props)
}
render() {
const list = this.props.downloadManager.getDownloads().map((download) => {
return (
<DownloadSection
key={download.path}
downloadName={download.path}
downloadManager={this.props.downloadManager}
/>
)
})
return <div className="DownloadList">{list.length > 0 ? list : 'No downloads present'}</div>
}
}

View File

@@ -1,29 +0,0 @@
.DownloadSection span {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: 100%;
}
.DownloadSection {
overflow: hidden;
width: 100%;
margin: 6px 0px;
}
.DownloadTitle {
display: flex;
flex-direction: row;
}
.DownloadPath {
text-overflow: ellipsis;
width: 250px;
overflow: hidden;
}
.DownloadStatus {
text-align: right;
}

View File

@@ -1,35 +0,0 @@
import React from 'react'
import DownloadHandler from '../../../utils/download'
import ProgressBar from './ProgressBar'
import './DownloadSection.css'
interface IProps {
downloadManager: DownloadHandler
downloadName: string
}
export default class DownloadSection extends React.Component<IProps, never> {
constructor(props: IProps) {
super(props)
}
getFilenameFromPath() {
const name = this.props.downloadName.replace(/\\/g, '/')
return name.split('/').pop()
}
render() {
return (
<div className="DownloadSection">
<div className="DownloadTitle">
<div className="DownloadPath">{this.getFilenameFromPath()}</div>
<div className="DownloadStatus"> - {this.props.downloadManager.getDownloadSize(this.props.downloadName)}</div>
</div>
<div className="DownloadSectionInner">
<ProgressBar path={this.props.downloadName} downloadManager={this.props.downloadManager} />
</div>
</div>
)
}
}

View File

@@ -1,33 +0,0 @@
.HelpSection {
display: inline-block;
margin-left: 20px;
vertical-align: middle;
}
.HelpButton {
height: 20px;
filter: drop-shadow(0px 0px 5px rgb(0 0 0 / 20%));
}
.HelpButton:hover {
cursor: pointer;
}
.HelpButton img {
height: 100%;
filter: invert(100%) sepia(2%) saturate(201%) hue-rotate(47deg) brightness(117%) contrast(100%);
}
.HelpContents {
text-align: center;
position: relative;
}
.HelpContents .MiniDialog {
position: absolute;
bottom: 40px;
right: -450%;
width: 200px;
height: 120px;
}

View File

@@ -1,33 +0,0 @@
import React from 'react'
import './HelpButton.css'
import Help from '../../../resources/icons/help.svg'
import { translate } from '../../../utils/language'
interface IProps {
children?: React.ReactNode[] | React.ReactNode
contents?: string
id?: string
}
export default class HelpButton extends React.Component<IProps, never> {
constructor(props: IProps) {
super(props)
this.showAlert = this.showAlert.bind(this)
}
async showAlert() {
if (this.props.contents) alert(await translate(this.props.contents))
}
render() {
return (
<div className="HelpSection">
<div className="HelpButton" onClick={this.showAlert}>
<img src={Help} />
</div>
</div>
)
}
}

View File

@@ -1,84 +0,0 @@
import React from 'react'
import DownloadHandler from '../../../utils/download'
import Tr from '../../../utils/language'
import './ProgressBar.css'
interface IProps {
downloadManager: DownloadHandler
withStats?: boolean
}
interface IState {
average: number
files: number
extracting: number
total: number
speed: string
}
/**
* This component differes because it averages all downloads together
*/
export default class ProgressBar extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
const { average, files, extracting, totalSize } = this.props.downloadManager.getTotalAverage()
this.state = {
average,
files,
extracting,
total: totalSize,
speed: '0 B/s',
}
}
componentDidMount() {
// Periodically check the progress of passed file path
setInterval(() => {
const prog = this.props.downloadManager.getTotalAverage()
this.setState({
average: prog?.average || 0,
files: prog?.files,
extracting: prog?.extracting,
total: prog?.totalSize || 0,
speed: prog?.speed || '0 B/s',
})
}, 200)
}
render() {
return (
<div className="MainProgressBarWrapper">
<div className="ProgressBar">
<div
className="InnerProgress"
style={{
width: `${(() => {
// Handles no files downloading
if (this.state.files === 0 || this.state.average >= 100) {
return '100'
}
if (this.state.total <= 0) {
return '0'
}
return this.state.average
})()}%`,
}}
></div>
</div>
{(this.props.withStats === undefined || this.props.withStats) && (
<div className="MainProgressText">
<Tr text="main.files_downloading" /> {this.state.files} ({this.state.speed})
<br />
<Tr text="main.files_extracting" /> {this.state.extracting}
</div>
)}
</div>
)
}
}

View File

@@ -1,24 +0,0 @@
.Notification {
position: absolute;
/* Default styles, changed when showing notif */
top: -100%;
right: 10%;
padding: 20px;
border-radius: 4px;
border: 1px solid #ffc61e;
background-color: #fff;
color: #000;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.2s ease;
}
.NotifShow {
top: 10%;
}

View File

@@ -1,22 +0,0 @@
import React from 'react'
import './Notification.css'
interface IProps {
children: React.ReactNode | null
show: boolean
}
export default class Notification extends React.Component<IProps> {
constructor(props: IProps) {
super(props)
}
render() {
return (
<div className={'Notification ' + (this.props.show ? 'NotifShow' : '')}>
<div className="NotificationMessage">{this.props.children}</div>
</div>
)
}
}

View File

@@ -1,95 +0,0 @@
.ProgressBar,
.InnerProgress {
border-radius: 4px;
}
.ProgressBarWrapper {
display: flex;
flex-direction: row;
align-items: center;
}
.ProgressBar {
height: 20px;
width: 100%;
background-color: rgba(204, 204, 204, 0.5);
color: #c5c5c5;
border: 1px solid #ccc;
}
.InnerProgress {
height: 100%;
background-color: #fff;
}
.MainProgressText,
.ProgressText {
color: #c5c5c5;
padding: 0px 10px;
width: 30%;
}
.MainProgressText {
width: 80%;
}
.MainProgressBarWrapper {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
color: #fff;
text-shadow: 1px 1px 14px black;
}
.MainProgressBarWrapper .MainProgressText {
font-size: 20px;
font-weight: bold;
color: #fff !important;
}
.MainProgressBarWrapper .ProgressBar {
box-shadow: 0px 0px 5px 4px rgb(0 0 0 / 20%);
}
.MainProgressBarWrapper:hover {
cursor: pointer;
}
.ProgressBarWrapper div {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.ProgressBarWrapper .InnerProgress {
background-color: #ffc61e;
}
.ProgressBarWrapper .ProgressBar {
border: none;
}
.DownloadControls {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-left: 10px;
}
.DownloadControls div {
height: 20px;
}
.DownloadControls div img {
height: 100%;
}
.downloadStop:hover {
cursor: pointer;
}

View File

@@ -1,92 +0,0 @@
import React from 'react'
import { capitalize } from '../../../utils/string'
import Stop from '../../../resources/icons/close.svg'
import './ProgressBar.css'
import DownloadHandler from '../../../utils/download'
import { translate } from '../../../utils/language'
interface IProps {
path: string
downloadManager: DownloadHandler
}
interface IState {
progress: number
status: string
total: number
}
export default class ProgressBar extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
progress: 0,
status: '',
total: this.props.downloadManager.getDownloadProgress(this.props.path)?.total || 0,
}
this.stopDownload = this.stopDownload.bind(this)
}
componentDidMount() {
// Periodically check the progress of passed file path
const intv = setInterval(async () => {
const prog = this.props.downloadManager.getDownloadProgress(this.props.path)
this.setState({
progress: prog?.progress || 0,
status: (await translate(`download_status.${prog?.status || 'stopped'}`)) || 'stopped',
total: prog?.total || 0,
})
if (this.state.status === 'finished' || this.state.status === 'error') {
// Ensure progress is 100%
clearInterval(intv)
}
}, 500)
}
stopDownload() {
this.props.downloadManager.stopDownload(this.props.path)
}
render() {
return (
<div className="ProgressBarWrapper">
<div
style={{
width: '80%',
}}
>
<div className="ProgressBar">
<div
className="InnerProgress"
style={{
width: `${(() => {
// Handles files with content-lengths of 0
if (this.state.status === 'finished') {
return '100'
}
if (this.state.total <= 0) {
return '0'
}
return (this.state.progress / this.state.total) * 100
})()}%`,
}}
></div>
</div>
<div className="DownloadControls">
<div onClick={this.stopDownload} className="downloadStop">
<img src={Stop}></img>
</div>
</div>
</div>
<div className="ProgressText">{capitalize(this.state.status) || 'Waiting'}</div>
</div>
)
}
}

View File

@@ -1,32 +0,0 @@
.SmallButtonSection {
display: inline-block;
margin-left: 20px;
vertical-align: middle;
}
.SmallButtonButton {
height: 20px;
filter: drop-shadow(0px 0px 5px rgb(0 0 0 / 20%));
}
.SmallButtonButton:hover {
cursor: pointer;
}
.SmallButtonButton img {
height: 100%;
}
.SmallButtonContents {
text-align: center;
position: relative;
}
.SmallButtonContents .MiniDialog {
position: absolute;
bottom: 40px;
right: -450%;
width: 200px;
height: 120px;
}

View File

@@ -1,39 +0,0 @@
import React from 'react'
import './SmallButton.css'
import Wrench from '../../../resources/icons/wrench.svg'
import { translate } from '../../../utils/language'
interface IProps {
children?: React.ReactNode[] | React.ReactNode
onClick: () => unknown
id?: string
contents?: string
}
export default class SmallButton extends React.Component<IProps> {
constructor(props: IProps) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
async showAlert() {
if (this.props.contents) alert(await translate(this.props.contents))
}
handleClick() {
this.props.onClick()
this.showAlert()
}
render() {
return (
<div className="SmallButtonSection">
<div className="SmallButtonButton" onClick={this.handleClick}>
<img src={Wrench} />
</div>
</div>
)
}
}

View File

@@ -1,31 +0,0 @@
.TextInput {
border: none;
border-bottom: 2px solid #cecece;
padding-right: 16px;
transition: border-bottom-color 0.1s ease-in-out;
}
.TextInput:focus {
outline: none;
border-bottom-color: #ffd326;
}
.TextClear {
height: 10px;
display: inline-block;
position: absolute;
right: 16%;
filter: invert(99%) sepia(0%) saturate(1188%) hue-rotate(186deg) brightness(97%) contrast(67%);
}
.TextClear:hover {
cursor: pointer;
filter: invert(73%) sepia(0%) saturate(380%) hue-rotate(224deg) brightness(94%) contrast(90%);
}
.TextInputClear {
height: 100%;
}

View File

@@ -1,77 +0,0 @@
import React from 'react'
import './TextInput.css'
import Close from '../../../resources/icons/close.svg'
interface IProps {
value?: string
initalValue?: string
placeholder?: string
onChange?: (value: string) => void
readOnly?: boolean
id?: string
clearable?: boolean
customClearBehaviour?: () => void
style?: React.CSSProperties
}
interface IState {
value: string
}
export default class TextInput extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
value: props.value || '',
}
}
async componentDidMount() {
if (this.props.initalValue) {
this.setState({
value: this.props.initalValue,
})
}
}
static getDerivedStateFromProps(props: IProps, state: IState) {
return { value: props.value || state.value }
}
render() {
return (
<div className="TextInputWrapper" style={this.props.style || {}}>
<input
id={this.props?.id}
readOnly={this.props.readOnly || false}
placeholder={this.props.placeholder || ''}
className="TextInput"
value={this.state.value}
onChange={(e) => {
this.setState({ value: e.target.value })
if (this.props.onChange) this.props.onChange(e.target.value)
}}
/>
{this.props.clearable ? (
<div
className="TextClear"
onClick={() => {
// Run custom behaviour first
if (this.props.customClearBehaviour) return this.props.customClearBehaviour()
this.setState({ value: '' })
if (this.props.onChange) this.props.onChange('')
this.forceUpdate()
}}
>
<img src={Close} className="TextInputClear" />
</div>
) : null}
</div>
)
}
}

View File

@@ -1,15 +0,0 @@
.Divider {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
width: 100%;
margin: 20px;
}
.DividerLine {
width: 60%;
border-top: 1px solid #ccc;
}

View File

@@ -1,13 +0,0 @@
import React from 'react'
import './Divider.css'
export default class Divider extends React.Component {
render() {
return (
<div className="Divider">
<div className="DividerLine"></div>
</div>
)
}
}

View File

@@ -1,41 +0,0 @@
.Downloads {
width: 40%;
}
.DownloadMenuSection {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
width: 90%;
min-height: 30px;
}
.DownloadValue {
height: 100%;
}
.HeaderText {
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
margin-bottom: 10px;
text-decoration-line: underline;
}
.DownloadValue .BigButton {
height: 100%;
min-height: 30px;
}
.DownloadValue .BigButtonText {
font-size: 12px;
}
.DownloadMenuSection .HelpButton img {
filter: none;
}

View File

@@ -1,369 +0,0 @@
import React from 'react'
import Menu from './Menu'
import Tr from '../../../utils/language'
import DownloadHandler from '../../../utils/download'
import { unzip } from '../../../utils/zipUtils'
import BigButton from '../common/BigButton'
import { dataDir } from '@tauri-apps/api/path'
import './Downloads.css'
import Divider from './Divider'
import { getConfigOption } from '../../../utils/configuration'
import { invoke } from '@tauri-apps/api'
import { listen } from '@tauri-apps/api/event'
import HelpButton from '../common/HelpButton'
const FULL_BUILD_DOWNLOAD = 'https://github.com/NotThorny/Grasscutter/releases/download/culti-aio/GrasscutterCulti.zip'
const FULL_QUEST_DOWNLOAD = 'https://github.com/NotThorny/Grasscutter/releases/download/culti-aio/GrasscutterQuests.zip'
const STABLE_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/stable.zip'
const DEV_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/development.zip'
const DEV_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip'
const RESOURCES_DOWNLOAD = 'https://gitlab.com/api/v4/projects/35984297/repository/archive.zip' // Use Yuuki res as grasscutter crepe res are broken
const MIGOTO_DOWNLOAD =
'https://github.com/SilentNightSound/GI-Model-Importer/releases/download/V7.0/3dmigoto-GIMI-for-playing-mods.zip'
interface IProps {
closeFn: () => void
downloadManager: DownloadHandler
}
interface IState {
fullbuild_downloading: boolean
grasscutter_downloading: boolean
resources_downloading: boolean
repo_downloading: boolean
migoto_downloading: boolean
grasscutter_set: boolean
resources_exist: boolean
swag: boolean
}
export default class Downloads extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
fullbuild_downloading: this.props.downloadManager.downloadingFullBuild(),
grasscutter_downloading: this.props.downloadManager.downloadingJar(),
resources_downloading: this.props.downloadManager.downloadingResources(),
repo_downloading: this.props.downloadManager.downloadingRepo(),
migoto_downloading: this.props.downloadManager.downloadingMigoto(),
grasscutter_set: false,
resources_exist: false,
swag: false,
}
this.getGrasscutterFolder = this.getGrasscutterFolder.bind(this)
this.downloadGrasscutterFullBuild = this.downloadGrasscutterFullBuild.bind(this)
this.downloadGrasscutterFullQuest = this.downloadGrasscutterFullQuest.bind(this)
this.downloadGrasscutterStableRepo = this.downloadGrasscutterStableRepo.bind(this)
this.downloadGrasscutterDevRepo = this.downloadGrasscutterDevRepo.bind(this)
this.downloadGrasscutterLatest = this.downloadGrasscutterLatest.bind(this)
this.downloadResources = this.downloadResources.bind(this)
this.downloadMigoto = this.downloadMigoto.bind(this)
this.toggleButtons = this.toggleButtons.bind(this)
}
async componentDidMount() {
const gc_path = await getConfigOption('grasscutter_path')
const swag = await getConfigOption('swag_mode')
this.setState({
swag: swag || false,
})
listen('jar_extracted', () => {
this.setState({ grasscutter_set: true }, this.forceUpdate)
})
if (!gc_path || gc_path === '') {
this.setState({
grasscutter_set: false,
resources_exist: false,
})
return
}
const path = gc_path.substring(0, gc_path.lastIndexOf('/'))
if (gc_path) {
const resources_exist: boolean =
((await invoke('dir_exists', {
path: path + '/resources',
})) as boolean) &&
(!(await invoke('dir_is_empty', {
path: path + '/resources',
})) as boolean)
this.setState({
grasscutter_set: gc_path !== '',
resources_exist,
})
}
}
async getGrasscutterFolder() {
const path = await getConfigOption('grasscutter_path')
let folderPath
// Set to default if not set
if (!path || path === '') {
const appdata = await dataDir()
folderPath = appdata + 'cultivation/grasscutter'
// Early return since its formatted properly
return folderPath
}
if (path.includes('/')) {
folderPath = path.substring(0, path.lastIndexOf('/'))
} else {
folderPath = path.substring(0, path.lastIndexOf('\\'))
}
return folderPath
}
async getCultivationFolder() {
const folderPath = (await dataDir()) + 'cultivation'
return folderPath
}
async downloadGrasscutterFullBuild() {
const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(FULL_BUILD_DOWNLOAD, folder + '/GrasscutterCulti.zip', async () => {
await unzip(folder + '/GrasscutterCulti.zip', folder + '/', true)
this.toggleButtons()
})
this.toggleButtons()
}
async downloadGrasscutterFullQuest() {
const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(FULL_QUEST_DOWNLOAD, folder + '/GrasscutterQuests.zip', async () => {
await unzip(folder + '/GrasscutterQuests.zip', folder + '/', true)
this.toggleButtons()
})
this.toggleButtons()
}
async downloadGrasscutterStableRepo() {
const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '/grasscutter_repo.zip', async () => {
await unzip(folder + '/grasscutter_repo.zip', folder + '/', true)
this.toggleButtons()
})
this.toggleButtons()
}
async downloadGrasscutterDevRepo() {
const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '/grasscutter_repo.zip', async () => {
await unzip(folder + '/grasscutter_repo.zip', folder + '/', true)
this.toggleButtons()
})
this.toggleButtons()
}
async downloadGrasscutterLatest() {
const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '/grasscutter.zip', async () => {
await unzip(folder + '/grasscutter.zip', folder + '/', true)
this.toggleButtons()
})
// Also add repo download
this.downloadGrasscutterDevRepo()
this.toggleButtons()
}
async downloadResources() {
// Tell the user this takes some time
alert(
'Extracting resources can take time! If your resources appear to be "stuck" extracting for less than 15-20 mins, they likely still are extracting.'
)
const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(RESOURCES_DOWNLOAD, folder + '/resources.zip', async () => {
// Delete the existing folder if it exists
if (
await invoke('dir_exists', {
path: folder + '/resources',
})
) {
await invoke('dir_delete', {
path: folder + '/resources',
})
}
await unzip(folder + '/resources.zip', folder + '/', true)
// Rename folder to resources
invoke('rename', {
path: folder + '/Resources',
newName: 'resources',
})
this.toggleButtons()
})
this.toggleButtons()
}
async downloadMigoto() {
const folder = (await this.getCultivationFolder()) + '/3dmigoto'
await invoke('dir_create', {
path: folder,
})
this.props.downloadManager.addDownload(MIGOTO_DOWNLOAD, folder + '/GIMI-3dmigoto.zip', async () => {
await unzip(folder + '/GIMI-3dmigoto.zip', folder + '/', true)
this.toggleButtons()
})
this.toggleButtons()
}
async toggleButtons() {
const gc_path = await getConfigOption('grasscutter_path')
// Set states since we know we are downloading something if this is called
this.setState({
fullbuild_downloading: this.props.downloadManager.downloadingFullBuild(),
grasscutter_downloading: this.props.downloadManager.downloadingJar(),
resources_downloading: this.props.downloadManager.downloadingResources(),
repo_downloading: this.props.downloadManager.downloadingRepo(),
migoto_downloading: this.props.downloadManager.downloadingMigoto(),
grasscutter_set: gc_path !== '',
})
}
render() {
return (
<Menu closeFn={this.props.closeFn} className="Downloads" heading="Downloads">
<Divider />
<div className="HeaderText" id="downloadMenuAIOHeader">
<Tr text="downloads.aio_header" />
</div>
<div className="DownloadMenuSection" id="downloadMenuContainerGCFullBuild">
<div className="DownloadLabel" id="downloadMenuLabelGCFullBuild">
<Tr text={'downloads.grasscutter_fullbuild'} />
<HelpButton contents="help.gc_fullbuild" />
</div>
<div className="DownloadValue" id="downloadMenuButtonGCFullBuild">
<BigButton
disabled={this.state.grasscutter_downloading}
onClick={this.downloadGrasscutterFullBuild}
id="grasscutterFullBuildBtn"
>
<Tr text="components.download" />
</BigButton>
</div>
</div>
<div className="DownloadMenuSection" id="downloadMenuContainerGCFullQuest">
<div className="DownloadLabel" id="downloadMenuLabelGCFullQuest">
<Tr text={'downloads.grasscutter_fullquest'} />
<HelpButton contents="help.gc_fullbuild" />
</div>
<div className="DownloadValue" id="downloadMenuButtonGCFullQuest">
<BigButton
disabled={this.state.grasscutter_downloading}
onClick={this.downloadGrasscutterFullQuest}
id="grasscutterFullBuildBtn"
>
<Tr text="components.download" />
</BigButton>
</div>
</div>
<Divider />
<div className="HeaderText" id="downloadMenuIndividualHeader">
<Tr text="downloads.individual_header" />
</div>
<div className="DownloadMenuSection" id="downloadMenuContainerGCDev">
<div className="DownloadLabel" id="downloadMenuLabelGCDev">
<Tr
text={this.state.grasscutter_set ? 'downloads.grasscutter_latest' : 'downloads.grasscutter_latest_update'}
/>
<HelpButton contents="help.gc_dev_jar" />
</div>
<div className="DownloadValue" id="downloadMenuButtonGCDev">
<BigButton
disabled={this.state.grasscutter_downloading}
onClick={this.downloadGrasscutterLatest}
id="grasscutterLatestBtn"
>
<Tr text="components.download" />
</BigButton>
</div>
</div>
<div className="DownloadMenuSection" id="downloadMenuContainerGCDevData">
<div className="DownloadLabel" id="downloadMenuLabelGCDevData">
<Tr
text={
this.state.grasscutter_set
? 'downloads.grasscutter_latest_data'
: 'downloads.grasscutter_latest_data_update'
}
/>
<HelpButton contents="help.gc_dev_data" />
</div>
<div className="DownloadValue" id="downloadMenuButtonGCDevData">
<BigButton
disabled={this.state.repo_downloading}
onClick={this.downloadGrasscutterStableRepo}
id="grasscutterDevRepo"
>
<Tr text="components.download" />
</BigButton>
</div>
</div>
<div className="DownloadMenuSection" id="downloadMenuContainerResources">
<div className="DownloadLabel" id="downloadMenuLabelResources">
<Tr text="downloads.resources" />
<HelpButton contents="help.resources" />
</div>
<div className="DownloadValue" id="downloadMenuButtonResources">
<BigButton
disabled={this.state.resources_downloading || !this.state.grasscutter_set || this.state.resources_exist}
onClick={this.downloadResources}
id="resourcesBtn"
>
<Tr text="components.download" />
</BigButton>
</div>
</div>
{this.state.swag && (
<>
<Divider />
<div className="HeaderText" id="downloadMenuModsHeader">
<Tr text="downloads.mods_header" />
</div>
<div className="DownloadMenuSection" id="downloadMenuContainerMigoto">
<div className="DownloadLabel" id="downloadMenuLabelMigoto">
<Tr text={'downloads.migoto'} />
<HelpButton contents="help.migoto" />
</div>
<div className="DownloadValue" id="downloadMenuButtonMigoto">
<BigButton disabled={this.state.migoto_downloading} onClick={this.downloadMigoto} id="migotoBtn">
<Tr text="components.download" />
</BigButton>
</div>
</div>
</>
)}
</Menu>
)
}
}

View File

@@ -1,32 +0,0 @@
.ExtrasMenu {
width: 20%;
height: 40%;
}
.ExtrasMenu .MenuInner {
justify-content: space-between;
height: 70%;
}
.ExtrasMenuContent {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.ExtraItem {
width: 80%;
padding: 6px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.ExtraLaunch .BigButton {
padding: 20px 50px;
}

View File

@@ -1,179 +0,0 @@
import React from 'react'
import { getConfig, saveConfig } from '../../../utils/configuration'
import Checkbox from '../common/Checkbox'
import Menu from './Menu'
import './ExtrasMenu.css'
import BigButton from '../common/BigButton'
import { invoke } from '@tauri-apps/api'
import Tr from '../../../utils/language'
import { getGameExecutable } from '../../../utils/game'
interface IProps {
children: React.ReactNode | React.ReactNode[]
closeFn: () => void
playGame: (exe?: string, proc_name?: string) => void
}
interface IState {
migoto?: string
akebi?: string
reshade?: string
launch_migoto: boolean
launch_akebi: boolean
launch_reshade: boolean
}
export class ExtrasMenu extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
launch_migoto: false,
launch_akebi: false,
launch_reshade: false,
}
this.launchPreprograms = this.launchPreprograms.bind(this)
this.toggleMigoto = this.toggleMigoto.bind(this)
this.toggleAkebi = this.toggleAkebi.bind(this)
this.toggleReshade = this.toggleReshade.bind(this)
}
async componentDidMount() {
const config = await getConfig()
this.setState({
migoto: config.migoto_path,
akebi: config.akebi_path,
reshade: config.reshade_path,
launch_akebi: config?.last_extras?.akebi ?? false,
launch_migoto: config?.last_extras?.migoto ?? false,
launch_reshade: config?.last_extras?.reshade ?? false,
})
}
async launchPreprograms() {
const config = await getConfig()
config.last_extras = {
migoto: this.state.launch_migoto,
akebi: this.state.launch_akebi,
reshade: this.state.launch_reshade,
}
await saveConfig(config)
// Close menu
this.props.closeFn()
// This injects independent of the game
if (this.state.launch_migoto) {
await this.launchMigoto()
}
// This injects independent of the game
if (this.state.launch_reshade) {
await this.launchReshade()
}
// This will launch the game
if (this.state.launch_akebi) {
await this.launchAkebi()
// This already launches the game
return
}
// Launch the game
await this.props.playGame()
}
async launchAkebi() {
const config = await getConfig()
// Get game exe from game path, so we can watch it
const pathArr = config.game_install_path.replace(/\\/g, '/').split('/')
const gameExec = pathArr[pathArr.length - 1]
await this.props.playGame(config.akebi_path, gameExec)
}
async launchMigoto() {
const config = await getConfig()
if (!config.migoto_path) return alert('Migoto not installed or set!')
await invoke('run_program_relative', { path: config.migoto_path })
}
async launchReshade() {
const config = await getConfig()
if (!config.reshade_path) return alert('Reshade not installed or set!')
await invoke('run_command', {
program: config.reshade_path,
args: [await getGameExecutable()],
relative: true,
})
}
toggleMigoto() {
this.setState({
launch_migoto: !this.state.launch_migoto,
})
}
toggleAkebi() {
this.setState({
launch_akebi: !this.state.launch_akebi,
})
}
toggleReshade() {
this.setState({
launch_reshade: !this.state.launch_reshade,
})
}
render() {
return (
<Menu closeFn={this.props.closeFn} heading="Extras" className="ExtrasMenu">
<div className="ExtrasMenuContent">
{this.state.migoto && (
<div className="ExtraItem">
<div className="ExtraItemLabel">
<Tr text="swag.migoto_name" />
</div>
<Checkbox id="MigotoCheckbox" checked={this.state.launch_migoto} onChange={this.toggleMigoto} />
</div>
)}
{this.state.akebi && (
<div className="ExtraItem">
<div className="ExtraItemLabel">
<Tr text="swag.akebi_name" />
</div>
<Checkbox id="AkebiCheckbox" checked={this.state.launch_akebi} onChange={this.toggleAkebi} />
</div>
)}
{this.state.reshade && (
<div className="ExtraItem">
<div className="ExtraItemLabel">
<Tr text="swag.reshade_name" />
</div>
<Checkbox id="ReshadeCheckbox" checked={this.state.launch_reshade} onChange={this.toggleReshade} />
</div>
)}
</div>
<div className="ExtraLaunch">
<BigButton id="ExtraLaunch" onClick={this.launchPreprograms}>
<Tr text="main.launch_button" />
</BigButton>
</div>
</Menu>
)
}
}

View File

@@ -1,37 +0,0 @@
.GameDownloadMenu {
width: 40%;
height: 40%;
}
.GameDownload {
display: flex;
flex-direction: row;
align-items: center;
padding: 18px;
}
.GameDownload .MiniDialog {
height: 30%;
}
.GameDownload .BigButton {
height: 50px;
}
.GameDownload .HelpButton img {
filter: invert(0%) sepia(91%) saturate(7464%) hue-rotate(101deg) brightness(0%) contrast(107%);
}
.GameDownloadDir {
width: 70%;
}
.GameDownloadDir .DirInput .TextInputWrapper,
.GameDownloadDir .DirInput {
width: 100%;
}
.GameDownloadDir .DirInput .TextInputWrapper input {
width: 80%;
}

View File

@@ -1,92 +0,0 @@
import React from 'react'
import Menu from './Menu'
import { translate } from '../../../utils/language'
import DownloadHandler from '../../../utils/download'
import './Game.css'
import DirInput from '../common/DirInput'
import BigButton from '../common/BigButton'
import HelpButton from '../common/HelpButton'
import { unzip } from '../../../utils/zipUtils'
const GAME_DOWNLOAD = ''
interface IProps {
closeFn: () => void
downloadManager: DownloadHandler
}
interface IState {
gameDownloading: boolean
gameDownloadFolder: string
dirPlaceholder: string
}
export default class Downloads extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
gameDownloading: false,
gameDownloadFolder: '',
dirPlaceholder: '',
}
this.downloadGame = this.downloadGame.bind(this)
}
async componentDidMount() {
this.setState({
dirPlaceholder: await translate('components.select_folder'),
})
console.log(this.state)
}
async downloadGame() {
const folder = this.state.gameDownloadFolder
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '/game.zip', async () => {
await unzip(folder + '/game.zip', folder + '/', true)
this.setState({
gameDownloading: false,
})
})
this.setState({
gameDownloading: true,
})
}
render() {
return (
<Menu heading="Download Game" closeFn={this.props.closeFn} className="GameDownloadMenu">
<div className="GameDownload">
{this.state.gameDownloadFolder !== '' && !this.state.gameDownloading ? (
<BigButton id="downloadGameBtn" onClick={this.downloadGame}>
Download Game
</BigButton>
) : (
<BigButton id="disabledGameBtn" onClick={() => null} disabled>
Download Game
</BigButton>
)}
<HelpButton contents="main.game_help_text" />
</div>
<div className="GameDownloadDir">
<DirInput
folder
placeholder={this.state.dirPlaceholder}
clearable={false}
readonly={true}
onChange={(value: string) =>
this.setState({
gameDownloadFolder: value,
})
}
/>
</div>
</Menu>
)
}
}

View File

@@ -1,15 +0,0 @@
.GameInstallNotify {
display: flex;
justify-content: center;
background-color: rgb(39, 39, 39);
color: white;
}
.GameInstallNotify span {
padding: 10px 0;
}
#pointer {
position: absolute;
right: 85px;
}

View File

@@ -1,15 +0,0 @@
import React from 'react'
import './GamePathNotify.css'
import Tr from '../../../utils/language'
export default class GamePathNotify extends React.Component {
render() {
return (
<div className="GameInstallNotify">
<span>
<Tr text={'main.game_path_notify'} />
</span>
</div>
)
}
}

View File

@@ -1,55 +0,0 @@
.Menu {
position: fixed;
z-index: 99;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 70%;
width: 60%;
background: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.2);
overflow-y: auto;
}
.Menu::-webkit-scrollbar {
display: none;
}
.MenuInner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.MenuHeading {
font-size: 2rem;
margin: 16px;
}
.MenuTop {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.MenuExit {
height: 30px;
margin: 20px;
}
.MenuExit:hover {
cursor: pointer;
}
.MenuExit img {
height: 100%;
}

View File

@@ -1,35 +0,0 @@
import React from 'react'
import './Menu.css'
import Close from '../../../resources/icons/close.svg'
interface IProps {
children: React.ReactNode[] | React.ReactNode
className?: string
heading: string
closeFn: () => void
}
export default class Menu extends React.Component<IProps, never> {
constructor(props: IProps) {
super(props)
}
render() {
return (
<div className={'Menu ' + this.props.className} id="menuContainer">
<div className="MenuTop" id="menuContainerTop">
<div className="MenuHeading" id="menuHeading">
{this.props.heading}
</div>
<div className="MenuExit" id="menuButtonCloseContainer" onClick={this.props.closeFn}>
<img src={Close} className="MenuClose" id="menuButtonCloseIcon" />
</div>
</div>
<div className="MenuInner" id="menuContent">
{this.props.children}
</div>
</div>
)
}
}

View File

@@ -1,22 +0,0 @@
.OptionSection {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
width: 80%;
}
.OptionSection .BigButton {
height: 100%;
min-height: 30px;
}
.OptionSection .BigButtonText {
font-size: 12px;
}
.OptionSection .HelpButton img {
filter: invert(0%) sepia(91%) saturate(7464%) hue-rotate(101deg) brightness(0%) contrast(107%);
}

View File

@@ -1,715 +0,0 @@
import React from 'react'
import { invoke } from '@tauri-apps/api'
import { dataDir } from '@tauri-apps/api/path'
import DirInput from '../common/DirInput'
import Menu from './Menu'
import Tr, { getLanguages } from '../../../utils/language'
import { setConfigOption, getConfig, getConfigOption, Configuration } from '../../../utils/configuration'
import Checkbox from '../common/Checkbox'
import Divider from './Divider'
import { getThemeList } from '../../../utils/themes'
import * as server from '../../../utils/server'
import './Options.css'
import BigButton from '../common/BigButton'
import DownloadHandler from '../../../utils/download'
import * as meta from '../../../utils/rsa'
import HelpButton from '../common/HelpButton'
import SmallButton from '../common/SmallButton'
import { confirm } from '@tauri-apps/api/dialog'
import TextInput from '../common/TextInput'
export enum GrasscutterElevation {
None = 'None',
Capability = 'Capability',
Root = 'Root',
}
interface IProps {
closeFn: () => void
downloadManager: DownloadHandler
}
interface IState {
game_install_path: string
grasscutter_path: string
java_path: string
grasscutter_with_game: boolean
language_options: { [key: string]: string }[]
current_language: string
bg_url_or_path: string
themes: string[]
theme: string
use_theme_background: boolean
encryption: boolean
patch_rsa: boolean
use_internal_proxy: boolean
wipe_login: boolean
horny_mode: boolean
auto_mongodb: boolean
swag: boolean
platform: string
un_elevated: boolean
redirect_more: boolean
launch_args: string
// Linux stuff
grasscutter_elevation: string
// Swag stuff
akebi_path: string
migoto_path: string
reshade_path: string
}
export default class Options extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
game_install_path: '',
grasscutter_path: '',
java_path: '',
grasscutter_with_game: false,
language_options: [],
current_language: 'en',
bg_url_or_path: '',
themes: ['default'],
theme: '',
use_theme_background: false,
encryption: false,
patch_rsa: false,
use_internal_proxy: false,
wipe_login: false,
horny_mode: false,
swag: false,
auto_mongodb: false,
platform: '',
un_elevated: false,
redirect_more: false,
launch_args: '',
// Linux stuff
grasscutter_elevation: GrasscutterElevation.None,
// Swag stuff
akebi_path: '',
migoto_path: '',
reshade_path: '',
}
this.setGameExecutable = this.setGameExecutable.bind(this)
this.setGrasscutterJar = this.setGrasscutterJar.bind(this)
this.setJavaPath = this.setJavaPath.bind(this)
this.setAkebi = this.setAkebi.bind(this)
this.setMigoto = this.setMigoto.bind(this)
this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this)
this.setCustomBackground = this.setCustomBackground.bind(this)
this.toggleEncryption = this.toggleEncryption.bind(this)
this.removeRSA = this.removeRSA.bind(this)
this.deleteWebCache = this.deleteWebCache.bind(this)
this.addMigotoDelay = this.addMigotoDelay.bind(this)
this.toggleUnElevatedGame = this.toggleUnElevatedGame.bind(this)
this.setLaunchArgs = this.setLaunchArgs.bind(this)
}
async componentDidMount() {
const config = await getConfig()
const languages = await getLanguages()
const platform: string = await invoke('get_platform')
let encEnabled
if (config.grasscutter_path) {
// Remove jar from path
const path = config.grasscutter_path.replace(/\\/g, '/')
const folderPath = path.substring(0, path.lastIndexOf('/'))
encEnabled = await server.encryptionEnabled(folderPath + '/config.json')
}
console.log(platform)
this.setState({
game_install_path: config.game_install_path || '',
grasscutter_path: config.grasscutter_path || '',
java_path: config.java_path || '',
grasscutter_with_game: config.grasscutter_with_game || false,
language_options: languages,
current_language: config.language || 'en',
bg_url_or_path: config.custom_background || '',
themes: (await getThemeList()).map((t) => t.name),
theme: config.theme || 'default',
use_theme_background: config.use_theme_background || false,
encryption: encEnabled || false,
patch_rsa: config.patch_rsa || 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,
auto_mongodb: config.auto_mongodb || false,
platform,
un_elevated: config.un_elevated || false,
redirect_more: config.redirect_more || false,
launch_args: config.launch_args,
// Linux stuff
grasscutter_elevation: config.grasscutter_elevation || GrasscutterElevation.None,
// Swag stuff
akebi_path: config.akebi_path || '',
migoto_path: config.migoto_path || '',
reshade_path: config.reshade_path || '',
})
this.forceUpdate()
}
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}`
)
}
// If setting any other game, automatically set to redirect more
if (!value.toLowerCase().includes('genshin' || 'yuanshen')) {
if (!this.state.redirect_more) {
this.toggleOption('redirect_more')
}
}
this.setState({
game_install_path: value,
})
}
async setGrasscutterJar(value: string) {
setConfigOption('grasscutter_path', value)
this.setState({
grasscutter_path: value,
})
const config = await getConfig()
const path = config.grasscutter_path.replace(/\\/g, '/')
const folderPath = path.substring(0, path.lastIndexOf('/'))
const encEnabled = await server.encryptionEnabled(folderPath + '/config.json')
// Update encryption button when setting new jar
this.setState({
encryption: encEnabled,
})
window.location.reload()
}
setJavaPath(value: string) {
setConfigOption('java_path', value)
this.setState({
java_path: value,
})
}
setAkebi(value: string) {
setConfigOption('akebi_path', value)
this.setState({
akebi_path: value,
})
}
setMigoto(value: string) {
setConfigOption('migoto_path', value)
this.setState({
migoto_path: value,
})
}
setReshade(value: string) {
setConfigOption('reshade_path', value)
this.setState({
reshade_path: value,
})
}
async setLanguage(value: string) {
await setConfigOption('language', value)
window.location.reload()
}
async setTheme(value: string) {
await setConfigOption('theme', value)
window.location.reload()
}
async toggleGrasscutterWithGame() {
const changedVal = !(await getConfigOption('grasscutter_with_game'))
setConfigOption('grasscutter_with_game', changedVal)
this.setState({
grasscutter_with_game: changedVal,
})
}
async setCustomBackground(value: string) {
const isUrl = /^(?:http(s)?:\/\/)/gm.test(value)
if (!value) return await setConfigOption('custom_background', '')
if (!isUrl) {
const filename = value.replace(/\\/g, '/').split('/').pop()
const localBgPath = (await dataDir()).replace(/\\/g, '/')
await setConfigOption('custom_background', `${localBgPath}/cultivation/bg/${filename}`)
// Copy the file over to the local directory
await invoke('copy_file', {
path: value.replace(/\\/g, '/'),
newPath: `${localBgPath}cultivation/bg/`,
})
window.location.reload()
} else {
await setConfigOption('custom_background', value)
window.location.reload()
}
}
async toggleEncryption() {
const config = await getConfig()
// Check if grasscutter path is set
if (!config.grasscutter_path) {
alert('Grasscutter not set!')
return
}
// Remove jar from path
const path = config.grasscutter_path.replace(/\\/g, '/')
const folderPath = path.substring(0, path.lastIndexOf('/'))
if (!(await server.encryptionEnabled(folderPath + '/config.json'))) {
if (
!(await confirm(
'Cultivation requires encryption DISABLED to connect and play locally. \n\n Are you sure you want to enable encryption?',
'Warning!'
))
) {
return
}
}
await server.toggleEncryption(folderPath + '/config.json')
this.setState({
encryption: await server.encryptionEnabled(folderPath + '/config.json'),
})
// Check if Grasscutter is running, and restart if so to apply changes
if (await invoke('is_grasscutter_running')) {
alert('Automatically restarting Grasscutter to apply encryption changes!')
await invoke('restart_grasscutter')
}
}
async toggleUnElevatedGame() {
const changedVal = !(await getConfigOption('un_elevated'))
setConfigOption('un_elevated', changedVal)
this.setState({
un_elevated: changedVal,
})
}
async setGCElevation(value: string) {
setConfigOption('grasscutter_elevation', value)
this.setState({
grasscutter_elevation: value,
})
}
async removeRSA() {
await meta.unpatchGame()
}
async addMigotoDelay() {
invoke('set_migoto_delay', {
migotoPath: this.state.migoto_path,
})
}
async installCert() {
await invoke('generate_ca_files', {
path: (await dataDir()) + 'cultivation',
})
}
async deleteWebCache() {
alert('Cultivation may freeze for a moment while this occurs!')
// Get webCaches folder path
const pathArr = this.state.game_install_path.replace(/\\/g, '/').split('/')
pathArr.pop()
const path = pathArr.join('/') + '/GenshinImpact_Data/webCaches'
const path2 = pathArr.join('/') + '/Yuanshen_Data/webCaches'
// Delete the folder
await invoke('dir_delete', { path: path })
await invoke('dir_delete', { path: path2 })
}
async toggleOption(opt: keyof Configuration) {
const changedVal = !(await getConfigOption(opt))
await setConfigOption(opt, changedVal)
// @ts-expect-error shut up bitch
this.setState({
[opt]: changedVal,
})
}
async setLaunchArgs(value: string) {
await setConfigOption('launch_args', value)
this.setState({
launch_args: value,
})
}
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" />
</div>
<div className="OptionValue" id="menuOptionsDirGamePath">
<DirInput onChange={this.setGameExecutable} value={this.state?.game_install_path} extensions={['exe']} />
</div>
</div>
<div className="OptionSection" id="menuOptionsContainermetaDownload">
<div className="OptionLabel" id="menuOptionsLabelmetaDownload">
<Tr text="options.recover_rsa" />
<HelpButton contents="help.emergency_rsa" />
</div>
<div className="OptionValue" id="menuOptionsButtonmetaDownload">
<BigButton onClick={this.removeRSA} id="metaDownload">
<Tr text="components.delete" />
</BigButton>
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerPatchMeta">
<div className="OptionLabel" id="menuOptionsLabelPatchMeta">
<Tr text="options.patch_rsa" />
<HelpButton contents="help.patch_rsa" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxPatchMeta">
<Checkbox onChange={() => this.toggleOption('patch_rsa')} checked={this.state?.patch_rsa} id="patchMeta" />
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerUseProxy">
<div className="OptionLabel" id="menuOptionsLabelUseProxy">
<Tr text="options.use_proxy" />
<HelpButton contents="help.use_proxy" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxUseProxy">
<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 className="OptionSection" id="menuOptionsContainerAutoMongodb">
<div className="OptionLabel" id="menuOptionsLabelAutoMongodb">
<Tr text="options.auto_mongodb" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxAutoMongodb">
<Checkbox
onChange={() => this.toggleOption('auto_mongodb')}
checked={this.state?.auto_mongodb}
id="autoMongodb"
/>
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerRedirect">
<div className="OptionLabel" id="menuOptionsLabelRedirect">
<Tr text="options.redirect_more" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxRedirect">
<Checkbox
onChange={() => this.toggleOption('redirect_more')}
checked={this.state?.redirect_more}
id="RedirectMore"
/>
</div>
</div>
<Divider />
<div className="OptionSection" id="menuOptionsContainerGCJar">
<div className="OptionLabel" id="menuOptionsLabelGCJar">
<Tr text="options.grasscutter_jar" />
</div>
<div className="OptionValue" id="menuOptionsDirGCJar">
<DirInput onChange={this.setGrasscutterJar} value={this.state?.grasscutter_path} extensions={['jar']} />
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerToggleEnc">
<div className="OptionLabel" id="menuOptionsLabelToggleEnc">
<Tr text="options.toggle_encryption" />
<HelpButton contents="help.encryption" />
</div>
<div className="OptionValue" id="menuOptionsButtonToggleEnc">
<Checkbox onChange={() => this.toggleEncryption()} checked={this.state.encryption} id="toggleEnc" />
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerInstallCert">
<div className="OptionLabel" id="menuOptionsLabelInstallCert">
<Tr text="options.install_certificate" />
</div>
<div className="OptionValue" id="menuOptionsButtonInstallCert">
<BigButton disabled={false} onClick={this.installCert} id="installCert">
<Tr text="components.install" />
</BigButton>
</div>
</div>
{this.state.platform === 'linux' && (
<>
<Divider />
<div className="OptionSection" id="menuOptionsContainerGCElevation">
<div className="OptionLabel" id="menuOptionsLabelGCElevation">
<Tr text="options.grasscutter_elevation" />
<HelpButton contents="help.grasscutter_elevation_help_text" />
</div>
<select
value={this.state.grasscutter_elevation}
id="menuOptionsSelectGCElevation"
onChange={(event) => {
this.setGCElevation(event.target.value)
}}
>
{Object.keys(GrasscutterElevation).map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
</div>
<div className="OptionSection" id="menuOptionsContainerCheckAAGL">
<div className="OptionLabel" id="menuOptionsLabelCheckAAGL">
<Tr text="options.check_aagl" />
</div>
</div>
</>
)}
{this.state.swag && (
<>
<Divider />
<div className="OptionSection" id="menuOptionsContainerAkebi">
<div className="OptionLabel" id="menuOptionsLabelAkebi">
<Tr text="swag.akebi" />
</div>
<div className="OptionValue" id="menuOptionsDirAkebi">
<DirInput onChange={this.setAkebi} value={this.state?.akebi_path} extensions={['exe']} />
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerMigoto">
<div className="OptionLabel" id="menuOptionsLabelMigoto">
<Tr text="swag.migoto" />
</div>
<div className="OptionValue" id="menuOptionsDirMigoto">
<SmallButton onClick={this.addMigotoDelay} id="migotoDelay" contents="help.add_delay"></SmallButton>
<DirInput onChange={this.setMigoto} value={this.state?.migoto_path} extensions={['exe']} />
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerReshade">
<div className="OptionLabel" id="menuOptionsLabelReshade">
<Tr text="swag.reshade" />
</div>
<div className="OptionValue" id="menuOptionsDirReshade">
<DirInput onChange={this.setReshade} value={this.state?.reshade_path} extensions={['exe']} />
</div>
</div>
</>
)}
<Divider />
<div className="OptionSection" id="menuOptionsContainerGCWGame">
<div className="OptionLabel" id="menuOptionsLabelGCWDame">
<Tr text="options.grasscutter_with_game" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxGCWGame">
<Checkbox
onChange={() => this.toggleOption('grasscutter_with_game')}
checked={this.state?.grasscutter_with_game}
id="gcWithGame"
/>
</div>
</div>
{this.state.platform !== 'linux' && (
<div className="OptionSection" id="menuOptionsContainerUEGame">
<div className="OptionLabel" id="menuOptionsLabelUEGame">
<Tr text="options.un_elevated" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxUEGame">
<Checkbox
onChange={() => this.toggleOption('un_elevated')}
checked={this.state?.un_elevated}
id="unElevatedGame"
/>
</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 />
<div className="OptionSection" id="menuOptionsContainerThemes">
<div className="OptionLabel" id="menuOptionsLabelThemes">
<Tr text="options.theme" />
</div>
<div className="OptionValue" id="menuOptionsSelectThemes">
<select
value={this.state.theme}
id="menuOptionsSelectMenuThemes"
onChange={(event) => {
this.setTheme(event.target.value)
}}
>
{this.state.themes.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerUseThemeBG">
<div className="OptionLabel" id="menuOptionsLabelUseThemeBG">
<Tr text="options.use_theme_background" />
</div>
<div className="OptionValue" id="menuOptionsUseThemeBG">
<Checkbox
onChange={() => this.toggleOption('use_theme_background')}
checked={this.state?.use_theme_background}
id="useThemeBG"
/>
</div>
</div>
<Divider />
<div className="OptionSection" id="menuOptionsContainerJavaPath">
<div className="OptionLabel" id="menuOptionsLabelJavaPath">
<Tr text="options.java_path" />
</div>
<div className="OptionValue" id="menuOptionsDirJavaPath">
<DirInput onChange={this.setJavaPath} value={this.state?.java_path} extensions={['exe']} />
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerBG">
<div className="OptionLabel" id="menuOptionsLabelBG">
<Tr text="options.background" />
</div>
<div className="OptionValue" id="menuOptionsDirBG">
<DirInput
onChange={this.setCustomBackground}
value={this.state?.bg_url_or_path}
extensions={['png', 'jpg', 'jpeg']}
readonly={false}
clearable={true}
customClearBehaviour={async () => {
await setConfigOption('custom_background', '')
window.location.reload()
}}
/>
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerLang">
<div className="OptionLabel" id="menuOptionsLabelLang">
<Tr text="options.language" />
</div>
<div className="OptionValue" id="menuOptionsSelectLang">
<select
value={this.state.current_language}
id="menuOptionsSelectMenuLang"
onChange={(event) => {
this.setLanguage(event.target.value)
}}
>
{this.state.language_options.map((lang) => (
<option key={Object.keys(lang)[0]} value={Object.keys(lang)[0]}>
{lang[Object.keys(lang)[0]]}
</option>
))}
</select>
</div>
</div>
<Divider />
<div className="OptionSection" id="menuOptionsContainerAdvanced">
<div className="OptionLabel" id="menuOptionsLabelWebCache">
<Tr text="options.web_cache" />
</div>
<div className="OptionValue" id="menuOptionsButtondeleteWebcache">
<BigButton onClick={this.deleteWebCache} id="deleteWebcache">
<Tr text="components.delete" />
</BigButton>
</div>
<div className="OptionLabel" id="menuOptionsLaunchArgs">
<Tr text="options.launch_args" />
</div>
<TextInput
id="launch_args"
key="launch_args"
placeholder={'-arg=value'}
onChange={this.setLaunchArgs}
value={this.state.launch_args}
/>
</div>
</Menu>
)
}
}

View File

@@ -1,39 +0,0 @@
/**
* Blatantly yoinked from https://loading.io/css/
*/
.LoadingCircle {
display: inline-block;
transform: translateZ(1px);
position: absolute;
top: 45%;
left: 48.5%;
}
.LoadingCircle > div {
display: inline-block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
background: #fff;
animation: loading 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
@keyframes loading {
0%,
100% {
animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5);
}
0% {
transform: rotateY(0deg);
}
50% {
transform: rotateY(1800deg);
animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1);
}
100% {
transform: rotateY(3600deg);
}
}

View File

@@ -1,13 +0,0 @@
import React from 'react'
import './LoadingCircle.css'
export class LoadingCircle extends React.Component {
render() {
return (
<div className="LoadingCircle">
<div></div>
</div>
)
}
}

View File

@@ -1,30 +0,0 @@
.ModHeader {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
width: 100%;
padding: 10px;
font-size: 20px;
font-weight: bold;
color: #fff;
background: rgba(77, 77, 77, 0.6);
}
.ModHeaderTitle {
display: flex;
justify-content: center;
width: 100%;
max-width: 20%;
}
.ModHeaderTitle:hover {
cursor: pointer;
}
.ModHeaderTitle.selected {
border-bottom: 2px solid #fff;
}

View File

@@ -1,52 +0,0 @@
import React from 'react'
import './ModHeader.css'
interface IProps {
headers: {
title: string
name: string
}[]
onChange: (value: string) => void
defaultHeader: string
}
interface IState {
selected: string
}
export class ModHeader extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
selected: this.props.defaultHeader,
}
}
setSelected(value: string) {
this.setState({
selected: value,
})
this.props.onChange(value)
}
render() {
return (
<div className="ModHeader">
{this.props.headers.map((header, index) => {
return (
<div
key={index}
className={`ModHeaderTitle ${this.state.selected === header.name ? 'selected' : ''}`}
onClick={() => this.setSelected(header.name)}
>
{header.title}
</div>
)
})}
</div>
)
}
}

View File

@@ -1,19 +0,0 @@
.ModList {
width: 100%;
height: 100%;
background-color: rgba(106, 105, 106, 0.6);
}
.ModListInner {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
flex-grow: 1;
align-items: center;
justify-content: center;
flex-wrap: wrap;
overflow-y: auto;
}

View File

@@ -1,107 +0,0 @@
import React from 'react'
import { getConfigOption } from '../../../utils/configuration'
import { getInstalledMods, getMods, ModData, PartialModData } from '../../../utils/gamebanana'
import { LoadingCircle } from './LoadingCircle'
import './ModList.css'
import { ModTile } from './ModTile'
interface IProps {
mode: string
page: number
search: string
addDownload: (mod: ModData) => void
}
interface IState {
horny: boolean
modList: ModData[] | null
installedList:
| {
path: string
info: ModData | PartialModData
}[]
| null
}
export class ModList extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
horny: false,
modList: null,
installedList: null,
}
this.downloadMod = this.downloadMod.bind(this)
}
async componentDidMount() {
if (this.props.mode === 'installed') {
const installedMods = (await getInstalledMods()).map((mod) => {
// Check if it's a partial mod, and if so, fill in some pseudo-data
if (!('id' in mod.info)) {
const newInfo = mod.info as PartialModData
newInfo.images = []
newInfo.submitter = { name: 'Unknown' }
newInfo.likes = 0
newInfo.views = 0
mod.info = newInfo
return mod
}
return mod
})
this.setState({
installedList: installedMods,
})
return
}
const mods = await getMods(this.props.mode, this.props.page, this.props.search)
const horny = await getConfigOption('horny_mode')
this.setState({
horny,
modList: mods,
})
}
async downloadMod(mod: ModData) {
this.props.addDownload(mod)
}
render() {
return (
<div className="ModList">
{(this.state.modList && this.props.mode !== 'installed') ||
(this.state.installedList && this.props.mode === 'installed') ? (
<div className="ModListInner">
{this.props.mode === 'installed'
? this.state.installedList?.map((mod) => (
<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 horny={this.state.horny} mod={mod} key={mod.id} onClick={this.downloadMod} />
))}
</div>
) : (
<LoadingCircle />
)}
</div>
)
}
}

View File

@@ -1,66 +0,0 @@
.ModPages {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
width: 100%;
z-index: 2;
position: relative;
padding: 5px;
font-size: 20px;
font-weight: bold;
color: #fff;
background: rgba(77, 77, 77, 0.6);
}
.ModPagesTitle {
display: flex;
justify-content: center;
z-index: 3;
width: 100%;
max-width: 20%;
}
.ModPagesTitle:hover {
cursor: pointer;
}
.ModPagesTitle.selected {
border-bottom: 0px solid #fff;
}
.ModPagesPage {
position: absolute;
justify-self: center;
left: 50%;
margin-top: 10px;
transform: translateX(-50%);
text-align: center;
padding: -5px;
z-index: 3;
font-size: 15px;
font-weight: bold;
color: #fff;
}
.ModPagesPage input {
text-align: center;
padding: 3px;
border-radius: 3px;
height: 18px;
font-size: 20px;
font-weight: bold;
color: #fff;
background: rgba(77, 77, 77, 0.6);
}
.ModPagesPage .TextInputWrapper {
background: rgba(77, 77, 77, 0.6);
z-index: -1;
display: inline-block;
}

View File

@@ -1,54 +0,0 @@
import React from 'react'
import './ModPages.css'
interface IProps {
headers: {
title: string
name: number
}[]
onClick: (value: number) => void
defaultHeader: number
}
interface IState {
selected: number
}
export class ModPages extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
selected: this.props.defaultHeader,
}
}
setSelected(value: number) {
const current = this.state.selected
if (current + value == 0) return
this.setState({
selected: current + value,
})
this.props.onClick(value)
}
render() {
return (
<div className="ModPages">
{this.props.headers.map((header, index) => {
return (
<div
key={index}
className={`ModPagesTitle ${this.state.selected === header.name ? 'selected' : ''}`}
onClick={() => this.setSelected(header.name)}
>
{header.title}
</div>
)
})}
</div>
)
}
}

View File

@@ -1,137 +0,0 @@
.ModListItem {
display: flex;
flex-direction: column;
align-items: center;
width: 23%;
margin: 10px;
background: rgb(99, 98, 98, 0.2);
border-radius: 10px;
backdrop-filter: blur(10px);
transition: background-color 0.1s ease-in-out;
}
.ModListItem:hover {
cursor: pointer;
background: rgb(99, 98, 98, 0.8);
}
.ModAuthor,
.ModName {
width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding: 10px 0 0 10px;
margin-left: 10px;
color: #fff;
font-weight: bold;
}
.ModAuthor {
font-weight: normal;
padding: 0 0 0 10px;
}
.ModTileOpen {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.ModListItem .Checkbox {
filter: invert(100%) sepia(2%) saturate(201%) hue-rotate(47deg) brightness(117%) contrast(100%);
}
.ModImage .ModTileFolder {
width: 40px !important;
height: 40px !important;
margin: 20px;
}
.ModTileFolder:hover {
cursor: pointer;
filter: invert(1) brightness(0.2);
}
.ModTileOpen,
.ModTileDownload {
position: absolute;
object-fit: contain;
left: 40%;
top: 45%;
z-index: 999;
width: 40px !important;
height: 40px !important;
filter: invert(1);
}
.ModTileOpen {
left: 37.5%;
}
.ModImage {
width: 100%;
height: 100%;
object-fit: cover;
display: flex;
align-items: center;
justify-content: center;
}
.ModImage .ModImageInner {
object-fit: cover;
width: 100%;
height: 150px;
margin: 14px;
}
img.blur {
filter: blur(6px);
}
img.nsfw {
filter: blur(16px);
}
.ModInner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
color: #fff;
padding-bottom: 10px;
}
.ModInner div {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0px 8px;
}
.ModInner div span {
margin: 0px 5px;
}
.ModInner img {
object-fit: contain;
width: 20px;
height: 20px;
margin: 0px;
filter: invert(1);
}

View File

@@ -1,137 +0,0 @@
import React from 'react'
import { ModData, PartialModData } from '../../../utils/gamebanana'
import { getConfigOption } from '../../../utils/configuration'
import './ModTile.css'
import Like from '../../../resources/icons/like.svg'
import Eye from '../../../resources/icons/eye.svg'
import Download from '../../../resources/icons/download.svg'
import Folder from '../../../resources/icons/folder.svg'
import { shell } from '@tauri-apps/api'
import Checkbox from '../common/Checkbox'
import { disableMod, enableMod, modIsEnabled } from '../../../utils/mods'
interface IProps {
mod: ModData | PartialModData
horny?: boolean
path?: string
onClick: (mod: ModData) => void
}
interface IState {
horny: boolean
hover: boolean
modEnabled: boolean
}
export class ModTile extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
horny: false,
hover: false,
modEnabled: false,
}
this.openInExplorer = this.openInExplorer.bind(this)
this.toggleMod = this.toggleMod.bind(this)
}
getModFolderName() {
if (!('id' in this.props.mod)) {
return this.props.mod.name.includes('DISABLED_') ? this.props.mod.name.split('DISABLED_')[1] : this.props.mod.name
}
return String(this.props.mod.id)
}
async componentDidMount() {
const horny = await getConfigOption('horny_mode')
if (!('id' in this.props.mod)) {
// Partial mod
this.setState({
modEnabled: await modIsEnabled(this.props.mod.name),
horny,
})
return
}
this.setState({
modEnabled: await modIsEnabled(String(this.props.mod.id)),
horny,
})
}
async openInExplorer() {
if (this.props.path) shell.open(this.props.path)
}
toggleMod() {
this.setState(
{
modEnabled: !this.state.modEnabled,
horny: !this.state.horny,
},
() => {
if (this.state.modEnabled) {
enableMod(String(this.getModFolderName()))
return
}
disableMod(String(this.getModFolderName()))
}
)
}
render() {
const { mod } = this.props
return (
<div
className="ModListItem"
onMouseEnter={() => this.setState({ hover: true })}
onMouseLeave={() => this.setState({ hover: false })}
{...(!this.props.path && {
onClick: () => {
if (!('id' in mod)) return
this.props.onClick(mod)
},
})}
>
<span className="ModName">{mod.name.includes('DISABLED_') ? mod.name.split('DISABLED_')[1] : mod.name}</span>
<span className="ModAuthor">{mod.submitter.name}</span>
<div className="ModImage">
{this.state.hover &&
(!this.props.path ? (
<img src={Download} className="ModTileDownload" alt="Download" />
) : (
<div className="ModTileOpen">
<img src={Folder} className="ModTileFolder" alt="Open" onClick={this.openInExplorer} />
<Checkbox checked={this.state.modEnabled} id={this.props.mod.name} onChange={this.toggleMod} />
</div>
))}
<img
src={mod.images[0]}
className={`ModImageInner ${'id' in mod && !this.state.horny && mod.nsfw ? 'nsfw' : ''} ${
this.state.hover ? 'blur' : ''
}`}
/>
</div>
<div className="ModInner">
<div className="likes">
<img src={Like} />
<span>{mod.likes.toLocaleString()}</span>
</div>
<div className="views">
<img src={Eye} />
<span>{mod.views.toLocaleString()}</span>
</div>
</div>
</div>
)
}
}

View File

@@ -1,97 +0,0 @@
.NewsSection {
background-color: rgba(106, 105, 106, 0.6);
position: absolute;
min-height: 219px;
height: 40%;
width: 512px;
bottom: 35%;
left: 5%;
}
@media (max-width: 830px) {
.NewsSection {
width: 61%;
}
}
.NewsTabs {
background-color: rgba(77, 77, 77, 0.6);
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
font-weight: bold;
color: #fff;
width: 100%;
height: 43px;
}
.NewsTab {
height: 50%;
margin: 0 10px;
text-align: center;
border-bottom: 1px solid transparent;
}
.NewsTab:hover {
cursor: pointer;
}
.NewsTab.selected {
border-bottom: 2px solid #ffc61e;
}
.NewsContent {
display: block;
height: calc(100% - 43px);
width: 100%;
color: #fff;
}
.NewsContent tbody {
display: inline-block;
height: 100%;
width: 100%;
overflow-y: auto;
scrollbar-width: none;
}
.NewsContent tbody::-webkit-scrollbar {
display: none;
}
.Commit {
margin: 0;
color: #fff;
max-height: 42px;
}
.CommitAuthor {
width: calc(100% * 0.4);
padding-left: 5px;
vertical-align: top;
font-weight: bold;
}
.CommitMessage {
width: calc(100% * 0.6);
}
.CommitMessage span {
display: -webkit-box;
width: 100%;
max-height: 42px;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 5px;
}

View File

@@ -1,171 +0,0 @@
/* eslint-disable indent */
import { invoke } from '@tauri-apps/api/tauri'
import React from 'react'
import Tr from '../../../utils/language'
import './NewsSection.css'
interface IProps {
selected?: string
}
interface IState {
selected: string
news?: JSX.Element
commitList?: JSX.Element[]
}
interface GrasscutterAPIResponse {
commits: {
gc_stable: CommitResponse[]
gc_dev: CommitResponse[]
cultivation: CommitResponse[]
}
}
interface CommitResponse {
sha: string
commit: Commit
}
interface Commit {
author: {
name: string
}
message: string
}
export default class NewsSection extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
selected: props.selected || 'commits',
}
this.setSelected = this.setSelected.bind(this)
this.showNews = this.showNews.bind(this)
}
componentDidMount() {
// Call showNews off the bat
this.showNews()
this.setSelected('commits')
}
setSelected(item: string) {
this.setState({ selected: item }, () => {
this.showNews()
})
}
async showLatestCommits() {
// Just use official API
const response: string = await invoke('req_get', {
url: 'https://api.github.com/repos/Grasscutters/Grasscutter/commits',
})
let grasscutterApiResponse: GrasscutterAPIResponse | null = null
try {
grasscutterApiResponse = JSON.parse(response)
} catch (e) {
grasscutterApiResponse = null
}
let commits: CommitResponse[]
if (grasscutterApiResponse?.commits == null) {
// If it didn't work, try again anyways
const response: string = await invoke('req_get', {
url: 'https://api.github.com/repos/Grasscutters/Grasscutter/commits',
})
commits = JSON.parse(response)
} else {
commits = grasscutterApiResponse.commits.gc_stable
}
// Probably rate-limited
if (!Array.isArray(commits)) return
// Get only first 5
const commitsList = commits.slice(0, 10)
const commitsListHtml = commitsList.map((commitResponse: CommitResponse) => {
return (
<tr className="Commit" id="newsCommitsTable" key={commitResponse.sha}>
<td className="CommitAuthor">
<span>{commitResponse.commit.author.name}</span>
</td>
<td className="CommitMessage">
<span>{commitResponse.commit.message}</span>
</td>
</tr>
)
})
this.setState({
commitList: commitsListHtml,
news: <>{commitsListHtml}</>,
})
return this.state.commitList
}
async showNews() {
let news: JSX.Element | JSX.Element[] = <tr></tr>
switch (this.state.selected) {
case 'commits': {
const commits = await this.showLatestCommits()
if (commits != null) {
news = commits
}
break
}
case 'latest_version':
news = (
<tr>
<td>Latest version: Grasscutter 1.7.0 - Cultivation 1.2.0</td>
</tr>
)
break
default:
news = (
<tr>
<td>Unknown</td>
</tr>
)
break
}
this.setState({
news: <>{news}</>,
})
}
render() {
return (
<div className="NewsSection" id="newsContainer">
<div className="NewsTabs" id="newsTabsContainer">
<div
className={'NewsTab ' + (this.state.selected === 'commits' ? 'selected' : '')}
id="commits"
onClick={() => this.setSelected('commits')}
>
<Tr text="news.latest_commits" />
</div>
<div
className={'NewsTab ' + (this.state.selected === 'latest_version' ? 'selected' : '')}
id="latest_version"
onClick={() => this.setSelected('latest_version')}
>
<Tr text="news.latest_version" />
</div>
</div>
<table className="NewsContent" id="newsContent">
<tbody>{this.state.news}</tbody>
</table>
</div>
)
}
}

View File

@@ -1,162 +0,0 @@
import { fs } from '@tauri-apps/api'
import { dataDir } from '@tauri-apps/api/path'
let configFilePath: string
let defaultConfig: Configuration
;(async () => {
defaultConfig = {
toggle_grasscutter: true,
game_install_path: 'C:\\Program Files\\Genshin Impact\\Genshin Impact game\\GenshinImpact.exe',
grasscutter_with_game: false,
grasscutter_path: '',
java_path: '',
close_action: 0,
startup_launch: false,
last_ip: 'localhost',
last_port: '443',
language: 'en',
custom_background: '',
use_theme_background: false,
cert_generated: false,
theme: 'default',
https_enabled: false,
debug_enabled: false,
patch_rsa: true,
use_internal_proxy: true,
wipe_login: false,
horny_mode: false,
auto_mongodb: false,
un_elevated: false,
redirect_more: false,
launch_args: '',
// Linux stuff
grasscutter_elevation: 'None',
}
})()
/**
* 'close_action': 0 = close, 1 = tray
*/
export interface Configuration {
toggle_grasscutter: boolean
game_install_path: string
grasscutter_with_game: boolean
grasscutter_path: string
java_path: string
close_action: number
startup_launch: boolean
last_ip: string
last_port: string
language: string
custom_background: string
use_theme_background: boolean
cert_generated: boolean
theme: string
https_enabled: boolean
debug_enabled: boolean
patch_rsa: boolean
use_internal_proxy: boolean
wipe_login: boolean
horny_mode: boolean
swag_mode?: boolean
auto_mongodb: boolean
un_elevated: boolean
redirect_more: boolean
launch_args: string
// Linux stuff
grasscutter_elevation: string
// Swag stuff
akebi_path?: string
migoto_path?: string
reshade_path?: string
last_extras?: {
migoto: boolean
akebi: boolean
reshade: boolean
}
}
export async function setConfigOption<K extends keyof Configuration>(key: K, value: Configuration[K]): Promise<void> {
const config = await getConfig()
config[key] = value
await saveConfig(<Configuration>config)
}
export async function getConfigOption<K extends keyof Configuration>(key: K): Promise<Configuration[K]> {
const config = await getConfig()
const defaults = defaultConfig
return config[key] === null || config[key] === undefined ? defaults[key] : config[key]
}
export async function getConfig() {
const raw = await readConfigFile()
let parsed: Configuration = defaultConfig
try {
parsed = <Configuration>JSON.parse(raw)
} catch (e) {
// We could not open the file
console.log(e)
// TODO: Create a popup saying the config file is corrupted.
}
return parsed
}
export async function saveConfig(obj: Configuration) {
const raw = JSON.stringify(obj)
await writeConfigFile(raw)
}
async function readConfigFile() {
const local = await dataDir()
if (!configFilePath) configFilePath = local + 'cultivation/configuration.json'
// Ensure Cultivation dir exists
const dirs = await fs.readDir(local)
if (!dirs.find((fileOrDir) => fileOrDir?.name === 'cultivation')) {
// Create dir
await fs.createDir(local + 'cultivation').catch((e) => console.log(e))
}
const innerDirs = await fs.readDir(local + 'cultivation')
// Create grasscutter dir for potential installation
if (!innerDirs.find((fileOrDir) => fileOrDir?.name === 'grasscutter')) {
// Create dir
await fs.createDir(local + 'cultivation/grasscutter').catch((e) => console.log(e))
}
const dataFiles = await fs.readDir(local + 'cultivation')
// Ensure config exists
if (!dataFiles.find((fileOrDir) => fileOrDir?.name === 'configuration.json')) {
// Create config file
const file: fs.FsTextFileOption = {
path: configFilePath,
contents: JSON.stringify(defaultConfig),
}
await fs.writeFile(file)
}
// Finally, read the file
return await fs.readTextFile(configFilePath)
}
async function writeConfigFile(raw: string) {
// All external config functions call readConfigFile, which ensure files exists
await fs.writeFile({
path: configFilePath,
contents: raw,
})
}

View File

@@ -1,182 +0,0 @@
import { invoke } from '@tauri-apps/api/tauri'
import { listen } from '@tauri-apps/api/event'
import { byteToString } from './string'
export default class DownloadHandler {
downloads: {
path: string
progress: number
total: number
total_downloaded: number
status: string
startTime: number
error?: string
speed?: string
onFinish?: () => void
}[]
// Pass tauri invoke function
constructor() {
this.downloads = []
listen('download_progress', ({ payload }) => {
// @ts-expect-error Payload may be unknown but backend always returns this object
const obj: {
downloaded: string
total: string
path: string
total_downloaded: string
} = payload
const index = this.downloads.findIndex((download) => download.path === obj.path)
this.downloads[index].progress = parseInt(obj.downloaded, 10)
this.downloads[index].total = parseInt(obj.total, 10)
this.downloads[index].total_downloaded = parseInt(obj.total_downloaded, 10)
// Set download speed based on startTime
const now = Date.now()
const timeDiff = now - this.downloads[index].startTime
let speed = (this.downloads[index].progress / timeDiff) * 1000
if (this.downloads[index].total === 0) {
// If our total is 0, then we are downloading a file without a size
// Calculate the average speed based total_downloaded and startTme
speed = (this.downloads[index].total_downloaded / timeDiff) * 1000
}
this.downloads[index].speed = byteToString(speed) + '/s'
})
listen('download_finished', ({ payload }) => {
// Remove from array
const filename = payload
// set status to finished
const index = this.downloads.findIndex((download) => download.path === filename)
this.downloads[index].status = 'finished'
// Call onFinish callback
if (this.downloads[index]?.onFinish) {
// @ts-expect-error onFinish is checked for existence before being called
this.downloads[index]?.onFinish()
}
})
listen('download_error', ({ payload }) => {
// @ts-expect-error shut up typescript
const errorData: {
path: string
error: string
} = payload
// Set download to error
const index = this.downloads.findIndex((download) => download.path === errorData.path)
this.downloads[index].status = 'error'
this.downloads[index].error = errorData.error
})
// Extraction events
listen('extract_start', ({ payload }) => {
// Find the download that is extracting and set it's status as such
const index = this.downloads.findIndex((download) => download.path === payload)
this.downloads[index].status = 'extracting'
})
listen('extract_end', ({ payload }) => {
// @ts-expect-error shut up typescript
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)
this.downloads[index].status = 'finished'
})
}
getDownloads() {
return this.downloads
}
downloadingJar() {
// Kinda hacky but it works
return this.downloads.some((d) => d.path.includes('grasscutter.zip') && d.status != ('finished' || 'error'))
}
downloadingFullBuild() {
// Kinda hacky but it works
return this.downloads.some((d) => d.path.includes('GrasscutterCulti') && d.status != ('finished' || 'error'))
}
downloadingResources() {
// Kinda hacky but it works
return this.downloads.some((d) => d.path.includes('resources') && d.status != ('finished' || 'error'))
}
downloadingRepo() {
return this.downloads.some((d) => d.path.includes('grasscutter_repo.zip') && d.status != ('finished' || 'error'))
}
downloadingMigoto() {
return this.downloads.some((d) => d.path.includes('3dmigoto') && d.status != ('finished' || 'error'))
}
addDownload(url: string, path: string, onFinish?: () => void) {
// Begin download from rust backend, don't add if the download addition fails
invoke('download_file', { url, path })
const obj = {
path,
progress: 0,
total: 0,
total_downloaded: 0,
status: 'downloading',
startTime: Date.now(),
onFinish,
}
this.downloads.push(obj)
}
stopDownload(path: string) {
// Stop download and remove from list.
invoke('stop_download', { path })
// Remove from list
const index = this.downloads.findIndex((download) => download.path === path)
this.downloads.splice(index, 1)
}
getDownloadProgress(path: string) {
const index = this.downloads.findIndex((download) => download.path === path)
return this.downloads[index] || null
}
getDownloadSize(path: string) {
const index = this.downloads.findIndex((download) => download.path === path)
return byteToString(this.downloads[index].total) || null
}
getTotalAverage() {
const files = this.downloads.filter((d) => d.status === 'downloading')
const total = files.reduce((acc, d) => acc + d.total, 0)
const progress = files.reduce((acc, d) => (d.progress !== 0 ? acc + d.progress : acc + d.total_downloaded), 0)
let speedStr = '0 B/s'
// Get download speed based on startTimes
if (files.length > 0) {
const now = Date.now()
const timeDiff = now - files[0].startTime
const speed = (progress / timeDiff) * 1000
speedStr = byteToString(speed) + '/s'
}
return {
average: (progress / total) * 100 || 0,
files: this.downloads.filter((d) => d.status === 'downloading').length,
extracting: this.downloads.filter((d) => d.status === 'extracting').length,
totalSize: total,
speed: speedStr,
}
}
}

View File

@@ -1,72 +0,0 @@
import { invoke } from '@tauri-apps/api'
import { getConfig } from './configuration'
export async function getGameExecutable() {
const config = await getConfig()
if (!config.game_install_path) {
return null
}
const pathArr = config.game_install_path.replace(/\\/g, '/').split('/')
return pathArr[pathArr.length - 1]
}
export async function getGrasscutterJar() {
const config = await getConfig()
if (!config.grasscutter_path) {
return null
}
const pathArr = config.grasscutter_path.replace(/\\/g, '/').split('/')
return pathArr[pathArr.length - 1]
}
export async function getGameFolder() {
const config = await getConfig()
if (!config.game_install_path) {
return null
}
const pathArr = config.game_install_path.replace(/\\/g, '/').split('/')
pathArr.pop()
const path = pathArr.join('/')
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,235 +0,0 @@
import { invoke } from '@tauri-apps/api'
import { getConfigOption } from './configuration'
// Generated with https://transform.tools/json-to-typescript I'm lazy cry about it
interface GamebananaResponse {
_idRow: number
_sModelName: string
_sSingularTitle: string
_sIconClasses: string
_sName: string
_sProfileUrl: string
_aPreviewMedia: PreviewMedia
_tsDateAdded: number
_bHasFiles: boolean
_aSubmitter: Submitter
_aRootCategory: RootCategory
_bIsNsfw: boolean
_sInitialVisibility: string
_nLikeCount?: number
_bIsOwnedByAccessor: boolean
_nViewCount?: number
_nPostCount?: number
_tsDateUpdated?: number
}
interface PreviewMedia {
_aImages?: Image[]
_aMetadata?: Metadata
}
interface Image {
_sType: string
_sBaseUrl: string
_sFile: string
_sFile530?: string
_sFile100: string
_sFile220?: string
_sCaption?: string
}
interface Metadata {
_sState?: string
_sSnippet: string
_nPostCount?: number
_nBounty?: number
}
interface Submitter {
_idRow: number
_sName: string
_bIsOnline: boolean
_bHasRipe: boolean
_sProfileUrl: string
_sAvatarUrl: string
_sUpicUrl?: string
_aClearanceLevels?: string[]
_sHdAvatarUrl?: string
}
interface RootCategory {
_sName: string
_sProfileUrl: string
_sIconUrl: string
}
export interface ModData {
id: number
name: string
images: string[]
dateadded: number
submitter: {
name: string
url: string
}
nsfw: boolean
likes: number
views: number
type: string
}
export interface PartialModData {
name: string
images: string[]
submitter: {
name: string
}
likes: number
views: number
}
interface GamebananaDownloads {
_bIsTrashed: boolean
_bIsWithheld: boolean
_aFiles: File[]
_sLicense: string
}
interface File {
_idRow: number
_sFile: string
_nFilesize: number
_sDescription: string
_tsDateAdded: number
_nDownloadCount: number
_sAnalysisState: string
_sDownloadUrl: string
_sMd5Checksum: string
_sClamAvResult: string
_sAnalysisResult: string
_bContainsExe: boolean
}
interface ModDownload {
filename: string
downloadUrl: string
filesize: number
containsExe: boolean
}
export async function getMods(mode: string, page: number, search: string) {
let modList: GamebananaResponse[] = []
if (search.length > 0) {
let hadMods = true
let page = 1
while (hadMods) {
const resp = JSON.parse(
await invoke('list_submissions', {
mode,
page: '' + page,
search: search,
})
)
const total = resp._aMetadata._nRecordCount
if (page > total / 15) hadMods = false
modList = [...modList, ...resp._aRecords]
page++
}
return formatGamebananaData(modList)
}
const resp = JSON.parse(
await invoke('list_submissions', {
mode,
page: '' + page,
search: '',
})
)
modList = [...modList, ...resp]
return formatGamebananaData(modList)
}
export async function formatGamebananaData(obj: GamebananaResponse[]) {
if (!obj) return []
return obj
.map((itm) => {
const img = itm?._aPreviewMedia?._aImages
return {
id: itm._idRow,
name: itm._sName,
images: img
? img.map((i) => {
return i._sBaseUrl + '/' + i._sFile220
})
: [],
dateadded: itm._tsDateAdded,
submitter: {
name: itm._aSubmitter._sName,
url: itm._aSubmitter._sProfileUrl,
},
nsfw: itm._bIsNsfw,
likes: itm?._nLikeCount || 0,
views: itm?._nViewCount || 0,
type: itm._sSingularTitle,
} as ModData
})
.filter((itm) => itm.type === 'Mod')
}
export async function getInstalledMods() {
const migotoPath = await getConfigOption('migoto_path')
if (!migotoPath) return []
const mods = (await invoke('list_mods', {
path: migotoPath,
})) as Record<string, string>
// These are returned as JSON strings, so we have to parse them
return Object.keys(mods).map((path) => {
const info = JSON.parse(mods[path]) as ModData | PartialModData
const modPathArr = path.replace(/\\/g, '/').split('/')
// If there is a file in this path, remove it from the path
if (modPathArr[modPathArr.length - 1].includes('.')) modPathArr.pop()
return {
path: modPathArr.join('/'),
info,
}
})
}
export async function getModDownload(modId: string) {
const resp = JSON.parse(
await invoke('get_download_links', {
modId,
})
) as GamebananaDownloads
return formatDownloadsData(resp)
}
export async function formatDownloadsData(obj: GamebananaDownloads) {
if (!obj) return []
return obj._aFiles.map((itm) => {
return {
filename: itm._sFile,
downloadUrl: `https://files.gamebanana.com/mods/${itm._sFile}`,
filesize: itm._nFilesize,
containsExe: itm._bContainsExe,
} as ModDownload
})
}

View File

@@ -1,132 +0,0 @@
import { invoke } from '@tauri-apps/api'
import React from 'react'
import { getConfigOption } from './configuration'
interface IProps {
text: string
}
interface IState {
language: string
translated_text: string
}
export default class Tr extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
language: 'en',
translated_text: '',
}
}
async componentDidMount() {
const { text } = this.props
let language = await getConfigOption('language')
// Get translation file
if (!language) language = 'en'
const response = await invoke('get_lang', { lang: language })
const default_resp = await invoke('get_lang', { lang: 'en' })
const translation_obj = JSON.parse((response as string) || '{}')
const default_obj = JSON.parse((default_resp as string) || '{}')
// Traversal
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 {
translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as string)
}
}
// 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() {
return this.state.translated_text
}
}
export async function getLanguages() {
const resp: {
[key: string]: string
} = await invoke('get_languages')
const lang_list: {
[key: string]: string
}[] = []
Object.keys(resp).forEach((k) => {
const parsed = JSON.parse(resp[k])
if (parsed.lang_name) {
lang_list.push({ [k.split('.json')[0]]: parsed.lang_name })
}
})
return lang_list
}
export async function translate(text: string) {
const language = (await getConfigOption('language')) || 'en'
const translation_json = JSON.parse((await invoke('get_lang', { lang: language })) || '{}')
const default_json = JSON.parse(await invoke('get_lang', { lang: 'en' }))
// Traversal
if (text.includes('.')) {
const keys = text.split('.')
let translation: string | Record<string, string> = translation_json
for (let i = 0; i < keys.length; i++) {
if (!translation) {
translation = ''
} else {
translation = typeof translation !== 'string' ? translation[keys[i]] : (translation as 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
} else {
return translation_json[text] || ''
}
}

View File

@@ -1,71 +0,0 @@
import { invoke } from '@tauri-apps/api'
import { getConfigOption } from './configuration'
export async function getModsFolder() {
const migotoPath = await getConfigOption('migoto_path')
if (!migotoPath) return null
// Remove exe from path
const pathArr = migotoPath.replace(/\\/g, '/').split('/')
pathArr.pop()
return pathArr.join('/') + '/Mods/'
}
export async function disableMod(modId: string) {
const path = (await getModsFolder()) + modId
const pathExists = await invoke('dir_exists', {
path,
})
if (!pathExists) return console.log("Path doesn't exist")
const modName = path.replace(/\\/g, '/').split('/').pop()
await invoke('rename', {
path,
newName: `DISABLED_${modName}`,
})
}
export async function enableMod(modId: string) {
const path = (await getModsFolder()) + `DISABLED_${modId}`
const modName = path.replace(/\\/g, '/').split('/').pop()
const pathExists = await invoke('dir_exists', {
path,
})
if (!pathExists) return console.log("Path doesn't exist")
if (!modName?.includes('DISABLED_')) return
const newName = modName.replace('DISABLED_', '')
await invoke('rename', {
path,
newName,
})
}
export async function getModFolderName(modId: string) {
const modsFolder = await getModsFolder()
if (!modsFolder) return null
const modEnabled = await invoke('dir_exists', {
path: modsFolder + modId,
})
const modDisabled = await invoke('dir_exists', {
path: modsFolder + 'DISABLED_' + modId,
})
if (!modEnabled && !modDisabled) return null
if (modEnabled) return modId
if (modDisabled) return 'DISABLED_' + modId
}
export async function modIsEnabled(modId: string) {
return !(await getModFolderName(modId))?.includes('DISABLED_')
}

View File

@@ -1,15 +0,0 @@
import { ReportHandler } from 'web-vitals'
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
})
}
}
export default reportWebVitals

View File

@@ -1,10 +0,0 @@
import { invoke } from '@tauri-apps/api'
// Patch file from: https://github.com/34736384/RSAPatch/
export async function patchGame() {
return invoke('patch_game')
}
export async function unpatchGame() {
return invoke('unpatch_game')
}

View File

@@ -1,44 +0,0 @@
import { invoke } from '@tauri-apps/api'
export async function toggleEncryption(path: string) {
let serverConf
try {
serverConf = JSON.parse(
await invoke('read_file', {
path,
})
)
} catch (e) {
console.log(`Server config at ${path} not found or invalid. Be sure to run the server at least once to generate it`)
return
}
const enabled = serverConf.server.http.encryption.useEncryption
serverConf.server.http.encryption.useEncryption = !enabled
serverConf.server.http.encryption.useInRouting = !enabled
// Write file
await invoke('write_file', {
path,
contents: JSON.stringify(serverConf, null, 2),
})
}
export async function encryptionEnabled(path: string) {
let serverConf
try {
serverConf = JSON.parse(
await invoke('read_file', {
path,
})
)
} catch (e) {
console.log(`Server config at ${path} not found or invalid. Be sure to run the server at least once to generate it`)
return false
}
return serverConf.server.http.encryption.useEncryption
}

View File

@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'

View File

@@ -1,11 +0,0 @@
export function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export function byteToString(bytes: number) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
if (bytes === 0) return '0 Bytes'
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString(), 10)
if (i === 0) return `${bytes} ${sizes[i]}`
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`
}

View File

@@ -1,146 +0,0 @@
import { invoke } from '@tauri-apps/api'
import { dataDir } from '@tauri-apps/api/path'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import { getConfig, setConfigOption } from './configuration'
interface Theme {
name: string
version: string
description: string
// Included custom CSS and JS files
includes: {
css: string[]
js: string[]
}
customBackgroundURL?: string
customBackgroundPath?: string
}
interface BackendThemeList {
json: string
path: string
}
interface ThemeList extends Theme {
path: string
}
const defaultTheme = {
name: 'default',
version: '1.0.0',
description: 'Default theme',
includes: {
css: [],
js: [],
},
path: 'default',
}
export async function getThemeList() {
// Do some invoke to backend to get the theme list
const themes = (await invoke('get_theme_list', {
dataDir: `${await dataDir()}cultivation`,
})) as BackendThemeList[]
const list: ThemeList[] = [
// ALWAYS include default theme
{
name: 'default',
version: '1.0.0',
description: 'Default theme',
includes: {
css: [],
js: [],
},
path: 'default',
},
]
themes.forEach((t) => {
let obj
try {
obj = JSON.parse(t.json)
} catch (e) {
console.error(e)
}
list.push({ ...obj, path: t.path })
})
return list
}
export async function getTheme(name: string) {
const themes = await getThemeList()
return themes.find((t) => t.name === name) || defaultTheme
}
export async function loadTheme(theme: ThemeList, document: Document) {
// Get config, since we will set the custom background in there
const config = await getConfig()
// We are going to dynamically load stylesheets into the document
const head = document.head
// Get all CSS includes
const cssIncludes = theme.includes.css
const jsIncludes = theme.includes.js
// Load CSS files
if (cssIncludes) {
cssIncludes?.forEach((css) => {
if (!css) return
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = convertFileSrc(theme.path + '/' + css)
head.appendChild(link)
})
}
// Load JS files
if (jsIncludes) {
jsIncludes.forEach((js) => {
if (!js) return
const script = document.createElement('script')
script.src = convertFileSrc(theme.path + '/' + js)
head.appendChild(script)
})
}
// Set custom background
if (theme.customBackgroundURL) {
// If the custom bg is already set don't overwrite unless user wants to force the new background
if (config.custom_background === '' || config.use_theme_background) {
config.custom_background = theme.customBackgroundURL
}
}
// Set custom background
if (theme.customBackgroundPath) {
const bgPath = (await dataDir()).replace(/\\/g, '/') + 'cultivation/bg/'
const imageName = theme.customBackgroundPath.split('/').pop()
// Save the background to our data dir
await invoke('copy_file', {
path: theme.path + '/' + theme.customBackgroundPath,
newPath: bgPath,
})
// Set the background
// If the custom bg is already set don't overwrite unless user wants to force the new background
if (config.custom_background === '' || config.use_theme_background) {
config.custom_background = bgPath + imageName
}
}
// Write config
await setConfigOption('custom_background', config.custom_background)
return
}

View File

@@ -1,33 +0,0 @@
import { invoke } from '@tauri-apps/api'
import { listen } from '@tauri-apps/api/event'
interface UnzipPayload {
file: string
new_folder: string
}
export function unzip(
file: string,
dest: string,
topLevelStrip?: boolean,
folderIfLoose?: boolean
): Promise<UnzipPayload> {
return new Promise((resolve) => {
invoke('unzip', {
zipfile: file,
destpath: dest,
topLevelStrip,
folderIfLoose,
})
listen('extract_end', ({ payload }) => {
console.log(payload)
console.log(file)
// @ts-expect-error Payload is an object
if (payload?.file === file) {
resolve(payload as UnzipPayload)
}
})
})
}

9330
yarn.lock

File diff suppressed because it is too large Load Diff