[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

View File

@@ -9,8 +9,74 @@ 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)]
@@ -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(&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) {
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(&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 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),
];

View File

@@ -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::<Value>(&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())
}

View File

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