Compare commits

..

29 Commits

Author SHA1 Message Date
SpikeHD
c735b8936a Merge pull request #151 from Grasscutters/more_cli
Passable amount of CLI options
2023-04-22 16:16:56 -07:00
SpikeHD
c88e3ce3a7 Merge pull request #152 from NotThorny/more_cli-2
Arg fixes
2023-04-22 16:15:12 -07:00
Thoronium
e5d151f512 Formatting
For rustfmt
2023-04-22 17:08:39 -06:00
Thoronium
361737c00d Unpatch when exiting disabled GUI 2023-04-22 16:56:59 -06:00
Thoronium
0f08bb5fbf Update comment 2023-04-22 16:10:07 -06:00
Thoronium
df674abd94 Format 2023-04-22 16:06:04 -06:00
Thoronium
c21c5ac0b1 Fix launch server from args 2023-04-22 16:05:22 -06:00
Thoronium
6ac0182784 Fix args 2023-04-22 15:41:54 -06:00
SpikeHD
2fa203163d game args err check 2023-04-21 18:25:23 -07:00
SpikeHD
75c6481778 another typo 2023-04-21 18:13:49 -07:00
SpikeHD
12b60d3b2a typo oops 2023-04-21 18:13:12 -07:00
SpikeHD
88b5b40300 lint and format 2023-04-21 18:10:05 -07:00
SpikeHD
45ecfcbeb3 fix host option 2023-04-21 18:09:27 -07:00
SpikeHD
e6492825dc optionally patch/unpatch 2023-04-21 18:06:53 -07:00
SpikeHD
3141bcea41 game options 2023-04-21 17:46:31 -07:00
SpikeHD
64a04e927c set host 2023-04-21 17:37:21 -07:00
SpikeHD
4bcfd7ec09 more options 2023-04-21 17:03:04 -07:00
SpikeHD
a67eca49e5 real arg parse 2023-04-21 16:50:53 -07:00
SpikeHD
48dff50a5d config reading 2023-04-21 16:28:56 -07:00
SpikeHD
5480af7835 quick fmt 2023-04-21 12:49:09 -07:00
SpikeHD
829b9822cb re-add opening as admin 2023-04-21 12:47:44 -07:00
SpikeHD
a6716e80f4 new release notifications 2023-04-21 12:47:17 -07:00
SpikeHD
6962518ced Merge pull request #148 from NotThorny/NonElevated
Non-elevated option & bugfix
2023-04-14 22:48:28 -07:00
Thoronium
09d0d8287f Run prettier
I forgor before
2023-04-14 23:36:25 -06:00
Thoronium
7bfa5e8e11 Add non-elevated game option 2023-04-14 23:30:54 -06:00
Thoronium
5de80f1655 Auto-remove spaces from address 2023-04-14 23:30:54 -06:00
Thoronium
33ce547bb2 Remove unnecessary duplicate patch 2023-04-14 23:30:54 -06:00
Thoronium
864f9f199c Fix mods appearing suck on extraction 2023-04-14 23:30:54 -06:00
Thoronium
5d8e4d7311 Fix download buttons getting stuck 2023-04-14 23:30:54 -06:00
34 changed files with 485 additions and 87 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"rust-analyzer.linkedProjects": [".\\src-tauri\\Cargo.toml"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "cultivation",
"version": "1.0.25",
"version": "1.0.26",
"private": true,
"dependencies": {
"@tauri-apps/api": "^1.0.0-rc.5",

27
src-tauri/Cargo.lock generated
View File

@@ -76,6 +76,16 @@ version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
[[package]]
name = "args"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b7432c65177b8d5c032d56e020dd8d407e939468479fc8c300e2d93e6d970b"
dependencies = [
"getopts",
"log",
]
[[package]]
name = "asn1-rs"
version = "0.3.1"
@@ -780,11 +790,13 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
name = "cultivation"
version = "0.1.0"
dependencies = [
"args",
"cc",
"ctrlc",
"duct",
"file_diff",
"futures-util",
"getopts",
"http",
"hudsucker",
"is_elevated",
@@ -1387,6 +1399,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.1.16"
@@ -4624,6 +4645,12 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-xid"
version = "0.2.3"

View File

@@ -26,6 +26,10 @@ sudo = "0.6.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.7", features = ["api-all"] }
# Arg parsing
args = "2.0"
getopts = "0.2"
# Access system process info.
sysinfo = "0.24.6"

View File

@@ -29,7 +29,8 @@
"use_proxy": "使用内置代理",
"wipe_login": "清除登录缓存",
"horny_mode": "Horny 模式",
"auto_mongodb": "自动启动 MongoDB"
"auto_mongodb": "自动启动 MongoDB",
"un_elevated": "非提升运行游戏(无管理员)"
},
"downloads": {
"grasscutter_fullbuild": "下载 Grasscutter 一体化",

View File

@@ -29,7 +29,8 @@
"use_proxy": "使用內建代理伺服器",
"wipe_login": "擦除登錄緩存",
"horny_mode": "Horny模式",
"auto_mongodb": "自動啟動 MongoDB"
"auto_mongodb": "自動啟動 MongoDB",
"un_elevated": "在不升高的情况下运行游戏(没有管理员)。"
},
"downloads": {
"grasscutter_fullbuild": "下載Grasscutter多合一下載",

View File

@@ -28,7 +28,8 @@
"use_proxy": "Gebruik interne proxy",
"wipe_login": "Wis de inlogcache",
"horny_mode": "Geile modus",
"auto_mongodb": "Start automatisch MongoDB"
"auto_mongodb": "Start automatisch MongoDB",
"un_elevated": "Führen Sie das Spiel nicht erhöht aus (kein Admin)"
},
"downloads": {
"grasscutter_fullbuild": "Alles in Einem Grasscutter Daten herunterladen",

View File

@@ -29,7 +29,8 @@
"use_proxy": "Use Internal Proxy",
"wipe_login": "Wipe Login Cache",
"horny_mode": "Horny Mode",
"auto_mongodb": "Automatically Start MongoDB"
"auto_mongodb": "Automatically Start MongoDB",
"un_elevated": "Run the game non-elevated (no admin)"
},
"downloads": {
"grasscutter_fullbuild": "Download Grasscutter All-in-One",

View File

@@ -29,7 +29,8 @@
"use_proxy": "Usar proxy interno",
"wipe_login": "Borrar caché de inicio de sesión",
"horny_mode": "Modo cachondo",
"auto_mongodb": "Iniciar automáticamente MongoDB"
"auto_mongodb": "Iniciar automáticamente MongoDB",
"un_elevated": "Ejecutar el juego no elevado (no admin)"
},
"downloads": {
"grasscutter_fullbuild": "Descargar Datos todo en uno de Grasscutter",

View File

@@ -29,7 +29,8 @@
"use_proxy": "Utiliser un proxy interne",
"wipe_login": "Effacer le cache de connexion",
"horny_mode": "Mode excitation",
"auto_mongodb": "Démarrer automatiquement MongoDB"
"auto_mongodb": "Démarrer automatiquement MongoDB",
"un_elevated": "Exécuter le jeu sans élévation (pas d'administrateur)"
},
"downloads": {
"grasscutter_fullbuild": "Telecharger Grasscutter tout-en-un",

View File

@@ -28,7 +28,8 @@
"use_proxy": "Gunakan Proxy Internal",
"wipe_login": "Menghapus Cache Login",
"horny_mode": "Mode Terangsang",
"auto_mongodb": "Mulai MongoDB secara otomatis"
"auto_mongodb": "Mulai MongoDB secara otomatis",
"un_elevated": "Jalankan game yang tidak ditinggikan (tanpa admin)"
},
"downloads": {
"grasscutter_fullbuild": "Sedang Mendownload Grasscutter Semua Dalam Satu",

View File

@@ -29,7 +29,8 @@
"use_proxy": "내부 프록시 사용",
"wipe_login": "로그인 캐시 지우기",
"horny_mode": "Horny 모드",
"auto_mongodb": "MongoDB 자동 시작"
"auto_mongodb": "MongoDB 자동 시작",
"un_elevated": "게임 비상승 실행(관리자 없음)"
},
"downloads": {
"grasscutter_fullbuild": "올인원 Grasscutter 다운로드",

View File

@@ -27,7 +27,8 @@
"use_proxy": "Izmantot iekšējo starpniekserveri",
"wipe_login": "Noslaucīt pieteikšanās kešatmiņu",
"horny_mode": "Uzbudināts režīms",
"auto_mongodb": "Automātiski startējiet MongoDB"
"auto_mongodb": "Automātiski startējiet MongoDB",
"un_elevated": "Palaist spēli bez paaugstinājuma (bez administratora)"
},
"downloads": {
"grasscutter_fullbuild": "Lejupielādējiet Grasscutter viss vienā",

View File

@@ -28,7 +28,8 @@
"use_proxy": "Gebruik Interne Proxy",
"wipe_login": "Login cache wissen",
"horny_mode": "Geile modus",
"auto_mongodb": "Start automatisch MongoDB"
"auto_mongodb": "Start automatisch MongoDB",
"un_elevated": "Voer het spel uit zonder hoogtevrees (geen admin)"
},
"downloads": {
"grasscutter_fullbuild": "Grasscutter Alles-in-één Downloaden",

View File

@@ -29,7 +29,8 @@
"use_proxy": "Usar Proxy Interno",
"wipe_login": "Limpar Cache de Login",
"horny_mode": "Modo com tesão",
"auto_mongodb": "Iniciar MongoDB Automaticamente"
"auto_mongodb": "Iniciar MongoDB Automaticamente",
"un_elevated": "Executar o jogo não-elevated (sem admin)"
},
"downloads": {
"grasscutter_fullbuild": "Baixar o Grasscutter Tudo-em-Um",

View File

@@ -28,7 +28,8 @@
"use_proxy": "Использовать встроенный Прокси",
"wipe_login": "Очистить кэш входа в систему",
"horny_mode": "роговой режим",
"auto_mongodb": "Автоматически запускать MongoDB"
"auto_mongodb": "Автоматически запускать MongoDB",
"un_elevated": "Запустите игру в неэлегантном режиме (без администратора)"
},
"downloads": {
"grasscutter_fullbuild": "Скачать все в одном Grasscutter",

View File

@@ -29,7 +29,8 @@
"use_proxy": "Sử dụng proxy nội bộ",
"wipe_login": "Tẩy sạch cache đăng nhập",
"horny_mode": "Chế độ hứng tình",
"auto_mongodb": "Tự động khởi động MongoDB"
"auto_mongodb": "Tự động khởi động MongoDB",
"un_elevated": "Chạy trò chơi không nâng cao (không có quản trị viên)"
},
"downloads": {
"grasscutter_fullbuild": "Tải Grasscutter tất cả trong một",

44
src-tauri/src/config.rs Normal file
View File

@@ -0,0 +1,44 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::string::String;
#[derive(Serialize, Deserialize, Debug)]
pub struct Configuration {
pub toggle_grasscutter: bool,
pub game_install_path: String,
pub grasscutter_with_game: bool,
pub grasscutter_path: String,
pub java_path: String,
pub close_action: u64,
pub startup_launch: bool,
pub last_ip: String,
pub last_port: String,
pub language: String,
pub customBackground: String,
pub cert_generated: bool,
pub theme: String,
pub https_enabled: bool,
pub debug_enabled: bool,
pub patch_rsa: bool,
pub use_internal_proxy: bool,
pub wipe_login: bool,
pub horny_mode: bool,
pub auto_mongodb: bool,
pub un_elevated: 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();
let config: Configuration = serde_json::from_str(&config).unwrap();
config
}

View File

@@ -1,5 +1,5 @@
use crate::system_helpers::*;
use std::path::{ Path, PathBuf };
use std::path::{Path, PathBuf};
#[tauri::command]
pub async fn get_lang(window: tauri::Window, lang: String) -> String {
@@ -55,4 +55,4 @@ pub fn emit_lang_err(window: tauri::Window, msg: String) {
res_hash.insert("error".to_string(), msg);
window.emit("lang_error", &res_hash).unwrap();
}
}

View File

@@ -3,8 +3,11 @@
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};
@@ -18,11 +21,14 @@ use sysinfo::{Pid, ProcessExt, System, SystemExt};
use crate::admin::reopen_as_admin;
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;
@@ -35,24 +41,116 @@ fn try_flush() {
std::io::stdout().flush().unwrap_or(())
}
fn has_arg(args: &[String], arg: &str) -> bool {
args.contains(&arg.to_string())
}
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(
"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,
);
async fn arg_handler(args: &[String]) {
if has_arg(args, "--proxy") {
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(String::new());
// Patch if needed
if args.value_of("patch")? {
patch::patch_game().await;
}
if args.value_of("non-elevated-game")? {
system_helpers::run_un_elevated(game_path.to_string(), Some(game_args))
} else {
system_helpers::run_program(game_path.to_string(), Some(game_args))
}
}
if args.value_of("server")? {
let server_jar = config.grasscutter_path;
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;
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");
connect(8035, pathbuf.to_str().unwrap().to_string()).await;
}
Ok(args)
}
fn main() {
fn main() -> Result<(), ArgsError> {
let args: Vec<String> = std::env::args().collect();
let parsed_args = block_on(parse_args(&args)).unwrap();
if !is_elevated() && !has_arg(&args, "--no-admin") {
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!("===============================================================================");
@@ -70,16 +168,15 @@ fn main() {
exe_path.pop();
std::env::set_current_dir(&exe_path).unwrap();
block_on(arg_handler(&args));
// For disabled GUI
ctrlc::set_handler(|| {
disconnect();
block_on(patch::unpatch_game());
std::process::exit(0);
})
.unwrap_or(());
if !has_arg(&args, "--no-gui") {
if !parsed_args.value_of("no-gui")? {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
enable_process_watcher,
@@ -105,8 +202,10 @@ fn main() {
system_helpers::set_migoto_delay,
system_helpers::wipe_registry,
system_helpers::get_platform,
system_helpers::run_un_elevated,
proxy::set_proxy_addr,
proxy::generate_ca_files,
release::get_latest_release,
unzip::unzip,
file_helpers::rename,
file_helpers::dir_create,
@@ -139,6 +238,11 @@ fn main() {
// Always disconnect upon closing the program
disconnect();
// Always unpatch game upon closing the program
block_on(patch::unpatch_game());
Ok(())
}
#[tauri::command]
@@ -211,11 +315,11 @@ fn restart_grasscutter(window: tauri::Window) -> bool {
// Kill it
if process.kill() {
// Also kill the cmd it was open in
if let Some(parent) = system.process(Pid::from(process.parent().unwrap())) {
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") {
if process_gc.cmd().last().unwrap().contains("grasscutter") {
process_gc.kill();
}
}

59
src-tauri/src/patch.rs Normal file
View File

@@ -0,0 +1,59 @@
use crate::config;
use crate::file_helpers;
use crate::system_helpers;
use std::path::PathBuf;
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
}
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
}
pub async fn get_game_rsa_path() -> Option<String> {
let config = config::get_config();
let mut game_folder = PathBuf::from(config.game_install_path);
game_folder.pop();
Some(format!("{}/", game_folder.to_str().unwrap()).replace('\\', "/"))
}

View File

@@ -41,7 +41,14 @@ struct ProxyHandler;
#[tauri::command]
pub fn set_proxy_addr(addr: String) {
*SERVER.lock().unwrap() = addr;
if addr.contains(' ') {
let addr2 = addr.replace(' ', "");
*SERVER.lock().unwrap() = addr2;
} else {
*SERVER.lock().unwrap() = addr;
}
println!("Set server to {}", SERVER.lock().unwrap());
}
#[async_trait]
@@ -88,11 +95,7 @@ impl HttpHandler for ProxyHandler {
async fn should_intercept(&mut self, _ctx: &HttpContext, _req: &Request<Body>) -> bool {
let uri = _req.uri().to_string();
if uri.contains("hoyoverse.com") || uri.contains("mihoyo.com") || uri.contains("yuanshen.com") {
true
} else {
false
}
uri.contains("hoyoverse.com") || uri.contains("mihoyo.com") || uri.contains("yuanshen.com")
}
}

32
src-tauri/src/release.rs Normal file
View File

@@ -0,0 +1,32 @@
#[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();
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

@@ -11,7 +11,13 @@ use registry::{Data, Hive, Security};
#[tauri::command]
pub fn run_program(path: String, args: Option<String>) {
// Without unwrap_or, this can crash when UAC prompt is denied
open::that(format!("{} {}", &path, &args.unwrap_or_else(|| "".into()))).unwrap_or(());
match open::with(
format!("{} {}", path, args.unwrap_or_else(|| "".into())),
path.clone(),
) {
Ok(_) => (),
Err(e) => println!("Failed to open program ({}): {}", &path, e),
};
}
#[tauri::command]
@@ -67,6 +73,8 @@ pub fn run_jar(path: String, execute_in: String, java_path: String) {
format!("\"{}\" -jar \"{}\"", java_path, path)
};
println!("Launching .jar with command: {}", &command);
// Open the program from the specified path.
match open::with(
format!("/k cd /D \"{}\" & {}", &execute_in, &command),
@@ -77,6 +85,22 @@ pub fn run_jar(path: String, execute_in: String, java_path: String) {
};
}
#[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),
};
}
#[tauri::command]
pub fn open_in_browser(url: String) {
// Open the URL in the default browser.
@@ -153,9 +177,7 @@ pub fn set_migoto_delay(migoto_path: String) -> bool {
};
// Set options
conf
.with_section(Some("Loader"))
.set("delay", "20");
conf.with_section(Some("Loader")).set("delay", "20");
// Write file
match conf.write_to_file(&migoto_pathbuf) {

View File

@@ -109,7 +109,10 @@ pub fn unzip(
if zipfile.contains("GIMI") {
window
.emit("migoto_extracted", destpath.to_string() + "3DMigoto Loader.exe")
.emit(
"migoto_extracted",
destpath.to_string() + "3DMigoto Loader.exe",
)
.unwrap();
}

View File

@@ -7,7 +7,7 @@
},
"package": {
"productName": "Cultivation",
"version": "1.0.25"
"version": "1.0.26"
},
"tauri": {
"allowlist": {

View File

@@ -11,9 +11,12 @@ 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 { 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'
@@ -24,7 +27,6 @@ import DownloadHandler from '../utils/download'
import cogBtn from '../resources/icons/cog.svg'
import downBtn from '../resources/icons/download.svg'
import wrenchBtn from '../resources/icons/wrench.svg'
import { ExtrasMenu } from './components/menu/ExtrasMenu'
interface IProps {
downloadHandler: DownloadHandler
@@ -39,6 +41,7 @@ interface IState {
extrasOpen: boolean
migotoSet: boolean
playGame: (exe?: string, proc_name?: string) => void
notification: React.ReactElement | null
}
export class Main extends React.Component<IProps, IState> {
@@ -55,6 +58,7 @@ export class Main extends React.Component<IProps, IState> {
playGame: () => {
alert('Error launching game')
},
notification: null,
}
listen('lang_error', (payload) => {
@@ -141,6 +145,35 @@ export class Main extends React.Component<IProps, IState> {
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({
@@ -192,6 +225,8 @@ export class Main extends React.Component<IProps, IState> {
</div> */}
</TopBar>
<Notification show={!!this.state.notification}>{this.state.notification}</Notification>
<RightBar />
<NewsSection />

View File

@@ -39,6 +39,8 @@ interface IState {
swag: boolean
akebiSet: boolean
migotoSet: boolean
unElevated: boolean
}
export default class ServerLaunchSection extends React.Component<IProps, IState> {
@@ -62,6 +64,7 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
swag: false,
akebiSet: false,
migotoSet: false,
unElevated: false,
}
this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
@@ -93,6 +96,7 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
swag: config.swag_mode || false,
akebiSet: config.akebi_path !== '',
migotoSet: config.migoto_path !== '',
unElevated: config.un_elevated || false,
})
}
@@ -188,7 +192,14 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
path: exe || config.game_install_path,
})
if (gameExists) await invoke('run_program_relative', { path: exe || config.game_install_path })
if (gameExists)
if (config.un_elevated) {
await invoke('run_un_elevated', {
path: config.game_install_path,
})
} else {
await invoke('run_program_relative', { path: exe || config.game_install_path })
}
else alert('Game not found! At: ' + (exe || config.game_install_path))
}

View File

@@ -0,0 +1,24 @@
.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

@@ -0,0 +1,22 @@
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

@@ -41,6 +41,7 @@ interface IState {
auto_mongodb: boolean
swag: boolean
platform: string
un_elevated: boolean
// Swag stuff
akebi_path: string
@@ -70,6 +71,7 @@ export default class Options extends React.Component<IProps, IState> {
swag: false,
auto_mongodb: false,
platform: '',
un_elevated: false,
// Swag stuff
akebi_path: '',
@@ -87,6 +89,7 @@ export default class Options extends React.Component<IProps, IState> {
this.toggleEncryption = this.toggleEncryption.bind(this)
this.removeRSA = this.removeRSA.bind(this)
this.addMigotoDelay = this.addMigotoDelay.bind(this)
this.toggleUnElevatedGame = this.toggleUnElevatedGame.bind(this)
}
async componentDidMount() {
@@ -119,6 +122,7 @@ export default class Options extends React.Component<IProps, IState> {
swag: config.swag_mode || false,
auto_mongodb: config.auto_mongodb || false,
platform,
un_elevated: config.un_elevated || false,
// Swag stuff
akebi_path: config.akebi_path || '',
@@ -274,6 +278,15 @@ export default class Options extends React.Component<IProps, IState> {
}
}
async toggleUnElevatedGame() {
const changedVal = !(await getConfigOption('un_elevated'))
setConfigOption('un_elevated', changedVal)
this.setState({
un_elevated: changedVal,
})
}
async removeRSA() {
await meta.unpatchGame()
}
@@ -455,6 +468,18 @@ export default class Options extends React.Component<IProps, IState> {
/>
</div>
</div>
<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">

View File

@@ -25,6 +25,7 @@ let defaultConfig: Configuration
wipe_login: false,
horny_mode: false,
auto_mongodb: false,
un_elevated: false,
}
})()
@@ -53,6 +54,7 @@ export interface Configuration {
horny_mode: boolean
swag_mode?: boolean
auto_mongodb: boolean
un_elevated: boolean
// Swag stuff
akebi_path?: string

View File

@@ -90,7 +90,7 @@ export default class DownloadHandler {
} = payload
// Find the download that is not extracting and set it's status as such
const index = this.downloads.findIndex((download) => download.path === obj.file || obj.new_folder)
const index = this.downloads.findIndex((download) => download.path === obj.file)
this.downloads[index].status = 'finished'
})
}
@@ -101,25 +101,25 @@ export default class DownloadHandler {
downloadingJar() {
// Kinda hacky but it works
return this.downloads.some((d) => d.path.includes('grasscutter.zip'))
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'))
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'))
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'))
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'))
return this.downloads.some((d) => d.path.includes('3dmigoto') && d.status != ('finished' || 'error'))
}
addDownload(url: string, path: string, onFinish?: () => void) {

View File

@@ -1,25 +1,12 @@
import { invoke } from '@tauri-apps/api'
import { dataDir } from '@tauri-apps/api/path'
import { getGameFolder } from './game'
// Patch file from: https://github.com/34736384/RSAPatch/
export async function patchGame() {
// Do we have a patch already?
const patchedExists = await invoke('dir_exists', {
path: (await getBackupRSAPath()) + '\\version.dll',
})
if (!patchedExists) {
// No patch found? Patching creates one
const patched = await downloadRSA()
if (!patched) {
return false
}
}
const patchPath = (await invoke('install_location')) + '\\patch\\version.dll'
// Are we already patched with mhypbase? If so, that's fine, just continue as normal
const gameIsPatched = await invoke('are_files_identical', {
path1: (await getBackupRSAPath()) + '\\version.dll',
path1: patchPath,
path2: (await getGameRSAPath()) + '\\mhypbase.dll',
})
@@ -31,7 +18,7 @@ export async function patchGame() {
// Copy the patch to game files
const replaced = await invoke('copy_file_with_new_name', {
path: (await getBackupRSAPath()) + '\\version.dll',
path: patchPath,
newPath: await getGameRSAPath(),
newName: 'version.dll',
})
@@ -61,25 +48,3 @@ export async function getGameRSAPath() {
return (gameData + '\\').replace(/\\/g, '/')
}
export async function getBackupRSAPath() {
return (await dataDir()) + 'cultivation\\rsa'
}
export async function downloadRSA() {
// Patch file from: https://github.com/34736384/RSAPatch/
// Should make sure rsa path exists
await invoke('dir_create', {
path: await getBackupRSAPath(),
})
// Copy patch from local for offline compatibility
await invoke('copy_file_with_new_name', {
path: (await invoke('install_location')) + '\\patch\\version.dll',
newPath: await getBackupRSAPath(),
newName: 'version.dll',
})
return true
}