diff --git a/Cargo.lock b/Cargo.lock index 57ff1fe..733615f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2468,18 +2468,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.20" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.20" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index 427ea3e..b78b525 100644 --- a/README.md +++ b/README.md @@ -150,10 +150,13 @@ - Install or reinstall redistributables - ```--prerelease``` - Update to prerelease version of clients (currently only available for IW4x) and launcher +- ```--rate``` + - Rate and display CDN servers - ```--cdn-url``` - ```--offline``` - ```--skip-connectivity-check``` + ##### Example: ```shell alterware-launcher.exe iw4x --bonus -u --path "C:\Games\IW4x" --pass "-console" diff --git a/src/cdn.rs b/src/cdn.rs index 59ea5b1..a4921e3 100644 --- a/src/cdn.rs +++ b/src/cdn.rs @@ -9,8 +9,74 @@ static CURRENT_CDN: Mutex>> = 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)] @@ -35,17 +101,16 @@ impl Server { 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 { - Duration::from_millis(500) + Duration::from_millis(1000) } else { Duration::from_millis(5000) }; match http::rating_request(&self.url(), timeout).await { Ok((latency, is_cloudflare)) => { self.latency = Some(latency); - // Always use complete rating calculation with all available information - self.rating = self.calculate_rating(latency, is_cloudflare, asn); + self.rating = self.calculate_rating(latency, is_cloudflare, asn, user_region); info!( "Server {} rated {} ({}ms, rating: {}, cloudflare: {}, region: {:?})", @@ -65,16 +130,32 @@ impl Server { } } - fn calculate_initial_rating(&self, latency: std::time::Duration) -> u8 { - let mut rating: f32 = 255.0; + fn rate_latency(&self, latency: std::time::Duration) -> u8 { 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 } - fn calculate_rating(&self, latency: std::time::Duration, is_cloudflare: bool, asn: u32) -> u8 { - let mut rating = self.calculate_initial_rating(latency); + fn calculate_rating( + &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 if is_cloudflare { @@ -85,9 +166,26 @@ impl Server { } } - if self.region == Region::Global { - rating = (rating as f32 * 1.1).min(255.0) as u8; - } + 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 { + 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 } @@ -106,12 +204,18 @@ impl Hosts { active_index: RwLock::new(None), }; - let asn = crate::http::get_asn().await; - hosts.rate(asn, true).await; + 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) { 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 @@ -145,11 +249,11 @@ impl Hosts { } /// 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 .servers .iter_mut() - .map(|server| server.rate(asn, is_initial)) + .map(|server| server.rate(asn, user_region, is_initial)) .collect(); join_all(rating_futures).await; @@ -165,3 +269,58 @@ impl Hosts { 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()); + } +} diff --git a/src/global.rs b/src/global.rs index 696c8e8..1bd8451 100644 --- a/src/global.rs +++ b/src/global.rs @@ -14,8 +14,9 @@ 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 CDN_HOSTS: [Server; 2] = [ +pub const CDN_HOSTS: [Server; 3] = [ Server::new("cdn.alterware.ovh", Region::Global), + Server::new("us-cdn.alterware.ovh", Region::NorthAmerica), Server::new("cdn.iw4x.dev", Region::Europe), ]; diff --git a/src/http.rs b/src/http.rs index 85b3061..d8654da 100644 --- a/src/http.rs +++ b/src/http.rs @@ -88,15 +88,22 @@ pub async fn rating_request( Ok((latency, is_cloudflare)) } -/// Retrieve client ASN -pub async fn get_asn() -> u32 { +/// 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::(&as_data_str) { - if let Some(as_number) = as_data.get("as_number").and_then(|v| v.as_u64()) { - return as_number as u32; - } + 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 + (0, "Unknown".to_string()) } diff --git a/src/main.rs b/src/main.rs index 03c57da..24aed54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -666,6 +666,7 @@ async fn main() { println!(" --prerelease: Update to prerelease version of clients and launcher"); 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 iw4x --bonus --pass \"-console -nointro\"" ); @@ -692,6 +693,11 @@ async fn main() { return; } + if arg_bool(&args, "--rate") { + cdn::rate_cdns_and_display().await; + return; + } + let install_path: PathBuf; if let Some(path) = arg_value(&args, "--path") { install_path = PathBuf::from(path);