Changes from 1.2.1 - 1.5.1

Contains slightly modified commits from between 1.2.1 and 1.5.1.
This commit is contained in:
Thoronium 2024-10-30 13:18:07 -06:00
parent 31c60755af
commit f6f5eae31c
46 changed files with 2619 additions and 1669 deletions

View File

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

3026
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "cultivation"
version = "1.2.0"
version = "1.5.1"
description = "A custom launcher for anime game."
authors = ["KingRainbow44", "SpikeHD"]
license = ""

View File

@ -35,11 +35,13 @@
"un_elevated": "非提升运行游戏(无管理员)",
"redirect_more": "还可以重定向其他MHY游戏",
"web_cache": "删除 webCaches 文件夹",
"launch_args": "Launch Args"
"launch_args": "启动参数",
"offline_mode": "离线模式",
"fix_res": "修复登录超时"
},
"downloads": {
"grasscutter_fullbuild": "下载 Grasscutter 一体化",
"grasscutter_fullquest": "下载 Quest 一体化",
"grasscutter_fullquest": "下载 5.0 一体化",
"grasscutter_stable_data": "下载 Grasscutter 稳定版数据",
"grasscutter_latest_data": "下载 Grasscutter 开发版数据",
"grasscutter_stable_data_update": "更新 Grasscutter 稳定版数据",
@ -67,7 +69,8 @@
"select_folder": "选择文件夹...",
"download": "下载",
"delete": "删除",
"install": "安装"
"install": "安装",
"fix": "Fix"
},
"news": {
"latest_commits": "最近提交",

View File

@ -35,11 +35,13 @@
"un_elevated": "在不升高的情况下运行游戏(没有管理员)。",
"redirect_more": "同時重定向其他 MHY 遊戲",
"web_cache": "刪除 webCaches 文件夾",
"launch_args": "Launch Args"
"launch_args": "啟動參數",
"offline_mode": "離線模式",
"fix_res": "修復登入逾時"
},
"downloads": {
"grasscutter_fullbuild": "下載Grasscutter多合一下載",
"grasscutter_fullquest": "下载 Quest 一体化",
"grasscutter_fullquest": "下载 5.0 一体化",
"grasscutter_stable_data": "下載Grasscutter穩定版數據Data",
"grasscutter_latest_data": "下載Grasscutter開發板數據Data",
"grasscutter_stable_data_update": "更新Grasscutter穩定版數據Data",
@ -67,7 +69,8 @@
"select_folder": "選擇資料夾...",
"download": "下載",
"delete": "刪除",
"install": "安裝"
"install": "安裝",
"fix": "Fix"
},
"news": {
"latest_commits": "最近的PR",

View File

@ -37,11 +37,13 @@
"check_aagl": "Für weitere Optionen, schaue weiter",
"grasscutter_elevation": "Methode zur Ausführung von GC auf eingeschränkten Ports",
"web_cache": "WebCaches-Ordner löschen",
"launch_args": "Launch Args"
"launch_args": "Start-Argumente",
"offline_mode": "Offline-Modus",
"fix_res": "Login-Zeitüberschreitung beheben"
},
"downloads": {
"grasscutter_fullbuild": "Grasscutter All-in-One herunterladen",
"grasscutter_fullquest": "Questing All-in-One herunterladen",
"grasscutter_fullquest": "5.0 All-in-One herunterladen",
"grasscutter_stable_data": "Stabile Grasscutter-Daten herunterladen",
"grasscutter_latest_data": "Neueste Grasscutter-Daten herunterladen",
"grasscutter_stable_data_update": "Stabile Grasscutter-Daten aktualisieren",
@ -69,7 +71,8 @@
"select_folder": "Ordner auswählen...",
"download": "Herunterladen",
"delete": "Löschen",
"install": "Installieren"
"install": "Installieren",
"fix": "Fix"
},
"news": {
"latest_commits": "Neueste Commits",

View File

@ -37,11 +37,13 @@
"check_aagl": "For more options, check the other launcher",
"grasscutter_elevation": "Method of running GC on restricted ports",
"web_cache": "Delete webCaches folder",
"launch_args": "Launch Args"
"launch_args": "Launch Args",
"offline_mode": "Offline Mode",
"fix_res": "Fix Login Timeout"
},
"downloads": {
"grasscutter_fullbuild": "Download Grasscutter All-in-One",
"grasscutter_fullquest": "Download Questing All-in-One",
"grasscutter_fullquest": "Download 5.0 All-in-One",
"grasscutter_stable_data": "Download Grasscutter Stable Data",
"grasscutter_latest_data": "Download Grasscutter Latest Data",
"grasscutter_stable_data_update": "Update Grasscutter Stable Data",
@ -69,7 +71,8 @@
"select_folder": "Select folder...",
"download": "Download",
"delete": "Delete",
"install": "Install"
"install": "Install",
"fix": "Fix"
},
"news": {
"latest_commits": "Recent Commits",
@ -96,7 +99,7 @@
"akebi_name": "Akebi",
"migoto_name": "Migoto",
"reshade_name": "Reshade",
"akebi": "Set Akebi/Acrepi Executable",
"akebi": "Set Akebi/Other Cheat Executable",
"migoto": "Set 3DMigoto Executable",
"reshade": "Set Reshade Injector"
}

View File

