Compare commits
265 Commits
v0.6.0-pre
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
dc6a61b8fc | ||
|
87535fb839 | ||
|
4d6f056d7b | ||
|
c9ead57ee5 | ||
|
d441eeaa36 | ||
|
2143b86d07 | ||
|
5372fba0d6 | ||
|
b0e445400a | ||
|
a71c789e29 | ||
|
a561839fd0 | ||
|
4ddd3c3ca7 | ||
|
bb66366f41 | ||
|
bd26533ae2 | ||
|
1452dfb265 | ||
|
55328a969b | ||
|
512a073b07 | ||
|
99010664bf | ||
|
c93fcba652 | ||
|
5450109cf2 | ||
|
02c3004231 | ||
|
6bb96006de | ||
|
9de8543590 | ||
|
dba849e3be | ||
|
399d7da9aa | ||
|
2454547c8f | ||
|
3d0a47ec60 | ||
|
5ef075d4c3 | ||
|
e8aee89f45 | ||
|
2760e2ab1e | ||
|
d44ae36f86 | ||
|
6e3c06af86 | ||
|
fcdc875272 | ||
|
32d1b129c8 | ||
|
1dd18317cf | ||
|
ccdde3d99f | ||
|
d5973b0aff | ||
|
652b908a39 | ||
|
f387a34b35 | ||
|
ea69e6d964 | ||
|
9cbfed8bf3 | ||
|
9f789ba556 | ||
|
7fc39f24bc | ||
|
1dd2e898d9 | ||
|
006ca15232 | ||
|
1730bf40be | ||
|
f40448b6fc | ||
|
b418732ddf | ||
|
bef7a88db0 | ||
|
9f95397bd2 | ||
|
c2ec4f6a45 | ||
|
cab26cfa16 | ||
|
1147e0673b | ||
|
4fec555c1f | ||
|
48fe7a4780 | ||
|
0e8b9285c9 | ||
|
725ec09e5e | ||
|
caaf73e9f3 | ||
|
458cd6d8bc | ||
|
2261a9a655 | ||
|
25a781df8e | ||
|
637fa0a504 | ||
|
23df31735d | ||
|
6ae5ff66f0 | ||
|
11f49cca32 | ||
|
ad024ab997 | ||
|
2db8e83502 | ||
|
8f674ef4e5 | ||
|
19484c8b79 | ||
|
92c46a9d87 | ||
|
e4de326b41 | ||
|
0728561eca | ||
|
7178f2ed22 | ||
|
ad4b5ca8aa | ||
|
9945fd1b1f | ||
|
3f8cadea8e | ||
|
08b04425a5 | ||
|
55fd51a1cc | ||
|
22bac811fc | ||
|
bb7786cb0a | ||
|
e3775ef3dc | ||
|
62e3569195 | ||
|
e48090a00c | ||
|
168e8bc110 | ||
|
8782642e4e | ||
|
646c10d95a | ||
|
25c3d25b10 | ||
|
570150777d | ||
|
0652f77355 | ||
|
6494af20de | ||
|
35a1d1866f | ||
|
3bbfc9bb81 | ||
|
3f6de0350b | ||
|
73c45de4be | ||
|
9fe9f58e70 | ||
|
09cec3f1ad | ||
|
1e62b5ffad | ||
|
f582cdd625 | ||
|
8cc1047367 | ||
|
0c004b6f0f | ||
|
f0a63a241a | ||
|
76896a22f9 | ||
|
7c37334199 | ||
|
897c5dae4b | ||
|
fdcf3d31bd | ||
|
935a72e05b | ||
|
174333ff93 | ||
|
17454d4dc8 | ||
|
9a6b7b531a | ||
|
806118862f | ||
|
9a9e64fb0e | ||
|
8c407de8fb | ||
|
7f6027fe2e | ||
|
dacaf322d4 | ||
|
78fa91ded4 | ||
|
aa69a26f1f | ||
|
f2f388e0bb | ||
|
9cf7049aaa | ||
|
f609a53377 | ||
|
24a745abff | ||
|
58b57f48ad | ||
|
9c26679a5a | ||
|
ca836bbda6 | ||
|
360d2376e1 | ||
|
0b60b2a893 | ||
|
cddc697e89 | ||
|
ec1b580b21 | ||
|
eeacb15d6f | ||
|
4273d00417 | ||
|
9596c5d491 | ||
|
123a8b7caf | ||
|
141b60393a | ||
|
4bdecabbe5 | ||
|
6a8c11b101 | ||
|
12526d7f36 | ||
|
f3e7a42043 | ||
|
fddd146361 | ||
|
b86d252c4b | ||
|
dee9011812 | ||
|
96bc7a73e5 | ||
|
f756e818b6 | ||
|
686a6e6352 | ||
|
a56b7f12ee | ||
|
e6cc1a9e34 | ||
|
3e5c0cc4ed | ||
|
5fa3332266 | ||
|
66b7187c0e | ||
|
eb8ada5040 | ||
|
e34939f303 | ||
|
1e7f2f6514 | ||
|
d50f3b83e9 | ||
|
ce18aea5aa | ||
|
93d1c9841a | ||
|
d8beb754fa | ||
|
af78ebfcaf | ||
|
79c48c240b | ||
|
fe6e3c227f | ||
|
65f106541c | ||
|
428c75bbf2 | ||
|
d47ebbcb28 | ||
|
a1004405ca | ||
|
3efecdad74 | ||
|
be3c45b79d | ||
|
b830ab7085 | ||
|
be3e7c8ac1 | ||
|
a5af98b0f3 | ||
|
0b4c6325c7 | ||
|
e1b76c8e04 | ||
|
26bb7567de | ||
|
0814c99363 | ||
|
f23acb6788 | ||
|
813d0613c1 | ||
|
349efa4e43 | ||
|
86b0bb1b7a | ||
|
32b1272ff6 | ||
|
ab8461b6df | ||
|
a7008ecbd5 | ||
|
a76efc6e6f | ||
|
932c41d369 | ||
|
0ec2a44082 | ||
|
94b6d26ddd | ||
|
7bc677cf81 | ||
|
1f76f8529c | ||
|
afda8902df | ||
|
bb5b3f8bf9 | ||
|
64c63bf24f | ||
|
9dc569e646 | ||
|
00c3740af0 | ||
|
42f48bbaf4 | ||
|
63f160ff4a | ||
|
ff13d11934 | ||
|
2cec22e170 | ||
|
7a2c6c7e37 | ||
|
416de17876 | ||
|
ced346f776 | ||
|
030c869fec | ||
|
5ad73dcc1c | ||
|
32f9fc2afe | ||
|
d8609fd7b1 | ||
|
7ea4db486b | ||
|
31d9f5497e | ||
|
917aa5cf6f | ||
|
c7da419866 | ||
|
86b969fa26 | ||
|
618227a484 | ||
|
5c3bd72482 | ||
|
2ccb17a342 | ||
|
f98e9ffed2 | ||
|
58423863f5 | ||
|
f787feed05 | ||
|
c8abf1567f | ||
|
5847b6a28a | ||
|
0dda3d295f | ||
|
ea209776b3 | ||
|
e3d13df76a | ||
|
4c4da71f6a | ||
|
d11326ffde | ||
|
ed82e072d0 | ||
|
39039c842f | ||
|
35ebdd4118 | ||
|
1efe17dc9e | ||
|
b8dd271aec | ||
|
e90255539a | ||
|
35da7817d1 | ||
|
02f98c732c | ||
|
7fd9381629 | ||
|
0a3edba1ba | ||
|
9205c0b12b | ||
|
5da40e87a5 | ||
|
dc36f83a9f | ||
|
bef4c64c81 | ||
|
ff43574c87 | ||
|
e296609ff3 | ||
|
bb1ab05588 | ||
|
f5216d7e29 | ||
|
a389b6d9fa | ||
|
97f24c27bb | ||
|
66f905e8d7 | ||
|
b46065a800 | ||
|
a5d3d9162e | ||
|
b2efdfd4b2 | ||
|
bc3eb8e5ba | ||
|
c7d8035eab | ||
|
753f60b83d | ||
|
21acf7893d | ||
|
389643078d | ||
|
e95e31610e | ||
|
06904102a4 | ||
|
e89f70e399 | ||
|
4620de247c | ||
|
27102f810a | ||
|
12550fd524 | ||
|
53bad9c7c4 | ||
|
dd3d1fd82b | ||
|
ee52e616c0 | ||
|
3a393776a3 | ||
|
62e8fc02ed | ||
|
8184d468bc | ||
|
58b361ed45 | ||
|
2289bcaa87 | ||
|
23dc1c085c | ||
|
13052d6727 | ||
|
f07a5684d3 | ||
|
cee51300b2 | ||
|
a2be263a7b | ||
|
000ebd466a |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -3,4 +3,4 @@ updates:
|
||||
- package-ecosystem: "cargo" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
|
40
.github/workflows/lint.yml
vendored
40
.github/workflows/lint.yml
vendored
@ -1,9 +1,3 @@
|
||||
# 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
|
||||
@ -14,18 +8,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Run cargo check
|
||||
uses: actions-rs/cargo@v1
|
||||
continue-on-error: true # WARNING: only for this example, remove it!
|
||||
uses: clechasseur/rs-cargo@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
command: check
|
||||
|
||||
@ -34,18 +26,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
continue-on-error: true # WARNING: only for this example, remove it!
|
||||
uses: clechasseur/rs-cargo@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
command: test
|
||||
|
||||
@ -54,26 +44,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
continue-on-error: true # WARNING: only for this example, remove it!
|
||||
uses: clechasseur/rs-cargo@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- name: Run cargo clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
continue-on-error: true # WARNING: only for this example, remove it!
|
||||
uses: clechasseur/rs-cargo@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
|
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@ -9,24 +9,34 @@ jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/create-gh-release-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
draft: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
upload-assets:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-20.04
|
||||
- windows-latest
|
||||
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
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/upload-rust-binary-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
bin: alterware-launcher
|
||||
tar: unix
|
||||
zip: windows
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
1450
Cargo.lock
generated
1450
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@ -1,28 +1,40 @@
|
||||
[package]
|
||||
name = "alterware-launcher"
|
||||
version = "0.2.3"
|
||||
version = "0.6.10"
|
||||
edition = "2021"
|
||||
build = "res/build.rs"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
|
||||
panic = "abort"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
http_req = { version = "0.9.0", default-features = false, features = [
|
||||
"rust-tls",
|
||||
] }
|
||||
sha1_smol = "1.0.0"
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.114"
|
||||
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"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
steamlocate = "=2.0.0-beta.2"
|
||||
mslnk = "0.1.8"
|
||||
self-replace = "1.3.7"
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1.12"
|
||||
|
||||
[package.metadata.winres]
|
||||
OriginalFilename = "alterware-launcher.exe"
|
||||
FileDescription = "AlterWare mod updater & launcher"
|
||||
ProductName = "AlterWare Launcher"
|
||||
FileDescription = "AlterWare Launcher"
|
||||
ProductName = "AlterWare Launcher"
|
||||
|
97
README.md
97
README.md
@ -1,10 +1,95 @@
|
||||
# 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
|
||||
3. Run alterware-launcher.exe, after updating the game will launch automatically
|
||||
### [AlterWare.dev](https://alterware.dev)
|
||||
|
||||
##### IW4x | IW4-SP | IW5-Mod | IW6-Mod | S1-Mod
|
||||
|
||||
  
