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]]
|
||||
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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
195
src/cdn.rs
195
src/cdn.rs
@@ -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(®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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
|
||||
|
||||
19
src/http.rs
19
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::<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())
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user