From 59ca1ab573f303dd510c4f34ad6508365e7d3226 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Sat, 15 Oct 2022 22:47:02 -0400 Subject: [PATCH] Set up Inventory module (#75) This is a very rough version of the inventory module and associated features. This will be a good base to start building something more robust. - adds a player inventory to all clients - makes clients keep track of what inventory they have open - adds very naive state sync for inventories related: #53 # Test plans ### Building 1. run `building` example 2. pick any block from creative 3. place it 4. pick a different block from creative 5. place 2 6. break the last one 7. see that both remaining blocks don't become dirt ### Chest inventories 1. run `chest` example 2. sneak to give yourself a stone block 3. open the chest (it's invisible on top of the stone block) 4. place stone in chest 5. see the stone moving around in the chest ## Known Issues To be fixed in later PRs - It's very easy to duplicate items if you take the stone out of the chest on the same tick as the server trying to move the item. - The chest is invisible when you spawn in. It appears if you break it. - `get_slot` and `set_slot` can panic. --- examples/building.rs | 13 +- examples/chest.rs | 283 +++++++++++++++++++++++++++++++++++++++++++ src/client.rs | 86 +++++++++++-- src/client/event.rs | 13 +- src/inventory.rs | 267 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/server.rs | 5 + 7 files changed, 657 insertions(+), 11 deletions(-) create mode 100644 examples/chest.rs create mode 100644 src/inventory.rs diff --git a/examples/building.rs b/examples/building.rs index 7242cc3..df3ad90 100644 --- a/examples/building.rs +++ b/examples/building.rs @@ -209,8 +209,17 @@ impl Config for Game { } => { if hand == Hand::Main { let place_at = location.get_in_direction(face); - // TODO: get block from player's inventory slot - world.chunks.set_block_state(place_at, BlockState::DIRT); + if let Some(stack) = client.held_item() { + if let Some(block_kind) = stack.item.to_block_kind() { + world.chunks.set_block_state( + place_at, + BlockState::from_kind(block_kind), + ); + if client.game_mode() != GameMode::Creative { + client.consume_one_held_item(); + } + } + } } } _ => {} diff --git a/examples/chest.rs b/examples/chest.rs new file mode 100644 index 0000000..a60a590 --- /dev/null +++ b/examples/chest.rs @@ -0,0 +1,283 @@ +use std::net::SocketAddr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use log::LevelFilter; +use num::Integer; +use valence::async_trait; +use valence::block::BlockState; +use valence::chunk::UnloadedChunk; +use valence::client::{handle_event_default, ClientEvent, Hand}; +use valence::config::{Config, ServerListPing}; +use valence::dimension::{Dimension, DimensionId}; +use valence::entity::{EntityId, EntityKind}; +use valence::inventory::{ + ConfigurableInventory, Inventory, InventoryId, PlayerInventory, WindowInventory, +}; +use valence::item::{ItemKind, ItemStack}; +use valence::player_list::PlayerListId; +use valence::protocol::packets::s2c::play::OpenScreen; +use valence::protocol::{SlotId, VarInt}; +use valence::server::{Server, SharedServer, ShutdownResult}; +use valence::text::{Color, TextFormat}; + +pub fn main() -> ShutdownResult { + env_logger::Builder::new() + .filter_module("valence", LevelFilter::Trace) + .parse_default_env() + .init(); + + valence::start_server( + Game { + player_count: AtomicUsize::new(0), + }, + ServerState { + player_list: None, + chest: Default::default(), + tick: 0, + }, + ) +} + +struct Game { + player_count: AtomicUsize, +} + +struct ServerState { + player_list: Option, + chest: InventoryId, + tick: u32, +} + +#[derive(Default)] +struct ClientState { + entity_id: EntityId, + // open_inventory: Option, +} + +const MAX_PLAYERS: usize = 10; + +const SIZE_X: usize = 100; +const SIZE_Z: usize = 100; + +#[async_trait] +impl Config for Game { + type ServerState = ServerState; + type ClientState = ClientState; + type EntityState = (); + type WorldState = (); + type ChunkState = (); + type PlayerListState = (); + + fn max_connections(&self) -> usize { + // We want status pings to be successful even if the server is full. + MAX_PLAYERS + 64 + } + + fn dimensions(&self) -> Vec { + vec![Dimension { + fixed_time: Some(6000), + ..Dimension::default() + }] + } + + async fn server_list_ping( + &self, + _server: &SharedServer, + _remote_addr: SocketAddr, + _protocol_version: i32, + ) -> ServerListPing { + ServerListPing::Respond { + online_players: self.player_count.load(Ordering::SeqCst) as i32, + max_players: MAX_PLAYERS as i32, + player_sample: Default::default(), + description: "Hello Valence!".color(Color::AQUA), + favicon_png: Some(include_bytes!("../assets/logo-64x64.png").as_slice().into()), + } + } + + fn init(&self, server: &mut Server) { + let world = server.worlds.insert(DimensionId::default(), ()).1; + server.state.player_list = Some(server.player_lists.insert(()).0); + + // initialize chunks + for chunk_z in -2..Integer::div_ceil(&(SIZE_Z as i32), &16) + 2 { + for chunk_x in -2..Integer::div_ceil(&(SIZE_X as i32), &16) + 2 { + world.chunks.insert( + [chunk_x as i32, chunk_z as i32], + UnloadedChunk::default(), + (), + ); + } + } + + // initialize blocks in the chunks + for x in 0..SIZE_X { + for z in 0..SIZE_Z { + world + .chunks + .set_block_state((x as i32, 0, z as i32), BlockState::GRASS_BLOCK); + } + } + + world.chunks.set_block_state((50, 0, 54), BlockState::STONE); + world.chunks.set_block_state((50, 1, 54), BlockState::CHEST); + + // create chest inventory + let inv = ConfigurableInventory::new(27, VarInt(2), None); + let (id, _inv) = server.inventories.insert(inv); + server.state.chest = id; + } + + fn update(&self, server: &mut Server) { + server.state.tick += 1; + if server.state.tick > 10 { + server.state.tick = 0; + } + let (world_id, world) = server.worlds.iter_mut().next().unwrap(); + + let spawn_pos = [SIZE_X as f64 / 2.0, 1.0, SIZE_Z as f64 / 2.0]; + + if let Some(inv) = server.inventories.get_mut(server.state.chest) { + if server.state.tick == 0 { + rotate_items(inv); + } + } + + server.clients.retain(|_, client| { + if client.created_this_tick() { + if self + .player_count + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + (count < MAX_PLAYERS).then_some(count + 1) + }) + .is_err() + { + client.disconnect("The server is full!".color(Color::RED)); + return false; + } + + match server + .entities + .insert_with_uuid(EntityKind::Player, client.uuid(), ()) + { + Some((id, _)) => client.state.entity_id = id, + None => { + client.disconnect("Conflicting UUID"); + return false; + } + } + + client.spawn(world_id); + client.set_flat(true); + client.teleport(spawn_pos, 0.0, 0.0); + client.set_player_list(server.state.player_list.clone()); + + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).insert( + client.uuid(), + client.username(), + client.textures().cloned(), + client.game_mode(), + 0, + None, + ); + } + + client.send_message("Welcome to Valence! Sneak to give yourself an item.".italic()); + } + + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + server.entities.remove(client.state.entity_id); + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).remove(client.uuid()); + } + return false; + } + + let player = server.entities.get_mut(client.state.entity_id).unwrap(); + + if client.position().y <= -20.0 { + client.teleport(spawn_pos, client.yaw(), client.pitch()); + } + + while let Some(event) = handle_event_default(client, player) { + match event { + ClientEvent::InteractWithBlock { hand, location, .. } => { + if hand == Hand::Main + && world.chunks.block_state(location) == Some(BlockState::CHEST) + { + client.send_message("Opening chest!"); + let window = WindowInventory::new(1, server.state.chest); + client.send_packet(OpenScreen { + window_id: VarInt(window.window_id.into()), + window_type: VarInt(2), + window_title: "Extra".italic() + + " Chesty".not_italic().bold().color(Color::RED) + + " Chest".not_italic(), + }); + client.open_inventory = Some(window); + } + } + ClientEvent::CloseScreen { window_id } => { + if window_id > 0 { + client.send_message(format!("Window closed: {}", window_id)); + client.send_message(format!("Chest: {:?}", server.state.chest)); + } + } + ClientEvent::ClickContainer { + window_id, + state_id, + slot_id, + mode, + slot_changes, + carried_item, + } => { + println!( + "window_id: {:?}, state_id: {:?}, slot_id: {:?}, mode: {:?}, \ + slot_changes: {:?}, carried_item: {:?}", + window_id, state_id, slot_id, mode, slot_changes, carried_item + ); + client.cursor_held_item = carried_item; + if let Some(window) = client.open_inventory.as_mut() { + if let Some(obj_inv) = + server.inventories.get_mut(window.object_inventory) + { + for (slot_id, slot) in slot_changes { + if slot_id < obj_inv.slot_count() as SlotId { + obj_inv.set_slot(slot_id, slot); + } else { + let offset = obj_inv.slot_count() as SlotId; + client.inventory.set_slot( + slot_id - offset + PlayerInventory::GENERAL_SLOTS.start, + slot, + ); + } + } + } + } + } + ClientEvent::StartSneaking => { + let slot_id: SlotId = PlayerInventory::HOTBAR_SLOTS.start; + let stack = match client.inventory.slot(slot_id) { + None => ItemStack::new(ItemKind::Stone, 1, None), + Some(s) => ItemStack::new(s.item, s.count() + 1, None), + }; + client.inventory.set_slot(slot_id, Some(stack)); + } + _ => {} + } + } + + true + }); + } +} + +fn rotate_items(inv: &mut ConfigurableInventory) { + for i in 1..inv.slot_count() { + let a = inv.slot((i - 1) as SlotId); + let b = inv.set_slot(i as SlotId, a.cloned()); + inv.set_slot((i - 1) as SlotId, b); + } +} diff --git a/src/client.rs b/src/client.rs index ff530b3..f2c1edf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -21,6 +21,10 @@ use crate::entity::{ self, velocity_to_packet_units, Entities, EntityId, EntityKind, StatusOrAnimation, }; use crate::ident::Ident; +use crate::inventory::{ + Inventories, Inventory, InventoryDirtyable, PlayerInventory, WindowInventory, +}; +use crate::item::ItemStack; use crate::player_list::{PlayerListId, PlayerLists}; use crate::player_textures::SignedPlayerTextures; use crate::protocol::packets::c2s::play::{ @@ -31,13 +35,13 @@ use crate::protocol::packets::s2c::play::{ AcknowledgeBlockChange, ClearTitles, CombatDeath, CustomSoundEffect, DisconnectPlay, EntityAnimationS2c, EntityAttributesProperty, EntityEvent, GameEvent, GameStateChangeReason, KeepAliveS2c, LoginPlay, PlayerPositionLookFlags, RemoveEntities, ResourcePackS2c, Respawn, - S2cPlayPacket, SetActionBarText, SetCenterChunk, SetDefaultSpawnPosition, SetEntityMetadata, - SetEntityVelocity, SetExperience, SetHeadRotation, SetHealth, SetRenderDistance, - SetSubtitleText, SetTitleText, SoundCategory, SynchronizePlayerPosition, SystemChatMessage, - TeleportEntity, UnloadChunk, UpdateAttributes, UpdateEntityPosition, + S2cPlayPacket, SetActionBarText, SetCenterChunk, SetContainerContent, SetDefaultSpawnPosition, + SetEntityMetadata, SetEntityVelocity, SetExperience, SetHeadRotation, SetHealth, + SetRenderDistance, SetSubtitleText, SetTitleText, SoundCategory, SynchronizePlayerPosition, + SystemChatMessage, TeleportEntity, UnloadChunk, UpdateAttributes, UpdateEntityPosition, UpdateEntityPositionAndRotation, UpdateEntityRotation, UpdateTime, }; -use crate::protocol::{BoundedInt, BoundedString, ByteAngle, RawBytes, Slot, VarInt}; +use crate::protocol::{BoundedInt, BoundedString, ByteAngle, RawBytes, SlotId, VarInt}; use crate::server::{C2sPacketChannels, NewClientData, S2cPlayMessage, SharedServer}; use crate::slab_versioned::{Key, VersionedSlab}; use crate::text::Text; @@ -230,13 +234,16 @@ pub struct Client { resource_pack_to_send: Option, attack_speed: f64, movement_speed: f64, + pub inventory: PlayerInventory, // TODO: make private or pub(crate) + pub open_inventory: Option, // TODO: make private or pub(crate) bits: ClientBits, /// The data for the client's own player entity. player_data: Player, entity_events: Vec, /// The item currently being held by the client's cursor in an inventory /// screen. Does not work for creative mode. - cursor_held_item: Slot, + pub cursor_held_item: Option, // TODO: make private or pub(crate) + selected_hotbar_slot: SlotId, } #[bitfield(u16)] @@ -300,6 +307,8 @@ impl Client { resource_pack_to_send: None, attack_speed: 4.0, movement_speed: 0.7, + inventory: PlayerInventory::new(), + open_inventory: None, bits: ClientBits::new() .with_modified_spawn_position(true) .with_got_keepalive(true) @@ -307,6 +316,7 @@ impl Client { player_data: Player::new(), entity_events: Vec::new(), cursor_held_item: None, + selected_hotbar_slot: PlayerInventory::HOTBAR_SLOTS.start, } } @@ -766,6 +776,16 @@ impl Client { self.settings.as_ref() } + /// The slot that the client has selected in their hotbar. + pub fn held_item(&self) -> Option<&ItemStack> { + self.inventory.slot(self.selected_hotbar_slot) + } + + /// Consume a single item from the stack that the client is holding. + pub fn consume_one_held_item(&mut self) { + self.inventory.consume_one(self.selected_hotbar_slot); + } + /// Disconnects this client from the server with the provided reason. This /// has no effect if the client is already disconnected. /// @@ -1047,7 +1067,10 @@ impl Client { C2sPlayPacket::SeenAdvancements(_) => {} C2sPlayPacket::SelectTrade(_) => {} C2sPlayPacket::SetBeaconEffect(_) => {} - C2sPlayPacket::SetHeldItemS2c(_) => {} + C2sPlayPacket::SetHeldItemS2c(e) => { + self.selected_hotbar_slot = + PlayerInventory::hotbar_to_slot(e.slot.0).unwrap_or(self.selected_hotbar_slot); + } C2sPlayPacket::ProgramCommandBlock(_) => {} C2sPlayPacket::ProgramCommandBlockMinecart(_) => {} C2sPlayPacket::SetCreativeModeSlot(e) => { @@ -1089,6 +1112,7 @@ impl Client { entities: &Entities, worlds: &Worlds, player_lists: &PlayerLists, + inventories: &Inventories, ) { // Mark the client as disconnected when appropriate. if self.recv.is_disconnected() || self.send.as_ref().map_or(true, |s| s.is_disconnected()) { @@ -1567,6 +1591,54 @@ impl Client { self.old_position = self.position; self.bits.set_created_this_tick(false); + // Update the player's inventory + if self.inventory.is_dirty() { + send_packet( + &mut self.send, + SetContainerContent { + window_id: 0, + state_id: VarInt(self.inventory.state_id), + slots: self + .inventory + .slots() + .into_iter() + // FIXME: cloning is necessary here to build the packet. + // However, it should be possible to avoid the clone if this packet + // could consume refs + .map(|s| s.cloned()) + .collect(), + carried_item: self.cursor_held_item.clone(), + }, + ); + self.inventory.state_id = self.inventory.state_id.wrapping_add(1); + self.inventory.mark_dirty(false); + } + + // Update the client's UI if they have an open inventory. + if let Some(window) = self.open_inventory.as_ref() { + // this client has an inventory open + let obj_inv_id = window.object_inventory; + if let Some(obj_inv) = inventories.get(obj_inv_id) { + if obj_inv.is_dirty() { + let window_id = window.window_id; + let slots = window.slots(obj_inv, &self.inventory) + .into_iter() + // FIXME: cloning is necessary here to build the packet. + // However, it should be possible to avoid the clone if this packet + // could consume refs + .map(|s| s.cloned()) + .collect(); + let carried_item = self.cursor_held_item.clone(); + self.send_packet(SetContainerContent { + window_id, + state_id: VarInt(1), + slots, + carried_item, + }); + } + } + } + send_packet(&mut self.send, S2cPlayMessage::Flush); } } diff --git a/src/client/event.rs b/src/client/event.rs index 8a67d90..9cc791b 100644 --- a/src/client/event.rs +++ b/src/client/event.rs @@ -7,6 +7,7 @@ use crate::block_pos::BlockPos; use crate::config::Config; use crate::entity::types::Pose; use crate::entity::{Entity, EntityEvent, EntityId, TrackedData}; +use crate::inventory::Inventory; use crate::item::ItemStack; use crate::protocol::packets::c2s::play::ClickContainerMode; pub use crate::protocol::packets::c2s::play::{ @@ -331,10 +332,18 @@ pub fn handle_event_default( ClientEvent::Digging { .. } => {} ClientEvent::InteractWithBlock { .. } => {} ClientEvent::ResourcePackStatusChanged(_) => {} - ClientEvent::CloseScreen { .. } => {} + ClientEvent::CloseScreen { window_id } => { + if let Some(window) = &client.open_inventory { + if window.window_id == *window_id { + client.open_inventory = None; + } + } + } ClientEvent::DropItem => {} ClientEvent::DropItemStack { .. } => {} - ClientEvent::SetSlotCreative { .. } => {} + ClientEvent::SetSlotCreative { slot_id, slot } => { + client.inventory.set_slot(*slot_id, slot.clone()); + } ClientEvent::ClickContainer { .. } => {} ClientEvent::RespawnRequest => {} } diff --git a/src/inventory.rs b/src/inventory.rs new file mode 100644 index 0000000..b5e92fb --- /dev/null +++ b/src/inventory.rs @@ -0,0 +1,267 @@ +use std::ops::Range; + +use crate::item::ItemStack; +use crate::protocol::{SlotId, VarInt}; +use crate::slab_versioned::{Key, VersionedSlab}; + +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) -> Option; + fn slot_range(&self) -> Range; + + fn slot_count(&self) -> usize { + self.slot_range().count() + } + + // TODO: `entry()` style api + + fn slots(&self) -> Vec> { + (0..self.slot_count()) + .map(|s| self.slot(s as SlotId)) + .collect() + } + + fn consume_one(&mut self, slot_id: SlotId) { + let mut slot = self.slot(slot_id).cloned(); + if let Some(stack) = slot.as_mut() { + stack.set_count(stack.count() - 1); + let slot = if stack.count() == 0 { + None + } else { + Some(stack) + }; + self.set_slot(slot_id, slot.cloned()); + } + } +} + +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; 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 = 9..45; + pub const HOTBAR_SLOTS: Range = 36..45; + + pub fn hotbar_to_slot(hotbar_slot: i16) -> Option { + 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 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) -> Option { + 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 { + 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>, + /// 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>, + /// 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>) -> 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) -> Option { + 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 { + 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, 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> { + 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, +} + +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 { + 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); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::item::{ItemKind, ItemStack}; + + #[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); + } +} diff --git a/src/lib.rs b/src/lib.rs index 02cb9d2..243ae6e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,6 +110,7 @@ pub mod dimension; pub mod enchant; pub mod entity; pub mod ident; +pub mod inventory; pub mod item; pub mod player_list; pub mod player_textures; diff --git a/src/server.rs b/src/server.rs index 8ce5919..e6781b5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -32,6 +32,7 @@ use crate::client::{Client, Clients}; use crate::config::{Config, ServerListPing}; use crate::dimension::{validate_dimensions, Dimension, DimensionId}; use crate::entity::Entities; +use crate::inventory::Inventories; use crate::player_list::PlayerLists; use crate::player_textures::SignedPlayerTextures; use crate::protocol::codec::{Decoder, Encoder}; @@ -66,6 +67,7 @@ pub struct Server { pub worlds: Worlds, /// All of the player lists on the server. pub player_lists: PlayerLists, + pub inventories: Inventories, } /// A handle to a Minecraft server containing the subset of functionality which @@ -292,6 +294,7 @@ pub fn start_server(config: C, data: C::ServerState) -> ShutdownResul entities: Entities::new(), worlds: Worlds::new(shared.clone()), player_lists: PlayerLists::new(), + inventories: Inventories::new(), }; shared.config().init(&mut server); @@ -438,6 +441,7 @@ fn do_update_loop(server: &mut Server) -> ShutdownResult { &server.entities, &server.worlds, &server.player_lists, + &server.inventories, ); }); @@ -448,6 +452,7 @@ fn do_update_loop(server: &mut Server) -> ShutdownResult { }); server.player_lists.update(); + server.inventories.update(); // Sleep for the remainder of the tick. let tick_duration = Duration::from_secs_f64((shared.0.tick_rate as f64).recip());