use anyhow::Result;
pub mod api;
pub mod calc;
pub mod external_api;
pub mod utils;
fn main() -> Result<()> {
#[cfg(not(feature = "python"))]
{
crate::cli::run_cli();
Ok(())
}
#[cfg(feature = "python")]
{
Err(anyhow::anyhow!(
"The entrypoint does not exist when compiling with the default 'python' feature"
))
}
}
#[cfg(not(feature = "python"))]
pub mod cli {
use std::{io::Read, path::Path};
use crate::utils::{
logger_utils::init_tracing, ram_input_utils::parse_human_size,
version_checker_utils::check_new_version,
};
use anyhow::{Result, anyhow};
use clap::{ArgAction, Parser, ValueHint};
use humansize::SizeFormatter;
use pyoe2_craftpath::{
GITHUB_REPOSITORY,
api::{
calculator::{Calculator, GroupRoute},
types::THashMap,
},
calc::{
matrix::presets::matrix_builder_presets::MatrixBuilderPreset,
statistics::presets::{
statistic_analyzer_currency_group_presets::StatisticAnalyzerCurrencyGroupPreset,
statistic_analyzer_path_presets::StatisticAnalyzerPathPreset,
},
},
external_api::{
coe::craftofexile_data_provider_adapter::CraftOfExileItemInfoProvider,
coe_emulator::coe_emulator_item_snapshot_provider::CraftOfExileEmulatorItemImport,
fetch_json_from_urls::retrieve_contents_from_urls_with_cache_unstable_order,
pn::poe_ninja_data_provider_adapter::PoeNinjaMarketPriceProvider,
},
};
pub fn run_cli() {
ctrlc::set_handler(|| std::process::exit(2)).unwrap();
init_tracing();
tracing::info!("Starting PyoE2 CraftPath CLI");
let res = calc_pipleline();
match res {
Err(e) => tracing::error!("{:#?}", e),
_ => {}
}
println!("Press Enter to close ...");
let _ = std::io::stdin().read(&mut [0u8]).unwrap();
}
#[derive(Parser, Debug)]
#[command(
author = "Wladislaw Jerokin (WladHD)",
version,
about = "pyoe2-craftpath",
long_about = "A tool for Path of Exile 2 to find the best craftpaths based on the categories: *most likely, most efficient and cheapest*, between a starting item and a target item."
)]
struct Args {
#[arg(
short,
long,
env = "START_ITEM_PATH",
default_value = "pyoe2-craftpath/startitem.json",
value_hint = ValueHint::FilePath
)]
start_item_path: String,
#[arg(
short,
long,
env = "TARGET_ITEM_PATH",
default_value = "pyoe2-craftpath/targetitem.json",
value_hint = ValueHint::FilePath
)]
target_item_path: String,
#[arg(
short, long, env = "CACHE_PATH",
default_value = "pyoe2-craftpath",
value_hint = ValueHint::DirPath
)]
cache_path: String,
#[arg(short, long, env = "AMOUNT_ROUTES", default_value_t = 5)]
amount_routes: u32,
#[arg(
short,
long,
env = "POE2_LEAGUE",
default_value = "
"
)]
poe2_league: String,
#[arg(long, env = "NO_UPDATES", action = ArgAction::SetTrue)]
no_updates: bool,
#[arg(long, env = "NO_GROUPS", action = ArgAction::SetTrue)]
no_groups: bool,
#[arg(long, env = "MAX_RAM", default_value = "1G", value_parser = parse_human_size)]
max_ram: u64,
}
fn calc_pipleline() -> Result<()> {
let args = Args::parse();
tracing::info!("You can run the program with '--help' to see all available options.");
tracing::info!(
"Running pipeline with config:\n\
start_item_path: {}\n\
target_item_path: {}\n\
cache_path: {}\n\
amount_routes: {}\n\
poe2_league: {}\n\
max_ram: {}\n\
disable group calculation? {}\n\
check for updates? {}",
args.start_item_path,
args.target_item_path,
args.cache_path,
args.amount_routes,
args.poe2_league,
SizeFormatter::new(args.max_ram, humansize::DECIMAL),
match args.no_groups {
true => "disable",
false => "calculate",
},
match args.no_updates {
true => "ain't nobody got time for that",
false => "check",
},
);
if !args.no_updates {
let res = check_new_version(GITHUB_REPOSITORY);
match res {
Ok(_) => {}
Err(e) => {
tracing::warn!(
"Could not check for updates. Program will continue.\nError: {:?}",
e
)
}
};
}
let res = start_calc_and_print(&args);
match res {
Ok(e) => Ok(e),
Err(e) => {
tracing::error!(
"An error occurred. Run program with '--help' to see possible configuration options."
);
Err(e)
}
}
}
fn check_cache_path(cache_path: &str) -> Result<()> {
let path = Path::new(cache_path);
if !path.exists() {
return Err(anyhow!(
"Cache path '{}' does not exist. Explicitly create it.",
cache_path
));
}
if !path.is_dir() {
return Err(anyhow!("Cache path '{}' is not a directory", cache_path));
}
Ok(())
}
fn start_calc_and_print(args: &Args) -> Result<()> {
check_cache_path(args.cache_path.as_str())?;
let item_provider_hm = THashMap::from_iter(vec![(
format!("{}/coe2.json", args.cache_path).to_string(),
"https://www.craftofexile.com/json/poe2/main/poec_data.json".to_string(),
)]);
let economy_provider_hm = THashMap::from_iter(vec![
(
format!("{}/pn_abyss.json", args.cache_path).to_string(),
format!("https://poe.ninja/poe2/api/economy/exchange/current/overview?league={}&type=Abyss", args.poe2_league.as_str()).to_string(),
),
(
format!("{}/pn_currency.json", args.cache_path).to_string(),
format!("https://poe.ninja/poe2/api/economy/exchange/current/overview?league={}&type=Currency", args.poe2_league.as_str()).to_string(),
),
(
format!("{}/pn_essences.json", args.cache_path).to_string(),
format!("https://poe.ninja/poe2/api/economy/exchange/current/overview?league={}&type=Essences", args.poe2_league.as_str()).to_string(),
),
(
format!("{}/pn_ritual.json", args.cache_path).to_string(),
format!("https://poe.ninja/poe2/api/economy/exchange/current/overview?league={}&type=Ritual", args.poe2_league.as_str()).to_string(),
),
]);
let item_cached_jsons = retrieve_contents_from_urls_with_cache_unstable_order(
item_provider_hm,
60_u64 * 60_u64 * 24_u64,
)?;
let economy_cached_jsons = retrieve_contents_from_urls_with_cache_unstable_order(
economy_provider_hm,
60_u64 * 60_u64,
)?;
let item_provider =
CraftOfExileItemInfoProvider::parse_from_json(item_cached_jsons.first().unwrap())?;
let market_info =
PoeNinjaMarketPriceProvider::parse_from_json_list(economy_cached_jsons.as_ref())?;
let start_item = {
tracing::info!("Reading contents from '{}' ...", args.start_item_path);
let i1 = std::fs::read_to_string(args.start_item_path.as_str())?;
CraftOfExileEmulatorItemImport::parse_itemsnapshot_from_string(&i1, &item_provider)?
};
let target_item = {
tracing::info!("Reading contents from '{}' ...", args.target_item_path);
let i2 = std::fs::read_to_string(args.target_item_path.as_str())?;
CraftOfExileEmulatorItemImport::parse_itemsnapshot_from_string(&i2, &item_provider)?
};
let calculator = Calculator::generate_item_matrix(
start_item,
target_item,
&item_provider,
&market_info,
MatrixBuilderPreset::HappyPathMatrixBuilder
.get_instance()
.0
.as_ref(),
)?;
let chance_inst = StatisticAnalyzerPathPreset::UniquePathChance.get_instance();
let best_routes_chance = calculator.calculate_statistics(
&item_provider,
&market_info,
args.amount_routes,
args.max_ram,
chance_inst.0.as_ref(),
)?;
let cost_inst = StatisticAnalyzerPathPreset::UniquePathCost.get_instance();
let best_routes_cost = calculator.calculate_statistics(
&item_provider,
&market_info,
args.amount_routes,
args.max_ram,
cost_inst.0.as_ref(),
)?;
let efficient_cost_inst = StatisticAnalyzerPathPreset::UniquePathEfficiency.get_instance();
let best_routes_efficient_cost = calculator.calculate_statistics(
&item_provider,
&market_info,
args.amount_routes,
args.max_ram,
efficient_cost_inst.0.as_ref(),
)?;
let mut groups: Option<Vec<GroupRoute>> = None;
if !args.no_groups {
let group_instance =
StatisticAnalyzerCurrencyGroupPreset::CurrencyGroupChance.get_instance();
groups = Some(
calculator.calculate_statistics_currency_group(
&item_provider,
&market_info,
args.max_ram,
StatisticAnalyzerCurrencyGroupPreset::CurrencyGroupChance
.get_instance()
.0
.as_ref(),
)?,
);
for group in groups
.as_ref()
.unwrap()
.iter()
.take(args.amount_routes as usize)
{
let out =
group.to_pretty_string(&item_provider, &market_info, group_instance.0.as_ref());
tracing::info!("{}", out);
}
}
for (analyzer, routes) in vec![
(&chance_inst, best_routes_chance),
(&efficient_cost_inst, best_routes_efficient_cost),
(&cost_inst, best_routes_cost),
] {
tracing::warn!("Printing results for '{}'", analyzer.0.get_name());
for br in routes {
let out = br.to_pretty_string(
&item_provider,
&market_info,
cost_inst.0.as_ref(),
&calculator,
groups.as_ref(),
);
println!("{}", out);
}
}
Ok(())
}
}