[cdn] region based rating; --rate flag

This commit is contained in:
2025-05-31 19:58:26 +02:00
parent 3a978ac39d
commit 1811c3adc3
6 changed files with 205 additions and 29 deletions

8
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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(&region_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(&region_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());
}
}

View File

@@ -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),
]; ];

View File

@@ -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
} }

View File

@@ -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);