mirror of
https://github.com/italicsjenga/valence.git
synced 2025-01-11 07:11:30 +11:00
Inventory module docs and more helper functions (#268)
<!-- Please make sure that your PR is aligned with the guidelines in CONTRIBUTING.md to the best of your ability. --> <!-- Good PRs have tests! Make sure you have sufficient test coverage. --> ## Description <!-- Describe the changes you've made. You may include any justification you want here. --> This mostly adds docs, but it also adds (and documents) some new helper functions: - `set_title()` - Allows us to put `must_use` on `replace_title` - `set_slot()` - Allows us to put `must_use` on `replace_slot` - `set_slot_amount()` - Allows users to modify stack counts without cloning the entire stack and replacing it. Useful if the stack has a lot of NBT data. - `first_empty_slot()` - `first_empty_slot_in()` - Useful, trivial to provide ## Test Plan <!-- Explain how you tested your changes, and include any code that you used to test this. --> <!-- If there is an example that is sufficient to use in place of a playground, replace the playground section with a note that indicates this. --> <details> <summary>Playground</summary> ```rust N/A ``` </details> <!-- You need to include steps regardless of whether or not you are using a playground. --> Steps: 1. `cargo test -p valence --doc` #### Related <!-- Link to any issues that have context for this or that this PR fixes. -->
This commit is contained in:
parent
62f882eec7
commit
9f8ff321c7
|
@ -140,14 +140,12 @@ fn place_blocks(
|
||||||
if client.game_mode() == GameMode::Survival {
|
if client.game_mode() == GameMode::Survival {
|
||||||
// check if the player has the item in their inventory and remove
|
// check if the player has the item in their inventory and remove
|
||||||
// it.
|
// it.
|
||||||
let slot = if stack.count() > 1 {
|
if stack.count() > 1 {
|
||||||
let mut stack = stack.clone();
|
let count = stack.count();
|
||||||
stack.set_count(stack.count() - 1);
|
inventory.set_slot_amount(slot_id, count - 1);
|
||||||
Some(stack)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
inventory.set_slot(slot_id, None);
|
||||||
};
|
}
|
||||||
inventory.replace_slot(slot_id, slot);
|
|
||||||
}
|
}
|
||||||
let real_pos = event.position.get_in_direction(event.direction);
|
let real_pos = event.position.get_in_direction(event.direction);
|
||||||
instance.set_block(real_pos, block_kind.to_state());
|
instance.set_block(real_pos, block_kind.to_state());
|
||||||
|
|
|
@ -1,5 +1,34 @@
|
||||||
|
//! The inventory system.
|
||||||
|
//!
|
||||||
|
//! This module contains the systems and components needed to handle
|
||||||
|
//! inventories. By default, clients will have a player inventory attached to
|
||||||
|
//! them.
|
||||||
|
//!
|
||||||
|
//! # Components
|
||||||
|
//!
|
||||||
|
//! - [`Inventory`]: The inventory component. This is the thing that holds
|
||||||
|
//! items.
|
||||||
|
//! - [`OpenInventory`]: The component that is attached to clients when they
|
||||||
|
//! have an inventory open.
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//!
|
||||||
|
//! An example system that will let you access all player's inventories:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! # use valence::prelude::*;
|
||||||
|
//! fn system(mut clients: Query<(&Client, &Inventory)>) {}
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ### See also
|
||||||
|
//!
|
||||||
|
//! Examples related to inventories in the `examples/` directory:
|
||||||
|
//! - `building`
|
||||||
|
//! - `chest`
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::iter::FusedIterator;
|
use std::iter::FusedIterator;
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_ecs::schedule::SystemConfigs;
|
use bevy_ecs::schedule::SystemConfigs;
|
||||||
|
@ -67,7 +96,36 @@ impl Inventory {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the slot at the given index to the given item stack.
|
||||||
|
///
|
||||||
|
/// See also [`Inventory::replace_slot`].
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use valence::prelude::*;
|
||||||
|
/// let mut inv = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
/// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
|
||||||
|
/// assert_eq!(inv.slot(0).unwrap().item, ItemKind::Diamond);
|
||||||
|
/// ```
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
|
#[inline]
|
||||||
|
pub fn set_slot(&mut self, idx: u16, item: impl Into<Option<ItemStack>>) {
|
||||||
|
let _ = self.replace_slot(idx, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the slot at the given index with the given item stack, and
|
||||||
|
/// returns the old stack in that slot.
|
||||||
|
///
|
||||||
|
/// See also [`Inventory::set_slot`].
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use valence::prelude::*;
|
||||||
|
/// let mut inv = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
/// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
|
||||||
|
/// let old = inv.replace_slot(0, ItemStack::new(ItemKind::IronIngot, 1, None));
|
||||||
|
/// assert_eq!(old.unwrap().item, ItemKind::Diamond);
|
||||||
|
/// ```
|
||||||
|
#[track_caller]
|
||||||
|
#[must_use]
|
||||||
pub fn replace_slot(
|
pub fn replace_slot(
|
||||||
&mut self,
|
&mut self,
|
||||||
idx: u16,
|
idx: u16,
|
||||||
|
@ -85,6 +143,17 @@ impl Inventory {
|
||||||
std::mem::replace(old, new)
|
std::mem::replace(old, new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Swap the contents of two slots. If the slots are the same, nothing
|
||||||
|
/// happens.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use valence::prelude::*;
|
||||||
|
/// let mut inv = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
/// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
|
||||||
|
/// assert_eq!(inv.slot(1), None);
|
||||||
|
/// inv.swap_slot(0, 1);
|
||||||
|
/// assert_eq!(inv.slot(1).unwrap().item, ItemKind::Diamond);
|
||||||
|
/// ```
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn swap_slot(&mut self, idx_a: u16, idx_b: u16) {
|
pub fn swap_slot(&mut self, idx_a: u16, idx_b: u16) {
|
||||||
assert!(idx_a < self.slot_count(), "slot index out of range");
|
assert!(idx_a < self.slot_count(), "slot index out of range");
|
||||||
|
@ -101,6 +170,30 @@ impl Inventory {
|
||||||
self.slots.swap(idx_a as usize, idx_b as usize);
|
self.slots.swap(idx_a as usize, idx_b as usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the amount of items in the given slot without replacing the slot
|
||||||
|
/// entirely. Valid values are 1-127, inclusive, and `amount` will be
|
||||||
|
/// clamped to this range. If the slot is empty, nothing happens.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use valence::prelude::*;
|
||||||
|
/// let mut inv = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
/// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
|
||||||
|
/// inv.set_slot_amount(0, 64);
|
||||||
|
/// assert_eq!(inv.slot(0).unwrap().count(), 64);
|
||||||
|
/// ```
|
||||||
|
#[track_caller]
|
||||||
|
pub fn set_slot_amount(&mut self, idx: u16, amount: u8) {
|
||||||
|
assert!(idx < self.slot_count(), "slot index out of range");
|
||||||
|
|
||||||
|
if let Some(item) = self.slots[idx as usize].as_mut() {
|
||||||
|
if item.count() == amount {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.set_count(amount);
|
||||||
|
self.modified |= 1 << idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn slot_count(&self) -> u16 {
|
pub fn slot_count(&self) -> u16 {
|
||||||
self.slots.len() as u16
|
self.slots.len() as u16
|
||||||
}
|
}
|
||||||
|
@ -119,10 +212,35 @@ impl Inventory {
|
||||||
self.kind
|
self.kind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The text displayed on the inventory's title bar.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use valence::inventory::{Inventory, InventoryKind};
|
||||||
|
/// # use valence_protocol::text::Text;
|
||||||
|
/// let inv = Inventory::with_title(InventoryKind::Generic9x3, "Box of Holding");
|
||||||
|
/// assert_eq!(inv.title(), &Text::from("Box of Holding"));
|
||||||
|
/// ```
|
||||||
pub fn title(&self) -> &Text {
|
pub fn title(&self) -> &Text {
|
||||||
&self.title
|
&self.title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the text displayed on the inventory's title bar.
|
||||||
|
///
|
||||||
|
/// To get the old title, use [`Inventory::replace_title`].
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use valence::inventory::{Inventory, InventoryKind};
|
||||||
|
/// let mut inv = Inventory::new(InventoryKind::Generic9x3);
|
||||||
|
/// inv.set_title("Box of Holding");
|
||||||
|
/// ```
|
||||||
|
#[inline]
|
||||||
|
pub fn set_title(&mut self, title: impl Into<Text>) {
|
||||||
|
let _ = self.replace_title(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the text displayed on the inventory's title bar, and returns the
|
||||||
|
/// old text.
|
||||||
|
#[must_use]
|
||||||
pub fn replace_title(&mut self, title: impl Into<Text>) -> Text {
|
pub fn replace_title(&mut self, title: impl Into<Text>) -> Text {
|
||||||
// TODO: set title modified flag
|
// TODO: set title modified flag
|
||||||
std::mem::replace(&mut self.title, title.into())
|
std::mem::replace(&mut self.title, title.into())
|
||||||
|
@ -131,6 +249,44 @@ impl Inventory {
|
||||||
fn slot_slice(&self) -> &[Option<ItemStack>] {
|
fn slot_slice(&self) -> &[Option<ItemStack>] {
|
||||||
self.slots.as_ref()
|
self.slots.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the first empty slot in the given range, or `None` if there are
|
||||||
|
/// no empty slots in the range.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use valence::prelude::*;
|
||||||
|
/// let mut inv = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
/// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
|
||||||
|
/// inv.set_slot(2, ItemStack::new(ItemKind::GoldIngot, 1, None));
|
||||||
|
/// inv.set_slot(3, ItemStack::new(ItemKind::IronIngot, 1, None));
|
||||||
|
/// assert_eq!(inv.first_empty_slot_in(0..6), Some(1));
|
||||||
|
/// assert_eq!(inv.first_empty_slot_in(2..6), Some(4));
|
||||||
|
/// ```
|
||||||
|
#[track_caller]
|
||||||
|
#[must_use]
|
||||||
|
pub fn first_empty_slot_in(&self, mut range: Range<u16>) -> Option<u16> {
|
||||||
|
assert!(
|
||||||
|
(0..=self.slot_count()).contains(&range.start)
|
||||||
|
&& (0..=self.slot_count()).contains(&range.end),
|
||||||
|
"slot range out of range"
|
||||||
|
);
|
||||||
|
|
||||||
|
range.find(|&idx| self.slots[idx as usize].is_none())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the first empty slot in the inventory, or `None` if there are no
|
||||||
|
/// empty slots.
|
||||||
|
/// ```
|
||||||
|
/// # use valence::prelude::*;
|
||||||
|
/// let mut inv = Inventory::new(InventoryKind::Generic9x1);
|
||||||
|
/// inv.set_slot(0, ItemStack::new(ItemKind::Diamond, 1, None));
|
||||||
|
/// inv.set_slot(2, ItemStack::new(ItemKind::GoldIngot, 1, None));
|
||||||
|
/// inv.set_slot(3, ItemStack::new(ItemKind::IronIngot, 1, None));
|
||||||
|
/// assert_eq!(inv.first_empty_slot(), Some(1));
|
||||||
|
/// ```
|
||||||
|
pub fn first_empty_slot(&self) -> Option<u16> {
|
||||||
|
self.first_empty_slot_in(0..self.slot_count())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send updates for each client's player inventory.
|
/// Send updates for each client's player inventory.
|
||||||
|
@ -384,12 +540,12 @@ fn handle_click_container(
|
||||||
for slot in event.slot_changes.clone() {
|
for slot in event.slot_changes.clone() {
|
||||||
if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) {
|
if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) {
|
||||||
// the client is interacting with a slot in the target inventory
|
// the client is interacting with a slot in the target inventory
|
||||||
target_inventory.replace_slot(slot.idx as u16, slot.item);
|
target_inventory.set_slot(slot.idx as u16, slot.item);
|
||||||
open_inventory.client_modified |= 1 << slot.idx;
|
open_inventory.client_modified |= 1 << slot.idx;
|
||||||
} else {
|
} else {
|
||||||
// the client is interacting with a slot in their own inventory
|
// the client is interacting with a slot in their own inventory
|
||||||
let slot_id = convert_to_player_slot_id(target_inventory.kind, slot.idx as u16);
|
let slot_id = convert_to_player_slot_id(target_inventory.kind, slot.idx as u16);
|
||||||
client_inventory.replace_slot(slot_id, slot.item);
|
client_inventory.set_slot(slot_id, slot.item);
|
||||||
client.inventory_slots_modified |= 1 << slot_id;
|
client.inventory_slots_modified |= 1 << slot_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -415,7 +571,7 @@ fn handle_click_container(
|
||||||
client.cursor_item = event.carried_item.clone();
|
client.cursor_item = event.carried_item.clone();
|
||||||
for slot in event.slot_changes.clone() {
|
for slot in event.slot_changes.clone() {
|
||||||
if (0i16..client_inventory.slot_count() as i16).contains(&slot.idx) {
|
if (0i16..client_inventory.slot_count() as i16).contains(&slot.idx) {
|
||||||
client_inventory.replace_slot(slot.idx as u16, slot.item);
|
client_inventory.set_slot(slot.idx as u16, slot.item);
|
||||||
client.inventory_slots_modified |= 1 << slot.idx;
|
client.inventory_slots_modified |= 1 << slot.idx;
|
||||||
} else {
|
} else {
|
||||||
// the client is trying to interact with a slot that does not exist,
|
// the client is trying to interact with a slot that does not exist,
|
||||||
|
@ -444,7 +600,7 @@ fn handle_set_slot_creative(
|
||||||
// the client is trying to interact with a slot that does not exist, ignore
|
// the client is trying to interact with a slot that does not exist, ignore
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
inventory.replace_slot(event.slot as u16, event.clicked_item.clone());
|
inventory.set_slot(event.slot as u16, event.clicked_item.clone());
|
||||||
inventory.modified &= !(1 << event.slot); // clear the modified bit, since we are about to send the update
|
inventory.modified &= !(1 << event.slot); // clear the modified bit, since we are about to send the update
|
||||||
client.inventory_state_id += 1;
|
client.inventory_state_id += 1;
|
||||||
let state_id = client.inventory_state_id.0;
|
let state_id = client.inventory_state_id.0;
|
||||||
|
@ -754,7 +910,7 @@ mod test {
|
||||||
.world
|
.world
|
||||||
.get_mut::<Inventory>(client_ent)
|
.get_mut::<Inventory>(client_ent)
|
||||||
.expect("could not find inventory for client");
|
.expect("could not find inventory for client");
|
||||||
inventory.replace_slot(20, ItemStack::new(ItemKind::Diamond, 2, None));
|
inventory.set_slot(20, ItemStack::new(ItemKind::Diamond, 2, None));
|
||||||
|
|
||||||
// Process a tick to get past the "on join" logic.
|
// Process a tick to get past the "on join" logic.
|
||||||
app.update();
|
app.update();
|
||||||
|
@ -817,7 +973,7 @@ mod test {
|
||||||
.world
|
.world
|
||||||
.get_mut::<Inventory>(client_ent)
|
.get_mut::<Inventory>(client_ent)
|
||||||
.expect("could not find inventory for client");
|
.expect("could not find inventory for client");
|
||||||
inventory.replace_slot(20, ItemStack::new(ItemKind::Diamond, 2, None));
|
inventory.set_slot(20, ItemStack::new(ItemKind::Diamond, 2, None));
|
||||||
|
|
||||||
// Process a tick to get past the "on join" logic.
|
// Process a tick to get past the "on join" logic.
|
||||||
app.update();
|
app.update();
|
||||||
|
@ -828,7 +984,7 @@ mod test {
|
||||||
.world
|
.world
|
||||||
.get_mut::<Inventory>(client_ent)
|
.get_mut::<Inventory>(client_ent)
|
||||||
.expect("could not find inventory for client");
|
.expect("could not find inventory for client");
|
||||||
inventory.replace_slot(21, ItemStack::new(ItemKind::IronIngot, 1, None));
|
inventory.set_slot(21, ItemStack::new(ItemKind::IronIngot, 1, None));
|
||||||
|
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
|
@ -958,7 +1114,7 @@ mod test {
|
||||||
.world
|
.world
|
||||||
.get_mut::<Inventory>(inventory_ent)
|
.get_mut::<Inventory>(inventory_ent)
|
||||||
.expect("could not find inventory for client");
|
.expect("could not find inventory for client");
|
||||||
inventory.replace_slot(5, ItemStack::new(ItemKind::IronIngot, 1, None));
|
inventory.set_slot(5, ItemStack::new(ItemKind::IronIngot, 1, None));
|
||||||
|
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
|
@ -1150,7 +1306,7 @@ mod test {
|
||||||
.world
|
.world
|
||||||
.get_mut::<Inventory>(client_ent)
|
.get_mut::<Inventory>(client_ent)
|
||||||
.expect("could not find inventory");
|
.expect("could not find inventory");
|
||||||
inventory.replace_slot(36, ItemStack::new(ItemKind::IronIngot, 3, None));
|
inventory.set_slot(36, ItemStack::new(ItemKind::IronIngot, 3, None));
|
||||||
|
|
||||||
// Process a tick to get past the "on join" logic.
|
// Process a tick to get past the "on join" logic.
|
||||||
app.update();
|
app.update();
|
||||||
|
@ -1205,7 +1361,7 @@ mod test {
|
||||||
.world
|
.world
|
||||||
.get_mut::<Inventory>(client_ent)
|
.get_mut::<Inventory>(client_ent)
|
||||||
.expect("could not find inventory");
|
.expect("could not find inventory");
|
||||||
inventory.replace_slot(36, ItemStack::new(ItemKind::IronIngot, 32, None));
|
inventory.set_slot(36, ItemStack::new(ItemKind::IronIngot, 32, None));
|
||||||
|
|
||||||
// Process a tick to get past the "on join" logic.
|
// Process a tick to get past the "on join" logic.
|
||||||
app.update();
|
app.update();
|
||||||
|
@ -1344,7 +1500,7 @@ mod test {
|
||||||
.world
|
.world
|
||||||
.get_mut::<Inventory>(client_ent)
|
.get_mut::<Inventory>(client_ent)
|
||||||
.expect("could not find inventory");
|
.expect("could not find inventory");
|
||||||
inventory.replace_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None));
|
inventory.set_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None));
|
||||||
|
|
||||||
// Process a tick to get past the "on join" logic.
|
// Process a tick to get past the "on join" logic.
|
||||||
app.update();
|
app.update();
|
||||||
|
@ -1392,7 +1548,7 @@ mod test {
|
||||||
.world
|
.world
|
||||||
.get_mut::<Inventory>(client_ent)
|
.get_mut::<Inventory>(client_ent)
|
||||||
.expect("could not find inventory");
|
.expect("could not find inventory");
|
||||||
inventory.replace_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None));
|
inventory.set_slot(40, ItemStack::new(ItemKind::IronIngot, 32, None));
|
||||||
|
|
||||||
// Process a tick to get past the "on join" logic.
|
// Process a tick to get past the "on join" logic.
|
||||||
app.update();
|
app.update();
|
||||||
|
|
Loading…
Reference in a new issue