52 Commits

Author SHA1 Message Date
572c66cc16 v0.5.3 2023-10-06 09:03:34 +02:00
cf87f7c741 misc 2023-10-06 08:34:42 +02:00
dc8b01b4c8 add changelog link 2023-10-06 08:30:29 +02:00
9e8893ce75 handle notfound; give tips to fix permission denied 2023-10-02 15:49:01 +02:00
6f92e1fb71 Create config path if it doesn't exist; don't panic 2023-10-02 15:48:22 +02:00
96c3e504f8 improve logging of http errors 2023-10-02 04:39:49 +02:00
00c14d2a02 handle shortcut creation errors 2023-10-01 00:36:12 +02:00
0378f19a75 set matrix target 2023-09-25 18:57:51 +02:00
64f4ae6429 v0.5.2 2023-09-25 18:52:35 +02:00
b2cc21aed0 Merge branch 'main' of github.com:mxve/alterware-launcher 2023-09-25 18:51:54 +02:00
dc5957ea41 build i686 target 2023-09-25 18:51:43 +02:00
54abce4d30 Merge pull request #34 from mxve/dependabot/cargo/semver-1.0.19
Bump semver from 1.0.18 to 1.0.19
2023-09-25 17:51:50 +02:00
6ae33cdcb3 self-replace 1.3.7 2023-09-25 17:51:21 +02:00
c9d30fa95a Bump semver from 1.0.18 to 1.0.19
Bumps [semver](https://github.com/dtolnay/semver) from 1.0.18 to 1.0.19.
- [Release notes](https://github.com/dtolnay/semver/releases)
- [Commits](https://github.com/dtolnay/semver/compare/1.0.18...1.0.19)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-25 15:28:54 +00:00
dc81430f6b don't ask if --bonus set 2023-09-23 01:29:04 +02:00
9c122506ce Merge pull request #31 from mxve/dependabot/cargo/http_req-0.9.3
Bump http_req from 0.9.2 to 0.9.3
2023-09-18 20:12:38 +02:00
f2ba92c31d Merge pull request #32 from mxve/dependabot/cargo/serde_json-1.0.107
Bump serde_json from 1.0.106 to 1.0.107
2023-09-18 20:12:30 +02:00
c92fb88e83 Bump serde_json from 1.0.106 to 1.0.107
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.106 to 1.0.107.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.106...v1.0.107)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 15:05:36 +00:00
1e0e0090f5 Bump http_req from 0.9.2 to 0.9.3
Bumps [http_req](https://github.com/jayjamesjay/http_req) from 0.9.2 to 0.9.3.
- [Release notes](https://github.com/jayjamesjay/http_req/releases)
- [Commits](https://github.com/jayjamesjay/http_req/compare/v0.9.2...v0.9.3)

---
updated-dependencies:
- dependency-name: http_req
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 15:05:29 +00:00
48f6a96a01 pwettify comfig file :3 2023-09-18 16:29:37 +02:00
f4fe1c6699 v0.5.1 2023-09-18 12:27:01 +02:00
59f1b09337 set current_dir when launching 2023-09-18 12:26:47 +02:00
237fa8c16f v0.5.0 2023-09-17 17:49:50 +02:00
78e155408e print passed args when launching; trim args 2023-09-16 16:41:18 +02:00
Edo
3033dd2315 Merge pull request #30 from diamante0018/main
maint(main): simply return out of the main function
2023-09-15 19:22:11 +02:00
Edo
48851fa8d3 maint(main): simply return out of the main function
instead of calling exit()
2023-09-15 19:19:04 +02:00
84ea4e48af update readme 2023-09-15 01:48:04 +02:00
b408f13cce add --help, --version/-v 2023-09-15 01:45:47 +02:00
959c3a8a61 allow loading client args from config 2023-09-14 10:32:03 +02:00
73df20ebb6 💩 2023-09-14 10:25:44 +02:00
08814a8c3e Merge pull request #29 from mxve/dependabot/cargo/serde_json-1.0.106
Bump serde_json from 1.0.105 to 1.0.106
2023-09-14 10:24:26 +02:00
65094d4701 add --pass to allow passing args to the client 2023-09-14 10:23:48 +02:00
a20b1acdda remove debug print 2023-09-14 10:09:21 +02:00
75b1d6254b strip value AND arg 😑 2023-09-14 10:08:26 +02:00
65f05a5a1c add --path, -p 2023-09-14 09:44:35 +02:00
fa6bdc9f29 prepend args with --, -, add short args 2023-09-14 09:37:35 +02:00
7595a46b44 update readme 2023-09-12 21:35:50 +02:00
c41a843315 store file hashes; added "force" arg
close #28
2023-09-12 21:28:53 +02:00
a90a60ec3a Bump serde_json from 1.0.105 to 1.0.106
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.105 to 1.0.106.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.105...v1.0.106)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 15:16:24 +00:00
209b599120 improve progress prints 2023-09-11 13:05:18 +02:00
9f00b0c0e7 Show more progress 2023-09-10 20:00:04 +02:00
aaede9b6cb download bonus content 2023-09-10 18:29:30 +02:00
8a14008706 use config values 2023-09-10 17:36:01 +02:00
ad7e78ec47 cfg 2023-09-10 16:58:42 +02:00
140f4c335f v0.4.8 2023-09-10 12:39:44 +02:00
3e1a266c3e check first dir when matching files to download 2023-09-10 12:38:21 +02:00
92663425ef update readme 2023-09-03 19:03:46 +02:00
ffa379e6dd misc
- reduce github calls
- latest_tag returns full tag
- rev_to_int default to 0 on strip_prefix
- lint
2023-08-30 13:06:49 +02:00
a41375a791 github::latest -> latest_tag 2023-08-30 12:46:13 +02:00
ac76e9bb89 steamlocate 2.0.0-alpha.0 2023-08-30 12:45:10 +02:00
546d8c4cdf v0.4.7 2023-08-29 22:13:05 +02:00
12ccc9554f update iw4x if dll doesn't exist 2023-08-29 22:12:42 +02:00
13 changed files with 554 additions and 167 deletions

View File

@ -18,14 +18,21 @@ jobs:
upload-assets:
strategy:
matrix:
os:
- ubuntu-20.04
- windows-latest
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-20.04
- target: i686-unknown-linux-gnu
os: ubuntu-20.04
- target: x86_64-pc-windows-msvc
os: windows-latest
- target: i686-pc-windows-msvc
os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: taiki-e/upload-rust-binary-action@v1
with:
target: ${{ matrix.target }}
bin: alterware-launcher
tar: unix
zip: windows

96
Cargo.lock generated
View File

@ -30,8 +30,9 @@ dependencies = [
[[package]]
name = "alterware-launcher"
version = "0.4.6"
version = "0.5.3"
dependencies = [
"colored",
"http_req",
"mslnk",
"rand",
@ -41,7 +42,6 @@ dependencies = [
"serde_json",
"sha1_smol",
"steamlocate",
"windows-sys",
"winres",
"zip",
]
@ -137,6 +137,17 @@ dependencies = [
"inout",
]
[[package]]
name = "colored"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6"
dependencies = [
"is-terminal",
"lazy_static",
"windows-sys",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
@ -199,23 +210,22 @@ dependencies = [
[[package]]
name = "dirs"
version = "5.0.1"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys",
"winapi",
]
[[package]]
@ -285,6 +295,12 @@ dependencies = [
"wasi",
]
[[package]]
name = "hermit-abi"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hmac"
version = "0.12.1"
@ -296,9 +312,9 @@ dependencies = [
[[package]]
name = "http_req"
version = "0.9.2"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f680177f2ebe4aabd573d07b322d15a5e0fbc97cd739fd627b08043c89041f8"
checksum = "42ce34c74ec562d68f2c23a532c62c1332ff1d1b6147fd118bd1938e090137d0"
dependencies = [
"rustls",
"unicase",
@ -324,6 +340,17 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "is-terminal"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
dependencies = [
"hermit-abi",
"rustix",
"windows-sys",
]
[[package]]
name = "itoa"
version = "1.0.6"
@ -373,6 +400,12 @@ dependencies = [
"thiserror",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.147"
@ -417,24 +450,12 @@ dependencies = [
"log",
]
[[package]]
name = "nom"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce"
[[package]]
name = "once_cell"
version = "1.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "password-hash"
version = "0.4.2"
@ -685,9 +706,9 @@ dependencies = [
[[package]]
name = "self-replace"
version = "1.3.6"
version = "1.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c56335359191626938ef6fdeb478f9f6a7c6020254d7f4641c7d810369fa0ec1"
checksum = "525db198616b2bcd0f245daf7bfd8130222f7ee6af9ff9984c19a61bf1160c55"
dependencies = [
"fastrand 1.9.0",
"tempfile",
@ -696,9 +717,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0"
[[package]]
name = "serde"
@ -722,9 +743,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.105"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
dependencies = [
"itoa",
"ryu",
@ -767,27 +788,17 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "steamlocate"
version = "1.2.1"
version = "2.0.0-alpha.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec01c74611d14a808cb212d17c6e03f0e30736a15ed1d5736f8a53154cea3ae"
checksum = "2b1568c4a70a26c4373fe1131ffa4eff055459631b6e40c6bc118615f2d870c3"
dependencies = [
"dirs",
"keyvalues-parser",
"keyvalues-serde",
"serde",
"steamy-vdf",
"winreg",
]
[[package]]
name = "steamy-vdf"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533127ad49314bfe71c3d3fd36b3ebac3d24f40618092e70e1cfe8362c7fac79"
dependencies = [
"nom",
]
[[package]]
name = "subtle"
version = "2.5.0"
@ -1082,11 +1093,10 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winreg"
version = "0.11.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"cfg-if",
"winapi",
]

View File

@ -1,6 +1,6 @@
[package]
name = "alterware-launcher"
version = "0.4.6"
version = "0.5.3"
edition = "2021"
build = "res/build.rs"
@ -15,24 +15,21 @@ 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 = [
http_req = { version = "0.9.3", default-features = false, features = [
"rust-tls",
] }
sha1_smol = "1.0.0"
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.105"
serde_json = "1.0.107"
rand = "0.8.5"
semver = "1.0.18"
semver = "1.0.19"
zip = "0.6.6"
colored = "2.0.4"
[target.'cfg(windows)'.dependencies]
steamlocate = "1.2.1"
steamlocate = "2.0.0-alpha.0"
mslnk = "0.1.8"
# https://github.com/mitsuhiko/self-replace/pull/16/
windows-sys = { version = "0.48", features = [
"Win32_Security",
] }
self-replace = "1.3.6"
self-replace = "1.3.7"
[build-dependencies]
winres = "0.1.12"

View File

@ -1,14 +1,69 @@
# alterware-launcher
# AlterWare Launcher
1. Download [latest release](https://github.com/mxve/alterware-launcher/releases/latest/download/alterware-launcher-x86_64-pc-windows-msvc.zip)
2. Unpack the archive and place alterware-launcher.exe in the game directory
### [AlterWare.dev](https://alterware.dev)
##### IW4x | IW4-SP | IW5-Mod | IW6-Mod | S1-Mod
---
#### Installation
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
---
- 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
#### Command line arguments
- ```iw4-sp```, ```iw4x```, ```iw5-mod```, ```iw6-mod```, ```s1-mod```
- Skip automatic detection and launch the specified game
- This should always be the first argument if used
- ```--update```, ```-u```
- Only update the game, don't launch it
- ```--skip-launcher-update```
- Don't update the launcher
- ```--bonus```
- Download bonus content
- ```--force```, ```-f```
- Force file hash recheck
- ```--path```, ```-p```
- Set the game path
- Do not include a trailing backslash in the path
- ```--pass```
- Pass additional arguments to the game
- ```--version```, ```-v```
- Print the launcher version
Example: ```alterware-launcher.exe iw4x --bonus -u --path "C:\Games\IW4x" --pass "-console"```
Some arguments can be set in alterware-launcher.json, args generally override the values of the config.
---
#### Support
Visit the [AlterWare Forum](https://forum.alterware.dev/) or [Discord](https://discord.gg/2ETE8engZM) for support.
---
#### Building from Source
- [Install Rust](https://rustup.rs/)
- Clone the repository
- Run ```cargo build --release```
- The executable will be located in ```target/release```
---
### Note for server owners:
When the launcher updates itself it needs to be restarted. It will return exit code 201 in this case.
```
@echo off
:loop
start /wait alterware-launcher.exe --update
if %errorlevel% equ 201 (
goto loop
)
```

42
src/config.rs Normal file
View File

@ -0,0 +1,42 @@
use crate::structs::Config;
use std::{fs, path::PathBuf};
pub fn load(config_path: PathBuf) -> Config {
if config_path.exists() {
let cfg = fs::read_to_string(&config_path).unwrap();
let cfg: Config = serde_json::from_str(&cfg).unwrap_or(Config::default());
return cfg;
}
save(config_path.clone(), Config::default());
Config::default()
}
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),
},
}
}
pub fn save_value(config_path: PathBuf, key: &str, value: bool) {
let mut config = load(config_path.clone());
match key {
"update_only" => config.update_only = value,
"skip_self_update" => config.skip_self_update = value,
"download_bonus_content" => config.download_bonus_content = value,
"ask_bonus_content" => config.ask_bonus_content = value,
"force_update" => config.force_update = value,
_ => (),
}
save(config_path, config);
}

View File

@ -1,6 +1,6 @@
use semver::Version;
pub fn latest(owner: &str, repo: &str) -> 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",
@ -9,13 +9,12 @@ pub fn latest(owner: &str, repo: &str) -> String {
.as_str(),
);
let github_json: serde_json::Value = serde_json::from_str(&github_body).unwrap();
github_json["tag_name"]
.to_string()
.replace(['v', '"'].as_ref(), "")
github_json["tag_name"].to_string().replace('"', "")
}
pub fn latest_version(owner: &str, repo: &str) -> Version {
Version::parse(&latest(owner, repo)).unwrap()
let tag = latest_tag(owner, repo).replace('v', "");
Version::parse(&tag).unwrap()
}
pub fn latest_release_url(owner: &str, repo: &str) -> String {

View File

@ -1,22 +1,42 @@
use crate::global;
use crate::misc;
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())
match http_req::request::Request::new(&url.try_into().unwrap())
.header(
"User-Agent",
"AlterWare Launcher | github.com/mxve/alterware-launcher",
&format!(
"AlterWare Launcher | github.com/{}/{}",
global::GH_OWNER,
global::GH_REPO
),
)
.send(&mut res)
.unwrap_or_else(|error| {
panic!("\n\n{}:\n{:?}", "Error", error);
});
if req.status_code() == http_req::response::StatusCode::new(302) {
{
Ok(req) => {
if req.status_code() == http_req::response::StatusCode::new(302)
|| req.status_code() == http_req::response::StatusCode::new(301)
{
let location = req.headers().get("Location").unwrap().as_str();
return get_body(location);
}
if req.status_code() != http_req::response::StatusCode::new(200) {
misc::fatal_error(&format!(
"Could not get body from {}, got {}",
url,
req.status_code()
));
}
}
Err(e) => {
misc::fatal_error(&format!("Could not get body from {}, got:\n{}", url, e));
}
}
res
}
@ -27,10 +47,38 @@ pub fn get_body_string(url: &str) -> String {
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);
});
match fs::File::create(file_path) {
Ok(mut file) => match file.write_all(&body) {
Ok(_) => (),
Err(e) => {
misc::fatal_error(&format!(
"Could not write to file {}, got:\n{}",
file_path.to_str().unwrap(),
e
));
}
},
Err(e) => {
match e.kind() {
std::io::ErrorKind::NotFound => {
fs::create_dir_all(file_path.parent().unwrap()).unwrap();
return download_file(url, file_path);
}
std::io::ErrorKind::PermissionDenied => {
misc::fatal_error(&format!(
"Permission to {} denied.\n Please try:\n 1. Running the launcher as administrator.\n 2. Manually deleting the last downloaded file.\n 3. If your game is in the program files directory try moving it to another location.\n\n\n{}",
file_path.to_str().unwrap(),
e
));
}
_ => (),
}
misc::fatal_error(&format!(
"Could not create file {}, got:\n{}",
file_path.to_str().unwrap(),
e
));
}
}
}

View File

@ -1,8 +1,9 @@
use crate::github;
use crate::global::*;
use crate::http;
use crate::misc;
use crate::global::*;
use colored::*;
use std::{fs, path::Path};
pub fn local_revision(dir: &Path) -> u16 {
@ -14,16 +15,22 @@ pub fn local_revision(dir: &Path) -> u16 {
}
pub fn remote_revision() -> u16 {
misc::rev_to_int(&github::latest(GH_IW4X_OWNER, GH_IW4X_REPO))
}
pub fn update_available(dir: &Path) -> bool {
local_revision(dir) < remote_revision()
misc::rev_to_int(&github::latest_tag(GH_IW4X_OWNER, GH_IW4X_REPO))
}
pub fn update(dir: &Path) {
if update_available(dir) {
println!("Updating IW4x...");
let remote = remote_revision();
let local = local_revision(dir);
if remote <= local && dir.join("iw4x.dll").exists() {
return;
}
println!(
"[{}] {}",
"Downloading".bright_yellow(),
dir.join("iw4x.dll").display()
);
http::download_file(
&format!(
"{}/download/iw4x.dll",
@ -31,10 +38,5 @@ pub fn update(dir: &Path) {
),
&dir.join("iw4x.dll"),
);
fs::write(
dir.join(".iw4xrevision"),
github::latest(GH_IW4X_OWNER, GH_IW4X_REPO),
)
.unwrap();
}
fs::write(dir.join(".iw4xrevision"), format!("r{}", remote)).unwrap();
}

View File

@ -1,3 +1,4 @@
mod config;
mod github;
mod global;
mod http;
@ -9,9 +10,10 @@ mod structs;
use global::*;
use structs::*;
use colored::*;
#[cfg(windows)]
use mslnk::ShellLink;
use std::{fs, path::Path, path::PathBuf};
use std::{borrow::Cow, collections::HashMap, fs, path::Path, path::PathBuf};
#[cfg(windows)]
use steamlocate::SteamDir;
@ -21,7 +23,7 @@ fn get_installed_games(games: &Vec<Game>) -> Vec<(u32, PathBuf)> {
let mut steamdir = match SteamDir::locate() {
Some(steamdir) => steamdir,
None => {
println!("Steam not found.");
println!("{}", "Steam not found!".yellow());
return installed_games;
}
};
@ -36,25 +38,34 @@ fn get_installed_games(games: &Vec<Game>) -> Vec<(u32, PathBuf)> {
}
#[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.");
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.");
}
}
let target = game_dir.join("alterware-launcher.exe");
#[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() {
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(
create_shortcut(
&game_dir.join(format!("launch-{}.lnk", c)),
&game_dir.join("alterware-launcher.exe"),
game_dir
.join(format!("{}.exe", c))
.to_string_lossy()
.into_owned(),
));
sl.create_lnk(&lnk).unwrap();
c.to_string(),
);
}
}
@ -69,19 +80,15 @@ fn setup_desktop_links(path: &Path, game: &Game) {
std::env::var("USERPROFILE").unwrap()
));
let target = path.join("alterware-launcher.exe");
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(
create_shortcut(
&desktop.join(format!("{}.lnk", c)),
&path.join("alterware-launcher.exe"),
path.join(format!("{}.exe", c))
.to_string_lossy()
.into_owned(),
));
sl.create_lnk(lnk).unwrap();
c.to_string(),
);
}
}
}
@ -90,16 +97,18 @@ fn setup_desktop_links(path: &Path, game: &Game) {
fn auto_install(path: &Path, game: &Game) {
setup_client_links(game, path);
setup_desktop_links(path, game);
update(game, path);
update(game, path, false, false);
}
#[cfg(windows)]
fn windows_launcher_install(games: &Vec<Game>) {
println!("No game specified/found. Checking for installed Steam games..");
println!(
"{}",
"No game specified/found. Checking for installed Steam games..".yellow()
);
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) {
@ -165,78 +174,225 @@ fn prompt_client_selection(games: &[Game]) -> String {
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());
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 update(game: &Game, dir: &Path) {
let cdn_info: Vec<CdnFile> = serde_json::from_str(&http::get_body_string(
format!("{}/files.json", MASTER).as_str(),
))
.unwrap();
fn update_dir(
cdn_info: &Vec<CdnFile>,
remote_dir: &str,
dir: &Path,
hashes: &mut HashMap<String, String>,
) {
let remote_dir = format!("{}/", remote_dir);
for file in cdn_info {
if !file.name.starts_with(game.engine) || file.name == "iw4/iw4x.dll" {
if !file.name.starts_with(&remote_dir) || file.name == "iw4/iw4x.dll" {
continue;
}
let file_path = dir.join(&file.name.replace(&format!("{}/", game.engine), ""));
if file_path.exists() {
let sha1_local = misc::get_file_sha1(&file_path).to_lowercase();
let sha1_remote = file.hash.to_lowercase();
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
.get(file_name)
.map(Cow::Borrowed)
.unwrap_or_else(|| Cow::Owned(misc::get_file_sha1(&file_path)))
.to_string();
if sha1_local != sha1_remote {
println!(
"Updating {}...\nLocal hash: {}\nRemote hash: {}",
file_path.display(),
sha1_local,
sha1_remote
"[{}] {}",
"Updating".bright_yellow(),
file_path.display()
);
http::download_file(&format!("{}/{}", MASTER, file.name), &file_path);
}
} else {
println!("Downloading {}...", file_path.display());
println!("[{}] {}", "Checked".bright_blue(), file_path.display());
}
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::download_file(&format!("{}/{}", MASTER, file.name), &file_path);
hashes.insert(file_name.to_owned(), sha1_remote.to_owned());
}
}
}
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 mut hashes = HashMap::new();
let hash_file = dir.join(".sha-sums");
if hash_file.exists() && !force {
let hash_file = fs::read_to_string(hash_file).unwrap();
for line in hash_file.lines() {
let mut split = line.split_whitespace();
let hash = split.next().unwrap();
let file = split.next().unwrap();
hashes.insert(file.to_owned(), hash.to_owned());
}
}
update_dir(&cdn_info, game.engine, dir, &mut hashes);
if game.engine == "iw4" {
iw4x::update(dir);
}
if bonus_content && !game.bonus.is_empty() {
for bonus in game.bonus.iter() {
update_dir(&cdn_info, bonus, dir, &mut hashes);
}
}
fn launch(file_path: &PathBuf) {
println!("Launching {}...", file_path.display());
let mut hash_file_content = String::new();
for (file, hash) in hashes.iter() {
hash_file_content.push_str(&format!("{} {}\n", hash, file));
}
fs::write(dir.join(".sha-sums"), hash_file_content).unwrap();
}
fn launch(file_path: &PathBuf, args: &str) {
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()
.expect("Failed to wait for the game process to finish");
}
fn main() {
let mut args: Vec<String> = std::env::args().collect();
let mut update_only = false;
if args.contains(&String::from("update")) {
update_only = true;
args.iter()
.position(|r| r == "update")
.map(|e| args.remove(e));
#[cfg(windows)]
fn setup_env() {
colored::control::set_virtual_terminal(true).unwrap_or_else(|error| {
println!("{:#?}", error);
colored::control::SHOULD_COLORIZE.set_override(false);
});
}
if !args.contains(&String::from("skip-launcher-update")) {
self_update::run(update_only);
} else {
fn arg_value(args: &[String], arg: &str) -> Option<String> {
args.iter()
.position(|r| r == "skip-launcher-update")
.map(|e| args.remove(e));
.position(|r| r == arg)
.map(|e| args[e + 1].clone())
}
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);
};
}
fn main() {
#[cfg(windows)]
setup_env();
let mut args: Vec<String> = std::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!(" --force/-f: Force file hash recheck");
println!(" --pass <args>: Pass arguments to the game");
println!(" --skip-launcher-update: Skip launcher self-update");
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 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 = std::env::current_dir().unwrap();
}
let mut cfg = config::load(install_path.join("alterware-launcher.json"));
if !arg_bool(&args, "--skip-launcher-update") && !cfg.skip_self_update {
self_update::run(cfg.update_only);
} 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");
}
if arg_bool(&args, "--force") || arg_bool(&args, "-f") {
cfg.force_update = true;
arg_remove(&mut args, "--force");
arg_remove(&mut args, "-f");
}
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::from("");
}
let games_json = http::get_body_string(format!("{}/games.json", MASTER).as_str());
@ -248,9 +404,9 @@ fn main() {
} else {
'main: for g in games.iter() {
for r in g.references.iter() {
if std::path::Path::new(r).exists() {
if install_path.join(r).exists() {
if g.client.len() > 1 {
if update_only {
if cfg.update_only {
game = String::from(g.client[0]);
break 'main;
}
@ -277,9 +433,30 @@ fn main() {
for g in games.iter() {
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)));
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,
);
if !cfg.update_only {
launch(&install_path.join(format!("{}.exe", c)), &cfg.args);
}
return;
}
@ -292,7 +469,7 @@ fn main() {
#[cfg(not(windows))]
manual_install(&games);
println!("Game not found!");
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)");
println!("Press enter to exit...");
std::io::stdin().read_line(&mut String::new()).unwrap();

View File

@ -1,5 +1,7 @@
use std::{fs, path::PathBuf};
use colored::Colorize;
pub fn get_file_sha1(path: &PathBuf) -> String {
let mut sha1 = sha1_smol::Sha1::new();
sha1.update(&fs::read(path).unwrap());
@ -13,5 +15,14 @@ pub fn stdin() -> String {
}
pub fn rev_to_int(rev: &str) -> u16 {
rev.strip_prefix('r').unwrap().parse::<u16>().unwrap_or(0)
rev.strip_prefix('r')
.unwrap_or("0")
.parse::<u16>()
.unwrap_or(0)
}
pub fn fatal_error(error: &str) {
println!("\n\n{}:\n{}", "Error".bright_red(), error);
stdin();
std::process::exit(1);
}

View File

@ -16,7 +16,10 @@ pub fn self_update_available() -> bool {
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!(
"Download it at {}",
github::latest_release_url(GH_OWNER, GH_REPO)
);
println!("Launching in 10 seconds..");
thread::sleep(time::Duration::from_secs(10));
}
@ -27,6 +30,7 @@ pub fn run(update_only: bool) {
use std::{fs, path::PathBuf};
use crate::http;
use crate::misc;
let working_dir = std::env::current_dir().unwrap();
let files = fs::read_dir(&working_dir).unwrap();
@ -57,10 +61,17 @@ pub 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"
};
println!("{}", launcher_name);
http::download_file(
&format!(
"{}/download/alterware-launcher.exe",
github::latest_release_url(GH_OWNER, GH_REPO)
"{}/download/{}",
github::latest_release_url(GH_OWNER, GH_REPO),
launcher_name
),
&file_path,
);
@ -72,9 +83,13 @@ pub fn run(update_only: bool) {
self_replace::self_replace("alterware-launcher-update.exe").unwrap();
fs::remove_file(&file_path).unwrap();
println!("Launcher updated. Please run it again.");
println!(
"Launcher updated. View the changelog at https://github.com/{}/{}/releases/latest",
GH_OWNER, GH_REPO,
);
println!("Please restart the launcher.");
if !update_only {
std::io::stdin().read_line(&mut String::new()).unwrap();
misc::stdin();
}
std::process::exit(201);
}

View File

@ -11,4 +11,28 @@ pub struct Game<'a> {
pub client: Vec<&'a str>,
pub references: Vec<&'a str>,
pub app_id: u32,
pub bonus: Vec<&'a str>,
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct Config {
pub update_only: bool,
pub skip_self_update: bool,
pub download_bonus_content: bool,
pub ask_bonus_content: bool,
pub force_update: bool,
pub args: String,
}
impl Default for Config {
fn default() -> Self {
Self {
update_only: false,
skip_self_update: false,
download_bonus_content: false,
ask_bonus_content: true,
force_update: false,
args: String::from(""),
}
}
}