mirror of
https://github.com/italicsjenga/valence.git
synced 2024-12-23 14:31:30 +11:00
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:
parent
f58e6662dd
commit
59ca1ab573
|
@ -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
283
examples/chest.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
267
src/inventory.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue