Files
Cultivation/src-tauri/src/main.rs
2024-10-30 13:32:09 -06:00

538 lines
15 KiB
Rust

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