@ -35,11 +35,13 @@
"un_elevated": "Ejecutar el juego sin permisos de administrador",
"redirect_more": "También redirigir otros juegos MHY",
"web_cache": "Eliminar la carpeta webCaches",
"launch_args": "Launch Args"
"launch_args": "Args de lanzamiento",
"offline_mode": "Modo sin conexión",
"fix_res": "Reparar el tiempo de espera"
},
"downloads": {
"grasscutter_fullbuild": "Descargar datos todo en uno de Grasscutter",
"grasscutter_fullquest": "Descargar datos todo en uno de Questing",
"grasscutter_fullquest": "Descargar datos todo en uno de 5.0",
"grasscutter_stable_data": "Descargar datos Estables de Grasscutter",
"grasscutter_latest_data": "Descargar datos más Recientes de Grasscutter",
"grasscutter_stable_data_update": "Actualizar datos estables de Grasscutter",
@ -67,7 +69,8 @@
"select_folder": "Seleccionar la carpeta",
"download": "Descargar",
"delete": "Borrar",
"install": "Instalar"
"install": "Instalar",
"fix": "Fix"
},
"news": {
"latest_commits": "Commits recientes",

View File

@ -35,11 +35,13 @@
"un_elevated": "Exécuter le jeu sans élévation (pas d'administrateur)",
"redirect_more": "Réorienter également les autres jeux MHY",
"web_cache": "Supprimer le dossier webCaches",
"launch_args": "Launch Args"
"launch_args": "Arguments de lancement",
"offline_mode": "Mode hors ligne",
"fix_res": "Réparation du login"
},
"downloads": {
"grasscutter_fullbuild": "Telecharger Grasscutter tout-en-un",
"grasscutter_fullquest": "Télécharger les Quêtes tout-en-un",
"grasscutter_fullquest": "Télécharger les 5.0 tout-en-un",
"grasscutter_stable_data": "Télécharger les donnees de Grasscutter (version stable)",
"grasscutter_latest_data": "Télécharger les donnees de Grasscutter (derniere version)",
"grasscutter_stable_data_update": "Mettre à jour les données de Grasscutter (version stable)",
@ -66,7 +68,8 @@
"select_folder": "Choisir un dossier...",
"download": "Télécharger",
"delete": "Supprimer",
"install": "Installer"
"install": "Installer",
"fix": "Fix"
},
"news": {
"latest_commits": "Commits récents",

View File

@ -34,11 +34,13 @@
"un_elevated": "Jalankan game yang tidak ditinggikan (tanpa admin)",
"redirect_more": "Juga mengarahkan ulang game MHY lainnya",
"web_cache": "Hapus folder webCaches",
"launch_args": "Launch Args"
"launch_args": "Luncurkan Args",
"offline_mode": "Mode Offline",
"fix_res": "Perbaiki batas waktu login"
},
"downloads": {
"grasscutter_fullbuild": "Sedang Mendownload Grasscutter Semua Dalam Satu",
"grasscutter_fullquest": "Unduh pencarian semua dalam satu",
"grasscutter_fullquest": "Unduh 5.0 semua dalam satu",
"grasscutter_stable_data": "Sedang Mendownload Grasscutter Versi Stabil",
"grasscutter_latest_data": "Sedang Mendownload Grasscutter Data Terbaru",
"grasscutter_stable_data_update": "Memperbaharui Grasscutter Data Stabil",
@ -64,7 +66,8 @@
"select_file": "Pilih File Atau Folder...",
"select_folder": "Pilih Folder...",
"download": "download",
"delete": "Menghapus"
"delete": "Menghapus",
"fix": "Fix"
},
"news": {
"latest_commits": "Commit Terbaru",

View File

@ -35,11 +35,13 @@
"un_elevated": "Avvia il gioco non-elevato (non admin)",
"redirect_more": "Reindirizza anche altri giochi MHY",
"web_cache": "Elimina la cartella webCaches",
"launch_args": "Launch Args"
"launch_args": "Argomenti di lancio",
"offline_mode": "Modalità Offline",
"fix_res": "Correggere il timeout dell'accesso"
},
"downloads": {
"grasscutter_fullbuild": "Scarica Grasscutter Tutto-in-Uno",
"grasscutter_fullquest": "Scarica Questing Tutto-in-Uno",
"grasscutter_fullquest": "Scarica 5.0 Tutto-in-Uno",
"grasscutter_stable_data": "Scarica i dati di Grasscutter Stabili",
"grasscutter_latest_data": "Scarica i dati di Grasscutter Più Recenti",
"grasscutter_stable_data_update": "Aggiorna i dati di Grasscutter Stabili",
@ -67,7 +69,8 @@
"select_folder": "Seleziona cartella...",
"download": "Scarica",
"delete": "Cancella",
"install": "Installa"
"install": "Installa",
"fix": "Fix"
},
"news": {
"latest_commits": "Commit Recenti",

View File

@ -35,11 +35,13 @@
"un_elevated": "게임 비상승 실행(관리자 없음)",
"redirect_more": "다른 MHY 게임도 리디렉션",
"web_cache": "webCaches 폴더 삭제",
"launch_args": "Launch Args"
"launch_args": "실행 인수",
"offline_mode": "오프라인 모드",
"fix_res": "로그인 시간 초과 수정"
},
"downloads": {
"grasscutter_fullbuild": "올인원 Grasscutter 다운로드",
"grasscutter_fullquest": "퀘스트 올인원 다운로드",
"grasscutter_fullquest": "5.0 올인원 다운로드",
"grasscutter_stable_data": "안정적인 데이터 다운로드",
"grasscutter_latest_data": "최신 데이터 다운로드",
"grasscutter_stable_data_update": "안정적 데이터 업데이트",
@ -67,7 +69,8 @@
"select_folder": "폴더 선택...",
"download": "다운로드",
"delete": "삭제",
"install": "설치"
"install": "설치",
"fix": "Fix"
},
"news": {
"latest_commits": "공지 사항",

View File

@ -33,11 +33,13 @@
"un_elevated": "Palaist spēli bez paaugstinājuma (bez administratora)",
"redirect_more": "Arī novirzīt citas MHY spēles",
"web_cache": "Dzēsiet mapi WebCaches",
"launch_args": "Launch Args"
"launch_args": "Palaišanas args",
"offline_mode": "Bezsaistes režīms",
"fix_res": "Fiksēt pieteikšanās laika"
},
"downloads": {
"grasscutter_fullbuild": "Lejupielādējiet Grasscutter viss vienā",
"grasscutter_fullquest": "Lejupielādēt questing viss vienā",
"grasscutter_fullquest": "Lejupielādēt 5.0 viss vienā",
"grasscutter_stable_data": "Lejupielādējiet Grasscutter stabilos datus",
"grasscutter_latest_data": "Lejupielādējiet Grasscutter jaunākos datus",
"grasscutter_stable_data_update": "Atjauniniet Grasscutter stabilos datus",
@ -63,7 +65,8 @@
"select_file": "Izvēlēties failu vai mapu...",
"select_folder": "Izvēlēties mapu...",
"download": "Lejupielādēt",
"delete": "Dzēst"
"delete": "Dzēst",
"fix": "Fix"
},
"news": {
"latest_commits": "Nesen kommitus",

View File

@ -34,11 +34,13 @@
"un_elevated": "Voer het spel uit zonder hoogtevrees (geen admin)",
"redirect_more": "Richt ook andere MHY-spellen",
"web_cache": "Verwijder de webCaches-map",
"launch_args": "Launch Args"
"launch_args": "Args starten",
"offline_mode": "Offline Modus",
"fix_res": "Time-out inloggen verhelpen"
},
"downloads": {
"grasscutter_fullbuild": "Grasscutter Alles-in-één Downloaden",
"grasscutter_fullquest": "Alles-in-één zoeken downloaden",
"grasscutter_fullquest": "Alles-in-één 5.0 downloaden",
"grasscutter_stable_data": "Download Stabiele Gegevens Van Grasscutter",
"grasscutter_latest_data": "Download De Nieuwste Gegevens Van Grasscutter",
"grasscutter_stable_data_update": "Stabiele gegevens Van Grasscutter bijwerken",
@ -66,7 +68,8 @@
"select_folder": "Select folder...",
"download": "Download",
"delete": "Verwijder",
"install": "Install"
"install": "Install",
"fix": "Fix"
},
"news": {
"latest_commits": "Recente Opdrachten",

View File

@ -37,11 +37,13 @@
"check_aagl": "Więcej opcji znajdziesz w drugim launcherze",
"grasscutter_elevation": "Sposób uruchomienia GC na ograniczonym porcie",
"web_cache": "Usuń folder webCaches",
"launch_args": "Launch Args"
"launch_args": "Argumenty uruchamiania",
"offline_mode": "Tryb offline",
"fix_res": "Napraw limit czasu logowania"
},
"downloads": {
"grasscutter_fullbuild": "Pobierz Grasscutter (wszystko w jednym)",
"grasscutter_fullquest": "Pobierz Questing (wszystko w jednym)",
"grasscutter_fullquest": "Pobierz 5.0 (wszystko w jednym)",
"grasscutter_stable_data": "Pobierz stabilne dane Grasscuttera",
"grasscutter_latest_data": "Pobierz najnowsze dane Grasscuttera",
"grasscutter_stable_data_update": "Zaaktualizuj stabilne dane Grasscuttera",
@ -69,7 +71,8 @@
"select_folder": "Wybierz folder...",
"download": "Pobierz",
"delete": "Usuń",
"install": "Zainstaluj"
"install": "Zainstaluj",
"fix": "Fix"
},
"news": {
"latest_commits": "Ostatnie Commity",

View File

@ -35,11 +35,13 @@
"un_elevated": "Executar o jogo não-elevated (sem admin)",
"redirect_more": "Também redirecionar outros jogos MHY",
"web_cache": "Excluir pasta webCaches",
"launch_args": "Launch Args"
"launch_args": "Argumentos de lançamento",
"offline_mode": "Modo offline",
"fix_res": "Corrigir o tempo limite de login"
},
"downloads": {
"grasscutter_fullbuild": "Baixar o Grasscutter Tudo-em-Um",
"grasscutter_fullquest": "Baixar de missões em um só lugar",
"grasscutter_fullquest": "Baixar de 5.0 em um só lugar",
"grasscutter_stable_data": "Baixar os Dados do Grasscutter Estável",
"grasscutter_latest_data": "Baixar os Dados do Grasscutter Mais Recente",
"grasscutter_stable_data_update": "Atualizar os Dados do Grasscutter Estável",
@ -67,7 +69,8 @@
"select_folder": "Selecione a pasta...",
"download": "Baixar",
"delete": "Deletar",
"install": "Instalar"
"install": "Instalar",
"fix": "Fix"
},
"news": {
"latest_commits": "Commits Recentes",

View File

@ -34,11 +34,13 @@
"un_elevated": "Запустите игру в неэлегантном режиме (без администратора)",
"redirect_more": "Также перенаправьте другие игры MHY",
"web_cache": "Удалить папку webCaches",
"launch_args": "Launch Args"
"launch_args": "Параметры запуска",
"offline_mode": "Автономный режим",
"fix_res": "Исправить таймаут входа в систему"
},
"downloads": {
"grasscutter_fullbuild": "Скачать все в одном Grasscutter",
"grasscutter_fullquest": "Скачать квесты все в одном",
"grasscutter_fullquest": "Скачать 5.0 все в одном",
"grasscutter_stable_data": "Скачать стабильные данные Grasscutter",
"grasscutter_latest_data": "Скачать последние данные Grasscutter",
"grasscutter_stable_data_update": "Обновить стабильные данные Grasscutter",
@ -66,7 +68,8 @@
"select_folder": "Выберите папку...",
"download": "Скачать",
"delete": "Удалить",
"install": "Установить"
"install": "Установить",
"fix": "Fix"
},
"news": {
"latest_commits": "Последние коммиты",

View File

@ -35,11 +35,13 @@
"un_elevated": "Chạy trò chơi không nâng cao (không có quản trị viên)",
"redirect_more": "Đồng thời chuyển hướng các trò chơi MHY khác",
"web_cache": "Xóa thư mục webCaches",
"launch_args": "Launch Args"
"launch_args": "Khởi chạy đối số",
"offline_mode": "Chế độ ngoại tuyến",
"fix_res": "Sửa lỗi hết thời gian đăng nhập"
},
"downloads": {
"grasscutter_fullbuild": "Tải Grasscutter tất cả trong một",
"grasscutter_fullquest": "Tải xuống truy vấn tất cả trong một",
"grasscutter_fullquest": "Tải 5.0 tất cả trong một",
"grasscutter_stable_data": "Tải dữ liệu Grasscutter bản ổn định",
"grasscutter_latest_data": "Tải dữ liệu Grasscutter bản mới nhất",
"grasscutter_stable_data_update": "Cập nhật dữ liệu Grasscutter bản ổn định",
@ -67,7 +69,8 @@
"select_folder": "Chọn thư mục...",
"download": "Tải",
"delete": "Xóa bỏ",
"install": "Cài"
"install": "Cài",
"fix": "Fix"
},
"news": {
"latest_commits": "Thay Đổi Gần Đây",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,6 @@
use file_diff::diff;
use std::fs;
use std::io::{Read, Write};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
#[tauri::command]
@ -57,6 +57,11 @@ pub fn are_files_identical(path1: &str, path2: &str) -> bool {
diff(path1, path2)
}
#[tauri::command]
pub fn does_file_exist(path1: &str) -> bool {
fs::metadata(path1).is_ok()
}
#[tauri::command]
pub fn copy_file(path: String, new_path: String) -> bool {
let filename = &path.split('/').last().unwrap();
@ -127,21 +132,46 @@ pub fn delete_file(path: String) -> bool {
#[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
}
};
println!("Debug: Reading file of path {}", path.clone(),);
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
// Version data is 3 bytes long, 3 bytes in
let ext = path_buf.extension().unwrap();
if ext.eq("bytes") {
let offset_bytes = 3;
let num_bytes = 3;
let mut byte_file = match std::fs::File::open(path_buf) {
Ok(byte_file) => byte_file,
Err(e) => {
println!("{}", e);
return String::new();
}
};
byte_file
.seek(SeekFrom::Start(offset_bytes))
.unwrap_or_default();
let mut buf = vec![0; num_bytes];
byte_file.read_exact(&mut buf).unwrap_or_default();
contents = String::from_utf8_lossy(&buf).to_string();
} else {
let mut file = match fs::File::open(path_buf) {
Ok(file) => file,
Err(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.");
} else {
println!("Failed to open file: {}", e);
}
return String::new(); // Send back error for handling by the caller
}
};
file.read_to_string(&mut contents).unwrap();
}
contents
}

View File

@ -28,12 +28,7 @@ pub async fn get_languages() -> std::collections::HashMap<String, String> {
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 filename = path.file_name().unwrap().to_str().unwrap();
let content = match std::fs::read_to_string(&path) {
Ok(x) => x,

View File

@ -108,14 +108,14 @@ async fn parse_args(inp: &Vec<String>) -> Result<Args, ArgsError> {
std::process::exit(0);
}
// Patch if needed
if args.value_of("patch")? {
patch::patch_game(false, 0.to_string()).await;
}
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;
}
let game_args: String = args.value_of("game-args").unwrap_or(String::new());
if game_path.is_some() {
if args.value_of("non-elevated-game")? {
@ -154,8 +154,8 @@ async fn parse_args(inp: &Vec<String>) -> Result<Args, ArgsError> {
pathbuf.push("cultivation");
pathbuf.push("ca");
if args.value_of("other_redirects")? {
proxy::set_redirect_more();
if args.value_of("other-redirects")? {
// proxy::set_redirect_more(); // Unused
}
connect(8035, pathbuf.to_str().unwrap().to_string()).await;
@ -218,7 +218,6 @@ fn main() -> Result<(), ArgsError> {
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,
@ -229,7 +228,6 @@ fn main() -> Result<(), ArgsError> {
patch::unpatch_game,
proxy::set_proxy_addr,
proxy::generate_ca_files,
proxy::set_redirect_more,
release::get_latest_release,
unzip::unzip,
file_helpers::rename,
@ -243,6 +241,7 @@ fn main() -> Result<(), ArgsError> {
file_helpers::are_files_identical,
file_helpers::read_file,
file_helpers::write_file,
file_helpers::does_file_exist,
downloader::download_file,
downloader::stop_download,
lang::get_lang,
@ -297,7 +296,7 @@ fn enable_process_watcher(window: tauri::Window, process: String) {
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));
std::thread::sleep(std::time::Duration::from_secs(60));
let mut system = System::new_all();
@ -336,7 +335,7 @@ fn enable_process_watcher(window: tauri::Window, process: String) {
fn enable_process_watcher(window: tauri::Window, process: String) {
drop(process);
thread::spawn(move || {
let end_time = Instant::now() + Duration::from_secs(60);
let end_time = Instant::now() + Duration::from_secs(90);
let game_thread = loop {
let mut lock = AAGL_THREAD.lock().unwrap();
if lock.is_some() {

View File

@ -50,8 +50,90 @@ struct WhatToUnpach {
#[cfg(windows)]
#[tauri::command]
pub async fn patch_game() -> bool {
let patch_path = PathBuf::from(system_helpers::install_location()).join("patch/version.dll");
pub async fn patch_game(newer_game: bool, version: String) -> bool {
let mut patch_path;
// Altpatch first - Now using as hoyonet switch
if newer_game {
let alt_patch_path = PathBuf::from(system_helpers::install_location()).join("altpatch");
// Should handle overwriting backup with new version backup later
let backup_path = PathBuf::from(system_helpers::install_location())
.join("altpatch/original-mihoyonet.dll")
.to_str()
.unwrap()
.to_string();
let backup_exists = file_helpers::does_file_exist(&backup_path);
if !backup_exists {
let backup = file_helpers::copy_file_with_new_name(
get_game_rsa_path().await.unwrap()
+ &String::from("/GenshinImpact_Data/Plugins/mihoyonet.dll"),
alt_patch_path.clone().to_str().unwrap().to_string(),
String::from("original-mihoyonet.dll"),
);
if !backup {
println!("Unable to backup file!");
}
}
patch_path = PathBuf::from(system_helpers::install_location()).join("altpatch/mihoyonet.dll");
// Copy the other part of patch to game files
let alt_replaced = file_helpers::copy_file_with_new_name(
patch_path.clone().to_str().unwrap().to_string(),
get_game_rsa_path().await.unwrap() + &String::from("/GenshinImpact_Data/Plugins"),
String::from("mihoyonet.dll"),
);
if !alt_replaced {
return false;
}
/*** For replacing old backup file with new one, for example when version changes
* Currently replaces when it shouldn't. Will figure it out when it matters
* ***/
// else {
// // Check if game file matches backup
// let matching_alt_backup = file_helpers::are_files_identical(
// &backup_path.clone(),
// PathBuf::from(get_game_rsa_path().await.unwrap())
// .join("/GenshinImpact_Data/Plugins/mihoyonet.dll")
// .to_str()
// .unwrap(),
// );
// let is_alt_patched = file_helpers::are_files_identical(
// PathBuf::from(system_helpers::install_location()).join("altpatch/mihoyonet.dll").to_str().unwrap(),
// PathBuf::from(get_game_rsa_path().await.unwrap())
// .join("/GenshinImpact_Data/Plugins/mihoyonet.dll")
// .to_str()
// .unwrap(),
// );
// // Check if already alt patched
// if !matching_alt_backup {
// // Copy new backup if it is not patched
// if !is_alt_patched {
// file_helpers::copy_file_with_new_name(
// get_game_rsa_path().await.unwrap() + &String::from("/GenshinImpact_Data/Plugins/mihoyonet.dll"),
// alt_patch_path.clone().to_str().unwrap().to_string(),
// String::from("original-mihoyonet.dll"),
// );
// }
// }
// }
}
// Standard patch
patch_path = PathBuf::from(system_helpers::install_location()).join("patch/version.dll");
let i_ver = version.parse::<i32>().unwrap();
// For newer than 4.0, use specific patch files
if i_ver > 40 {
let patch_version = format!("patch/{version}version.dll");
patch_path = PathBuf::from(system_helpers::install_location()).join(patch_version);
}
// Are we already patched with mhypbase? If so, that's fine, just continue as normal
let game_is_patched = file_helpers::are_files_identical(
@ -70,6 +152,21 @@ pub async fn patch_game() -> bool {
return true;
}
// For 5.0 and up
if i_ver > 49 {
let replaced50 = file_helpers::copy_file_with_new_name(
patch_path.clone().to_str().unwrap().to_string(),
get_game_rsa_path().await.unwrap(),
String::from("Astrolabe.dll"),
);
if replaced50 {
return true;
} else {
return false;
}
}
// Copy the patch to game files
let replaced = file_helpers::copy_file_with_new_name(
patch_path.clone().to_str().unwrap().to_string(),
@ -86,7 +183,7 @@ pub async fn patch_game() -> bool {
#[cfg(target_os = "linux")]
#[tauri::command]
pub async fn patch_game() -> bool {
pub async fn patch_game(_newer_game: bool, _version: String) -> bool {
let mut patch_state_mutex = PATCH_STATE.lock().await;
if patch_state_mutex.is_some() {
println!("Game already patched!");
@ -170,6 +267,36 @@ pub async fn unpatch_game() -> bool {
.to_string(),
);
file_helpers::delete_file(
PathBuf::from(get_game_rsa_path().await.unwrap())
.join("Astrolabe.dll")
.to_str()
.unwrap()
.to_string(),
);
let core_patch_path = PathBuf::from(system_helpers::install_location());
let patch_path = core_patch_path.clone().join("altpatch/mihoyonet.dll");
let backup_path = core_patch_path
.clone()
.join("altpatch/original-mihoyonet.dll");
let is_alt_patched = file_helpers::are_files_identical(
patch_path.clone().to_str().unwrap(),
PathBuf::from(get_game_rsa_path().await.unwrap())
.join("GenshinImpact_Data/Plugins/mihoyonet.dll")
.to_str()
.unwrap(),
);
if is_alt_patched {
file_helpers::copy_file_with_new_name(
backup_path.clone().to_str().unwrap().to_string(),
get_game_rsa_path().await.unwrap() + &String::from("/GenshinImpact_Data/Plugins"),
String::from("mihoyonet.dll"),
);
}
deleted
}

View File

@ -3,8 +3,6 @@
* 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};
@ -41,8 +39,6 @@ async fn shutdown_signal() {
// 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;
@ -58,11 +54,6 @@ pub fn set_proxy_addr(addr: String) {
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(
@ -72,93 +63,33 @@ impl HttpHandler for ProxyHandler {
) -> RequestOrResponse {
let uri = req.uri().to_string();
let mut more = get_config().redirect_more;
if uri.contains("hoyoverse.com")
|| uri.contains("mihoyo.com")
|| uri.contains("yuanshen.com")
|| uri.ends_with(".yuanshen.com:12401")
|| 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();
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;
}
}
// 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;
}
}
@ -176,26 +107,14 @@ impl HttpHandler for ProxyHandler {
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")
}
}
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")
}
}
@ -307,7 +226,7 @@ pub fn connect_to_proxy(proxy_port: u16) {
Config::update(config);
}
#[cfg(target_od = "macos")]
#[cfg(target_os = "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)")
}

View File

@ -16,7 +16,6 @@ pub async fn get_latest_release() -> Release {
.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

View File

@ -390,44 +390,6 @@ pub fn install_location() -> 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);
@ -448,9 +410,18 @@ pub fn set_migoto_delay(migoto_path: String) -> bool {
// Set options
conf.with_section(Some("Loader")).set("delay", "20");
conf
.with_section(Some("Include"))
.set("include", "ShaderFixes\\help.ini");
// Write file
match conf.write_to_file(&migoto_pathbuf) {
match conf.write_to_file_opt(
&migoto_pathbuf,
ini::WriteOption {
escape_policy: (ini::EscapePolicy::Nothing),
line_separator: (ini::LineSeparator::SystemDefault),
},
) {
Ok(_) => {
println!("Wrote delay!");
true
@ -483,6 +454,31 @@ pub fn wipe_registry(exec_name: String) {
Ok(_) => (),
Err(e) => println!("Error wiping registry: {}", e),
}
match settings.set_value(
"MIHOYOSDK_ADL_PROD_CN_h3123967166",
&Data::String("".parse().unwrap()),
) {
Ok(_) => (),
Err(e) => println!("Error wiping registry: {}", e),
}
let hsr_settings =
match Hive::CurrentUser.open(format!("Software\\Cognosphere\\Star Rail"), Security::Write) {
Ok(s) => s,
Err(e) => {
println!("Error getting registry setting: {}", e);
return;
}
};
match hsr_settings.set_value(
"MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810",
&Data::String("".parse().unwrap()),
) {
Ok(_) => (),
Err(e) => println!("Error wiping registry: {}", e),
}
}
#[cfg(windows)]

View File

@ -107,7 +107,8 @@ pub fn unzip(
.unwrap();
}
if zipfile.contains("GrasscutterQuests") {
// Alternate builds
if zipfile.contains("GrasscutterQuests") || zipfile.contains("Grasscutter50") {
window
.emit("jar_extracted", destpath.to_string() + "grasscutter.jar")
.unwrap();

View File

@ -1,5 +1,5 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"$schema": "..\\node_modules/@tauri-apps/cli\\schema.json",
"build": {
"beforeDevCommand": "yarn start",
"devPath": "http://localhost:3000",
@ -7,7 +7,7 @@
},
"package": {
"productName": "Cultivation",
"version": "1.2.0"
"version": "1.5.1"
},
"tauri": {
"allowlist": {

View File

@ -17,7 +17,7 @@ interface IState {
}
const downloadHandler = new DownloadHandler()
const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile'
const WEB_BG = 'https://api.grasscutter.io/cultivation/bgfile'
class App extends React.Component<Readonly<unknown>, IState> {
constructor(props: Readonly<unknown>) {
@ -25,7 +25,7 @@ class App extends React.Component<Readonly<unknown>, IState> {
this.state = {
page: 'main',
bgFile: DEFAULT_BG,
bgFile: FALLBACK_BG,
}
}
@ -39,8 +39,9 @@ class App extends React.Component<Readonly<unknown>, IState> {
// Get custom bg AFTER theme is loaded !! important !!
const custom_bg = await getConfigOption('custom_background')
const offline_mode = await getConfigOption('offline_mode')
if (custom_bg) {
if (custom_bg || offline_mode) {
const isUrl = /^http(s)?:\/\//gm.test(custom_bg)
if (!isUrl) {
@ -50,7 +51,7 @@ class App extends React.Component<Readonly<unknown>, IState> {
this.setState(
{
bgFile: isValid ? convertFileSrc(custom_bg) : DEFAULT_BG,
bgFile: isValid ? convertFileSrc(custom_bg) : FALLBACK_BG,
},
this.forceUpdate
)
@ -62,7 +63,7 @@ class App extends React.Component<Readonly<unknown>, IState> {
this.setState(
{
bgFile: isValid ? custom_bg : DEFAULT_BG,
bgFile: isValid ? custom_bg : FALLBACK_BG,
},
this.forceUpdate
)
@ -70,12 +71,12 @@ class App extends React.Component<Readonly<unknown>, IState> {
} else {
// Check if api bg is accessible
const isDefaultValid = await invoke('valid_url', {
url: DEFAULT_BG,
url: WEB_BG,
})
this.setState(
{
bgFile: isDefaultValid ? DEFAULT_BG : FALLBACK_BG,
bgFile: isDefaultValid ? WEB_BG : FALLBACK_BG,
},
this.forceUpdate
)
@ -86,6 +87,7 @@ class App extends React.Component<Readonly<unknown>, IState> {
// @ts-expect-error - TS doesn't like our custom event
page: e.detail,
})
this.forceUpdate
})
}

View File

@ -15,7 +15,7 @@ async function setProxyAddress(address: string) {
}
async function startProxy() {
await invoke('connect', { port: 2222, certificatePath: (await dataDir()) + 'cultivation/ca' })
await invoke('connect', { port: 2222, certificatePath: (await dataDir()) + '\\cultivation\\ca' })
await invoke('open_in_browser', { url: 'https://hoyoverse.com' })
}
@ -24,12 +24,12 @@ async function stopProxy() {
}
async function generateCertificates() {
await invoke('generate_ca_files', { path: (await dataDir()) + 'cultivation' })
await invoke('generate_ca_files', { path: (await dataDir()) + '\\cultivation' })
}
async function generateInfo() {
console.log({
certificatePath: (await dataDir()) + 'cultivation/ca',
certificatePath: (await dataDir()) + '\\cultivation\\ca',
isAdmin: await invoke('is_elevated'),
connectingTo: proxyAddress,
})

View File

@ -76,8 +76,10 @@ export class Main extends React.Component<IProps, IState> {
setConfigOption('grasscutter_path', payload)
})
listen('migoto_extracted', ({ payload }: { payload: string }) => {
setConfigOption('migoto_path', payload)
listen('migoto_extracted', async ({ payload }: { payload: string }) => {
await setConfigOption('migoto_path', payload)
this.setState({ migotoSet: true })
window.location.reload()
})
// Emitted for rsa replacing-purposes
@ -93,14 +95,6 @@ export class Main extends React.Component<IProps, IState> {
}
})
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')
@ -182,33 +176,35 @@ export class Main extends React.Component<IProps, IState> {
// Update launch args to allow launching when updating from old versions
await setConfigOption('launch_args', await getConfigOption('launch_args'))
// 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, '')
if (!(await getConfigOption('offline_mode'))) {
// 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(() => {
// Check if tagName is different than current version
if (tagName && tagName !== (await getVersion())) {
// Display notification of new release
this.setState({
notification: null,
notification: (
<>
Cultivation{' '}
<a href="#" onClick={() => invoke('open_in_browser', { url: latestVersion.link })}>
{latestVersion?.tag_name}
</a>{' '}
is now available!
</>
),
})
}, 6000)
setTimeout(() => {
this.setState({
notification: null,
})
}, 6000)
}
}
// Period check to only show progress bar when downloading files
@ -355,7 +351,7 @@ export class Main extends React.Component<IProps, IState> {
}
<div className="BottomSection" id="bottomSectionContainer">
<ServerLaunchSection openExtras={this.openExtrasMenu} />
<ServerLaunchSection openExtras={this.openExtrasMenu} downloadHandler={this.props.downloadHandler} />
<div
id="DownloadProgress"

View File

@ -151,7 +151,7 @@ export class Mods extends React.Component<IProps, IState> {
},
this.forceUpdate
)
}, 500)
}, 300)
}
render() {
@ -211,7 +211,7 @@ export class Mods extends React.Component<IProps, IState> {
<TextInput
id="search"
key="search"
placeholder={this.state.page.toString()}
placeholder={'Search Mods - Page ' + this.state.page.toString()}
onChange={(text: string) => {
this.setSearch(text)
}}

View File

@ -17,9 +17,11 @@ import { getGameExecutable, getGameVersion, getGrasscutterJar } from '../../util
import { patchGame, unpatchGame } from '../../utils/rsa'
import { listen } from '@tauri-apps/api/event'
import { confirm } from '@tauri-apps/api/dialog'
import DownloadHandler from '../../utils/download'
interface IProps {
openExtras: (playGame: () => void) => void
downloadHandler: DownloadHandler
}
interface IState {
@ -72,10 +74,15 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
this.setPort = this.setPort.bind(this)
this.toggleHttps = this.toggleHttps.bind(this)
this.launchServer = this.launchServer.bind(this)
this.setButtonLabel = this.setButtonLabel.bind(this)
listen('start_grasscutter', async () => {
this.launchServer()
})
listen('set_game', async () => {
this.setButtonLabel()
})
}
async componentDidMount() {
@ -96,6 +103,8 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
migotoSet: config.migoto_path !== '',
unElevated: config.un_elevated || false,
})
this.setButtonLabel()
}
async toggleGrasscutter() {
@ -140,8 +149,9 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
// Connect to proxy
if (config.toggle_grasscutter) {
const game_exe = await getGameExecutable()
let newerGame = false
const patchable = game_exe?.toLowerCase().includes('genshin' || 'yuanshen')
const patchable = game_exe?.toLowerCase().includes('yuanshen') || game_exe?.toLowerCase().includes('genshin')
if (config.patch_rsa && patchable) {
const gameVersion = await getGameVersion()
@ -164,10 +174,58 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
return
}
const patched = await patchGame()
if (gameVersion?.major == 4 && gameVersion?.minor == 5) {
await confirm(
'Please use Cultivation version 1.4.0 for game version 4.5. You can find that here: https://github.com/NotThorny/Cultivation/releases/tag/1.4.0'
)
return
}
const versionString = gameVersion?.major.toString() + gameVersion?.minor.toString()
if ((gameVersion?.major == 4 && gameVersion?.minor > 5) || config.newer_game) {
newerGame = true
const path = (await invoke('install_location')) as string
const patchstring = '\\altpatch\\'
const altPatch = path + patchstring
const ALT_PATCH =
'https://autopatchhk.yuanshen.com/client_app/download/pc_zip/20231030132335_iOEfPMcbrXpiA8Ca/ScatteredFiles/GenshinImpact_Data/Plugins/mihoyonet.dll'
const pExists = (await invoke('dir_exists', {
path: altPatch,
})) as boolean
if (!pExists) {
await invoke('dir_create', {
path: altPatch,
})
this.props.downloadHandler.addDownload(ALT_PATCH, path + '/altpatch/mihoyonet.dll')
await confirm('Please wait for the download in the bottom left to disappear, then click yes')
}
/* For custom address patch only, used in 4.5 */
// let httpString = 'http://'
// if (this.state.httpsEnabled) {
// httpString = 'https://'
// }
// config.launch_args = '-server=' + httpString + this.state.ip + ':' + this.state.port
}
const patched = await patchGame(newerGame, versionString)
if (!patched) {
alert('Could not patch! Try launching again, or patching manually.')
alert(
"Could not patch! You're trying to launch a version that you don't have a patch for!" +
"\nEnsure you're using a valid game version, and have the patch for this version in your Cultivation install folder." +
'\n\nIf this means nothing to you, YOU HAVE THE WRONG GAME VERSION.' +
// Add game version due to overwhelming number of people saying they are using a version they are not using
'\n\nYOUR GAME VERSION: ' +
gameVersion?.major +
'.' +
gameVersion?.minor
)
return
}
}
@ -186,7 +244,7 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
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' })
await invoke('connect', { port: 8365, certificatePath: (await dataDir()) + '\\cultivation\\ca' })
}
// Open server as well if the options are set
@ -314,6 +372,19 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
await saveConfig(config)
}
async setButtonLabel() {
const ver = await getGameVersion()
if (ver != null) {
this.setState({
buttonLabel: (await translate('main.launch_button')) + ' ' + ver?.major + '.' + ver?.minor,
})
} else {
this.setState({
buttonLabel: await translate('main.launch_button'),
})
}
}
render() {
return (
<div id="playButton">

View File

@ -8,19 +8,23 @@ import { dataDir } from '@tauri-apps/api/path'
import './Downloads.css'
import Divider from './Divider'
import { getConfigOption } from '../../../utils/configuration'
import { getConfigOption, setConfigOption } from '../../../utils/configuration'
import { invoke } from '@tauri-apps/api'
import { listen } from '@tauri-apps/api/event'
import HelpButton from '../common/HelpButton'
import { ask } from '@tauri-apps/api/dialog'
const FULL_BUILD_DOWNLOAD = 'https://github.com/NotThorny/Grasscutter/releases/download/culti-aio/GrasscutterCulti.zip'
const FULL_BUILD_DOWNLOAD = 'https://github.com/NotThorny/Grasscutter/releases/download/culti-aio/GrasscutterCulti.zip' // Change to link that can be updated without modifying here
const FULL_QUEST_DOWNLOAD = 'https://github.com/NotThorny/Grasscutter/releases/download/culti-aio/GrasscutterQuests.zip'
const FULL_50_DOWNLOAD = 'https://github.com/NotThorny/Grasscutter/releases/download/culti-aio/GrasscutterLunaGC50.zip' // https://github.com/Kei-Luna/LunaGC_5.0.0
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 UNSTABLE_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/unstable/Grasscutter.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'
'https://github.com/SilentNightSound/GI-Model-Importer/releases/download/v7.0/3dmigoto-GIMI-for-playing-mods.zip'
const MIGOTO_FALLBACK = 'https://cdn.discordapp.com/attachments/615655311960965130/1177724469847003268/GIMI7.zip' // Since main dl fails for a few too many users
interface IProps {
closeFn: () => void
@ -56,8 +60,10 @@ export default class Downloads extends React.Component<IProps, IState> {
this.getGrasscutterFolder = this.getGrasscutterFolder.bind(this)
this.downloadGrasscutterFullBuild = this.downloadGrasscutterFullBuild.bind(this)
this.downloadGrasscutterFullQuest = this.downloadGrasscutterFullQuest.bind(this)
this.downloadGrasscutterFull50 = this.downloadGrasscutterFull50.bind(this)
this.downloadGrasscutterStableRepo = this.downloadGrasscutterStableRepo.bind(this)
this.downloadGrasscutterDevRepo = this.downloadGrasscutterDevRepo.bind(this)
this.downloadGrasscutterUnstable = this.downloadGrasscutterUnstable.bind(this)
this.downloadGrasscutterLatest = this.downloadGrasscutterLatest.bind(this)
this.downloadResources = this.downloadResources.bind(this)
this.downloadMigoto = this.downloadMigoto.bind(this)
@ -76,6 +82,19 @@ export default class Downloads extends React.Component<IProps, IState> {
this.setState({ grasscutter_set: true }, this.forceUpdate)
})
// Listen for GIMI failure to initiate fallback
listen('download_error', ({ payload }) => {
// @ts-expect-error shut up typescript
const errorData: {
path: string
error: string
} = payload
if (errorData.path.includes('GIMI.zip')) {
this.downloadMigotoFallback()
}
})
if (!gc_path || gc_path === '') {
this.setState({
grasscutter_set: false,
@ -85,15 +104,15 @@ export default class Downloads extends React.Component<IProps, IState> {
return
}
const path = gc_path.substring(0, gc_path.lastIndexOf('/'))
const path = gc_path.substring(0, gc_path.lastIndexOf('\\'))
if (gc_path) {
const resources_exist: boolean =
((await invoke('dir_exists', {
path: path + '/resources',
path: path + '\\resources',
})) as boolean) &&
(!(await invoke('dir_is_empty', {
path: path + '/resources',
path: path + '\\resources',
})) as boolean)
this.setState({
@ -110,7 +129,7 @@ export default class Downloads extends React.Component<IProps, IState> {
// Set to default if not set
if (!path || path === '') {
const appdata = await dataDir()
folderPath = appdata + 'cultivation/grasscutter'
folderPath = appdata + 'cultivation\\grasscutter'
// Early return since its formatted properly
return folderPath
@ -133,8 +152,8 @@ export default class Downloads extends React.Component<IProps, IState> {
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.props.downloadManager.addDownload(FULL_BUILD_DOWNLOAD, folder + '\\GrasscutterCulti.zip', async () => {
await unzip(folder + '\\GrasscutterCulti.zip', folder + '\\', true)
this.toggleButtons()
})
@ -143,8 +162,18 @@ export default class Downloads extends React.Component<IProps, IState> {
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.props.downloadManager.addDownload(FULL_QUEST_DOWNLOAD, folder + '\\GrasscutterQuests.zip', async () => {
await unzip(folder + '\\GrasscutterQuests.zip', folder + '\\', true)
this.toggleButtons()
})
this.toggleButtons()
}
async downloadGrasscutterFull50() {
const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(FULL_50_DOWNLOAD, folder + '\\Grasscutter50.zip', async () => {
await unzip(folder + '\\Grasscutter50.zip', folder + '\\', true)
this.toggleButtons()
})
@ -153,8 +182,8 @@ export default class Downloads extends React.Component<IProps, IState> {
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.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
this.toggleButtons()
})
@ -163,18 +192,28 @@ export default class Downloads extends React.Component<IProps, IState> {
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.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
this.toggleButtons()
})
this.toggleButtons()
}
async downloadGrasscutterUnstable() {
const folder = await this.getGrasscutterFolder()
this.props.downloadManager.addDownload(UNSTABLE_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
await unzip(folder + '\\grasscutter.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.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
await unzip(folder + '\\grasscutter.zip', folder + '\\', true)
this.toggleButtons()
})
@ -185,27 +224,38 @@ export default class Downloads extends React.Component<IProps, IState> {
}
async downloadResources() {
// Tell the user this is not needed in most cases
if (
!(await ask(
'These are not needed if you have already downloaded the All-in-One!! \nAre you sure you want to continue this download?'
))
) {
// If refusing confirmation
return
}
// 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 () => {
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',
path: folder + '\\resources',
})
) {
await invoke('dir_delete', {
path: folder + '/resources',
path: folder + '\\resources',
})
}
await unzip(folder + '/resources.zip', folder + '/', true)
await unzip(folder + '\\resources.zip', folder + '\\', true)
// Rename folder to resources
invoke('rename', {
path: folder + '/Resources',
path: folder + '\\Resources',
newName: 'resources',
})
@ -216,13 +266,27 @@ export default class Downloads extends React.Component<IProps, IState> {
}
async downloadMigoto() {
const folder = (await this.getCultivationFolder()) + '/3dmigoto'
await invoke('dir_create', {
path: folder,
if (!this.state.swag) {
await setConfigOption('swag_mode', true)
this.setState({ swag: true })
await setConfigOption('last_extras', { migoto: true, akebi: false, reshade: false })
}
const folder = await this.getCultivationFolder()
this.props.downloadManager.addDownload(MIGOTO_DOWNLOAD, folder + '\\GIMI.zip', async () => {
await unzip(folder + '\\GIMI.zip', folder + '\\', true, true)
this.toggleButtons()
})
this.props.downloadManager.addDownload(MIGOTO_DOWNLOAD, folder + '/GIMI-3dmigoto.zip', async () => {
await unzip(folder + '/GIMI-3dmigoto.zip', folder + '/', true)
this.toggleButtons()
}
async downloadMigotoFallback() {
const folder = await this.getCultivationFolder()
this.props.downloadManager.addDownload(MIGOTO_FALLBACK, folder + '\\GIMI7.zip', async () => {
await unzip(folder + '\\GIMI7.zip', folder + '\\', true, true)
this.toggleButtons()
})
@ -256,7 +320,6 @@ export default class Downloads extends React.Component<IProps, IState> {
<Tr text={'downloads.grasscutter_fullbuild'} />
<HelpButton contents="help.gc_fullbuild" />
</div>
<div className="DownloadValue" id="downloadMenuButtonGCFullBuild">
<BigButton
disabled={this.state.grasscutter_downloading}
@ -267,7 +330,6 @@ export default class Downloads extends React.Component<IProps, IState> {
</BigButton>
</div>
</div>
<div className="DownloadMenuSection" id="downloadMenuContainerGCFullQuest">
<div className="DownloadLabel" id="downloadMenuLabelGCFullQuest">
<Tr text={'downloads.grasscutter_fullquest'} />
@ -276,18 +338,38 @@ export default class Downloads extends React.Component<IProps, IState> {
<div className="DownloadValue" id="downloadMenuButtonGCFullQuest">
<BigButton
disabled={this.state.grasscutter_downloading}
onClick={this.downloadGrasscutterFullQuest}
onClick={this.downloadGrasscutterFull50}
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="downloadMenuContainerGCUnstable">
<div className="DownloadLabel" id="downloadMenuLabelGCUnstable">
<Tr
text={
this.state.grasscutter_set ? 'downloads.grasscutter_unstable' : 'downloads.grasscutter_unstable_update'
}
/>
<HelpButton contents="help.gc_unstable_jar" />
</div>
<div className="DownloadValue" id="downloadMenuButtonGCUnstable">
<BigButton
disabled={this.state.grasscutter_downloading}
onClick={this.downloadGrasscutterUnstable}
id="grasscutterUnstableBtn"
>
<Tr text="components.download" />
</BigButton>
</div>
</div> */}
<div className="DownloadMenuSection" id="downloadMenuContainerGCDev">
<div className="DownloadLabel" id="downloadMenuLabelGCDev">
<Tr
@ -306,7 +388,28 @@ export default class Downloads extends React.Component<IProps, IState> {
</div>
</div>
<div className="DownloadMenuSection" id="downloadMenuContainerGCDevData">
{/* <div className="DownloadMenuSection" id="downloadMenuContainerGCStableData">
<div className="DownloadLabel" id="downloadMenuLabelGCStableData">
<Tr
text={
this.state.grasscutter_set
? 'downloads.grasscutter_stable_data'
: 'downloads.grasscutter_stable_data_update'
}
/>
<HelpButton contents="help.gc_stable_data" />
</div>
<div className="DownloadValue" id="downloadMenuButtonGCStableData">
<BigButton
disabled={this.state.repo_downloading}
onClick={this.downloadGrasscutterStableRepo}
id="grasscutterStableRepo"
>
<Tr text="components.download" />
</BigButton>
</div>
</div> */}
{/* <div className="DownloadMenuSection" id="downloadMenuContainerGCDevData">
<div className="DownloadLabel" id="downloadMenuLabelGCDevData">
<Tr
text={
@ -326,7 +429,7 @@ export default class Downloads extends React.Component<IProps, IState> {
<Tr text="components.download" />
</BigButton>
</div>
</div>
</div> */}
<div className="DownloadMenuSection" id="downloadMenuContainerResources">
<div className="DownloadLabel" id="downloadMenuLabelResources">
@ -344,25 +447,23 @@ export default class Downloads extends React.Component<IProps, IState> {
</div>
</div>
{this.state.swag && (
<>
<Divider />
<div className="HeaderText" id="downloadMenuModsHeader">
<Tr text="downloads.mods_header" />
<>
<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="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 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

@ -45,8 +45,8 @@ export default class Downloads extends React.Component<IProps, IState> {
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.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', async () => {
await unzip(folder + '\\game.zip', folder + '\\', true)
this.setState({
gameDownloading: false,
})

View File

@ -16,8 +16,11 @@ 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 { ask, confirm } from '@tauri-apps/api/dialog'
import TextInput from '../common/TextInput'
import { unzip } from '../../../utils/zipUtils'
import { getGameExecutable } from '../../../utils/game'
import { emit } from '@tauri-apps/api/event'
export enum GrasscutterElevation {
None = 'None',
@ -52,6 +55,8 @@ interface IState {
un_elevated: boolean
redirect_more: boolean
launch_args: string
offline_mode: boolean
newer_game: boolean
// Linux stuff
grasscutter_elevation: string
@ -88,6 +93,8 @@ export default class Options extends React.Component<IProps, IState> {
un_elevated: false,
redirect_more: false,
launch_args: '',
offline_mode: false,
newer_game: false,
// Linux stuff
grasscutter_elevation: GrasscutterElevation.None,
@ -118,13 +125,10 @@ export default class Options extends React.Component<IProps, IState> {
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')
}
// Remove jar from path
const path = config.grasscutter_path.replace(/\\/g, '/')
const folderPath = path.substring(0, path.lastIndexOf('/'))
const encEnabled = await server.encryptionEnabled(folderPath + '/config.json')
console.log(platform)
@ -150,6 +154,8 @@ export default class Options extends React.Component<IProps, IState> {
un_elevated: config.un_elevated || false,
redirect_more: config.redirect_more || false,
launch_args: config.launch_args,
offline_mode: config.offline_mode || false,
newer_game: config.newer_game || false,
// Linux stuff
grasscutter_elevation: config.grasscutter_elevation || GrasscutterElevation.None,
@ -163,22 +169,28 @@ export default class Options extends React.Component<IProps, IState> {
this.forceUpdate()
}
setGameExecutable(value: string) {
setConfigOption('game_install_path', value)
async setGameExecutable(value: string) {
await setConfigOption('game_install_path', value)
// I hope this stops people setting launcher.exe because oml it's annoying
if (value.endsWith('launcher.exe')) {
if (value.endsWith('launcher.exe') || value.endsWith('.lnk')) {
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 (value.endsWith('.lnk')) {
alert(
'You have set your game executable to a shortcut. You should not do this. Your patching will not work, and your proxy may shut off unexpectedly.'
)
} else {
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 (!value.toLowerCase().includes('genshin') || !value.toLowerCase().includes('yuanshen')) {
if (!this.state.redirect_more) {
this.toggleOption('redirect_more')
}
@ -187,26 +199,24 @@ export default class Options extends React.Component<IProps, IState> {
this.setState({
game_install_path: value,
})
emit('set_game', { game_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({
grasscutter_path: value,
encryption: encEnabled,
})
window.location.reload()
// Encryption refuses to re-render w/o reload unless updated twice
this.forceUpdateEncyption()
}
setJavaPath(value: string) {
@ -267,7 +277,7 @@ export default class Options extends React.Component<IProps, IState> {
if (!isUrl) {
const filename = value.replace(/\\/g, '/').split('/').pop()
const localBgPath = (await dataDir()).replace(/\\/g, '/')
const localBgPath = ((await dataDir()) as string).replace(/\\/g, '/')
await setConfigOption('custom_background', `${localBgPath}/cultivation/bg/${filename}`)
@ -284,6 +294,16 @@ export default class Options extends React.Component<IProps, IState> {
}
}
async forceUpdateEncyption() {
const config = await getConfig()
const path = config.grasscutter_path.replace(/\\/g, '/')
const folderPath = path.substring(0, path.lastIndexOf('/'))
this.setState({
encryption: await server.encryptionEnabled(folderPath + '/config.json'),
})
}
async toggleEncryption() {
const config = await getConfig()
@ -343,6 +363,15 @@ export default class Options extends React.Component<IProps, IState> {
}
async addMigotoDelay() {
if (
!(await ask(
'Set delay for 3dmigoto loader? This is specifically made for GIMI v6 and earlier. Using it on latest GIMI or SRMI will cause issues!!! \n\nWould you like to continue?',
{ title: 'GIMI Delay', type: 'warning' }
))
) {
return
}
invoke('set_migoto_delay', {
migotoPath: this.state.migoto_path,
})
@ -355,6 +384,15 @@ export default class Options extends React.Component<IProps, IState> {
}
async deleteWebCache() {
if (await ask('Would you like to clear login cache? Yes to clear login cache. No to clear web cache.')) {
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'),
})
alert('Cleared login cache!')
return
}
alert('Cultivation may freeze for a moment while this occurs!')
// Get webCaches folder path
@ -364,8 +402,70 @@ export default class Options extends React.Component<IProps, IState> {
const path2 = pathArr.join('/') + '/Yuanshen_Data/webCaches'
// Delete the folder
await invoke('dir_delete', { path: path })
await invoke('dir_delete', { path: path2 })
if (await invoke('dir_exists', { path: path })) {
await invoke('dir_delete', { path: path })
}
if (await invoke('dir_exists', { path: path2 })) {
await invoke('dir_delete', { path: path2 })
}
}
async fixRes() {
const config = await getConfig()
const path = config.grasscutter_path.replace(/\\/g, '/')
let folderPath = path.substring(0, path.lastIndexOf('/'))
// Set to default if not set
if (!path || path === '') {
const appdata = await dataDir()
folderPath = appdata + 'cultivation\\grasscutter'
}
if (path.includes('/')) {
folderPath = path.substring(0, path.lastIndexOf('/'))
} else {
folderPath = path.substring(0, path.lastIndexOf('\\'))
}
// Check if Grasscutter path exists
if (folderPath.length < 1) {
alert('Grasscutter not installed or not set! This option can only work when it is installed.')
return
}
// Check if resources zip exists
if (
!(await invoke('dir_exists', {
path: folderPath + '\\GC-Resources-4.0.zip',
}))
) {
alert('Resources are already unzipped or do not exist! Ensure your resources zip is named "GC-Resources-4.0.zip"')
return
}
alert(
'This may fix white screen issues on login! Please be patient while extraction occurs, it may take some time (5-10 minutes). \n\n !! You will be alerted when it is done !!'
)
// Unzip resources
await unzip(folderPath + '\\GC-Resources-4.0.zip', folderPath + '\\', true)
// Rename folder to resources
invoke('rename', {
path: folderPath + '\\Resources',
newName: 'resources',
})
// Update config.json to read from folder
await server.changeResourcePath(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 for changes to apply!')
await invoke('restart_grasscutter')
}
alert('Resource fixing finished! Please launch the server again and try playing.')
}
async toggleOption(opt: keyof Configuration) {
@ -542,7 +642,7 @@ export default class Options extends React.Component<IProps, IState> {
<Tr text="swag.migoto" />
</div>
<div className="OptionValue" id="menuOptionsDirMigoto">
<SmallButton onClick={this.addMigotoDelay} id="migotoDelay" contents="help.add_delay"></SmallButton>
<SmallButton onClick={this.addMigotoDelay} id="migotoDelay"></SmallButton>
<DirInput onChange={this.setMigoto} value={this.state?.migoto_path} extensions={['exe']} />
</div>
</div>
@ -599,6 +699,31 @@ export default class Options extends React.Component<IProps, IState> {
</div>
</div>
) : null}
<div className="OptionSection" id="menuOptionsContainerOffline">
<div className="OptionLabel" id="menuOptionsLabelOffline">
<Tr text="options.offline_mode" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxOffline">
<Checkbox
onChange={() => this.toggleOption('offline_mode')}
checked={this.state?.offline_mode}
id="offlineMode"
/>
</div>
</div>
<div className="OptionSection" id="menuOptionsContainerNewerGame">
<div className="OptionLabel" id="menuOptionsLabelNewerGame">
<Tr text="Patch Mihoyonet" />
</div>
<div className="OptionValue" id="menuOptionsCheckboxNewerGame">
<Checkbox
onChange={() => this.toggleOption('newer_game')}
checked={this.state?.newer_game}
id="newerGame"
/>
</div>
</div>
<Divider />
@ -709,6 +834,15 @@ export default class Options extends React.Component<IProps, IState> {
value={this.state.launch_args}
/>
</div>
<div className="OptionLabel" id="menuOptionsLabelFixRes">
<Tr text="options.fix_res" />
</div>
<div className="OptionValue" id="menuOptionsButtonfixRes">
<BigButton onClick={this.fixRes} id="fixRes">
<Tr text="components.fix" />
</BigButton>
</div>
</Menu>
)
}

View File

@ -2,6 +2,7 @@
import { invoke } from '@tauri-apps/api/tauri'
import React from 'react'
import Tr from '../../../utils/language'
import { getConfig, getConfigOption } from '../../../utils/configuration'
import './NewsSection.css'
@ -10,6 +11,7 @@ interface IProps {
}
interface IState {
offline: boolean
selected: string
news?: JSX.Element
commitList?: JSX.Element[]
@ -40,6 +42,7 @@ export default class NewsSection extends React.Component<IProps, IState> {
super(props)
this.state = {
offline: false,
selected: props.selected || 'commits',
}
@ -47,7 +50,16 @@ export default class NewsSection extends React.Component<IProps, IState> {
this.showNews = this.showNews.bind(this)
}
componentDidMount() {
async componentDidMount() {
const config = await getConfig()
this.setState({
offline: config.offline_mode || false,
})
// If offline, don't call news
if (this.state.offline) {
return
}
// Call showNews off the bat
this.showNews()
this.setSelected('commits')
@ -110,6 +122,11 @@ export default class NewsSection extends React.Component<IProps, IState> {
}
async showNews() {
const offline_mode = await getConfigOption('offline_mode')
if (offline_mode) {
return
}
let news: JSX.Element | JSX.Element[] = <tr></tr>
switch (this.state.selected) {
@ -124,7 +141,10 @@ export default class NewsSection extends React.Component<IProps, IState> {
case 'latest_version':
news = (
<tr>
<td>Latest version: Grasscutter 1.7.0 - Cultivation 1.2.0</td>
<td>
Work in progress area! These numbers may be outdated, so please do not use them as reference. Latest
version: Grasscutter 1.7.4 - Cultivation 1.5.1
</td>
</tr>
)
break
@ -144,6 +164,10 @@ export default class NewsSection extends React.Component<IProps, IState> {
}
render() {
if (this.state.offline) {
return null
}
return (
<div className="NewsSection" id="newsContainer">
<div className="NewsTabs" id="newsTabsContainer">

View File

@ -29,6 +29,8 @@ let defaultConfig: Configuration
un_elevated: false,
redirect_more: false,
launch_args: '',
offline_mode: false,
newer_game: false,
// Linux stuff
grasscutter_elevation: 'None',
@ -64,6 +66,8 @@ export interface Configuration {
un_elevated: boolean
redirect_more: boolean
launch_args: string
offline_mode: boolean
newer_game: boolean
// Linux stuff
grasscutter_elevation: string
@ -128,7 +132,7 @@ async function readConfigFile() {
await fs.createDir(local + 'cultivation').catch((e) => console.log(e))
}
const innerDirs = await fs.readDir(local + 'cultivation')
const innerDirs = await fs.readDir(local + '/cultivation')
// Create grasscutter dir for potential installation
if (!innerDirs.find((fileOrDir) => fileOrDir?.name === 'grasscutter')) {

View File

@ -73,6 +73,11 @@ export default class DownloadHandler {
const index = this.downloads.findIndex((download) => download.path === errorData.path)
this.downloads[index].status = 'error'
this.downloads[index].error = errorData.error
// Remove GIMI from list as fallback will replace it
if (errorData.path.includes('GIMI.zip')) {
this.downloads.splice(index, 1)
}
})
// Extraction events
@ -92,6 +97,9 @@ export default class DownloadHandler {
// 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'
// Remove completed extraction from list
this.downloads.splice(index, 1)
})
}

View File

@ -45,7 +45,7 @@ export async function getGameDataFolder() {
return null
}
return (await getGameFolder()) + '/' + gameExec.replace('.exe', '_Data')
return (await getGameFolder()) + '\\' + gameExec.replace('.exe', '_Data')
}
export async function getGameVersion() {
@ -55,9 +55,33 @@ export async function getGameVersion() {
return null
}
const hasAsb = await invoke('dir_exists', {
path: GameData + '\\StreamingAssets\\asb_settings.json',
})
if (!hasAsb) {
// For games that cannot determine game version
const otherGameVer: string = await invoke('read_file', {
path: GameData + '\\StreamingAssets\\BinaryVersion.bytes',
})
const versionRaw = otherGameVer.split('.')
const version = {
major: parseInt(versionRaw[0]),
minor: parseInt(versionRaw[1]),
// This will probably never matter, just use major/minor. If needed, full version values are near EOF
release: 0,
}
if (otherGameVer == null || otherGameVer.length < 1) {
return null
}
return version
}
const settings = JSON.parse(
await invoke('read_file', {
path: GameData + '/StreamingAssets/asb_settings.json',
path: GameData + '\\StreamingAssets\\asb_settings.json',
})
)

View File

@ -65,7 +65,7 @@ export default class Tr extends React.Component<IProps, IState> {
})
} else {
this.setState({
translated_text: translation_obj[text] || '',
translated_text: translation_obj[text] || text,
})
}
}

View File

@ -1,8 +1,8 @@
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 patchGame(newerGame: boolean, version: string) {
return invoke('patch_game', { newerGame, version })
}
export async function unpatchGame() {

View File

@ -40,5 +40,32 @@ export async function encryptionEnabled(path: string) {
return false
}
return serverConf.server.http.encryption.useEncryption
if ('server' in serverConf) {
return serverConf.server.http.encryption.useEncryption
}
return false
}
export async function changeResourcePath(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
}
serverConf.folderStructure.resources = './resources/'
// Write file
await invoke('write_file', {
path,
contents: JSON.stringify(serverConf, null, 2),
})
}

View File

@ -40,7 +40,7 @@ const defaultTheme = {
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`,
dataDir: `${await dataDir()}/cultivation`,
})) as BackendThemeList[]
const list: ThemeList[] = [
// ALWAYS include default theme