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