diff --git a/Cargo.lock b/Cargo.lock index bb7ff78..70c20d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -812,6 +812,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -902,6 +912,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1198,6 +1214,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1261,7 +1286,7 @@ dependencies = [ [[package]] name = "syntax_bootstrapper" -version = "1.2.0" +version = "1.2.1" dependencies = [ "chrono", "colored", @@ -1273,6 +1298,8 @@ dependencies = [ "reqwest", "term_size", "tokio", + "tracing", + "tracing-subscriber", "winreg 0.51.0", "winres", "zip-extract", @@ -1321,6 +1348,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.1.45" @@ -1435,22 +1472,59 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] [[package]] -name = "tracing-core" -version = "0.1.31" +name = "tracing-attributes" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -1503,6 +1577,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 525585c..1c2728f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ tokio = { version = "1.32.0", features=["full"]} futures-util = "0.3.28" md5 = "0.7.0" zip-extract = "0.1.2" +tracing = "0.1.40" +tracing-subscriber = "0.3.17" [target.'cfg(windows)'.dependencies] winreg = "0.51.0" diff --git a/src/ascii.txt b/src/ascii.txt new file mode 100644 index 0000000..577be38 --- /dev/null +++ b/src/ascii.txt @@ -0,0 +1,8 @@ + .d8888b. Y88b d88P 888b 888 88888888888 d8888 Y88b d88P + d88P Y88b Y88b d88P 8888b 888 888 d88888 Y88b d88P + Y88b. Y88o88P 88888b 888 888 d88P888 Y88o88P + \"Y888b. Y888P 888Y88b 888 888 d88P 888 Y888P + \"Y88b. 888 888 Y88b888 888 d88P 888 d888b + \"888 888 888 Y88888 888 d88P 888 d88888b + Y88b d88P 888 888 Y8888 888 d8888888888 d88P Y88b + \"Y8888P\" 888 888 Y888 888 d88P 888 d88P Y88b \ No newline at end of file diff --git a/src/constants/mod.rs b/src/constants/mod.rs new file mode 100644 index 0000000..d5e1e23 --- /dev/null +++ b/src/constants/mod.rs @@ -0,0 +1,19 @@ +pub const FILES_TO_DOWNLOAD: [[&str; 2]; 17] = [ + ["SyntaxApp.zip", "./"], + ["NPSyntaxProxy.zip", "./"], + ["SyntaxProxy.zip", "./"], + ["Libraries.zip", "./"], + ["redist.zip", "./"], + ["content-textures.zip", "./content/textures"], + ["content-textures2.zip", "./content/textures"], + ["content-textures3.zip", "./content/textures"], + ["content-terrain.zip", "./content/terrain"], + ["content-fonts.zip", "./content/fonts"], + ["content-sounds.zip", "./content/sounds"], + ["content-scripts.zip", "./content/scripts"], + ["content-sky.zip", "./content/sky"], + ["content-music.zip", "./content/music"], + ["content-particles.zip", "./content/particles"], + ["2018client.zip", "./Client2018"], + ["2020client.zip", "./Client2020"], +]; diff --git a/src/main.rs b/src/main.rs index bf6a159..8151ccd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,159 +1,83 @@ use colored::*; -use std::path::PathBuf; -use reqwest::Client; use dirs::data_local_dir; use futures_util::StreamExt; use md5; +use metadata::LevelFilter; +use reqwest::Client; +use std::path::PathBuf; +use tokio::task::JoinSet; use zip_extract; +mod constants; +mod util; + +use constants::*; + +use tracing::*; +use util::*; + +#[cfg(not(target_os = "windows"))] +use std::io::prelude::*; +#[cfg(not(target_os = "windows"))] +use std::os::unix::fs::FileExt; #[cfg(target_os = "windows")] use std::os::windows::prelude::FileExt; #[cfg(target_os = "windows")] use winreg::enums::*; #[cfg(target_os = "windows")] use winreg::RegKey; -#[cfg(not(target_os = "windows"))] -use std::io::prelude::*; -#[cfg(not(target_os = "windows"))] -use std::os::unix::fs::FileExt; - -fn info( message : &str ) { - let time = chrono::Local::now().format("%H:%M:%S").to_string(); - println!("[{}] [{}] {}", time.bold().blue(), "INFO".bold().green(), message); -} - -fn error( message : &str ) { - let time = chrono::Local::now().format("%H:%M:%S").to_string(); - println!("[{}] [{}] {}", time.bold().blue(), "ERROR".bold().red(), message); -} #[cfg(debug_assertions)] -fn debug( message : &str ) { - let time = chrono::Local::now().format("%H:%M:%S").to_string(); - println!("[{}] [{}] {}", time.bold().blue(), "DEBUG".bold().yellow(), message); -} - +const DEBUG: bool = true; +#[cfg(debug_assertions)] +const MAX_TRACING_LEVEL: LevelFilter = LevelFilter::DEBUG; #[cfg(not(debug_assertions))] -fn debug( message : &str ) {} +const DEBUG: bool = false; +#[cfg(not(debug_assertions))] +const MAX_TRACING_LEVEL: LevelFilter = LevelFilter::ERROR; -pub async fn http_get( client: &Client ,url: &str ) -> Result { - debug(&format!("{} {}", "GET".green(), url.bright_blue())); - let response = client.get(url).send().await; - if (response.is_err()) { - debug(&format!("Failed to fetch {}", url.bright_blue())); - return Err(response.err().unwrap()); - } - let response_body = response.unwrap().text().await.unwrap(); - Ok(response_body) -} - -pub async fn download_file( client: &Client, url: &str, path: &PathBuf ) { - debug(&format!("{} {}", "GET".green(), url.bright_blue())); - let response = client.get(url).send().await.unwrap(); - let content_length = response.content_length().unwrap(); - debug(&format!("Content Length: {}", content_length)); - - let time = chrono::Local::now().format("%H:%M:%S").to_string(); - let pg_bar_str = " {spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})"; - let progress_bar = indicatif::ProgressBar::new(content_length); - let progress_style = indicatif::ProgressStyle::default_bar() - .template( - format!("{}\n{}", - format!( - "[{}] [{}] {}", - time.bold().blue(), - "INFO".bold().green(), - &format!("Downloading {}", &url.bright_blue()) - ), - pg_bar_str).as_str() - ) - .unwrap().progress_chars("#>-"); - progress_bar.set_style(progress_style); - progress_bar.set_message("Downloading File"); - - let file = std::fs::File::create(path).unwrap(); - let mut downloaded: u64 = 0; - let mut stream = response.bytes_stream(); - - while let Some(item) = stream.next().await { - let chunk = item.or(Err(format!("Error while downloading file"))).unwrap(); - #[cfg(target_os = "windows")] - { - file.seek_write(chunk.as_ref(), downloaded).unwrap(); - } - #[cfg(not(target_os = "windows"))] - { - file.write_at(chunk.as_ref(), downloaded).unwrap(); - } - let new = std::cmp::min(downloaded + (chunk.len() as u64), content_length); - downloaded = new; - progress_bar.set_position(new); - } - progress_bar.finish(); - info(format!("Finished downloading {}", url.green()).as_str()); -} - -pub async fn download_file_prefix( client: &Client, url: &str, path_prefix : &PathBuf ) -> PathBuf { - let path = path_prefix.join(generate_md5(url).await); - download_file(client, url, &path).await; - return path; -} - -pub async fn generate_md5( input : &str ) -> String { - let hashed_input = md5::compute(input.as_bytes()); - return format!("{:x}", hashed_input); -} - -pub async fn create_folder_if_not_exists( path: &PathBuf ) { - if !path.exists() { - info(&format!("Creating folder {}", path.to_str().unwrap().bright_blue())); - std::fs::create_dir_all(path).unwrap(); - } -} - -fn get_installation_directory() -> PathBuf { - return PathBuf::from(data_local_dir().unwrap().to_str().unwrap()).join("Syntax"); -} +const ASCII_ART: &str = include_str!("./ascii.txt"); #[tokio::main] async fn main() { - // Clear the terminal before printing the startup text #[cfg(target_os = "windows")] { std::process::Command::new("cmd") - .args(&["/c", "cls"]) - .spawn() - .expect("cls command failed to start") - .wait() - .expect("failed to wait"); + .args(&["/c", "cls"]) + .spawn() + .expect("cls command failed to start") + .wait() + .expect("failed to wait"); } #[cfg(not(target_os = "windows"))] { std::process::Command::new("clear").spawn().unwrap(); } + tracing_subscriber::fmt() + .with_max_level(MAX_TRACING_LEVEL) + .pretty() + .init(); + let args: Vec = std::env::args().collect(); - let base_url : &str = "www.syntax.eco"; - let mut setup_url : &str = "setup.syntax.eco"; - let fallback_setup_url : &str = "d2f3pa9j0u8v6f.cloudfront.net"; - let mut bootstrapper_filename :&str = "SyntaxPlayerLauncher.exe"; + let base_url: &str = "www.syntax.eco"; + let mut setup_url: &str = "setup.syntax.eco"; + let fallback_setup_url: &str = "d2f3pa9j0u8v6f.cloudfront.net"; + let mut bootstrapper_filename: &str = "SyntaxPlayerLauncher.exe"; #[cfg(not(target_os = "windows"))] { bootstrapper_filename = "SyntaxPlayerLinuxLauncher"; } let build_date = include_str!(concat!(env!("OUT_DIR"), "/build_date.txt")); - let startup_text = format!(" - .d8888b. Y88b d88P 888b 888 88888888888 d8888 Y88b d88P - d88P Y88b Y88b d88P 8888b 888 888 d88888 Y88b d88P - Y88b. Y88o88P 88888b 888 888 d88P888 Y88o88P - \"Y888b. Y888P 888Y88b 888 888 d88P 888 Y888P - \"Y88b. 888 888 Y88b888 888 d88P 888 d888b - \"888 888 888 Y88888 888 d88P 888 d88888b - Y88b d88P 888 888 Y8888 888 d8888888888 d88P Y88b - \"Y8888P\" 888 888 Y888 888 d88P 888 d88P Y88b - - {} | Build Date: {} | Version: {}", base_url ,build_date, env!("CARGO_PKG_VERSION")); + let startup_text = ASCII_ART.to_owned(); + + let bootstrapper_info = format!( + "{} | Build Date: {} | Version: {}", + base_url, + build_date, + env!("CARGO_PKG_VERSION"), + ); // Format the startup text to be centered let mut terminal_width = 80; @@ -161,51 +85,80 @@ async fn main() { terminal_width = w; } if terminal_width < 80 { - print!("{}\n", format!("SYNTAX Bootstrapper | {} | Build Date: {} | Version: {}", base_url, build_date, env!("CARGO_PKG_VERSION")).to_string().magenta().cyan().italic().on_black()); // Fallback message + print!( + "{}\n", + format!( + "SYNTAX Bootstrapper | {} | Build Date: {} | Version: {}", + base_url, + build_date, + env!("CARGO_PKG_VERSION") + ) + .to_string() + .magenta() + .cyan() + .italic() + .on_black() + ); // Fallback message } else { let startup_text_lines = startup_text.lines().collect::>(); //println!("{}", startup_text.bold().blue().on_black()); - + // print all lines except the last one - for line in &startup_text_lines[0..startup_text_lines.len() - 1] { + for line in startup_text_lines { let spaces = (terminal_width - line.len()) / 2; let formatted_line = format!("{}{}", " ".repeat(spaces), line); println!("{}", formatted_line.bright_magenta().italic().on_black()); } // print last line as a different color - let last_line = startup_text_lines[startup_text_lines.len() - 1]; - let spaces = (terminal_width - last_line.len()) / 2; - let last_line = format!("{}{}", " ".repeat(spaces), last_line); - println!("{}\n", last_line.magenta().cyan().italic().on_black()); + println!( + "{}\n", + bootstrapper_info.magenta().cyan().italic().on_black() + ); } - let http_client: Client = reqwest::Client::builder() - .no_gzip() - .build() - .unwrap(); - debug(format!("Setup Server: {} | Base Server: {}", setup_url.bright_blue(), base_url.bright_blue()).as_str()); - debug("Fetching latest client version from setup server"); - - let latest_client_version : String; - let latest_client_version_response = http_get(&http_client ,&format!("https://{}/version", setup_url)).await; + let http_client: Client = reqwest::Client::builder().no_gzip().build().unwrap(); + debug!( + "Setup Server: {} | Base Server: {}", + setup_url.bright_blue(), + base_url.bright_blue() + ); + + debug!("Fetching latest client version from setup server"); + + let latest_client_version: String; + let latest_client_version_response = + http_get(&http_client, &format!("https://{}/version", setup_url)).await; match latest_client_version_response { Ok(latest_client_version_result) => { - debug(&format!("Latest Client Version: {}", latest_client_version_result.bright_blue())); + debug!( + "Latest Client Version: {}", + latest_client_version_result.bright_blue() + ); latest_client_version = latest_client_version_result; - }, + } Err(e) => { - error(&format!("Failed to fetch latest client version from setup server: [{}], attempting to fallback to {}", e.to_string().bright_red(), fallback_setup_url.bright_blue())); - let fallback_client_version_response = http_get(&http_client ,&format!("https://{}/version", fallback_setup_url)).await; + error!("Failed to fetch latest client version from setup server: [{}], attempting to fallback to {}", e.to_string().bright_red(), fallback_setup_url.bright_blue()); + let fallback_client_version_response = http_get( + &http_client, + &format!("https://{}/version", fallback_setup_url), + ) + .await; match fallback_client_version_response { Ok(fallback_client_version_result) => { - info(&format!("Successfully fetched latest client version from fallback setup server: {}", fallback_setup_url.bright_blue())); - debug(&format!("Latest Client Version: {}", fallback_client_version_result.bright_blue())); + info!( + "Successfully fetched latest client version from fallback setup server: {}", + fallback_setup_url.bright_blue() + ); + debug!( + "Latest Client Version: {}", + fallback_client_version_result.bright_blue() + ); latest_client_version = fallback_client_version_result; setup_url = fallback_setup_url; - }, + } Err(e) => { - error(&format!("Failed to fetch latest client version from fallback setup server: {}, are you connected to the internet?", e)); + error!("Failed to fetch latest client version from fallback setup server: {}, are you connected to the internet?", e); std::thread::sleep(std::time::Duration::from_secs(10)); std::process::exit(0); } @@ -214,35 +167,58 @@ async fn main() { } // Wait for the latest client version to be fetched - info(&format!("Latest Client Version: {}", latest_client_version.cyan().underline())); - debug(&format!("Setup Server: {}", setup_url.cyan().underline())); + info!( + "Latest Client Version: {}", + latest_client_version.cyan().underline() + ); + debug!("Setup Server: {}", setup_url.cyan().underline()); let installation_directory = get_installation_directory(); - debug(&format!("Installation Directory: {}", installation_directory.to_str().unwrap().bright_blue())); + debug!( + "Installation Directory: {}", + installation_directory.to_str().unwrap().bright_blue() + ); create_folder_if_not_exists(&installation_directory).await; let versions_directory = installation_directory.join("Versions"); - debug(&format!("Versions Directory: {}", versions_directory.to_str().unwrap().bright_blue())); + debug!( + "Versions Directory: {}", + versions_directory.to_str().unwrap().bright_blue() + ); create_folder_if_not_exists(&versions_directory).await; let temp_downloads_directory = installation_directory.join("Downloads"); - debug(&format!("Temp Downloads Directory: {}", temp_downloads_directory.to_str().unwrap().bright_blue())); + debug!( + "Temp Downloads Directory: {}", + temp_downloads_directory.to_str().unwrap().bright_blue() + ); create_folder_if_not_exists(&temp_downloads_directory).await; let current_version_directory = versions_directory.join(format!("{}", latest_client_version)); - debug(&format!("Current Version Directory: {}", current_version_directory.to_str().unwrap().bright_blue())); + debug!( + "Current Version Directory: {}", + current_version_directory.to_str().unwrap().bright_blue() + ); create_folder_if_not_exists(¤t_version_directory).await; let latest_bootstrapper_path = current_version_directory.join(bootstrapper_filename); // Is the program currently running from the latest version directory? let current_exe_path = std::env::current_exe().unwrap(); // If the current exe path is not in the current version directory, then we need to run the latest bootstrapper ( download if needed ) - if !current_exe_path.starts_with(¤t_version_directory) { + if !current_exe_path.starts_with(¤t_version_directory) && !DEBUG { // Check if the latest bootstrapper is downloaded if !latest_bootstrapper_path.exists() { - info("Downloading the latest bootstrapper and restarting"); + info!("Downloading the latest bootstrapper and restarting"); // Download the latest bootstrapper - download_file(&http_client, &format!("https://{}/{}-{}", setup_url, latest_client_version, bootstrapper_filename), &latest_bootstrapper_path).await; + download_to_file( + &http_client, + &format!( + "https://{}/{}-{}", + setup_url, latest_client_version, bootstrapper_filename + ), + &latest_bootstrapper_path, + ) + .await; } // Run the latest bootstrapper ( with the same arguments passed to us ) and exit #[cfg(target_os = "windows")] @@ -250,12 +226,20 @@ async fn main() { let mut command = std::process::Command::new(latest_bootstrapper_path.clone()); command.args(&args[1..]); match command.spawn() { - Ok(_) => {}, + Ok(_) => {} Err(e) => { - debug(&format!("Bootstrapper errored with error {}", e)); + debug!(&format!("Bootstrapper errored with error {}", e)); info("Found bootstrapper was corrupted! Downloading..."); std::fs::remove_file(latest_bootstrapper_path.clone()).unwrap(); - download_file(&http_client, &format!("https://{}/{}-{}", setup_url, latest_client_version, bootstrapper_filename), &latest_bootstrapper_path).await; + download_file( + &http_client, + &format!( + "https://{}/{}-{}", + setup_url, latest_client_version, bootstrapper_filename + ), + &latest_bootstrapper_path, + ) + .await; command.spawn().expect("Bootstrapper is still corrupted."); std::thread::sleep(std::time::Duration::from_secs(20)); } @@ -264,9 +248,13 @@ async fn main() { #[cfg(not(target_os = "windows"))] { // Make sure the latest bootstrapper is executable - std::process::Command::new("chmod").arg("+x").arg(latest_bootstrapper_path.to_str().unwrap()).spawn().unwrap(); + std::process::Command::new("chmod") + .arg("+x") + .arg(latest_bootstrapper_path.to_str().unwrap()) + .spawn() + .unwrap(); - info("We need permission to run the latest bootstrapper"); + info!("We need permission to run the latest bootstrapper"); let mut command = std::process::Command::new(latest_bootstrapper_path); command.args(&args[1..]); command.spawn().unwrap(); @@ -275,13 +263,13 @@ async fn main() { } // Looks like we are running from the latest version directory, so we can continue with the update process - // Check for "AppSettings.xml" in the current version directory + // Check for "AppSettings.xml" in the current version directory // If it doesent exist, then we got either a fresh directory or a corrupted installation // So delete the every file in the current version directory except for the Bootstrapper itself let app_settings_path = current_version_directory.join("AppSettings.xml"); let client_executable_path = current_version_directory.join("SyntaxPlayerBeta.exe"); if !app_settings_path.exists() || !client_executable_path.exists() { - info("Downloading the latest client files, this may take a while."); + info!("Downloading the latest client files, this may take a while."); for entry in std::fs::read_dir(¤t_version_directory).unwrap() { let entry = entry.unwrap(); let path = entry.path(); @@ -294,117 +282,70 @@ async fn main() { } } - let VersionURLPrefix = format!("https://{}/{}-", setup_url, latest_client_version); - let SyntaxAppZip : PathBuf = download_file_prefix(&http_client, format!("{}SyntaxApp.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let NPSyntaxProxyZip : PathBuf = download_file_prefix(&http_client, format!("{}NPSyntaxProxy.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let SyntaxProxyZip : PathBuf = download_file_prefix(&http_client, format!("{}SyntaxProxy.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let LibrariesZip : PathBuf = download_file_prefix(&http_client, format!("{}Libraries.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let RedistZip : PathBuf = download_file_prefix(&http_client, format!("{}redist.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; + let version_url_prefix = format!("https://{}/{}-", setup_url, latest_client_version); - let ContentTexturesZip : PathBuf = download_file_prefix(&http_client, format!("{}content-textures.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let ContentTextures2Zip : PathBuf = download_file_prefix(&http_client, format!("{}content-textures2.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let ContentTextures3Zip : PathBuf = download_file_prefix(&http_client, format!("{}content-textures3.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let ContentTerrainZip : PathBuf = download_file_prefix(&http_client, format!("{}content-terrain.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let ContentFontsZip : PathBuf = download_file_prefix(&http_client, format!("{}content-fonts.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let ContentSoundsZip : PathBuf = download_file_prefix(&http_client, format!("{}content-sounds.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let ContentScriptsZip : PathBuf = download_file_prefix(&http_client, format!("{}content-scripts.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let ContentSkyZip : PathBuf = download_file_prefix(&http_client, format!("{}content-sky.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let ContentMusicZip : PathBuf = download_file_prefix(&http_client, format!("{}content-music.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let ContentParticles : PathBuf = download_file_prefix(&http_client, format!("{}content-particles.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; + /* Use a joisnet to run multiple async functions at once */ - let ShadersZip : PathBuf = download_file_prefix(&http_client, format!("{}shaders.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; + let mut set = JoinSet::new(); - let Client2018Zip : PathBuf = download_file_prefix(&http_client, format!("{}2018client.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - let Client2020Zip : PathBuf = download_file_prefix(&http_client, format!("{}2020client.zip", VersionURLPrefix).as_str(), &temp_downloads_directory).await; - info("Download finished, extracting files."); - - fn extract_to_dir( zip_file : &PathBuf, target_dir : &PathBuf ) { - let zip_file_cursor = std::fs::File::open(zip_file).unwrap(); - zip_extract::extract(zip_file_cursor, target_dir, false).unwrap(); + for [value, _] in FILES_TO_DOWNLOAD { + set.spawn(download_and_extract( + value.to_string(), + version_url_prefix.clone(), + current_version_directory.clone(), + )); } - extract_to_dir(&SyntaxAppZip, ¤t_version_directory); - extract_to_dir(&NPSyntaxProxyZip, ¤t_version_directory); - extract_to_dir(&SyntaxProxyZip, ¤t_version_directory); - extract_to_dir(&LibrariesZip, ¤t_version_directory); - extract_to_dir(&RedistZip, ¤t_version_directory); - let content_directory = current_version_directory.join("content"); - let platform_content_directory = current_version_directory.join("PlatformContent"); - let shaders_directory = current_version_directory.join("shaders"); + while let Some(value) = set.join_next().await { + value.unwrap() + } - create_folder_if_not_exists(&content_directory).await; - create_folder_if_not_exists(&platform_content_directory).await; - create_folder_if_not_exists(&shaders_directory).await; + info!("Binary installed"); - let fonts_directory = content_directory.join("fonts"); - let music_directory = content_directory.join("music"); - let particles_directory = content_directory.join("particles"); - let sky_directory = content_directory.join("sky"); - let sounds_directory = content_directory.join("sounds"); - let textures_directory = content_directory.join("textures"); - let scripts_directory = content_directory.join("scripts"); + /* Convert to async due to this being a slow function */ - create_folder_if_not_exists(&fonts_directory).await; - create_folder_if_not_exists(&music_directory).await; - create_folder_if_not_exists(&particles_directory).await; - create_folder_if_not_exists(&sky_directory).await; - create_folder_if_not_exists(&sounds_directory).await; - create_folder_if_not_exists(&textures_directory).await; - - extract_to_dir(&ContentTexturesZip, &textures_directory); - extract_to_dir(&ContentTextures2Zip, &textures_directory); - extract_to_dir(&ContentFontsZip, &fonts_directory); - extract_to_dir(&ContentSoundsZip, &sounds_directory); - extract_to_dir(&ContentSkyZip, &sky_directory); - extract_to_dir(&ContentMusicZip, &music_directory); - extract_to_dir(&ContentParticles, &particles_directory); - extract_to_dir(&ContentScriptsZip, &scripts_directory); - - let platform_pc_directory = platform_content_directory.join("pc"); - create_folder_if_not_exists(&platform_pc_directory).await; - let terrain_directory = platform_pc_directory.join("terrain"); - let textures_directory = platform_pc_directory.join("textures"); - create_folder_if_not_exists(&terrain_directory).await; - create_folder_if_not_exists(&textures_directory).await; - - extract_to_dir(&ContentTerrainZip, &terrain_directory); - extract_to_dir(&ContentTextures3Zip, &textures_directory); - extract_to_dir(&ShadersZip, &shaders_directory); - - let client_2018_directory = current_version_directory.join("Client2018"); - create_folder_if_not_exists(&client_2018_directory).await; - extract_to_dir(&Client2018Zip, &client_2018_directory); - - let client_2020_directory = current_version_directory.join("Client2020"); - create_folder_if_not_exists(&client_2020_directory).await; - extract_to_dir(&Client2020Zip, &client_2020_directory); - - info("Finished extracting files, cleaning up."); - std::fs::remove_dir_all(&temp_downloads_directory).unwrap(); + // Redacted for lagging vscode // Install the syntax-player scheme in the registry - info("Installing syntax-player scheme"); + info!("Installing syntax-player scheme"); #[cfg(target_os = "windows")] { let hkey_current_user = RegKey::predef(HKEY_CURRENT_USER); - let hkey_classes_root : RegKey = hkey_current_user.open_subkey("Software\\Classes").unwrap(); + let hkey_classes_root: RegKey = + hkey_current_user.open_subkey("Software\\Classes").unwrap(); let hkey_syntax_player = hkey_classes_root.create_subkey("syntax-player").unwrap().0; let hkey_syntax_player_shell = hkey_syntax_player.create_subkey("shell").unwrap().0; - let hkey_syntax_player_shell_open = hkey_syntax_player_shell.create_subkey("open").unwrap().0; - let hkey_syntax_player_shell_open_command = hkey_syntax_player_shell_open.create_subkey("command").unwrap().0; + let hkey_syntax_player_shell_open = + hkey_syntax_player_shell.create_subkey("open").unwrap().0; + let hkey_syntax_player_shell_open_command = hkey_syntax_player_shell_open + .create_subkey("command") + .unwrap() + .0; let defaulticon = hkey_syntax_player.create_subkey("DefaultIcon").unwrap().0; - hkey_syntax_player_shell_open_command.set_value("", &format!("\"{}\" \"%1\"", current_exe_path.to_str().unwrap())).unwrap(); - defaulticon.set_value("", &format!("\"{}\",0", current_exe_path.to_str().unwrap())).unwrap(); - hkey_syntax_player.set_value("", &format!("URL: Syntax Protocol")).unwrap(); + hkey_syntax_player_shell_open_command + .set_value( + "", + &format!("\"{}\" \"%1\"", current_exe_path.to_str().unwrap()), + ) + .unwrap(); + defaulticon + .set_value("", &format!("\"{}\",0", current_exe_path.to_str().unwrap())) + .unwrap(); + hkey_syntax_player + .set_value("", &format!("URL: Syntax Protocol")) + .unwrap(); hkey_syntax_player.set_value("URL Protocol", &"").unwrap(); } #[cfg(not(target_os = "windows"))] { // Linux support // We have to write a .desktop file to ~/.local/share/applications - let desktop_file_path = dirs::data_local_dir().unwrap().join("applications").join("syntax-player.desktop"); + let desktop_file_path = dirs::data_local_dir() + .unwrap() + .join("applications") + .join("syntax-player.desktop"); let desktop_file = format!( -"[Desktop Entry] + "[Desktop Entry] Name=Syntax Launcher Exec={} %u Terminal=true @@ -414,31 +355,37 @@ Icon={} StartupWMClass=SyntaxLauncher Categories=Game; Comment=Syntax Launcher -", current_exe_path.to_str().unwrap(), current_exe_path.to_str().unwrap()); +", + current_exe_path.to_str().unwrap(), + current_exe_path.to_str().unwrap() + ); std::fs::write(desktop_file_path, desktop_file).unwrap(); // We also have to write a mimeapps.list file to ~/.config let mimeapps_list_path = dirs::config_dir().unwrap().join("mimeapps.list"); let mimeapps_list = format!( -"[Default Applications] + "[Default Applications] x-scheme-handler/syntax-player=syntax-player.desktop -"); +" + ); std::fs::write(mimeapps_list_path, mimeapps_list).unwrap(); // We also have to write a mimeapps.list file to ~/.local/share let mimeapps_list_path = dirs::data_local_dir().unwrap().join("mimeapps.list"); let mimeapps_list = format!( -"[Default Applications] + "[Default Applications] x-scheme-handler/syntax-player=syntax-player.desktop -"); +" + ); std::fs::write(mimeapps_list_path, mimeapps_list).unwrap(); } // Write the AppSettings.xml file let app_settings_xml = format!( -" + " content https://{} -", base_url +", + base_url ); std::fs::write(app_settings_path, app_settings_xml).unwrap(); @@ -456,17 +403,25 @@ x-scheme-handler/syntax-player=syntax-player.desktop // Parse the arguments passed to the bootstrapper // Looks something like "syntax-player://1+launchmode:play+gameinfo:TICKET+placelauncherurl:https://www.syntax.eco/Game/placelauncher.ashx?placeId=660&t=TICKET+k:l" - debug(&format!("Arguments Passed: {}", args.join(" ").bright_blue())); + debug!("Arguments Passed: {}", args.join(" ").bright_blue()); if args.len() == 1 { // Just open the website #[cfg(target_os = "windows")] { - std::process::Command::new("cmd").arg("/c").arg("start").arg("https://www.syntax.eco/games").spawn().unwrap(); + std::process::Command::new("cmd") + .arg("/c") + .arg("start") + .arg("https://www.syntax.eco/games") + .spawn() + .unwrap(); std::process::exit(0); } #[cfg(not(target_os = "windows"))] { - std::process::Command::new("xdg-open").arg("https://www.syntax.eco/games").spawn().unwrap(); + std::process::Command::new("xdg-open") + .arg("https://www.syntax.eco/games") + .spawn() + .unwrap(); std::process::exit(0); } } @@ -479,30 +434,29 @@ x-scheme-handler/syntax-player=syntax-player.desktop let mut authentication_ticket = String::new(); let mut join_script = String::new(); let mut client_year = String::new(); - + for arg in main_args { let mut arg_split = arg.split(":"); let key = arg_split.next().unwrap(); - let value = - if arg_split.clone().count() > 0 { - arg_split.collect::>().join(":") - } else { - String::new() - }; - debug(&format!("{}: {}", key.bright_blue(), value.bright_blue())); + let value = if arg_split.clone().count() > 0 { + arg_split.collect::>().join(":") + } else { + String::new() + }; + debug!("{}: {}", key.bright_blue(), value.bright_blue()); match key { "launchmode" => { launch_mode = value.to_string(); - }, + } "gameinfo" => { authentication_ticket = value.to_string(); - }, + } "placelauncherurl" => { join_script = value.to_string(); - }, + } "clientyear" => { client_year = value.to_string(); - }, + } _ => {} } } @@ -514,18 +468,22 @@ x-scheme-handler/syntax-player=syntax-player.desktop let wine_path_file = installation_directory.join("winepath.txt"); if wine_path_file.exists() { let custom_wine = std::fs::read_to_string(wine_path_file).unwrap(); - info(&format!("Using custom wine binary: {}", custom_wine.bright_blue())); + info!("Using custom wine binary: {}", custom_wine.bright_blue()); } else { - info("No custom wine binary specified, using default wine command"); - info(format!("If you want to use a custom wine binary, please create a file at {} with the path to the wine binary", wine_path_file.to_str().unwrap()).as_str()); + info!("No custom wine binary specified, using default wine command"); + info!("If you want to use a custom wine binary, please create a file at {} with the path to the wine binary", wine_path_file.to_str().unwrap()); } } - let client_executable_path : PathBuf; - debug(&client_year.to_string()); + let client_executable_path: PathBuf; + debug!("{}", &client_year.to_string()); if client_year == "2018" { - client_executable_path = current_version_directory.join("Client2018").join("SyntaxPlayerBeta.exe"); + client_executable_path = current_version_directory + .join("Client2018") + .join("SyntaxPlayerBeta.exe"); } else if client_year == "2020" { - client_executable_path = current_version_directory.join("Client2020").join("SyntaxPlayerBeta.exe"); + client_executable_path = current_version_directory + .join("Client2020") + .join("SyntaxPlayerBeta.exe"); } else { client_executable_path = current_version_directory.join("SyntaxPlayerBeta.exe"); } @@ -534,17 +492,25 @@ x-scheme-handler/syntax-player=syntax-player.desktop let app_settings_path = current_version_directory.join("AppSettings.xml"); std::fs::remove_file(app_settings_path).unwrap(); - error("Failed to run SyntaxPlayerBeta.exe, is your antivirus removing it? The bootstrapper will attempt to redownload the client on next launch."); + error!("Failed to run SyntaxPlayerBeta.exe, is your antivirus removing it? The bootstrapper will attempt to redownload the client on next launch."); std::thread::sleep(std::time::Duration::from_secs(20)); std::process::exit(0); } match launch_mode.as_str() { "play" => { - info("Launching SYNTAX"); + info!("Launching SYNTAX"); #[cfg(target_os = "windows")] - { + { let mut command = std::process::Command::new(client_executable_path); - command.args(&["--play","--authenticationUrl", format!("https://{}/Login/Negotiate.ashx", base_url).as_str(), "--authenticationTicket", authentication_ticket.as_str(), "--joinScriptUrl", format!("{}",join_script.as_str()).as_str()]); + command.args(&[ + "--play", + "--authenticationUrl", + format!("https://{}/Login/Negotiate.ashx", base_url).as_str(), + "--authenticationTicket", + authentication_ticket.as_str(), + "--joinScriptUrl", + format!("{}", join_script.as_str()).as_str(), + ]); command.spawn().unwrap(); std::thread::sleep(std::time::Duration::from_secs(5)); std::process::exit(0); @@ -553,18 +519,27 @@ x-scheme-handler/syntax-player=syntax-player.desktop { // We have to launch the game through wine let mut command = std::process::Command::new(custom_wine); - command.args(&[client_executable_path.to_str().unwrap(), "--play","--authenticationUrl", format!("https://{}/Login/Negotiate.ashx", base_url).as_str(), "--authenticationTicket", authentication_ticket.as_str(), "--joinScriptUrl", format!("{}",join_script.as_str()).as_str()]); + command.args(&[ + client_executable_path.to_str().unwrap(), + "--play", + "--authenticationUrl", + format!("https://{}/Login/Negotiate.ashx", base_url).as_str(), + "--authenticationTicket", + authentication_ticket.as_str(), + "--joinScriptUrl", + format!("{}", join_script.as_str()).as_str(), + ]); // We must wait for the game to exit before exiting the bootstrapper let mut child = command.spawn().unwrap(); child.wait().unwrap(); std::thread::sleep(std::time::Duration::from_secs(1)); std::process::exit(0); } - }, + } _ => { - error("Unknown launch mode, exiting."); + error!("Unknown launch mode, exiting."); std::thread::sleep(std::time::Duration::from_secs(10)); std::process::exit(0); } } -} \ No newline at end of file +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..5927a4c --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,159 @@ +/* + Move all generic functions into this area +*/ +use colored::*; +use dirs::data_local_dir; +use futures_util::StreamExt; +use md5; +use reqwest::Client; +use reqwest::ClientBuilder; +use std::io::Cursor; +use std::path::PathBuf; +use tokio::fs; +use tokio::fs::create_dir_all; +use zip_extract; + +use crate::constants::*; +use tracing::*; + +#[cfg(not(target_os = "windows"))] +use std::io::prelude::*; +/* +#[cfg(not(target_os = "windows"))] +use std::os::unix::fs::FileExt; +#[cfg(target_os = "windows")] +use std::os::windows::prelude::FileExt; +*/ +#[cfg(target_os = "windows")] +use winreg::enums::*; +#[cfg(target_os = "windows")] +use winreg::RegKey; + +pub async fn http_get(client: &Client, url: &str) -> Result { + debug!("{} {}", "GET".green(), url.bright_blue()); + let response = client.get(url).send().await; + if response.is_err() { + debug!("Failed to fetch {}", url.bright_blue()); + return Err(response.err().unwrap()); + } + let response_body = response.unwrap().text().await.unwrap(); + Ok(response_body) +} + +pub async fn download_file(client: &Client, url: &str) -> Vec { + debug!("{} {}", "GET".green(), url.bright_blue()); + let response = client.get(url).send().await.unwrap(); + /* Why through over a visual bug? */ + let content_length = response.content_length().or(Some(0)).unwrap(); + debug!("Content Length: {}", content_length); + + let time = chrono::Local::now().format("%H:%M:%S").to_string(); + let pg_bar_str = + " {spinner:.green} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})"; + let progress_bar = indicatif::ProgressBar::new(content_length); + let progress_style = indicatif::ProgressStyle::default_bar() + .template( + format!( + "{}\n{}", + format!( + "[{}] [{}] {}", + time.bold().blue(), + "INFO".bold().green(), + &format!("Downloading {}", &url.bright_blue()) + ), + pg_bar_str + ) + .as_str(), + ) + .unwrap() + .progress_chars("#>-"); + progress_bar.set_style(progress_style); + progress_bar.set_message("Downloading File"); + + let mut buffer: Vec = vec![]; + let mut downloaded: u64 = 0; + let mut stream = response.bytes_stream(); + + while let Some(item) = stream.next().await { + let chunk = item + .or(Err(format!("Error while downloading file"))) + .unwrap(); + + buffer.write_all(chunk.as_ref()).unwrap(); + let new = std::cmp::min(downloaded + (chunk.len() as u64), content_length); + downloaded = new; + progress_bar.set_position(new); + } + progress_bar.finish(); + info!("Finished downloading {}", url.green()); + + return buffer; +} + +pub async fn download_to_file(client: &Client, url: &str, path: &PathBuf) { + let bytes = download_file(client, url).await; + fs::write(path, bytes).await.unwrap(); +} + +pub async fn download_file_prefix>(client: &Client, url: T) -> Vec { + let string: String = url.into(); + let buffer = download_file(client, &string).await; + return buffer; +} + +/* +pub async fn generate_md5(input: &str) -> String { + let hashed_input = md5::compute(input.as_bytes()); + return format!("{:x}", hashed_input); +}*/ +pub async fn create_folder_if_not_exists(path: &PathBuf) { + if !path.exists() { + info!("Creating folder {}", path.to_str().unwrap().bright_blue()); + fs::create_dir_all(path).await.unwrap(); + } +} + +pub fn get_installation_directory() -> PathBuf { + return PathBuf::from(data_local_dir().unwrap().to_str().unwrap()).join("Syntax"); +} + +/* Why was this in the main function i will never know */ +pub async fn extract_to_dir(zip_file: &Vec, target_dir: &PathBuf) { + let zip_file_cursor = Cursor::new(zip_file); + zip_extract::extract(zip_file_cursor, target_dir, false).unwrap(); +} + +fn get_location_from_file_name>(file_name: T) -> String { + let file_name = file_name.as_ref(); + for [first, last] in FILES_TO_DOWNLOAD { + if first == file_name { + return last.to_owned(); + } + } + let formated = format!("Is not a valid file {}", file_name); + error!("{}", formated); + panic!("{}", formated) +} + +pub async fn download_and_extract, T2: Into, P: Into>( + file_name: T, + url_prefix: T2, + extract_location: P, +) { + let http_client = ClientBuilder::default().build().unwrap(); + let file_name: String = file_name.into(); + let url_prefix: String = url_prefix.into(); + let extract_location: PathBuf = extract_location.into(); + + let buffer = download_file_prefix(&http_client, format!("{}{file_name}", url_prefix)).await; + drop(http_client); + drop(url_prefix); + let dir = extract_location.join(get_location_from_file_name(&file_name)); + + create_dir_all(&dir).await.unwrap(); + info!("Extracting file {}", file_name); + extract_to_dir(&buffer, &dir).await; + drop(buffer); + + info!("File {} installed to {:?}", file_name, dir.display()); +}