From 9f8ff321c7186462fc92c17c94267f34da34fa4a Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 4 Mar 2023 19:31:21 -0500 Subject: [PATCH] Inventory module docs and more helper functions (#268) ## Description 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
Playground ```rust N/A ```
Steps: 1. `cargo test -p valence --doc` #### Related --- crates/valence/examples/building.rs | 12 +- crates/valence/src/inventory.rs | 180 ++++++++++++++++++++++++++-- 2 files changed, 173 insertions(+), 19 deletions(-) diff --git a/crates/valence/examples/building.rs b/crates/valence/examples/building.rs index 3c188e9..ab7c2b1 100644 --- a/crates/valence/examples/building.rs +++ b/crates/valence/examples/building.rs @@ -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()); diff --git a/crates/valence/src/inventory.rs b/crates/valence/src/inventory.rs index bd89733..a699407 100644 --- a/crates/valence/src/inventory.rs +++ b/crates/valence/src/inventory.rs @@ -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>) { + 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) { + 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 { // TODO: set title modified flag std::mem::replace(&mut self.title, title.into()) @@ -131,6 +249,44 @@ impl Inventory { fn slot_slice(&self) -> &[Option] { 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) -> Option { + 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 { + 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::(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::(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::(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_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::(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::(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::(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::(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();