1use 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#[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 pub(crate) slots: Box<[Slot]>,
36}
37
38impl Inventory {
39 pub fn new(size: Ix) -> Self {
43 Inventory {
44 slots: vec![Slot::Empty; size.into()].into_boxed_slice(),
45 }
46 }
47
48 #[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 pub fn is_empty(&self) -> bool {
61 self.slots.iter().all(|slot| matches!(slot, Slot::Empty))
62 }
63
64 pub fn slots(&self) -> &[Slot] {
66 &self.slots
67 }
68
69 pub fn size(&self) -> Ix {
73 self.slots.len() as Ix
75 }
76
77 pub fn get(&self, slot_index: Ix) -> Option<&Slot> {
81 self.slots.get(usize::from(slot_index))
82 }
83
84 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 let tool_transaction = match (count, new_tool) {
109 (_, None) => {
110 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 None
120 }
121 (&Slot::COUNT_ONE, Some(new_tool)) => {
122 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 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 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 #[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#[derive(Clone, Eq, Hash, PartialEq)]
195#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
196#[non_exhaustive]
197pub enum Slot {
198 Empty,
200 Stack(NonZeroU16, Tool),
202}
203
204impl Slot {
205 const COUNT_ONE: NonZeroU16 = NonZeroU16::MIN;
206
207 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 #[doc(hidden)]
219 pub const fn one(tool: Tool) -> Self {
220 Self::Stack(Self::COUNT_ONE, tool)
221 }
222
223 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 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 pub fn count(&self) -> u16 {
244 match self {
245 Slot::Empty => 0,
246 Slot::Stack(count, _) => count.get(),
247 }
248 }
249
250 #[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 fn unload_to(&mut self, destination: &mut Self) -> bool {
269 let count_to_move = match (&mut *self, &mut *destination) {
272 (Slot::Empty, _) => {
273 return false;
275 }
276 (source @ Slot::Stack(_, _), destination @ Slot::Empty) => {
277 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 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 *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 count_to_move
297 }
298 } else {
299 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) }
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#[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 pub(crate) fn get(self) -> u16 {
369 match self {
370 StackLimit::One => 1,
371 StackLimit::Standard => 100,
373 }
374 }
375}
376
377#[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 pub fn insert<S: Into<Slot>, I: IntoIterator<Item = S>>(stacks: I) -> Self {
393 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 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 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 if self.replace.is_empty() && self.insert.is_empty() {
450 return Ok(None);
451 }
452
453 let mut slots = inventory.slots.clone();
460 let mut changed = Vec::new();
461
462 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 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#[derive(Debug)]
536pub struct InventoryCheck {
537 new: Box<[Slot]>,
538 change: InventoryChange,
539}
540
541#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
543#[non_exhaustive]
544pub enum InventoryMismatch {
545 Full,
547
548 OutOfBounds,
550
551 UnexpectedSlot(Ix),
553}
554
555#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
557#[non_exhaustive]
558pub enum InventoryConflict {
559 #[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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
571#[non_exhaustive]
572pub struct InventoryChange {
573 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 #[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 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 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 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, ];
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}