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 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#[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 match def.affix_location {
205 AffixLocationEnum::Prefix => prefix_count += 1,
206 AffixLocationEnum::Suffix => suffix_count += 1,
207 AffixLocationEnum::Socket => {} }
209
210 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); 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); assert_eq!(item.helper.target_proximity, 2); let item = Item::build_with(item_snapshot_b.clone(), &item_snapshot_a, &provider)?;
421 assert_eq!(item.helper.unwanted_affixes.len(), 0); assert_eq!(item.helper.target_proximity, 1); tracing::info!("{:?}", item);
425
426 Ok(())
427 }
428}