2022-10-16 13:47:02 +11:00
|
|
|
use std::ops::Range;
|
|
|
|
|
2022-11-02 13:31:28 +11:00
|
|
|
use thiserror::Error;
|
2022-11-15 17:30:20 +11:00
|
|
|
use valence_protocol::{ItemStack, VarInt};
|
2022-11-02 13:31:28 +11:00
|
|
|
|
2022-10-16 13:47:02 +11:00
|
|
|
use crate::slab_versioned::{Key, VersionedSlab};
|
|
|
|
|
2022-11-14 01:10:42 +11:00
|
|
|
pub type SlotId = i16;
|
|
|
|
|
2022-10-16 13:47:02 +11:00
|
|
|
pub trait Inventory {
|
|
|
|
fn slot(&self, slot_id: SlotId) -> Option<&ItemStack>;
|
|
|
|
/// Sets the slot to the desired contents. Returns the previous contents of
|
|
|
|
/// the slot.
|
|
|
|
fn set_slot(&mut self, slot_id: SlotId, slot: Option<ItemStack>) -> Option<ItemStack>;
|
|
|
|
fn slot_range(&self) -> Range<SlotId>;
|
|
|
|
|
|
|
|
fn slot_count(&self) -> usize {
|
|
|
|
self.slot_range().count()
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: `entry()` style api
|
|
|
|
|
|
|
|
fn slots(&self) -> Vec<Option<&ItemStack>> {
|
|
|
|
(0..self.slot_count())
|
|
|
|
.map(|s| self.slot(s as SlotId))
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
2022-11-02 13:31:28 +11:00
|
|
|
/// Decreases the count for stack in the slot by amount. If there is not
|
|
|
|
/// enough items in the stack to perform the operation, then it will fail.
|
|
|
|
///
|
|
|
|
/// Returns `Ok` if the stack had enough items, and the operation was
|
|
|
|
/// carried out. Otherwise, it returns `Err` if `amount > stack.count()`,
|
|
|
|
/// and no changes were made to the inventory.
|
|
|
|
#[allow(clippy::unnecessary_unwrap)]
|
|
|
|
fn consume(&mut self, slot_id: SlotId, amount: impl Into<u8>) -> Result<(), InventoryError> {
|
|
|
|
let amount: u8 = amount.into();
|
|
|
|
let slot = self.slot(slot_id).cloned();
|
|
|
|
if slot.is_some() {
|
|
|
|
// Intentionally not using `if let` so stack can be moved out of the slot as mut
|
|
|
|
// to avoid another clone later.
|
|
|
|
let mut stack = slot.unwrap();
|
|
|
|
if amount > stack.count() {
|
|
|
|
return Err(InventoryError);
|
|
|
|
}
|
|
|
|
let slot = if amount == stack.count() {
|
2022-10-16 13:47:02 +11:00
|
|
|
None
|
|
|
|
} else {
|
2022-11-02 13:31:28 +11:00
|
|
|
stack.set_count(stack.count() - amount);
|
2022-10-16 13:47:02 +11:00
|
|
|
Some(stack)
|
|
|
|
};
|
2022-11-02 13:31:28 +11:00
|
|
|
|
|
|
|
self.set_slot(slot_id, slot);
|
2022-10-16 13:47:02 +11:00
|
|
|
}
|
2022-11-02 13:31:28 +11:00
|
|
|
Ok(())
|
2022-10-16 13:47:02 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) trait InventoryDirtyable {
|
|
|
|
fn mark_dirty(&mut self, dirty: bool);
|
|
|
|
fn is_dirty(&self) -> bool;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Represents a player's Inventory.
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct PlayerInventory {
|
|
|
|
pub(crate) slots: Box<[Option<ItemStack>; 46]>,
|
|
|
|
dirty: bool,
|
|
|
|
pub(crate) state_id: i32,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl PlayerInventory {
|
|
|
|
/// General slots are the slots that can hold all items, including the
|
|
|
|
/// hotbar, excluding offhand. These slots are shown when the player is
|
|
|
|
/// looking at another inventory.
|
|
|
|
pub const GENERAL_SLOTS: Range<SlotId> = 9..45;
|
|
|
|
pub const HOTBAR_SLOTS: Range<SlotId> = 36..45;
|
|
|
|
|
|
|
|
pub fn hotbar_to_slot(hotbar_slot: i16) -> Option<SlotId> {
|
|
|
|
if !(0..=8).contains(&hotbar_slot) {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(Self::HOTBAR_SLOTS.start + hotbar_slot)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
// Can't do the shorthand because Option<ItemStack> is not Copy.
|
|
|
|
slots: Box::new(std::array::from_fn(|_| None)),
|
|
|
|
dirty: true,
|
|
|
|
state_id: Default::default(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Inventory for PlayerInventory {
|
|
|
|
fn slot(&self, slot_id: SlotId) -> Option<&ItemStack> {
|
|
|
|
if !self.slot_range().contains(&slot_id) {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
self.slots[slot_id as usize].as_ref()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_slot(&mut self, slot_id: SlotId, slot: Option<ItemStack>) -> Option<ItemStack> {
|
|
|
|
if !self.slot_range().contains(&slot_id) {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
self.mark_dirty(true);
|
|
|
|
std::mem::replace(&mut self.slots[slot_id as usize], slot)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn slot_range(&self) -> Range<SlotId> {
|
|
|
|
0..(self.slots.len() as SlotId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl InventoryDirtyable for PlayerInventory {
|
|
|
|
fn mark_dirty(&mut self, dirty: bool) {
|
|
|
|
self.dirty = dirty
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_dirty(&self) -> bool {
|
|
|
|
self.dirty
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct ConfigurableInventory {
|
|
|
|
slots: Vec<Option<ItemStack>>,
|
|
|
|
/// The slots that the player can place items into for crafting. The
|
|
|
|
/// crafting result slot is always zero, and should not be included in this
|
|
|
|
/// range.
|
|
|
|
#[allow(dead_code)] // TODO: implement crafting
|
|
|
|
crafting_slots: Option<Range<SlotId>>,
|
|
|
|
/// The type of window that should be used to display this inventory.
|
|
|
|
pub window_type: VarInt,
|
|
|
|
dirty: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ConfigurableInventory {
|
|
|
|
pub fn new(size: usize, window_type: VarInt, crafting_slots: Option<Range<SlotId>>) -> Self {
|
|
|
|
ConfigurableInventory {
|
|
|
|
slots: vec![None; size],
|
|
|
|
crafting_slots,
|
|
|
|
window_type,
|
|
|
|
dirty: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Inventory for ConfigurableInventory {
|
|
|
|
fn slot(&self, slot_id: SlotId) -> Option<&ItemStack> {
|
|
|
|
if !self.slot_range().contains(&slot_id) {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
self.slots[slot_id as usize].as_ref()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn set_slot(&mut self, slot_id: SlotId, slot: Option<ItemStack>) -> Option<ItemStack> {
|
|
|
|
if !self.slot_range().contains(&slot_id) {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
self.mark_dirty(true);
|
|
|
|
std::mem::replace(&mut self.slots[slot_id as usize], slot)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn slot_range(&self) -> Range<SlotId> {
|
|
|
|
0..(self.slots.len() as SlotId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl InventoryDirtyable for ConfigurableInventory {
|
|
|
|
fn mark_dirty(&mut self, dirty: bool) {
|
|
|
|
self.dirty = dirty
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_dirty(&self) -> bool {
|
|
|
|
self.dirty
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/// Represents what the player sees when they open an object's Inventory.
|
|
|
|
///
|
|
|
|
/// This exists because when an object inventory screen is being shown to the
|
|
|
|
/// player, it also shows part of the player's inventory so they can move items
|
|
|
|
/// between the inventories.
|
|
|
|
pub struct WindowInventory {
|
|
|
|
pub window_id: u8,
|
|
|
|
pub object_inventory: InventoryId,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl WindowInventory {
|
|
|
|
pub fn new(window_id: impl Into<u8>, object_inventory: InventoryId) -> Self {
|
|
|
|
WindowInventory {
|
|
|
|
window_id: window_id.into(),
|
|
|
|
object_inventory,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn slots<'a>(
|
|
|
|
&self,
|
|
|
|
obj_inventory: &'a ConfigurableInventory,
|
|
|
|
player_inventory: &'a PlayerInventory,
|
|
|
|
) -> Vec<Option<&'a ItemStack>> {
|
|
|
|
let total_slots = obj_inventory.slots.len() + PlayerInventory::GENERAL_SLOTS.len();
|
|
|
|
(0..total_slots)
|
|
|
|
.map(|s| {
|
|
|
|
if s < obj_inventory.slot_count() {
|
|
|
|
return obj_inventory.slot(s as SlotId);
|
|
|
|
}
|
|
|
|
let offset = obj_inventory.slot_count();
|
|
|
|
player_inventory.slot((s - offset) as SlotId + PlayerInventory::GENERAL_SLOTS.start)
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
|
|
|
|
pub struct InventoryId(Key);
|
|
|
|
|
|
|
|
/// Manages all inventories that are present in the server.
|
|
|
|
pub struct Inventories {
|
|
|
|
slab: VersionedSlab<ConfigurableInventory>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Inventories {
|
|
|
|
pub(crate) fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
slab: VersionedSlab::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Creates a new inventory on a server.
|
|
|
|
pub fn insert(
|
|
|
|
&mut self,
|
|
|
|
inv: ConfigurableInventory,
|
|
|
|
) -> (InventoryId, &mut ConfigurableInventory) {
|
|
|
|
let (key, value) = self.slab.insert(inv);
|
|
|
|
(InventoryId(key), value)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Removes an inventory from the server.
|
|
|
|
pub fn remove(&mut self, inv: InventoryId) -> Option<ConfigurableInventory> {
|
|
|
|
self.slab.remove(inv.0)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns the number of inventories in this container.
|
|
|
|
pub fn len(&self) -> usize {
|
|
|
|
self.slab.len()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns `true` if there are no inventories.
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
|
|
self.slab.len() == 0
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get(&self, inv: InventoryId) -> Option<&ConfigurableInventory> {
|
|
|
|
self.slab.get(inv.0)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_mut(&mut self, inv: InventoryId) -> Option<&mut ConfigurableInventory> {
|
|
|
|
self.slab.get_mut(inv.0)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) fn update(&mut self) {
|
|
|
|
// now that we have synced all the dirty inventories, mark them as clean
|
|
|
|
for (_, inv) in self.slab.iter_mut() {
|
|
|
|
inv.mark_dirty(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-14 01:10:42 +11:00
|
|
|
#[derive(Copy, Clone, Debug, Error)]
|
2022-11-02 13:31:28 +11:00
|
|
|
#[error("InventoryError")]
|
|
|
|
pub struct InventoryError;
|
|
|
|
|
2022-10-16 13:47:02 +11:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
2022-11-15 17:30:20 +11:00
|
|
|
use valence_protocol::{ItemKind, ItemStack};
|
2022-11-14 01:10:42 +11:00
|
|
|
|
2022-10-16 13:47:02 +11:00
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_get_set_slots() {
|
|
|
|
let mut inv = PlayerInventory::new();
|
|
|
|
let slot = Some(ItemStack::new(ItemKind::Bone, 12, None));
|
|
|
|
let prev = inv.set_slot(9, slot.clone());
|
|
|
|
assert_eq!(inv.slot(9), slot.as_ref());
|
|
|
|
assert_eq!(prev, None);
|
|
|
|
}
|
2022-11-02 13:31:28 +11:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_consume() {
|
|
|
|
let mut inv = PlayerInventory::new();
|
|
|
|
let slot_id = 9;
|
|
|
|
let slot = Some(ItemStack::new(ItemKind::Bone, 12, None));
|
|
|
|
inv.set_slot(slot_id, slot);
|
|
|
|
assert!(matches!(inv.consume(slot_id, 2), Ok(_)));
|
|
|
|
assert_eq!(inv.slot(slot_id).unwrap().count(), 10);
|
|
|
|
assert!(matches!(inv.consume(slot_id, 20), Err(_)));
|
|
|
|
assert_eq!(inv.slot(slot_id).unwrap().count(), 10);
|
|
|
|
assert!(matches!(inv.consume(slot_id, 10), Ok(_)));
|
|
|
|
assert_eq!(inv.slot(slot_id), None);
|
|
|
|
}
|
2022-10-16 13:47:02 +11:00
|
|
|
}
|