mirror of
https://github.com/alterware/alterware-launcher.git
synced 2025-12-04 07:17:50 +00:00
[cdn] region based rating; --rate flag
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -2468,18 +2468,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.20"
|
version = "0.8.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c"
|
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.20"
|
version = "0.8.25"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700"
|
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -150,10 +150,13 @@
|
|||||||
- Install or reinstall redistributables
|
- Install or reinstall redistributables
|
||||||
- ```--prerelease```
|
- ```--prerelease```
|
||||||
- Update to prerelease version of clients (currently only available for IW4x) and launcher
|
- Update to prerelease version of clients (currently only available for IW4x) and launcher
|
||||||
|
- ```--rate```
|
||||||
|
- Rate and display CDN servers
|
||||||
- ```--cdn-url```
|
- ```--cdn-url```
|
||||||
- ```--offline```
|
- ```--offline```
|
||||||
- ```--skip-connectivity-check```
|
- ```--skip-connectivity-check```
|
||||||
|
|
||||||
|
|
||||||
##### Example:
|
##### Example:
|
||||||
```shell
|
```shell
|
||||||
alterware-launcher.exe iw4x --bonus -u --path "C:\Games\IW4x" --pass "-console"
|
alterware-launcher.exe iw4x --bonus -u --path "C:\Games\IW4x" --pass "-console"
|
||||||
|
|||||||
191
src/cdn.rs
191
src/cdn.rs
@@ -9,8 +9,74 @@ static CURRENT_CDN: Mutex<Option<Arc<Server>>> = Mutex::new(None);
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum Region {
|
pub enum Region {
|
||||||
|
Africa,
|
||||||
|
Asia,
|
||||||
Europe,
|
Europe,
|
||||||
|
NorthAmerica,
|
||||||
|
Oceania,
|
||||||
|
SouthAmerica,
|
||||||
Global,
|
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)]
|
||||||
@@ -35,17 +101,16 @@ impl Server {
|
|||||||
format!("https://{}/", self.host)
|
format!("https://{}/", self.host)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rate(&mut self, asn: u32, is_initial: bool) {
|
async fn rate(&mut self, asn: u32, user_region: Region, is_initial: bool) {
|
||||||
let timeout = if is_initial {
|
let timeout = if is_initial {
|
||||||
Duration::from_millis(500)
|
Duration::from_millis(1000)
|
||||||
} else {
|
} else {
|
||||||
Duration::from_millis(5000)
|
Duration::from_millis(5000)
|
||||||
};
|
};
|
||||||
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);
|
||||||
// Always use complete rating calculation with all available information
|
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: {}, region: {:?})",
|
||||||
@@ -65,16 +130,32 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_initial_rating(&self, latency: std::time::Duration) -> u8 {
|
fn rate_latency(&self, latency: std::time::Duration) -> u8 {
|
||||||
let mut rating: f32 = 255.0;
|
|
||||||
let ms = latency.as_millis() as f32;
|
let ms = latency.as_millis() as f32;
|
||||||
let latency_mult = (200.0 / ms.max(200.0)).powf(0.5);
|
|
||||||
rating *= latency_mult;
|
let rating = if ms <= 50.0 {
|
||||||
|
240.0
|
||||||
|
} else if ms <= 100.0 {
|
||||||
|
240.0 - (ms - 50.0) * 1.0
|
||||||
|
} else if ms <= 200.0 {
|
||||||
|
190.0 - (ms - 100.0) * 0.5
|
||||||
|
} else if ms <= 500.0 {
|
||||||
|
140.0 - (ms - 200.0) * 0.033
|
||||||
|
} else {
|
||||||
|
100.0
|
||||||
|
};
|
||||||
|
|
||||||
rating.clamp(1.0, 255.0) as u8
|
rating.clamp(1.0, 255.0) as u8
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_rating(&self, latency: std::time::Duration, is_cloudflare: bool, asn: u32) -> u8 {
|
fn calculate_rating(
|
||||||
let mut rating = self.calculate_initial_rating(latency);
|
&self,
|
||||||
|
latency: std::time::Duration,
|
||||||
|
is_cloudflare: bool,
|
||||||
|
asn: u32,
|
||||||
|
user_region: Region,
|
||||||
|
) -> u8 {
|
||||||
|
let mut rating = self.rate_latency(latency);
|
||||||
|
|
||||||
// Additional factors for full rating
|
// Additional factors for full rating
|
||||||
if is_cloudflare {
|
if is_cloudflare {
|
||||||
@@ -85,9 +166,26 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let distance_km = user_region.distance_to(self.region);
|
||||||
|
let region_multiplier = if distance_km == 0.0 {
|
||||||
|
1.3
|
||||||
|
} else if user_region == Region::Unknown {
|
||||||
if self.region == Region::Global {
|
if self.region == Region::Global {
|
||||||
rating = (rating as f32 * 1.1).min(255.0) as u8;
|
1.1
|
||||||
|
} else {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -106,12 +204,18 @@ impl Hosts {
|
|||||||
active_index: RwLock::new(None),
|
active_index: RwLock::new(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let asn = crate::http::get_asn().await;
|
let (asn, region_str) = crate::http::get_location_info().await;
|
||||||
hosts.rate(asn, true).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 500ms timeout, retrying with 5000ms timeout");
|
info!("All CDN servers failed with 500ms timeout, retrying with 5000ms timeout");
|
||||||
hosts.rate(asn, false).await;
|
hosts.rate(asn, user_region, false).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts
|
hosts
|
||||||
@@ -145,11 +249,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, is_initial: bool) {
|
pub async fn rate(&mut self, asn: u32, user_region: Region, is_initial: bool) {
|
||||||
let rating_futures: Vec<_> = self
|
let rating_futures: Vec<_> = self
|
||||||
.servers
|
.servers
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.map(|server| server.rate(asn, is_initial))
|
.map(|server| server.rate(asn, user_region, is_initial))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
join_all(rating_futures).await;
|
join_all(rating_futures).await;
|
||||||
@@ -165,3 +269,58 @@ 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 mut hosts = Hosts {
|
||||||
|
servers: CDN_HOSTS.to_vec(),
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ pub const GH_IW4X_OWNER: &str = "iw4x";
|
|||||||
pub const GH_IW4X_REPO: &str = "iw4x-client";
|
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; 3] = [
|
||||||
Server::new("cdn.alterware.ovh", Region::Global),
|
Server::new("cdn.alterware.ovh", Region::Global),
|
||||||
|
Server::new("us-cdn.alterware.ovh", Region::NorthAmerica),
|
||||||
Server::new("cdn.iw4x.dev", Region::Europe),
|
Server::new("cdn.iw4x.dev", Region::Europe),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
19
src/http.rs
19
src/http.rs
@@ -88,15 +88,22 @@ pub async fn rating_request(
|
|||||||
Ok((latency, is_cloudflare))
|
Ok((latency, is_cloudflare))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve client ASN
|
/// Retrieve client ASN and region
|
||||||
pub async fn get_asn() -> u32 {
|
pub async fn get_location_info() -> (u32, String) {
|
||||||
let response = quick_request(crate::global::IP2ASN).await;
|
let response = quick_request(crate::global::IP2ASN).await;
|
||||||
if let Ok(as_data_str) = response {
|
if let Ok(as_data_str) = response {
|
||||||
if let Ok(as_data) = serde_json::from_str::<Value>(&as_data_str) {
|
if let Ok(as_data) = serde_json::from_str::<Value>(&as_data_str) {
|
||||||
if let Some(as_number) = as_data.get("as_number").and_then(|v| v.as_u64()) {
|
let as_number = as_data
|
||||||
return as_number as u32;
|
.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())
|
||||||
0
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -666,6 +666,7 @@ async fn main() {
|
|||||||
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!(" --skip-connectivity-check: Don't check connectivity");
|
||||||
|
println!(" --rate: Display CDN rating information and exit");
|
||||||
println!(
|
println!(
|
||||||
"\nExample:\n alterware-launcher.exe iw4x --bonus --pass \"-console -nointro\""
|
"\nExample:\n alterware-launcher.exe iw4x --bonus --pass \"-console -nointro\""
|
||||||
);
|
);
|
||||||
@@ -692,6 +693,11 @@ async fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if arg_bool(&args, "--rate") {
|
||||||
|
cdn::rate_cdns_and_display().await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let install_path: PathBuf;
|
let install_path: PathBuf;
|
||||||
if let Some(path) = arg_value(&args, "--path") {
|
if let Some(path) = arg_value(&args, "--path") {
|
||||||
install_path = PathBuf::from(path);
|
install_path = PathBuf::from(path);
|
||||||
|
|||||||
Reference in New Issue
Block a user