#![deny(clippy::pedantic)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::struct_excessive_bools)]
#[macro_use]
extern crate prettytable;
use colored::Colorize;
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
use league_client_connector::LeagueClientConnector;
use anyhow::{anyhow, Result};
use prettytable::{format, Table};
use std::env::args_os;
use std::io;
use std::io::Write;
use std::process::exit;
use text_io::read;
use ugg_types::mappings::{self, Mode};
use crate::styling::format_ability_order;
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
use ugg_types::client_runepage::NewRunePage;
mod api;
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
mod client_api;
mod config;
mod styling;
mod util;
static DEFAULT_MODE: mappings::Mode = mappings::Mode::Normal;
static DEFAULT_ROLE: mappings::Role = mappings::Role::Automatic;
static DEFAULT_REGION: mappings::Region = mappings::Region::World;
const HELP_MESSAGE: &str = "\
CLI tool to query builds from u.gg, for League of Legends.
Usage: uggo [OPTIONS] [CHAMP]
Arguments:
[CHAMP]
The name of the champion you want to match. A best effort will be made to find the champ if it's only a partial query.
If left blank, will open the interactive version of uggo.
Options:
-m, --mode <MODE>
[default: Normal]
[possible values: normal, aram, one-for-all, urf, arurf]
-r, --role <ROLE>
[default: Automatic]
[possible values: jungle, support, ad-carry, top, mid, none, automatic]
-R, --region <REGION>
[default: World]
[possible values: na1, euw1, kr, eun1, br1, la1, la2, oc1, run, tr1, jp1, world]
-h, --help
Print help information (use `-h` for a summary)
-V, --version
Print version information
";
#[derive(Debug)]
struct Args {
mode: mappings::Mode,
role: mappings::Role,
region: mappings::Region,
champ: Option<String>,
}
fn fetch(
ugg: &api::UggApi,
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
client: &Option<client_api::ClientAPI>,
#[cfg(not(any(target_os = "windows", target_os = "macos", target_feature = "clippy")))] _client: Option<bool>,
champ: &str,
mode: mappings::Mode,
role: mappings::Role,
region: mappings::Region,
) {
let query_champ = ugg.find_champ(champ);
let formatted_champ_name = query_champ.name.as_str().green().bold();
let mut query_message = vec![format!("Looking up info for {formatted_champ_name}")];
if role != DEFAULT_ROLE {
query_message.push(format!(", playing {}", role.to_string().blue().bold()));
}
if region != DEFAULT_REGION {
query_message.push(format!(", in {}", region.to_string().red().bold()));
}
query_message.push("...".to_string());
util::log_info(query_message.concat().as_str());
let champ_overview = if let Ok(data) = ugg.get_stats(query_champ, role, region, mode) {
*data
} else {
util::log_error(format!("Couldn't get required data for {formatted_champ_name}.").as_str());
return;
};
let matchups = if mode == Mode::ARAM {
Err(anyhow!("ARAM does not have matchup data."))
} else {
ugg.get_matchups(query_champ, role, region, mode)
};
let stats_message = vec![format!("Build for {formatted_champ_name}")];
let true_length = 10 + query_champ.name.len();
let stats_message_str = stats_message.concat();
println!(" {}", "-".repeat(true_length));
println!(" {stats_message_str}");
println!(" {}", "-".repeat(true_length));
let champ_runes = util::group_runes(&champ_overview.runes.rune_ids, &ugg.runes);
let mut rune_table = Table::new();
rune_table.set_format(*format::consts::FORMAT_CLEAN);
rune_table.add_row(row![
styling::format_rune_group(champ_runes[0].0.as_str()),
"",
styling::format_rune_group(champ_runes[1].0.as_str()),
""
]);
rune_table.add_row(row![
&champ_runes[0].1[0].rune.name,
styling::format_rune_position(champ_runes[0].1[0]),
format!(
"{} (Row {})",
&champ_runes[1].1[0].rune.name, &champ_runes[1].1[0].slot
),
styling::format_rune_position(champ_runes[1].1[0])
]);
rune_table.add_row(row![
&champ_runes[0].1[1].rune.name,
styling::format_rune_position(champ_runes[0].1[1]),
format!(
"{} (Row {})",
&champ_runes[1].1[1].rune.name, &champ_runes[1].1[1].slot
),
styling::format_rune_position(champ_runes[1].1[1])
]);
rune_table.add_row(row![
&champ_runes[0].1[2].rune.name,
styling::format_rune_position(champ_runes[0].1[2])
]);
rune_table.add_row(row![
&champ_runes[0].1[3].rune.name,
styling::format_rune_position(champ_runes[0].1[3])
]);
rune_table.printstd();
println!();
println!(" {}", "Shards:".magenta().bold());
for shard in &util::process_shards(&champ_overview.shards.shard_ids) {
println!(" {shard}");
}
println!();
println!(
" {} {}, {}",
"Spells:".yellow().bold(),
&ugg.summoner_spells[&champ_overview.summoner_spells.spell_ids[0]],
&ugg.summoner_spells[&champ_overview.summoner_spells.spell_ids[1]]
);
println!();
println!(" {}", "Ability Order:".bright_cyan().bold(),);
format_ability_order(&champ_overview.abilities.ability_order).printstd();
let mut item_table = Table::new();
item_table.set_format(*format::consts::FORMAT_CLEAN);
item_table.add_row(row![
r->"Starting:".green(),
util::process_items(&champ_overview.starting_items.item_ids, &ugg.items)
]);
item_table.add_row(row![
r->"Core:".green(),
util::process_items(&champ_overview.core_items.item_ids, &ugg.items)
]);
item_table.add_row(row![
r->"4th:".green(),
util::process_items(&champ_overview.item_4_options.iter().map(|x| x.id).collect::<Vec<i64>>(), &ugg.items)
]);
item_table.add_row(row![
r->"5th:".green(),
util::process_items(&champ_overview.item_5_options.iter().map(|x| x.id).collect::<Vec<i64>>(), &ugg.items)
]);
item_table.add_row(row![
r->"6th:".green(),
util::process_items(&champ_overview.item_6_options.iter().map(|x| x.id).collect::<Vec<i64>>(), &ugg.items)
]);
println!();
item_table.printstd();
if let Ok(safe_matchups) = matchups {
let mut matchup_table = Table::new();
matchup_table.set_format(*format::consts::FORMAT_CLEAN);
matchup_table.add_row(row![
r->"Best Matchups:".cyan().bold(),
safe_matchups
.best_matchups
.into_iter()
.map(|m| util::find_champ_by_key(m.champion_id, &ugg.champ_data)
.unwrap()
.name
.as_str())
.collect::<Vec<&str>>()
.join(", ")
]);
matchup_table.add_row(row![
r->"Worst Matchups:".red().bold(),
safe_matchups
.worst_matchups
.into_iter()
.map(|m| util::find_champ_by_key(m.champion_id, &ugg.champ_data)
.unwrap()
.name
.as_str())
.collect::<Vec<&str>>()
.join(", ")
]);
println!();
matchup_table.printstd();
}
if champ_overview.low_sample_size {
println!();
println!(
" {} Data has a low sample size for this combination!",
"Warning:".yellow().bold()
);
}
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
match client {
Some(ref api) => match api.get_current_rune_page() {
Some(data) => {
let (primary_style_id, sub_style_id, selected_perk_ids) =
util::generate_perk_array(&champ_runes, &champ_overview.shards.shard_ids);
api.update_rune_page(
&data.id,
&NewRunePage {
name: match mode {
mappings::Mode::ARAM => {
format!("uggo: {}, ARAM", &query_champ.name)
}
mappings::Mode::URF => format!("uggo: {}, URF", &query_champ.name),
_ => format!("uggo: {}, Normal", &query_champ.name),
},
primary_style_id,
sub_style_id,
selected_perk_ids,
},
);
}
_ => (),
},
None => (),
}
}
fn main() -> Result<()> {
let ugg = api::UggApi::new()?;
let mut mode = mappings::Mode::Normal;
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
let client_lockfile = LeagueClientConnector::parse_lockfile().ok();
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
let mut clientapi: Option<client_api::ClientAPI> = None;
#[cfg(all(
debug_assertions,
any(target_os = "windows", target_os = "macos", target_feature = "clippy")
))]
if !client_lockfile.as_ref().is_none() {
let lockfile = client_lockfile.clone().unwrap();
util::log_info(&format!(
"- Found client running at https://127.0.0.1:{}/",
lockfile.port
));
clientapi = Some(client_api::ClientAPI::new(lockfile));
}
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
if !client_lockfile.as_ref().is_none() {
clientapi = Some(client_api::ClientAPI::new(client_lockfile.clone().unwrap()));
}
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
match clientapi {
Some(ref api) => match api.get_summoner_info() {
Some(summoner) => {
#[cfg(debug_assertions)]
util::log_info(&format!(
"- Found summoner {} (id: {})",
summoner.display_name, summoner.summoner_id
));
}
None => (),
},
None => (),
}
let mut raw_args: Vec<_> = args_os().collect();
raw_args.remove(0);
let mut args = pico_args::Arguments::from_vec(raw_args);
if args.contains(["-h", "--help"]) {
print!("{HELP_MESSAGE}");
exit(0);
}
if args.contains(["-V", "--version"]) {
println!("uggo v{}", env!("CARGO_PKG_VERSION"));
exit(0);
}
let parsed_args = Args {
champ: args.free_from_str().ok(),
mode: args
.value_from_str::<[&str; 2], mappings::Mode>(["-m", "--mode"])
.map_or(DEFAULT_MODE, |m| m),
role: args
.value_from_str::<[&str; 2], mappings::Role>(["-r", "--role"])
.map_or(DEFAULT_ROLE, |r| r),
region: args
.value_from_str::<[&str; 2], mappings::Region>(["-R", "--region"])
.map_or(DEFAULT_REGION, |r| r),
};
if let Some(champ_name) = parsed_args.champ {
#[cfg(not(any(target_os = "windows", target_os = "macos", target_feature = "clippy")))]
fetch(
&ugg,
None,
&champ_name,
parsed_args.mode,
parsed_args.role,
parsed_args.region,
);
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
fetch(
&ugg,
&clientapi,
&champ_name,
parsed_args.mode,
parsed_args.role,
parsed_args.region,
);
return Ok(());
}
loop {
print!("query> ");
io::stdout().flush().unwrap();
let user_input: String = read!("{}\n");
let clean_user_input = user_input.trim();
let user_input_split = clean_user_input
.split(',')
.map(str::trim)
.collect::<Vec<&str>>();
if clean_user_input == "modes" {
util::log_info("Available modes:");
mappings::Mode::all()
.iter()
.for_each(|m| util::log_info(format!("- {m:?}").as_str()));
continue;
}
if clean_user_input.starts_with("mode") {
let mode_to_set = clean_user_input.split(' ').collect::<Vec<&str>>();
if mode_to_set.len() > 1 {
mode = mappings::Mode::from(mode_to_set[1]);
util::log_info(format!("Switching mode to {mode:?}...").as_str());
continue;
}
util::log_info(format!("Current mode: {mode:?}").as_str());
continue;
}
let mut query_champ_name = "";
let mut query_role = DEFAULT_ROLE;
let mut query_region = DEFAULT_REGION;
if user_input_split.is_empty()
|| user_input_split.len() > 3
|| user_input_split[0].is_empty()
{
util::log_info("This doesn't look like a valid query.");
util::log_info("Query format is <champion>[,<role>][,<region>]");
continue;
}
if !user_input_split.is_empty() {
query_champ_name = user_input_split[0];
}
if user_input_split.len() >= 2 {
let try_role = mappings::get_role(user_input_split[1]);
if try_role == query_role {
query_region = mappings::get_region(user_input_split[1]);
} else {
query_role = try_role;
}
}
if user_input_split.len() == 3 {
query_role = mappings::get_role(user_input_split[1]);
query_region = mappings::get_region(user_input_split[2]);
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_feature = "clippy")))]
fetch(&ugg, None, query_champ_name, mode, query_role, query_region);
#[cfg(any(target_os = "windows", target_os = "macos", target_feature = "clippy"))]
fetch(
&ugg,
&clientapi,
query_champ_name,
mode,
query_role,
query_region,
);
println!();
}
}