Compare commits
113 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
237fa8c16f | ||
|
78e155408e | ||
|
3033dd2315 | ||
|
48851fa8d3 | ||
|
84ea4e48af | ||
|
b408f13cce | ||
|
959c3a8a61 | ||
|
73df20ebb6 | ||
|
08814a8c3e | ||
|
65094d4701 | ||
|
a20b1acdda | ||
|
75b1d6254b | ||
|
65f05a5a1c | ||
|
fa6bdc9f29 | ||
|
7595a46b44 | ||
|
c41a843315 | ||
|
a90a60ec3a | ||
|
209b599120 | ||
|
9f00b0c0e7 | ||
|
aaede9b6cb | ||
|
8a14008706 | ||
|
ad7e78ec47 | ||
|
140f4c335f | ||
|
3e1a266c3e | ||
|
92663425ef | ||
|
ffa379e6dd | ||
|
a41375a791 | ||
|
ac76e9bb89 | ||
|
546d8c4cdf | ||
|
12ccc9554f | ||
|
c117eaeb31 | ||
|
50c7b9fbd7 | ||
|
f09271c29c | ||
|
b71c15cb8e | ||
|
a3000bd2fa | ||
|
cc00cad4f2 | ||
|
6a16299cee | ||
|
565a69566c | ||
|
1727446cfc | ||
|
4282eb7b75 | ||
|
a2922540c5 | ||
|
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 | ||
|
744e309190 | ||
|
83e1f67d0d | ||
|
b83720af0a | ||
|
7e36b7496c | ||
|
98cbc2a9a7 | ||
|
85e17260eb | ||
|
14181845fb | ||
|
9323f12442 | ||
|
a7bf44f105 | ||
|
e715cd5abf | ||
|
a234232751 | ||
|
e84a8db0aa | ||
|
fdf35efa81 | ||
|
e94d71b80e | ||
|
d233dfb53c | ||
|
d04083c473 | ||
|
98d43d2627 | ||
|
4fe5ed1f77 | ||
|
7398ca6ca7 | ||
|
4e6ef5d0e5 | ||
|
33463f1a6e | ||
|
41311ed5ad | ||
|
69686b3a09 | ||
|
8be94a31ac | ||
|
99da9832ff | ||
|
b175fc2b5f | ||
|
02ae0459cb | ||
|
611049ec1b | ||
|
2e4467779d | ||
|
e2c22a716b | ||
|
796c67d708 | ||
|
21b7b1ea4b | ||
|
c4046ae148 | ||
|
b1decd0b17 | ||
|
966a63df62 | ||
|
abb583aeff | ||
|
06584a3195 | ||
|
7fcfa2aa57 | ||
|
2e4436ed9a | ||
|
d92319e15f | ||
|
a5cb68e7fa | ||
|
eb161c2375 |
40
.github/workflows/lint.yml
vendored
40
.github/workflows/lint.yml
vendored
@ -1,3 +1,9 @@
|
||||
# Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md
|
||||
#
|
||||
# While our "example" application has the platform-specific code,
|
||||
# for simplicity we are compiling and testing everything on the Ubuntu environment only.
|
||||
# For multi-OS testing see the `cross.yml` workflow.
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
name: lint
|
||||
@ -8,16 +14,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Run cargo check
|
||||
uses: clechasseur/rs-cargo@v2
|
||||
continue-on-error: true
|
||||
uses: actions-rs/cargo@v1
|
||||
continue-on-error: true # WARNING: only for this example, remove it!
|
||||
with:
|
||||
command: check
|
||||
|
||||
@ -26,16 +34,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Run cargo test
|
||||
uses: clechasseur/rs-cargo@v2
|
||||
continue-on-error: true
|
||||
uses: actions-rs/cargo@v1
|
||||
continue-on-error: true # WARNING: only for this example, remove it!
|
||||
with:
|
||||
command: test
|
||||
|
||||
@ -44,24 +54,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run cargo fmt
|
||||
uses: clechasseur/rs-cargo@v2
|
||||
continue-on-error: true
|
||||
uses: actions-rs/cargo@v1
|
||||
continue-on-error: true # WARNING: only for this example, remove it!
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Run cargo clippy
|
||||
uses: clechasseur/rs-cargo@v2
|
||||
continue-on-error: true
|
||||
uses: actions-rs/cargo@v1
|
||||
continue-on-error: true # WARNING: only for this example, remove it!
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
|
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- uses: taiki-e/create-gh-release-action@v1
|
||||
with:
|
||||
draft: true
|
||||
@ -18,23 +18,14 @@ jobs:
|
||||
upload-assets:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
os: ubuntu-20.04
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
os: ubuntu-20.04
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
- target: i686-pc-windows-msvc
|
||||
os: windows-latest
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
os:
|
||||
- ubuntu-20.04
|
||||
- windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
- uses: taiki-e/upload-rust-binary-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
bin: alterware-launcher
|
||||
tar: unix
|
||||
zip: windows
|
||||
|
1224
Cargo.lock
generated
1224
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
32
Cargo.toml
32
Cargo.toml
@ -1,35 +1,39 @@
|
||||
[package]
|
||||
name = "alterware-launcher"
|
||||
version = "0.6.10"
|
||||
version = "0.5.0"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
http_req = { version = "0.9.2", default-features = false, features = [
|
||||
"rust-tls",
|
||||
] }
|
||||
sha1_smol = "1.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
serde_json = "1.0.106"
|
||||
rand = "0.8.5"
|
||||
semver = "1.0.22"
|
||||
colored = "2.1.0"
|
||||
reqwest = { version = "0.11.24", features = ["stream"] }
|
||||
futures-util = "0.3.30"
|
||||
indicatif = "0.17.8"
|
||||
tokio = {version="1.36.0", features = ["rt-multi-thread", "macros"]}
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
openssl = { version = "0.10.64", default-features = false, features = ["vendored"] }
|
||||
semver = "1.0.18"
|
||||
zip = "0.6.6"
|
||||
colored = "2.0.4"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
steamlocate = "=2.0.0-beta.2"
|
||||
steamlocate = "2.0.0-alpha.0"
|
||||
mslnk = "0.1.8"
|
||||
self-replace = "1.3.7"
|
||||
# https://github.com/mitsuhiko/self-replace/pull/16/
|
||||
windows-sys = { version = "0.48", features = [
|
||||
"Win32_Security",
|
||||
] }
|
||||
self-replace = "1.3.6"
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1.12"
|
||||
|
52
README.md
52
README.md
@ -4,16 +4,13 @@
|
||||
|
||||
##### IW4x | IW4-SP | IW5-Mod | IW6-Mod | S1-Mod
|
||||
|
||||
  
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
#### Installation
|
||||
|
||||
1. Download the game from [Steam](https://store.steampowered.com/)
|
||||
2. Download the [latest alterware-launcher.exe](https://github.com/mxve/alterware-launcher/releases/latest/download/alterware-launcher.exe)
|
||||
3. Place alterware-launcher.exe in the game directory
|
||||
4. Run alterware-launcher.exe, after updating the game will launch automatically
|
||||
1. Download the [latest alterware-launcher.exe](https://github.com/mxve/alterware-launcher/releases/latest/download/alterware-launcher.exe)
|
||||
2. Place alterware-launcher.exe in the game directory
|
||||
3. Run alterware-launcher.exe, after updating the game will launch automatically
|
||||
|
||||
---
|
||||
|
||||
@ -35,11 +32,8 @@
|
||||
- Do not include a trailing backslash in the path
|
||||
- ```--pass```
|
||||
- Pass additional arguments to the game
|
||||
- See [client-args.md](client-args.md)
|
||||
- ```--version```, ```-v```
|
||||
- Print the launcher version
|
||||
- ```--ignore-required-files```
|
||||
- Install client even if required files are missing
|
||||
|
||||
Example: ```alterware-launcher.exe iw4x --bonus -u --path "C:\Games\IW4x" --pass "-console"```
|
||||
|
||||
@ -47,33 +41,6 @@ Some arguments can be set in alterware-launcher.json, args generally override th
|
||||
|
||||
---
|
||||
|
||||
#### Config file
|
||||
alterware-launcher.json
|
||||
|
||||
- ```update_only```
|
||||
- See --update
|
||||
- Default: false
|
||||
- ```skip_self_update```
|
||||
- See --skip-launcher-update
|
||||
- Default: false
|
||||
- ```download_bonus_content```
|
||||
- See --bonus
|
||||
- Default: false
|
||||
- ```ask_bonus_content```
|
||||
- Ask the user if they want to download bonus content
|
||||
- Default: true; false after asking
|
||||
- ```force_update```
|
||||
- See --force
|
||||
- Default: false
|
||||
- ```args```
|
||||
- See --pass
|
||||
- Default: ""
|
||||
- ```use_https```
|
||||
- Use HTTPS for downloads
|
||||
- Default: false
|
||||
|
||||
---
|
||||
|
||||
#### Support
|
||||
|
||||
Visit the [AlterWare Forum](https://forum.alterware.dev/) or [Discord](https://discord.gg/2ETE8engZM) for support.
|
||||
@ -90,6 +57,13 @@ Visit the [AlterWare Forum](https://forum.alterware.dev/) or [Discord](https://d
|
||||
---
|
||||
|
||||
### Note for server owners:
|
||||
When the launcher updates itself __on Windows__ it will restart by spawning a new console. If you are automating this process, you should probably use ```--skip-launcher-update``` and download the latest launcher yourself from [here](https://github.com/mxve/alterware-launcher/releases/latest/download/alterware-launcher.exe).
|
||||
When the launcher updates itself it needs to be restarted. It will return exit code 201 in this case.
|
||||
|
||||
The linux build does __not__ update itself.
|
||||
```
|
||||
@echo off
|
||||
:loop
|
||||
start /wait alterware-launcher.exe --update
|
||||
if %errorlevel% equ 201 (
|
||||
goto loop
|
||||
)
|
||||
```
|
@ -1,32 +0,0 @@
|
||||
# IW4x
|
||||
[github.com/iw4x/iw4x-client#command-line-arguments](https://github.com/iw4x/iw4x-client#command-line-arguments)
|
||||
|
||||
| Argument | Description |
|
||||
|:------------------------|:-----------------------------------------------|
|
||||
| `-tests` | Perform unit tests. |
|
||||
| `-entries` | Print to the console a list of every asset as they are loaded from zonefiles. |
|
||||
| `-stdout` | Redirect all logging output to the terminal iw4x is started from, or if there is none, creates a new terminal window to write log information in. |
|
||||
| `-console` | Allow the game to display its own separate interactive console window. |
|
||||
| `-dedicated` | Starts the game as a headless dedicated server. |
|
||||
| `-bigminidumps` | Include all code sections from loaded modules in the dump. |
|
||||
| `-reallybigminidumps` | Include data sections from all loaded modules in the dump. |
|
||||
| `-dump` | Write info of loaded assets to the raw folder as they are being loaded. |
|
||||
| `-nointro` | Skip game's cinematic intro. |
|
||||
| `-version` | Print IW4x build info on startup. |
|
||||
| `-nosteam` | Disable friends feature and do not update Steam about the game's current status just like an invisible mode. |
|
||||
| `-unprotect-dvars` | Allow the server to modify saved/archive dvars. |
|
||||
| `-zonebuilder` | Start the interactive zonebuilder tool console instead of starting the game. |
|
||||
| `-disable-notifies` | Disable "Anti-CFG" checks |
|
||||
| `-disable-mongoose` | Disable Mongoose HTTP server |
|
||||
| `-disable-rate-limit-check` | Disable RCOn rate limit checks |
|
||||
| `+<command>` | Execute game command (ex. `+set net_port 1337`)|
|
||||
|
||||
|
||||
# S1-Mod, IW6-Mod
|
||||
| Argument | Description |
|
||||
|:------------------------|:-----------------------------------------------|
|
||||
| `-headless` | Use system console |
|
||||
| `-dedicated` | Dedicated server |
|
||||
| `-singleplayer` | Start singleplayer; Skip launcher |
|
||||
| `-multiplayer` | Start multiplayer; Skip launcher |
|
||||
| `+<command>` | Execute game command (ex. `+set net_port 1337`)|
|
@ -13,19 +13,7 @@ pub fn load(config_path: PathBuf) -> Config {
|
||||
}
|
||||
|
||||
pub fn save(config_path: PathBuf, config: Config) {
|
||||
match fs::write(
|
||||
config_path.clone(),
|
||||
serde_json::to_string_pretty(&config).unwrap(),
|
||||
) {
|
||||
Ok(_) => (),
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotFound => {
|
||||
fs::create_dir_all(config_path.parent().unwrap()).unwrap();
|
||||
save(config_path, config);
|
||||
}
|
||||
_ => println!("Could not save config file, got:\n{}\n", e),
|
||||
},
|
||||
}
|
||||
fs::write(config_path, serde_json::to_string(&config).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
pub fn save_value(config_path: PathBuf, key: &str, value: bool) {
|
||||
@ -36,17 +24,6 @@ pub fn save_value(config_path: PathBuf, key: &str, value: bool) {
|
||||
"download_bonus_content" => config.download_bonus_content = value,
|
||||
"ask_bonus_content" => config.ask_bonus_content = value,
|
||||
"force_update" => config.force_update = value,
|
||||
"use_https" => config.use_https = value,
|
||||
_ => (),
|
||||
}
|
||||
save(config_path, config);
|
||||
}
|
||||
|
||||
pub fn save_value_s(config_path: PathBuf, key: &str, value: String) {
|
||||
let mut config = load(config_path.clone());
|
||||
match key {
|
||||
"args" => config.args = value.to_string(),
|
||||
"engine" => config.engine = value.to_string(),
|
||||
_ => (),
|
||||
}
|
||||
save(config_path, config);
|
||||
|
@ -1,21 +1,19 @@
|
||||
use semver::Version;
|
||||
|
||||
pub async fn latest_tag(owner: &str, repo: &str) -> String {
|
||||
let github_body = crate::http_async::get_body_string(
|
||||
pub fn latest_tag(owner: &str, repo: &str) -> String {
|
||||
let github_body = crate::http::get_body_string(
|
||||
format!(
|
||||
"https://api.github.com/repos/{}/{}/releases/latest",
|
||||
owner, repo
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
);
|
||||
let github_json: serde_json::Value = serde_json::from_str(&github_body).unwrap();
|
||||
github_json["tag_name"].to_string().replace('"', "")
|
||||
}
|
||||
|
||||
pub async fn latest_version(owner: &str, repo: &str) -> Version {
|
||||
let tag = latest_tag(owner, repo).await.replace('v', "");
|
||||
pub fn latest_version(owner: &str, repo: &str) -> Version {
|
||||
let tag = latest_tag(owner, repo).replace('v', "");
|
||||
Version::parse(&tag).unwrap()
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
pub const MASTER: &str = "cdn.alterware.ovh";
|
||||
pub const MASTER: &str = "https://master.alterware.dev";
|
||||
pub const GH_OWNER: &str = "mxve";
|
||||
pub const GH_REPO: &str = "alterware-launcher";
|
||||
pub const GH_IW4X_OWNER: &str = "iw4x";
|
||||
|
36
src/http.rs
Normal file
36
src/http.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use std::{fs, io::Write, path::Path, str};
|
||||
|
||||
pub fn get_body(url: &str) -> Vec<u8> {
|
||||
let mut res: Vec<u8> = Vec::new();
|
||||
let req = http_req::request::Request::new(&url.try_into().unwrap())
|
||||
.header(
|
||||
"User-Agent",
|
||||
"AlterWare Launcher | github.com/mxve/alterware-launcher",
|
||||
)
|
||||
.send(&mut res)
|
||||
.unwrap_or_else(|error| {
|
||||
panic!("\n\n{}:\n{:?}", "Error", error);
|
||||
});
|
||||
|
||||
if req.status_code() == http_req::response::StatusCode::new(302) {
|
||||
let location = req.headers().get("Location").unwrap().as_str();
|
||||
return get_body(location);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub fn get_body_string(url: &str) -> String {
|
||||
String::from_utf8(get_body(url)).unwrap()
|
||||
}
|
||||
|
||||
pub fn download_file(url: &str, file_path: &Path) {
|
||||
let body = get_body(url);
|
||||
|
||||
let mut f = fs::File::create(file_path).unwrap_or_else(|error| {
|
||||
panic!("\n\n{}:\n{:?}", "Error", error);
|
||||
});
|
||||
f.write_all(&body).unwrap_or_else(|error| {
|
||||
panic!("\n\n{}:\n{:?}", "Error", error);
|
||||
});
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
use std::cmp::min;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use colored::*;
|
||||
use futures_util::StreamExt;
|
||||
use indicatif::ProgressBar;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::misc;
|
||||
|
||||
pub async fn download_file_progress(
|
||||
client: &Client,
|
||||
pb: &ProgressBar,
|
||||
url: &str,
|
||||
path: &PathBuf,
|
||||
size: u64,
|
||||
) -> Result<(), String> {
|
||||
let res = client
|
||||
.get(url)
|
||||
.header(
|
||||
"User-Agent",
|
||||
&format!(
|
||||
"AlterWare Launcher | github.com/{}/{}",
|
||||
crate::global::GH_OWNER,
|
||||
crate::global::GH_REPO
|
||||
),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.or(Err(format!("Failed to GET from '{}'", &url)))?;
|
||||
// Fix for CF shenanigans
|
||||
let total_size = res.content_length().unwrap_or(size);
|
||||
pb.set_length(total_size);
|
||||
pb.println(format!(
|
||||
"[{}] {} ({})",
|
||||
"Downloading".bright_yellow(),
|
||||
misc::cute_path(path),
|
||||
misc::human_readable_bytes(total_size)
|
||||
));
|
||||
pb.set_message(path.file_name().unwrap().to_str().unwrap().to_string());
|
||||
|
||||
let mut file =
|
||||
File::create(path).or(Err(format!("Failed to create file '{}'", path.display())))?;
|
||||
let mut downloaded: u64 = 0;
|
||||
let mut stream = res.bytes_stream();
|
||||
|
||||
while let Some(item) = stream.next().await {
|
||||
let chunk = item.or(Err("Error while downloading file"))?;
|
||||
file.write_all(&chunk)
|
||||
.or(Err("Error while writing to file"))?;
|
||||
let new = min(downloaded + (chunk.len() as u64), total_size);
|
||||
downloaded = new;
|
||||
pb.set_position(new);
|
||||
}
|
||||
|
||||
pb.set_message(String::default());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_file(url: &str, path: &PathBuf) -> Result<(), String> {
|
||||
let client = Client::new();
|
||||
match client
|
||||
.get(url)
|
||||
.header(
|
||||
"User-Agent",
|
||||
&format!(
|
||||
"AlterWare Launcher | github.com/{}/{}",
|
||||
crate::global::GH_OWNER,
|
||||
crate::global::GH_REPO
|
||||
),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(res) => {
|
||||
let body = res.bytes().await.or(Err("Failed to download file"))?;
|
||||
let mut file = File::create(path).or(Err("Failed to create file"))?;
|
||||
file.write_all(&body).or(Err("Failed to write to file"))?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
misc::fatal_error(&format!(
|
||||
"Could not download file from {}, got:\n{}",
|
||||
url, e
|
||||
));
|
||||
Err("Could not download file".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_body(url: &str) -> Result<Vec<u8>, String> {
|
||||
let client = Client::new();
|
||||
match client
|
||||
.get(url)
|
||||
.header(
|
||||
"User-Agent",
|
||||
&format!(
|
||||
"AlterWare Launcher | github.com/{}/{}",
|
||||
crate::global::GH_OWNER,
|
||||
crate::global::GH_REPO
|
||||
),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(res) => {
|
||||
println!(
|
||||
"[DEBUG] {} {}",
|
||||
res.status().to_string().bright_yellow(),
|
||||
url.bright_yellow()
|
||||
);
|
||||
let body = res.bytes().await.or(Err("Failed to get body"))?;
|
||||
Ok(body.to_vec())
|
||||
}
|
||||
Err(e) => {
|
||||
misc::fatal_error(&format!("Could not get body from {}, got:\n{}", url, e));
|
||||
Err("Could not get body".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_body_string(url: &str) -> Result<String, String> {
|
||||
let body = get_body(url).await?;
|
||||
Ok(String::from_utf8(body).unwrap())
|
||||
}
|
22
src/io.rs
Normal file
22
src/io.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use std::{fs, path::Path};
|
||||
|
||||
pub fn unzip(zip_path: &Path, out_path: &Path) {
|
||||
let mut archive = zip::ZipArchive::new(fs::File::open(zip_path).unwrap()).unwrap();
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).unwrap();
|
||||
let outpath = out_path.join(file.name());
|
||||
|
||||
if (*file.name()).ends_with('/') {
|
||||
fs::create_dir_all(outpath).unwrap();
|
||||
} else {
|
||||
println!("Unpacking {}", file.name());
|
||||
if let Some(p) = outpath.parent() {
|
||||
if !p.exists() {
|
||||
fs::create_dir_all(p).unwrap();
|
||||
}
|
||||
}
|
||||
let mut outfile = fs::File::create(&outpath).unwrap();
|
||||
std::io::copy(&mut file, &mut outfile).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
26
src/iw4x.rs
26
src/iw4x.rs
@ -1,6 +1,6 @@
|
||||
use crate::github;
|
||||
use crate::global::*;
|
||||
use crate::http_async;
|
||||
use crate::http;
|
||||
use crate::misc;
|
||||
|
||||
use colored::*;
|
||||
@ -14,39 +14,29 @@ pub fn local_revision(dir: &Path) -> u16 {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remote_revision() -> u16 {
|
||||
misc::rev_to_int(&github::latest_tag(GH_IW4X_OWNER, GH_IW4X_REPO).await)
|
||||
pub fn remote_revision() -> u16 {
|
||||
misc::rev_to_int(&github::latest_tag(GH_IW4X_OWNER, GH_IW4X_REPO))
|
||||
}
|
||||
|
||||
pub async fn update(dir: &Path) {
|
||||
let remote = remote_revision().await;
|
||||
pub fn update(dir: &Path) {
|
||||
let remote = remote_revision();
|
||||
let local = local_revision(dir);
|
||||
|
||||
if remote <= local && dir.join("iw4x.dll").exists() {
|
||||
println!(
|
||||
"[{}] No files to download for IW4x",
|
||||
"Info".bright_magenta(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[{}] Downloading outdated or missing files for IW4x",
|
||||
"Info".bright_magenta()
|
||||
);
|
||||
println!(
|
||||
"[{}] {}",
|
||||
"Downloading".bright_yellow(),
|
||||
misc::cute_path(&dir.join("iw4x.dll"))
|
||||
dir.join("iw4x.dll").display()
|
||||
);
|
||||
http_async::download_file(
|
||||
http::download_file(
|
||||
&format!(
|
||||
"{}/download/iw4x.dll",
|
||||
github::latest_release_url(GH_IW4X_OWNER, GH_IW4X_REPO)
|
||||
),
|
||||
&dir.join("iw4x.dll"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
);
|
||||
fs::write(dir.join(".iw4xrevision"), format!("r{}", remote)).unwrap();
|
||||
}
|
||||
|
451
src/main.rs
451
src/main.rs
@ -1,7 +1,7 @@
|
||||
mod config;
|
||||
mod github;
|
||||
mod global;
|
||||
mod http_async;
|
||||
mod http;
|
||||
mod iw4x;
|
||||
mod misc;
|
||||
mod self_update;
|
||||
@ -11,69 +11,52 @@ use global::*;
|
||||
use structs::*;
|
||||
|
||||
use colored::*;
|
||||
use indicatif::ProgressBar;
|
||||
#[cfg(windows)]
|
||||
use mslnk::ShellLink;
|
||||
use std::{borrow::Cow, collections::HashMap, env, fs, path::Path, path::PathBuf};
|
||||
use std::{borrow::Cow, collections::HashMap, fs, path::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) => {
|
||||
println!("Error locating Steam: {}", error);
|
||||
let mut steamdir = match SteamDir::locate() {
|
||||
Some(steamdir) => steamdir,
|
||||
None => {
|
||||
println!("{}", "Steam not found!".yellow());
|
||||
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));
|
||||
if let Some(app) = steamdir.app(&game.app_id) {
|
||||
installed_games.push((game.app_id, PathBuf::from(&app.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| {
|
||||
println!("Error creating shortcut.\n{:#?}", error);
|
||||
});
|
||||
} else {
|
||||
println!("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.");
|
||||
}
|
||||
|
||||
let target = game_dir.join("alterware-launcher.exe");
|
||||
|
||||
for c in game.client.iter() {
|
||||
create_shortcut(
|
||||
&game_dir.join(format!("launch-{}.lnk", c)),
|
||||
&game_dir.join("alterware-launcher.exe"),
|
||||
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(),
|
||||
c.to_string(),
|
||||
);
|
||||
));
|
||||
sl.create_lnk(&lnk).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,30 +66,37 @@ fn setup_desktop_links(path: &Path, game: &Game) {
|
||||
let input = misc::stdin().to_ascii_lowercase();
|
||||
|
||||
if input == "y" || input.is_empty() {
|
||||
let desktop = PathBuf::from(&format!("{}\\Desktop", env::var("USERPROFILE").unwrap()));
|
||||
let desktop = PathBuf::from(&format!(
|
||||
"{}\\Desktop",
|
||||
std::env::var("USERPROFILE").unwrap()
|
||||
));
|
||||
|
||||
let target = path.join("alterware-launcher.exe");
|
||||
|
||||
for c in game.client.iter() {
|
||||
create_shortcut(
|
||||
&desktop.join(format!("{}.lnk", c)),
|
||||
&path.join("alterware-launcher.exe"),
|
||||
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", c))
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
c.to_string(),
|
||||
);
|
||||
));
|
||||
sl.create_lnk(lnk).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn auto_install(path: &Path, game: &Game<'_>, master_url: &String) {
|
||||
fn auto_install(path: &Path, game: &Game) {
|
||||
setup_client_links(game, path);
|
||||
setup_desktop_links(path, game);
|
||||
update(game, path, false, false, None, master_url, None).await;
|
||||
update(game, path, false, false);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn windows_launcher_install(games: &Vec<Game<'_>>, master_url: &String) {
|
||||
fn windows_launcher_install(games: &Vec<Game>) {
|
||||
println!(
|
||||
"{}",
|
||||
"No game specified/found. Checking for installed Steam games..".yellow()
|
||||
@ -114,13 +104,13 @@ async fn windows_launcher_install(games: &Vec<Game<'_>>, master_url: &String) {
|
||||
let installed_games = get_installed_games(games);
|
||||
|
||||
if !installed_games.is_empty() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
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();
|
||||
auto_install(path, game, master_url).await;
|
||||
auto_install(path, game);
|
||||
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);
|
||||
@ -133,21 +123,25 @@ async fn windows_launcher_install(games: &Vec<Game<'_>>, master_url: &String) {
|
||||
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 = misc::stdin().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 = env::current_exe().unwrap();
|
||||
let launcher_path = std::env::current_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());
|
||||
}
|
||||
auto_install(path, game, master_url).await;
|
||||
auto_install(path, game);
|
||||
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;
|
||||
@ -155,73 +149,47 @@ async fn windows_launcher_install(games: &Vec<Game<'_>>, master_url: &String) {
|
||||
}
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
manual_install(games);
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_client_selection(games: &[Game]) -> String {
|
||||
println!(
|
||||
"No installed games found. Make sure to place the launcher in the game directory."
|
||||
"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 = misc::stdin().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(), false, false);
|
||||
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 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 = misc::stdin().parse().unwrap();
|
||||
// String::from(games[input].client[0])
|
||||
// }
|
||||
|
||||
// async fn manual_install(games: &[Game<'_>]) {
|
||||
// let selection = prompt_client_selection(games);
|
||||
// let game = games.iter().find(|&g| g.client[0] == selection).unwrap();
|
||||
// update(game, &env::current_dir().unwrap(), false, false).await;
|
||||
// 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 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(
|
||||
fn update_dir(
|
||||
cdn_info: &Vec<CdnFile>,
|
||||
remote_dir: &str,
|
||||
dir: &Path,
|
||||
hashes: &mut HashMap<String, String>,
|
||||
pb: &ProgressBar,
|
||||
skip_iw4x_sp: bool,
|
||||
master_url: &String,
|
||||
) {
|
||||
misc::pb_style_download(pb, false);
|
||||
|
||||
let remote_dir_pre = format!("{}/", remote_dir);
|
||||
|
||||
let mut files_to_download: Vec<CdnFile> = vec![];
|
||||
let remote_dir = format!("{}/", remote_dir);
|
||||
|
||||
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" {
|
||||
if !file.name.starts_with(&remote_dir) || file.name == "iw4/iw4x.dll" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sha1_remote = file.hash.to_lowercase();
|
||||
let file_name = &file.name.replace(remote_dir_pre.as_str(), "");
|
||||
let file_name = &file.name.replace(remote_dir.as_str(), "");
|
||||
let file_path = dir.join(file_name);
|
||||
if file_path.exists() {
|
||||
let sha1_local = hashes
|
||||
@ -231,84 +199,38 @@ async fn update_dir(
|
||||
.to_string();
|
||||
|
||||
if sha1_local != sha1_remote {
|
||||
files_to_download.push(file.clone());
|
||||
} else {
|
||||
pb.println(format!(
|
||||
println!(
|
||||
"[{}] {}",
|
||||
"Checked".bright_blue(),
|
||||
misc::cute_path(&file_path)
|
||||
));
|
||||
hashes.insert(file_name.to_owned(), file.hash.to_lowercase());
|
||||
}
|
||||
"Updating".bright_yellow(),
|
||||
file_path.display()
|
||||
);
|
||||
http::download_file(&format!("{}/{}", MASTER, file.name), &file_path);
|
||||
} else {
|
||||
files_to_download.push(file.clone());
|
||||
println!("[{}] {}", "Checked".bright_blue(), file_path.display());
|
||||
}
|
||||
}
|
||||
|
||||
if files_to_download.is_empty() {
|
||||
pb.println(format!(
|
||||
"[{}] No files to download for {}",
|
||||
"Info".bright_magenta(),
|
||||
remote_dir
|
||||
));
|
||||
return;
|
||||
}
|
||||
pb.println(format!(
|
||||
"[{}] Downloading outdated or missing files for {}, {}",
|
||||
"Info".bright_magenta(),
|
||||
remote_dir,
|
||||
misc::human_readable_bytes(total_download_size(&files_to_download, remote_dir))
|
||||
));
|
||||
|
||||
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);
|
||||
hashes.insert(file_name.to_owned(), sha1_remote.to_owned());
|
||||
} else {
|
||||
println!(
|
||||
"[{}] {}",
|
||||
"Downloading".bright_yellow(),
|
||||
file_path.display()
|
||||
);
|
||||
if let Some(parent) = file_path.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
}
|
||||
http_async::download_file_progress(
|
||||
&client,
|
||||
pb,
|
||||
&format!("{}/{}", master_url, file.name),
|
||||
&file_path,
|
||||
file.size as u64,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
hashes.insert(file_name.to_owned(), file.hash.to_lowercase());
|
||||
http::download_file(&format!("{}/{}", MASTER, file.name), &file_path);
|
||||
hashes.insert(file_name.to_owned(), sha1_remote.to_owned());
|
||||
}
|
||||
}
|
||||
misc::pb_style_download(pb, false);
|
||||
}
|
||||
|
||||
async fn update(
|
||||
game: &Game<'_>,
|
||||
dir: &Path,
|
||||
bonus_content: bool,
|
||||
force: bool,
|
||||
skip_iw4x_sp: Option<bool>,
|
||||
master_url: &String,
|
||||
ignore_required_files: Option<bool>,
|
||||
) {
|
||||
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_url).as_str())
|
||||
.await
|
||||
fn update(game: &Game, dir: &Path, bonus_content: bool, force: bool) {
|
||||
let cdn_info: Vec<CdnFile> = serde_json::from_str(&http::get_body_string(
|
||||
format!("{}/files.json", MASTER).as_str(),
|
||||
))
|
||||
.unwrap();
|
||||
let cdn_info: Vec<CdnFile> = serde_json::from_str(&res).unwrap();
|
||||
|
||||
if !ignore_required_files && !game.required_files_exist(dir) {
|
||||
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 mut hashes = HashMap::new();
|
||||
let hash_file = dir.join(".sha-sums");
|
||||
@ -322,124 +244,15 @@ async fn update(
|
||||
}
|
||||
}
|
||||
|
||||
update_dir(&cdn_info, game.engine, dir, &mut hashes);
|
||||
|
||||
if game.engine == "iw4" {
|
||||
iw4x::update(dir).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;
|
||||
iw4x::update(dir);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[{}] {}",
|
||||
"Removed".bright_red(),
|
||||
misc::cute_path(&file_path)
|
||||
);
|
||||
|
||||
if fs::remove_file(&file_path).is_err() {
|
||||
println!(
|
||||
"[{}] Couldn't delete {}",
|
||||
"Error".bright_red(),
|
||||
misc::cute_path(&file_path)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pb = ProgressBar::new(0);
|
||||
update_dir(
|
||||
&cdn_info,
|
||||
game.engine,
|
||||
dir,
|
||||
&mut hashes,
|
||||
&pb,
|
||||
skip_iw4x_sp,
|
||||
master_url,
|
||||
)
|
||||
.await;
|
||||
|
||||
if bonus_content && !game.bonus.is_empty() {
|
||||
for bonus in game.bonus.iter() {
|
||||
update_dir(
|
||||
&cdn_info,
|
||||
bonus,
|
||||
dir,
|
||||
&mut hashes,
|
||||
&pb,
|
||||
skip_iw4x_sp,
|
||||
master_url,
|
||||
)
|
||||
.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 {}",
|
||||
"Error".bright_red(),
|
||||
misc::cute_path(&file_path)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"[{}] {}",
|
||||
"Removed".bright_red(),
|
||||
misc::cute_path(&file_path)
|
||||
);
|
||||
}
|
||||
} else if file_path.is_dir() {
|
||||
if fs::remove_dir_all(&file_path).is_err() {
|
||||
println!(
|
||||
"[{}] Couldn't delete {}",
|
||||
"Error".bright_red(),
|
||||
misc::cute_path(&file_path)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"[{}] {}",
|
||||
"Removed".bright_red(),
|
||||
misc::cute_path(&file_path)
|
||||
);
|
||||
}
|
||||
update_dir(&cdn_info, bonus, dir, &mut hashes);
|
||||
}
|
||||
}
|
||||
|
||||
@ -451,11 +264,9 @@ async fn update(
|
||||
}
|
||||
|
||||
fn launch(file_path: &PathBuf, args: &str) {
|
||||
println!("\n\nJoin the AlterWare Discord server:\nhttps://discord.gg/2ETE8engZM\n\n");
|
||||
println!("Launching {} {}", file_path.display(), args);
|
||||
std::process::Command::new(file_path)
|
||||
.args(args.trim().split(' '))
|
||||
.current_dir(file_path.parent().unwrap())
|
||||
.spawn()
|
||||
.expect("Failed to launch the game")
|
||||
.wait()
|
||||
@ -468,22 +279,6 @@ fn setup_env() {
|
||||
println!("{:#?}", 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) {
|
||||
eprintln!("{:#?}", error);
|
||||
} else {
|
||||
println!("Running from the system directory. Changed working directory to the executable location.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn arg_value(args: &[String], arg: &str) -> Option<String> {
|
||||
@ -507,11 +302,11 @@ fn arg_remove_value(args: &mut Vec<String>, arg: &str) {
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
setup_env();
|
||||
let mut args: Vec<String> = env::args().collect();
|
||||
|
||||
let mut args: Vec<String> = std::env::args().collect();
|
||||
|
||||
if arg_bool(&args, "--help") {
|
||||
println!("CLI Args:");
|
||||
@ -524,7 +319,6 @@ async fn main() {
|
||||
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!(
|
||||
"\nExample:\n alterware-launcher.exe iw4x --bonus --pass \"-console -nointro\""
|
||||
);
|
||||
@ -559,19 +353,13 @@ async fn main() {
|
||||
install_path = PathBuf::from(path);
|
||||
arg_remove_value(&mut args, "-p");
|
||||
} else {
|
||||
install_path = env::current_dir().unwrap();
|
||||
install_path = std::env::current_dir().unwrap();
|
||||
}
|
||||
|
||||
let mut cfg = config::load(install_path.join("alterware-launcher.json"));
|
||||
|
||||
let master_url = if cfg.use_https {
|
||||
format!("https://{}", MASTER)
|
||||
} else {
|
||||
format!("http://{}", MASTER)
|
||||
};
|
||||
|
||||
if !arg_bool(&args, "--skip-launcher-update") && !cfg.skip_self_update {
|
||||
self_update::run(cfg.update_only).await;
|
||||
self_update::run(cfg.update_only);
|
||||
} else {
|
||||
arg_remove(&mut args, "--skip-launcher-update");
|
||||
}
|
||||
@ -584,7 +372,6 @@ async fn main() {
|
||||
|
||||
if arg_bool(&args, "--bonus") {
|
||||
cfg.download_bonus_content = true;
|
||||
cfg.ask_bonus_content = false;
|
||||
arg_remove(&mut args, "--bonus");
|
||||
}
|
||||
|
||||
@ -594,27 +381,15 @@ async fn main() {
|
||||
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();
|
||||
cfg.args = String::from("");
|
||||
}
|
||||
|
||||
let games_json = http_async::get_body_string(format!("{}/games.json", master_url).as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
let games: Vec<Game> = serde_json::from_str(&games_json).unwrap_or_else(|error| {
|
||||
println!("Error parsing games.json: {:#?}", error);
|
||||
fs::write("alterware-launcher-error.txt", &games_json).unwrap();
|
||||
misc::stdin();
|
||||
std::process::exit(1);
|
||||
});
|
||||
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 {
|
||||
@ -630,7 +405,7 @@ async fn main() {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
setup_client_links(g, &env::current_dir().unwrap());
|
||||
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.");
|
||||
@ -651,23 +426,6 @@ async fn 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.ask_bonus_content && !g.bonus.is_empty() {
|
||||
println!("Download bonus content? (Y/n)");
|
||||
let input = misc::stdin().to_ascii_lowercase();
|
||||
@ -689,11 +447,7 @@ async fn main() {
|
||||
install_path.as_path(),
|
||||
cfg.download_bonus_content,
|
||||
cfg.force_update,
|
||||
Some(&game != "iw4x-sp"),
|
||||
&master_url,
|
||||
Some(ignore_required_files),
|
||||
)
|
||||
.await;
|
||||
);
|
||||
if !cfg.update_only {
|
||||
launch(&install_path.join(format!("{}.exe", c)), &cfg.args);
|
||||
}
|
||||
@ -703,7 +457,10 @@ async fn main() {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
windows_launcher_install(&games, &master_url).await;
|
||||
windows_launcher_install(&games);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
manual_install(&games);
|
||||
|
||||
println!("{}", "Game not found!".bright_red());
|
||||
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)");
|
||||
|
40
src/misc.rs
40
src/misc.rs
@ -1,10 +1,4 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
pub fn get_file_sha1(path: &PathBuf) -> String {
|
||||
let mut sha1 = sha1_smol::Sha1::new();
|
||||
@ -24,35 +18,3 @@ pub fn rev_to_int(rev: &str) -> u16 {
|
||||
.parse::<u16>()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn fatal_error(error: &str) {
|
||||
println!("\n\n{}:\n{}", "Error".bright_red(), error);
|
||||
stdin();
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
pub fn human_readable_bytes(bytes: u64) -> String {
|
||||
let mut bytes = bytes as f64;
|
||||
let mut i = 0;
|
||||
let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
while bytes > 1024.0 {
|
||||
bytes /= 1024.0;
|
||||
i += 1;
|
||||
}
|
||||
format!("{:.2}{}", bytes, units[i])
|
||||
}
|
||||
|
||||
pub fn pb_style_download(pb: &ProgressBar, state: bool) {
|
||||
if state {
|
||||
pb.set_style(
|
||||
ProgressStyle::with_template("{spinner:.magenta} {msg:.magenta} > {bytes}/{total_bytes} | {bytes_per_sec} | {eta}")
|
||||
.unwrap(),
|
||||
);
|
||||
} else {
|
||||
pb.set_style(ProgressStyle::with_template("{spinner:.magenta} {msg}").unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cute_path(path: &Path) -> String {
|
||||
path.to_str().unwrap().replace('\\', "/")
|
||||
}
|
||||
|
@ -2,46 +2,34 @@ use crate::github;
|
||||
use crate::global::*;
|
||||
|
||||
use semver::Version;
|
||||
#[cfg(not(windows))]
|
||||
use std::{thread, time};
|
||||
|
||||
pub async fn self_update_available() -> bool {
|
||||
pub fn self_update_available() -> bool {
|
||||
let current_version: Version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||
let latest_version = github::latest_version(GH_OWNER, GH_REPO).await;
|
||||
let latest_version = github::latest_version(GH_OWNER, GH_REPO);
|
||||
|
||||
current_version < latest_version
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub async fn run(_update_only: bool) {
|
||||
if self_update_available().await {
|
||||
pub fn run(_update_only: bool) {
|
||||
if self_update_available() {
|
||||
println!("A new version of the AlterWare launcher is available.");
|
||||
println!(
|
||||
"Download it at {}",
|
||||
github::latest_release_url(GH_OWNER, GH_REPO)
|
||||
);
|
||||
println!("Launching in 10 seconds..");
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
|
||||
thread::sleep(time::Duration::from_secs(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn restart() -> std::io::Error {
|
||||
use std::os::windows::process::CommandExt;
|
||||
match std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args(std::env::args().skip(1))
|
||||
.creation_flags(0x00000010) // CREATE_NEW_CONSOLE
|
||||
.spawn()
|
||||
{
|
||||
Ok(_) => std::process::exit(0),
|
||||
Err(err) => err,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub async fn run(update_only: bool) {
|
||||
pub fn run(update_only: bool) {
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::http_async;
|
||||
use crate::misc;
|
||||
use crate::http;
|
||||
|
||||
let working_dir = std::env::current_dir().unwrap();
|
||||
let files = fs::read_dir(&working_dir).unwrap();
|
||||
@ -54,13 +42,11 @@ pub async fn run(update_only: bool) {
|
||||
&& (file_name.contains(".__relocated__.exe")
|
||||
|| file_name.contains(".__selfdelete__.exe"))
|
||||
{
|
||||
fs::remove_file(file.path()).unwrap_or_else(|_| {
|
||||
println!("Failed to remove old launcher file.");
|
||||
});
|
||||
fs::remove_file(file.path()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if self_update_available().await {
|
||||
if self_update_available() {
|
||||
println!("Performing launcher self-update.");
|
||||
println!(
|
||||
"If you run into any issues, please download the latest version at {}",
|
||||
@ -74,22 +60,13 @@ pub async fn run(update_only: bool) {
|
||||
fs::remove_file(&update_binary).unwrap();
|
||||
}
|
||||
|
||||
let launcher_name = if cfg!(target_arch = "x86") {
|
||||
"alterware-launcher-x86.exe"
|
||||
} else {
|
||||
"alterware-launcher.exe"
|
||||
};
|
||||
|
||||
http_async::download_file(
|
||||
http::download_file(
|
||||
&format!(
|
||||
"{}/download/{}",
|
||||
github::latest_release_url(GH_OWNER, GH_REPO),
|
||||
launcher_name
|
||||
"{}/download/alterware-launcher.exe",
|
||||
github::latest_release_url(GH_OWNER, GH_REPO)
|
||||
),
|
||||
&file_path,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
if !file_path.exists() {
|
||||
println!("Failed to download launcher update.");
|
||||
@ -98,13 +75,9 @@ pub async fn run(update_only: bool) {
|
||||
|
||||
self_replace::self_replace("alterware-launcher-update.exe").unwrap();
|
||||
fs::remove_file(&file_path).unwrap();
|
||||
|
||||
// restarting spawns a new console, automation should manually restart on exit code 201
|
||||
println!("Launcher updated. Please run it again.");
|
||||
if !update_only {
|
||||
let restart_error = restart().to_string();
|
||||
println!("Failed to restart launcher: {}", restart_error);
|
||||
println!("Please restart the launcher manually.");
|
||||
misc::stdin();
|
||||
std::io::stdin().read_line(&mut String::new()).unwrap();
|
||||
}
|
||||
std::process::exit(201);
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Clone)]
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct CdnFile {
|
||||
pub name: String,
|
||||
pub size: u32,
|
||||
@ -14,21 +12,6 @@ pub struct Game<'a> {
|
||||
pub references: Vec<&'a str>,
|
||||
pub app_id: u32,
|
||||
pub bonus: Vec<&'a str>,
|
||||
pub delete: Vec<&'a str>,
|
||||
pub required: Vec<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Game<'a> {
|
||||
pub fn required_files_exist(&self, dir: &Path) -> bool {
|
||||
for required_file in &self.required {
|
||||
let file_path = dir.join(required_file);
|
||||
if !file_path.exists() {
|
||||
println!("Required file {} does not exist", file_path.display());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
@ -39,10 +22,6 @@ pub struct Config {
|
||||
pub ask_bonus_content: bool,
|
||||
pub force_update: bool,
|
||||
pub args: String,
|
||||
#[serde(default)]
|
||||
pub engine: String,
|
||||
#[serde(default)]
|
||||
pub use_https: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@ -53,9 +32,7 @@ impl Default for Config {
|
||||
download_bonus_content: false,
|
||||
ask_bonus_content: true,
|
||||
force_update: false,
|
||||
args: String::default(),
|
||||
engine: String::default(),
|
||||
use_https: true,
|
||||
args: String::from(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user