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:
Carson McManus 2023-03-04 19:31:21 -05:00 committed by GitHub
parent 62f882eec7
commit 9f8ff321c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 173 additions and 19 deletions

View file

@ -140,14 +140,12 @@ fn place_blocks(
if client.game_mode() == GameMode::Survival {
// check if the player has the item in their inventory and remove
// it.
let slot = if stack.count() > 1 {
let mut stack = stack.clone();
stack.set_count(stack.count() - 1);
Some(stack)
if stack.count() > 1 {
let count = stack.count();
inventory.set_slot_amount(slot_id, count - 1);
} else {
None
};
inventory.replace_slot(slot_id, slot);
inventory.set_slot(slot_id, None);
}
}
let real_pos = event.position.get_in_direction(event.direction);
instance.set_block(real_pos, block_kind.to_state());

View file

@ -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::iter::FusedIterator;
use std::ops::Range;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::SystemConfigs;
@ -67,7 +96,36 @@ impl Inventory {
.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]
#[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(
&mut self,
idx: u16,
@ -85,6 +143,17 @@ impl Inventory {
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]
pub fn swap_slot(&mut self, idx_a: u16, idx_b: u16) {
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);
}
/// 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 {
self.slots.len() as u16
}
@ -119,10 +212,35 @@ impl Inventory {
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 {
&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 {
// TODO: set title modified flag
std::mem::replace(&mut self.title, title.into())
@ -131,6 +249,44 @@ impl Inventory {
fn slot_slice(&self) -> &[Option<ItemStack>] {
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.
@ -384,12 +540,12 @@ fn handle_click_container(
for slot in event.slot_changes.clone() {
if (0i16..target_inventory.slot_count() as i16).contains(&slot.idx) {
// 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;
} else {
// 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);
client_inventory.replace_slot(slot_id, slot.item);
client_inventory.set_slot(slot_id, slot.item);
client.inventory_slots_modified |= 1 << slot_id;
}
}
@ -415,7 +571,7 @@ fn handle_click_container(
client.cursor_item = event.carried_item.clone();
for slot in event.slot_changes.clone() {
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;
} else {
// 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
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
client.inventory_state_id += 1;
let state_id = client.inventory_state_id.0;
@ -754,7 +910,7 @@ mod test {
.world
.get_mut::<Inventory>(client_ent)
.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.
app.update();
@ -817,7 +973,7 @@ mod test {
.world
.get_mut::<Inventory>(client_ent)
.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.
app.update();
@ -828,7 +984,7 @@ mod test {
.world
.get_mut::<Inventory>(client_ent)
.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();
@ -958,7 +1114,7 @@ mod test {
.world
.get_mut::<Inventory>(inventory_ent)
.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();
@ -1150,7 +1306,7 @@ mod test {
.world
.get_mut::<Inventory>(client_ent)
.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.
app.update();
@ -1205,7 +1361,7 @@ mod test {
.world
.get_mut::<Inventory>(client_ent)
.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.
app.update();
@ -1344,7 +1500,7 @@ mod test {
.world
.get_mut::<Inventory>(client_ent)
.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.
app.update();
@ -1392,7 +1548,7 @@ mod test {
.world
.get_mut::<Inventory>(client_ent)
.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.
app.update();