use std::borrow::Cow;
use std::collections::BTreeMap;
use std::num::NonZeroU16;
use std::sync::Arc;
use crate::block::Block;
use crate::character::{Character, CharacterTransaction, Cursor};
use crate::inv::{Icons, Tool, ToolError, ToolInput};
use crate::linking::BlockProvider;
use crate::transaction::{
CommitError, Merge, PreconditionFailed, Transaction, TransactionConflict,
};
use crate::universe::{RefVisitor, URef, UniverseTransaction, VisitRefs};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub struct Inventory {
pub slots: Vec<Slot>,
}
impl Inventory {
pub fn new(size: usize) -> Self {
Inventory {
slots: vec![Slot::Empty; size],
}
}
pub(crate) fn from_slots(mut items: Vec<Slot>) -> Self {
items.shrink_to_fit();
Inventory { slots: items }
}
pub fn use_tool(
&self,
cursor: Option<&Cursor>,
character: URef<Character>,
slot_index: usize,
) -> Result<UniverseTransaction, ToolError> {
let original_slot = self.slots.get(slot_index);
match original_slot {
None | Some(Slot::Empty) => Err(ToolError::NoTool),
Some(Slot::Stack(count, original_tool)) => {
let input = ToolInput {
cursor: cursor.cloned(),
character: Some(character.clone()),
};
let (new_tool, transaction) = original_tool.clone().use_tool(&input)?;
let tool_transaction = match (count, new_tool) {
(_, None) => {
Some(InventoryTransaction::replace(
slot_index,
original_slot.unwrap().clone(),
Slot::stack(count.get() - 1, original_tool.clone()),
))
}
(_, Some(new_tool)) if new_tool == *original_tool => {
None
}
(&Slot::COUNT_ONE, Some(new_tool)) => {
Some(InventoryTransaction::replace(
slot_index,
original_slot.unwrap().clone(),
new_tool.into(),
))
}
(count_greater_than_one, Some(new_tool)) => {
Some(
InventoryTransaction::replace(
slot_index,
original_slot.unwrap().clone(),
Slot::stack(
count_greater_than_one.get() - 1,
original_tool.clone(),
),
)
.merge(InventoryTransaction::insert(new_tool))
.unwrap(),
)
}
};
Ok(match tool_transaction {
Some(tool_transaction) => transaction
.merge(CharacterTransaction::inventory(tool_transaction).bind(character))
.expect("failed to merge tool self-update"),
None => transaction,
})
}
}
}
#[cfg(test)]
pub(crate) fn count_of(&self, item: &Tool) -> u32 {
self.slots
.iter()
.map(|slot| u32::from(slot.count_of(item)))
.sum::<u32>()
}
}
impl VisitRefs for Inventory {
fn visit_refs(&self, visitor: &mut dyn RefVisitor) {
let Self { slots } = self;
slots.visit_refs(visitor);
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Slot {
Empty,
Stack(NonZeroU16, Tool),
}
impl Slot {
const COUNT_ONE: NonZeroU16 = {
unsafe { NonZeroU16::new_unchecked(1) }
};
pub fn stack(count: u16, tool: Tool) -> Self {
match NonZeroU16::new(count) {
Some(count) => Self::Stack(count, tool),
None => Self::Empty,
}
}
#[doc(hidden)]
pub const fn one(tool: Tool) -> Self {
Self::Stack(Self::COUNT_ONE, tool)
}
pub fn icon<'a>(&'a self, predefined: &'a BlockProvider<Icons>) -> Cow<'a, Block> {
match self {
Slot::Empty => Cow::Borrowed(&predefined[Icons::EmptySlot]),
Slot::Stack(_, tool) => tool.icon(predefined),
}
}
pub fn count(&self) -> u16 {
match self {
Slot::Empty => 0,
Slot::Stack(count, _) => count.get(),
}
}
#[cfg(test)]
pub(crate) fn count_of(&self, item: &Tool) -> u16 {
match self {
Slot::Stack(count, slot_item) if slot_item == item => count.get(),
Slot::Stack(_, _) => 0,
Slot::Empty => 0,
}
}
fn unload_to(&mut self, destination: &mut Self) -> bool {
let count_to_move = match (&mut *self, &mut *destination) {
(Slot::Empty, _) => {
return false;
}
(source @ Slot::Stack(_, _), destination @ Slot::Empty) => {
std::mem::swap(source, destination);
return true;
}
(Slot::Stack(s_count, source_item), Slot::Stack(d_count, destination_item)) => {
if source_item == destination_item {
let max_stack = destination_item.stack_limit().get();
let count_to_move = s_count.get().min(max_stack.saturating_sub(d_count.get()));
if count_to_move == 0 {
return false;
} else if count_to_move < s_count.get() {
*s_count = NonZeroU16::new(s_count.get() - count_to_move).unwrap();
*d_count = NonZeroU16::new(d_count.get() + count_to_move).unwrap();
return true;
} else {
count_to_move
}
} else {
return false;
}
}
};
debug_assert_eq!(count_to_move, self.count());
if let Slot::Stack(d_count, _) = destination {
*self = Slot::Empty;
*d_count = NonZeroU16::new(d_count.get() + count_to_move).unwrap();
} else {
unreachable!();
}
true
}
}
impl From<Tool> for Slot {
fn from(tool: Tool) -> Self {
Self::Stack(Self::COUNT_ONE, tool)
}
}
impl From<Option<Tool>> for Slot {
fn from(tool: Option<Tool>) -> Self {
match tool {
Some(tool) => Self::Stack(Self::COUNT_ONE, tool),
None => Self::Empty,
}
}
}
impl VisitRefs for Slot {
fn visit_refs(&self, visitor: &mut dyn RefVisitor) {
match self {
Slot::Empty => {}
Slot::Stack(_count, tool) => tool.visit_refs(visitor),
}
}
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub(crate) enum StackLimit {
One,
Standard,
}
impl StackLimit {
pub(crate) fn get(self) -> u16 {
match self {
StackLimit::One => 1,
StackLimit::Standard => 100,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[must_use]
pub struct InventoryTransaction {
replace: BTreeMap<usize, (Slot, Slot)>,
insert: Vec<Slot>,
}
impl InventoryTransaction {
pub fn insert(stack: impl Into<Slot>) -> Self {
let stack = stack.into();
if matches!(stack, Slot::Empty) {
return Self::default();
}
Self {
replace: BTreeMap::default(),
insert: vec![stack],
}
}
pub fn replace(slot: usize, old: Slot, new: Slot) -> Self {
let mut replace = BTreeMap::new();
replace.insert(slot, (old, new));
InventoryTransaction {
replace,
insert: vec![],
}
}
}
impl Transaction<Inventory> for InventoryTransaction {
type CommitCheck = Option<InventoryCheck>;
type Output = Option<InventoryChange>;
fn check(&self, inventory: &Inventory) -> Result<Self::CommitCheck, PreconditionFailed> {
if self.replace.is_empty() && self.insert.is_empty() {
return Ok(None);
}
let mut slots = inventory.slots.clone();
let mut changed = Vec::new();
for (&index, (old, new)) in self.replace.iter() {
match slots.get_mut(index) {
None => {
return Err(PreconditionFailed {
location: "Inventory",
problem: "slot out of bounds",
});
}
Some(actual_old) if actual_old != old => {
return Err(PreconditionFailed {
location: "Inventory",
problem: "old slot not as expected",
}); }
Some(slot) => {
*slot = new.clone();
changed.push(index);
}
}
}
for new_stack in self.insert.iter() {
let mut new_stack = new_stack.clone();
for (index, slot) in slots.iter_mut().enumerate() {
if new_stack == Slot::Empty {
break;
}
if new_stack.unload_to(slot) {
changed.push(index);
}
}
if new_stack != Slot::Empty {
return Err(PreconditionFailed {
location: "Inventory",
problem: "insufficient empty slots",
});
}
}
Ok(Some(InventoryCheck {
new: slots,
change: InventoryChange {
slots: changed.into(),
},
}))
}
fn commit(
&self,
inventory: &mut Inventory,
check: Self::CommitCheck,
) -> Result<Self::Output, CommitError> {
if let Some(InventoryCheck { new, change }) = check {
assert_eq!(new.len(), inventory.slots.len());
inventory.slots = new;
Ok(Some(change))
} else {
Ok(None)
}
}
}
impl Merge for InventoryTransaction {
type MergeCheck = ();
fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, TransactionConflict> {
if self
.replace
.keys()
.any(|slot| other.replace.contains_key(slot))
{
return Err(TransactionConflict {});
}
Ok(())
}
fn commit_merge(mut self, other: Self, (): Self::MergeCheck) -> Self {
self.replace.extend(other.replace);
self.insert.extend(other.insert);
self
}
}
#[derive(Debug)]
pub struct InventoryCheck {
new: Vec<Slot>,
change: InventoryChange,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub struct InventoryChange {
pub slots: Arc<[usize]>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::content::make_some_blocks;
use crate::math::Rgba;
use crate::transaction::TransactionTester;
use itertools::Itertools;
use pretty_assertions::assert_eq;
#[test]
fn txn_identity_no_notification() {
assert_eq!(
InventoryTransaction::default()
.execute(&mut Inventory::from_slots(vec![Slot::Empty]))
.unwrap(),
None
);
}
#[test]
fn txn_insert_success() {
let occupied_slot: Slot = Tool::CopyFromSpace.into();
let mut inventory = Inventory::from_slots(vec![
occupied_slot.clone(),
occupied_slot.clone(),
Slot::Empty,
occupied_slot,
Slot::Empty,
]);
let new_item = Tool::InfiniteBlocks(Rgba::WHITE.into());
assert_eq!(inventory.slots[2], Slot::Empty);
assert_eq!(
InventoryTransaction::insert(new_item.clone())
.execute(&mut inventory)
.unwrap(),
Some(InventoryChange {
slots: Arc::new([2])
})
);
assert_eq!(inventory.slots[2], new_item.into());
}
#[test]
fn txn_insert_no_space() {
let contents = vec![
Slot::from(Tool::CopyFromSpace),
Slot::from(Tool::CopyFromSpace),
];
let inventory = Inventory::from_slots(contents.clone());
let new_item = Tool::InfiniteBlocks(Rgba::WHITE.into());
assert_eq!(inventory.slots, contents);
InventoryTransaction::insert(new_item.clone())
.check(&inventory)
.expect_err("should have failed");
assert_eq!(inventory.slots, contents);
}
#[test]
fn txn_insert_into_existing_stack() {
let [this, other] = make_some_blocks();
let this = Tool::Block(this);
let other = Tool::Block(other);
let mut inventory = Inventory::from_slots(vec![
Slot::stack(10, other.clone()),
Slot::stack(10, this.clone()),
Slot::stack(10, other.clone()),
Slot::stack(10, this.clone()),
Slot::Empty,
]);
InventoryTransaction::insert(this.clone())
.execute(&mut inventory)
.unwrap();
assert_eq!(
inventory.slots,
vec![
Slot::stack(10, other.clone()),
Slot::stack(11, this.clone()),
Slot::stack(10, other.clone()),
Slot::stack(10, this.clone()),
Slot::Empty,
]
);
}
#[test]
fn txn_systematic() {
let old_item = Tool::InfiniteBlocks(Block::from(rgb_const!(1.0, 0.0, 0.0)));
let new_item_1 = Tool::InfiniteBlocks(Block::from(rgb_const!(0.0, 1.0, 0.0)));
let new_item_2 = Tool::InfiniteBlocks(Block::from(rgb_const!(0.0, 0.0, 1.0)));
TransactionTester::new()
.transaction(
InventoryTransaction::insert(new_item_1.clone()),
|before, after| {
if after.count_of(&new_item_1) <= before.count_of(&new_item_1) {
return Err("missing added new_item_1".into());
}
Ok(())
},
)
.transaction(
InventoryTransaction::replace(
0,
old_item.clone().into(),
new_item_1.clone().into(),
),
|_, after| {
if after.slots[0].count_of(&old_item) != 0 {
return Err("did not replace old_item".into());
}
if after.slots[0].count_of(&new_item_1) == 0 {
return Err("did not insert new_item_1".into());
}
Ok(())
},
)
.transaction(
InventoryTransaction::replace(
0,
old_item.clone().into(),
new_item_2.clone().into(),
),
|_, after| {
if after.slots[0].count_of(&old_item) != 0 {
return Err("did not replace old_item".into());
}
if after.slots[0].count_of(&new_item_2) == 0 {
return Err("did not insert new_item_2".into());
}
Ok(())
},
)
.target(|| Inventory::from_slots(vec![]))
.target(|| Inventory::from_slots(vec![Slot::Empty]))
.target(|| Inventory::from_slots(vec![Slot::Empty; 10]))
.target(|| Inventory::from_slots(vec![Slot::from(old_item.clone()), Slot::Empty]))
.test();
}
#[test]
fn slot_unload_systematic() {
let [block1, block2] = make_some_blocks();
let tools = [
Tool::Block(block1),
Tool::Block(block2),
Tool::Activate, ];
const MAX: u16 = u16::MAX;
let gen_slots = move || {
[
0,
1,
2,
3,
10,
MAX / 2,
MAX / 2 + 1,
MAX - 10,
MAX - 2,
MAX - 1,
MAX,
]
.into_iter()
.cartesian_product(tools.clone())
.map(|(count, item)| Slot::stack(count, item))
};
for slot1_in in gen_slots() {
for slot2_in in gen_slots() {
let different = matches!((&slot1_in, &slot2_in), (Slot::Stack(_, i1), Slot::Stack(_, i2)) if i1 != i2);
let mut slot1_out = slot1_in.clone();
let mut slot2_out = slot2_in.clone();
slot1_out.unload_to(&mut slot2_out);
assert_eq!(
u64::from(slot1_in.count()) + u64::from(slot2_in.count()),
u64::from(slot1_out.count()) + u64::from(slot2_out.count()),
"not conservative"
);
if different {
assert_eq!(
(&slot1_in, &slot2_in),
(&slot1_out, &slot2_out),
"combined different items"
);
}
}
}
}
}