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