CGGItemSets/src/main.rs
nyyu 03d4537743
All checks were successful
ci/woodpecker/push/linux Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/mingw Pipeline was successful
refactor: centralize HTTP client creation with dynamic user agent
2025-06-02 09:18:07 +02:00

267 lines
8.1 KiB
Rust

use chrono::{Datelike, Local, NaiveDate};
use indexmap::IndexMap;
#[cfg(target_os = "windows")]
use log::debug;
use log::{LevelFilter, error, info};
use rayon::prelude::*;
use serde_derive::Deserialize;
use std::env;
#[cfg(target_os = "windows")]
use std::io;
use std::io::Error;
#[cfg(target_os = "windows")]
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::time::Instant;
use std::{fs, thread, time};
use time::Duration;
use ureq::Agent;
#[cfg(target_os = "windows")]
use winreg::RegKey;
mod data_source;
mod kb_data_source;
mod ms_data_source;
use data_source::DataSource;
use kb_data_source::KBDataSource;
use ms_data_source::MSDataSource;
#[derive(Deserialize)]
struct Realm {
v: String,
}
#[derive(Deserialize)]
pub struct Champion {
data: IndexMap<String, ChampInfo>,
}
#[derive(Deserialize)]
pub struct ChampInfo {
id: String,
name: String,
key: String,
}
const DEFAULT_LOL_CHAMPS_DIR: &str = "./champs";
#[cfg(target_os = "windows")]
const REG_KEY_LOL_RADS: &str = r"SOFTWARE\WOW6432Node\Riot Games\RADS";
#[cfg(target_os = "windows")]
const REG_KEY_LOL_INC: &str = r"SOFTWARE\WOW6432Node\Riot Games, Inc\League of Legends";
#[cfg(target_os = "windows")]
const REG_KEY_WIN_64_UNINSTALL: &str =
r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall";
#[cfg(target_os = "windows")]
const REG_KEY_WIN_UNINSTALL: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\";
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
let mut level = LevelFilter::Info;
for s in &args {
if s.eq_ignore_ascii_case("-v") || s.eq_ignore_ascii_case("--verbose") {
level = LevelFilter::Debug;
break;
}
}
logsy::set_echo(true);
logsy::set_level(level);
info!("CGG Item Sets");
let lol_champs_dir: PathBuf = match lol_champ_dir() {
Ok(x) => x,
Err(_e) => PathBuf::from(DEFAULT_LOL_CHAMPS_DIR),
};
info!("LoL Champs Folder: {}", lol_champs_dir.display());
let client: Agent = create_http_client();
let realm: Realm = client
.get("https://ddragon.leagueoflegends.com/realms/euw.json")
.call()?
.body_mut()
.read_json()?;
info!("LoL version: {}", realm.v);
let champion: Champion = client
.get(&format!(
"https://ddragon.leagueoflegends.com/cdn/{}/data/en_US/champion.json",
realm.v
))
.call()?
.body_mut()
.read_json()?;
info!("LoL numbers of champs: {}", champion.data.len());
let data_sources: Vec<Box<dyn DataSource + Sync + Send>> = vec![
Box::new(KBDataSource::new(&client)),
Box::new(MSDataSource::new(&client)),
];
data_sources.par_iter().for_each(|data_source| {
let init = Instant::now();
execute_data_source(&**data_source, &champion, &lol_champs_dir);
info!(
"{}: done in {}s",
data_source.get_alias(),
init.elapsed().as_secs_f32()
);
});
Ok(())
}
fn get_champ_from_key(champs: &Champion, key: u32) -> Option<String> {
for champ in champs.data.values() {
if key.to_string() == champ.key {
return Some(champ.id.to_owned());
}
}
None
}
fn execute_data_source(
data_source: &(dyn DataSource + Sync + Send),
champion: &Champion,
lol_champs_dir: &Path,
) {
let champs = data_source.get_champs_with_positions(champion);
info!(
"{} numbers of champs: {}",
data_source.get_alias(),
champs.len()
);
let process = |(id, positions): (&u32, &Vec<String>)| {
get_and_write_item_set(data_source, champion, lol_champs_dir, *id, positions);
if data_source.get_timeout() > 0 {
thread::sleep(Duration::from_millis(data_source.get_timeout()));
}
};
if data_source.get_timeout() == 0 {
champs.par_iter().for_each(process);
} else {
champs.iter().for_each(process);
}
}
fn get_and_write_item_set(
data_source: &(dyn DataSource + Sync + Send),
champion: &Champion,
lol_champs_dir: &Path,
id: u32,
positions: &[String],
) {
if let Some(champ_id) = get_champ_from_key(champion, id) {
if let Some(champ) = champion.data.get(&champ_id) {
if positions.is_empty() {
error!("{}: {} empty positions", data_source.get_alias(), &champ_id);
} else {
let path = lol_champs_dir.join(&champ_id).join("Recommended");
match fs::create_dir_all(&path) {
Ok(_) => match data_source.write_item_set(champ, positions, &path) {
Ok(_) => (),
Err(e) => error!(
"{}: Failed to write item set for {} at {}: {}",
data_source.get_alias(),
champ.name,
positions.join(", "),
e
),
},
Err(e) => {
error!("Failed to create directory for {}: {}", champ_id, e);
}
}
}
} else {
error!("{} not found in LoL champs", &champ_id);
}
}
}
fn create_http_client() -> Agent {
Agent::config_builder()
.user_agent(get_browser_user_agent())
.timeout_global(Some(Duration::from_secs(10)))
.build()
.into()
}
fn get_browser_user_agent() -> String {
let base_version = 125;
let start_date = NaiveDate::from_ymd_opt(2024, 4, 16).unwrap();
let now = Local::now().naive_local().date();
let months_between =
(now.year() - start_date.year()) * 12 + (now.month() as i32 - start_date.month() as i32);
let version = base_version + months_between;
format!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{version}.0) Gecko/20100101 Firefox/{version}.0"
)
}
#[cfg(target_os = "windows")]
fn lol_champ_dir() -> Result<PathBuf, Error> {
let hklm = RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE);
let path = if let Ok(node) = hklm.open_subkey(REG_KEY_LOL_RADS) {
debug!(
"Use registry key {} for relative champ directory",
REG_KEY_LOL_RADS
);
let val: String = node.get_value("LocalRootFolder")?;
match PathBuf::from(val).parent() {
Some(parent) => parent.to_path_buf(),
None => return Err(Error::from(ErrorKind::NotFound)),
}
} else if let Ok(node) = hklm.open_subkey(REG_KEY_LOL_INC) {
debug!(
"Use registry key {} for relative champ directory",
REG_KEY_LOL_INC
);
let val: String = node.get_value("Location")?;
PathBuf::from(val)
} else if let Ok(node) =
find_subnode_from_path(hklm, REG_KEY_WIN_64_UNINSTALL, "League of Legends")
{
debug!(
"Use registry key {} for relative champ directory",
REG_KEY_WIN_64_UNINSTALL
);
let val: String = node.get_value("InstallLocation")?;
PathBuf::from(val)
} else if let Ok(node) = find_subnode_from_path(
RegKey::predef(winreg::enums::HKEY_CURRENT_USER),
REG_KEY_WIN_UNINSTALL,
"Riot Game league_of_legends.live",
) {
debug!(
"Use registry key {} for relative champ directory",
REG_KEY_WIN_UNINSTALL
);
let val: String = node.get_value("InstallLocation")?;
PathBuf::from(val)
} else {
return Err(Error::from(ErrorKind::NotFound));
};
Ok(path.join("Config").join("Champions"))
}
#[cfg(not(target_os = "windows"))]
fn lol_champ_dir() -> Result<PathBuf, Error> {
Ok(PathBuf::from(DEFAULT_LOL_CHAMPS_DIR))
}
#[cfg(target_os = "windows")]
fn find_subnode_from_path(reg: RegKey, path: &str, key: &str) -> io::Result<RegKey> {
if let Ok(node) = reg.open_subkey(path) {
if let Some(k) = node
.enum_keys()
.filter_map(Result::ok)
.find(|x| x.starts_with(key))
{
return node.open_subkey(k);
}
}
Err(Error::from(ErrorKind::NotFound))
}