all_is_cubes/inv/
inventory.rs

1//! [`Inventory`] for storing items.
2
3use alloc::borrow::Cow;
4use alloc::boxed::Box;
5use alloc::collections::BTreeMap;
6use alloc::sync::Arc;
7use alloc::vec::Vec;
8use core::fmt;
9use core::num::NonZeroU16;
10
11use crate::block::Block;
12use crate::character::{Character, CharacterTransaction, Cursor};
13use crate::inv::{Icons, Ix, Tool, ToolError, ToolInput};
14use crate::linking::BlockProvider;
15use crate::transaction::{CommitError, Merge, Transaction};
16use crate::universe::{Handle, HandleVisitor, ReadTicket, UniverseTransaction, VisitHandles};
17
18/// A collection of [`Tool`]s (items).
19///
20/// Note that unlike many other game objects in `all_is_cubes`, an `Inventory` does not
21/// deliver change notifications. Instead, this is the responsibility of the `Inventory`'s
22/// owner; its operations produce [`InventoryChange`]s (sometimes indirectly via
23/// [`InventoryTransaction`]'s output) which the owner is responsible for forwarding
24/// appropriately. This design choice allows an [`Inventory`] to be placed inside
25/// other objects directly rather than via [`Handle`].
26///
27#[doc = include_str!("../save/serde-warning.md")]
28#[derive(Clone, Debug, Eq, Hash, PartialEq)]
29#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
30#[non_exhaustive]
31pub struct Inventory {
32    // Design note: This is a boxed slice to keep the size of the type small,
33    // and because inventories have sizes determined by game mechanics separately from their
34    // contents, so they do not need to resize to accomodate their contents expanding.
35    pub(crate) slots: Box<[Slot]>,
36}
37
38impl Inventory {
39    /// Construct an [`Inventory`] with the specified number of slots.
40    ///
41    /// Ordinary user actions cannot change the number of slots.
42    pub fn new(size: Ix) -> Self {
43        Inventory {
44            slots: vec![Slot::Empty; size.into()].into_boxed_slice(),
45        }
46    }
47
48    /// Construct an [`Inventory`] with the given slots.
49    ///
50    /// Ordinary user actions cannot change the number of slots.
51    #[track_caller]
52    pub fn from_slots(items: impl Into<Box<[Slot]>>) -> Self {
53        let slots = items.into();
54        Ix::try_from(slots.len())
55            .expect("input has more slots than an inventory is allowed to have");
56        Inventory { slots }
57    }
58
59    /// Returns whether all slots in this inventory are empty.
60    pub fn is_empty(&self) -> bool {
61        self.slots.iter().all(|slot| matches!(slot, Slot::Empty))
62    }
63
64    /// Returns a view of all slots in this inventory.
65    pub fn slots(&self) -> &[Slot] {
66        &self.slots
67    }
68
69    /// Returns the number of slots in the inventory.
70    ///
71    /// This is equal in value to `self.slots().len()`.
72    pub fn size(&self) -> Ix {
73        // cannot overflow because we checked on construction
74        self.slots.len() as Ix
75    }
76
77    /// Returns the contents of the slot in this inventory with the given index.
78    ///
79    /// Returns `None` if, and only if, `slot_index >= self.size()`.
80    pub fn get(&self, slot_index: Ix) -> Option<&Slot> {
81        self.slots.get(usize::from(slot_index))
82    }
83
84    /// Use a tool stored in this inventory.
85    ///
86    /// Returns a [`inv::InventoryTransaction`] for the change to the inventory, if any,
87    /// and a [`UniverseTransaction`] for the outside effects of the tool.
88    /// The caller should commit both or neither.
89    pub(crate) fn use_tool_it(
90        &self,
91        read_ticket: ReadTicket<'_>,
92        cursor: Option<&Cursor>,
93        character: Option<Handle<Character>>,
94        slot_index: Ix,
95    ) -> Result<(InventoryTransaction, UniverseTransaction), ToolError> {
96        let original_slot = self.get(slot_index);
97        match original_slot {
98            None | Some(Slot::Empty) => Err(ToolError::NoTool),
99            Some(Slot::Stack(count, original_tool)) => {
100                let input = ToolInput {
101                    read_ticket,
102                    cursor: cursor.cloned(),
103                    character,
104                };
105                let (new_tool, effect_transaction) = original_tool.clone().use_tool(&input)?;
106
107                // TODO: This is way too long. Inventory-stacking logic should be in InventoryTransaction, probably?
108                let tool_transaction = match (count, new_tool) {
109                    (_, None) => {
110                        // Tool deletes itself.
111                        Some(InventoryTransaction::replace(
112                            slot_index,
113                            original_slot.unwrap().clone(),
114                            Slot::stack(count.get() - 1, original_tool.clone()),
115                        ))
116                    }
117                    (_, Some(new_tool)) if new_tool == *original_tool => {
118                        // Tool is unaffected.
119                        None
120                    }
121                    (&Slot::COUNT_ONE, Some(new_tool)) => {
122                        // Tool modifies itself and is not stacked.
123                        Some(InventoryTransaction::replace(
124                            slot_index,
125                            original_slot.unwrap().clone(),
126                            new_tool.into(),
127                        ))
128                    }
129                    (count_greater_than_one, Some(new_tool)) => {
130                        // Tool modifies itself and is in a stack, so we have to unstack the new tool.
131                        // TODO: In some cases it might make sense to put the stack aside and keep the modified tool.
132                        Some(
133                            InventoryTransaction::replace(
134                                slot_index,
135                                original_slot.unwrap().clone(),
136                                Slot::stack(
137                                    count_greater_than_one.get() - 1,
138                                    original_tool.clone(),
139                                ),
140                            )
141                            .merge(InventoryTransaction::insert([new_tool]))
142                            .unwrap(),
143                        )
144                    }
145                };
146
147                Ok((tool_transaction.unwrap_or_default(), effect_transaction))
148            }
149        }
150    }
151
152    /// Use a tool stored in this inventory.
153    ///
154    /// `character` must be the character containing the inventory. TODO: Bad API
155    pub fn use_tool(
156        &self,
157        read_ticket: ReadTicket<'_>,
158        cursor: Option<&Cursor>,
159        character: Handle<Character>,
160        slot_index: Ix,
161    ) -> Result<UniverseTransaction, ToolError> {
162        let (tool_transaction, effect_transaction) =
163            self.use_tool_it(read_ticket, cursor, Some(character.clone()), slot_index)?;
164
165        if tool_transaction.is_empty() {
166            Ok(effect_transaction)
167        } else {
168            effect_transaction
169                .merge(CharacterTransaction::inventory(tool_transaction).bind(character))
170                .map_err(|_| ToolError::Internal("failed to merge tool self-update".into()))
171        }
172    }
173
174    /// Returns the total count of the given item in this inventory.
175    ///
176    /// Note on numeric range: this can overflow if the inventory has over 65537 slots.
177    /// Let's not do that.
178    ///
179    /// TODO: Added for tests; is this generally useful?
180    #[cfg(test)]
181    pub(crate) fn count_of(&self, item: &Tool) -> u32 {
182        self.slots.iter().map(|slot| u32::from(slot.count_of(item))).sum::<u32>()
183    }
184}
185
186impl VisitHandles for Inventory {
187    fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
188        let Self { slots } = self;
189        slots.visit_handles(visitor);
190    }
191}
192
193/// The direct child of [`Inventory`]; a container for any number of identical [`Tool`]s.
194#[derive(Clone, Eq, Hash, PartialEq)]
195#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
196#[non_exhaustive]
197pub enum Slot {
198    /// Slot contains nothing.
199    Empty,
200    /// Slot contains one or more of the given [`Tool`].
201    Stack(NonZeroU16, Tool),
202}
203
204impl Slot {
205    const COUNT_ONE: NonZeroU16 = NonZeroU16::MIN;
206
207    /// Construct a [`Slot`] containing `count` copies of `tool`.
208    ///
209    /// If `count` is zero, the `tool` will be ignored.
210    pub fn stack(count: u16, tool: Tool) -> Self {
211        match NonZeroU16::new(count) {
212            Some(count) => Self::Stack(count, tool),
213            None => Self::Empty,
214        }
215    }
216
217    /// Temporary const version of `<Slot as From<Tool>>::from`.
218    #[doc(hidden)]
219    pub const fn one(tool: Tool) -> Self {
220        Self::Stack(Self::COUNT_ONE, tool)
221    }
222
223    /// Returns the icon to use for this tool in the user interface.
224    ///
225    /// Note that this is _not_ the same as the block that a [`Tool::Block`] places.
226    pub fn icon<'a>(&'a self, predefined: &'a BlockProvider<Icons>) -> Cow<'a, Block> {
227        match self {
228            Slot::Empty => Cow::Borrowed(&predefined[Icons::EmptySlot]),
229            Slot::Stack(_, tool) => tool.icon(predefined),
230        }
231    }
232
233    /// Kludge restricted version of `icon()` to get inventory-in-a-block rendering working at all.
234    /// TODO(inventory): <https://github.com/kpreid/all-is-cubes/issues/480>
235    pub(crate) fn icon_only_if_intrinsic(&self) -> Option<&Block> {
236        match self {
237            Slot::Empty => None,
238            Slot::Stack(_, tool) => tool.icon_only_if_intrinsic(),
239        }
240    }
241
242    /// Returns the count of items in this slot.
243    pub fn count(&self) -> u16 {
244        match self {
245            Slot::Empty => 0,
246            Slot::Stack(count, _) => count.get(),
247        }
248    }
249
250    /// If the given tool is in this slot, return the count thereof.
251    ///
252    /// TODO: Added for tests; is this generally useful?
253    #[cfg(test)]
254    pub(crate) fn count_of(&self, item: &Tool) -> u16 {
255        match self {
256            Slot::Stack(count, slot_item) if slot_item == item => count.get(),
257            Slot::Stack(_, _) => 0,
258            Slot::Empty => 0,
259        }
260    }
261
262    /// Moves as many items as possible from `self` to `destination` while obeying item
263    /// stacking rules.
264    ///
265    /// Does nothing if `self` and `destination` contain different items.
266    ///
267    /// Returns whether anything was moved.
268    fn unload_to(&mut self, destination: &mut Self) -> bool {
269        // First, handle the simple cases, or decide how many to move.
270        // This has to be multiple passes to satisfy the borrow checker.
271        let count_to_move = match (&mut *self, &mut *destination) {
272            (Slot::Empty, _) => {
273                // Source is empty; nothing to do.
274                return false;
275            }
276            (source @ Slot::Stack(_, _), destination @ Slot::Empty) => {
277                // Destination is empty (and source isn't); just swap.
278                core::mem::swap(source, destination);
279                return true;
280            }
281            (Slot::Stack(s_count, source_item), Slot::Stack(d_count, destination_item)) => {
282                if source_item == destination_item {
283                    // Stacks of identical items; figure out how much to move.
284                    let max_stack = destination_item.stack_limit().get();
285                    let count_to_move = s_count.get().min(max_stack.saturating_sub(d_count.get()));
286                    if count_to_move == 0 {
287                        return false;
288                    } else if count_to_move < s_count.get() {
289                        // The source stack is not completely transferred; update counts.
290                        *s_count = NonZeroU16::new(s_count.get() - count_to_move).unwrap();
291                        *d_count = NonZeroU16::new(d_count.get() + count_to_move).unwrap();
292                        return true;
293                    } else {
294                        // The source stack is completely transferred; exit this match so that we
295                        // can reassign *self.
296                        count_to_move
297                    }
298                } else {
299                    // Stacks of different items.
300                    return false;
301                }
302            }
303        };
304        debug_assert_eq!(count_to_move, self.count());
305        if let Slot::Stack(d_count, _) = destination {
306            *self = Slot::Empty;
307            *d_count = NonZeroU16::new(d_count.get() + count_to_move).unwrap();
308        } else {
309            unreachable!();
310        }
311        true
312    }
313}
314
315impl fmt::Debug for Slot {
316    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317        match self {
318            Self::Empty => write!(f, "Empty"),
319            Self::Stack(count, tool) => {
320                write!(f, "{count} × ")?;
321                tool.fmt(f) // pass through formatter options
322            }
323        }
324    }
325}
326
327impl From<Tool> for Slot {
328    fn from(tool: Tool) -> Self {
329        Self::Stack(Self::COUNT_ONE, tool)
330    }
331}
332
333impl From<Option<Tool>> for Slot {
334    fn from(tool: Option<Tool>) -> Self {
335        match tool {
336            Some(tool) => Self::Stack(Self::COUNT_ONE, tool),
337            None => Self::Empty,
338        }
339    }
340}
341
342impl VisitHandles for Slot {
343    fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
344        match self {
345            Slot::Empty => {}
346            Slot::Stack(_count, tool) => tool.visit_handles(visitor),
347        }
348    }
349}
350
351/// Specifies a limit on the number of a particular item that should be combined in a
352/// single [`Slot`].
353///
354/// Each value of this enum is currently equivalent to a particular number, but (TODO:)
355/// in the future, it may be possible for inventories or universes to specify a normal
356/// stack size and specific deviations from it.
357#[non_exhaustive]
358#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
359#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
360pub(crate) enum StackLimit {
361    One,
362    Standard,
363}
364
365impl StackLimit {
366    /// TODO: This is not public because we don't know what environment parameters it
367    /// should need yet.
368    pub(crate) fn get(self) -> u16 {
369        match self {
370            StackLimit::One => 1,
371            // TODO: This should be a per-universe (at least) configuration.
372            StackLimit::Standard => 100,
373        }
374    }
375}
376
377/// Transaction type for [`Inventory`].
378///
379/// The output type is the change notification which should be passed on after commit,
380/// if any change is made.
381#[derive(Clone, Debug, Default, PartialEq)]
382#[expect(clippy::derive_partial_eq_without_eq)]
383#[must_use]
384pub struct InventoryTransaction {
385    replace: BTreeMap<Ix, (Slot, Slot)>,
386    insert: Vec<Slot>,
387}
388
389impl InventoryTransaction {
390    /// Transaction to insert items/stacks into an inventory, which will fail if there is
391    /// not sufficient space.
392    pub fn insert<S: Into<Slot>, I: IntoIterator<Item = S>>(stacks: I) -> Self {
393        // TODO: Should we coalesce identical insertions? Or leave that for when the
394        // transaction is executed?
395        Self {
396            replace: BTreeMap::default(),
397            insert: stacks
398                .into_iter()
399                .map(|s| -> Slot { s.into() })
400                .filter(|s| s.count() > 0)
401                .collect(),
402        }
403    }
404
405    /// Transaction to replace the contents of an existing slot in an inventory, which
406    /// will fail if the existing slot is not as expected.
407    ///
408    /// TODO: Right now, this requires an exact match. In the future, we should be able
409    /// to compose multiple modifications like "add 1 item to stack" ×2 into "add 2 items".
410    pub fn replace(slot: Ix, old: Slot, new: Slot) -> Self {
411        let mut replace = BTreeMap::new();
412        replace.insert(slot, (old, new));
413        InventoryTransaction {
414            replace,
415            insert: vec![],
416        }
417    }
418
419    /// Returns whether this transaction does nothing and checks nothing.
420    ///
421    /// # Example
422    ///
423    /// ```
424    /// use all_is_cubes::inv;
425    ///
426    /// assert!(inv::InventoryTransaction::default().is_empty());
427    ///
428    /// assert!(!inv::InventoryTransaction::insert([inv::Tool::Activate]).is_empty());
429    /// ```
430    pub fn is_empty(&self) -> bool {
431        let Self { replace, insert } = self;
432        replace.is_empty() && insert.is_empty()
433    }
434}
435
436impl Transaction for InventoryTransaction {
437    type Target = Inventory;
438    type Context<'a> = ();
439    type CommitCheck = Option<InventoryCheck>;
440    type Output = InventoryChange;
441    type Mismatch = InventoryMismatch;
442
443    fn check(
444        &self,
445        inventory: &Inventory,
446        (): Self::Context<'_>,
447    ) -> Result<Self::CommitCheck, Self::Mismatch> {
448        // Don't do the expensive copy if we have one already
449        if self.replace.is_empty() && self.insert.is_empty() {
450            return Ok(None);
451        }
452
453        // The simplest bulletproof algorithm to ensure we're stacking everything right
454        // and not overflowing is to build the entire new inventory. The disadvantage of
455        // this strategy is, of course, that we're cloning the entire inventory. If that
456        // proves to be a performance problem, we can improve things by adding per-slot
457        // copy-on-write or something like that.
458
459        let mut slots = inventory.slots.clone();
460        let mut changed = Vec::new();
461
462        // Check and apply .replace, explicit slot replacements
463        for (&index, (old, new)) in self.replace.iter() {
464            match slots.get_mut(usize::from(index)) {
465                None => {
466                    return Err(InventoryMismatch::OutOfBounds);
467                }
468                Some(actual_old) if actual_old != old => {
469                    return Err(InventoryMismatch::UnexpectedSlot(index));
470                }
471                Some(slot) => {
472                    *slot = new.clone();
473                    changed.push(index);
474                }
475            }
476        }
477
478        // Find locations for .insert items
479        for new_stack in self.insert.iter() {
480            let mut new_stack = new_stack.clone();
481            for (slot, index) in slots.iter_mut().zip(0..) {
482                if new_stack == Slot::Empty {
483                    break;
484                }
485                if new_stack.unload_to(slot) {
486                    changed.push(index);
487                }
488            }
489            if new_stack != Slot::Empty {
490                return Err(InventoryMismatch::Full);
491            }
492        }
493
494        Ok(Some(InventoryCheck {
495            new: slots,
496            change: InventoryChange {
497                slots: changed.into(),
498            },
499        }))
500    }
501
502    fn commit(
503        self,
504        inventory: &mut Inventory,
505        check: Self::CommitCheck,
506        outputs: &mut dyn FnMut(Self::Output),
507    ) -> Result<(), CommitError> {
508        if let Some(InventoryCheck { new, change }) = check {
509            assert_eq!(new.len(), inventory.slots.len());
510            inventory.slots = new;
511            outputs(change);
512        }
513        Ok(())
514    }
515}
516
517impl Merge for InventoryTransaction {
518    type MergeCheck = ();
519    type Conflict = InventoryConflict;
520
521    fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
522        if let Some(&slot) = self.replace.keys().find(|slot| other.replace.contains_key(slot)) {
523            return Err(InventoryConflict::ReplaceSameSlot { slot });
524        }
525        Ok(())
526    }
527
528    fn commit_merge(&mut self, other: Self, (): Self::MergeCheck) {
529        self.replace.extend(other.replace);
530        self.insert.extend(other.insert);
531    }
532}
533
534/// Implementation type for [`InventoryTransaction::CommitCheck`].
535#[derive(Debug)]
536pub struct InventoryCheck {
537    new: Box<[Slot]>,
538    change: InventoryChange,
539}
540
541/// Transaction precondition error type for an [`InventoryTransaction`].
542#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
543#[non_exhaustive]
544pub enum InventoryMismatch {
545    /// insufficient empty slots
546    Full,
547
548    /// slot out of bounds
549    OutOfBounds,
550
551    /// contents of slot {0} not as expected
552    UnexpectedSlot(Ix),
553}
554
555/// Transaction conflict error type for an [`InventoryTransaction`].
556#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
557#[non_exhaustive]
558pub enum InventoryConflict {
559    /// Tried to replace the same inventory slot.
560    #[displaydoc("tried to replace the same inventory slot, {slot}, twice")]
561    #[non_exhaustive]
562    #[allow(missing_docs)]
563    ReplaceSameSlot { slot: Ix },
564}
565
566impl core::error::Error for InventoryMismatch {}
567impl core::error::Error for InventoryConflict {}
568
569/// Description of a change to an [`Inventory`] for use in listeners.
570#[derive(Clone, Debug, Eq, Hash, PartialEq)]
571#[non_exhaustive]
572pub struct InventoryChange {
573    /// Which slots of the inventory have been changed.
574    pub slots: Arc<[Ix]>,
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use crate::block;
581    use crate::content::make_some_blocks;
582    use crate::math::Rgba;
583    use crate::transaction::TransactionTester;
584    use itertools::Itertools;
585    use pretty_assertions::assert_eq;
586
587    // TODO: test for Inventory::use_tool
588
589    #[test]
590    fn txn_identity_no_notification() {
591        InventoryTransaction::default()
592            .execute(
593                &mut Inventory::from_slots(vec![Slot::Empty]),
594                (),
595                &mut |_| unreachable!("shouldn't notify"),
596            )
597            .unwrap()
598    }
599
600    #[test]
601    fn txn_insert_empty_list() {
602        let list: [Slot; 0] = [];
603        assert_eq!(
604            InventoryTransaction::insert(list),
605            InventoryTransaction::default()
606        );
607    }
608
609    #[test]
610    fn txn_insert_filtered_empty() {
611        assert_eq!(
612            InventoryTransaction::insert([Slot::Empty, Slot::Empty]),
613            InventoryTransaction::default()
614        );
615    }
616
617    #[test]
618    fn txn_insert_success() {
619        let occupied_slot: Slot = Tool::CopyFromSpace.into();
620        let mut inventory = Inventory::from_slots(vec![
621            occupied_slot.clone(),
622            occupied_slot.clone(),
623            Slot::Empty,
624            occupied_slot,
625            Slot::Empty,
626        ]);
627        let new_item = Tool::InfiniteBlocks(Rgba::WHITE.into());
628        assert_eq!(inventory.slots[2], Slot::Empty);
629
630        let mut outputs = Vec::new();
631        InventoryTransaction::insert([new_item.clone()])
632            .execute(&mut inventory, (), &mut |x| outputs.push(x))
633            .unwrap();
634
635        assert_eq!(
636            outputs,
637            vec![InventoryChange {
638                slots: Arc::new([2])
639            }]
640        );
641        assert_eq!(inventory.slots[2], new_item.into());
642    }
643
644    #[test]
645    fn txn_insert_no_space() {
646        let contents = vec![
647            Slot::from(Tool::CopyFromSpace),
648            Slot::from(Tool::CopyFromSpace),
649        ]
650        .into_boxed_slice();
651        let inventory = Inventory::from_slots(contents.clone());
652        let new_item = Tool::InfiniteBlocks(Rgba::WHITE.into());
653
654        assert_eq!(inventory.slots, contents);
655        InventoryTransaction::insert([new_item.clone()])
656            .check(&inventory, ())
657            .expect_err("should have failed");
658        assert_eq!(inventory.slots, contents);
659    }
660
661    #[test]
662    fn txn_insert_into_existing_stack() {
663        // TODO: make_some_tools to simplify this?
664        let [this, other] = make_some_blocks();
665        let this = Tool::Block(this);
666        let other = Tool::Block(other);
667        let mut inventory = Inventory::from_slots(vec![
668            Slot::stack(10, other.clone()),
669            Slot::stack(10, this.clone()),
670            Slot::stack(10, other.clone()),
671            Slot::stack(10, this.clone()),
672            Slot::Empty,
673        ]);
674        InventoryTransaction::insert([this.clone()])
675            .execute(&mut inventory, (), &mut drop)
676            .unwrap();
677        assert_eq!(
678            inventory.slots,
679            vec![
680                Slot::stack(10, other.clone()),
681                Slot::stack(11, this.clone()),
682                Slot::stack(10, other.clone()),
683                Slot::stack(10, this.clone()),
684                Slot::Empty,
685            ]
686            .into_boxed_slice()
687        );
688    }
689
690    #[test]
691    fn txn_systematic() {
692        let old_item = Tool::InfiniteBlocks(block::from_color!(1.0, 0.0, 0.0));
693        let new_item_1 = Tool::InfiniteBlocks(block::from_color!(0.0, 1.0, 0.0));
694        let new_item_2 = Tool::InfiniteBlocks(block::from_color!(0.0, 0.0, 1.0));
695
696        // TODO: Add tests of stack modification, emptying, merging
697
698        TransactionTester::new()
699            .transaction(
700                InventoryTransaction::insert([new_item_1.clone()]),
701                |before, after| {
702                    if after.count_of(&new_item_1) <= before.count_of(&new_item_1) {
703                        return Err("missing added new_item_1".into());
704                    }
705                    Ok(())
706                },
707            )
708            .transaction(
709                InventoryTransaction::replace(
710                    0,
711                    old_item.clone().into(),
712                    new_item_1.clone().into(),
713                ),
714                |_, after| {
715                    if after.slots[0].count_of(&old_item) != 0 {
716                        return Err("did not replace old_item".into());
717                    }
718                    if after.slots[0].count_of(&new_item_1) == 0 {
719                        return Err("did not insert new_item_1".into());
720                    }
721                    Ok(())
722                },
723            )
724            .transaction(
725                // This one conflicts with the above one
726                InventoryTransaction::replace(
727                    0,
728                    old_item.clone().into(),
729                    new_item_2.clone().into(),
730                ),
731                |_, after| {
732                    if after.slots[0].count_of(&old_item) != 0 {
733                        return Err("did not replace old_item".into());
734                    }
735                    if after.slots[0].count_of(&new_item_2) == 0 {
736                        return Err("did not insert new_item_2".into());
737                    }
738                    Ok(())
739                },
740            )
741            .target(|| Inventory::from_slots(vec![]))
742            .target(|| Inventory::from_slots(vec![Slot::Empty]))
743            .target(|| Inventory::from_slots(vec![Slot::Empty; 10]))
744            .target(|| Inventory::from_slots(vec![Slot::from(old_item.clone()), Slot::Empty]))
745            .test(());
746    }
747
748    #[test]
749    fn slot_unload_systematic() {
750        let [block1, block2] = make_some_blocks();
751        let tools = [
752            Tool::Block(block1),
753            Tool::Block(block2),
754            Tool::Activate, // not stackable
755        ];
756        const MAX: u16 = u16::MAX;
757        let gen_slots = move || {
758            [
759                0,
760                1,
761                2,
762                3,
763                10,
764                MAX / 2,
765                MAX / 2 + 1,
766                MAX - 10,
767                MAX - 2,
768                MAX - 1,
769                MAX,
770            ]
771            .into_iter()
772            .cartesian_product(tools.clone())
773            .map(|(count, item)| Slot::stack(count, item))
774        };
775        for slot1_in in gen_slots() {
776            for slot2_in in gen_slots() {
777                let different = matches!((&slot1_in, &slot2_in), (Slot::Stack(_, i1), Slot::Stack(_, i2)) if i1 != i2);
778
779                let mut slot1_out = slot1_in.clone();
780                let mut slot2_out = slot2_in.clone();
781                slot1_out.unload_to(&mut slot2_out);
782
783                assert_eq!(
784                    u64::from(slot1_in.count()) + u64::from(slot2_in.count()),
785                    u64::from(slot1_out.count()) + u64::from(slot2_out.count()),
786                    "not conservative"
787                );
788                if different {
789                    assert_eq!(
790                        (&slot1_in, &slot2_in),
791                        (&slot1_out, &slot2_out),
792                        "combined different items"
793                    );
794                }
795            }
796        }
797    }
798}