Skip to main content

sf_api/gamestate/
items.rs

1use std::cmp::Ordering;
2
3use chrono::{DateTime, Local};
4use enum_map::{Enum, EnumMap};
5use log::warn;
6use num_derive::FromPrimitive;
7use num_traits::FromPrimitive;
8use strum::{EnumCount, EnumIter};
9
10use super::{
11    CFPGet, Class, EnumMapGet, HabitatType, SFError, ServerTime,
12    unlockables::EquipmentIdent,
13};
14use crate::{
15    command::{AttributeType, ShopType},
16    gamestate::{CCGet, CGet, ShopPosition},
17};
18
19/// The basic inventory, that every player has
20#[derive(Debug, Default, Clone, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct Inventory {
23    pub backpack: Vec<Option<Item>>,
24}
25
26/// The game keeps track between 5 slot bag and the extended inventory.
27#[derive(Debug, Default, Clone, PartialEq, Eq, Copy)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct BagPosition(pub(crate) usize);
30
31impl BagPosition {
32    /// The 0 based index into the backpack vec, where the item is parsed into
33    #[must_use]
34    pub fn backpack_pos(&self) -> usize {
35        self.0
36    }
37    /// The inventory type and position within it, where the item is stored
38    /// according to previous inventory management logic. This is what you use
39    /// for commands
40    #[must_use]
41    pub fn inventory_pos(&self) -> (InventoryType, usize) {
42        let pos = self.0;
43        if pos <= 4 {
44            (InventoryType::MainInventory, pos)
45        } else {
46            (InventoryType::ExtendedInventory, pos - 5)
47        }
48    }
49}
50
51impl Inventory {
52    // Splits the backpack, as if it was the old bag/fortress chest layout.
53    // The first slice will be the bag, the second the fortress chest.
54    // If the backback if empty for unknown reasons, or is shorter than 5
55    // elements, both slices will be empty
56    #[must_use]
57    pub fn as_split(&self) -> (&[Option<Item>], &[Option<Item>]) {
58        if self.backpack.len() < 5 {
59            return (&[], &[]);
60        }
61        self.backpack.split_at(5)
62    }
63
64    // Splits the backpack, as if it was the old bag/fortress chest layout.
65    // The first slice will be the bag, the second the fortress chest
66    // If the backback if empty for unknown reasons, or is shorter than 5
67    // elements, both slices will be emptys
68    #[must_use]
69    pub fn as_split_mut(
70        &mut self,
71    ) -> (&mut [Option<Item>], &mut [Option<Item>]) {
72        if self.backpack.len() < 5 {
73            return (&mut [], &mut []);
74        }
75        self.backpack.split_at_mut(5)
76    }
77
78    /// Returns a place in the inventory, that can store a new item.
79    /// This is only useful, when you are dealing with commands, that require
80    /// a free slot position. The index will be 0 based per inventory
81    #[must_use]
82    pub fn free_slot(&self) -> Option<BagPosition> {
83        for (pos, item) in self.iter() {
84            if item.is_none() {
85                return Some(pos);
86            }
87        }
88        None
89    }
90
91    #[must_use]
92    pub fn count_free_slots(&self) -> usize {
93        self.backpack.iter().filter(|slot| slot.is_none()).count()
94    }
95
96    /// Creates an iterator over the inventory slots.
97    pub fn iter(&self) -> impl Iterator<Item = (BagPosition, Option<&Item>)> {
98        self.backpack
99            .iter()
100            .enumerate()
101            .map(|(pos, item)| (BagPosition(pos), item.as_ref()))
102    }
103}
104
105/// All the parts of `ItemPlace`, that are owned by the player
106#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
108#[allow(missing_docs)]
109pub enum PlayerItemPlace {
110    Equipment = 1,
111    MainInventory = 2,
112    ExtendedInventory = 5,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117pub struct ItemPosition {
118    pub place: ItemPlace,
119    pub position: usize,
120}
121
122impl std::fmt::Display for ItemPosition {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        write!(f, "{}/{}", self.place as usize, self.position + 1)
125    }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub struct PlayerItemPosition {
131    pub place: PlayerItemPlace,
132    pub position: usize,
133}
134
135impl std::fmt::Display for PlayerItemPosition {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(f, "{}/{}", self.place as usize, self.position + 1)
138    }
139}
140
141impl From<PlayerItemPosition> for ItemPosition {
142    fn from(value: PlayerItemPosition) -> Self {
143        Self {
144            place: value.place.item_position(),
145            position: value.position,
146        }
147    }
148}
149
150impl From<BagPosition> for ItemPosition {
151    fn from(value: BagPosition) -> Self {
152        let player: PlayerItemPosition = value.into();
153        player.into()
154    }
155}
156
157impl From<EquipmentSlot> for ItemPosition {
158    fn from(value: EquipmentSlot) -> Self {
159        let player: PlayerItemPosition = value.into();
160        player.into()
161    }
162}
163
164impl From<ShopPosition> for ItemPosition {
165    fn from(value: ShopPosition) -> Self {
166        Self {
167            place: value.typ.into(),
168            position: value.pos,
169        }
170    }
171}
172
173impl From<ShopType> for ItemPlace {
174    fn from(value: ShopType) -> Self {
175        match value {
176            ShopType::Weapon => ItemPlace::WeaponShop,
177            ShopType::Magic => ItemPlace::MageShop,
178        }
179    }
180}
181
182impl From<BagPosition> for PlayerItemPosition {
183    fn from(value: BagPosition) -> Self {
184        let p = value.inventory_pos();
185        Self {
186            place: p.0.player_item_position(),
187            position: p.1,
188        }
189    }
190}
191
192impl From<EquipmentSlot> for PlayerItemPosition {
193    fn from(value: EquipmentSlot) -> Self {
194        Self {
195            place: PlayerItemPlace::Equipment,
196            position: value as usize - 1,
197        }
198    }
199}
200
201impl PlayerItemPlace {
202    /// `InventoryType` is a subset of `ItemPlace`. This is a convenient
203    /// function to convert between them
204    #[must_use]
205    pub fn item_position(&self) -> ItemPlace {
206        match self {
207            PlayerItemPlace::Equipment => ItemPlace::Equipment,
208            PlayerItemPlace::MainInventory => ItemPlace::MainInventory,
209            PlayerItemPlace::ExtendedInventory => ItemPlace::FortressChest,
210        }
211    }
212}
213
214/// All the parts of `ItemPlace`, that are owned by the player
215#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217#[allow(missing_docs)]
218pub enum InventoryType {
219    MainInventory = 2,
220    ExtendedInventory = 5,
221}
222
223impl InventoryType {
224    /// `InventoryType` is a subset of `ItemPlace`. This is a convenient
225    /// function to convert between them
226    #[must_use]
227    pub fn item_position(&self) -> ItemPlace {
228        match self {
229            InventoryType::MainInventory => ItemPlace::MainInventory,
230            InventoryType::ExtendedInventory => ItemPlace::FortressChest,
231        }
232    }
233    /// `InventoryType` is a subset of `ItemPlace`. This is a convenient
234    /// function to convert between them
235    #[must_use]
236    pub fn player_item_position(&self) -> PlayerItemPlace {
237        match self {
238            InventoryType::MainInventory => PlayerItemPlace::MainInventory,
239            InventoryType::ExtendedInventory => {
240                PlayerItemPlace::ExtendedInventory
241            }
242        }
243    }
244}
245
246/// All places, that items can be dragged to excluding companions
247#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
248#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
249pub enum ItemPlace {
250    /// The stuff a player can wear
251    Equipment = 1,
252    /// All items in the main 5 inventory slots
253    MainInventory = 2,
254    /// The items in the weapon slot
255    WeaponShop = 3,
256    /// The items in the mage slot
257    MageShop = 4,
258    /// The items in the fortress chest slots
259    FortressChest = 5,
260}
261
262/// All the equipment a player is wearing
263#[derive(Debug, Default, Clone)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
265pub struct Equipment(pub EnumMap<EquipmentSlot, Option<Item>>);
266
267impl Equipment {
268    /// Checks if the character has an item with the enchantment equipped
269    #[must_use]
270    pub fn has_enchantment(&self, enchantment: Enchantment) -> bool {
271        let item = self.0.get(enchantment.equipment_slot());
272        if let Some(item) = item {
273            return item.enchantment == Some(enchantment);
274        }
275        false
276    }
277
278    /// Expects the input `data` to have items directly at data[0]
279    #[allow(clippy::indexing_slicing)]
280    pub(crate) fn parse(
281        data: &[i64],
282        server_time: ServerTime,
283    ) -> Result<Equipment, SFError> {
284        let mut res = Equipment::default();
285        if !data.len().is_multiple_of(ITEM_PARSE_LEN) {
286            return Err(SFError::ParsingError(
287                "Invalid Equipment",
288                format!("{data:?}"),
289            ));
290        }
291        for (chunk, slot) in
292            data.chunks_exact(ITEM_PARSE_LEN).zip(res.0.as_mut_slice())
293        {
294            *slot = Item::parse(chunk, server_time)?;
295        }
296        Ok(res)
297    }
298}
299
300pub(crate) const ITEM_PARSE_LEN: usize = 19;
301
302/// Information about a single item. This can be anything, that is either in a
303/// inventory, in a reward slot, or similar
304#[derive(Debug, Clone, PartialEq, Eq)]
305#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
306pub struct Item {
307    /// The type of this item. May contain further type specific values
308    pub typ: ItemType,
309    /// Either the price to buy, or sell
310    pub price: u32,
311    /// The price you would have to pay for this item. Note that this value is
312    /// junk for other players and potentially in other cases, where you should
313    /// not be able to see a price
314    pub mushroom_price: u32,
315    /// The non-truncated version of the model id. The normal `model_id` is
316    /// fine to identify this item visually, but this here is for doing more
317    /// specific calculations, apart from that
318    pub full_model_id: u32,
319    /// The model id of this item
320    pub model_id: u16,
321    /// The class restriction, that this item has. Will only cover the three
322    /// main classes
323    pub class: Option<Class>,
324    /// Either the armor, weapon dmg, or other. You should be using `armor()`,
325    /// or the weapon types damages though, if you want to have a safe
326    /// abstraction. This is only public in case I am missing a case here
327    pub type_specific_val: u32,
328    /// The stats this item gives, when equipped
329    pub attributes: EnumMap<AttributeType, u32>,
330    /// The gemslot of this item, if any. A gemslot can be filled or empty
331    pub gem_slot: Option<GemSlot>,
332    /// The rune on this item
333    pub rune: Option<Rune>,
334    /// The enchantment applied to this item
335    pub enchantment: Option<Enchantment>,
336    /// This is the color, or other cosmetic variation of an item. There is no
337    /// clear 1 => red mapping, so only the raw value here
338    pub color: u8,
339    /// The amount of times this item has been upgraded at the blacksmith
340    pub upgrade_count: u8,
341    /// The quality level of this item
342    pub item_quality: u32,
343    /// Has this item been through the washing cycle?
344    pub is_washed: bool,
345}
346
347#[derive(Debug, Clone, Copy, PartialEq)]
348#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
349pub struct ItemCommandIdent {
350    typ: u8,
351    full_model_id: u32,
352    price: u32,
353    mush_price: u32,
354}
355
356impl std::fmt::Display for ItemCommandIdent {
357    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358        write!(
359            f,
360            "{}/{}/{}/{}",
361            self.typ, self.full_model_id, self.price, self.mush_price
362        )
363    }
364}
365
366#[derive(Debug, Clone, Copy)]
367pub struct BlacksmithPayment {
368    pub metal: u64,
369    pub arcane: u64,
370}
371
372impl Item {
373    /// Calculates the amount of metal & arcane we are expected to receive from
374    /// the blacksmith
375    ///
376    /// This code is a direct port of the implementation available here:
377    /// <https://snfsmithsim.12hp.de>/ . As such, all credit goes to:
378    /// `ÐonMuErte`, `Werwolf Legion (F17)` & `Rising Phoenix (F21)`
379    #[must_use]
380    pub fn dismantle_reward(&self) -> BlacksmithPayment {
381        let mut attribute_val =
382            f64::from(*self.attributes.values().max().unwrap_or(&0));
383        let item_stats = self.attributes.values().filter(|a| **a > 0).count();
384        let is_scout_or_mage_weapon = self
385            .class
386            .is_some_and(|a| a == Class::Scout || a == Class::Mage)
387            && self.typ.is_weapon();
388
389        if self.price != 0 {
390            for _ in 0..self.upgrade_count {
391                attribute_val = (attribute_val / 1.04).round();
392            }
393        }
394
395        if item_stats >= 4 {
396            attribute_val *= 1.2;
397        }
398        if is_scout_or_mage_weapon {
399            attribute_val /= 2.0;
400        }
401        // // 1-stat items
402        if (item_stats == 1) && attribute_val > 66.0 {
403            attribute_val = attribute_val.round() * 0.75;
404        }
405
406        attribute_val = attribute_val.round().powf(1.2).floor();
407
408        let (min_dmg, max_dmg) = match self.typ {
409            ItemType::Weapon { min_dmg, max_dmg } => (min_dmg, max_dmg),
410            _ => (0, 0),
411        };
412
413        let price = (u32::from(self.typ.raw_id()) * 37)
414            + (self.full_model_id * 83)
415            + (min_dmg * 1731)
416            + (max_dmg * 162);
417
418        let (metal_price, arcane_price) = match item_stats {
419            1 => (75 + (price % 26), price % 2),
420            2 => (50 + (price % 31), 5 + (price % 6)),
421            // Epics
422            _ => (25 + (price % 26), 50 + (price % 51)),
423        };
424
425        #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
426        let calc_result = |rng: u32| {
427            ((attribute_val * f64::from(rng)) / 100.0).floor() as u64
428        };
429        let mut metal_result = calc_result(metal_price);
430        let mut arcane_result = calc_result(arcane_price);
431
432        if is_scout_or_mage_weapon {
433            metal_result *= 2;
434            arcane_result *= 2;
435        }
436        BlacksmithPayment {
437            metal: metal_result * 2,
438            arcane: arcane_result * 2,
439        }
440    }
441
442    /// Calculates the amount of metal & arcane it would cost to upgrade this
443    /// item. Each upgrade increases the highest attribute by 3% (all highest
444    /// for epics)
445    ///
446    /// This code is a direct port of the implementation available here:
447    /// <https://snfsmithsim.12hp.de>/ . As such, all credit goes to:
448    /// `ÐonMuErte`, `Werwolf Legion (F17)` & `Rising Phoenix (F21)`
449    #[must_use]
450    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
451    pub fn upgrade_costs(&self) -> Option<BlacksmithPayment> {
452        if self.upgrade_count >= 20 || self.equipment_ident().is_none() {
453            return None;
454        }
455
456        let item_stats = self.attributes.values().filter(|a| **a > 0).count();
457        let is_scout_or_mage_weapon = self
458            .class
459            .is_some_and(|a| a == Class::Scout || a == Class::Mage)
460            && self.typ.is_weapon();
461
462        // Highest attribue is the base price
463        let mut price =
464            f64::from(*self.attributes.values().max().unwrap_or(&0));
465
466        // 5-stats items
467        if item_stats >= 4 {
468            price *= 1.2;
469        }
470
471        if is_scout_or_mage_weapon {
472            price /= 2.0;
473        }
474
475        // 1-stat items
476        if item_stats == 1 && price > 66.0 {
477            price = (price * 0.75).ceil();
478        }
479
480        price = price.round().powf(1.2).floor();
481
482        let mut metal_price = 50;
483        let mut arcane_price = match item_stats {
484            1 => 25,
485            2 => 50,
486            // Epics
487            _ => 75,
488        };
489
490        let i = i64::from(self.upgrade_count);
491        match i {
492            0 => {
493                metal_price *= 3;
494                arcane_price = 0;
495            }
496            1 => {
497                metal_price *= 4;
498                arcane_price = 1;
499            }
500            2..=7 => {
501                metal_price *= i + 3;
502                arcane_price *= i - 1;
503            }
504            8 => {
505                metal_price *= 12;
506                arcane_price *= 8;
507            }
508            9 => {
509                metal_price *= 15;
510                arcane_price *= 10;
511            }
512            _ => {
513                metal_price *= i + 6;
514                arcane_price *= 10 + 2 * (i - 9);
515            }
516        }
517
518        metal_price = ((price * (metal_price as f64)) / 100.0).floor() as i64;
519        arcane_price = ((price * (arcane_price as f64)) / 100.0).floor() as i64;
520
521        if is_scout_or_mage_weapon {
522            metal_price *= 2;
523            arcane_price *= 2;
524        }
525
526        Some(BlacksmithPayment {
527            metal: metal_price.try_into().unwrap_or(0),
528            arcane: arcane_price.try_into().unwrap_or(0),
529        })
530    }
531
532    /// Maps an item to its ident. This is mainly useful, if you want to see,
533    /// if a item is already in your scrapbook
534    #[must_use]
535    pub fn equipment_ident(&self) -> Option<EquipmentIdent> {
536        Some(EquipmentIdent {
537            class: self.class,
538            typ: self.typ.equipment_slot()?,
539            model_id: self.model_id,
540            color: self.color,
541        })
542    }
543
544    /// Commands require an ident for the source ident now. Most likely to make
545    /// sure the item has not changed, which could be the case in the shop.
546    /// This function produces the required identification for an item
547    #[must_use]
548    pub fn command_ident(&self) -> ItemCommandIdent {
549        ItemCommandIdent {
550            typ: self.typ.raw_id(),
551            full_model_id: self.full_model_id,
552            price: self.price,
553            mush_price: self.mushroom_price,
554        }
555    }
556
557    /// Checks, if this item is unique. Technically they are not always unique,
558    /// as the scrapbook/keys can be sold, but it should be clear what this is
559    #[must_use]
560    pub fn is_unique(&self) -> bool {
561        self.typ.is_unique()
562    }
563
564    /// Checks if this item is an epic
565    #[must_use]
566    pub fn is_epic(&self) -> bool {
567        self.model_id >= 50
568    }
569
570    /// Checks if this item is a legendary
571    #[must_use]
572    pub fn is_legendary(&self) -> bool {
573        self.model_id >= 90
574    }
575
576    /// The armor rating of this item. This is just the `effect_val`, if any
577    #[must_use]
578    pub fn armor(&self) -> u32 {
579        #[allow(clippy::enum_glob_use)]
580        use ItemType::*;
581        match self.typ {
582            Hat | BreastPlate | Gloves | FootWear | Amulet | Belt | Ring
583            | Talisman => self.type_specific_val,
584            _ => 0,
585        }
586    }
587
588    /// Checks, if this item can be enchanted
589    #[must_use]
590    pub fn is_enchantable(&self) -> bool {
591        self.typ.is_enchantable()
592    }
593
594    /// Checks if a companion of the given class can equip this item.
595    ///
596    /// Returns `true` if the item itself is equipment and this class has the
597    /// ability to wear it
598    #[must_use]
599    pub fn can_be_equipped_by_companion(
600        &self,
601        class: impl Into<Class>,
602    ) -> bool {
603        !self.typ.is_shield() && self.can_be_equipped_by(class.into())
604    }
605
606    /// Checks if a character of the given class can equip this item. Note that
607    /// this only checks the class, so this will make no sense if you use this
608    /// for anything that can not equip items at all (monsters, etc.). For
609    /// companions you should use `can_companion_equip`
610    ///
611    /// Returns `true` if the item itself is equipment and this class has the
612    /// ability to wear it
613    #[must_use]
614    pub fn can_be_equipped_by(&self, class: Class) -> bool {
615        self.typ.equipment_slot().is_some() && self.can_be_used_by(class)
616    }
617
618    /// Checks if a character of the given class can use this item. If you want
619    /// to check equipment, you should use `can_be_equipped_by`
620    ///
621    /// Returns `true` if the item does not have a class requirement, or if the
622    /// class requirement matches the given class.
623    #[must_use]
624    #[allow(clippy::enum_glob_use, clippy::match_same_arms)]
625    pub fn can_be_used_by(&self, class: Class) -> bool {
626        use Class::*;
627
628        // Without a class requirement any class can use this
629        let Some(class_requirement) = self.class else {
630            return true;
631        };
632
633        match class {
634            Warrior | Paladin => class_requirement == Warrior,
635            Berserker => class_requirement == Warrior && !self.typ.is_shield(),
636            Scout => class_requirement == Scout,
637            Mage | Necromancer => class_requirement == Mage,
638            Assassin => match class_requirement {
639                Warrior => self.typ.is_weapon(),
640                Scout => !self.typ.is_weapon(),
641                _ => false,
642            },
643            Bard | Druid => match class_requirement {
644                Mage => self.typ.is_weapon(),
645                Scout => !self.typ.is_weapon(),
646                _ => false,
647            },
648            BattleMage | PlagueDoctor => match class_requirement {
649                Warrior => self.typ.is_weapon(),
650                Mage => !self.typ.is_weapon(),
651                _ => false,
652            },
653            DemonHunter => match class_requirement {
654                Scout => self.typ.is_weapon(),
655                Warrior => !self.typ.is_weapon() && !self.typ.is_shield(),
656                _ => false,
657            },
658        }
659    }
660
661    /// Parses an item, that starts at the start of the given data
662    pub(crate) fn parse(
663        data: &[i64],
664        server_time: ServerTime,
665    ) -> Result<Option<Self>, SFError> {
666        let Some(typ) = ItemType::parse(data, server_time)? else {
667            return Ok(None);
668        };
669
670        let enchantment = data.cfpget(2, "item enchantment", |a| a)?;
671        let gem_slot_val = data.cimget(1, "gem slot val", |a| a)?;
672        let gem_pwr = data.cimget(16, "gem pwr", |a| a)?;
673
674        let gem_slot = GemSlot::parse(gem_slot_val, gem_pwr);
675
676        let class = if typ.is_class_item() {
677            data.cfpget(3, "item class", |x| (x & 0xFFFF) / 1000)?
678        } else {
679            None
680        };
681        let mut rune = None;
682        let mut attributes: EnumMap<AttributeType, u32> = EnumMap::default();
683        let price = data.csiget(13, "item price", u32::MAX)?;
684
685        if typ.equipment_slot().is_some() {
686            for i in 0..3 {
687                let atr_typ = data.cget(i + 7, "item atr typ")?;
688                let Ok(atr_typ) = atr_typ.try_into() else {
689                    warn!("Invalid attribute typ: {atr_typ}, {typ:?}");
690                    continue;
691                };
692                let atr_val = data.cget(i + 10, "item atr val")?;
693                let Ok(atr_val): Result<u32, _> = atr_val.try_into() else {
694                    warn!("Invalid attribute value: {atr_val}, {typ:?}");
695                    continue;
696                };
697                match atr_typ {
698                    0 => {}
699                    1..=5 => {
700                        let Some(atr_typ) = FromPrimitive::from_usize(atr_typ)
701                        else {
702                            continue;
703                        };
704                        *attributes.get_mut(atr_typ) += atr_val;
705                    }
706                    6 => {
707                        for atr in attributes.values_mut() {
708                            *atr += atr_val;
709                        }
710                    }
711                    21 => {
712                        for atr in [
713                            AttributeType::Strength,
714                            AttributeType::Constitution,
715                            AttributeType::Luck,
716                        ] {
717                            *attributes.get_mut(atr) += atr_val;
718                        }
719                    }
720                    22 => {
721                        for atr in [
722                            AttributeType::Dexterity,
723                            AttributeType::Constitution,
724                            AttributeType::Luck,
725                        ] {
726                            *attributes.get_mut(atr) += atr_val;
727                        }
728                    }
729                    23 => {
730                        for atr in [
731                            AttributeType::Intelligence,
732                            AttributeType::Constitution,
733                            AttributeType::Luck,
734                        ] {
735                            *attributes.get_mut(atr) += atr_val;
736                        }
737                    }
738                    rune_typ => {
739                        let Some(typ) = FromPrimitive::from_usize(rune_typ)
740                        else {
741                            warn!(
742                                "Unhandled item val: {atr_typ} -> {atr_val} \
743                                 for {class:?} {typ:?}",
744                            );
745                            continue;
746                        };
747                        let Ok(value) = atr_val.try_into() else {
748                            warn!("Rune value too big for a u8: {atr_val}");
749                            continue;
750                        };
751                        rune = Some(Rune { typ, value });
752                    }
753                }
754            }
755        }
756        let model_id: u16 =
757            data.cimget(3, "item model id", |x| (x & 0xFFFF) % 1000)?;
758
759        let color = match model_id {
760            ..=49 if typ != ItemType::Talisman => data
761                .get(5..=12)
762                .map(|a| a.iter().sum::<i64>())
763                .map(|a| (a % 5) + 1)
764                .and_then(|a| a.try_into().ok())
765                .unwrap_or(1),
766            _ => 1,
767        };
768
769        let item = Item {
770            typ,
771            model_id,
772            rune,
773            type_specific_val: data.csiget(5, "effect value", 0)?,
774            gem_slot,
775            enchantment,
776            class,
777            attributes,
778            color,
779            price,
780            mushroom_price: data.csiget(14, "mushroom price", u32::MAX)?,
781            upgrade_count: data.csiget(15, "upgrade count", u8::MAX)?,
782            item_quality: data.csiget(17, "item quality", 0)?,
783            is_washed: data.csiget(18, "is washed", 0)? != 0,
784            full_model_id: data.csiget(3, "raw model id", 0)?,
785        };
786        Ok(Some(item))
787    }
788}
789
790/// A enchantment, that gives a bonus to an aspect, if the item
791#[derive(
792    Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, EnumIter, Hash, Enum,
793)]
794#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
795pub enum Enchantment {
796    /// Increased crit damage
797    SwordOfVengeance = 11,
798    /// Finds more mushrooms
799    MariosBeard = 31,
800    /// Shortens travel time
801    ManyFeetBoots = 41,
802    /// Increased reaction score in combat
803    ShadowOfTheCowboy = 51,
804    /// Extra XP on expeditions
805    AdventurersArchaeologicalAura = 61,
806    /// Allows an extra beer
807    ThirstyWanderer = 71,
808    /// Find items at paths edge (expeditions) more often
809    UnholyAcquisitiveness = 81,
810    /// Find extra gold on expeditions
811    TheGraveRobbersPrayer = 91,
812    /// Increase the chance of loot against other players
813    RobberBaronRitual = 101,
814}
815
816impl Enchantment {
817    #[must_use]
818    pub fn equipment_slot(&self) -> EquipmentSlot {
819        match self {
820            Enchantment::SwordOfVengeance => EquipmentSlot::Weapon,
821            Enchantment::MariosBeard => EquipmentSlot::BreastPlate,
822            Enchantment::ManyFeetBoots => EquipmentSlot::FootWear,
823            Enchantment::ShadowOfTheCowboy => EquipmentSlot::Gloves,
824            Enchantment::AdventurersArchaeologicalAura => EquipmentSlot::Hat,
825            Enchantment::ThirstyWanderer => EquipmentSlot::Belt,
826            Enchantment::UnholyAcquisitiveness => EquipmentSlot::Amulet,
827            Enchantment::TheGraveRobbersPrayer => EquipmentSlot::Ring,
828            Enchantment::RobberBaronRitual => EquipmentSlot::Talisman,
829        }
830    }
831}
832
833/// A rune, which has both a type and a strength
834#[derive(Debug, Clone, Copy, PartialEq, Eq)]
835#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
836pub struct Rune {
837    /// The type of tune this is
838    pub typ: RuneType,
839    /// The "strength" of this rune. So a value like 50 here and a typ of
840    /// `FireResistance` would mean 50% fire resistance
841    pub value: u8,
842}
843
844#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, EnumIter, Hash)]
845#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
846#[allow(missing_docs)]
847/// The effect of a rune
848pub enum RuneType {
849    QuestGold = 31,
850    EpicChance,
851    ItemQuality,
852    QuestXP,
853    ExtraHitPoints,
854    FireResistance,
855    ColdResistence,
856    LightningResistance,
857    TotalResistence,
858    FireDamage,
859    ColdDamage,
860    LightningDamage,
861}
862
863/// A gem slot for an item
864#[derive(Debug, Clone, PartialEq, Eq, Copy)]
865#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
866pub enum GemSlot {
867    /// This gemslot has been filled and can only be emptied by the blacksmith
868    Filled(Gem),
869    /// A gem can be inserted into this item
870    Empty,
871}
872
873impl GemSlot {
874    pub(crate) fn parse(slot_val: i64, gem_pwr: i64) -> Option<GemSlot> {
875        match slot_val {
876            0 => return None,
877            1 => return Some(GemSlot::Empty),
878            _ => {}
879        }
880
881        let Ok(value) = gem_pwr.try_into() else {
882            warn!("Invalid gem power {gem_pwr}");
883            return None;
884        };
885
886        match GemType::parse(slot_val, value) {
887            Some(typ) => Some(GemSlot::Filled(Gem { typ, value })),
888            None => Some(GemSlot::Empty),
889        }
890    }
891}
892
893/// A potion. This is not just itemtype to make active potions easier
894#[derive(Debug, Clone, PartialEq, Eq, Copy)]
895#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
896pub struct Potion {
897    /// The rtype of potion
898    pub typ: PotionType,
899    /// The size of potion
900    pub size: PotionSize,
901    /// The time at which this potion expires. If this is none, the time is not
902    /// known. This can happen for other players
903    pub expires: Option<DateTime<Local>>,
904}
905
906/// Identifies a specific item and contains all values related to the specific
907/// type. The only thing missing is armor, which can be found as a method on
908/// `Item`
909#[derive(Debug, Clone, PartialEq, Eq, Copy)]
910#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
911#[allow(missing_docs)]
912pub enum ItemType {
913    Hat,
914    BreastPlate,
915    Gloves,
916    FootWear,
917    Weapon {
918        min_dmg: u32,
919        max_dmg: u32,
920    },
921    Amulet,
922    Belt,
923    Ring,
924    Talisman,
925    Shield {
926        block_chance: u32,
927    },
928    Shard {
929        piece: u32,
930    },
931    Potion(Potion),
932    Scrapbook,
933    DungeonKey {
934        id: u32,
935        shadow_key: bool,
936    },
937    Gem(Gem),
938    PetItem {
939        typ: PetItem,
940    },
941    QuickSandGlass,
942    HeartOfDarkness,
943    WheelOfFortune,
944    Mannequin,
945    Resource {
946        amount: u32,
947        typ: ResourceType,
948    },
949    ToiletKey,
950    Gral,
951    EpicItemBag,
952    /// If there is a new item added to the game, this will be the placeholder
953    /// to make sure you never think a place is empty somewhere, if it is not
954    Unknown(u8),
955}
956
957impl ItemType {
958    /// Checks if this item type is a weapon.
959    #[must_use]
960    pub const fn is_weapon(self) -> bool {
961        matches!(self, ItemType::Weapon { .. })
962    }
963
964    /// Checks if this item type is a shield.
965    #[must_use]
966    pub const fn is_shield(self) -> bool {
967        matches!(self, ItemType::Shield { .. })
968    }
969
970    /// Checks if this type can only be worn by only a particular class
971    #[must_use]
972    pub fn is_class_item(&self) -> bool {
973        matches!(
974            self,
975            ItemType::Hat
976                | ItemType::Belt
977                | ItemType::Gloves
978                | ItemType::FootWear
979                | ItemType::Shield { .. }
980                | ItemType::Weapon { .. }
981                | ItemType::BreastPlate
982        )
983    }
984
985    /// Checks, if this item type is unique. Technically they are not always
986    /// unique, as the scrapbook/keys can be sold, but it should be clear
987    /// what this is
988    #[must_use]
989    pub fn is_unique(&self) -> bool {
990        matches!(
991            self,
992            ItemType::Scrapbook
993                | ItemType::HeartOfDarkness
994                | ItemType::WheelOfFortune
995                | ItemType::Mannequin
996                | ItemType::ToiletKey
997                | ItemType::Gral
998                | ItemType::EpicItemBag
999                | ItemType::DungeonKey { .. }
1000        )
1001    }
1002
1003    /// The equipment slot, that this item type can be equipped to
1004    #[must_use]
1005    pub fn equipment_slot(&self) -> Option<EquipmentSlot> {
1006        Some(match self {
1007            ItemType::Hat => EquipmentSlot::Hat,
1008            ItemType::BreastPlate => EquipmentSlot::BreastPlate,
1009            ItemType::Gloves => EquipmentSlot::Gloves,
1010            ItemType::FootWear => EquipmentSlot::FootWear,
1011            ItemType::Weapon { .. } => EquipmentSlot::Weapon,
1012            ItemType::Amulet => EquipmentSlot::Amulet,
1013            ItemType::Belt => EquipmentSlot::Belt,
1014            ItemType::Ring => EquipmentSlot::Ring,
1015            ItemType::Talisman => EquipmentSlot::Talisman,
1016            ItemType::Shield { .. } => EquipmentSlot::Shield,
1017            _ => return None,
1018        })
1019    }
1020
1021    /// Checks, if this item type can be enchanted
1022    #[must_use]
1023    pub fn is_enchantable(&self) -> bool {
1024        self.equipment_slot()
1025            .is_some_and(|e| e.enchantment().is_some())
1026    }
1027
1028    pub(crate) fn parse(
1029        data: &[i64],
1030        _server_time: ServerTime,
1031    ) -> Result<Option<Self>, SFError> {
1032        let raw_typ: u8 = data.csimget(0, "item type", 255, |a| a & 0xFF)?;
1033        let unknown_item = |name: &'static str| {
1034            warn!("Could no parse item of type: {raw_typ}. {name} is faulty");
1035            Ok(Some(ItemType::Unknown(raw_typ)))
1036        };
1037
1038        let sub_ident = data.cget(3, "item sub type")?;
1039
1040        Ok(Some(match raw_typ {
1041            0 => return Ok(None),
1042            1 => ItemType::Weapon {
1043                min_dmg: data.csiget(5, "weapon min dmg", 0)?,
1044                max_dmg: data.csiget(6, "weapon min dmg", 0)?,
1045            },
1046            2 => ItemType::Shield {
1047                block_chance: data.csiget(5, "shield block chance", 0)?,
1048            },
1049            3 => ItemType::BreastPlate,
1050            4 => ItemType::FootWear,
1051            5 => ItemType::Gloves,
1052            6 => ItemType::Hat,
1053            7 => ItemType::Belt,
1054            8 => ItemType::Amulet,
1055            9 => ItemType::Ring,
1056            10 => ItemType::Talisman,
1057            11 => {
1058                let id = sub_ident & 0xFFFF;
1059                let Ok(id) = id.try_into() else {
1060                    return unknown_item("unique sub ident");
1061                };
1062                match id {
1063                    1..=11 | 17 | 19 | 22 | 69 | 70 => ItemType::DungeonKey {
1064                        id,
1065                        shadow_key: false,
1066                    },
1067                    20 => ItemType::ToiletKey,
1068                    51..=64 | 67..=68 => ItemType::DungeonKey {
1069                        id,
1070                        shadow_key: true,
1071                    },
1072                    10000 => ItemType::EpicItemBag,
1073                    piece => ItemType::Shard { piece },
1074                }
1075            }
1076            12 => {
1077                let id = sub_ident & 0xFF;
1078                if id > 16 {
1079                    let Some(typ) = FromPrimitive::from_i64(id) else {
1080                        return unknown_item("resource type");
1081                    };
1082                    ItemType::Resource {
1083                        // TODO:
1084                        // data.csiget(7, "resource amount", 0)?,
1085                        amount: 0,
1086                        typ,
1087                    }
1088                } else {
1089                    let Some(typ) = PotionType::parse(id) else {
1090                        return unknown_item("potion type");
1091                    };
1092                    let Some(size) = PotionSize::parse(id) else {
1093                        return unknown_item("potion size");
1094                    };
1095                    ItemType::Potion(Potion {
1096                        typ,
1097                        size,
1098                        // TODO:
1099                        expires: None,
1100                        // expires: data.cstget(
1101                        //     4,
1102                        //     "potion expires",
1103                        //     server_time,
1104                        // )?,
1105                    })
1106                }
1107            }
1108            13 => ItemType::Scrapbook,
1109            15 => {
1110                let gem_value = data.csiget(16, "gem pwr", 0)?;
1111                let Some(typ) = GemType::parse(sub_ident, gem_value) else {
1112                    return unknown_item("gem type");
1113                };
1114                let gem = Gem {
1115                    typ,
1116                    value: gem_value,
1117                };
1118                ItemType::Gem(gem)
1119            }
1120            16 => {
1121                let Some(typ) = PetItem::parse(sub_ident & 0xFFFF) else {
1122                    return unknown_item("pet item");
1123                };
1124                ItemType::PetItem { typ }
1125            }
1126            17 if (sub_ident & 0xFFFF) == 4 => ItemType::Gral,
1127            17 => ItemType::QuickSandGlass,
1128            18 => ItemType::HeartOfDarkness,
1129            19 => ItemType::WheelOfFortune,
1130            20 => ItemType::Mannequin,
1131            _ => {
1132                return unknown_item("main ident");
1133            }
1134        }))
1135    }
1136
1137    /// The id, that the server has associated with this item. I honestly forgot
1138    /// why I have this function public
1139    #[must_use]
1140    pub fn raw_id(&self) -> u8 {
1141        match self {
1142            ItemType::Weapon { .. } => 1,
1143            ItemType::Shield { .. } => 2,
1144            ItemType::BreastPlate => 3,
1145            ItemType::FootWear => 4,
1146            ItemType::Gloves => 5,
1147            ItemType::Hat => 6,
1148            ItemType::Belt => 7,
1149            ItemType::Amulet => 8,
1150            ItemType::Ring => 9,
1151            ItemType::Talisman => 10,
1152            ItemType::Shard { .. }
1153            | ItemType::DungeonKey { .. }
1154            | ItemType::ToiletKey
1155            | ItemType::EpicItemBag => 11,
1156            ItemType::Potion { .. } | ItemType::Resource { .. } => 12,
1157            ItemType::Scrapbook => 13,
1158            ItemType::Gem(_) => 15,
1159            ItemType::PetItem { .. } => 16,
1160            ItemType::QuickSandGlass | ItemType::Gral => 17,
1161            ItemType::HeartOfDarkness => 18,
1162            ItemType::WheelOfFortune => 19,
1163            ItemType::Mannequin => 20,
1164            ItemType::Unknown(u) => *u,
1165        }
1166    }
1167}
1168
1169/// The effect, that the potion is going to have
1170#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
1171#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1172#[allow(missing_docs)]
1173pub enum PotionType {
1174    Strength,
1175    Dexterity,
1176    Intelligence,
1177    Constitution,
1178    Luck,
1179    EternalLife,
1180}
1181
1182impl From<AttributeType> for PotionType {
1183    fn from(value: AttributeType) -> Self {
1184        match value {
1185            AttributeType::Strength => PotionType::Strength,
1186            AttributeType::Dexterity => PotionType::Dexterity,
1187            AttributeType::Intelligence => PotionType::Intelligence,
1188            AttributeType::Constitution => PotionType::Constitution,
1189            AttributeType::Luck => PotionType::Luck,
1190        }
1191    }
1192}
1193
1194impl PotionType {
1195    pub(crate) fn parse(id: i64) -> Option<PotionType> {
1196        if id == 0 {
1197            return None;
1198        }
1199        if id == 16 {
1200            return Some(PotionType::EternalLife);
1201        }
1202        Some(match id % 5 {
1203            0 => PotionType::Luck,
1204            1 => PotionType::Strength,
1205            2 => PotionType::Dexterity,
1206            3 => PotionType::Intelligence,
1207            _ => PotionType::Constitution,
1208        })
1209    }
1210}
1211
1212/// The size and with that, the strength, that this potion has
1213#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
1214#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1215#[allow(missing_docs)]
1216pub enum PotionSize {
1217    Small,
1218    Medium,
1219    Large,
1220}
1221
1222impl PartialOrd for PotionSize {
1223    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1224        self.effect().partial_cmp(&other.effect())
1225    }
1226}
1227
1228impl PotionSize {
1229    #[must_use]
1230    pub fn effect(&self) -> f64 {
1231        match self {
1232            PotionSize::Small => 0.1,
1233            PotionSize::Medium => 0.15,
1234            PotionSize::Large => 0.25,
1235        }
1236    }
1237
1238    pub(crate) fn parse(id: i64) -> Option<Self> {
1239        Some(match id {
1240            1..=5 => PotionSize::Small,
1241            6..=10 => PotionSize::Medium,
1242            11..=16 => PotionSize::Large,
1243            _ => return None,
1244        })
1245    }
1246}
1247
1248/// Differentiates resource items
1249#[derive(Debug, Clone, PartialEq, Eq, Copy, FromPrimitive)]
1250#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1251#[allow(missing_docs)]
1252pub enum ResourceType {
1253    Wood = 17,
1254    Stone,
1255    Souls,
1256    Arcane,
1257    Metal,
1258}
1259
1260/// A gem, that is either socketed in an item, or in the inventory
1261#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1262#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1263pub struct Gem {
1264    /// The type of gem
1265    pub typ: GemType,
1266    /// The strength of this gem
1267    pub value: u32,
1268}
1269
1270/// The type the gam has
1271#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1272#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1273#[allow(missing_docs)]
1274pub enum GemType {
1275    Strength,
1276    Dexterity,
1277    Intelligence,
1278    Constitution,
1279    Luck,
1280    All,
1281    Legendary,
1282}
1283
1284impl GemType {
1285    pub(crate) fn parse(id: i64, debug_value: u32) -> Option<GemType> {
1286        Some(match id {
1287            0 | 1 => return None,
1288            10..=40 => match id % 10 {
1289                0 => GemType::Strength,
1290                1 => GemType::Dexterity,
1291                2 => GemType::Intelligence,
1292                3 => GemType::Constitution,
1293                4 => GemType::Luck,
1294                5 => GemType::All,
1295                // Just put this here because it makes sense. I only ever
1296                // see 4 for these
1297                6 => GemType::Legendary,
1298                _ => {
1299                    return None;
1300                }
1301            },
1302            _ => {
1303                warn!("Unknown gem: {id} - {debug_value}");
1304                return None;
1305            }
1306        })
1307    }
1308}
1309
1310/// Denotes the place, where an item is equipped
1311#[derive(
1312    Debug, Copy, Clone, PartialEq, Eq, Hash, Enum, EnumIter, EnumCount,
1313)]
1314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1315#[allow(missing_docs)]
1316pub enum EquipmentSlot {
1317    Hat = 1,
1318    BreastPlate,
1319    Gloves,
1320    FootWear,
1321    Amulet,
1322    Belt,
1323    Ring,
1324    Talisman,
1325    Weapon,
1326    Shield,
1327}
1328
1329impl EquipmentSlot {
1330    /// The value the game internally uses for these slots. No idea, why this is
1331    /// pub
1332    #[must_use]
1333    pub fn raw_id(&self) -> u8 {
1334        match self {
1335            EquipmentSlot::Weapon => 1,
1336            EquipmentSlot::Shield => 2,
1337            EquipmentSlot::BreastPlate => 3,
1338            EquipmentSlot::FootWear => 4,
1339            EquipmentSlot::Gloves => 5,
1340            EquipmentSlot::Hat => 6,
1341            EquipmentSlot::Belt => 7,
1342            EquipmentSlot::Amulet => 8,
1343            EquipmentSlot::Ring => 9,
1344            EquipmentSlot::Talisman => 10,
1345        }
1346    }
1347
1348    /// Returns the corresponding enchantment for this equipment slot, if it
1349    /// can be enchanted
1350    #[must_use]
1351    pub const fn enchantment(&self) -> Option<Enchantment> {
1352        match self {
1353            EquipmentSlot::Hat => {
1354                Some(Enchantment::AdventurersArchaeologicalAura)
1355            }
1356            EquipmentSlot::BreastPlate => Some(Enchantment::MariosBeard),
1357            EquipmentSlot::Gloves => Some(Enchantment::ShadowOfTheCowboy),
1358            EquipmentSlot::FootWear => Some(Enchantment::ManyFeetBoots),
1359            EquipmentSlot::Amulet => Some(Enchantment::UnholyAcquisitiveness),
1360            EquipmentSlot::Belt => Some(Enchantment::ThirstyWanderer),
1361            EquipmentSlot::Ring => Some(Enchantment::TheGraveRobbersPrayer),
1362            EquipmentSlot::Talisman => Some(Enchantment::RobberBaronRitual),
1363            EquipmentSlot::Weapon => Some(Enchantment::SwordOfVengeance),
1364            EquipmentSlot::Shield => None,
1365        }
1366    }
1367}
1368
1369/// An item usable for pets
1370#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1371#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1372#[allow(missing_docs)]
1373pub enum PetItem {
1374    Egg(HabitatType),
1375    SpecialEgg(HabitatType),
1376    GoldenEgg,
1377    Nest,
1378    Fruit(HabitatType),
1379}
1380
1381impl PetItem {
1382    pub(crate) fn parse(val: i64) -> Option<Self> {
1383        Some(match val {
1384            1..=5 => PetItem::Egg(HabitatType::from_typ_id(val)?),
1385            11..=15 => PetItem::SpecialEgg(HabitatType::from_typ_id(val - 10)?),
1386            21 => PetItem::GoldenEgg,
1387            22 => PetItem::Nest,
1388            31..=35 => PetItem::Fruit(HabitatType::from_typ_id(val - 30)?),
1389            _ => return None,
1390        })
1391    }
1392}
1393
1394pub(crate) fn parse_active_potions(
1395    data: &[i64],
1396    server_time: ServerTime,
1397) -> [Option<Potion>; 3] {
1398    if data.len() < 10 {
1399        return Default::default();
1400    }
1401    #[allow(clippy::indexing_slicing)]
1402    core::array::from_fn(move |i| {
1403        Some(Potion {
1404            typ: PotionType::parse(data[i + 1])?,
1405            size: PotionSize::parse(data[i + 1])?,
1406            expires: server_time.convert_to_local(data[4 + i], "potion exp"),
1407            // 6 => effect, but no idea why we would want to use that
1408        })
1409    })
1410}