mirror of
https://github.com/alterware/alterware-launcher.git
synced 2025-12-04 15:27:48 +00:00
967 lines
33 KiB
Rust
967 lines
33 KiB
Rust
mod cache;
|
|
mod config;
|
|
mod extend;
|
|
mod github;
|
|
mod global;
|
|
mod http_async;
|
|
mod iw4x;
|
|
mod misc;
|
|
mod self_update;
|
|
mod structs;
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
use extend::*;
|
|
use global::*;
|
|
use structs::*;
|
|
|
|
#[macro_use]
|
|
extern crate simple_log;
|
|
|
|
use colored::Colorize;
|
|
use indicatif::ProgressBar;
|
|
#[cfg(windows)]
|
|
use mslnk::ShellLink;
|
|
use simple_log::LogConfigBuilder;
|
|
use std::{
|
|
borrow::Cow,
|
|
collections::HashMap,
|
|
env, fs,
|
|
path::{Path, PathBuf},
|
|
};
|
|
#[cfg(windows)]
|
|
use steamlocate::SteamDir;
|
|
|
|
#[cfg(windows)]
|
|
fn get_installed_games(games: &Vec<Game>) -> Vec<(u32, PathBuf)> {
|
|
let mut installed_games = Vec::new();
|
|
let steamdir_result = SteamDir::locate();
|
|
|
|
let steamdir = match steamdir_result {
|
|
Ok(steamdir) => steamdir,
|
|
Err(error) => {
|
|
crate::println_error!("Error locating Steam: {error}");
|
|
return installed_games;
|
|
}
|
|
};
|
|
|
|
for game in games {
|
|
if let Ok(Some((app, library))) = steamdir.find_app(game.app_id) {
|
|
let game_path = library
|
|
.path()
|
|
.join("steamapps")
|
|
.join("common")
|
|
.join(&app.install_dir);
|
|
installed_games.push((game.app_id, game_path));
|
|
}
|
|
}
|
|
|
|
installed_games
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn create_shortcut(path: &Path, target: &Path, icon: String, args: String) {
|
|
if let Ok(mut sl) = ShellLink::new(target) {
|
|
sl.set_arguments(Some(args));
|
|
sl.set_icon_location(Some(icon));
|
|
sl.create_lnk(path).unwrap_or_else(|error| {
|
|
crate::println_error!("Error creating shortcut.\n{error}");
|
|
});
|
|
} else {
|
|
crate::println_error!("Error creating shortcut.");
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn setup_client_links(game: &Game, game_dir: &Path) {
|
|
if game.client.len() > 1 {
|
|
println!("Multiple clients installed, use the shortcuts (launch-<client>.lnk in the game directory or on the desktop) to launch a specific client.");
|
|
}
|
|
|
|
for c in game.client.iter() {
|
|
create_shortcut(
|
|
&game_dir.join(format!("launch-{c}.lnk")),
|
|
&game_dir.join("alterware-launcher.exe"),
|
|
game_dir
|
|
.join(format!("{c}.exe"))
|
|
.to_string_lossy()
|
|
.into_owned(),
|
|
c.to_string(),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn setup_desktop_links(path: &Path, game: &Game) {
|
|
println!("Create Desktop shortcut? (Y/n)");
|
|
let input = misc::stdin().to_ascii_lowercase();
|
|
|
|
if input != "n" {
|
|
let desktop = PathBuf::from(&format!("{}\\Desktop", env::var("USERPROFILE").unwrap()));
|
|
|
|
for c in game.client.iter() {
|
|
create_shortcut(
|
|
&desktop.join(format!("{c}.lnk")),
|
|
&path.join("alterware-launcher.exe"),
|
|
path.join(format!("{c}.exe")).to_string_lossy().into_owned(),
|
|
c.to_string(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
async fn auto_install(path: &Path, game: &Game<'_>) {
|
|
setup_client_links(game, path);
|
|
setup_desktop_links(path, game);
|
|
update(game, path, false, false, None, None).await;
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
async fn windows_launcher_install(games: &Vec<Game<'_>>) {
|
|
crate::println_info!(
|
|
"{}",
|
|
"No game specified/found. Checking for installed Steam games..".yellow()
|
|
);
|
|
let installed_games = get_installed_games(games);
|
|
|
|
if !installed_games.is_empty() {
|
|
let current_dir = env::current_dir().unwrap();
|
|
for (id, path) in installed_games.iter() {
|
|
if current_dir.starts_with(path) {
|
|
crate::println_info!("Found game in current directory.");
|
|
crate::println_info!("Installing AlterWare client for {}.", id);
|
|
let game = games.iter().find(|&g| g.app_id == *id).unwrap();
|
|
auto_install(path, game).await;
|
|
crate::println_info!("Installation complete. Please run the launcher again or use a shortcut to launch the game.");
|
|
std::io::stdin().read_line(&mut String::new()).unwrap();
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
|
|
println!("Installed games:");
|
|
|
|
for (id, path) in installed_games.iter() {
|
|
println!("{id}: {}", path.display());
|
|
}
|
|
|
|
println!("Enter the ID of the game you want to install the AlterWare client for:");
|
|
let input: u32 = misc::stdin().parse().unwrap();
|
|
|
|
for (id, path) in installed_games.iter() {
|
|
if *id == input {
|
|
let game = games.iter().find(|&g| g.app_id == input).unwrap();
|
|
|
|
let launcher_path = env::current_exe().unwrap();
|
|
let target_path = path.join("alterware-launcher.exe");
|
|
|
|
if launcher_path != target_path {
|
|
fs::copy(launcher_path, &target_path).unwrap();
|
|
crate::println_info!("Launcher copied to {}", path.display());
|
|
}
|
|
auto_install(path, game).await;
|
|
crate::println_info!("Installation complete.");
|
|
crate::println_info!("Please use one of the shortcuts (on your Desktop or in the game folder) to play.");
|
|
crate::println_info!(
|
|
"Alternatively run the launcher again from the game folder {}",
|
|
target_path.display()
|
|
);
|
|
std::io::stdin().read_line(&mut String::new()).unwrap();
|
|
break;
|
|
}
|
|
}
|
|
std::process::exit(0);
|
|
} else {
|
|
println!(
|
|
"No installed games found. Make sure to place the launcher in the game directory."
|
|
);
|
|
std::io::stdin().read_line(&mut String::new()).unwrap();
|
|
std::process::exit(0);
|
|
}
|
|
}
|
|
|
|
fn total_download_size(cdn_info: &Vec<CdnFile>, remote_dir: &str) -> u64 {
|
|
let remote_dir = format!("{remote_dir}/");
|
|
let mut size: u64 = 0;
|
|
for file in cdn_info {
|
|
if !file.name.starts_with(&remote_dir) || file.name == "iw4/iw4x.dll" {
|
|
continue;
|
|
}
|
|
size += file.size as u64;
|
|
}
|
|
size
|
|
}
|
|
|
|
async fn update_dir(
|
|
cdn_info: &Vec<CdnFile>,
|
|
remote_dir: &str,
|
|
dir: &Path,
|
|
hashes: &mut HashMap<String, String>,
|
|
pb: &ProgressBar,
|
|
skip_iw4x_sp: bool,
|
|
) {
|
|
misc::pb_style_download(pb, false);
|
|
|
|
let remote_dir_pre = format!("{remote_dir}/");
|
|
|
|
let mut files_to_download: Vec<CdnFile> = vec![];
|
|
|
|
for file in cdn_info {
|
|
if !file.name.starts_with(&remote_dir_pre) || file.name == "iw4/iw4x.dll" {
|
|
continue;
|
|
}
|
|
if skip_iw4x_sp && file.name == "iw4/iw4x-sp.exe" {
|
|
continue;
|
|
}
|
|
|
|
let hash_remote = file.blake3.to_lowercase();
|
|
let file_name = &file.name.replace(remote_dir_pre.as_str(), "");
|
|
let file_path = dir.join(file_name);
|
|
if file_path.exists() {
|
|
let hash_local = hashes
|
|
.get(file_name)
|
|
.map(Cow::Borrowed)
|
|
.unwrap_or_else(|| Cow::Owned(file_path.get_blake3().unwrap()))
|
|
.to_string();
|
|
|
|
if hash_local != hash_remote {
|
|
files_to_download.push(file.clone());
|
|
} else {
|
|
let msg = format!("{}{}", misc::prefix("checked"), file_path.cute_path());
|
|
pb.println(&msg);
|
|
info!("{msg}");
|
|
hashes.insert(file_name.to_owned(), file.blake3.to_lowercase());
|
|
}
|
|
} else {
|
|
files_to_download.push(file.clone());
|
|
}
|
|
}
|
|
|
|
if files_to_download.is_empty() {
|
|
let msg = format!(
|
|
"{}No files to download for {}",
|
|
misc::prefix("info"),
|
|
remote_dir
|
|
);
|
|
pb.println(&msg);
|
|
info!("{msg}");
|
|
return;
|
|
}
|
|
let msg = format!(
|
|
"{}Downloading outdated or missing files for {remote_dir}, {}",
|
|
misc::prefix("info"),
|
|
misc::human_readable_bytes(total_download_size(&files_to_download, remote_dir))
|
|
);
|
|
pb.println(&msg);
|
|
info!("{msg}");
|
|
|
|
misc::pb_style_download(pb, true);
|
|
let client = reqwest::Client::new();
|
|
for file in files_to_download {
|
|
let file_name = &file.name.replace(&remote_dir_pre, "");
|
|
let file_path = dir.join(file_name);
|
|
if let Some(parent) = file_path.parent() {
|
|
if !parent.exists() {
|
|
fs::create_dir_all(parent).unwrap();
|
|
}
|
|
}
|
|
|
|
// Prompt user to retry downloads if they fail
|
|
let mut download_complete = false;
|
|
let mut bust_cache = false;
|
|
let mut local_hash = String::default();
|
|
while !download_complete {
|
|
let url = format!("{}/{}", MASTER.lock().unwrap(), file.name);
|
|
let url = if bust_cache {
|
|
bust_cache = false;
|
|
format!("{}?{}", url, misc::random_string(6))
|
|
} else {
|
|
url
|
|
};
|
|
|
|
if let Err(err) =
|
|
http_async::download_file_progress(&client, pb, &url, &file_path, file.size as u64)
|
|
.await
|
|
{
|
|
let file_name = file_path.clone().cute_path();
|
|
println_error!("{err}");
|
|
println!("Failed to download file {file_name}, retry? (Y/n)");
|
|
let input = misc::stdin().to_ascii_lowercase();
|
|
if input == "n" {
|
|
error!("Download for file {file_name} failed with {err}");
|
|
panic!("{err}");
|
|
} else {
|
|
warn!(
|
|
"Download for file {file_name} failed with {err} user chose to retry download"
|
|
);
|
|
}
|
|
};
|
|
|
|
local_hash = file_path.get_blake3().unwrap().to_lowercase();
|
|
let remote = file.blake3.to_lowercase();
|
|
if local_hash != remote {
|
|
println_error!("Downloaded file hash does not match remote!\nRemote {remote}, local {local_hash}, {}\nIf this issue persists please try again in 15 minutes.", file_path.cute_path());
|
|
println!("Retry download? (Y/n)");
|
|
let input = misc::stdin().to_ascii_lowercase();
|
|
if input != "n" {
|
|
println_info!(
|
|
"Retrying download for {} due to hash mismatch",
|
|
file_path.cute_path()
|
|
);
|
|
bust_cache = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
download_complete = true;
|
|
}
|
|
|
|
hashes.insert(file_name.to_owned(), local_hash);
|
|
|
|
#[cfg(unix)]
|
|
if file_name.ends_with(".exe") {
|
|
let perms = std::os::unix::fs::PermissionsExt::from_mode(0o755);
|
|
fs::set_permissions(&file_path, perms).unwrap_or_else(|error| {
|
|
crate::println_error!("Error setting permissions for {file_name}: {error}");
|
|
})
|
|
}
|
|
}
|
|
misc::pb_style_download(pb, false);
|
|
}
|
|
|
|
async fn update(
|
|
game: &Game<'_>,
|
|
dir: &Path,
|
|
bonus_content: bool,
|
|
force: bool,
|
|
skip_iw4x_sp: Option<bool>,
|
|
ignore_required_files: Option<bool>,
|
|
) {
|
|
info!("Starting update for game engine: {}", game.engine);
|
|
info!("Update path: {}", dir.display());
|
|
debug!("Bonus content: {}, Force update: {}", bonus_content, force);
|
|
|
|
let skip_iw4x_sp = skip_iw4x_sp.unwrap_or(false);
|
|
let ignore_required_files = ignore_required_files.unwrap_or(false);
|
|
|
|
let res =
|
|
http_async::get_body_string(format!("{}/files.json", MASTER.lock().unwrap()).as_str())
|
|
.await
|
|
.unwrap();
|
|
debug!("Retrieved files.json from server");
|
|
let cdn_info: Vec<CdnFile> = serde_json::from_str(&res).unwrap();
|
|
|
|
if !ignore_required_files && !game.required_files_exist(dir) {
|
|
error!("Critical game files missing. Required files check failed.");
|
|
println!(
|
|
"{}\nVerify game file integrity on Steam or reinstall the game.",
|
|
"Critical game files missing.".bright_red()
|
|
);
|
|
std::io::stdin().read_line(&mut String::new()).unwrap();
|
|
std::process::exit(0);
|
|
}
|
|
|
|
let old_files = [".sha-sums", ".iw4xrevision"];
|
|
for f in old_files {
|
|
if dir.join(f).exists() {
|
|
match fs::remove_file(dir.join(f)) {
|
|
Ok(_) => {}
|
|
Err(error) => {
|
|
crate::println_error!("Error removing {f}: {error}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut cache = if force {
|
|
structs::Cache::default()
|
|
} else {
|
|
cache::get_cache(dir)
|
|
};
|
|
|
|
if game.engine == "iw4" {
|
|
iw4x::update(dir, &mut cache).await;
|
|
|
|
let iw4x_dirs = vec!["iw4x", "zone/patch"];
|
|
for d in &iw4x_dirs {
|
|
if let Ok(dir_iter) = dir.join(d).read_dir() {
|
|
'outer: for file in dir_iter.filter_map(|entry| entry.ok()) {
|
|
let file_path = file.path();
|
|
|
|
if file_path.is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let file_path_rel = match file_path.strip_prefix(dir) {
|
|
Ok(rel) => rel.to_path_buf(),
|
|
Err(_) => continue,
|
|
};
|
|
|
|
if iw4x_dirs
|
|
.iter()
|
|
.any(|prefix| file_path_rel.starts_with(Path::new(prefix)))
|
|
{
|
|
if !cdn_info
|
|
.iter()
|
|
.any(|cdn_file| cdn_file.name.starts_with("iw4"))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let should_continue = cdn_info.iter().any(|cdn_file| {
|
|
let path_rem = Path::new(&cdn_file.name)
|
|
.strip_prefix(Path::new("iw4"))
|
|
.unwrap_or_else(|_| Path::new(&cdn_file.name));
|
|
path_rem == file_path_rel
|
|
});
|
|
|
|
if should_continue {
|
|
continue 'outer;
|
|
}
|
|
|
|
crate::println_info!(
|
|
"{}{}",
|
|
misc::prefix("removed"),
|
|
file_path.cute_path()
|
|
);
|
|
|
|
if fs::remove_file(&file_path).is_err() {
|
|
crate::println_error!(
|
|
"{}Couldn't delete {}",
|
|
misc::prefix("error"),
|
|
file_path.cute_path()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let pb = ProgressBar::new(0);
|
|
update_dir(
|
|
&cdn_info,
|
|
game.engine,
|
|
dir,
|
|
&mut cache.hashes,
|
|
&pb,
|
|
skip_iw4x_sp,
|
|
)
|
|
.await;
|
|
|
|
if bonus_content && !game.bonus.is_empty() {
|
|
for bonus in game.bonus.iter() {
|
|
update_dir(&cdn_info, bonus, dir, &mut cache.hashes, &pb, skip_iw4x_sp).await;
|
|
}
|
|
}
|
|
|
|
pb.finish();
|
|
|
|
for f in game.delete.iter() {
|
|
let file_path = dir.join(f);
|
|
if file_path.is_file() {
|
|
if fs::remove_file(&file_path).is_err() {
|
|
println!(
|
|
"{}Couldn't delete {}",
|
|
misc::prefix("error"),
|
|
file_path.cute_path()
|
|
);
|
|
} else {
|
|
println!("{}{}", misc::prefix("removed"), file_path.cute_path());
|
|
}
|
|
} else if file_path.is_dir() {
|
|
if fs::remove_dir_all(&file_path).is_err() {
|
|
println!(
|
|
"{}Couldn't delete {}",
|
|
misc::prefix("error"),
|
|
file_path.cute_path()
|
|
);
|
|
} else {
|
|
println!("{}{}", misc::prefix("removed"), file_path.cute_path());
|
|
}
|
|
}
|
|
}
|
|
|
|
cache::save_cache(dir, cache);
|
|
|
|
// Store game data for offline mode
|
|
let mut stored_data = cache::get_stored_data().unwrap_or_default();
|
|
stored_data.game_path = dir.to_string_lossy().into_owned();
|
|
|
|
// Store available clients for this engine
|
|
stored_data.clients.insert(
|
|
game.engine.to_string(),
|
|
game.client.iter().map(|s| s.to_string()).collect(),
|
|
);
|
|
|
|
if let Err(e) = cache::store_game_data(&stored_data) {
|
|
println!(
|
|
"{} Failed to store game data: {}",
|
|
PREFIXES.get("error").unwrap().formatted(),
|
|
e
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn launch(file_path: &PathBuf, args: &str) {
|
|
info!(
|
|
"Launching game on Windows: {} {}",
|
|
file_path.display(),
|
|
args
|
|
);
|
|
println!("\n\nJoin the AlterWare Discord server:\nhttps://discord.gg/2ETE8engZM\n\n");
|
|
crate::println_info!("Launching {} {args}", file_path.display());
|
|
let exit_status = std::process::Command::new(file_path)
|
|
.args(args.trim().split(' '))
|
|
.current_dir(file_path.parent().unwrap())
|
|
.spawn()
|
|
.expect("Failed to launch the game")
|
|
.wait()
|
|
.expect("Failed to wait for the game process to finish");
|
|
|
|
if exit_status.success() {
|
|
info!("Game exited successfully with status: {}", exit_status);
|
|
} else {
|
|
error!("Game exited with error status: {}", exit_status);
|
|
}
|
|
|
|
crate::println_error!("Game exited with {exit_status}");
|
|
if !exit_status.success() {
|
|
misc::stdin();
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn launch(file_path: &PathBuf, args: &str) {
|
|
println!("\n\nJoin the AlterWare Discord server:\nhttps://discord.gg/2ETE8engZM\n\n");
|
|
crate::println_info!("Launching {} {args}", file_path.display());
|
|
let exit_status = if misc::is_program_in_path("wine") {
|
|
println!("Found wine, launching game using wine.\nIf you run into issues or want to launch a different way, run {} manually.", file_path.display());
|
|
std::process::Command::new("wine")
|
|
.args([file_path.to_str().unwrap(), args.trim()])
|
|
.current_dir(file_path.parent().unwrap())
|
|
.spawn()
|
|
.expect("Failed to launch the game")
|
|
.wait()
|
|
.expect("Failed to wait for the game process to finish")
|
|
} else {
|
|
std::process::Command::new(file_path)
|
|
.args(args.trim().split(' '))
|
|
.current_dir(file_path.parent().unwrap())
|
|
.spawn()
|
|
.expect("Failed to launch the game")
|
|
.wait()
|
|
.expect("Failed to wait for the game process to finish")
|
|
};
|
|
|
|
crate::println_error!("Game exited with {exit_status}");
|
|
if !exit_status.success() {
|
|
misc::stdin();
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn setup_env() {
|
|
colored::control::set_virtual_terminal(true).unwrap_or_else(|error| {
|
|
crate::println_error!("{:#?}", error);
|
|
colored::control::SHOULD_COLORIZE.set_override(false);
|
|
});
|
|
|
|
if let Ok(system_root) = env::var("SystemRoot") {
|
|
if let Ok(current_dir) = env::current_dir() {
|
|
if current_dir.starts_with(system_root) {
|
|
if let Ok(current_exe) = env::current_exe() {
|
|
if let Some(parent) = current_exe.parent() {
|
|
if let Err(error) = env::set_current_dir(parent) {
|
|
crate::println_error!("{:#?}", error);
|
|
} else {
|
|
crate::println_info!("Running from the system directory. Changed working directory to the executable location.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn arg_value(args: &[String], arg: &str) -> Option<String> {
|
|
if let Some(e) = args.iter().position(|r| r == arg) {
|
|
if e + 1 < args.len() {
|
|
return Some(args[e + 1].clone());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn arg_bool(args: &[String], arg: &str) -> bool {
|
|
args.iter().any(|r| r == arg)
|
|
}
|
|
|
|
fn arg_remove(args: &mut Vec<String>, arg: &str) {
|
|
args.iter().position(|r| r == arg).map(|e| args.remove(e));
|
|
}
|
|
|
|
fn arg_remove_value(args: &mut Vec<String>, arg: &str) {
|
|
if let Some(e) = args.iter().position(|r| r == arg) {
|
|
args.remove(e);
|
|
args.remove(e);
|
|
};
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
#[cfg(windows)]
|
|
let log_file = env::current_exe()
|
|
.unwrap_or(PathBuf::from("alterware-launcher"))
|
|
.with_extension("log");
|
|
#[cfg(unix)]
|
|
let log_file = PathBuf::from("/var/log/alterware-launcher.log");
|
|
|
|
if log_file.exists() && fs::remove_file(&log_file).is_err() {
|
|
println!("Couldn't clear log file, make sure target directory is writable.");
|
|
}
|
|
let logger_config = LogConfigBuilder::builder()
|
|
.path(log_file.to_str().unwrap())
|
|
.time_format("%Y-%m-%d %H:%M:%S.%f")
|
|
.level("debug")
|
|
.unwrap()
|
|
.output_file()
|
|
.build();
|
|
let _ = simple_log::new(logger_config);
|
|
|
|
#[cfg(windows)]
|
|
setup_env();
|
|
|
|
let mut args: Vec<String> = env::args().collect();
|
|
|
|
if arg_bool(&args, "--help") {
|
|
println!("CLI Args:");
|
|
println!(" <client>: Specify the client to launch");
|
|
println!(" --help: Display this help message");
|
|
println!(" --version: Display the launcher version");
|
|
println!(" --path/-p <path>: Specify the game directory");
|
|
println!(" --update/-u: Update only, don't launch the game");
|
|
println!(" --bonus: Download bonus content");
|
|
println!(" --skip-bonus: Don't download bonus content");
|
|
println!(" --force/-f: Force file hash recheck");
|
|
println!(" --pass <args>: Pass arguments to the game");
|
|
println!(" --skip-launcher-update: Skip launcher self-update");
|
|
println!(" --ignore-required-files: Skip required files check");
|
|
println!(" --skip-redist: Skip redistributable installation");
|
|
println!(" --redist: (Re-)Install redistributables");
|
|
println!(
|
|
"\nExample:\n alterware-launcher.exe iw4x --bonus --pass \"-console -nointro\""
|
|
);
|
|
return;
|
|
}
|
|
|
|
if arg_bool(&args, "--version") || arg_bool(&args, "-v") {
|
|
println!(
|
|
"{} v{}",
|
|
"AlterWare Launcher".bright_green(),
|
|
env!("CARGO_PKG_VERSION")
|
|
);
|
|
println!("https://github.com/{GH_OWNER}/{GH_REPO}");
|
|
println!(
|
|
"\n{}{}{}{}{}{}{}",
|
|
"For ".on_black(),
|
|
"Alter".bright_blue().on_black().underline(),
|
|
"Ware".white().on_black().underline(),
|
|
".dev".on_black().underline(),
|
|
" by ".on_black(),
|
|
"mxve".bright_magenta().on_black().underline(),
|
|
".de".on_black().underline()
|
|
);
|
|
return;
|
|
}
|
|
|
|
let offline_mode = !global::check_connectivity().await;
|
|
if offline_mode {
|
|
// Check if this is a first-time run (no stored data)
|
|
let stored_data = cache::get_stored_data();
|
|
if stored_data.is_none() {
|
|
println!(
|
|
"{} Internet connection is required for first-time installation.",
|
|
PREFIXES.get("error").unwrap().formatted()
|
|
);
|
|
error!("Internet connection required for first-time installation");
|
|
println!("Please connect to the internet and try again.");
|
|
println!("Press enter to exit...");
|
|
misc::stdin();
|
|
std::process::exit(1);
|
|
}
|
|
|
|
println!(
|
|
"{} No internet connection or MASTER server is unreachable. Running in offline mode...",
|
|
PREFIXES.get("error").unwrap().formatted()
|
|
);
|
|
warn!("No internet connection or MASTER server is unreachable. Running in offline mode...");
|
|
|
|
// Handle path the same way as online mode
|
|
let install_path: PathBuf;
|
|
if let Some(path) = arg_value(&args, "--path") {
|
|
install_path = PathBuf::from(path);
|
|
arg_remove_value(&mut args, "--path");
|
|
} else if let Some(path) = arg_value(&args, "-p") {
|
|
install_path = PathBuf::from(path);
|
|
arg_remove_value(&mut args, "-p");
|
|
} else {
|
|
install_path = env::current_dir().unwrap();
|
|
}
|
|
|
|
let cfg = config::load(install_path.join("alterware-launcher.json"));
|
|
|
|
// Try to get stored game data
|
|
let stored_data = cache::get_stored_data();
|
|
if let Some(ref data) = stored_data {
|
|
info!("Found stored game data for path: {}", data.game_path);
|
|
} else {
|
|
warn!("No stored game data found");
|
|
}
|
|
|
|
// Get client from args, config, or prompt user
|
|
let client = if args.len() > 1 {
|
|
args[1].clone()
|
|
} else if let Some(engine) = stored_data
|
|
.as_ref()
|
|
.and_then(|d| d.clients.get(&cfg.engine))
|
|
{
|
|
if engine.len() > 1 {
|
|
println!("Multiple clients available, select one to launch:");
|
|
for (i, c) in engine.iter().enumerate() {
|
|
println!("{i}: {c}");
|
|
}
|
|
info!("Multiple clients available, prompting user for selection");
|
|
engine[misc::stdin().parse::<usize>().unwrap()].clone()
|
|
} else if !engine.is_empty() {
|
|
info!("Using single available client: {}", engine[0]);
|
|
engine[0].clone()
|
|
} else {
|
|
println!(
|
|
"{} No client specified and no stored clients available.",
|
|
PREFIXES.get("error").unwrap().formatted()
|
|
);
|
|
error!("No client specified and no stored clients available");
|
|
std::process::exit(1);
|
|
}
|
|
} else {
|
|
println!(
|
|
"{} No client specified and no stored data available.",
|
|
PREFIXES.get("error").unwrap().formatted()
|
|
);
|
|
error!("No client specified and no stored data available");
|
|
std::process::exit(1);
|
|
};
|
|
|
|
info!("Launching game in offline mode with client: {}", client);
|
|
// Launch game without updates
|
|
launch(&install_path.join(format!("{}.exe", client)), &cfg.args);
|
|
return;
|
|
}
|
|
|
|
let install_path: PathBuf;
|
|
if let Some(path) = arg_value(&args, "--path") {
|
|
install_path = PathBuf::from(path);
|
|
arg_remove_value(&mut args, "--path");
|
|
} else if let Some(path) = arg_value(&args, "-p") {
|
|
install_path = PathBuf::from(path);
|
|
arg_remove_value(&mut args, "-p");
|
|
} else {
|
|
install_path = env::current_dir().unwrap();
|
|
}
|
|
|
|
let mut cfg = config::load(install_path.join("alterware-launcher.json"));
|
|
|
|
if !cfg.use_https {
|
|
let mut master_url = MASTER.lock().unwrap();
|
|
*master_url = master_url.replace("https://", "http://");
|
|
};
|
|
|
|
if !arg_bool(&args, "--skip-launcher-update") && !cfg.skip_self_update {
|
|
self_update::run(cfg.update_only).await;
|
|
} else {
|
|
arg_remove(&mut args, "--skip-launcher-update");
|
|
}
|
|
|
|
if arg_bool(&args, "--update") || arg_bool(&args, "-u") {
|
|
cfg.update_only = true;
|
|
arg_remove(&mut args, "--update");
|
|
arg_remove(&mut args, "-u");
|
|
}
|
|
|
|
if arg_bool(&args, "--bonus") {
|
|
cfg.download_bonus_content = true;
|
|
cfg.ask_bonus_content = false;
|
|
arg_remove(&mut args, "--bonus");
|
|
} else if arg_bool(&args, "--skip-bonus") {
|
|
cfg.download_bonus_content = false;
|
|
cfg.ask_bonus_content = false;
|
|
arg_remove(&mut args, "--skip-bonus")
|
|
}
|
|
|
|
if arg_bool(&args, "--force") || arg_bool(&args, "-f") {
|
|
cfg.force_update = true;
|
|
arg_remove(&mut args, "--force");
|
|
arg_remove(&mut args, "-f");
|
|
}
|
|
|
|
let ignore_required_files = arg_bool(&args, "--ignore-required-files");
|
|
if ignore_required_files {
|
|
arg_remove(&mut args, "--ignore-required-files");
|
|
}
|
|
|
|
if let Some(pass) = arg_value(&args, "--pass") {
|
|
cfg.args = pass;
|
|
arg_remove_value(&mut args, "--pass");
|
|
} else if cfg.args.is_empty() {
|
|
cfg.args = String::default();
|
|
}
|
|
|
|
if arg_bool(&args, "--skip-redist") {
|
|
cfg.skip_redist = true;
|
|
arg_remove(&mut args, "--skip-redist");
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
if arg_bool(&args, "--redist") {
|
|
arg_remove(&mut args, "--redist");
|
|
misc::install_dependencies(&install_path).await;
|
|
println_info!("Redistributables installation finished. Press enter to exit...");
|
|
misc::stdin();
|
|
std::process::exit(0);
|
|
}
|
|
|
|
let games_json =
|
|
http_async::get_body_string(format!("{}/games.json", MASTER.lock().unwrap()).as_str())
|
|
.await
|
|
.unwrap_or_else(|error| {
|
|
crate::println_error!("Failed to get games.json: {:#?}", error);
|
|
misc::stdin();
|
|
std::process::exit(1);
|
|
});
|
|
let games: Vec<Game> = serde_json::from_str(&games_json).unwrap_or_else(|error| {
|
|
crate::println_error!("Error parsing games.json: {:#?}", error);
|
|
misc::stdin();
|
|
std::process::exit(1);
|
|
});
|
|
|
|
let mut game: String = String::new();
|
|
if args.len() > 1 {
|
|
game = String::from(&args[1]);
|
|
} else {
|
|
'main: for g in games.iter() {
|
|
for r in g.references.iter() {
|
|
if install_path.join(r).exists() {
|
|
if g.client.len() > 1 {
|
|
if cfg.update_only {
|
|
game = String::from(g.client[0]);
|
|
break 'main;
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
setup_client_links(g, &env::current_dir().unwrap());
|
|
|
|
#[cfg(not(windows))]
|
|
println!("Multiple clients installed, set the client as the first argument to launch a specific client.");
|
|
println!("Select a client to launch:");
|
|
for (i, c) in g.client.iter().enumerate() {
|
|
println!("{i}: {c}");
|
|
}
|
|
game = String::from(g.client[misc::stdin().parse::<usize>().unwrap()]);
|
|
break 'main;
|
|
}
|
|
game = String::from(g.client[0]);
|
|
break 'main;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for g in games.iter() {
|
|
for c in g.client.iter() {
|
|
if c == &game {
|
|
if cfg.engine.is_empty() {
|
|
cfg.engine = String::from(g.engine);
|
|
config::save_value_s(
|
|
install_path.join("alterware-launcher.json"),
|
|
"engine",
|
|
cfg.engine.clone(),
|
|
);
|
|
if cfg.engine == "iw4" && cfg.args.is_empty() {
|
|
cfg.args = String::from("-stdout");
|
|
config::save_value_s(
|
|
install_path.join("alterware-launcher.json"),
|
|
"args",
|
|
cfg.args.clone(),
|
|
);
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
if !cfg.skip_redist {
|
|
misc::install_dependencies(&install_path).await;
|
|
}
|
|
}
|
|
|
|
if cfg.ask_bonus_content && !g.bonus.is_empty() {
|
|
println!("Download bonus content? (Y/n)");
|
|
let input = misc::stdin().to_ascii_lowercase();
|
|
cfg.download_bonus_content = input != "n";
|
|
config::save_value(
|
|
install_path.join("alterware-launcher.json"),
|
|
"download_bonus_content",
|
|
cfg.download_bonus_content,
|
|
);
|
|
config::save_value(
|
|
install_path.join("alterware-launcher.json"),
|
|
"ask_bonus_content",
|
|
false,
|
|
);
|
|
}
|
|
|
|
update(
|
|
g,
|
|
install_path.as_path(),
|
|
cfg.download_bonus_content,
|
|
cfg.force_update,
|
|
Some(&game != "iw4x-sp"),
|
|
Some(ignore_required_files),
|
|
)
|
|
.await;
|
|
if !cfg.update_only {
|
|
launch(&install_path.join(format!("{c}.exe")), &cfg.args);
|
|
}
|
|
|
|
// Store game data for offline mode
|
|
let mut stored_data = cache::get_stored_data().unwrap_or_default();
|
|
stored_data.game_path = install_path.to_string_lossy().into_owned();
|
|
|
|
// Store available clients for this engine
|
|
stored_data.clients.insert(
|
|
g.engine.to_string(),
|
|
g.client.iter().map(|s| s.to_string()).collect(),
|
|
);
|
|
|
|
if let Err(e) = cache::store_game_data(&stored_data) {
|
|
println!(
|
|
"{} Failed to store game data: {}",
|
|
PREFIXES.get("error").unwrap().formatted(),
|
|
e
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
windows_launcher_install(&games).await;
|
|
|
|
crate::println_error!("Game not found!");
|
|
println!("Place the launcher in the game folder, if that doesn't work specify the client on the command line (ex. alterware-launcher.exe iw4-sp)");
|
|
println!("Press enter to exit...");
|
|
std::io::stdin().read_line(&mut String::new()).unwrap();
|
|
}
|