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#[derive(Debug, Default, Clone, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21/// The basic inventory, that every player has
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    #[must_use]
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    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#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106#[allow(missing_docs)]
107/// All the parts of `ItemPlace`, that are owned by the player
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#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
200#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
201#[allow(missing_docs)]
202/// All the parts of `ItemPlace`, that are owned by the player
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#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)]
232#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
233/// All places, that items can be dragged to excluding companions
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#[derive(Debug, Default, Clone)]
248#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
249/// All the equipment a player is wearing
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    #[must_use]
276    /// Checks if the character has an item with the enchantment equipped
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#[derive(Debug, Clone, PartialEq, Eq)]
310#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
311/// Information about a single item. This can be anything, that is either in a
312/// inventory, in a reward slot, or similar
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#[derive(
593    Debug, Clone, Copy, FromPrimitive, PartialEq, Eq, EnumIter, Hash, Enum,
594)]
595#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
596/// A enchantment, that gives a bonus to an aspect, if the item
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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
636#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
637/// A rune, which has both a type and a strength
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#[derive(Debug, Clone, PartialEq, Eq, Copy)]
666#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
667/// A gem slot for an item
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#[derive(Debug, Clone, PartialEq, Eq, Copy)]
695#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
696/// A potion. This is not just itemtype to make active potions easier
697pub struct Potion {
698    /// The rtype of potion
699    pub typ: PotionType,
700    /// The size of potion
701    pub size: PotionSize,
702    /// The time at which this potion expires. If this is none, the time is not
703    /// known. This can happen for other players
704    pub expires: Option<DateTime<Local>>,
705}
706
707#[derive(Debug, Clone, PartialEq, Eq, Copy)]
708#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
709#[allow(missing_docs)]
710/// Identifies a specific item and contains all values related to the specific
711/// type. The only thing missing is armor, which can be found as a method on
712/// `Item`
713pub enum ItemType {
714    Hat,
715    BreastPlate,
716    Gloves,
717    FootWear,
718    Weapon {
719        min_dmg: u32,
720        max_dmg: u32,
721    },
722    Amulet,
723    Belt,
724    Ring,
725    Talisman,
726    Shield {
727        block_chance: u32,
728    },
729    Shard {
730        piece: u32,
731    },
732    Potion(Potion),
733    Scrapbook,
734    DungeonKey {
735        id: u32,
736        shadow_key: bool,
737    },
738    Gem(Gem),
739    PetItem {
740        typ: PetItem,
741    },
742    QuickSandGlass,
743    HeartOfDarkness,
744    WheelOfFortune,
745    Mannequin,
746    Resource {
747        amount: u32,
748        typ: ResourceType,
749    },
750    ToiletKey,
751    Gral,
752    EpicItemBag,
753    /// If there is a new item added to the game, this will be the placeholder
754    /// to make sure you never think a place is empty somewhere, if it is not
755    Unknown(u8),
756}
757
758impl ItemType {
759    /// Checks if this item type is a weapon.
760    #[must_use]
761    pub const fn is_weapon(self) -> bool {
762        matches!(self, ItemType::Weapon { .. })
763    }
764
765    /// Checks if this item type is a shield.
766    #[must_use]
767    pub const fn is_shield(self) -> bool {
768        matches!(self, ItemType::Shield { .. })
769    }
770
771    /// Checks if this type can only be worn by only a particular class
772    #[must_use]
773    pub fn is_class_item(&self) -> bool {
774        matches!(
775            self,
776            ItemType::Hat
777                | ItemType::Belt
778                | ItemType::Gloves
779                | ItemType::FootWear
780                | ItemType::Shield { .. }
781                | ItemType::Weapon { .. }
782                | ItemType::BreastPlate
783        )
784    }
785
786    /// Checks, if this item type is unique. Technically they are not always
787    /// unique, as the scrapbook/keys can be sold, but it should be clear
788    /// what this is
789    #[must_use]
790    pub fn is_unique(&self) -> bool {
791        matches!(
792            self,
793            ItemType::Scrapbook
794                | ItemType::HeartOfDarkness
795                | ItemType::WheelOfFortune
796                | ItemType::Mannequin
797                | ItemType::ToiletKey
798                | ItemType::Gral
799                | ItemType::EpicItemBag
800                | ItemType::DungeonKey { .. }
801        )
802    }
803
804    /// The equipment slot, that this item type can be equipped to
805    #[must_use]
806    pub fn equipment_slot(&self) -> Option<EquipmentSlot> {
807        Some(match self {
808            ItemType::Hat => EquipmentSlot::Hat,
809            ItemType::BreastPlate => EquipmentSlot::BreastPlate,
810            ItemType::Gloves => EquipmentSlot::Gloves,
811            ItemType::FootWear => EquipmentSlot::FootWear,
812            ItemType::Weapon { .. } => EquipmentSlot::Weapon,
813            ItemType::Amulet => EquipmentSlot::Amulet,
814            ItemType::Belt => EquipmentSlot::Belt,
815            ItemType::Ring => EquipmentSlot::Ring,
816            ItemType::Talisman => EquipmentSlot::Talisman,
817            ItemType::Shield { .. } => EquipmentSlot::Shield,
818            _ => return None,
819        })
820    }
821
822    /// Checks, if this item type can be enchanted
823    #[must_use]
824    pub fn is_enchantable(&self) -> bool {
825        self.equipment_slot()
826            .is_some_and(|e| e.enchantment().is_some())
827    }
828
829    pub(crate) fn parse_active_potions(
830        data: &[i64],
831        server_time: ServerTime,
832    ) -> [Option<Potion>; 3] {
833        if data.len() < 6 {
834            return Default::default();
835        }
836        #[allow(clippy::indexing_slicing)]
837        core::array::from_fn(move |i| {
838            Some(Potion {
839                typ: PotionType::parse(data[i])?,
840                size: PotionSize::parse(data[i])?,
841                expires: server_time
842                    .convert_to_local(data[3 + i], "potion exp"),
843            })
844        })
845    }
846
847    pub(crate) fn parse(
848        data: &[i64],
849        _server_time: ServerTime,
850    ) -> Result<Option<Self>, SFError> {
851        let raw_typ: u8 = data.csimget(0, "item type", 255, |a| a & 0xFF)?;
852        let unknown_item = |name: &'static str| {
853            warn!("Could no parse item of type: {raw_typ}. {name} is faulty");
854            Ok(Some(ItemType::Unknown(raw_typ)))
855        };
856
857        let sub_ident = data.cget(3, "item sub type")?;
858
859        Ok(Some(match raw_typ {
860            0 => return Ok(None),
861            1 => ItemType::Weapon {
862                min_dmg: data.csiget(5, "weapon min dmg", 0)?,
863                max_dmg: data.csiget(6, "weapon min dmg", 0)?,
864            },
865            2 => ItemType::Shield {
866                block_chance: data.csiget(5, "shield block chance", 0)?,
867            },
868            3 => ItemType::BreastPlate,
869            4 => ItemType::FootWear,
870            5 => ItemType::Gloves,
871            6 => ItemType::Hat,
872            7 => ItemType::Belt,
873            8 => ItemType::Amulet,
874            9 => ItemType::Ring,
875            10 => ItemType::Talisman,
876            11 => {
877                let id = sub_ident & 0xFFFF;
878                let Ok(id) = id.try_into() else {
879                    return unknown_item("unique sub ident");
880                };
881                match id {
882                    1..=11 | 17 | 19 | 22 | 69 | 70 => ItemType::DungeonKey {
883                        id,
884                        shadow_key: false,
885                    },
886                    20 => ItemType::ToiletKey,
887                    51..=64 | 67..=68 => ItemType::DungeonKey {
888                        id,
889                        shadow_key: true,
890                    },
891                    10000 => ItemType::EpicItemBag,
892                    piece => ItemType::Shard { piece },
893                }
894            }
895            12 => {
896                let id = sub_ident & 0xFF;
897                if id > 16 {
898                    let Some(typ) = FromPrimitive::from_i64(id) else {
899                        return unknown_item("resource type");
900                    };
901                    ItemType::Resource {
902                        // TODO:
903                        // data.csiget(7, "resource amount", 0)?,
904                        amount: 0,
905                        typ,
906                    }
907                } else {
908                    let Some(typ) = PotionType::parse(id) else {
909                        return unknown_item("potion type");
910                    };
911                    let Some(size) = PotionSize::parse(id) else {
912                        return unknown_item("potion size");
913                    };
914                    ItemType::Potion(Potion {
915                        typ,
916                        size,
917                        // TODO:
918                        expires: None,
919                        // expires: data.cstget(
920                        //     4,
921                        //     "potion expires",
922                        //     server_time,
923                        // )?,
924                    })
925                }
926            }
927            13 => ItemType::Scrapbook,
928            15 => {
929                let gem_value = data.csiget(16, "gem pwr", 0)?;
930                let Some(typ) = GemType::parse(sub_ident, gem_value) else {
931                    return unknown_item("gem type");
932                };
933                let gem = Gem {
934                    typ,
935                    value: gem_value,
936                };
937                ItemType::Gem(gem)
938            }
939            16 => {
940                let Some(typ) = PetItem::parse(sub_ident & 0xFFFF) else {
941                    return unknown_item("pet item");
942                };
943                ItemType::PetItem { typ }
944            }
945            17 if (sub_ident & 0xFFFF) == 4 => ItemType::Gral,
946            17 => ItemType::QuickSandGlass,
947            18 => ItemType::HeartOfDarkness,
948            19 => ItemType::WheelOfFortune,
949            20 => ItemType::Mannequin,
950            _ => {
951                return unknown_item("main ident");
952            }
953        }))
954    }
955
956    /// The id, that the server has associated with this item. I honestly forgot
957    /// why I have this function public
958    #[must_use]
959    pub fn raw_id(&self) -> u8 {
960        match self {
961            ItemType::Weapon { .. } => 1,
962            ItemType::Shield { .. } => 2,
963            ItemType::BreastPlate => 3,
964            ItemType::FootWear => 4,
965            ItemType::Gloves => 5,
966            ItemType::Hat => 6,
967            ItemType::Belt => 7,
968            ItemType::Amulet => 8,
969            ItemType::Ring => 9,
970            ItemType::Talisman => 10,
971            ItemType::Shard { .. }
972            | ItemType::DungeonKey { .. }
973            | ItemType::ToiletKey
974            | ItemType::EpicItemBag => 11,
975            ItemType::Potion { .. } | ItemType::Resource { .. } => 12,
976            ItemType::Scrapbook => 13,
977            ItemType::Gem(_) => 15,
978            ItemType::PetItem { .. } => 16,
979            ItemType::QuickSandGlass | ItemType::Gral => 17,
980            ItemType::HeartOfDarkness => 18,
981            ItemType::WheelOfFortune => 19,
982            ItemType::Mannequin => 20,
983            ItemType::Unknown(u) => *u,
984        }
985    }
986}
987
988#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
989#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
990#[allow(missing_docs)]
991/// The effect, that the potion is going to have
992pub enum PotionType {
993    Strength,
994    Dexterity,
995    Intelligence,
996    Constitution,
997    Luck,
998    EternalLife,
999}
1000
1001impl From<AttributeType> for PotionType {
1002    fn from(value: AttributeType) -> Self {
1003        match value {
1004            AttributeType::Strength => PotionType::Strength,
1005            AttributeType::Dexterity => PotionType::Dexterity,
1006            AttributeType::Intelligence => PotionType::Intelligence,
1007            AttributeType::Constitution => PotionType::Constitution,
1008            AttributeType::Luck => PotionType::Luck,
1009        }
1010    }
1011}
1012
1013impl PotionType {
1014    pub(crate) fn parse(id: i64) -> Option<PotionType> {
1015        if id == 0 {
1016            return None;
1017        }
1018        if id == 16 {
1019            return Some(PotionType::EternalLife);
1020        }
1021        Some(match id % 5 {
1022            0 => PotionType::Luck,
1023            1 => PotionType::Strength,
1024            2 => PotionType::Dexterity,
1025            3 => PotionType::Intelligence,
1026            _ => PotionType::Constitution,
1027        })
1028    }
1029}
1030
1031#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
1032#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1033#[allow(missing_docs)]
1034/// The size and with that, the strength, that this potion has
1035pub enum PotionSize {
1036    Small,
1037    Medium,
1038    Large,
1039}
1040
1041impl PartialOrd for PotionSize {
1042    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1043        self.effect().partial_cmp(&other.effect())
1044    }
1045}
1046
1047impl PotionSize {
1048    #[must_use]
1049    pub fn effect(&self) -> f64 {
1050        match self {
1051            PotionSize::Small => 0.1,
1052            PotionSize::Medium => 0.15,
1053            PotionSize::Large => 0.25,
1054        }
1055    }
1056
1057    pub(crate) fn parse(id: i64) -> Option<Self> {
1058        Some(match id {
1059            1..=5 => PotionSize::Small,
1060            6..=10 => PotionSize::Medium,
1061            11..=16 => PotionSize::Large,
1062            _ => return None,
1063        })
1064    }
1065}
1066
1067#[derive(Debug, Clone, PartialEq, Eq, Copy, FromPrimitive)]
1068#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1069#[allow(missing_docs)]
1070/// Differentiates resource items
1071pub enum ResourceType {
1072    Wood = 17,
1073    Stone,
1074    Souls,
1075    Arcane,
1076    Metal,
1077}
1078
1079#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1080#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1081/// A gem, that is either socketed in an item, or in the inventory
1082pub struct Gem {
1083    /// The type of gem
1084    pub typ: GemType,
1085    /// The strength of this gem
1086    pub value: u32,
1087}
1088
1089#[derive(Debug, Clone, PartialEq, Eq, Copy)]
1090#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1091#[allow(missing_docs)]
1092/// The type the gam has
1093pub enum GemType {
1094    Strength,
1095    Dexterity,
1096    Intelligence,
1097    Constitution,
1098    Luck,
1099    All,
1100    Legendary,
1101}
1102
1103impl GemType {
1104    pub(crate) fn parse(id: i64, debug_value: u32) -> Option<GemType> {
1105        Some(match id {
1106            0 | 1 => return None,
1107            10..=40 => match id % 10 {
1108                0 => GemType::Strength,
1109                1 => GemType::Dexterity,
1110                2 => GemType::Intelligence,
1111                3 => GemType::Constitution,
1112                4 => GemType::Luck,
1113                5 => GemType::All,
1114                // Just put this here because it makes sense. I only ever
1115                // see 4 for these
1116                6 => GemType::Legendary,
1117                _ => {
1118                    return None;
1119                }
1120            },
1121            _ => {
1122                warn!("Unknown gem: {id} - {debug_value}");
1123                return None;
1124            }
1125        })
1126    }
1127}
1128
1129#[derive(
1130    Debug, Copy, Clone, PartialEq, Eq, Hash, Enum, EnumIter, EnumCount,
1131)]
1132#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1133#[allow(missing_docs)]
1134/// Denotes the place, where an item is equipped
1135pub enum EquipmentSlot {
1136    Hat = 1,
1137    BreastPlate,
1138    Gloves,
1139    FootWear,
1140    Amulet,
1141    Belt,
1142    Ring,
1143    Talisman,
1144    Weapon,
1145    Shield,
1146}
1147
1148impl EquipmentSlot {
1149    /// The value the game internally uses for these slots. No idea, why this is
1150    /// pub
1151    #[must_use]
1152    pub fn raw_id(&self) -> u8 {
1153        match self {
1154            EquipmentSlot::Weapon => 1,
1155            EquipmentSlot::Shield => 2,
1156            EquipmentSlot::BreastPlate => 3,
1157            EquipmentSlot::FootWear => 4,
1158            EquipmentSlot::Gloves => 5,
1159            EquipmentSlot::Hat => 6,
1160            EquipmentSlot::Belt => 7,
1161            EquipmentSlot::Amulet => 8,
1162            EquipmentSlot::Ring => 9,
1163            EquipmentSlot::Talisman => 10,
1164        }
1165    }
1166
1167    /// Returns the corresponding enchantment for this equipment slot, if it
1168    /// can be enchanted
1169    #[must_use]
1170    pub const fn enchantment(&self) -> Option<Enchantment> {
1171        match self {
1172            EquipmentSlot::Hat => {
1173                Some(Enchantment::AdventurersArchaeologicalAura)
1174            }
1175            EquipmentSlot::BreastPlate => Some(Enchantment::MariosBeard),
1176            EquipmentSlot::Gloves => Some(Enchantment::ShadowOfTheCowboy),
1177            EquipmentSlot::FootWear => Some(Enchantment::ManyFeetBoots),
1178            EquipmentSlot::Amulet => Some(Enchantment::UnholyAcquisitiveness),
1179            EquipmentSlot::Belt => Some(Enchantment::ThirstyWanderer),
1180            EquipmentSlot::Ring => Some(Enchantment::TheGraveRobbersPrayer),
1181            EquipmentSlot::Talisman => Some(Enchantment::RobberBaronRitual),
1182            EquipmentSlot::Weapon => Some(Enchantment::SwordOfVengeance),
1183            EquipmentSlot::Shield => None,
1184        }
1185    }
1186}
1187
1188#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1189#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1190#[allow(missing_docs)]
1191/// An item usable for pets
1192pub enum PetItem {
1193    Egg(HabitatType),
1194    SpecialEgg(HabitatType),
1195    GoldenEgg,
1196    Nest,
1197    Fruit(HabitatType),
1198}
1199
1200impl PetItem {
1201    pub(crate) fn parse(val: i64) -> Option<Self> {
1202        Some(match val {
1203            1..=5 => PetItem::Egg(HabitatType::from_typ_id(val)?),
1204            11..=15 => PetItem::SpecialEgg(HabitatType::from_typ_id(val - 10)?),
1205            21 => PetItem::GoldenEgg,
1206            22 => PetItem::Nest,
1207            31..=35 => PetItem::Fruit(HabitatType::from_typ_id(val - 30)?),
1208            _ => return None,
1209        })
1210    }
1211}