547 lines
20 KiB
Rust
547 lines
20 KiB
Rust
use colored::*;
|
|
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(debug_assertions)]
|
|
const DEBUG: bool = true;
|
|
#[cfg(debug_assertions)]
|
|
const MAX_TRACING_LEVEL: LevelFilter = LevelFilter::DEBUG;
|
|
#[cfg(not(debug_assertions))]
|
|
const DEBUG: bool = false;
|
|
#[cfg(not(debug_assertions))]
|
|
const MAX_TRACING_LEVEL: LevelFilter = LevelFilter::ERROR;
|
|
|
|
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");
|
|
}
|
|
#[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<String> = 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";
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
bootstrapper_filename = "SyntaxPlayerLinuxLauncher";
|
|
}
|
|
let build_date = include_str!(concat!(env!("OUT_DIR"), "/build_date.txt"));
|
|
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;
|
|
if let Some((w, _h)) = term_size::dimensions() {
|
|
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
|
|
} else {
|
|
let startup_text_lines = startup_text.lines().collect::<Vec<&str>>();
|
|
//println!("{}", startup_text.bold().blue().on_black());
|
|
|
|
// print all lines except the last one
|
|
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
|
|
println!(
|
|
"{}\n",
|
|
bootstrapper_info.magenta().cyan().italic().on_black()
|
|
);
|
|
}
|
|
|
|
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!(
|
|
"Latest Client Version: {}",
|
|
latest_client_version_result.bright_blue()
|
|
);
|
|
latest_client_version = latest_client_version_result;
|
|
}
|
|
Err(e) => {
|
|
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!(
|
|
"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!("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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wait for the latest client version to be fetched
|
|
info!(
|
|
"Latest Client Version: {}",
|
|
latest_client_version.cyan().underline()
|
|
);
|
|
debug!("Setup Server: {}", setup_url.cyan().underline());
|
|
|
|
let installation_directory = get_installation_directory();
|
|
debug!(
|
|
"Instillation Directory: {}",
|
|
format!("{:?}", installation_directory.display()).bright_blue()
|
|
);
|
|
create_folder_if_not_exists(&installation_directory).await;
|
|
|
|
let versions_directory = installation_directory.join("Versions");
|
|
debug!(
|
|
"Versions Directory: {}",
|
|
format!("{:?}", versions_directory.display()).bright_blue()
|
|
);
|
|
create_folder_if_not_exists(&versions_directory).await;
|
|
|
|
let temp_downloads_directory = installation_directory.join("Downloads");
|
|
debug!(
|
|
"Temp Downloads Directory: {}",
|
|
format!("{:?}", temp_downloads_directory.display()).bright_blue()
|
|
);
|
|
create_folder_if_not_exists(&temp_downloads_directory).await;
|
|
|
|
let current_version_directory = versions_directory.join(format!("{}", latest_client_version));
|
|
debug!(
|
|
"Current Version Directory: {}",
|
|
format!("{:?}", current_version_directory.display()).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) && !DEBUG {
|
|
// Check if the latest bootstrapper is downloaded
|
|
if !latest_bootstrapper_path.exists() {
|
|
info!("Downloading the latest bootstrapper and restarting");
|
|
// Download the latest bootstrapper
|
|
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")]
|
|
{
|
|
let mut command = std::process::Command::new(latest_bootstrapper_path.clone());
|
|
command.args(&args[1..]);
|
|
match command.spawn() {
|
|
Ok(_) => {}
|
|
Err(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;
|
|
command.spawn().expect("Bootstrapper is still corrupted.");
|
|
std::thread::sleep(std::time::Duration::from_secs(20));
|
|
}
|
|
}
|
|
}
|
|
#[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();
|
|
|
|
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();
|
|
}
|
|
std::process::exit(0);
|
|
}
|
|
|
|
// 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
|
|
// 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.");
|
|
for entry in std::fs::read_dir(¤t_version_directory).unwrap() {
|
|
let entry = entry.unwrap();
|
|
let path = entry.path();
|
|
if path.is_file() {
|
|
if path != current_exe_path {
|
|
std::fs::remove_file(path).unwrap();
|
|
}
|
|
} else {
|
|
std::fs::remove_dir_all(path).unwrap();
|
|
}
|
|
}
|
|
|
|
let version_url_prefix = format!("https://{}/{}-", setup_url, latest_client_version);
|
|
|
|
/* Use a joisnet to run multiple async functions at once */
|
|
|
|
let mut set = JoinSet::new();
|
|
|
|
for [value, _] in FILES_TO_DOWNLOAD {
|
|
set.spawn(download_and_extract(
|
|
value.to_string(),
|
|
version_url_prefix.clone(),
|
|
current_version_directory.clone(),
|
|
));
|
|
}
|
|
|
|
while let Some(value) = set.join_next().await {
|
|
value.unwrap()
|
|
}
|
|
|
|
info!("Binary installed");
|
|
|
|
/* Convert to async due to this being a slow function */
|
|
|
|
// Redacted for lagging vscode
|
|
|
|
// Install the syntax-player scheme in the registry
|
|
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_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 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.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 = format!(
|
|
"[Desktop Entry]
|
|
Name=Syntax Launcher
|
|
Exec={} %u
|
|
Terminal=true
|
|
Type=Application
|
|
MimeType=x-scheme-handler/syntax-player;
|
|
Icon={}
|
|
StartupWMClass=SyntaxLauncher
|
|
Categories=Game;
|
|
Comment=Syntax Launcher
|
|
",
|
|
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]
|
|
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]
|
|
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!(
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
|
<Settings>
|
|
<ContentFolder>content</ContentFolder>
|
|
<BaseUrl>https://{}</BaseUrl>
|
|
</Settings>",
|
|
base_url
|
|
);
|
|
std::fs::write(app_settings_path, app_settings_xml).unwrap();
|
|
|
|
// Check for any other version directories and deletes them
|
|
for entry in std::fs::read_dir(&versions_directory).unwrap() {
|
|
let entry = entry.unwrap();
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
if path != current_version_directory {
|
|
std::fs::remove_dir_all(path).unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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!("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::exit(0);
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
std::process::Command::new("xdg-open")
|
|
.arg("https://www.syntax.eco/games")
|
|
.spawn()
|
|
.unwrap();
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
|
|
let main_args = &args[1];
|
|
let main_args = main_args.replace("syntax-player://", "");
|
|
let main_args = main_args.split("+").collect::<Vec<&str>>();
|
|
|
|
let mut launch_mode = String::new();
|
|
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::<Vec<&str>>().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();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let custom_wine = "wine";
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
// We allow user to specify the wine binary path in installation_directory/winepath.txt
|
|
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!("Using custom wine binary: {}", custom_wine.bright_blue());
|
|
} else {
|
|
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());
|
|
if client_year == "2018" {
|
|
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");
|
|
} else {
|
|
client_executable_path = current_version_directory.join("SyntaxPlayerBeta.exe");
|
|
}
|
|
if !client_executable_path.exists() {
|
|
// Delete AppSettings.xml so the bootstrapper will download the client again
|
|
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.");
|
|
std::thread::sleep(std::time::Duration::from_secs(20));
|
|
std::process::exit(0);
|
|
}
|
|
match launch_mode.as_str() {
|
|
"play" => {
|
|
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.spawn().unwrap();
|
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
|
std::process::exit(0);
|
|
}
|
|
#[cfg(not(target_os = "windows"))]
|
|
{
|
|
// 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(),
|
|
]);
|
|
// 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.");
|
|
std::thread::sleep(std::time::Duration::from_secs(10));
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
}
|