50 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
13 changed files with 554 additions and 170 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.7"
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.7"
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.
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

@ -2,4 +2,4 @@ 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";
pub const GH_IW4X_REPO: &str = "iw4x-client";
pub const GH_IW4X_REPO: &str = "iw4x-client";

View File

@ -1,20 +1,40 @@
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);
});
{
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(302) {
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,30 +15,28 @@ 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 {
if !dir.join("iw4x.dll").exists() {
return true;
}
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...");
http::download_file(
&format!(
"{}/download/iw4x.dll",
github::latest_release_url(GH_IW4X_OWNER, GH_IW4X_REPO)
),
&dir.join("iw4x.dll"),
);
fs::write(
dir.join(".iw4xrevision"),
github::latest(GH_IW4X_OWNER, GH_IW4X_REPO),
)
.unwrap();
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",
github::latest_release_url(GH_IW4X_OWNER, GH_IW4X_REPO)
),
&dir.join("iw4x.dll"),
);
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;
}
};
@ -35,26 +37,35 @@ fn get_installed_games(games: &Vec<Game>) -> Vec<(u32, PathBuf)> {
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 desktop shortcuts) to launch a specific client.");
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() {
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), ""));
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 = misc::get_file_sha1(&file_path).to_lowercase();
let sha1_remote = file.hash.to_lowercase();
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!("[{}] {}", "Checked".bright_blue(), file_path.display());
}
hashes.insert(file_name.to_owned(), sha1_remote.to_owned());
} else {
println!("Downloading {}...", file_path.display());
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);
}
}
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) {
println!("Launching {}...", file_path.display());
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");
}
#[cfg(windows)]
fn setup_env() {
colored::control::set_virtual_terminal(true).unwrap_or_else(|error| {
println!("{:#?}", error);
colored::control::SHOULD_COLORIZE.set_override(false);
});
}
fn arg_value(args: &[String], arg: &str) -> Option<String> {
args.iter()
.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();
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));
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 !args.contains(&String::from("skip-launcher-update")) {
self_update::run(update_only);
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 {
args.iter()
.position(|r| r == "skip-launcher-update")
.map(|e| args.remove(e));
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(""),
}
}
}