pyoe2_craftpath/api/
item.rs

1use std::hash::{Hash, Hasher};
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::fmt::Write;
6
7use crate::{
8    api::{
9        calculator::Calculator,
10        provider::item_info::ItemInfoProvider,
11        types::{
12            AffixClassEnum, AffixDefinition, AffixLocationEnum, AffixSpecifier,
13            AffixTierLevelBoundsEnum, BaseItemId, ItemLevel, ItemRarityEnum, THashSet,
14        },
15    },
16    utils::{hash_utils::hash_set_unordered, pretty_print_unique_utils::print_affix},
17};
18
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
20#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
21#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
22#[cfg_attr(
23    feature = "python",
24    pyo3(eq, weakref, from_py_object, frozen, hash, get_all, str)
25)]
26pub struct ItemSnapshot {
27    pub item_level: ItemLevel,
28    pub rarity: ItemRarityEnum,
29    pub base_id: BaseItemId,
30    pub affixes: THashSet<AffixSpecifier>,
31    pub corrupted: bool,
32    pub allowed_sockets: u8,
33    pub sockets: THashSet<AffixSpecifier>,
34}
35
36impl ItemSnapshot {
37    pub fn to_pretty_string(
38        &self,
39        item_provider: &ItemInfoProvider,
40        print_affixes: bool,
41    ) -> String {
42        let mut out = String::new();
43
44        let base_group_id = item_provider.lookup_base_group(&self.base_id).unwrap();
45        let base_group_def = item_provider
46            .lookup_base_group_definition(&base_group_id)
47            .unwrap();
48
49        writeln!(
50            &mut out,
51            "Base Group: {} (#{}), Max Rarity: {}, Max Affixes: {} ({} per side), Max. Sockets: {} ({} corrupt)",
52            base_group_def.name_base_group,
53            base_group_id.get_raw_value(),
54            match base_group_def.is_rare {
55                true => "Rare",
56                false => "Magic",
57            },
58            base_group_def.max_affix,
59            base_group_def.max_affix / 2,
60            base_group_def.max_sockets,
61            base_group_def.max_sockets + 1,
62        )
63        .unwrap();
64
65        writeln!(
66            &mut out,
67            "BaseId: #{}, Rarity: {:?}, ItemLevel: {}, Sockets: {}",
68            self.base_id.get_raw_value(),
69            self.rarity,
70            self.item_level.get_raw_value(),
71            self.allowed_sockets
72        )
73        .unwrap();
74
75        if print_affixes {
76            for affix in &self.affixes {
77                print_affix(
78                    &mut out,
79                    None,
80                    affix,
81                    None,
82                    &item_provider,
83                    false,
84                    &self.base_id,
85                    false,
86                );
87            }
88        }
89
90        return out;
91    }
92}
93
94#[cfg(feature = "python")]
95#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pymethods)]
96#[cfg_attr(feature = "python", pyo3::prelude::pymethods)]
97impl ItemSnapshot {
98    #[pyo3(name = "to_pretty_string")]
99    pub fn to_pretty_string_py(
100        &self,
101        item_provider: &ItemInfoProvider,
102        print_affixes: bool,
103    ) -> String {
104        self.to_pretty_string(item_provider, print_affixes)
105    }
106}
107
108impl Hash for ItemSnapshot {
109    fn hash<H: Hasher>(&self, state: &mut H) {
110        self.item_level.hash(state);
111        self.rarity.hash(state);
112        self.base_id.hash(state);
113        self.corrupted.hash(state);
114        self.allowed_sockets.hash(state);
115
116        let affix_hash = hash_set_unordered(&self.affixes);
117        let socket_hash = hash_set_unordered(&self.sockets);
118
119        affix_hash.hash(state);
120        socket_hash.hash(state);
121    }
122}
123
124#[cfg(feature = "python")]
125crate::derive_DebugDisplay!(ItemSnapshot);
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
129#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
130#[cfg_attr(feature = "python", pyo3(weakref, from_py_object, get_all, str))]
131pub struct ItemSnapshotHelper {
132    // distance of affixes to target
133    // 0 -> target item
134    // 6 -> empty item, to target item with 6 wanted affixes
135    // 12 -> 6 unwanted affixes, to target item with 6 wanted affixes
136    pub target_proximity: u8,
137    pub prefix_count: u8,
138    pub suffix_count: u8,
139    pub blocked_modgroups: THashSet<String>,
140    pub homogenized_mods: THashSet<u8>,
141    pub unwanted_affixes: THashSet<AffixSpecifier>,
142    pub is_desecrated: bool,
143    pub has_desecrated_target: Option<AffixSpecifier>,
144    pub marked_by_abyssal_lord: Option<AffixSpecifier>,
145    pub has_essences_target: THashSet<AffixSpecifier>,
146}
147
148// idk if item needs to be marked for sth
149#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
150#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
151#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
152#[cfg_attr(feature = "python", pyo3(eq, weakref, from_py_object, get_all, str))]
153pub struct ItemTechnicalMeta {
154    pub mark_for_essence_only: bool,
155}
156
157impl ItemTechnicalMeta {
158    pub fn default() -> Self {
159        Self {
160            mark_for_essence_only: false,
161        }
162    }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pyclass)]
167#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
168#[cfg_attr(feature = "python", pyo3(weakref, from_py_object, get_all, str))]
169pub struct Item {
170    pub snapshot: ItemSnapshot,
171    pub helper: ItemSnapshotHelper,
172    pub meta: ItemTechnicalMeta,
173}
174
175#[cfg(feature = "python")]
176crate::derive_DebugDisplay!(Item, ItemTechnicalMeta, ItemSnapshotHelper);
177
178impl Item {
179    pub fn build_with(
180        snapshot: ItemSnapshot,
181        target: &ItemSnapshot,
182        provider: &ItemInfoProvider,
183    ) -> Result<Self> {
184        let mut blocked_modgroups = THashSet::default();
185        let mut homogenized_mods = THashSet::default();
186        let mut unwanted_affixes = THashSet::default();
187        let mut is_desecrated = false;
188        let mut prefix_count = 0;
189        let mut suffix_count = 0;
190
191        for specifier in &snapshot.affixes {
192            let def = provider.lookup_affix_definition(&specifier.affix)?;
193
194            blocked_modgroups.extend(def.exlusive_groups.iter().cloned());
195            homogenized_mods.extend(def.tags.iter().cloned());
196
197            if !provider.is_abyssal_mark(&specifier.affix)
198                && def.affix_class == AffixClassEnum::Desecrated
199            {
200                is_desecrated = true;
201            }
202
203            // Count by affix location
204            match def.affix_location {
205                AffixLocationEnum::Prefix => prefix_count += 1,
206                AffixLocationEnum::Suffix => suffix_count += 1,
207                AffixLocationEnum::Socket => {} // TODO? <-- this will be in own
208            }
209
210            // Determine if this affix is unwanted
211            let unwanted = match target.affixes.iter().find(|t| t.affix == specifier.affix) {
212                Some(t) => match t.tier.bounds {
213                    AffixTierLevelBoundsEnum::Exact if t.tier.tier != specifier.tier.tier => true,
214                    AffixTierLevelBoundsEnum::Minimum if t.tier.tier < specifier.tier.tier => true,
215                    _ => false,
216                },
217                None => true,
218            };
219
220            if unwanted {
221                unwanted_affixes.insert(specifier.clone());
222            }
223        }
224
225        fn find_target<F>(
226            target: &THashSet<AffixSpecifier>,
227            provider: &ItemInfoProvider,
228            pred: F,
229        ) -> Option<AffixSpecifier>
230        where
231            F: Fn(Option<&AffixDefinition>, &AffixSpecifier) -> bool,
232        {
233            for spec in target.iter() {
234                if pred(provider.lookup_affix_definition(&spec.affix).ok(), spec) {
235                    return Some(spec.clone());
236                }
237            }
238            None
239        }
240
241        let has_desecrated_target = find_target(
242            &target.affixes,
243            provider,
244            |def, _| matches!(def, Some(def) if def.affix_class == AffixClassEnum::Desecrated),
245        );
246
247        let marked_by_abyssal_lord = find_target(&target.affixes, provider, |_, spec| {
248            provider.is_abyssal_mark(&spec.affix)
249        });
250
251        let has_essences_target = target
252            .affixes
253            .iter()
254            .filter_map(|spec| match provider.lookup_affix_definition(&spec.affix) {
255                Ok(def) if def.affix_class == AffixClassEnum::Essence => Some(Ok(spec.clone())),
256                Ok(_) => None,
257                Err(e) => Some(Err(e)),
258            })
259            .collect::<Result<THashSet<_>, _>>()?;
260
261        let target_proximity =
262            Calculator::calculate_target_proximity(&snapshot, &target, &provider)?;
263
264        Ok(Self {
265            snapshot,
266            helper: ItemSnapshotHelper {
267                prefix_count,
268                suffix_count,
269                blocked_modgroups,
270                homogenized_mods,
271                unwanted_affixes,
272                is_desecrated,
273                has_desecrated_target,
274                marked_by_abyssal_lord,
275                has_essences_target,
276                target_proximity,
277            },
278            meta: ItemTechnicalMeta::default(),
279        })
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use anyhow::Result;
286    use tracing::instrument;
287
288    use crate::{
289        api::{
290            item::{Item, ItemSnapshot},
291            types::{
292                AffixId, AffixSpecifier, AffixTierConstraints, AffixTierLevel,
293                AffixTierLevelBoundsEnum, BaseItemId, ItemLevel, ItemRarityEnum, THashMap,
294                THashSet,
295            },
296        },
297        external_api::{
298            coe::craftofexile_data_provider_adapter::CraftOfExileItemInfoProvider,
299            fetch_json_from_urls::retrieve_contents_from_urls_with_cache_unstable_order,
300        },
301        utils::logger_utils::init_tracing,
302    };
303
304    #[test]
305    #[instrument]
306    fn test_item_snapshot() -> Result<()> {
307        init_tracing();
308        tracing::info!("Checking correct function of ItemSnapshot comparisons");
309
310        let mut item_snapshot_a = ItemSnapshot {
311            item_level: ItemLevel::from(100),
312            affixes: THashSet::default(),
313            base_id: BaseItemId::from(20),
314            rarity: ItemRarityEnum::Rare,
315            corrupted: false,
316            allowed_sockets: 0,
317            sockets: THashSet::default(),
318        };
319
320        let mut item_snapshot_b = ItemSnapshot {
321            item_level: ItemLevel::from(100),
322            affixes: THashSet::default(),
323            base_id: BaseItemId::from(20),
324            rarity: ItemRarityEnum::Rare,
325            corrupted: false,
326            allowed_sockets: 0,
327            sockets: THashSet::default(),
328        };
329
330        item_snapshot_a.affixes.insert(AffixSpecifier {
331            affix: AffixId::from(5119),
332            tier: AffixTierConstraints {
333                bounds: AffixTierLevelBoundsEnum::Exact,
334                tier: AffixTierLevel::from(3),
335            },
336            fractured: false,
337        });
338
339        item_snapshot_b.affixes.insert(AffixSpecifier {
340            affix: AffixId::from(5119),
341            tier: AffixTierConstraints {
342                bounds: AffixTierLevelBoundsEnum::Exact,
343                tier: AffixTierLevel::from(3),
344            },
345            fractured: false,
346        });
347
348        assert_eq!(item_snapshot_a, item_snapshot_b);
349        assert_eq!(item_snapshot_b, item_snapshot_a);
350
351        item_snapshot_a.affixes.insert(AffixSpecifier {
352            affix: AffixId::from(5121),
353            tier: AffixTierConstraints {
354                bounds: AffixTierLevelBoundsEnum::Exact,
355                tier: AffixTierLevel::from(3),
356            },
357            fractured: false,
358        });
359
360        item_snapshot_b.affixes.insert(AffixSpecifier {
361            affix: AffixId::from(5127),
362            tier: AffixTierConstraints {
363                bounds: AffixTierLevelBoundsEnum::Exact,
364                tier: AffixTierLevel::from(3),
365            },
366            fractured: false,
367        });
368
369        assert_ne!(item_snapshot_a, item_snapshot_b);
370
371        item_snapshot_b.affixes.insert(AffixSpecifier {
372            affix: AffixId::from(5121),
373            tier: AffixTierConstraints {
374                bounds: AffixTierLevelBoundsEnum::Exact,
375                tier: AffixTierLevel::from(3),
376            },
377            fractured: false,
378        });
379
380        item_snapshot_a.affixes.insert(AffixSpecifier {
381            affix: AffixId::from(5127),
382            tier: AffixTierConstraints {
383                bounds: AffixTierLevelBoundsEnum::Exact,
384                tier: AffixTierLevel::from(3),
385            },
386            fractured: false,
387        });
388
389        assert_eq!(item_snapshot_a, item_snapshot_b);
390
391        tracing::info!("Checking correct function of initializing actual items");
392
393        let hm = THashMap::from_iter(
394            vec![(
395                "./cache/coe2.json".to_string(),
396                "https://www.craftofexile.com/json/poe2/main/poec_data.json".to_string(),
397            )]
398            .into_iter(),
399        );
400
401        let provider = retrieve_contents_from_urls_with_cache_unstable_order(hm, 60_u64 * 60_u64)?;
402        let provider = CraftOfExileItemInfoProvider::parse_from_json(
403            provider.first().expect("Provider returned no item info"),
404        )?;
405
406        let item = Item::build_with(item_snapshot_a.clone(), &item_snapshot_b, &provider)?;
407        assert_eq!(item.helper.unwanted_affixes.len(), 0);
408        assert_eq!(item.helper.target_proximity, 0); // item reached wanted form
409
410        tracing::info!("{:?}", item);
411
412        item_snapshot_b
413            .affixes
414            .retain(|test| test.affix != AffixId::from(5119));
415
416        let item = Item::build_with(item_snapshot_a.clone(), &item_snapshot_b, &provider)?;
417        assert_eq!(item.helper.unwanted_affixes.len(), 1); // starting item has affix that is not in target
418        assert_eq!(item.helper.target_proximity, 2); // item requires removal of affix + applience of affix = 2
419
420        let item = Item::build_with(item_snapshot_b.clone(), &item_snapshot_a, &provider)?;
421        assert_eq!(item.helper.unwanted_affixes.len(), 0); // starting items affixes all are included in target 
422        assert_eq!(item.helper.target_proximity, 1); // item requires addition of affix = 1
423
424        tracing::info!("{:?}", item);
425
426        Ok(())
427    }
428}