|
||||
|
||||
---
|
||||
|
||||
- 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
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
#### 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
|
||||
- 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"```
|
||||
|
||||
Some arguments can be set in alterware-launcher.json, args generally override the values of the config.
|
||||
|
||||
---
|
||||
|
||||
#### 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.
|
||||
|
||||
---
|
||||
|
||||
#### 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 __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).
|
||||
|
||||
The linux build does __not__ update itself.
|
32
client-args.md
Normal file
32
client-args.md
Normal file
@ -0,0 +1,32 @@
|
||||
# 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`)|
|
53
src/config.rs
Normal file
53
src/config.rs
Normal file
@ -0,0 +1,53 @@
|
||||
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,
|
||||
"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);
|
||||
}
|
24
src/github.rs
Normal file
24
src/github.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use semver::Version;
|
||||
|
||||
pub async fn latest_tag(owner: &str, repo: &str) -> String {
|
||||
let github_body = crate::http_async::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', "");
|
||||
Version::parse(&tag).unwrap()
|
||||
}
|
||||
|
||||
pub fn latest_release_url(owner: &str, repo: &str) -> String {
|
||||
format!("https://github.com/{}/{}/releases/latest", owner, repo)
|
||||
}
|
5
src/global.rs
Normal file
5
src/global.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub const MASTER: &str = "cdn.alterware.ovh";
|
||||
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";
|
25
src/http.rs
25
src/http.rs
@ -1,25 +0,0 @@
|
||||
use std::{fs, io::Write, path::Path, str};
|
||||
|
||||
pub fn get_body(url: &str) -> Vec<u8> {
|
||||
let mut res: Vec<u8> = Vec::new();
|
||||
http_req::request::get(url, &mut res).unwrap_or_else(|error| {
|
||||
panic!("\n\n{}:\n{:?}", "Error", error);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
127
src/http_async.rs
Normal file
127
src/http_async.rs
Normal file
@ -0,0 +1,127 @@
|
||||
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())
|
||||
}
|
52
src/iw4x.rs
Normal file
52
src/iw4x.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use crate::github;
|
||||
use crate::global::*;
|
||||
use crate::http_async;
|
||||
use crate::misc;
|
||||
|
||||
use colored::*;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
pub fn local_revision(dir: &Path) -> u16 {
|
||||
if let Ok(revision) = fs::read_to_string(dir.join(".iw4xrevision")) {
|
||||
misc::rev_to_int(&revision)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remote_revision() -> u16 {
|
||||
misc::rev_to_int(&github::latest_tag(GH_IW4X_OWNER, GH_IW4X_REPO).await)
|
||||
}
|
||||
|
||||
pub async fn update(dir: &Path) {
|
||||
let remote = remote_revision().await;
|
||||
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"))
|
||||
);
|
||||
http_async::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();
|
||||
}
|
768
src/main.rs
768
src/main.rs
@ -1,110 +1,620 @@
|
||||
mod http;
|
||||
use std::{fs, path::PathBuf};
|
||||
mod config;
|
||||
mod github;
|
||||
mod global;
|
||||
mod http_async;
|
||||
mod iw4x;
|
||||
mod misc;
|
||||
mod self_update;
|
||||
mod structs;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
struct CdnFile {
|
||||
name: String,
|
||||
size: u32,
|
||||
hash: String,
|
||||
}
|
||||
use global::*;
|
||||
use structs::*;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
struct Game<'a> {
|
||||
engine: &'a str,
|
||||
client: &'a str,
|
||||
references: Vec<&'a str>,
|
||||
}
|
||||
use colored::*;
|
||||
use indicatif::ProgressBar;
|
||||
#[cfg(windows)]
|
||||
use mslnk::ShellLink;
|
||||
use std::{borrow::Cow, collections::HashMap, env, fs, path::Path, path::PathBuf};
|
||||
#[cfg(windows)]
|
||||
use steamlocate::SteamDir;
|
||||
|
||||
const MASTER: &str = "https://master.alterware.dev";
|
||||
#[cfg(windows)]
|
||||
fn get_installed_games(games: &Vec<Game>) -> Vec<(u32, PathBuf)> {
|
||||
let mut installed_games = Vec::new();
|
||||
let steamdir_result = SteamDir::locate();
|
||||
|
||||
fn file_get_sha1(path: &PathBuf) -> String {
|
||||
let mut sha1 = sha1_smol::Sha1::new();
|
||||
sha1.update(&fs::read(path).unwrap());
|
||||
sha1.digest().to_string()
|
||||
}
|
||||
let steamdir = match steamdir_result {
|
||||
Ok(steamdir) => steamdir,
|
||||
Err(error) => {
|
||||
println!("Error locating Steam: {}", error);
|
||||
return installed_games;
|
||||
}
|
||||
};
|
||||
|
||||
fn update(game: &Game) {
|
||||
let cdn_info: Vec<CdnFile> = serde_json::from_str(&http::get_body_string(
|
||||
format!(
|
||||
"{}/files.json?{}",
|
||||
MASTER,
|
||||
rand::Rng::gen_range(&mut rand::thread_rng(), 0..1000)
|
||||
)
|
||||
.as_str(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let mut files_to_update: Vec<CdnFile> = Vec::new();
|
||||
for file in cdn_info {
|
||||
if file.name.starts_with(game.engine) {
|
||||
files_to_update.push(file);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
for file in files_to_update {
|
||||
let file_path = PathBuf::from(&file.name.replace(&format!("{}/", game.engine), ""));
|
||||
if file_path.exists() {
|
||||
let sha1_local = file_get_sha1(&file_path).to_lowercase();
|
||||
let sha1_remote = file.hash.to_lowercase();
|
||||
if sha1_local != sha1_remote {
|
||||
println!(
|
||||
"Updating {}...\nLocal hash: {}\nRemote hash: {}",
|
||||
file_path.display(),
|
||||
sha1_local,
|
||||
sha1_remote
|
||||
);
|
||||
http::download_file(
|
||||
&format!(
|
||||
"{}/{}?{}",
|
||||
MASTER,
|
||||
file.name,
|
||||
rand::Rng::gen_range(&mut rand::thread_rng(), 0..1000)
|
||||
),
|
||||
&file_path,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!("Downloading {}...", 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,
|
||||
rand::Rng::gen_range(&mut rand::thread_rng(), 0..1000)
|
||||
),
|
||||
&file_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.");
|
||||
}
|
||||
|
||||
for c in game.client.iter() {
|
||||
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(),
|
||||
c.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn setup_desktop_links(path: &Path, game: &Game) {
|
||||
println!("Create Desktop shortcut? (Y/n)");
|
||||
let input = misc::stdin().to_ascii_lowercase();
|
||||
|
||||
if input == "y" || input.is_empty() {
|
||||
let desktop = PathBuf::from(&format!("{}\\Desktop", env::var("USERPROFILE").unwrap()));
|
||||
|
||||
for c in game.client.iter() {
|
||||
create_shortcut(
|
||||
&desktop.join(format!("{}.lnk", c)),
|
||||
&path.join("alterware-launcher.exe"),
|
||||
path.join(format!("{}.exe", c))
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
c.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn launch(file_path: &PathBuf) {
|
||||
println!("Launching {}...", file_path.display());
|
||||
std::process::Command::new(file_path)
|
||||
.spawn()
|
||||
.unwrap()
|
||||
.wait()
|
||||
.unwrap();
|
||||
#[cfg(windows)]
|
||||
async fn auto_install(path: &Path, game: &Game<'_>, master_url: &String) {
|
||||
setup_client_links(game, path);
|
||||
setup_desktop_links(path, game);
|
||||
update(game, path, false, false, None, master_url, None).await;
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args: Vec<String> = std::env::args().collect();
|
||||
#[cfg(windows)]
|
||||
async fn windows_launcher_install(games: &Vec<Game<'_>>, master_url: &String) {
|
||||
println!(
|
||||
"{}",
|
||||
"No game specified/found. Checking for installed Steam games..".yellow()
|
||||
);
|
||||
let installed_games = get_installed_games(games);
|
||||
|
||||
let games_json = http::get_body_string(format!("{}/games.json", MASTER).as_str());
|
||||
let games: Vec<Game> = serde_json::from_str(&games_json).unwrap();
|
||||
if !installed_games.is_empty() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
for (id, path) in installed_games.iter() {
|
||||
if current_dir.starts_with(path) {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
println!("Installed games:");
|
||||
|
||||
for (id, path) in installed_games.iter() {
|
||||
println!("{}: {}", id, path.display());
|
||||
}
|
||||
|
||||
println!("Enter the ID of the game you want to install the AlterWare client for:");
|
||||
let input: u32 = misc::stdin().parse().unwrap();
|
||||
|
||||
for (id, path) in installed_games.iter() {
|
||||
if *id == input {
|
||||
let game = games.iter().find(|&g| g.app_id == input).unwrap();
|
||||
|
||||
let launcher_path = env::current_exe().unwrap();
|
||||
let target_path = path.join("alterware-launcher.exe");
|
||||
|
||||
if launcher_path != target_path {
|
||||
fs::copy(launcher_path, target_path).unwrap();
|
||||
println!("Launcher copied to {}", path.display());
|
||||
}
|
||||
auto_install(path, game, master_url).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();
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
println!(
|
||||
"No installed games found. Make sure to place the launcher in the game directory."
|
||||
);
|
||||
std::io::stdin().read_line(&mut String::new()).unwrap();
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// fn 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(
|
||||
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![];
|
||||
|
||||
for file in cdn_info {
|
||||
if !file.name.starts_with(&remote_dir_pre) || file.name == "iw4/iw4x.dll" {
|
||||
continue;
|
||||
}
|
||||
if skip_iw4x_sp && file.name == "iw4/iw4x-sp.exe" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sha1_remote = file.hash.to_lowercase();
|
||||
let file_name = &file.name.replace(remote_dir_pre.as_str(), "");
|
||||
let file_path = dir.join(file_name);
|
||||
if file_path.exists() {
|
||||
let 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 {
|
||||
files_to_download.push(file.clone());
|
||||
} else {
|
||||
pb.println(format!(
|
||||
"[{}] {}",
|
||||
"Checked".bright_blue(),
|
||||
misc::cute_path(&file_path)
|
||||
));
|
||||
hashes.insert(file_name.to_owned(), file.hash.to_lowercase());
|
||||
}
|
||||
} else {
|
||||
files_to_download.push(file.clone());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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());
|
||||
}
|
||||
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
|
||||
.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");
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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!("\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()
|
||||
.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);
|
||||
});
|
||||
|
||||
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> {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
#[cfg(windows)]
|
||||
setup_env();
|
||||
let mut args: Vec<String> = env::args().collect();
|
||||
|
||||
if arg_bool(&args, "--help") {
|
||||
println!("CLI Args:");
|
||||
println!(" <client>: Specify the client to launch");
|
||||
println!(" --help: Display this help message");
|
||||
println!(" --version: Display the launcher version");
|
||||
println!(" --path/-p <path>: Specify the game directory");
|
||||
println!(" --update/-u: Update only, don't launch the game");
|
||||
println!(" --bonus: Download bonus content");
|
||||
println!(" --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\""
|
||||
);
|
||||
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 = 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;
|
||||
} 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");
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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 mut game: String = String::new();
|
||||
if args.len() > 1 {
|
||||
@ -112,8 +622,26 @@ fn main() {
|
||||
} else {
|
||||
'main: for g in games.iter() {
|
||||
for r in g.references.iter() {
|
||||
if std::path::Path::new(r).exists() {
|
||||
game = String::from(g.client);
|
||||
if install_path.join(r).exists() {
|
||||
if g.client.len() > 1 {
|
||||
if cfg.update_only {
|
||||
game = String::from(g.client[0]);
|
||||
break 'main;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
setup_client_links(g, &env::current_dir().unwrap());
|
||||
|
||||
#[cfg(not(windows))]
|
||||
println!("Multiple clients installed, set the client as the first argument to launch a specific client.");
|
||||
println!("Select a client to launch:");
|
||||
for (i, c) in g.client.iter().enumerate() {
|
||||
println!("{}: {}", i, c);
|
||||
}
|
||||
game = String::from(g.client[misc::stdin().parse::<usize>().unwrap()]);
|
||||
break 'main;
|
||||
}
|
||||
game = String::from(g.client[0]);
|
||||
break 'main;
|
||||
}
|
||||
}
|
||||
@ -121,17 +649,63 @@ fn main() {
|
||||
}
|
||||
|
||||
for g in games.iter() {
|
||||
if g.client == game {
|
||||
update(g);
|
||||
if update_only {
|
||||
for c in g.client.iter() {
|
||||
if c == &game {
|
||||
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();
|
||||
cfg.download_bonus_content = input != "n";
|
||||
config::save_value(
|
||||
install_path.join("alterware-launcher.json"),
|
||||
"download_bonus_content",
|
||||
cfg.download_bonus_content,
|
||||
);
|
||||
config::save_value(
|
||||
install_path.join("alterware-launcher.json"),
|
||||
"ask_bonus_content",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
update(
|
||||
g,
|
||||
install_path.as_path(),
|
||||
cfg.download_bonus_content,
|
||||
cfg.force_update,
|
||||
Some(&game != "iw4x-sp"),
|
||||
&master_url,
|
||||
Some(ignore_required_files),
|
||||
)
|
||||
.await;
|
||||
if !cfg.update_only {
|
||||
launch(&install_path.join(format!("{}.exe", c)), &cfg.args);
|
||||
}
|
||||
return;
|
||||
}
|
||||
launch(&PathBuf::from(format!("{}.exe", g.client)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
println!("Game not found!");
|
||||
#[cfg(windows)]
|
||||
windows_launcher_install(&games, &master_url).await;
|
||||
|
||||
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();
|
||||
|
58
src/misc.rs
Normal file
58
src/misc.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
pub fn get_file_sha1(path: &PathBuf) -> String {
|
||||
let mut sha1 = sha1_smol::Sha1::new();
|
||||
sha1.update(&fs::read(path).unwrap());
|
||||
sha1.digest().to_string()
|
||||
}
|
||||
|
||||
pub fn stdin() -> String {
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).unwrap();
|
||||
input.trim().to_string()
|
||||
}
|
||||
|
||||
pub fn rev_to_int(rev: &str) -> u16 {
|
||||
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);
|
||||
}
|
||||
|
||||
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('\\', "/")
|
||||
}
|
111
src/self_update.rs
Normal file
111
src/self_update.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use crate::github;
|
||||
use crate::global::*;
|
||||
|
||||
use semver::Version;
|
||||
|
||||
pub async 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;
|
||||
|
||||
current_version < latest_version
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub async fn run(_update_only: bool) {
|
||||
if self_update_available().await {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
#[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) {
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::http_async;
|
||||
use crate::misc;
|
||||
|
||||
let working_dir = std::env::current_dir().unwrap();
|
||||
let files = fs::read_dir(&working_dir).unwrap();
|
||||
|
||||
for file in files {
|
||||
let file = file.unwrap();
|
||||
let file_name = file.file_name().into_string().unwrap();
|
||||
|
||||
if file_name.contains("alterware-launcher")
|
||||
&& (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.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self_update_available().await {
|
||||
println!("Performing launcher self-update.");
|
||||
println!(
|
||||
"If you run into any issues, please download the latest version at {}",
|
||||
github::latest_release_url(GH_OWNER, GH_REPO)
|
||||
);
|
||||
|
||||
let update_binary = PathBuf::from("alterware-launcher-update.exe");
|
||||
let file_path = working_dir.join(&update_binary);
|
||||
|
||||
if update_binary.exists() {
|
||||
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(
|
||||
&format!(
|
||||
"{}/download/{}",
|
||||
github::latest_release_url(GH_OWNER, GH_REPO),
|
||||
launcher_name
|
||||
),
|
||||
&file_path,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !file_path.exists() {
|
||||
println!("Failed to download launcher update.");
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
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::process::exit(201);
|
||||
}
|
||||
}
|
61
src/structs.rs
Normal file
61
src/structs.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Clone)]
|
||||
pub struct CdnFile {
|
||||
pub name: String,
|
||||
pub size: u32,
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
pub struct Game<'a> {
|
||||
pub engine: &'a str,
|
||||
pub client: Vec<&'a str>,
|
||||
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)]
|
||||
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,
|
||||
#[serde(default)]
|
||||
pub engine: String,
|
||||
#[serde(default)]
|
||||
pub use_https: bool,
|
||||
}
|
||||
|
||||
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::default(),
|
||||
engine: String::default(),
|
||||
use_https: true,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user