veloren_common/comp/inventory/
mod.rs

1use core::ops::Not;
2use hashbrown::HashMap;
3use serde::{Deserialize, Serialize};
4use specs::{Component, DerefFlaggedStorage};
5use std::{cmp::Ordering, convert::TryFrom, mem, num::NonZeroU32, ops::Range};
6use tracing::{debug, trace, warn};
7use vek::Vec3;
8
9use crate::{
10    LoadoutBuilder,
11    comp::{
12        Item,
13        body::Body,
14        inventory::{
15            item::{
16                ItemDef, ItemDefinitionIdOwned, ItemKind, MaterialStatManifest, TagExampleInfo,
17                item_key::ItemKey, tool::AbilityMap,
18            },
19            loadout::Loadout,
20            recipe_book::RecipeBook,
21            slot::{EquipSlot, Slot, SlotError},
22        },
23        loot_owner::LootOwnerKind,
24        slot::{InvSlotId, SlotId},
25    },
26    recipe::{Recipe, RecipeBookManifest},
27    resources::Time,
28    terrain::SpriteKind,
29    uid::Uid,
30};
31
32use super::FrontendItem;
33
34pub mod item;
35pub mod loadout;
36pub mod loadout_builder;
37pub mod recipe_book;
38pub mod slot;
39#[cfg(test)] mod test;
40#[cfg(test)] mod test_helpers;
41pub mod trade_pricing;
42
43pub type InvSlot = Option<Item>;
44const DEFAULT_INVENTORY_SLOTS: usize = 18;
45
46/// NOTE: Do not add a PartialEq instance for Inventory; that's broken!
47#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct Inventory {
49    next_sort_order: InventorySortOrder,
50    loadout: Loadout,
51    /// The "built-in" slots belonging to the inventory itself, all other slots
52    /// are provided by equipped items
53    slots: Vec<InvSlot>,
54    /// For when slot amounts are rebalanced or the inventory otherwise does not
55    /// have enough space to hold all the items after loading from database.
56    /// These slots are "remove-only" meaning that during normal gameplay items
57    /// can only be removed from these slots and never entered.
58    overflow_items: Vec<Item>,
59    /// Recipes that are available for use
60    recipe_book: RecipeBook,
61}
62
63/// Errors which the methods on `Inventory` produce
64#[derive(Debug)]
65pub enum Error {
66    /// The inventory is full and items could not be added. The extra items have
67    /// been returned.
68    Full(Vec<Item>),
69}
70
71impl Error {
72    pub fn returned_items(self) -> impl Iterator<Item = Item> {
73        match self {
74            Error::Full(items) => items.into_iter(),
75        }
76    }
77}
78
79#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
80pub enum InventorySortOrder {
81    Name,
82    Quality,
83    Category,
84    Tag,
85    Amount,
86}
87
88impl InventorySortOrder {
89    fn next(&self) -> InventorySortOrder {
90        match self {
91            InventorySortOrder::Name => InventorySortOrder::Quality,
92            InventorySortOrder::Quality => InventorySortOrder::Tag,
93            InventorySortOrder::Tag => InventorySortOrder::Category,
94            InventorySortOrder::Category => InventorySortOrder::Amount,
95            InventorySortOrder::Amount => InventorySortOrder::Name,
96        }
97    }
98}
99
100pub enum CustomOrder {
101    Name,
102    Quality,
103    KindPartial,
104    KindFull,
105    Tag,
106}
107
108/// Represents the Inventory of an entity. The inventory has 18 "built-in"
109/// slots, with further slots being provided by items equipped in the Loadout
110/// sub-struct. Inventory slots are indexed by `InvSlotId` which is
111/// comprised of `loadout_idx` - the index of the loadout item that provides the
112/// slot, 0 being the built-in inventory slots, and `slot_idx` - the index of
113/// the slot within that loadout item.
114///
115/// Currently, it is not supported for inventories to contain items that have
116/// items inside them. This is due to both game balance purposes, and the lack
117/// of a UI to show such items. Because of this, any action that would result in
118/// such an item being put into the inventory (item pickup, unequipping an item
119/// that contains items etc) must first ensure items are unloaded from the item.
120/// This is handled in `inventory\slot.rs`
121impl Inventory {
122    pub fn with_empty() -> Inventory {
123        Self::with_loadout_humanoid(LoadoutBuilder::empty().build())
124    }
125
126    pub fn with_loadout(loadout: Loadout, body: Body) -> Inventory {
127        if let Body::Humanoid(_) = body {
128            Self::with_loadout_humanoid(loadout)
129        } else {
130            Self::with_loadout_animal(loadout)
131        }
132    }
133
134    pub fn with_loadout_humanoid(loadout: Loadout) -> Inventory {
135        Inventory {
136            next_sort_order: InventorySortOrder::Name,
137            loadout,
138            slots: vec![None; DEFAULT_INVENTORY_SLOTS],
139            overflow_items: Vec::new(),
140            recipe_book: RecipeBook::default(),
141        }
142    }
143
144    pub fn with_loadout_animal(loadout: Loadout) -> Inventory {
145        Inventory {
146            next_sort_order: InventorySortOrder::Name,
147            loadout,
148            slots: vec![None; 1],
149            overflow_items: Vec::new(),
150            recipe_book: RecipeBook::default(),
151        }
152    }
153
154    pub fn with_recipe_book(mut self, recipe_book: RecipeBook) -> Inventory {
155        self.recipe_book = recipe_book;
156        self
157    }
158
159    /// Total number of slots in in the inventory.
160    pub fn capacity(&self) -> usize { self.slots().count() }
161
162    /// An iterator of all inventory slots
163    pub fn slots(&self) -> impl Iterator<Item = &InvSlot> {
164        self.slots
165            .iter()
166            .chain(self.loadout.inv_slots_with_id().map(|(_, slot)| slot))
167    }
168
169    /// An iterator of all overflow slots in the inventory
170    pub fn overflow_items(&self) -> impl Iterator<Item = &Item> { self.overflow_items.iter() }
171
172    /// A mutable iterator of all inventory slots
173    fn slots_mut(&mut self) -> impl Iterator<Item = &mut InvSlot> {
174        self.slots.iter_mut().chain(self.loadout.inv_slots_mut())
175    }
176
177    fn slots_mut_with_mutable_recently_unequipped_items(
178        &mut self,
179    ) -> (
180        impl Iterator<Item = &mut InvSlot>,
181        &mut HashMap<ItemDefinitionIdOwned, (Time, u8)>,
182    ) {
183        let (slots_mut, recently_unequipped) = self
184            .loadout
185            .inv_slots_mut_with_mutable_recently_unequipped_items();
186        (self.slots.iter_mut().chain(slots_mut), recently_unequipped)
187    }
188
189    /// An iterator of all inventory slots and their position
190    pub fn slots_with_id(&self) -> impl Iterator<Item = (InvSlotId, &InvSlot)> {
191        self.slots
192            .iter()
193            .enumerate()
194            .map(|(i, slot)| ((InvSlotId::new(0, u16::try_from(i).unwrap())), slot))
195            .chain(
196                self.loadout
197                    .inv_slots_with_id()
198                    .map(|(loadout_slot_id, inv_slot)| (loadout_slot_id.into(), inv_slot)),
199            )
200    }
201
202    /// If custom_order is empty, it will always return Ordering::Equal
203    pub fn order_by_custom(custom_order: &[CustomOrder], a: &Item, b: &Item) -> Ordering {
204        let mut order = custom_order.iter();
205        let a_quality = a.quality();
206        let b_quality = b.quality();
207        let a_kind = a.kind().get_itemkind_string();
208        let b_kind = b.kind().get_itemkind_string();
209        let mut cmp = Ordering::Equal;
210        while cmp == Ordering::Equal {
211            match order.next() {
212                Some(CustomOrder::KindFull) => cmp = Ord::cmp(&a_kind, &b_kind),
213                Some(CustomOrder::KindPartial) => {
214                    cmp = Ord::cmp(
215                        &a_kind.split_once(':').unwrap().0,
216                        &b_kind.split_once(':').unwrap().0,
217                    )
218                },
219                Some(CustomOrder::Quality) => cmp = Ord::cmp(&b_quality, &a_quality),
220                #[expect(deprecated)]
221                Some(CustomOrder::Name) => cmp = Ord::cmp(&a.name(), &b.name()),
222                Some(CustomOrder::Tag) => {
223                    cmp = Ord::cmp(
224                        &a.tags().first().map_or("", |tag| tag.name()),
225                        &b.tags().first().map_or("", |tag| tag.name()),
226                    )
227                },
228                _ => break,
229            }
230        }
231        cmp
232    }
233
234    /// Sorts the inventory using the next sort order
235    pub fn sort(&mut self) {
236        let sort_order = self.next_sort_order;
237        let mut items: Vec<Item> = self.slots_mut().filter_map(mem::take).collect();
238
239        items.sort_by(|a, b| match sort_order {
240            #[expect(deprecated)]
241            InventorySortOrder::Name => Ord::cmp(&a.name(), &b.name()),
242            // Quality is sorted in reverse since we want high quality items first
243            InventorySortOrder::Quality => Ord::cmp(&b.quality(), &a.quality()),
244            InventorySortOrder::Category => {
245                let order = [
246                    CustomOrder::KindPartial,
247                    CustomOrder::Quality,
248                    CustomOrder::KindFull,
249                    CustomOrder::Name,
250                ];
251                Self::order_by_custom(&order, a, b)
252            },
253            InventorySortOrder::Tag => Ord::cmp(
254                &a.tags().first().map_or("", |tag| tag.name()),
255                &b.tags().first().map_or("", |tag| tag.name()),
256            ),
257            // Amount is sorted in reverse since we want high amounts items first
258            InventorySortOrder::Amount => Ord::cmp(&b.amount(), &a.amount()),
259        });
260
261        self.push_all(items.into_iter()).expect(
262            "It is impossible for there to be insufficient inventory space when sorting the \
263             inventory",
264        );
265
266        self.next_sort_order = self.next_sort_order.next();
267    }
268
269    /// Returns the sort order that will be used when Inventory::sort() is next
270    /// called
271    pub fn next_sort_order(&self) -> InventorySortOrder { self.next_sort_order }
272
273    /// Same as [`push`], but if `slot` is empty it will put the item there.
274    /// Stackables will first be merged into existing stacks even when a `slot`
275    /// is provided.
276    fn push_prefer_slot(
277        &mut self,
278        mut item: Item,
279        slot: Option<InvSlotId>,
280    ) -> Result<(), (Item, Option<NonZeroU32>)> {
281        // If the item is stackable, we can increase the amount of other equal items up
282        // to max_amount before inserting a new item if there is still a remaining
283        // amount (caused by overflow or no other equal stackable being present in the
284        // inventory).
285        if item.is_stackable() {
286            let total_amount = item.amount();
287
288            let remaining = self
289                .slots_mut()
290                .filter_map(Option::as_mut)
291                .filter(|s| *s == &item)
292                .try_fold(total_amount, |remaining, current| {
293                    debug_assert_eq!(
294                        item.max_amount(),
295                        current.max_amount(),
296                        "max_amount of two equal items must match"
297                    );
298
299                    // NOTE: Invariant that current.amount <= current.max_amount(), so this
300                    // subtraction is safe.
301                    let new_remaining = remaining
302                        .checked_sub(current.max_amount() - current.amount())
303                        .filter(|&remaining| remaining > 0);
304                    if new_remaining.is_some() {
305                        // Not enough capacity left to hold all the remaining items, so we set this
306                        // one to max.
307                        current
308                            .set_amount(current.max_amount())
309                            .expect("max_amount() is always a valid amount");
310                    } else {
311                        // Enough capacity to hold all the remaining items.
312                        current.increase_amount(remaining).expect(
313                            "This item must be able to contain the remaining amount, because \
314                             remaining < current.max_amount() - current.amount()",
315                        );
316                    }
317
318                    new_remaining
319                });
320
321            if let Some(remaining) = remaining {
322                item.set_amount(remaining)
323                    .expect("Remaining is known to be > 0");
324                self.insert_prefer_slot(item, slot)
325                    .map_err(|item| (item, NonZeroU32::new(total_amount - remaining)))
326            } else {
327                Ok(())
328            }
329        } else {
330            // The item isn't stackable, insert it directly
331            self.insert_prefer_slot(item, slot)
332                .map_err(|item| (item, None))
333        }
334    }
335
336    /// Adds a new item to the fitting slots of the inventory or starts a
337    /// new group. Returns the item in an error if no space was found.
338    ///
339    /// WARNING: This **may** make inventory modifications if `Err(item)` is
340    /// returned. The second tuple field in the error is the number of items
341    /// that were successfully inserted into the inventory.
342    pub fn push(&mut self, item: Item) -> Result<(), (Item, Option<NonZeroU32>)> {
343        self.push_prefer_slot(item, None)
344    }
345
346    /// Add a series of items to inventory, returning any which do not fit as an
347    /// error.
348    pub fn push_all<I: Iterator<Item = Item>>(&mut self, items: I) -> Result<(), Error> {
349        // Vec doesn't allocate for zero elements so this should be cheap
350        let mut leftovers = Vec::new();
351        for item in items {
352            if let Err((item, _)) = self.push(item) {
353                leftovers.push(item);
354            }
355        }
356        if !leftovers.is_empty() {
357            Err(Error::Full(leftovers))
358        } else {
359            Ok(())
360        }
361    }
362
363    /// Add a series of items to an inventory without giving duplicates.
364    /// (n * m complexity)
365    ///
366    /// Error if inventory cannot contain the items (is full), returning the
367    /// un-added items. This is a lazy inefficient implementation, as it
368    /// iterates over the inventory more times than necessary (n^2) and with
369    /// the proper structure wouldn't need to iterate at all, but because
370    /// this should be fairly cold code, clarity has been favored over
371    /// efficiency.
372    pub fn push_all_unique<I: Iterator<Item = Item>>(&mut self, mut items: I) -> Result<(), Error> {
373        let mut leftovers = Vec::new();
374        for item in &mut items {
375            if self.contains(&item).not() {
376                if let Err((overflow, _)) = self.push(item) {
377                    leftovers.push(overflow);
378                }
379            } // else drop item if it was already in
380        }
381        if !leftovers.is_empty() {
382            Err(Error::Full(leftovers))
383        } else {
384            Ok(())
385        }
386    }
387
388    /// Replaces an item in a specific slot of the inventory. Returns the old
389    /// item or the same item again if that slot was not found.
390    pub fn insert_at(&mut self, inv_slot_id: InvSlotId, item: Item) -> Result<Option<Item>, Item> {
391        match self.slot_mut(inv_slot_id) {
392            Some(slot) => Ok(mem::replace(slot, Some(item))),
393            None => Err(item),
394        }
395    }
396
397    /// Merge the stack of items at src into the stack at dst if the items are
398    /// compatible and stackable, and return whether anything was changed
399    pub fn merge_stack_into(&mut self, src: InvSlotId, dst: InvSlotId) -> bool {
400        let mut amount = None;
401        if let (Some(srcitem), Some(dstitem)) = (self.get(src), self.get(dst)) {
402            // The equality check ensures the items have the same definition, to avoid e.g.
403            // transmuting coins to diamonds, and the stackable check avoids creating a
404            // stack of swords
405            if srcitem == dstitem && srcitem.is_stackable() {
406                amount = Some(srcitem.amount());
407            }
408        }
409        if let Some(amount) = amount {
410            let dstitem = self
411                .get_mut(dst)
412                .expect("self.get(dst) was Some right above this");
413            dstitem
414                .increase_amount(amount)
415                .map(|_| {
416                    // Suceeded in adding the item, so remove it from `src`.
417                    self.remove(src).expect("Already verified that src was populated.");
418                })
419                // Can fail if we exceed `max_amount`
420                .is_ok()
421        } else {
422            false
423        }
424    }
425
426    /// Checks if inserting item exists in given cell. Inserts an item if it
427    /// exists.
428    pub fn insert_or_stack_at(
429        &mut self,
430        inv_slot_id: InvSlotId,
431        item: Item,
432    ) -> Result<Option<Item>, Item> {
433        if item.is_stackable() {
434            match self.slot_mut(inv_slot_id) {
435                Some(Some(slot_item)) => {
436                    Ok(if slot_item == &item {
437                        slot_item
438                            .increase_amount(item.amount())
439                            .err()
440                            .and(Some(item))
441                    } else {
442                        let old_item = mem::replace(slot_item, item);
443                        // No need to recount--we know the count is the same.
444                        Some(old_item)
445                    })
446                },
447                Some(None) => self.insert_at(inv_slot_id, item),
448                None => Err(item),
449            }
450        } else {
451            self.insert_at(inv_slot_id, item)
452        }
453    }
454
455    /// Attempts to equip the item into a compatible, unpopulated loadout slot.
456    /// If no slot is available the item is returned.
457    #[must_use = "Returned item will be lost if not used"]
458    pub fn try_equip(&mut self, item: Item) -> Result<(), Item> { self.loadout.try_equip(item) }
459
460    pub fn populated_slots(&self) -> usize { self.slots().filter_map(|slot| slot.as_ref()).count() }
461
462    pub fn free_slots(&self) -> usize { self.slots().filter(|slot| slot.is_none()).count() }
463
464    /// Check if an item is in this inventory.
465    pub fn contains(&self, item: &Item) -> bool {
466        self.slots().any(|slot| slot.as_ref() == Some(item))
467    }
468
469    /// Return the first slot id containing the item
470    pub fn get_slot_of_item(&self, item: &Item) -> Option<InvSlotId> {
471        self.slots_with_id()
472            .find(|&(_, it)| {
473                if let Some(it) = it {
474                    it.item_definition_id() == item.item_definition_id()
475                } else {
476                    false
477                }
478            })
479            .map(|(slot, _)| slot)
480    }
481
482    pub fn get_slot_of_item_by_def_id(
483        &self,
484        item_def_id: &item::ItemDefinitionIdOwned,
485    ) -> Option<InvSlotId> {
486        self.slots_with_id()
487            .find(|&(_, it)| {
488                if let Some(it) = it {
489                    it.item_definition_id() == *item_def_id
490                } else {
491                    false
492                }
493            })
494            .map(|(slot, _)| slot)
495    }
496
497    /// Get content of a slot
498    pub fn get(&self, inv_slot_id: InvSlotId) -> Option<&Item> {
499        self.slot(inv_slot_id).and_then(Option::as_ref)
500    }
501
502    /// Get content of an overflow slot
503    pub fn get_overflow(&self, overflow: usize) -> Option<&Item> {
504        self.overflow_items.get(overflow)
505    }
506
507    /// Get content of any kind of slot
508    pub fn get_slot(&self, slot: Slot) -> Option<&Item> {
509        match slot {
510            Slot::Inventory(inv_slot) => self.get(inv_slot),
511            Slot::Equip(equip) => self.equipped(equip),
512            Slot::Overflow(overflow) => self.get_overflow(overflow),
513        }
514    }
515
516    /// Get item from inventory
517    pub fn get_by_hash(&self, item_hash: u64) -> Option<&Item> {
518        self.slots().flatten().find(|i| i.item_hash() == item_hash)
519    }
520
521    /// Get slot from hash
522    pub fn get_slot_from_hash(&self, item_hash: u64) -> Option<InvSlotId> {
523        let slot_with_id = self.slots_with_id().find(|slot| match slot.1 {
524            None => false,
525            Some(item) => item.item_hash() == item_hash,
526        });
527        slot_with_id.map(|s| s.0)
528    }
529
530    /// Mutably get content of a slot
531    fn get_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut Item> {
532        self.slot_mut(inv_slot_id).and_then(Option::as_mut)
533    }
534
535    /// Returns a reference to the item (if any) equipped in the given EquipSlot
536    pub fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> {
537        self.loadout.equipped(equip_slot)
538    }
539
540    pub fn loadout_items_with_persistence_key(
541        &self,
542    ) -> impl Iterator<Item = (&str, Option<&Item>)> {
543        self.loadout.items_with_persistence_key()
544    }
545
546    /// Returns the range of inventory slot indexes that a particular equipped
547    /// item provides (used for UI highlighting of inventory slots when hovering
548    /// over a loadout item)
549    pub fn get_slot_range_for_equip_slot(&self, equip_slot: EquipSlot) -> Option<Range<usize>> {
550        // The slot range returned from `Loadout` must be offset by the number of slots
551        // that the inventory itself provides.
552        let offset = self.slots.len();
553        self.loadout
554            .slot_range_for_equip_slot(equip_slot)
555            .map(|loadout_range| (loadout_range.start + offset)..(loadout_range.end + offset))
556    }
557
558    /// Swap the items inside of two slots
559    pub fn swap_slots(&mut self, a: InvSlotId, b: InvSlotId) {
560        if self.slot(a).is_none() || self.slot(b).is_none() {
561            warn!("swap_slots called with non-existent inventory slot(s)");
562            return;
563        }
564
565        let slot_a = mem::take(self.slot_mut(a).unwrap());
566        let slot_b = mem::take(self.slot_mut(b).unwrap());
567        *self.slot_mut(a).unwrap() = slot_b;
568        *self.slot_mut(b).unwrap() = slot_a;
569    }
570
571    /// Moves an item from an overflow slot to an inventory slot
572    pub fn move_overflow_item(&mut self, overflow: usize, inv_slot: InvSlotId) {
573        match self.slot(inv_slot) {
574            Some(Some(_)) => {
575                warn!("Attempted to move from overflow slot to a filled inventory slot");
576                return;
577            },
578            None => {
579                warn!("Attempted to move from overflow slot to a non-existent inventory slot");
580                return;
581            },
582            Some(None) => {},
583        };
584
585        let item = self.overflow_items.remove(overflow);
586        *self.slot_mut(inv_slot).unwrap() = Some(item);
587    }
588
589    /// Remove an item from the slot
590    pub fn remove(&mut self, inv_slot_id: InvSlotId) -> Option<Item> {
591        self.slot_mut(inv_slot_id).and_then(|item| item.take())
592    }
593
594    /// Remove an item from an overflow slot
595    #[must_use = "Returned items will be lost if not used"]
596    pub fn overflow_remove(&mut self, overflow_slot: usize) -> Option<Item> {
597        if overflow_slot < self.overflow_items.len() {
598            Some(self.overflow_items.remove(overflow_slot))
599        } else {
600            None
601        }
602    }
603
604    /// Remove just one item from the slot
605    pub fn take(
606        &mut self,
607        inv_slot_id: InvSlotId,
608        ability_map: &AbilityMap,
609        msm: &MaterialStatManifest,
610    ) -> Option<Item> {
611        if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
612            let mut return_item = item.duplicate(ability_map, msm);
613
614            if item.is_stackable() && item.amount() > 1 {
615                item.decrease_amount(1).ok()?;
616                return_item
617                    .set_amount(1)
618                    .expect("Items duplicated from a stackable item must be stackable.");
619                Some(return_item)
620            } else {
621                self.remove(inv_slot_id)
622            }
623        } else {
624            None
625        }
626    }
627
628    /// Takes an amount of items from a slot. If the amount to take is larger
629    /// than the item amount, the item amount will be returned instead.
630    pub fn take_amount(
631        &mut self,
632        inv_slot_id: InvSlotId,
633        amount: NonZeroU32,
634        ability_map: &AbilityMap,
635        msm: &MaterialStatManifest,
636    ) -> Option<Item> {
637        if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
638            if item.is_stackable() && item.amount() > amount.get() {
639                let mut return_item = item.duplicate(ability_map, msm);
640                let return_amount = amount.get();
641                // Will never overflow since we know item.amount() > amount.get()
642                let new_amount = item.amount() - return_amount;
643
644                return_item
645                    .set_amount(return_amount)
646                    .expect("We know that 0 < return_amount < item.amount()");
647                item.set_amount(new_amount)
648                    .expect("new_amount must be > 0 since return item is < item.amount");
649
650                Some(return_item)
651            } else {
652                // If return_amount == item.amount or the item's amount is one, we
653                // can just pop it from the inventory
654                self.remove(inv_slot_id)
655            }
656        } else {
657            None
658        }
659    }
660
661    /// Takes half of the items from a slot in the inventory
662    #[must_use = "Returned items will be lost if not used"]
663    pub fn take_half(
664        &mut self,
665        inv_slot_id: InvSlotId,
666        ability_map: &AbilityMap,
667        msm: &MaterialStatManifest,
668    ) -> Option<Item> {
669        if let Some(Some(item)) = self.slot_mut(inv_slot_id) {
670            item.take_half(ability_map, msm)
671                .or_else(|| self.remove(inv_slot_id))
672        } else {
673            None
674        }
675    }
676
677    /// Takes half of the items from an overflow slot
678    #[must_use = "Returned items will be lost if not used"]
679    pub fn overflow_take_half(
680        &mut self,
681        overflow_slot: usize,
682        ability_map: &AbilityMap,
683        msm: &MaterialStatManifest,
684    ) -> Option<Item> {
685        if let Some(item) = self.overflow_items.get_mut(overflow_slot) {
686            item.take_half(ability_map, msm)
687                .or_else(|| self.overflow_remove(overflow_slot))
688        } else {
689            None
690        }
691    }
692
693    /// Takes all items from the inventory
694    pub fn drain(&mut self) -> impl Iterator<Item = Item> + '_ {
695        self.slots_mut()
696            .filter(|x| x.is_some())
697            .filter_map(mem::take)
698    }
699
700    /// Determine how many of a particular item there is in the inventory.
701    pub fn item_count(&self, item_def: &ItemDef) -> u64 {
702        self.slots()
703            .flatten()
704            .filter(|it| it.is_same_item_def(item_def))
705            .map(|it| u64::from(it.amount()))
706            .sum()
707    }
708
709    /// Determine whether the inventory has space to contain the given item, of
710    /// the given amount.
711    pub fn has_space_for(&self, item_def: &ItemDef, amount: u32) -> bool {
712        let mut free_space = 0u32;
713        self.slots().any(|i| {
714            free_space = free_space.saturating_add(if let Some(item) = i {
715                if item.is_same_item_def(item_def) {
716                    // Invariant amount <= max_amount *should* take care of this, but let's be
717                    // safe
718                    item.max_amount().saturating_sub(item.amount())
719                } else {
720                    0
721                }
722            } else {
723                // A free slot can hold ItemDef::max_amount items!
724                item_def.max_amount()
725            });
726            free_space >= amount
727        })
728    }
729
730    /// Returns true if the item can fit in the inventory without taking up an
731    /// empty inventory slot.
732    pub fn can_stack(&self, item: &Item) -> bool {
733        let mut free_space = 0u32;
734        self.slots().any(|i| {
735            free_space = free_space.saturating_add(if let Some(inv_item) = i {
736                if inv_item == item {
737                    // Invariant amount <= max_amount *should* take care of this, but let's be
738                    // safe
739                    inv_item.max_amount().saturating_sub(inv_item.amount())
740                } else {
741                    0
742                }
743            } else {
744                0
745            });
746            free_space >= item.amount()
747        })
748    }
749
750    /// Remove the given amount of the given item from the inventory.
751    ///
752    /// The returned items will have arbitrary amounts, but their sum will be
753    /// `amount`.
754    ///
755    /// If the inventory does not contain sufficient items, `None` will be
756    /// returned.
757    pub fn remove_item_amount(
758        &mut self,
759        item_def: &ItemDef,
760        amount: u32,
761        ability_map: &AbilityMap,
762        msm: &MaterialStatManifest,
763    ) -> Option<Vec<Item>> {
764        let mut amount = amount;
765        if self.item_count(item_def) >= u64::from(amount) {
766            let mut removed_items = Vec::new();
767            for slot in self.slots_mut() {
768                if amount == 0 {
769                    // We've collected enough
770                    return Some(removed_items);
771                } else if let Some(item) = slot
772                    && item.is_same_item_def(item_def)
773                {
774                    if amount < item.amount() {
775                        // Remove just the amount we need to finish off
776                        // Note: Unwrap is fine, we've already checked that amount > 0
777                        removed_items.push(item.take_amount(ability_map, msm, amount).unwrap());
778                        return Some(removed_items);
779                    } else {
780                        // Take the whole item and keep going
781                        amount -= item.amount();
782                        removed_items.push(slot.take().unwrap());
783                    }
784                }
785            }
786            debug_assert_eq!(amount, 0);
787            Some(removed_items)
788        } else {
789            None
790        }
791    }
792
793    /// Adds a new item to `slot` if empty or the first empty slot of the
794    /// inventory. Returns the item again in an Err if no free slot was
795    /// found.
796    fn insert_prefer_slot(&mut self, item: Item, slot: Option<InvSlotId>) -> Result<(), Item> {
797        if let Some(slot @ None) = slot.and_then(|slot| self.slot_mut(slot)) {
798            *slot = Some(item);
799            Ok(())
800        } else {
801            self.insert(item)
802        }
803    }
804
805    /// Adds a new item to the first empty slot of the inventory. Returns the
806    /// item again in an Err if no free slot was found.
807    fn insert(&mut self, item: Item) -> Result<(), Item> {
808        match self.slots_mut().find(|slot| slot.is_none()) {
809            Some(slot) => {
810                *slot = Some(item);
811                Ok(())
812            },
813            None => Err(item),
814        }
815    }
816
817    pub fn slot(&self, inv_slot_id: InvSlotId) -> Option<&InvSlot> {
818        match SlotId::from(inv_slot_id) {
819            SlotId::Inventory(slot_idx) => self.slots.get(slot_idx),
820            SlotId::Loadout(loadout_slot_id) => self.loadout.inv_slot(loadout_slot_id),
821        }
822    }
823
824    pub fn slot_mut(&mut self, inv_slot_id: InvSlotId) -> Option<&mut InvSlot> {
825        match SlotId::from(inv_slot_id) {
826            SlotId::Inventory(slot_idx) => self.slots.get_mut(slot_idx),
827            SlotId::Loadout(loadout_slot_id) => self.loadout.inv_slot_mut(loadout_slot_id),
828        }
829    }
830
831    /// Returns the number of free slots in the inventory ignoring any slots
832    /// granted by the item (if any) equipped in the provided EquipSlot.
833    pub fn free_slots_minus_equipped_item(&self, equip_slot: EquipSlot) -> usize {
834        if let Some(mut equip_slot_idx) = self.loadout.loadout_idx_for_equip_slot(equip_slot) {
835            // Offset due to index 0 representing built-in inventory slots
836            equip_slot_idx += 1;
837
838            self.slots_with_id()
839                .filter(|(inv_slot_id, slot)| {
840                    inv_slot_id.loadout_idx() != equip_slot_idx && slot.is_none()
841                })
842                .count()
843        } else {
844            // TODO: return Option<usize> and evaluate to None here
845            warn!(
846                "Attempted to fetch loadout index for non-existent EquipSlot: {:?}",
847                equip_slot
848            );
849            0
850        }
851    }
852
853    pub fn equipped_items(&self) -> impl Iterator<Item = &Item> { self.loadout.items() }
854
855    pub fn equipped_items_with_slot(&self) -> impl Iterator<Item = (EquipSlot, &Item)> {
856        self.loadout.items_with_slot()
857    }
858
859    /// Replaces the loadout item (if any) in the given EquipSlot with the
860    /// provided item, returning the item that was previously in the slot.
861    pub fn replace_loadout_item(
862        &mut self,
863        equip_slot: EquipSlot,
864        replacement_item: Option<Item>,
865        time: Time,
866    ) -> Option<Item> {
867        self.loadout.swap(equip_slot, replacement_item, time)
868    }
869
870    /// Equip an item from a slot in inventory. The currently equipped item will
871    /// go into inventory.
872    /// Since loadout slots cannot currently hold items with an amount larger
873    /// than one, only one item will be taken from the inventory and
874    /// equipped
875    #[must_use = "Returned items will be lost if not used"]
876    pub fn equip(
877        &mut self,
878        inv_slot: InvSlotId,
879        time: Time,
880        ability_map: &AbilityMap,
881        msm: &MaterialStatManifest,
882    ) -> Result<Option<Vec<Item>>, SlotError> {
883        let Some(item) = self.get(inv_slot) else {
884            return Ok(None);
885        };
886
887        let Some(equip_slot) = self.loadout.get_slot_to_equip_into(&item.kind()) else {
888            return Ok(None);
889        };
890
891        let item = self
892            .take(inv_slot, ability_map, msm)
893            .expect("We got this successfully above");
894
895        if let Some(mut unequipped_item) = self.replace_loadout_item(equip_slot, Some(item), time) {
896            let mut unloaded_items: Vec<Item> = unequipped_item.drain().collect();
897            if let Err((item, _)) = self.push_prefer_slot(unequipped_item, Some(inv_slot)) {
898                // Insert it at 0 to prioritize the uneqipped item.
899                unloaded_items.insert(0, item);
900            }
901            // Unload any items that were inside the equipped item into the inventory, with
902            // any that don't fit to be to be dropped on the floor by the caller
903            match self.push_all(unloaded_items.into_iter()) {
904                Ok(()) => {},
905                Err(Error::Full(items)) => return Ok(Some(items)),
906            }
907        }
908
909        Ok(None)
910    }
911
912    /// Determines how many free inventory slots will be left after equipping an
913    /// item (because it could be swapped with an already equipped item that
914    /// provides more inventory slots than the item being equipped)
915    pub fn free_after_equip(&self, inv_slot: InvSlotId) -> i32 {
916        let (inv_slot_for_equipped, slots_from_equipped) = self
917            .get(inv_slot)
918            .and_then(|item| self.loadout.get_slot_to_equip_into(&item.kind()))
919            .and_then(|equip_slot| self.equipped(equip_slot))
920            .map_or((1, 0), |item| {
921                (
922                    if item.is_stackable() && self.can_stack(item) {
923                        1
924                    } else {
925                        0
926                    },
927                    item.slots().len(),
928                )
929            });
930
931        let (inv_slot_for_inv, slots_from_inv) = self.get(inv_slot).map_or((0, 0), |item| {
932            (if item.amount() > 1 { -1 } else { 0 }, item.slots().len())
933        });
934
935        i32::try_from(self.capacity()).expect("Inventory with more than i32::MAX slots")
936            - i32::try_from(slots_from_equipped)
937                .expect("Equipped item with more than i32::MAX slots")
938            + i32::try_from(slots_from_inv).expect("Inventory item with more than i32::MAX slots")
939            - i32::try_from(self.populated_slots())
940                .expect("Inventory item with more than i32::MAX used slots")
941            + inv_slot_for_equipped // If there is no item already in the equip slot we gain 1 slot
942            + inv_slot_for_inv
943    }
944
945    /// Handles picking up an item, unloading any items inside the item being
946    /// picked up and pushing them to the inventory to ensure that items
947    /// containing items aren't inserted into the inventory as this is not
948    /// currently supported.
949    ///
950    /// WARNING: The `Err(_)` variant may still cause inventory modifications,
951    /// note on [`Inventory::push`]
952    pub fn pickup_item(&mut self, mut item: Item) -> Result<(), (Item, Option<NonZeroU32>)> {
953        if item.is_stackable() {
954            return self.push(item);
955        }
956
957        if self.free_slots() < item.populated_slots() + 1 {
958            return Err((item, None));
959        }
960
961        // Unload any items contained within the item, and push those items and the item
962        // itself into the inventory. We already know that there are enough free slots
963        // so push will never give us an item back.
964        item.drain().for_each(|item| {
965            self.push(item).unwrap();
966        });
967        self.push(item)
968    }
969
970    /// Unequip an item from slot and place into inventory. Will leave the item
971    /// equipped if inventory has no slots available.
972    #[must_use = "Returned items will be lost if not used"]
973    #[expect(clippy::needless_collect)] // This is a false positive, the collect is needed
974    pub fn unequip(
975        &mut self,
976        equip_slot: EquipSlot,
977        time: Time,
978    ) -> Result<Option<Vec<Item>>, SlotError> {
979        // Ensure there is enough space in the inventory to place the unequipped item
980        if self.free_slots_minus_equipped_item(equip_slot) == 0 {
981            return Err(SlotError::InventoryFull);
982        }
983
984        Ok(self
985            .loadout
986            .swap(equip_slot, None, time)
987            .and_then(|mut unequipped_item| {
988                let unloaded_items: Vec<Item> = unequipped_item.drain().collect();
989                self.push(unequipped_item)
990                    .expect("Failed to push item to inventory, precondition failed?");
991
992                // Unload any items that were inside the equipped item into the inventory, with
993                // any that don't fit to be to be dropped on the floor by the caller
994                match self.push_all(unloaded_items.into_iter()) {
995                    Err(Error::Full(leftovers)) => Some(leftovers),
996                    Ok(()) => None,
997                }
998            }))
999    }
1000
1001    /// Determines how many free inventory slots will be left after unequipping
1002    /// an item
1003    pub fn free_after_unequip(&self, equip_slot: EquipSlot) -> i32 {
1004        let (inv_slot_for_unequipped, slots_from_equipped) = self
1005            .equipped(equip_slot)
1006            .map_or((0, 0), |item| (1, item.slots().len()));
1007
1008        i32::try_from(self.capacity()).expect("Inventory with more than i32::MAX slots")
1009            - i32::try_from(slots_from_equipped)
1010                .expect("Equipped item with more than i32::MAX slots")
1011            - i32::try_from(self.populated_slots())
1012                .expect("Inventory item with more than i32::MAX used slots")
1013            - inv_slot_for_unequipped // If there is an item being unequipped we lose 1 slot
1014    }
1015
1016    /// Swaps items from two slots, regardless of if either is inventory or
1017    /// loadout.
1018    #[must_use = "Returned items will be lost if not used"]
1019    pub fn swap(&mut self, slot_a: Slot, slot_b: Slot, time: Time) -> Vec<Item> {
1020        match (slot_a, slot_b) {
1021            (Slot::Inventory(slot_a), Slot::Inventory(slot_b)) => {
1022                self.swap_slots(slot_a, slot_b);
1023                Vec::new()
1024            },
1025            (Slot::Inventory(inv_slot), Slot::Equip(equip_slot))
1026            | (Slot::Equip(equip_slot), Slot::Inventory(inv_slot)) => {
1027                self.swap_inventory_loadout(inv_slot, equip_slot, time)
1028            },
1029            (Slot::Equip(slot_a), Slot::Equip(slot_b)) => {
1030                self.loadout.swap_slots(slot_a, slot_b, time);
1031                Vec::new()
1032            },
1033            (Slot::Overflow(overflow_slot), Slot::Inventory(inv_slot))
1034            | (Slot::Inventory(inv_slot), Slot::Overflow(overflow_slot)) => {
1035                self.move_overflow_item(overflow_slot, inv_slot);
1036                Vec::new()
1037            },
1038            // Items from overflow slots cannot be equipped until moved into a real inventory slot
1039            (Slot::Overflow(_), Slot::Equip(_)) | (Slot::Equip(_), Slot::Overflow(_)) => Vec::new(),
1040            // Items cannot be moved between overflow slots
1041            (Slot::Overflow(_), Slot::Overflow(_)) => Vec::new(),
1042        }
1043    }
1044
1045    /// Determines how many free inventory slots will be left after swapping two
1046    /// item slots
1047    pub fn free_after_swap(&self, equip_slot: EquipSlot, inv_slot: InvSlotId) -> i32 {
1048        let (inv_slot_for_equipped, slots_from_equipped) = self
1049            .equipped(equip_slot)
1050            .map_or((0, 0), |item| (1, item.slots().len()));
1051        let (inv_slot_for_inv_item, slots_from_inv_item) = self
1052            .get(inv_slot)
1053            .map_or((0, 0), |item| (1, item.slots().len()));
1054
1055        // Return the number of inventory slots that will be free once this slot swap is
1056        // performed
1057        i32::try_from(self.capacity())
1058            .expect("inventory with more than i32::MAX slots")
1059            - i32::try_from(slots_from_equipped)
1060            .expect("equipped item with more than i32::MAX slots")
1061            + i32::try_from(slots_from_inv_item)
1062            .expect("inventory item with more than i32::MAX slots")
1063            - i32::try_from(self.populated_slots())
1064            .expect("inventory with more than i32::MAX used slots")
1065            - inv_slot_for_equipped // +1 inventory slot required if an item was unequipped
1066            + inv_slot_for_inv_item // -1 inventory slot required if an item was equipped
1067    }
1068
1069    /// Swap item in an inventory slot with one in a loadout slot.
1070    #[must_use = "Returned items will be lost if not used"]
1071    pub fn swap_inventory_loadout(
1072        &mut self,
1073        inv_slot_id: InvSlotId,
1074        equip_slot: EquipSlot,
1075        time: Time,
1076    ) -> Vec<Item> {
1077        if !self.can_swap(inv_slot_id, equip_slot) {
1078            return Vec::new();
1079        }
1080
1081        // Take the item from the inventory
1082        let from_inv = self.remove(inv_slot_id);
1083
1084        // Swap the equipped item for the item from the inventory
1085        let from_equip = self.loadout.swap(equip_slot, from_inv, time);
1086
1087        let unloaded_items = from_equip
1088            .map(|mut from_equip| {
1089                // Unload any items held inside the previously equipped item
1090                let mut items: Vec<Item> = from_equip.drain().collect();
1091
1092                // Attempt to put the unequipped item in the same slot that the inventory item
1093                // was in - if that slot no longer exists (because a large container was
1094                // swapped for a smaller one) then we will attempt to push it to the inventory
1095                // with the rest of the unloaded items.
1096                if let Err(returned) = self.insert_at(inv_slot_id, from_equip) {
1097                    items.insert(0, returned);
1098                }
1099
1100                items
1101            })
1102            .unwrap_or_default();
1103
1104        // If 2 1h weapons are equipped, and mainhand weapon removed, move offhand into
1105        // mainhand
1106        match equip_slot {
1107            EquipSlot::ActiveMainhand => {
1108                if self.loadout.equipped(EquipSlot::ActiveMainhand).is_none()
1109                    && self.loadout.equipped(EquipSlot::ActiveOffhand).is_some()
1110                {
1111                    let offhand = self.loadout.swap(EquipSlot::ActiveOffhand, None, time);
1112                    assert!(
1113                        self.loadout
1114                            .swap(EquipSlot::ActiveMainhand, offhand, time)
1115                            .is_none()
1116                    );
1117                }
1118            },
1119            EquipSlot::InactiveMainhand => {
1120                if self.loadout.equipped(EquipSlot::InactiveMainhand).is_none()
1121                    && self.loadout.equipped(EquipSlot::InactiveOffhand).is_some()
1122                {
1123                    let offhand = self.loadout.swap(EquipSlot::InactiveOffhand, None, time);
1124                    assert!(
1125                        self.loadout
1126                            .swap(EquipSlot::InactiveMainhand, offhand, time)
1127                            .is_none()
1128                    );
1129                }
1130            },
1131            _ => {},
1132        }
1133
1134        // Attempt to put any items unloaded from the unequipped item into empty
1135        // inventory slots and return any that don't fit to the caller where they
1136        // will be dropped on the ground
1137        match self.push_all(unloaded_items.into_iter()) {
1138            Err(Error::Full(leftovers)) => leftovers,
1139            Ok(()) => Vec::new(),
1140        }
1141    }
1142
1143    /// Determines if an inventory and loadout slot can be swapped, taking into
1144    /// account whether there will be free space in the inventory for the
1145    /// loadout item once any slots that were provided by it have been
1146    /// removed.
1147    pub fn can_swap(&self, inv_slot_id: InvSlotId, equip_slot: EquipSlot) -> bool {
1148        // Check if loadout slot can hold item
1149        if !self
1150            .get(inv_slot_id)
1151            .is_none_or(|item| self.loadout.slot_can_hold(equip_slot, Some(&*item.kind())))
1152        {
1153            trace!("can_swap = false, equip slot can't hold item");
1154            return false;
1155        }
1156
1157        if self.slot(inv_slot_id).is_none() {
1158            debug!(
1159                "can_swap = false, tried to swap into non-existent inventory slot: {:?}",
1160                inv_slot_id
1161            );
1162            return false;
1163        }
1164
1165        if self.get(inv_slot_id).is_some_and(|item| item.amount() > 1) {
1166            trace!("can_swap = false, equip slot can't hold more than one item");
1167            return false;
1168        }
1169
1170        true
1171    }
1172
1173    pub fn equipped_items_replaceable_by<'a>(
1174        &'a self,
1175        item_kind: &'a ItemKind,
1176    ) -> impl Iterator<Item = &'a Item> {
1177        self.loadout.equipped_items_replaceable_by(item_kind)
1178    }
1179
1180    pub fn swap_equipped_weapons(&mut self, time: Time) { self.loadout.swap_equipped_weapons(time) }
1181
1182    /// Update internal computed state of all top level items in this loadout.
1183    /// Used only when loading in persistence code.
1184    pub fn persistence_update_all_item_states(
1185        &mut self,
1186        ability_map: &AbilityMap,
1187        msm: &MaterialStatManifest,
1188    ) {
1189        self.slots_mut().for_each(|slot| {
1190            if let Some(item) = slot {
1191                item.update_item_state(ability_map, msm);
1192            }
1193        });
1194        self.overflow_items
1195            .iter_mut()
1196            .for_each(|item| item.update_item_state(ability_map, msm));
1197    }
1198
1199    /// Increments durability lost for all valid items equipped in loadout and
1200    /// recently unequipped from loadout by 1
1201    pub fn damage_items(
1202        &mut self,
1203        ability_map: &item::tool::AbilityMap,
1204        msm: &item::MaterialStatManifest,
1205        time: Time,
1206    ) {
1207        self.loadout.damage_items(ability_map, msm);
1208        self.loadout.cull_recently_unequipped_items(time);
1209
1210        let (slots_mut, recently_unequipped_items) =
1211            self.slots_mut_with_mutable_recently_unequipped_items();
1212        slots_mut.filter_map(|slot| slot.as_mut()).for_each(|item| {
1213            if item
1214                .durability_lost()
1215                .is_some_and(|dur| dur < Item::MAX_DURABILITY)
1216                && let Some((_unequip_time, count)) =
1217                    recently_unequipped_items.get_mut(&item.item_definition_id())
1218                && *count > 0
1219            {
1220                *count -= 1;
1221                item.increment_damage(ability_map, msm);
1222            }
1223        });
1224    }
1225
1226    /// Resets durability of item in specified slot
1227    pub fn repair_item_at_slot(
1228        &mut self,
1229        slot: Slot,
1230        ability_map: &item::tool::AbilityMap,
1231        msm: &item::MaterialStatManifest,
1232    ) {
1233        match slot {
1234            Slot::Inventory(invslot) => {
1235                if let Some(Some(item)) = self.slot_mut(invslot) {
1236                    item.reset_durability(ability_map, msm);
1237                }
1238            },
1239            Slot::Equip(equip_slot) => {
1240                self.loadout
1241                    .repair_item_at_slot(equip_slot, ability_map, msm);
1242            },
1243            // Items in overflow slots cannot be repaired until they are moved to a real slot
1244            Slot::Overflow(_) => {},
1245        }
1246    }
1247
1248    /// When loading a character from the persistence system, pushes any items
1249    /// to overflow_items that were not able to be loaded into or pushed to the
1250    /// inventory
1251    pub fn persistence_push_overflow_items<I: Iterator<Item = Item>>(&mut self, overflow_items: I) {
1252        self.overflow_items.extend(overflow_items);
1253    }
1254
1255    pub fn recipes_iter(&self) -> impl ExactSizeIterator<Item = &String> { self.recipe_book.iter() }
1256
1257    pub fn recipe_groups_iter(&self) -> impl ExactSizeIterator<Item = &Item> {
1258        self.recipe_book.iter_groups()
1259    }
1260
1261    pub fn available_recipes_iter<'a>(
1262        &'a self,
1263        rbm: &'a RecipeBookManifest,
1264    ) -> impl Iterator<Item = (&'a String, &'a Recipe)> + 'a {
1265        self.recipe_book.get_available_iter(rbm)
1266    }
1267
1268    pub fn recipe_book_len(&self) -> usize { self.recipe_book.len() }
1269
1270    pub fn get_recipe<'a>(
1271        &'a self,
1272        recipe_key: &str,
1273        rbm: &'a RecipeBookManifest,
1274    ) -> Option<&'a Recipe> {
1275        self.recipe_book.get(recipe_key, rbm)
1276    }
1277
1278    pub fn push_recipe_group(&mut self, recipe_group: Item) -> Result<(), Item> {
1279        self.recipe_book.push_group(recipe_group)
1280    }
1281
1282    /// Returns whether the specified recipe can be crafted and the sprite, if
1283    /// any, that is required to do so.
1284    pub fn can_craft_recipe(
1285        &self,
1286        recipe_key: &str,
1287        amount: u32,
1288        rbm: &RecipeBookManifest,
1289    ) -> (bool, Option<SpriteKind>) {
1290        if let Some(recipe) = self.recipe_book.get(recipe_key, rbm) {
1291            (
1292                recipe.inventory_contains_ingredients(self, amount).is_ok(),
1293                recipe.craft_sprite,
1294            )
1295        } else {
1296            (false, None)
1297        }
1298    }
1299
1300    pub fn recipe_is_known(&self, recipe_key: &str) -> bool {
1301        self.recipe_book.is_known(recipe_key)
1302    }
1303
1304    pub fn reset_recipes(&mut self) { self.recipe_book.reset(); }
1305
1306    pub fn persistence_recipes_iter_with_index(&self) -> impl Iterator<Item = (usize, &Item)> {
1307        self.recipe_book.persistence_recipes_iter_with_index()
1308    }
1309}
1310
1311impl Component for Inventory {
1312    type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
1313}
1314
1315#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
1316pub enum CollectFailedReason {
1317    InventoryFull,
1318    LootOwned {
1319        owner: LootOwnerKind,
1320        expiry_secs: u64,
1321    },
1322}
1323
1324#[derive(Clone, Debug, Serialize, Deserialize)]
1325pub enum InventoryUpdateEvent {
1326    Init,
1327    Used,
1328    Consumed(ItemKey),
1329    Gave,
1330    Given,
1331    Swapped,
1332    Dropped,
1333    Collected(FrontendItem),
1334    BlockCollectFailed {
1335        pos: Vec3<i32>,
1336        reason: CollectFailedReason,
1337    },
1338    EntityCollectFailed {
1339        entity: Uid,
1340        reason: CollectFailedReason,
1341    },
1342    Possession,
1343    Debug,
1344    Craft,
1345}
1346
1347impl Default for InventoryUpdateEvent {
1348    fn default() -> Self { Self::Init }
1349}
1350
1351#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1352pub struct InventoryUpdate {
1353    events: Vec<InventoryUpdateEvent>,
1354}
1355
1356impl InventoryUpdate {
1357    pub fn new(event: InventoryUpdateEvent) -> Self {
1358        Self {
1359            events: vec![event],
1360        }
1361    }
1362
1363    pub fn push(&mut self, event: InventoryUpdateEvent) { self.events.push(event); }
1364
1365    pub fn take_events(&mut self) -> Vec<InventoryUpdateEvent> { std::mem::take(&mut self.events) }
1366}
1367
1368impl Component for InventoryUpdate {
1369    // TODO: This could probabably be `DenseVecStorage` (except we call clear on
1370    // this and that essentially leaks for `DenseVecStorage` atm afaict).
1371    type Storage = specs::VecStorage<Self>;
1372}