diff --git a/feather/protocol/src/packets/server/play.rs b/feather/protocol/src/packets/server/play.rs index 01c83b03f..7c4c8aca7 100644 --- a/feather/protocol/src/packets/server/play.rs +++ b/feather/protocol/src/packets/server/play.rs @@ -7,7 +7,10 @@ pub use chunk_data::{ChunkData, ChunkDataKind}; use quill_common::components::PreviousGamemode; pub use update_light::UpdateLight; -use crate::{io::VarLong, Readable, Writeable}; +use crate::{ + io::VarLong, packets::server::EquipmentSlot::MainHand, InventorySlot::Empty, Readable, + Writeable, +}; use super::*; @@ -947,7 +950,22 @@ packets! { #[derive(Debug, Clone)] pub struct EntityEquipment { pub entity_id: i32, - pub entries: Vec, + /// The entries in this `EntityEquipment` packet + /// + /// Must not be empty. If nothing is equipped, send an empty main hand. + entries: Vec, +} + +impl EntityEquipment { + pub fn new(entity_id: i32, mut entries: Vec) -> Self { + if entries.is_empty() { + entries.push(EquipmentEntry { + slot: MainHand, + item: Empty, + }) + } + EntityEquipment { entity_id, entries } + } } impl Readable for EntityEquipment { diff --git a/feather/server/src/client.rs b/feather/server/src/client.rs index edca37eea..08efad135 100644 --- a/feather/server/src/client.rs +++ b/feather/server/src/client.rs @@ -9,12 +9,16 @@ use ahash::AHashSet; use flume::{Receiver, Sender}; use uuid::Uuid; +use crate::{ + entities::PreviousPosition, initial_handler::NewPlayer, network_id_registry::NetworkId, Options, +}; use base::{ - BlockId, ChunkHandle, ChunkPosition, EntityKind, EntityMetadata, Gamemode, Position, - ProfileProperty, Text, ValidBlockPosition, + Area, BlockId, ChunkHandle, ChunkPosition, EntityKind, EntityMetadata, Gamemode, Inventory, + Position, ProfileProperty, Text, ValidBlockPosition, }; use common::{ chat::{ChatKind, ChatMessage}, + entities::player::HotbarSlot, Window, }; use libcraft_items::InventorySlot; @@ -27,18 +31,16 @@ use protocol::{ self, server::{ AddPlayer, Animation, BlockChange, ChatPosition, ChunkData, ChunkDataKind, - DestroyEntities, Disconnect, EntityAnimation, EntityHeadLook, JoinGame, KeepAlive, - PlayerInfo, PlayerPositionAndLook, PluginMessage, SendEntityMetadata, SpawnPlayer, - Title, UnloadChunk, UpdateViewPosition, WindowItems, + DestroyEntities, Disconnect, EntityAnimation, EntityEquipment, EntityHeadLook, + EquipmentEntry, + EquipmentSlot::{Boots, Chestplate, Helmet, Leggings, MainHand, OffHand}, + JoinGame, KeepAlive, PlayerInfo, PlayerPositionAndLook, PluginMessage, + SendEntityMetadata, SpawnPlayer, Title, UnloadChunk, UpdateViewPosition, WindowItems, }, }, ClientPlayPacket, Nbt, ProtocolVersion, ServerPlayPacket, Writeable, }; use quill_common::components::{OnGround, PreviousGamemode}; - -use crate::{ - entities::PreviousPosition, initial_handler::NewPlayer, network_id_registry::NetworkId, Options, -}; use slab::Slab; /// Max number of chunks to send to a client per tick. @@ -539,11 +541,11 @@ impl Client { self.set_slot(-1, item); } - pub fn send_player_model_flags(&self, netowrk_id: NetworkId, model_flags: u8) { + pub fn send_player_model_flags(&self, network_id: NetworkId, model_flags: u8) { let mut entity_metadata = EntityMetadata::new(); entity_metadata.set(16, model_flags); self.send_packet(SendEntityMetadata { - entity_id: netowrk_id.0, + entity_id: network_id.0, entries: entity_metadata, }); } @@ -558,6 +560,62 @@ impl Client { }); } + // TODO this probably needs to be more general (i.e. applicable for all entities and not just players) + pub fn send_entity_equipment( + &self, + network_id: NetworkId, + inventory: &Inventory, + hotbar_slot: &HotbarSlot, + ) { + if self.network_id == Some(network_id) { + return; + } + + let mut equipment = Vec::::new(); + if let Some(m) = inventory.item(Area::Hotbar, hotbar_slot.get()) { + equipment.push(EquipmentEntry { + slot: MainHand, + item: m.clone(), + }) + }; + if let Some(m) = inventory.item(Area::Offhand, 0) { + equipment.push(EquipmentEntry { + slot: OffHand, + item: m.clone(), + }) + }; + if let Some(m) = inventory.item(Area::Boots, 0) { + equipment.push(EquipmentEntry { + slot: Boots, + item: m.clone(), + }) + }; + if let Some(m) = inventory.item(Area::Leggings, 0) { + equipment.push(EquipmentEntry { + slot: Leggings, + item: m.clone(), + }) + }; + if let Some(m) = inventory.item(Area::Chestplate, 0) { + equipment.push(EquipmentEntry { + slot: Chestplate, + item: m.clone(), + }) + }; + if let Some(m) = inventory.item(Area::Helmet, 0) { + equipment.push(EquipmentEntry { + slot: Helmet, + item: m.clone(), + }) + }; + + if equipment.is_empty() { + log::warn!("Could not get entity equipment"); + } + + self.send_packet(EntityEquipment::new(network_id.0, equipment)); + } + pub fn send_abilities(&self, abilities: &base::anvil::player::PlayerAbilities) { let mut bitfield = 0; if *abilities.invulnerable { diff --git a/feather/server/src/packet_handlers.rs b/feather/server/src/packet_handlers.rs index b0452c96a..7551cf6be 100644 --- a/feather/server/src/packet_handlers.rs +++ b/feather/server/src/packet_handlers.rs @@ -62,7 +62,7 @@ pub fn handle_packet( handle_player_block_placement(game, server, packet, player_id) } - ClientPlayPacket::HeldItemChange(packet) => handle_held_item_change(player, packet), + ClientPlayPacket::HeldItemChange(packet) => handle_held_item_change(server, player, packet), ClientPlayPacket::InteractEntity(packet) => { handle_interact_entity(game, server, packet, player_id) } diff --git a/feather/server/src/packet_handlers/interaction.rs b/feather/server/src/packet_handlers/interaction.rs index 1c6f69d9c..f813a27e6 100644 --- a/feather/server/src/packet_handlers/interaction.rs +++ b/feather/server/src/packet_handlers/interaction.rs @@ -1,5 +1,8 @@ use crate::{ClientId, NetworkId, Server}; -use base::inventory::{SLOT_HOTBAR_OFFSET, SLOT_OFFHAND}; +use base::{ + inventory::{SLOT_HOTBAR_OFFSET, SLOT_OFFHAND}, + Inventory, Position, +}; use common::entities::player::HotbarSlot; use common::interactable::InteractableRegistry; use common::{Game, Window}; @@ -224,36 +227,25 @@ pub fn handle_interact_entity( Ok(()) } -pub fn handle_held_item_change(player: EntityRef, packet: HeldItemChange) -> SysResult { +pub fn handle_held_item_change( + server: &mut Server, + player: EntityRef, + packet: HeldItemChange, +) -> SysResult { let new_id = packet.slot as usize; let mut slot = player.get_mut::()?; log::trace!("Got player slot change from {} to {}", slot.get(), new_id); slot.set(new_id)?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use common::Game; - use protocol::packets::client::HeldItemChange; - use super::*; + // Send an entity equipment packet to nearby players + let position = *player.get::()?; + let network_id = *player.get::()?; + let inventory = player.get::()?; + server.broadcast_nearby_with(position, |client| { + client.send_entity_equipment(network_id, &inventory, &slot); + }); - #[test] - fn held_item_change() { - let mut game = Game::new(); - let entity = game.ecs.spawn((HotbarSlot::new(0),)); - let player = game.ecs.entity(entity).unwrap(); - - let packet = HeldItemChange { slot: 8 }; - - handle_held_item_change(player, packet).unwrap(); - - assert_eq!( - *game.ecs.get::(entity).unwrap(), - HotbarSlot::new(8) - ); - } + Ok(()) } diff --git a/feather/server/src/packet_handlers/inventory.rs b/feather/server/src/packet_handlers/inventory.rs index 48a397197..9abb93993 100644 --- a/feather/server/src/packet_handlers/inventory.rs +++ b/feather/server/src/packet_handlers/inventory.rs @@ -1,10 +1,10 @@ use anyhow::bail; -use base::Gamemode; -use common::{window::BackingWindow, Window}; +use base::{Area, Gamemode, Inventory, Position}; +use common::{entities::player::HotbarSlot, window::BackingWindow, Window}; use ecs::{EntityRef, SysResult}; use protocol::packets::client::{ClickWindow, CreativeInventoryAction}; -use crate::{ClientId, Server}; +use crate::{ClientId, NetworkId, Server}; pub fn handle_creative_inventory_action( player: EntityRef, @@ -30,6 +30,35 @@ pub fn handle_creative_inventory_action( let client_id = *player.get::()?; let client = server.clients.get(client_id).unwrap(); client.send_window_items(&window); + + let visible_change = + if let Some((_, area, slot)) = window.inner().index_to_slot(packet.slot as usize) { + match area { + Area::Hotbar => { + let hotbar_slot = *player.get::()?; + hotbar_slot.get() == slot + } + Area::Helmet => true, + Area::Chestplate => true, + Area::Leggings => true, + Area::Boots => true, + Area::Offhand => true, + _ => false, + } + } else { + false + }; + + if visible_change { + // Send an entity equipment packet to nearby players + let position = *player.get::()?; + let network_id = *player.get::()?; + let inventory = player.get::()?; + let hotbar_slot = player.get::()?; + server.broadcast_nearby_with(position, |client| { + client.send_entity_equipment(network_id, &inventory, &hotbar_slot); + }); + } } Ok(()) @@ -67,7 +96,7 @@ fn _handle_click_window(player: &EntityRef, packet: &ClickWindow) -> SysResult { 0 => match packet.button { 0 => window.left_click(packet.slot as usize)?, 1 => window.right_click(packet.slot as usize)?, - _ => bail!("unrecgonized click"), + _ => bail!("unrecognized click"), }, 1 => window.shift_click(packet.slot as usize)?, 5 => match packet.button { diff --git a/feather/server/src/systems/entity/spawn_packet.rs b/feather/server/src/systems/entity/spawn_packet.rs index fcb4ea03a..bbe22c8a0 100644 --- a/feather/server/src/systems/entity/spawn_packet.rs +++ b/feather/server/src/systems/entity/spawn_packet.rs @@ -1,11 +1,13 @@ use ahash::AHashSet; use anyhow::Context; -use base::Position; +use base::{Inventory, Position}; use common::{ + entities::player::HotbarSlot, events::{ChunkCrossEvent, EntityCreateEvent, EntityRemoveEvent, ViewUpdateEvent}, Game, }; use ecs::{SysResult, SystemExecutor}; +use quill_common::entities::Player; use crate::{entities::SpawnPacketSender, ClientId, NetworkId, Server}; @@ -37,6 +39,18 @@ pub fn update_visible_entities(game: &mut Game, server: &mut Server) -> SysResul .send(&entity_ref, client) .context("failed to send spawn packet")?; } + + // If the current entity is a player, send an entity equipment packet to nearby players + // TODO this should also update other entities + if entity_ref.get::().is_ok() { + let position = *entity_ref.get::()?; + let network_id = *entity_ref.get::()?; + let inventory = entity_ref.get::()?; + let hotbar_slot = entity_ref.get::()?; + server.broadcast_nearby_with(position, |client| { + client.send_entity_equipment(network_id, &inventory, &hotbar_slot); + }); + } } } }