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.
This commit is contained in:
Carson McManus 2022-10-15 22:47:02 -04:00 committed by GitHub
parent f58e6662dd
commit 59ca1ab573
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 657 additions and 11 deletions

View file

@ -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();
}
}
}
}
}
_ => {}

283
examples/chest.rs Normal file
View file

@ -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<PlayerListId>,
chest: InventoryId,
tick: u32,
}
#[derive(Default)]
struct ClientState {
entity_id: EntityId,
// open_inventory: Option<WindowInventory>,
}
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<Dimension> {
vec![Dimension {
fixed_time: Some(6000),
..Dimension::default()
}]
}
async fn server_list_ping(
&self,
_server: &SharedServer<Self>,
_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<Self>) {
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<Self>) {
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);
}
}

View file

@ -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<C: Config> {
resource_pack_to_send: Option<ResourcePackS2c>,
attack_speed: f64,
movement_speed: f64,
pub inventory: PlayerInventory, // TODO: make private or pub(crate)
pub open_inventory: Option<WindowInventory>, // TODO: make private or pub(crate)
bits: ClientBits,
/// The data for the client's own player entity.
player_data: Player,
entity_events: Vec<entity::EntityEvent>,
/// 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<ItemStack>, // TODO: make private or pub(crate)
selected_hotbar_slot: SlotId,
}
#[bitfield(u16)]
@ -300,6 +307,8 @@ impl<C: Config> Client<C> {
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<C: Config> Client<C> {
player_data: Player::new(),
entity_events: Vec::new(),
cursor_held_item: None,
selected_hotbar_slot: PlayerInventory::HOTBAR_SLOTS.start,
}
}
@ -766,6 +776,16 @@ impl<C: Config> Client<C> {
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<C: Config> Client<C> {
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<C: Config> Client<C> {
entities: &Entities<C>,
worlds: &Worlds<C>,
player_lists: &PlayerLists<C>,
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<C: Config> Client<C> {
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);
}
}

View file

@ -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<C: Config>(
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 => {}
}

267
src/inventory.rs Normal file
View file

@ -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<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()
}
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<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);
}
}
}
#[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);
}
}

View file

@ -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;

View file

@ -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<C: Config> {
pub worlds: Worlds<C>,
/// All of the player lists on the server.
pub player_lists: PlayerLists<C>,
pub inventories: Inventories,
}
/// A handle to a Minecraft server containing the subset of functionality which
@ -292,6 +294,7 @@ pub fn start_server<C: Config>(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<C: Config>(server: &mut Server<C>) -> ShutdownResult {
&server.entities,
&server.worlds,
&server.player_lists,
&server.inventories,
);
});
@ -448,6 +452,7 @@ fn do_update_loop<C: Config>(server: &mut Server<C>) -> 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());