pyoe2_craftpath/api/
calculator.rs

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// do not include references ??
53// item and chance are w/e since sizewise nothing changes u64 + u32 + u32 (+ struct)
54// but HashSet could be costly?? if to much revert to ref
55#[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// this needs to be converted to Python types either way
69#[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, // for internal 15-17 digit precision, i think inaccuracies on deep paths are acceptable, if not swap to rust_decimal
79    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(&currency, 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(&currency, 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        // return 0 if target item AFFIXES reached -> can be followed with some socketing shenanigans or sth
498        // return 12 on max distance
499        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        // provide an item and check if the selected mods are reachable.
507        // e. g. exclusive mods, multiple fractures etc.
508    }
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        // allow parallelization
545        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);