mirror of
https://github.com/alterware/alterware-launcher.git
synced 2026-03-26 12:03:04 +00:00
Compare commits
72 Commits
dependabot
...
v0.11.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
f4ac00d29f
|
|||
| b45ef12f5a | |||
| 2b61682464 | |||
| 7928e4dbfb | |||
| 863b9266b8 | |||
| cb2a278b14 | |||
| fb18d179b0 | |||
|
|
cc77ef778e | ||
|
|
a3cb1bd6aa | ||
|
|
24c35c577f | ||
|
|
ff4d75bc7c | ||
|
|
0b700bd825 | ||
| c8d8964a33 | |||
| 4f5f1bc1a5 | |||
|
|
c455278ca6 | ||
| 86a3587aa7 | |||
| 83d353ffba | |||
| 902fc70b56 | |||
| 9a9153fc93 | |||
| c4425b786e | |||
| 3d609ff570 | |||
|
|
6be4c1a8e5 | ||
|
|
f1cd1f225d | ||
|
|
ec221ed543 | ||
|
|
b8cec3a404 | ||
|
|
a17e47b97d | ||
|
|
24d66aab75 | ||
|
|
4e7f2fa03b | ||
|
|
f9a64a9a45 | ||
|
|
72ac6d56aa | ||
|
|
695a8174c8 | ||
| 36aab56037 | |||
| 79477bf231 | |||
| 3d9bb6c424 | |||
| 334e989c2d | |||
|
|
b4b6c93e3d | ||
|
|
b658ac5ae1 | ||
|
|
57a3c315bf | ||
|
|
3717f1315e | ||
| 9442c733f9 | |||
| 71a3ccdf47 | |||
| 2454b25d56 | |||
| c8155761f2 | |||
| ab872bd2ab | |||
|
|
7f977ea680 | ||
|
|
cfa3629fa0 | ||
|
|
0b35a865b4 | ||
|
|
cc62261b2f | ||
|
|
cfc9826fb0 | ||
| 066311f7bb | |||
| d9b7f7fc07 | |||
| 1f7142b30b | |||
| b69f2e3338 | |||
|
|
2cd227fec4 | ||
|
|
c9cb2c3604 | ||
|
|
2c2184a8b3 | ||
|
|
609bfde418 | ||
|
|
a2e4f304dc | ||
|
|
2dab50111b | ||
| 27795f6680 | |||
| 39db6099e4 | |||
| 0493a7a4d8 | |||
| bb9ebcf830 | |||
| 1e2dca0162 | |||
|
|
d4e7cb1eb1 | ||
|
|
cb2f433c39 | ||
|
|
764a892f71 | ||
|
|
66554a0374 | ||
|
|
e96e0ca630 | ||
|
|
a30d6950c8 | ||
|
|
6f241dffe2 | ||
|
|
806758088d |
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install stable toolchain
|
- name: Install stable toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install stable toolchain
|
- name: Install stable toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install stable toolchain
|
- name: Install stable toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
|
|||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -9,8 +9,8 @@ jobs:
|
|||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: taiki-e/create-gh-release-action@v1.9.1
|
- uses: taiki-e/create-gh-release-action@v1.9.2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -29,8 +29,8 @@ jobs:
|
|||||||
os: windows-latest
|
os: windows-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: taiki-e/upload-rust-binary-action@v1.26.0
|
- uses: taiki-e/upload-rust-binary-action@v1.28.0
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
bin: alterware-launcher
|
bin: alterware-launcher
|
||||||
|
|||||||
725
Cargo.lock
generated
725
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "alterware-launcher"
|
name = "alterware-launcher"
|
||||||
version = "0.11.1"
|
version = "0.11.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "res/build.rs"
|
build = "res/build.rs"
|
||||||
|
|
||||||
@@ -18,12 +18,12 @@ rand = "0.9"
|
|||||||
semver = "1.0"
|
semver = "1.0"
|
||||||
colored = "3.0"
|
colored = "3.0"
|
||||||
once_cell = "1.21"
|
once_cell = "1.21"
|
||||||
reqwest = { version = "0.12", features = ["stream"] }
|
reqwest = { version = "0.13", features = ["stream"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
indicatif = "0.17"
|
indicatif = "0.18"
|
||||||
tokio = { version="1.45", features = ["rt-multi-thread", "macros"] }
|
tokio = { version="1.47", features = ["rt-multi-thread", "macros"] }
|
||||||
simple-log = "2.3"
|
simple-log = "2.4"
|
||||||
walkdir = "2.5"
|
walkdir = "2.5"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
@@ -37,11 +37,11 @@ self-replace = "1.5"
|
|||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
winresource = "0.1"
|
winresource = "0.1"
|
||||||
static_vcruntime = "2.0"
|
static_vcruntime = "3.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
strip-ansi-escapes = "0.2"
|
strip-ansi-escapes = "0.2"
|
||||||
serial_test = "3.2"
|
serial_test = "3.4"
|
||||||
|
|
||||||
[package.metadata.winresource]
|
[package.metadata.winresource]
|
||||||
OriginalFilename = "alterware-launcher.exe"
|
OriginalFilename = "alterware-launcher.exe"
|
||||||
|
|||||||
@@ -146,11 +146,8 @@
|
|||||||
- Install or reinstall redistributables
|
- Install or reinstall redistributables
|
||||||
- ```--prerelease```
|
- ```--prerelease```
|
||||||
- Update to prerelease version of the launcher
|
- Update to prerelease version of the launcher
|
||||||
- ```--rate```
|
|
||||||
- Rate and display CDN servers
|
|
||||||
- ```--cdn-url```
|
- ```--cdn-url```
|
||||||
- ```--offline```
|
- ```--offline```
|
||||||
- ```--skip-connectivity-check```
|
|
||||||
|
|
||||||
|
|
||||||
##### Example:
|
##### Example:
|
||||||
@@ -175,7 +172,6 @@ alterware-launcher.exe iw6 --bonus -u --path "C:\Games\IW6x" --pass "-headless"
|
|||||||
- `prerelease`: Update to prerelease version of clients and launcher. Default: `false`.
|
- `prerelease`: Update to prerelease version of clients and launcher. Default: `false`.
|
||||||
- `cdn_url`
|
- `cdn_url`
|
||||||
- `offline`
|
- `offline`
|
||||||
- `skip-connectivity-check`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub fn save_cache(dir: &Path, cache: Cache) {
|
|||||||
let cache_path = dir.join("awcache.json");
|
let cache_path = dir.join("awcache.json");
|
||||||
let cache_serialized = serde_json::to_string_pretty(&cache).unwrap();
|
let cache_serialized = serde_json::to_string_pretty(&cache).unwrap();
|
||||||
fs::write(cache_path, cache_serialized).unwrap_or_else(|e| {
|
fs::write(cache_path, cache_serialized).unwrap_or_else(|e| {
|
||||||
error!("Failed to save cache: {}", e);
|
error!("Failed to save cache: {e}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
184
src/cdn.rs
184
src/cdn.rs
@@ -6,93 +6,19 @@ use std::time::Duration;
|
|||||||
|
|
||||||
static CURRENT_CDN: Mutex<Option<Arc<Server>>> = Mutex::new(None);
|
static CURRENT_CDN: Mutex<Option<Arc<Server>>> = Mutex::new(None);
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Region {
|
|
||||||
Africa,
|
|
||||||
Asia,
|
|
||||||
Europe,
|
|
||||||
NorthAmerica,
|
|
||||||
Oceania,
|
|
||||||
SouthAmerica,
|
|
||||||
Global,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Region {
|
|
||||||
pub fn from_str(region_str: &str) -> Self {
|
|
||||||
match region_str {
|
|
||||||
"Africa" => Region::Africa,
|
|
||||||
"Asia" => Region::Asia,
|
|
||||||
"Europe" => Region::Europe,
|
|
||||||
"NorthAmerica" => Region::NorthAmerica,
|
|
||||||
"Oceania" => Region::Oceania,
|
|
||||||
"SouthAmerica" => Region::SouthAmerica,
|
|
||||||
_ => Region::Unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn coordinates(&self) -> Option<(f64, f64)> {
|
|
||||||
match self {
|
|
||||||
Region::Europe => Some((54.0, 15.0)),
|
|
||||||
Region::Asia => Some((35.0, 105.0)),
|
|
||||||
Region::NorthAmerica => Some((45.0, -100.0)),
|
|
||||||
Region::SouthAmerica => Some((-15.0, -60.0)),
|
|
||||||
Region::Africa => Some((0.0, 20.0)),
|
|
||||||
Region::Oceania => Some((-25.0, 140.0)),
|
|
||||||
Region::Global => None,
|
|
||||||
Region::Unknown => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn distance_to(&self, other: Region) -> f64 {
|
|
||||||
if *self == Region::Global || other == Region::Global {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if *self == other {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (lat1, lon1) = match self.coordinates() {
|
|
||||||
Some(coords) => coords,
|
|
||||||
None => return 20000.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let (lat2, lon2) = match other.coordinates() {
|
|
||||||
Some(coords) => coords,
|
|
||||||
None => return 20000.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// haversine
|
|
||||||
let r = 6371.0;
|
|
||||||
let d_lat = (lat2 - lat1).to_radians();
|
|
||||||
let d_lon = (lon2 - lon1).to_radians();
|
|
||||||
let lat1_rad = lat1.to_radians();
|
|
||||||
let lat2_rad = lat2.to_radians();
|
|
||||||
|
|
||||||
let a = (d_lat / 2.0).sin().powi(2)
|
|
||||||
+ lat1_rad.cos() * lat2_rad.cos() * (d_lon / 2.0).sin().powi(2);
|
|
||||||
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
|
|
||||||
|
|
||||||
r * c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
pub host: &'static str,
|
pub host: &'static str,
|
||||||
pub rating: u8,
|
pub rating: u8,
|
||||||
pub latency: Option<std::time::Duration>,
|
pub latency: Option<std::time::Duration>,
|
||||||
pub region: Region,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
pub const fn new(host: &'static str, region: Region) -> Self {
|
pub const fn new(host: &'static str) -> Self {
|
||||||
Server {
|
Server {
|
||||||
host,
|
host,
|
||||||
rating: 255,
|
rating: 255,
|
||||||
latency: None,
|
latency: None,
|
||||||
region,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +26,7 @@ impl Server {
|
|||||||
format!("https://{}/", self.host)
|
format!("https://{}/", self.host)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rate(&mut self, asn: u32, user_region: Region, is_initial: bool) {
|
async fn rate(&mut self, asn: u32, is_initial: bool) {
|
||||||
let timeout = if is_initial {
|
let timeout = if is_initial {
|
||||||
Duration::from_millis(1000)
|
Duration::from_millis(1000)
|
||||||
} else {
|
} else {
|
||||||
@@ -109,16 +35,15 @@ impl Server {
|
|||||||
match http::rating_request(&self.url(), timeout).await {
|
match http::rating_request(&self.url(), timeout).await {
|
||||||
Ok((latency, is_cloudflare)) => {
|
Ok((latency, is_cloudflare)) => {
|
||||||
self.latency = Some(latency);
|
self.latency = Some(latency);
|
||||||
self.rating = self.calculate_rating(latency, is_cloudflare, asn, user_region);
|
self.rating = self.calculate_rating(latency, is_cloudflare, asn);
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Server {} rated {} ({}ms, rating: {}, cloudflare: {}, region: {:?})",
|
"Server {} rated {} ({}ms, rating: {}, cloudflare: {})",
|
||||||
self.host,
|
self.host,
|
||||||
self.rating,
|
self.rating,
|
||||||
latency.as_millis(),
|
latency.as_millis(),
|
||||||
self.rating,
|
self.rating,
|
||||||
is_cloudflare,
|
is_cloudflare,
|
||||||
self.region
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -147,13 +72,7 @@ impl Server {
|
|||||||
rating.clamp(1.0, 255.0) as u8
|
rating.clamp(1.0, 255.0) as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_rating(
|
fn calculate_rating(&self, latency: std::time::Duration, is_cloudflare: bool, asn: u32) -> u8 {
|
||||||
&self,
|
|
||||||
latency: std::time::Duration,
|
|
||||||
is_cloudflare: bool,
|
|
||||||
asn: u32,
|
|
||||||
user_region: Region,
|
|
||||||
) -> u8 {
|
|
||||||
let mut rating = self.rate_latency(latency);
|
let mut rating = self.rate_latency(latency);
|
||||||
|
|
||||||
// Additional factors for full rating
|
// Additional factors for full rating
|
||||||
@@ -165,25 +84,6 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let distance_km = user_region.distance_to(self.region);
|
|
||||||
let region_multiplier = if self.region == Region::Global {
|
|
||||||
1.4
|
|
||||||
} else if distance_km == 0.0 {
|
|
||||||
1.3
|
|
||||||
} else if user_region == Region::Unknown {
|
|
||||||
1.0
|
|
||||||
} else if distance_km <= 2000.0 {
|
|
||||||
1.25
|
|
||||||
} else if distance_km <= 5000.0 {
|
|
||||||
1.15
|
|
||||||
} else if distance_km <= 10000.0 {
|
|
||||||
1.05
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
rating = (rating as f32 * region_multiplier).min(255.0) as u8;
|
|
||||||
|
|
||||||
rating
|
rating
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,23 +98,13 @@ impl Hosts {
|
|||||||
pub async fn new() -> Self {
|
pub async fn new() -> Self {
|
||||||
let cdn_hosts = crate::global::CDN_HOSTS.to_vec();
|
let cdn_hosts = crate::global::CDN_HOSTS.to_vec();
|
||||||
|
|
||||||
let mut hosts = Hosts {
|
let hosts = Hosts {
|
||||||
servers: cdn_hosts,
|
servers: cdn_hosts,
|
||||||
active_index: RwLock::new(None),
|
active_index: RwLock::new(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (asn, region_str) = crate::http::get_location_info().await;
|
|
||||||
let user_region = Region::from_str(®ion_str);
|
|
||||||
info!(
|
|
||||||
"Detected user region as {:?} (region: {})",
|
|
||||||
user_region, region_str
|
|
||||||
);
|
|
||||||
|
|
||||||
hosts.rate(asn, user_region, true).await;
|
|
||||||
|
|
||||||
if hosts.servers.iter().all(|server| server.rating == 0) {
|
if hosts.servers.iter().all(|server| server.rating == 0) {
|
||||||
info!("All CDN servers failed with 1000ms timeout, retrying with 5000ms timeout");
|
info!("All CDN servers failed with 1000ms timeout, retrying with 5000ms timeout");
|
||||||
hosts.rate(asn, user_region, false).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts
|
hosts
|
||||||
@@ -250,11 +140,11 @@ impl Hosts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// rate and order all servers, then select the best one
|
/// rate and order all servers, then select the best one
|
||||||
pub async fn rate(&mut self, asn: u32, user_region: Region, is_initial: bool) {
|
pub async fn rate(&mut self, asn: u32, is_initial: bool) {
|
||||||
let rating_futures: Vec<_> = self
|
let rating_futures: Vec<_> = self
|
||||||
.servers
|
.servers
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.map(|server| server.rate(asn, user_region, is_initial))
|
.map(|server| server.rate(asn, is_initial))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
join_all(rating_futures).await;
|
join_all(rating_futures).await;
|
||||||
@@ -270,61 +160,3 @@ impl Hosts {
|
|||||||
self.active_url()
|
self.active_url()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CDN rating function for --rate flag
|
|
||||||
pub async fn rate_cdns_and_display() {
|
|
||||||
use colored::Colorize;
|
|
||||||
|
|
||||||
let (asn, region_str) = crate::http::get_location_info().await;
|
|
||||||
let user_region = Region::from_str(®ion_str);
|
|
||||||
|
|
||||||
if user_region == Region::Unknown {
|
|
||||||
println!(
|
|
||||||
"User region: {} (using Global server preference)",
|
|
||||||
"Unknown".bright_red()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!("User region: {:?}", user_region);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Rating CDNs...");
|
|
||||||
|
|
||||||
let cdn_hosts = crate::global::CDN_HOSTS.to_vec();
|
|
||||||
|
|
||||||
let mut hosts = Hosts {
|
|
||||||
servers: cdn_hosts,
|
|
||||||
active_index: RwLock::new(None),
|
|
||||||
};
|
|
||||||
|
|
||||||
hosts.rate(asn, user_region, true).await;
|
|
||||||
|
|
||||||
if hosts.servers.iter().all(|server| server.rating == 0) {
|
|
||||||
println!("Retrying with longer timeout...");
|
|
||||||
hosts.rate(asn, user_region, false).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
for server in hosts.servers.iter() {
|
|
||||||
let latency_str = server
|
|
||||||
.latency
|
|
||||||
.map_or("timeout".to_string(), |l| format!("{} ms", l.as_millis()));
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"{}: rating {}, latency {}",
|
|
||||||
server.host.bright_white(),
|
|
||||||
server.rating.to_string().bright_cyan(),
|
|
||||||
latency_str
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show selected CDN
|
|
||||||
if hosts.next() {
|
|
||||||
if let Some(best_url) = hosts.active_url() {
|
|
||||||
println!();
|
|
||||||
println!("Selected: {}", best_url.bright_green());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println!();
|
|
||||||
println!("{}", "No available CDN servers".bright_red());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,17 +17,13 @@ pub async fn latest_tag_full(
|
|||||||
repo: &str,
|
repo: &str,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let github_body = crate::http_async::get_body_string(
|
let github_body = crate::http_async::get_body_string(
|
||||||
format!(
|
format!("https://api.github.com/repos/{owner}/{repo}/releases/latest").as_str(),
|
||||||
"https://api.github.com/repos/{}/{}/releases/latest",
|
|
||||||
owner, repo
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to fetch GitHub API: {}", e))?;
|
.map_err(|e| format!("Failed to fetch GitHub API: {e}"))?;
|
||||||
|
|
||||||
let github_json: serde_json::Value = serde_json::from_str(&github_body)
|
let github_json: serde_json::Value = serde_json::from_str(&github_body)
|
||||||
.map_err(|e| format!("Failed to parse GitHub API response: {}", e))?;
|
.map_err(|e| format!("Failed to parse GitHub API response: {e}"))?;
|
||||||
|
|
||||||
let tag_name = github_json
|
let tag_name = github_json
|
||||||
.get("tag_name")
|
.get("tag_name")
|
||||||
@@ -43,13 +39,13 @@ pub async fn latest_tag_prerelease(
|
|||||||
repo: &str,
|
repo: &str,
|
||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
let github_body = crate::http_async::get_body_string(
|
let github_body = crate::http_async::get_body_string(
|
||||||
format!("https://api.github.com/repos/{}/{}/releases", owner, repo).as_str(),
|
format!("https://api.github.com/repos/{owner}/{repo}/releases").as_str(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to fetch GitHub API: {}", e))?;
|
.map_err(|e| format!("Failed to fetch GitHub API: {e}"))?;
|
||||||
|
|
||||||
let github_json: serde_json::Value = serde_json::from_str(&github_body)
|
let github_json: serde_json::Value = serde_json::from_str(&github_body)
|
||||||
.map_err(|e| format!("Failed to parse GitHub API response: {}", e))?;
|
.map_err(|e| format!("Failed to parse GitHub API response: {e}"))?;
|
||||||
|
|
||||||
let latest_release = github_json.get(0).ok_or("No releases found")?;
|
let latest_release = github_json.get(0).ok_or("No releases found")?;
|
||||||
|
|
||||||
@@ -70,7 +66,7 @@ pub async fn latest_version(
|
|||||||
let tag = latest_tag(owner, repo, prerelease).await?;
|
let tag = latest_tag(owner, repo, prerelease).await?;
|
||||||
let cleaned_tag = tag.replace('v', "");
|
let cleaned_tag = tag.replace('v', "");
|
||||||
Version::parse(&cleaned_tag)
|
Version::parse(&cleaned_tag)
|
||||||
.map_err(|e| format!("Failed to parse version '{}': {}", cleaned_tag, e).into())
|
.map_err(|e| format!("Failed to parse version '{cleaned_tag}': {e}").into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn download_url(owner: &str, repo: &str, tag: Option<&str>) -> String {
|
pub fn download_url(owner: &str, repo: &str, tag: Option<&str>) -> String {
|
||||||
|
|||||||
@@ -6,20 +6,13 @@ use std::future::Future;
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use crate::cdn::{Hosts, Region, Server};
|
use crate::cdn::{Hosts, Server};
|
||||||
|
|
||||||
pub const GH_OWNER: &str = "alterware";
|
pub const GH_OWNER: &str = "alterware";
|
||||||
pub const GH_REPO: &str = "alterware-launcher";
|
pub const GH_REPO: &str = "alterware-launcher";
|
||||||
pub const GH_IW4X_OWNER: &str = "iw4x";
|
|
||||||
pub const GH_IW4X_REPO: &str = "iw4x-client";
|
|
||||||
pub const DEFAULT_MASTER: &str = "https://cdn.alterware.ovh";
|
pub const DEFAULT_MASTER: &str = "https://cdn.alterware.ovh";
|
||||||
|
|
||||||
pub const CDN_HOSTS: [Server; 2] = [
|
pub const CDN_HOSTS: [Server; 1] = [Server::new("cdn.alterware.ovh")];
|
||||||
Server::new("cdn.alterware.ovh", Region::Global),
|
|
||||||
Server::new("us-cdn.alterware.ovh", Region::NorthAmerica),
|
|
||||||
];
|
|
||||||
|
|
||||||
pub const IP2ASN: &str = "https://ip2asn.getserve.rs/v1/as/ip/self";
|
|
||||||
|
|
||||||
pub static USER_AGENT: Lazy<String> = Lazy::new(|| {
|
pub static USER_AGENT: Lazy<String> = Lazy::new(|| {
|
||||||
format!(
|
format!(
|
||||||
@@ -33,8 +26,6 @@ pub static USER_AGENT: Lazy<String> = Lazy::new(|| {
|
|||||||
|
|
||||||
pub static MASTER_URL: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::from(DEFAULT_MASTER)));
|
pub static MASTER_URL: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::from(DEFAULT_MASTER)));
|
||||||
|
|
||||||
pub static IS_OFFLINE: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
|
|
||||||
|
|
||||||
pub static PREFIXES: Lazy<HashMap<&'static str, PrintPrefix>> = Lazy::new(|| {
|
pub static PREFIXES: Lazy<HashMap<&'static str, PrintPrefix>> = Lazy::new(|| {
|
||||||
HashMap::from([
|
HashMap::from([
|
||||||
(
|
(
|
||||||
@@ -81,54 +72,3 @@ pub static PREFIXES: Lazy<HashMap<&'static str, PrintPrefix>> = Lazy::new(|| {
|
|||||||
),
|
),
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
pub async fn check_connectivity_and_rate_cdns() -> Pin<Box<dyn Future<Output = bool> + Send>> {
|
|
||||||
Box::pin(async move {
|
|
||||||
crate::println_info!("Initializing CDN rating system...");
|
|
||||||
let hosts = Hosts::new().await;
|
|
||||||
let best_cdn = hosts.get_master_url();
|
|
||||||
|
|
||||||
if let Some(cdn_url) = best_cdn {
|
|
||||||
let cdn_url = cdn_url.trim_end_matches('/');
|
|
||||||
*MASTER_URL.lock().unwrap() = cdn_url.to_string();
|
|
||||||
crate::println_info!("Selected CDN: {}", cdn_url);
|
|
||||||
|
|
||||||
match crate::http_async::get_body_string(cdn_url).await {
|
|
||||||
Ok(_) => {
|
|
||||||
info!("Successfully connected to CDN: {}", cdn_url);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to connect to selected CDN {}: {}", cdn_url, e);
|
|
||||||
*IS_OFFLINE.lock().unwrap() = true;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
crate::println_error!("No CDN hosts are available");
|
|
||||||
*IS_OFFLINE.lock().unwrap() = true;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_connectivity(
|
|
||||||
master_url: Option<String>,
|
|
||||||
) -> Pin<Box<dyn Future<Output = bool> + Send>> {
|
|
||||||
Box::pin(async move {
|
|
||||||
if let Some(url) = master_url {
|
|
||||||
*MASTER_URL.lock().unwrap() = url.clone();
|
|
||||||
crate::println_info!("Using fallback connectivity check on {}", url);
|
|
||||||
|
|
||||||
match crate::http_async::get_body_string(&url).await {
|
|
||||||
Ok(_) => true,
|
|
||||||
Err(_) => {
|
|
||||||
*IS_OFFLINE.lock().unwrap() = true;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
check_connectivity_and_rate_cdns().await.await
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
43
src/http.rs
43
src/http.rs
@@ -1,5 +1,4 @@
|
|||||||
use reqwest::header::HeaderMap;
|
use reqwest::header::HeaderMap;
|
||||||
use serde_json::Value;
|
|
||||||
use simple_log::*;
|
use simple_log::*;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -17,18 +16,18 @@ pub async fn quick_request(url: &str) -> Result<String, Box<dyn std::error::Erro
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Err(e) = &res {
|
if let Err(e) = &res {
|
||||||
error!("Failed to get {}: {}", url, e);
|
error!("Failed to get {url}: {e}");
|
||||||
return Err(format!("Failed to get {} {}", url, e).into());
|
return Err(format!("Failed to get {url} {e}").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = res.unwrap();
|
let res = res.unwrap();
|
||||||
match res.text().await {
|
match res.text().await {
|
||||||
Ok(text) => {
|
Ok(text) => {
|
||||||
info!("Successfully received response from: {}", url);
|
info!("Successfully received response from: {url}");
|
||||||
Ok(text)
|
Ok(text)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to get response text from {}: {}", url, e);
|
warn!("Failed to get response text from {url}: {e}");
|
||||||
Err(e.into())
|
Err(e.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,8 +63,8 @@ pub async fn rating_request(
|
|||||||
let latency = start.elapsed();
|
let latency = start.elapsed();
|
||||||
|
|
||||||
if let Err(e) = &res {
|
if let Err(e) = &res {
|
||||||
error!("Failed to get {}: {} (after {:?})", url, e, latency);
|
error!("Failed to get {url}: {e} (after {latency:?})");
|
||||||
return Err(format!("Failed to get {} {} (after {:?})", url, e, latency).into());
|
return Err(format!("Failed to get {url} {e} (after {latency:?})").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let res = res.unwrap();
|
let res = res.unwrap();
|
||||||
@@ -74,36 +73,10 @@ pub async fn rating_request(
|
|||||||
|
|
||||||
// We don't need the response body for rating
|
// We don't need the response body for rating
|
||||||
if let Err(e) = res.text().await {
|
if let Err(e) = res.text().await {
|
||||||
warn!(
|
warn!("Failed to get response text from {url}: {e} (after {latency:?})");
|
||||||
"Failed to get response text from {}: {} (after {:?})",
|
|
||||||
url, e, latency
|
|
||||||
);
|
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(
|
info!("Successfully rated {url} in {latency:?} (cloudflare: {is_cloudflare})");
|
||||||
"Successfully rated {} in {:?} (cloudflare: {})",
|
|
||||||
url, latency, is_cloudflare
|
|
||||||
);
|
|
||||||
Ok((latency, is_cloudflare))
|
Ok((latency, is_cloudflare))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve client ASN and region
|
|
||||||
pub async fn get_location_info() -> (u32, String) {
|
|
||||||
let response = quick_request(crate::global::IP2ASN).await;
|
|
||||||
if let Ok(as_data_str) = response {
|
|
||||||
if let Ok(as_data) = serde_json::from_str::<Value>(&as_data_str) {
|
|
||||||
let as_number = as_data
|
|
||||||
.get("as_number")
|
|
||||||
.and_then(|v| v.as_u64())
|
|
||||||
.unwrap_or(0) as u32;
|
|
||||||
let region = as_data
|
|
||||||
.get("region")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("Unknown")
|
|
||||||
.to_string();
|
|
||||||
return (as_number, region);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(0, "Unknown".to_string())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,10 +32,14 @@ pub async fn download_file_progress(
|
|||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Failed to GET from '{}': {}", url, e);
|
error!("Failed to GET from '{url}': {e}");
|
||||||
format!("Failed to GET from '{url}'")
|
format!("Failed to GET from '{url}'")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
if !res.status().is_success() {
|
||||||
|
return Err(format!("Request failed with status: {}", res.status()));
|
||||||
|
}
|
||||||
|
|
||||||
let total_size = res.content_length().unwrap_or(size);
|
let total_size = res.content_length().unwrap_or(size);
|
||||||
debug!("Download size: {}", misc::human_readable_bytes(total_size));
|
debug!("Download size: {}", misc::human_readable_bytes(total_size));
|
||||||
pb.set_length(total_size);
|
pb.set_length(total_size);
|
||||||
@@ -89,14 +93,18 @@ pub async fn get_body(url: &str) -> Result<Vec<u8>, String> {
|
|||||||
)
|
)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
.map_err(|e| format!("Failed to send request: {e}"))?;
|
||||||
|
|
||||||
debug!("{} {url}", res.status());
|
debug!("{} {url}", res.status());
|
||||||
|
|
||||||
|
if !res.status().is_success() {
|
||||||
|
return Err(format!("Request failed with status: {}", res.status()));
|
||||||
|
}
|
||||||
|
|
||||||
res.bytes()
|
res.bytes()
|
||||||
.await
|
.await
|
||||||
.map(|b| b.to_vec())
|
.map(|b| b.to_vec())
|
||||||
.map_err(|e| format!("Failed to get body: {}", e))
|
.map_err(|e| format!("Failed to get body: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_body_string(url: &str) -> Result<String, String> {
|
pub async fn get_body_string(url: &str) -> Result<String, String> {
|
||||||
@@ -119,7 +127,7 @@ pub async fn get_json<T: serde::de::DeserializeOwned>(url: &str) -> Result<T, St
|
|||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to send request: {}", e))?;
|
.map_err(|e| format!("Failed to send request: {e}"))?;
|
||||||
|
|
||||||
debug!("{} {}", res.status(), url);
|
debug!("{} {}", res.status(), url);
|
||||||
|
|
||||||
@@ -130,7 +138,7 @@ pub async fn get_json<T: serde::de::DeserializeOwned>(url: &str) -> Result<T, St
|
|||||||
let body = res
|
let body = res
|
||||||
.bytes()
|
.bytes()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to read response body: {}", e))?;
|
.map_err(|e| format!("Failed to read response body: {e}"))?;
|
||||||
|
|
||||||
serde_json::from_slice::<T>(&body).map_err(|e| format!("Failed to parse JSON: {}", e))
|
serde_json::from_slice::<T>(&body).map_err(|e| format!("Failed to parse JSON: {e}"))
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/iw4x.rs
46
src/iw4x.rs
@@ -1,46 +0,0 @@
|
|||||||
use crate::extend::*;
|
|
||||||
use crate::github;
|
|
||||||
use crate::global::*;
|
|
||||||
use crate::http_async;
|
|
||||||
use crate::misc;
|
|
||||||
use crate::structs;
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
pub async fn remote_revision(prerelease: Option<bool>) -> u16 {
|
|
||||||
match github::latest_tag(GH_IW4X_OWNER, GH_IW4X_REPO, prerelease).await {
|
|
||||||
Ok(tag) => misc::rev_to_int(&tag),
|
|
||||||
Err(_) => {
|
|
||||||
crate::println_error!("Failed to get latest version for {GH_IW4X_OWNER}/{GH_IW4X_REPO}, assuming we are up to date.");
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update(dir: &Path, cache: &mut structs::Cache, prerelease: Option<bool>) {
|
|
||||||
let remote = remote_revision(prerelease).await;
|
|
||||||
let local = misc::rev_to_int(&cache.iw4x_revision);
|
|
||||||
|
|
||||||
if remote <= local && dir.join("iw4x.dll").exists() {
|
|
||||||
crate::println_info!("No files to download for IW4x");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
crate::println_info!("Downloading outdated or missing files for IW4x",);
|
|
||||||
println!(
|
|
||||||
"{}{}",
|
|
||||||
misc::prefix("downloading"),
|
|
||||||
dir.join("iw4x.dll").cute_path()
|
|
||||||
);
|
|
||||||
http_async::download_file(
|
|
||||||
&format!(
|
|
||||||
"{}/iw4x.dll",
|
|
||||||
github::download_url(GH_IW4X_OWNER, GH_IW4X_REPO, Some(&format!("r{remote}")))
|
|
||||||
),
|
|
||||||
&dir.join("iw4x.dll"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
cache.iw4x_revision = format!("r{remote}");
|
|
||||||
}
|
|
||||||
125
src/main.rs
125
src/main.rs
@@ -6,7 +6,6 @@ mod github;
|
|||||||
mod global;
|
mod global;
|
||||||
mod http;
|
mod http;
|
||||||
mod http_async;
|
mod http_async;
|
||||||
mod iw4x;
|
|
||||||
mod misc;
|
mod misc;
|
||||||
mod self_update;
|
mod self_update;
|
||||||
mod structs;
|
mod structs;
|
||||||
@@ -28,7 +27,7 @@ use mslnk::ShellLink;
|
|||||||
use simple_log::LogConfigBuilder;
|
use simple_log::LogConfigBuilder;
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{HashMap, HashSet},
|
collections::HashMap,
|
||||||
env, fs,
|
env, fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
@@ -117,7 +116,7 @@ fn setup_desktop_links(path: &Path, game: &Game) {
|
|||||||
async fn auto_install(path: &Path, game: &Game<'_>) {
|
async fn auto_install(path: &Path, game: &Game<'_>) {
|
||||||
setup_client_links(game, path);
|
setup_client_links(game, path);
|
||||||
setup_desktop_links(path, game);
|
setup_desktop_links(path, game);
|
||||||
update(game, path, false, false, None, None, None).await;
|
update(game, path, false, false, None).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -187,7 +186,7 @@ fn total_download_size(cdn_info: &Vec<CdnFile>, remote_dir: &str) -> u64 {
|
|||||||
let remote_dir = format!("{remote_dir}/");
|
let remote_dir = format!("{remote_dir}/");
|
||||||
let mut size: u64 = 0;
|
let mut size: u64 = 0;
|
||||||
for file in cdn_info {
|
for file in cdn_info {
|
||||||
if !file.name.starts_with(&remote_dir) || file.name == "iw4/iw4x.dll" {
|
if !file.name.starts_with(&remote_dir) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
size += file.size as u64;
|
size += file.size as u64;
|
||||||
@@ -201,7 +200,6 @@ async fn update_dir(
|
|||||||
dir: &Path,
|
dir: &Path,
|
||||||
hashes: &mut HashMap<String, String>,
|
hashes: &mut HashMap<String, String>,
|
||||||
pb: &ProgressBar,
|
pb: &ProgressBar,
|
||||||
skip_iw4x_sp: bool,
|
|
||||||
) {
|
) {
|
||||||
misc::pb_style_download(pb, false);
|
misc::pb_style_download(pb, false);
|
||||||
|
|
||||||
@@ -210,10 +208,7 @@ async fn update_dir(
|
|||||||
let mut files_to_download: Vec<CdnFile> = vec![];
|
let mut files_to_download: Vec<CdnFile> = vec![];
|
||||||
|
|
||||||
for file in cdn_info {
|
for file in cdn_info {
|
||||||
if !file.name.starts_with(&remote_dir_pre) || file.name == "iw4/iw4x.dll" {
|
if !file.name.starts_with(&remote_dir_pre) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if skip_iw4x_sp && file.name == "iw4/iw4x-sp.exe" {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,15 +332,12 @@ async fn update(
|
|||||||
dir: &Path,
|
dir: &Path,
|
||||||
bonus_content: bool,
|
bonus_content: bool,
|
||||||
force: bool,
|
force: bool,
|
||||||
skip_iw4x_sp: Option<bool>,
|
|
||||||
ignore_required_files: Option<bool>,
|
ignore_required_files: Option<bool>,
|
||||||
prerelease: Option<bool>,
|
|
||||||
) {
|
) {
|
||||||
info!("Starting update for game engine: {}", game.engine);
|
info!("Starting update for game engine: {}", game.engine);
|
||||||
info!("Update path: {}", dir.display());
|
info!("Update path: {}", dir.display());
|
||||||
debug!("Bonus content: {}, Force update: {}", bonus_content, force);
|
debug!("Bonus content: {}, Force update: {}", bonus_content, force);
|
||||||
|
|
||||||
let skip_iw4x_sp = skip_iw4x_sp.unwrap_or(false);
|
|
||||||
let ignore_required_files = ignore_required_files.unwrap_or(false);
|
let ignore_required_files = ignore_required_files.unwrap_or(false);
|
||||||
|
|
||||||
let res =
|
let res =
|
||||||
@@ -405,66 +397,12 @@ async fn update(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if game.engine == "iw4" {
|
|
||||||
iw4x::update(dir, &mut cache, prerelease).await;
|
|
||||||
|
|
||||||
let scan_dirs = ["iw4x", "zone/patch"];
|
|
||||||
let valid_files: HashSet<_> = cdn_info
|
|
||||||
.iter()
|
|
||||||
.filter_map(|cdn_file| {
|
|
||||||
if cdn_file.name.starts_with("iw4/") || cdn_file.name.starts_with("iw4-dlc/") {
|
|
||||||
Some(Path::new(&cdn_file.name).to_path_buf())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for scan_dir in scan_dirs.iter() {
|
|
||||||
let full_scan_dir = dir.join(scan_dir);
|
|
||||||
if !full_scan_dir.exists() || !full_scan_dir.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry in walkdir::WalkDir::new(&full_scan_dir)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter(|e| e.file_type().is_file())
|
|
||||||
{
|
|
||||||
let rel_path = entry.path().strip_prefix(dir).unwrap_or(entry.path());
|
|
||||||
let cdn_paths = [
|
|
||||||
Path::new("iw4").join(rel_path),
|
|
||||||
Path::new("iw4-dlc").join(rel_path),
|
|
||||||
];
|
|
||||||
|
|
||||||
if !cdn_paths.iter().any(|path| valid_files.contains(path)) {
|
|
||||||
crate::println_info!("{}{}", misc::prefix("removed"), entry.path().cute_path());
|
|
||||||
if std::fs::remove_file(entry.path()).is_err() {
|
|
||||||
crate::println_error!(
|
|
||||||
"{}Couldn't delete {}",
|
|
||||||
misc::prefix("error"),
|
|
||||||
entry.path().cute_path()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let pb = ProgressBar::new(0);
|
let pb = ProgressBar::new(0);
|
||||||
update_dir(
|
update_dir(&cdn_info, game.engine, dir, &mut cache.hashes, &pb).await;
|
||||||
&cdn_info,
|
|
||||||
game.engine,
|
|
||||||
dir,
|
|
||||||
&mut cache.hashes,
|
|
||||||
&pb,
|
|
||||||
skip_iw4x_sp,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if bonus_content && !game.bonus.is_empty() {
|
if bonus_content && !game.bonus.is_empty() {
|
||||||
for bonus in game.bonus.iter() {
|
for bonus in game.bonus.iter() {
|
||||||
update_dir(&cdn_info, bonus, dir, &mut cache.hashes, &pb, skip_iw4x_sp).await;
|
update_dir(&cdn_info, bonus, dir, &mut cache.hashes, &pb).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,9 +487,18 @@ fn launch(file_path: &PathBuf, args: &str) {
|
|||||||
fn launch(file_path: &PathBuf, args: &str) {
|
fn launch(file_path: &PathBuf, args: &str) {
|
||||||
println!("\n\nJoin the AlterWare Discord server:\nhttps://discord.gg/2ETE8engZM\n\n");
|
println!("\n\nJoin the AlterWare Discord server:\nhttps://discord.gg/2ETE8engZM\n\n");
|
||||||
crate::println_info!("Launching {} {args}", file_path.display());
|
crate::println_info!("Launching {} {args}", file_path.display());
|
||||||
let exit_status = if misc::is_program_in_path("wine") {
|
|
||||||
println!("Found wine, launching game using wine.\nIf you run into issues or want to launch a different way, run {} manually.", file_path.display());
|
let launcher = if misc::is_program_in_path("umu-run") {
|
||||||
std::process::Command::new("wine")
|
Some("umu-run")
|
||||||
|
} else if misc::is_program_in_path("wine") {
|
||||||
|
Some("wine")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let exit_status = if let Some(launcher) = launcher {
|
||||||
|
println!("Found {launcher}, launching game using {launcher}.\nIf you run into issues or want to launch a different way, run {} manually.", file_path.display());
|
||||||
|
std::process::Command::new(launcher)
|
||||||
.args([file_path.to_str().unwrap(), args.trim()])
|
.args([file_path.to_str().unwrap(), args.trim()])
|
||||||
.current_dir(file_path.parent().unwrap())
|
.current_dir(file_path.parent().unwrap())
|
||||||
.spawn()
|
.spawn()
|
||||||
@@ -627,7 +574,7 @@ fn show_iw4x_info() {
|
|||||||
"{}",
|
"{}",
|
||||||
"IW4x is not provided through AlterWare anymore.".bright_red()
|
"IW4x is not provided through AlterWare anymore.".bright_red()
|
||||||
);
|
);
|
||||||
println!("Please use iw4x-launcher.exe instead or visit www.iw4x.dev/install");
|
println!("Please visit https://aka.alterware.dev/iw4x for more information");
|
||||||
misc::stdin();
|
misc::stdin();
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
@@ -679,8 +626,6 @@ async fn main() {
|
|||||||
println!(" --redist: (Re-)Install redistributables");
|
println!(" --redist: (Re-)Install redistributables");
|
||||||
println!(" --prerelease: Update to prerelease version of clients and launcher");
|
println!(" --prerelease: Update to prerelease version of clients and launcher");
|
||||||
println!(" --offline: Run in offline mode");
|
println!(" --offline: Run in offline mode");
|
||||||
println!(" --skip-connectivity-check: Don't check connectivity");
|
|
||||||
println!(" --rate: Display CDN rating information and exit");
|
|
||||||
println!("\nExample:\n alterware-launcher.exe iw6 --pass \"-headless\"");
|
println!("\nExample:\n alterware-launcher.exe iw6 --pass \"-headless\"");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -716,11 +661,6 @@ async fn main() {
|
|||||||
install_path = env::current_dir().unwrap();
|
install_path = env::current_dir().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if arg_bool(&args, "--rate") {
|
|
||||||
cdn::rate_cdns_and_display().await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cfg = config::load(install_path.join("alterware-launcher.json"));
|
let mut cfg = config::load(install_path.join("alterware-launcher.json"));
|
||||||
|
|
||||||
if let Some(cdn_url) = arg_value(&args, "--cdn-url") {
|
if let Some(cdn_url) = arg_value(&args, "--cdn-url") {
|
||||||
@@ -733,11 +673,6 @@ async fn main() {
|
|||||||
arg_remove(&mut args, "--offline");
|
arg_remove(&mut args, "--offline");
|
||||||
}
|
}
|
||||||
|
|
||||||
if arg_bool(&args, "--skip-connectivity-check") {
|
|
||||||
cfg.skip_connectivity_check = true;
|
|
||||||
arg_remove(&mut args, "--skip-connectivity-check");
|
|
||||||
}
|
|
||||||
|
|
||||||
let initial_cdn = if !cfg.cdn_url.is_empty() {
|
let initial_cdn = if !cfg.cdn_url.is_empty() {
|
||||||
info!("Using custom CDN URL: {}", cfg.cdn_url);
|
info!("Using custom CDN URL: {}", cfg.cdn_url);
|
||||||
Some(cfg.cdn_url.clone())
|
Some(cfg.cdn_url.clone())
|
||||||
@@ -745,14 +680,6 @@ async fn main() {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
if !cfg.offline && !cfg.skip_connectivity_check {
|
|
||||||
if initial_cdn.is_some() {
|
|
||||||
cfg.offline = !global::check_connectivity(initial_cdn).await;
|
|
||||||
} else {
|
|
||||||
cfg.offline = !global::check_connectivity_and_rate_cdns().await.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.offline {
|
if cfg.offline {
|
||||||
// Check if this is a first-time run (no stored data)
|
// Check if this is a first-time run (no stored data)
|
||||||
let stored_data = cache::get_stored_data();
|
let stored_data = cache::get_stored_data();
|
||||||
@@ -816,9 +743,9 @@ async fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Launching game in offline mode with client: {}", client);
|
info!("Launching game in offline mode with client: {client}");
|
||||||
// Launch game without updates
|
// Launch game without updates
|
||||||
launch(&install_path.join(format!("{}.exe", client)), &cfg.args);
|
launch(&install_path.join(format!("{client}.exe")), &cfg.args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,14 +871,6 @@ async fn main() {
|
|||||||
"engine",
|
"engine",
|
||||||
cfg.engine.clone(),
|
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(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
if !cfg.skip_redist {
|
if !cfg.skip_redist {
|
||||||
@@ -980,9 +899,7 @@ async fn main() {
|
|||||||
install_path.as_path(),
|
install_path.as_path(),
|
||||||
cfg.download_bonus_content,
|
cfg.download_bonus_content,
|
||||||
cfg.force_update,
|
cfg.force_update,
|
||||||
Some(&game != "iw4x-sp"),
|
|
||||||
Some(ignore_required_files),
|
Some(ignore_required_files),
|
||||||
Some(cfg.prerelease),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if !cfg.update_only {
|
if !cfg.update_only {
|
||||||
|
|||||||
15
src/misc.rs
15
src/misc.rs
@@ -12,13 +12,6 @@ pub fn stdin() -> String {
|
|||||||
input.trim().to_string()
|
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 human_readable_bytes(bytes: u64) -> String {
|
pub fn human_readable_bytes(bytes: u64) -> String {
|
||||||
let mut bytes = bytes as f64;
|
let mut bytes = bytes as f64;
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -48,7 +41,7 @@ pub fn is_program_in_path(program: &str) -> bool {
|
|||||||
paths.to_str().map(|paths| {
|
paths.to_str().map(|paths| {
|
||||||
paths
|
paths
|
||||||
.split(':')
|
.split(':')
|
||||||
.any(|dir| fs::metadata(format!("{}/{}", dir, program)).is_ok())
|
.any(|dir| fs::metadata(format!("{dir}/{program}")).is_ok())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
@@ -60,9 +53,9 @@ pub fn is_program_in_path(program: &str) -> bool {
|
|||||||
.and_then(|paths| {
|
.and_then(|paths| {
|
||||||
paths.to_str().map(|paths| {
|
paths.to_str().map(|paths| {
|
||||||
paths.split(';').any(|dir| {
|
paths.split(';').any(|dir| {
|
||||||
fs::metadata(format!("{}\\{}.exe", dir, program)).is_ok()
|
fs::metadata(format!("{dir}\\{program}.exe")).is_ok()
|
||||||
|| fs::metadata(format!("{}\\{}.cmd", dir, program)).is_ok()
|
|| fs::metadata(format!("{dir}\\{program}.cmd")).is_ok()
|
||||||
|| fs::metadata(format!("{}\\{}.bat", dir, program)).is_ok()
|
|| fs::metadata(format!("{dir}\\{program}.bat")).is_ok()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub async fn self_update_available(prerelease: Option<bool>) -> bool {
|
|||||||
let latest_version = match github::latest_version(GH_OWNER, GH_REPO, prerelease).await {
|
let latest_version = match github::latest_version(GH_OWNER, GH_REPO, prerelease).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to get latest version: {}", e);
|
error!("Failed to get latest version: {e}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -93,7 +93,7 @@ pub async fn run(update_only: bool, prerelease: Option<bool>) {
|
|||||||
"alterware-launcher.exe"
|
"alterware-launcher.exe"
|
||||||
};
|
};
|
||||||
|
|
||||||
http_async::download_file(
|
if let Err(e) = http_async::download_file(
|
||||||
&format!(
|
&format!(
|
||||||
"{}/download/{}",
|
"{}/download/{}",
|
||||||
github::download_url(GH_OWNER, GH_REPO, None),
|
github::download_url(GH_OWNER, GH_REPO, None),
|
||||||
@@ -102,7 +102,11 @@ pub async fn run(update_only: bool, prerelease: Option<bool>) {
|
|||||||
&file_path,
|
&file_path,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
{
|
||||||
|
error!("Self-update download failed: {e}");
|
||||||
|
crate::println_error!("Self-update failed ({e}), skipping update.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
crate::println_error!("Failed to download launcher update.");
|
crate::println_error!("Failed to download launcher update.");
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ pub struct Config {
|
|||||||
pub cdn_url: String,
|
pub cdn_url: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub offline: bool,
|
pub offline: bool,
|
||||||
#[serde(default)]
|
|
||||||
pub skip_connectivity_check: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@@ -73,7 +71,6 @@ impl Default for Config {
|
|||||||
prerelease: false,
|
prerelease: false,
|
||||||
cdn_url: String::default(),
|
cdn_url: String::default(),
|
||||||
offline: false,
|
offline: false,
|
||||||
skip_connectivity_check: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user