1use std::hash::{Hash, Hasher};
2
3use crate::api::calculator_utils::calculate_target_proximity::calculate_target_proximity;
4use crate::api::errors::CraftPathError;
5use crate::api::item::ItemTechnicalMeta;
6use crate::api::provider::market_prices::PriceInDivines;
7use crate::calc::statistics::helpers::{RouteChance, RouteCustomWeight, SubpathAmount};
8use crate::{
9 api::{
10 currency::CraftCurrencyList,
11 item::{Item, ItemSnapshot},
12 provider::{item_info::ItemInfoProvider, market_prices::MarketPriceProvider},
13 types::THashMap,
14 },
15 utils::fraction_utils::Fraction,
16};
17use anyhow::Result;
18use serde::{Deserialize, Serialize};
19use tracing::instrument;
20
21#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
23#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
24#[cfg_attr(feature = "python", pyo3(eq, weakref, from_py_object, get_all, str))]
25pub struct PropagationTarget {
26 pub next: ItemSnapshot,
27 pub chance: Fraction,
28 pub meta: ItemTechnicalMeta,
29}
30
31impl PropagationTarget {
32 pub fn new(chance: Fraction, next: ItemSnapshot) -> Self {
33 Self {
34 next,
35 chance,
36 meta: ItemTechnicalMeta::default(),
37 }
38 }
39}
40
41#[derive(Clone, Debug, Serialize, Deserialize)]
42#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
43#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
44#[cfg_attr(feature = "python", pyo3(weakref, from_py_object, get_all, str))]
45pub struct ItemMatrixNode {
46 pub item: Item,
47 pub propagate: THashMap<CraftCurrencyList, Vec<PropagationTarget>>,
48}
49
50pub type ItemMatrix = THashMap<u64, ItemMatrixNode>;
51
52#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
56#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
57#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
58#[cfg_attr(
59 feature = "python",
60 pyo3(eq, weakref, from_py_object, get_all, frozen, hash, str)
61)]
62pub struct ItemRouteNode {
63 pub item_matrix_id: u64,
64 pub chance: Fraction,
65 pub currency_list: CraftCurrencyList,
66}
67
68#[derive(Clone, Debug, Serialize, Deserialize)]
70#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
71#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
72#[cfg_attr(
73 feature = "python",
74 pyo3(eq, weakref, from_py_object, get_all, frozen, hash, str)
75)]
76pub struct ItemRoute {
77 pub route: Vec<ItemRouteNode>,
78 pub weight: RouteCustomWeight, pub chance: RouteChance,
80}
81
82#[derive(Clone, Debug, Serialize, Deserialize)]
83#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
84#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
85#[cfg_attr(
86 feature = "python",
87 pyo3(weakref, from_py_object, get_all, frozen, str)
88)]
89pub struct GroupRoute {
90 pub group: Vec<CraftCurrencyList>,
91 pub weight: RouteCustomWeight,
92 pub unique_route_weights: Vec<Vec<RouteChance>>,
93 pub chance: RouteChance,
94 pub amount_subpaths: SubpathAmount,
95}
96
97impl PartialEq for ItemRoute {
98 fn eq(&self, other: &Self) -> bool {
99 self.route == other.route
100 }
101}
102
103impl Eq for ItemRoute {}
104
105impl Hash for ItemRoute {
106 fn hash<H: Hasher>(&self, state: &mut H) {
107 self.route.hash(state);
108 }
109}
110
111pub trait MatrixBuilder: Send + Sync {
112 fn get_name(&self) -> &'static str;
113 fn get_description(&self) -> &'static str;
114 fn generate_item_matrix(
115 &self,
116 starting_item: ItemSnapshot,
117 target: ItemSnapshot,
118 item_info: &ItemInfoProvider,
119 market_info: &MarketPriceProvider,
120 ) -> Result<ItemMatrix>;
121}
122
123#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
124#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
125#[cfg_attr(feature = "python", pyo3(str))]
126pub struct DynMatrixBuilder(pub Box<dyn MatrixBuilder + Send + Sync>);
127
128impl std::fmt::Display for DynMatrixBuilder {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 write!(
131 f,
132 "Matrix Builder ({})\nDescription: {}",
133 self.0.get_name(),
134 self.0.get_description()
135 )
136 }
137}
138
139pub trait StatisticAnalyzerPaths {
140 fn get_name(&self) -> &'static str;
141 fn get_description(&self) -> &'static str;
142 fn get_unit_type(&self) -> &'static str;
143 fn lower_is_better(&self) -> bool;
144 fn get_statistic(
145 &self,
146 calculator: &Calculator,
147 item_provider: &ItemInfoProvider,
148 market_provider: &MarketPriceProvider,
149 max_routes: u32,
150 max_ram_in_bytes: u64,
151 ) -> Result<Vec<ItemRoute>>;
152 fn calculate_cost_per_craft(
153 &self,
154 currency: &Vec<CraftCurrencyList>,
155 item_info: &ItemInfoProvider,
156 market_provider: &MarketPriceProvider,
157 ) -> PriceInDivines;
158 fn calculate_tries_needed_for_60_percent(&self, route: &ItemRoute) -> u64;
159 fn format_display_more_info(
160 &self,
161 route: &ItemRoute,
162 item_provider: &ItemInfoProvider,
163 market_provider: &MarketPriceProvider,
164 ) -> Option<String>;
165}
166
167pub trait StatisticAnalyzerCurrencyGroups {
168 fn get_name(&self) -> &'static str;
169
170 fn get_description(&self) -> &'static str;
171
172 fn get_unit_type(&self) -> &'static str;
173
174 fn lower_is_better(&self) -> bool;
175
176 fn get_statistic(
177 &self,
178 calculator: &Calculator,
179 item_provider: &ItemInfoProvider,
180 market_provider: &MarketPriceProvider,
181 max_ram_in_bytes: u64,
182 ) -> Result<Vec<GroupRoute>>;
183
184 fn calculate_chance_for_group_step_index(
185 &self,
186 group_routes: &Vec<Vec<RouteChance>>,
187 amount_subpaths: SubpathAmount,
188 index: usize,
189 ) -> RouteChance;
190
191 fn calculate_cost_per_craft(
192 &self,
193 currency: &Vec<CraftCurrencyList>,
194 item_info: &ItemInfoProvider,
195 market_provider: &MarketPriceProvider,
196 ) -> PriceInDivines;
197
198 fn calculate_tries_needed_for_60_percent(&self, group_route: &GroupRoute) -> u64;
199
200 fn format_display_more_info(
201 &self,
202 group_route: &GroupRoute,
203 item_provider: &ItemInfoProvider,
204 market_provider: &MarketPriceProvider,
205 ) -> Option<String>;
206}
207
208#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
209#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
210#[cfg_attr(feature = "python", pyo3(str))]
211pub struct DynStatisticAnalyzerPaths(pub Box<dyn StatisticAnalyzerPaths + Send + Sync>);
212
213impl std::fmt::Display for DynStatisticAnalyzerPaths {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 write!(
216 f,
217 "Statistic Analyzer ({})\nDescription: {}\nLower is better? {}",
218 self.0.get_name(),
219 self.0.get_description(),
220 self.0.lower_is_better(),
221 )
222 }
223}
224
225#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pymethods)]
226#[cfg_attr(feature = "python", pyo3::prelude::pymethods)]
227impl DynStatisticAnalyzerPaths {
228 fn get_name(&self) -> &'static str {
229 self.0.get_name()
230 }
231
232 fn get_description(&self) -> &'static str {
233 self.0.get_description()
234 }
235
236 fn get_unit_type(&self) -> &'static str {
237 self.0.get_unit_type()
238 }
239
240 fn lower_is_better(&self) -> bool {
241 self.0.lower_is_better()
242 }
243
244 #[cfg(feature = "python")]
245 fn get_statistic(
246 &self,
247 calculator: &Calculator,
248 item_provider: &ItemInfoProvider,
249 market_provider: &MarketPriceProvider,
250 max_routes: u32,
251 max_ram_in_bytes: u64,
252 ) -> pyo3::PyResult<Vec<ItemRoute>> {
253 self.0
254 .get_statistic(
255 calculator,
256 item_provider,
257 market_provider,
258 max_routes,
259 max_ram_in_bytes,
260 )
261 .map_err(|err| pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))
262 }
263
264 fn calculate_cost_per_craft(
265 &self,
266 currency: Vec<CraftCurrencyList>,
267 item_info: &ItemInfoProvider,
268 market_provider: &MarketPriceProvider,
269 ) -> PriceInDivines {
270 self.0
271 .calculate_cost_per_craft(¤cy, item_info, market_provider)
272 }
273
274 fn calculate_tries_needed_for_60_percent(&self, route: &ItemRoute) -> u64 {
275 self.0.calculate_tries_needed_for_60_percent(route)
276 }
277
278 fn format_display_more_info(
279 &self,
280 route: &ItemRoute,
281 item_provider: &ItemInfoProvider,
282 market_provider: &MarketPriceProvider,
283 ) -> Option<String> {
284 self.0
285 .format_display_more_info(route, item_provider, market_provider)
286 }
287}
288
289#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
290#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
291#[cfg_attr(feature = "python", pyo3(str))]
292pub struct DynStatisticAnalyzerCurrencyGroups(
293 pub Box<dyn StatisticAnalyzerCurrencyGroups + Send + Sync>,
294);
295
296#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pymethods)]
297#[cfg_attr(feature = "python", pyo3::prelude::pymethods)]
298impl DynStatisticAnalyzerCurrencyGroups {
299 fn get_name(&self) -> &'static str {
300 self.0.get_name()
301 }
302
303 fn get_description(&self) -> &'static str {
304 self.0.get_description()
305 }
306
307 fn get_unit_type(&self) -> &'static str {
308 self.0.get_unit_type()
309 }
310
311 fn lower_is_better(&self) -> bool {
312 self.0.lower_is_better()
313 }
314
315 #[cfg(feature = "python")]
316 fn get_statistic(
317 &self,
318 calculator: &Calculator,
319 item_provider: &ItemInfoProvider,
320 market_provider: &MarketPriceProvider,
321 max_ram_in_bytes: u64,
322 ) -> pyo3::PyResult<Vec<GroupRoute>> {
323 self.0
324 .get_statistic(calculator, item_provider, market_provider, max_ram_in_bytes)
325 .map_err(|err| pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))
326 }
327
328 fn calculate_weight_for_group_step_index(
329 &self,
330 group_routes: Vec<Vec<RouteChance>>,
331 subpath_amount: SubpathAmount,
332 index: usize,
333 ) -> RouteChance {
334 self.0
335 .calculate_chance_for_group_step_index(&group_routes, subpath_amount, index)
336 }
337
338 fn format_display_more_info(
339 &self,
340 group_route: &GroupRoute,
341 item_provider: &ItemInfoProvider,
342 market_provider: &MarketPriceProvider,
343 ) -> Option<String> {
344 self.0
345 .format_display_more_info(group_route, item_provider, market_provider)
346 }
347
348 fn calculate_cost_per_craft(
349 &self,
350 currency: Vec<CraftCurrencyList>,
351 item_info: &ItemInfoProvider,
352 market_provider: &MarketPriceProvider,
353 ) -> PriceInDivines {
354 self.0
355 .calculate_cost_per_craft(¤cy, item_info, market_provider)
356 }
357
358 fn calculate_tries_needed_for_60_percent(&self, group_route: &GroupRoute) -> u64 {
359 self.0.calculate_tries_needed_for_60_percent(group_route)
360 }
361}
362
363impl std::fmt::Display for DynStatisticAnalyzerCurrencyGroups {
364 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365 write!(
366 f,
367 "Statistic Analyzer ({})\nDescription: {}",
368 self.0.get_name(),
369 self.0.get_description()
370 )
371 }
372}
373
374#[derive(Clone, Debug, Serialize, Deserialize)]
375#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
376#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
377#[cfg_attr(
378 feature = "python",
379 pyo3(weakref, from_py_object, get_all, frozen, str)
380)]
381pub struct StatisticResult {
382 pub sorted_routes: Vec<ItemRoute>,
383 pub lower_is_better: bool,
384}
385
386#[derive(Clone, Debug, Serialize, Deserialize)]
387#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
388#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
389#[cfg_attr(feature = "python", pyo3(weakref, from_py_object, get_all, str))]
390pub struct Calculator {
391 pub matrix: ItemMatrix,
392 pub starting_item: ItemSnapshot,
393 pub target_item: ItemSnapshot,
394 pub statistics: THashMap<String, StatisticResult>,
395 pub statistics_grouped: THashMap<String, Vec<GroupRoute>>,
396}
397
398impl Calculator {
399 #[instrument(skip_all)]
400 pub fn generate_item_matrix(
401 starting_item: ItemSnapshot,
402 target: ItemSnapshot,
403 item_provider: &ItemInfoProvider,
404 market_info: &MarketPriceProvider,
405 matrix_builder: &dyn MatrixBuilder,
406 ) -> Result<Self> {
407 tracing::info!(
408 "Using '{}' to generate item matrix ...",
409 matrix_builder.get_name()
410 );
411 tracing::info!("Description: {}", matrix_builder.get_description());
412
413 let res = matrix_builder.generate_item_matrix(
414 starting_item.clone(),
415 target.clone(),
416 item_provider,
417 market_info,
418 )?;
419
420 let reached = res
421 .iter()
422 .any(|test| test.1.item.helper.target_proximity == 0);
423
424 if !reached {
425 return Err(CraftPathError::ItemMatrixCouldNotReachTarget().into());
426 }
427
428 tracing::info!("Successfully generated item matrix.");
429
430 Ok(Self {
431 matrix: res,
432 starting_item: starting_item,
433 target_item: target,
434 statistics: THashMap::default(),
435 statistics_grouped: THashMap::default(),
436 })
437 }
438
439 #[instrument(skip_all)]
440 pub fn calculate_statistics(
441 &self,
442 item_provider: &ItemInfoProvider,
443 market_provider: &MarketPriceProvider,
444 max_routes: u32,
445 max_ram_in_bytes: u64,
446 statistic_analyzer: &dyn StatisticAnalyzerPaths,
447 ) -> Result<Vec<ItemRoute>> {
448 tracing::info!(
449 "Using '{}' to calculate statistics ...",
450 statistic_analyzer.get_name()
451 );
452 tracing::info!("Description: {}", statistic_analyzer.get_description());
453 let res = statistic_analyzer.get_statistic(
454 &self,
455 item_provider,
456 market_provider,
457 max_routes,
458 max_ram_in_bytes,
459 )?;
460 tracing::info!("Successfully calculated statistics.");
461
462 Ok(res)
463 }
464
465 #[instrument(skip_all)]
466 pub fn calculate_statistics_currency_group(
467 &self,
468 item_provider: &ItemInfoProvider,
469 market_provider: &MarketPriceProvider,
470 max_ram_in_bytes: u64,
471 statistic_analyzer: &dyn StatisticAnalyzerCurrencyGroups,
472 ) -> Result<Vec<GroupRoute>> {
473 tracing::info!(
474 "Using '{}' to calculate statistics ...",
475 statistic_analyzer.get_name()
476 );
477 tracing::info!("Description: {}", statistic_analyzer.get_description());
478
479 let res = statistic_analyzer.get_statistic(
480 &self,
481 item_provider,
482 market_provider,
483 max_ram_in_bytes,
484 )?;
485
486 tracing::info!("Successfully calculated statistics.");
487
488 Ok(res)
489 }
490
491 #[instrument(skip_all)]
492 pub fn calculate_target_proximity(
493 start: &ItemSnapshot,
494 target: &ItemSnapshot,
495 provider: &ItemInfoProvider,
496 ) -> Result<u8> {
497 calculate_target_proximity(start, target, provider)
500 }
501
502 #[instrument(skip_all)]
503 pub fn sanity_check_item(_start: &ItemSnapshot, _provider: &ItemInfoProvider) -> bool {
504 todo!()
505
506 }
509}
510
511#[cfg(feature = "python")]
512#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pymethods)]
513#[cfg_attr(feature = "python", pyo3::pymethods)]
514impl Calculator {
515 #[staticmethod]
516 #[pyo3(name = "generate_item_matrix")]
517 fn generate_item_matrix_py(
518 starting_item: ItemSnapshot,
519 target: ItemSnapshot,
520 item_provider: &ItemInfoProvider,
521 market_info: &MarketPriceProvider,
522 matrix_builder: &DynMatrixBuilder,
523 ) -> pyo3::PyResult<Self> {
524 Calculator::generate_item_matrix(
525 starting_item,
526 target,
527 item_provider,
528 market_info,
529 matrix_builder.0.as_ref(),
530 )
531 .map_err(|err| pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))
532 }
533
534 #[pyo3(name = "calculate_statistics")]
535 fn calculate_statistics_py(
536 &mut self,
537 py: pyo3::Python,
538 item_provider: &ItemInfoProvider,
539 market_provider: &MarketPriceProvider,
540 max_routes: u32,
541 max_ram_in_bytes: u64,
542 statistic_analyzer: &DynStatisticAnalyzerPaths,
543 ) -> pyo3::PyResult<Vec<ItemRoute>> {
544 py.detach(move || {
546 self.calculate_statistics(
547 item_provider,
548 market_provider,
549 max_routes,
550 max_ram_in_bytes,
551 statistic_analyzer.0.as_ref(),
552 )
553 .map_err(|err| pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))
554 })
555 }
556
557 #[pyo3(name = "calculate_statistics_currency_group")]
558 pub fn calculate_statistics_currency_group_py(
559 &mut self,
560 item_provider: &ItemInfoProvider,
561 market_provider: &MarketPriceProvider,
562 max_ram_in_bytes: u64,
563 statistic_analyzer: &DynStatisticAnalyzerCurrencyGroups,
564 ) -> pyo3::PyResult<Vec<GroupRoute>> {
565 self.calculate_statistics_currency_group(
566 item_provider,
567 market_provider,
568 max_ram_in_bytes,
569 statistic_analyzer.0.as_ref(),
570 )
571 .map_err(|err| pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))
572 }
573
574 #[staticmethod]
575 #[pyo3(name = "calculate_target_proximity")]
576 fn calculate_target_proximity_py(
577 start: &ItemSnapshot,
578 target: &ItemSnapshot,
579 provider: &ItemInfoProvider,
580 ) -> pyo3::PyResult<u8> {
581 Calculator::calculate_target_proximity(start, target, provider)
582 .map_err(|err| pyo3::exceptions::PyRuntimeError::new_err(err.to_string()))
583 }
584
585 #[staticmethod]
586 #[pyo3(name = "sanity_check_item")]
587 fn sanity_check_item_py(start: &ItemSnapshot, provider: &ItemInfoProvider) -> bool {
588 Calculator::sanity_check_item(start, provider)
589 }
590}
591
592#[cfg(feature = "python")]
593crate::derive_DebugDisplay!(
594 PropagationTarget,
595 ItemMatrixNode,
596 Calculator,
597 ItemRouteNode,
598 ItemRoute,
599 StatisticResult,
600 GroupRoute
601);