Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
601abd008f | |||
322fd2f98a | |||
e061bca7a3 | |||
87e86cc954 | |||
d5d847df75 | |||
d620bc9838 | |||
76a7d8b2c6 | |||
6c7fbd1ff1 | |||
18651a68ca | |||
79559fe46a | |||
8fd66d16af | |||
4d3c6b9dab | |||
7ca7615222 | |||
2195f42abc | |||
f7635d4089 | |||
9598ec3dfe | |||
37266207e7 | |||
e041df80c5 | |||
b157bcb2c2 | |||
beae0adce5 | |||
f9ec044a15 | |||
0be3adf8d1 | |||
59f347462d | |||
78e4e18176 | |||
e0f4a5102e | |||
c80765d091 | |||
3b77755848 | |||
4c1114f3e0 | |||
b69611e66b | |||
6ec4deed32 |
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@ -11,8 +11,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: taiki-e/create-gh-release-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
draft: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload-assets:
|
||||
strategy:
|
||||
|
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -13,7 +13,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alterware-launcher"
|
||||
version = "0.3.0"
|
||||
version = "0.4.5"
|
||||
dependencies = [
|
||||
"http_req",
|
||||
"mslnk",
|
||||
@ -538,18 +538,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.183"
|
||||
version = "1.0.185"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c"
|
||||
checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.183"
|
||||
version = "1.0.185"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816"
|
||||
checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -558,9 +558,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.104"
|
||||
version = "1.0.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c"
|
||||
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
|
10
Cargo.toml
10
Cargo.toml
@ -1,11 +1,15 @@
|
||||
[package]
|
||||
name = "alterware-launcher"
|
||||
version = "0.3.0"
|
||||
version = "0.4.5"
|
||||
edition = "2021"
|
||||
build = "res/build.rs"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
|
||||
# Symbols are a nice thing
|
||||
debug = true
|
||||
|
||||
panic = "abort"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@ -15,8 +19,8 @@ http_req = { version = "0.9.2", default-features = false, features = [
|
||||
"rust-tls",
|
||||
] }
|
||||
sha1_smol = "1.0.0"
|
||||
serde = { version = "1.0.183", features = ["derive"] }
|
||||
serde_json = "1.0.104"
|
||||
serde = { version = "1.0.185", features = ["derive"] }
|
||||
serde_json = "1.0.105"
|
||||
rand = "0.8.5"
|
||||
semver = "1.0.18"
|
||||
|
||||
|
@ -8,3 +8,7 @@
|
||||
|
||||
- Passing ```iw4-sp```, ```iw5-mod```, ```iw6-mod``` or ```s1-mod``` as the first argument will skip automatic game detection
|
||||
- Passing ```update``` will stop the launcher from launching the game
|
||||
- ```skip-launcher-update``` skips self-update
|
||||
|
||||
### Note for server owners:
|
||||
When the launcher updates itself it needs to be restarted. It will return exit code 201 in this case.
|
184
src/main.rs
184
src/main.rs
@ -2,8 +2,7 @@ mod http;
|
||||
#[cfg(windows)]
|
||||
use mslnk::ShellLink;
|
||||
use semver::Version;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{fs, path::PathBuf};
|
||||
use std::{fs, path::Path, path::PathBuf};
|
||||
#[cfg(not(windows))]
|
||||
use std::{thread, time};
|
||||
#[cfg(windows)]
|
||||
@ -19,7 +18,7 @@ struct CdnFile {
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
struct Game<'a> {
|
||||
engine: &'a str,
|
||||
client: &'a str,
|
||||
client: Vec<&'a str>,
|
||||
references: Vec<&'a str>,
|
||||
app_id: u32,
|
||||
}
|
||||
@ -27,20 +26,12 @@ struct Game<'a> {
|
||||
const MASTER: &str = "https://master.alterware.dev";
|
||||
const REPO: &str = "mxve/alterware-launcher";
|
||||
|
||||
fn get_cache_buster() -> u64 {
|
||||
match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_sha1(path: &PathBuf) -> String {
|
||||
let mut sha1 = sha1_smol::Sha1::new();
|
||||
sha1.update(&fs::read(path).unwrap());
|
||||
sha1.digest().to_string()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn get_input() -> String {
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
@ -62,7 +53,7 @@ fn self_update_available() -> bool {
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn self_update() {
|
||||
fn self_update(_update_only: bool) {
|
||||
if self_update_available() {
|
||||
println!("A new version of the AlterWare launcher is available.");
|
||||
println!("Download it at https://github.com/{}/releases/latest", REPO);
|
||||
@ -72,7 +63,7 @@ fn self_update() {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn self_update() {
|
||||
fn self_update(update_only: bool) {
|
||||
let working_dir = std::env::current_dir().unwrap();
|
||||
let files = fs::read_dir(&working_dir).unwrap();
|
||||
|
||||
@ -115,15 +106,23 @@ fn self_update() {
|
||||
self_replace::self_replace("alterware-launcher-update.exe").unwrap();
|
||||
fs::remove_file(&file_path).unwrap();
|
||||
println!("Launcher updated. Please run it again.");
|
||||
if !update_only {
|
||||
std::io::stdin().read_line(&mut String::new()).unwrap();
|
||||
std::process::exit(0);
|
||||
}
|
||||
std::process::exit(201);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn get_installed_games(games: &Vec<Game>) -> Vec<(u32, PathBuf)> {
|
||||
let mut installed_games = Vec::new();
|
||||
let mut steamdir = SteamDir::locate().unwrap();
|
||||
let mut steamdir = match SteamDir::locate() {
|
||||
Some(steamdir) => steamdir,
|
||||
None => {
|
||||
println!("Steam not found.");
|
||||
return installed_games;
|
||||
}
|
||||
};
|
||||
|
||||
for game in games {
|
||||
if let Some(app) = steamdir.app(&game.app_id) {
|
||||
@ -134,28 +133,75 @@ fn get_installed_games(games: &Vec<Game>) -> Vec<(u32, PathBuf)> {
|
||||
installed_games
|
||||
}
|
||||
|
||||
#[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 desktop shortcuts) to launch a specific client.");
|
||||
}
|
||||
|
||||
let target = game_dir.join("alterware-launcher.exe");
|
||||
|
||||
for c in game.client.iter() {
|
||||
let lnk = game_dir.join(format!("launch-{}.lnk", c));
|
||||
|
||||
let mut sl = ShellLink::new(target.clone()).unwrap();
|
||||
sl.set_arguments(Some(c.to_string()));
|
||||
sl.set_icon_location(Some(
|
||||
game_dir
|
||||
.join(format!("{}.exe", c))
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
));
|
||||
sl.create_lnk(&lnk).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_launcher_install(games: &Vec<Game>) {
|
||||
println!("No game specified/found. Checking for installed Steam games..");
|
||||
let installed_games = get_installed_games(games);
|
||||
|
||||
if !installed_games.is_empty() {
|
||||
// if current directory is in the steamapps/common folder of a game, use that game
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
for (id, path) in installed_games.iter() {
|
||||
if current_dir.starts_with(path) {
|
||||
println!("Found game in current directory.");
|
||||
println!("Installing AlterWare client for {}.", id);
|
||||
let game = games.iter().find(|&g| g.app_id == *id).unwrap();
|
||||
setup_client_links(game, path);
|
||||
update(game, path);
|
||||
println!("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:");
|
||||
println!("Enter the ID of the game you want to install the AlterWare client for, enter 0 for manual selection:");
|
||||
let input: u32 = get_input().parse().unwrap();
|
||||
|
||||
if input == 0 {
|
||||
return manual_install(games);
|
||||
}
|
||||
|
||||
for (id, path) in installed_games.iter() {
|
||||
if *id == input {
|
||||
let game = games.iter().find(|&g| g.app_id == input).unwrap();
|
||||
|
||||
let launcher_path = std::env::current_exe().unwrap();
|
||||
fs::copy(launcher_path, path.join("alterware-launcher.exe")).unwrap();
|
||||
let target_path = path.join("alterware-launcher.exe");
|
||||
|
||||
if launcher_path != target_path {
|
||||
fs::copy(launcher_path, target_path).unwrap();
|
||||
println!("Launcher copied to {}", path.display());
|
||||
}
|
||||
setup_client_links(game, path);
|
||||
|
||||
println!("Create Desktop shortcut? (Y/n)");
|
||||
let input = get_input().to_ascii_lowercase();
|
||||
@ -167,31 +213,58 @@ fn windows_launcher_install(games: &Vec<Game>) {
|
||||
));
|
||||
|
||||
let target = path.join("alterware-launcher.exe");
|
||||
let lnk = desktop.join(format!("{}.lnk", game.client));
|
||||
|
||||
let mut sl = ShellLink::new(target).unwrap();
|
||||
for c in game.client.iter() {
|
||||
let lnk = desktop.join(format!("{}.lnk", c));
|
||||
|
||||
let mut sl = ShellLink::new(target.clone()).unwrap();
|
||||
sl.set_arguments(Some(c.to_string()));
|
||||
sl.set_icon_location(Some(
|
||||
path.join(format!("{}.exe", game.client))
|
||||
path.join(format!("{}.exe", c))
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
));
|
||||
sl.create_lnk(lnk).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
update(game, path);
|
||||
println!("Installation complete. Please run the launcher again or use a shortcut to launch the game.");
|
||||
std::io::stdin().read_line(&mut String::new()).unwrap();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
manual_install(games);
|
||||
}
|
||||
}
|
||||
|
||||
println!("No installed Steam games found. Please install a supported game first or place the launcher in the game folder.");
|
||||
fn prompt_client_selection(games: &[Game]) -> String {
|
||||
println!(
|
||||
"Couldn't detect any games, please select a client to install in the current directory:"
|
||||
);
|
||||
for (i, g) in games.iter().enumerate() {
|
||||
for c in g.client.iter() {
|
||||
println!("{}: {}", i, c);
|
||||
}
|
||||
}
|
||||
let input: usize = get_input().parse().unwrap();
|
||||
String::from(games[input].client[0])
|
||||
}
|
||||
|
||||
fn manual_install(games: &[Game]) {
|
||||
let selection = prompt_client_selection(games);
|
||||
let game = games.iter().find(|&g| g.client[0] == selection).unwrap();
|
||||
update(game, &std::env::current_dir().unwrap());
|
||||
println!("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);
|
||||
}
|
||||
|
||||
fn update(game: &Game) {
|
||||
fn update(game: &Game, dir: &Path) {
|
||||
let cdn_info: Vec<CdnFile> = serde_json::from_str(&http::get_body_string(
|
||||
format!("{}/files.json?{}", MASTER, get_cache_buster()).as_str(),
|
||||
format!("{}/files.json", MASTER).as_str(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
@ -200,7 +273,7 @@ fn update(game: &Game) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_path = PathBuf::from(&file.name.replace(&format!("{}/", game.engine), ""));
|
||||
let file_path = dir.join(&file.name.replace(&format!("{}/", game.engine), ""));
|
||||
if file_path.exists() {
|
||||
let sha1_local = get_file_sha1(&file_path).to_lowercase();
|
||||
let sha1_remote = file.hash.to_lowercase();
|
||||
@ -211,10 +284,7 @@ fn update(game: &Game) {
|
||||
sha1_local,
|
||||
sha1_remote
|
||||
);
|
||||
http::download_file(
|
||||
&format!("{}/{}?{}", MASTER, file.name, get_cache_buster()),
|
||||
&file_path,
|
||||
);
|
||||
http::download_file(&format!("{}/{}", MASTER, file.name), &file_path);
|
||||
}
|
||||
} else {
|
||||
println!("Downloading {}...", file_path.display());
|
||||
@ -223,10 +293,7 @@ fn update(game: &Game) {
|
||||
fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
}
|
||||
http::download_file(
|
||||
&format!("{}/{}?{}", MASTER, file.name, get_cache_buster()),
|
||||
&file_path,
|
||||
);
|
||||
http::download_file(&format!("{}/{}", MASTER, file.name), &file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -241,14 +308,8 @@ fn launch(file_path: &PathBuf) {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
self_update();
|
||||
|
||||
let mut args: Vec<String> = std::env::args().collect();
|
||||
|
||||
let games_json =
|
||||
http::get_body_string(format!("{}/games.json?{}", MASTER, get_cache_buster()).as_str());
|
||||
let games: Vec<Game> = serde_json::from_str(&games_json).unwrap();
|
||||
|
||||
let mut update_only = false;
|
||||
if args.contains(&String::from("update")) {
|
||||
update_only = true;
|
||||
@ -257,6 +318,17 @@ fn main() {
|
||||
.map(|e| args.remove(e));
|
||||
}
|
||||
|
||||
if !args.contains(&String::from("skip-launcher-update")) {
|
||||
self_update(update_only);
|
||||
} else {
|
||||
args.iter()
|
||||
.position(|r| r == "skip-launcher-update")
|
||||
.map(|e| args.remove(e));
|
||||
}
|
||||
|
||||
let games_json = http::get_body_string(format!("{}/games.json", MASTER).as_str());
|
||||
let games: Vec<Game> = serde_json::from_str(&games_json).unwrap();
|
||||
|
||||
let mut game: String = String::new();
|
||||
if args.len() > 1 {
|
||||
game = String::from(&args[1]);
|
||||
@ -264,7 +336,25 @@ fn main() {
|
||||
'main: for g in games.iter() {
|
||||
for r in g.references.iter() {
|
||||
if std::path::Path::new(r).exists() {
|
||||
game = String::from(g.client);
|
||||
if g.client.len() > 1 {
|
||||
if update_only {
|
||||
game = String::from(g.client[0]);
|
||||
break 'main;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
setup_client_links(g, &std::env::current_dir().unwrap());
|
||||
|
||||
#[cfg(not(windows))]
|
||||
println!("Multiple clients installed, set the client as the first argument to launch a specific client.");
|
||||
|
||||
for (i, c) in g.client.iter().enumerate() {
|
||||
println!("{}: {}", i, c);
|
||||
}
|
||||
game = String::from(g.client[get_input().parse::<usize>().unwrap()]);
|
||||
break 'main;
|
||||
}
|
||||
game = String::from(g.client[0]);
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
@ -272,19 +362,23 @@ fn main() {
|
||||
}
|
||||
|
||||
for g in games.iter() {
|
||||
if g.client == game {
|
||||
update(g);
|
||||
if update_only {
|
||||
for c in g.client.iter() {
|
||||
if c == &game {
|
||||
update(g, &std::env::current_dir().unwrap());
|
||||
if !update_only {
|
||||
launch(&PathBuf::from(format!("{}.exe", c)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
launch(&PathBuf::from(format!("{}.exe", g.client)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
windows_launcher_install(&games);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
manual_install(&games);
|
||||
|
||||
println!("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...");
|
||||
|
Reference in New Issue
Block a user