#![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> = Lazy::new(|| Mutex::new(String::new())); static WATCH_GRASSCUTTER_PROCESS: Lazy> = Lazy::new(|| Mutex::new(String::new())); static GC_PID: std::sync::Mutex = Mutex::new(696969); #[cfg(target_os = "linux")] pub static AAGL_THREAD: Lazy>>> = Lazy::new(|| Mutex::new(None)); fn try_flush() { std::io::stdout().flush().unwrap_or(()) } async fn parse_args(inp: &Vec) -> Result { 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::("host").is_ok() && !args.value_of::("host")?.is_empty() { let host = args.value_of::("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 = 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> { 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 }