From 8bfe3ed57c52ca37987226ab40835792f6de6d1c Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sun, 13 Oct 2024 14:27:45 +0200 Subject: [PATCH 01/23] add `valence_equipment` --- Cargo.toml | 4 + crates/valence_equipment/Cargo.toml | 20 ++++ crates/valence_equipment/src/lib.rs | 170 ++++++++++++++++++++++++++++ examples/equipment.rs | 150 ++++++++++++++++++++++++ src/lib.rs | 7 ++ src/tests.rs | 1 + src/tests/equipment.rs | 96 ++++++++++++++++ 7 files changed, 448 insertions(+) create mode 100644 crates/valence_equipment/Cargo.toml create mode 100644 crates/valence_equipment/src/lib.rs create mode 100644 examples/equipment.rs create mode 100644 src/tests/equipment.rs diff --git a/Cargo.toml b/Cargo.toml index 1371871b4..237b5efb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ default = [ "advancement", "anvil", "boss_bar", + "equipment", "inventory", "log", "network", @@ -32,6 +33,7 @@ default = [ advancement = ["dep:valence_advancement"] anvil = ["dep:valence_anvil"] boss_bar = ["dep:valence_boss_bar"] +equipment = ["dep:valence_equipment", "dep:valence_inventory"] inventory = ["dep:valence_inventory"] log = ["dep:bevy_log"] network = ["dep:valence_network"] @@ -59,6 +61,7 @@ valence_command = { workspace = true, optional = true } valence_command_macros = { workspace = true, optional = true } valence_ident_macros.workspace = true valence_ident.workspace = true +valence_equipment = { workspace = true, optional = true } valence_inventory = { workspace = true, optional = true } valence_lang.workspace = true valence_network = { workspace = true, optional = true } @@ -192,6 +195,7 @@ valence_entity = { path = "crates/valence_entity", version = "0.2.0-alpha.1" } valence_generated = { path = "crates/valence_generated", version = "0.2.0-alpha.1" } valence_ident = { path = "crates/valence_ident", version = "0.2.0-alpha.1" } valence_ident_macros = { path = "crates/valence_ident_macros", version = "0.2.0-alpha.1" } +valence_equipment = { path = "crates/valence_equipment", version = "0.2.0-alpha.1" } valence_inventory = { path = "crates/valence_inventory", version = "0.2.0-alpha.1" } valence_lang = { path = "crates/valence_lang", version = "0.2.0-alpha.1" } valence_math = { path = "crates/valence_math", version = "0.2.0-alpha.1" } diff --git a/crates/valence_equipment/Cargo.toml b/crates/valence_equipment/Cargo.toml new file mode 100644 index 000000000..37e62d153 --- /dev/null +++ b/crates/valence_equipment/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "valence_equipment" +description = "Equipment syncing support for Valence" +readme = "README.md" +version.workspace = true +edition.workspace = true +repository.workspace = true +documentation.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +bevy_app.workspace = true +bevy_ecs.workspace = true +derive_more.workspace = true +tracing.workspace = true +valence_server.workspace = true +valence_inventory.workspace = true diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs new file mode 100644 index 000000000..31c63e264 --- /dev/null +++ b/crates/valence_equipment/src/lib.rs @@ -0,0 +1,170 @@ +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use valence_server::client::{Client, FlushPacketsSet, SpawnClientsSet}; +use valence_server::entity::EntityId; +use valence_server::protocol::packets::play::entity_equipment_update_s2c::EquipmentEntry; +use valence_server::protocol::packets::play::EntityEquipmentUpdateS2c; +use valence_server::protocol::WritePacket; +use valence_server::ItemStack; + +pub struct EquipmentPlugin; + +impl Plugin for EquipmentPlugin { + fn build(&self, app: &mut App) { + app.add_systems(PreUpdate, init_new_client_equipment.after(SpawnClientsSet)) + .add_systems( + PostUpdate, + (emit_equipment_change_event, update_equipment).before(FlushPacketsSet), + ) + .add_event::(); + } +} + +#[derive(Debug, Default, Clone, Component)] +pub struct Equipment { + equipment: [ItemStack; Self::SLOT_COUNT], + /// Contains a set bit for each modified slot in `slots`. + #[doc(hidden)] + pub(crate) changed: u8, +} + +impl Equipment { + pub const SLOT_COUNT: usize = 6; + + pub const MAIN_HAND_IDX: u8 = 0; + pub const OFF_HAND_IDX: u8 = 1; + pub const BOOTS_IDX: u8 = 2; + pub const LEGGINGS_IDX: u8 = 3; + pub const CHESTPLATE_IDX: u8 = 4; + pub const HELMET_IDX: u8 = 5; + + pub fn slot(&self, idx: u8) -> &ItemStack { + &self.equipment[idx as usize] + } + + pub fn set_slot(&mut self, idx: u8, item: ItemStack) { + assert!( + idx < Self::SLOT_COUNT as u8, + "slot index of {idx} out of bounds" + ); + if self.equipment[idx as usize] != item { + self.equipment[idx as usize] = item; + self.changed |= 1 << idx; + } + } + + pub fn main_hand(&self) -> &ItemStack { + self.slot(Self::MAIN_HAND_IDX) + } + + pub fn off_hand(&self) -> &ItemStack { + self.slot(Self::OFF_HAND_IDX) + } + + pub fn boots(&self) -> &ItemStack { + self.slot(Self::BOOTS_IDX) + } + + pub fn leggings(&self) -> &ItemStack { + self.slot(Self::LEGGINGS_IDX) + } + + pub fn chestplate(&self) -> &ItemStack { + self.slot(Self::CHESTPLATE_IDX) + } + + pub fn helmet(&self) -> &ItemStack { + self.slot(Self::HELMET_IDX) + } + + pub fn set_main_hand(&mut self, item: ItemStack) { + self.set_slot(Self::MAIN_HAND_IDX, item); + } + + pub fn set_off_hand(&mut self, item: ItemStack) { + self.set_slot(Self::OFF_HAND_IDX, item); + } + + pub fn set_boots(&mut self, item: ItemStack) { + self.set_slot(Self::BOOTS_IDX, item); + } + + pub fn set_leggings(&mut self, item: ItemStack) { + self.set_slot(Self::LEGGINGS_IDX, item); + } + + pub fn set_chestplate(&mut self, item: ItemStack) { + self.set_slot(Self::CHESTPLATE_IDX, item); + } + + pub fn set_helmet(&mut self, item: ItemStack) { + self.set_slot(Self::HELMET_IDX, item); + } + + pub fn clear(&mut self) { + for slot in 0..Self::SLOT_COUNT as u8 { + self.set_slot(slot, ItemStack::EMPTY); + } + } +} + +fn init_new_client_equipment(clients: Query>, mut commands: Commands) { + for entity in &clients { + commands.entity(entity).insert(Equipment::default()); + } +} + +#[derive(Debug, Clone, Event)] +pub struct EquipmentChangeEvent { + pub client: Entity, +} + +fn emit_equipment_change_event( + mut clients: Query<(Entity, &mut Equipment), Changed>, + mut event_writer: EventWriter, +) { + for (entity, mut equipment) in &mut clients { + if equipment.changed != 0 { + event_writer.send(EquipmentChangeEvent { client: entity }); + + equipment.changed = 0; + } + } +} + +fn update_equipment( + mut clients: Query<(&EntityId, Option<&mut Client>, &Equipment)>, + mut events: EventReader, +) { + for event in events.read() { + let Ok((entity_id, _, equipment)) = clients.get(event.client) else { + continue; + }; + + // The entity ID of the entity that changed equipment. + let entity_id_changed_equipment = entity_id.get(); + + let mut entries = Vec::with_capacity(Equipment::SLOT_COUNT); + for slot in 0..Equipment::SLOT_COUNT { + let item = equipment.slot(slot as u8); + entries.push(EquipmentEntry { + slot: slot as i8, + item: item.clone(), + }); + } + + for (entity_id, client, _) in &mut clients { + // Dont send the packet to the entity that changed equipment. + if entity_id.get() == entity_id_changed_equipment { + continue; + } + + if let Some(mut client) = client { + client.write_packet(&EntityEquipmentUpdateS2c { + entity_id: entity_id_changed_equipment.into(), + equipment: entries.clone(), + }); + } + } + } +} diff --git a/examples/equipment.rs b/examples/equipment.rs new file mode 100644 index 000000000..6fbbf222e --- /dev/null +++ b/examples/equipment.rs @@ -0,0 +1,150 @@ +#![allow(clippy::type_complexity)] + +const SPAWN_Y: i32 = 64; + +use rand::Rng; +use valence::entity::zombie::ZombieEntityBundle; +use valence::prelude::*; +use valence_inventory::player_inventory::PlayerInventory; +use valence_inventory::HeldItem; + +pub fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (despawn_disconnected_clients,)) + .add_systems( + Update, + ( + init_clients, + despawn_disconnected_clients, + randomize_equipment, + update_player_inventory, + ), + ) + .run(); +} + +fn setup( + mut commands: Commands, + server: Res, + dimensions: Res, + biomes: Res, +) { + let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + layer.chunk.insert_chunk([x, z], UnloadedChunk::new()); + } + } + + for z in -25..25 { + for x in -25..25 { + layer + .chunk + .set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + commands.spawn(layer); +} + +fn init_clients( + mut clients: Query< + ( + &mut Position, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, + mut commands: Commands, +) { + for ( + mut pos, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + pos.0 = [0.0, f64::from(SPAWN_Y) + 1.0, 0.0].into(); + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + *game_mode = GameMode::Survival; + + commands + .spawn(ZombieEntityBundle { + position: *pos, + layer: *layer_id, + ..Default::default() + }) + .insert(Equipment::default()); + } +} + +fn randomize_equipment(mut query: Query<&mut Equipment>, server: Res) { + let ticks = server.current_tick() as u32; + // every second + if ticks % server.tick_rate() != 0 { + return; + } + + for mut equipment in &mut query { + equipment.clear(); + + tracing::info!("Randomizing equipment for entity"); + + let (slot, item_stack) = match rand::thread_rng().gen_range(0..=5) { + 0 => ( + Equipment::MAIN_HAND_IDX, + ItemStack::new(ItemKind::DiamondSword, 1, None), + ), + 1 => ( + Equipment::OFF_HAND_IDX, + ItemStack::new(ItemKind::Shield, 1, None), + ), + 2 => ( + Equipment::BOOTS_IDX, + ItemStack::new(ItemKind::DiamondBoots, 1, None), + ), + 3 => ( + Equipment::LEGGINGS_IDX, + ItemStack::new(ItemKind::DiamondLeggings, 1, None), + ), + 4 => ( + Equipment::CHESTPLATE_IDX, + ItemStack::new(ItemKind::DiamondChestplate, 1, None), + ), + 5 => ( + Equipment::HELMET_IDX, + ItemStack::new(ItemKind::DiamondHelmet, 1, None), + ), + _ => unreachable!(), + }; + + equipment.set_slot(slot, item_stack); + } +} + +/// Updating the Equipment will only be visible for other players, +/// so we need to update the player's inventory as well. +fn update_player_inventory( + mut clients: Query<(&mut Inventory, &Equipment, &HeldItem), Changed>, +) { + for (mut inv, equipment, held_item) in &mut clients { + inv.set_slot(PlayerInventory::SLOT_HEAD, equipment.helmet().clone()); + inv.set_slot(PlayerInventory::SLOT_CHEST, equipment.chestplate().clone()); + inv.set_slot(PlayerInventory::SLOT_LEGS, equipment.leggings().clone()); + inv.set_slot(PlayerInventory::SLOT_FEET, equipment.boots().clone()); + inv.set_slot(PlayerInventory::SLOT_OFFHAND, equipment.off_hand().clone()); + inv.set_slot(held_item.slot(), equipment.main_hand().clone()); + } +} diff --git a/src/lib.rs b/src/lib.rs index d8049a5cf..7c651f4c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,6 +108,8 @@ pub mod prelude { event::AdvancementTabChangeEvent, Advancement, AdvancementBundle, AdvancementClientUpdate, AdvancementCriteria, AdvancementDisplay, AdvancementFrameType, AdvancementRequirements, }; + #[cfg(feature = "equipment")] + pub use valence_equipment::Equipment; #[cfg(feature = "inventory")] pub use valence_inventory::{ CursorItem, Inventory, InventoryKind, InventoryWindow, InventoryWindowMut, OpenInventory, @@ -214,6 +216,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_player_list::PlayerListPlugin) } + #[cfg(feature = "equipment")] + { + group = group.add(valence_equipment::EquipmentPlugin) + } + #[cfg(feature = "inventory")] { group = group.add(valence_inventory::InventoryPlugin) diff --git a/src/tests.rs b/src/tests.rs index e82971210..f1a916a05 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,5 +1,6 @@ mod boss_bar; mod client; +mod equipment; mod example; mod hunger; mod inventory; diff --git a/src/tests/equipment.rs b/src/tests/equipment.rs new file mode 100644 index 000000000..9bf87322c --- /dev/null +++ b/src/tests/equipment.rs @@ -0,0 +1,96 @@ +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use valence_equipment::Equipment; +use valence_server::entity::zombie::ZombieEntityBundle; +use valence_server::protocol::packets::play::EntityEquipmentUpdateS2c; +use valence_server::{ItemKind, ItemStack}; + +use crate::testing::ScenarioSingleClient; + +#[test] +fn test_only_send_update_to_other_players() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + let mut player_equipment = app + .world_mut() + .get_mut::(client) + .expect("could not get player equipment"); + + player_equipment.set_chestplate(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + + app.update(); + + // Make assertions + let sent_packets = helper.collect_received(); + + // We only have one player, so we should not have sent any packets. + sent_packets.assert_count::(0); +} + +#[test] +fn test_multiple_entities() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + let zombie_bundle = ZombieEntityBundle { + ..Default::default() + }; + + let zombie = app.world_mut().spawn(zombie_bundle).id(); + + // add equipment to the zombie,its not attached to the zombie by default + app.world_mut() + .commands() + .entity(zombie) + .insert(Equipment::default()); + + app.update(); + + let mut equipment = app + .world_mut() + .get_mut::(zombie) + .expect("could not get entity equipment"); + + equipment.set_chestplate(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + equipment.set_helmet(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); + + app.update(); + + // Make assertions + let sent_packets = helper.collect_received(); + sent_packets.assert_count::(1); + + helper.clear_received(); + + let mut equipment = app + .world_mut() + .get_mut::(zombie) + .expect("could not get entity equipment"); + + // Set the zombie's equipment to the same items + equipment.set_chestplate(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + equipment.set_helmet(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); + + app.update(); + + // Make assertions + let sent_packets = helper.collect_received(); + sent_packets.assert_count::(0); +} From 17c469d64a0069810e19f4b4b116a94e21266ee8 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sun, 13 Oct 2024 14:40:18 +0200 Subject: [PATCH 02/23] redo event --- crates/valence_equipment/src/lib.rs | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index 31c63e264..1a21d801b 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -114,9 +114,16 @@ fn init_new_client_equipment(clients: Query>, mut commands } } +#[derive(Debug, Clone)] +pub struct EquipmentSlotChange { + idx: u8, + stack: ItemStack, +} + #[derive(Debug, Clone, Event)] pub struct EquipmentChangeEvent { pub client: Entity, + pub changed: Vec, } fn emit_equipment_change_event( @@ -125,7 +132,22 @@ fn emit_equipment_change_event( ) { for (entity, mut equipment) in &mut clients { if equipment.changed != 0 { - event_writer.send(EquipmentChangeEvent { client: entity }); + let mut slots_changed: Vec = + Vec::with_capacity(Equipment::SLOT_COUNT); + + for slot in 0..Equipment::SLOT_COUNT { + if equipment.changed & (1 << slot) != 0 { + slots_changed.push(EquipmentSlotChange { + idx: slot as u8, + stack: equipment.equipment[slot].clone(), + }); + } + } + + event_writer.send(EquipmentChangeEvent { + client: entity, + changed: slots_changed, + }); equipment.changed = 0; } @@ -133,27 +155,26 @@ fn emit_equipment_change_event( } fn update_equipment( - mut clients: Query<(&EntityId, Option<&mut Client>, &Equipment)>, + mut clients: Query<(&EntityId, Option<&mut Client>)>, mut events: EventReader, ) { for event in events.read() { - let Ok((entity_id, _, equipment)) = clients.get(event.client) else { + let Ok((entity_id, _)) = clients.get(event.client) else { continue; }; // The entity ID of the entity that changed equipment. let entity_id_changed_equipment = entity_id.get(); - let mut entries = Vec::with_capacity(Equipment::SLOT_COUNT); - for slot in 0..Equipment::SLOT_COUNT { - let item = equipment.slot(slot as u8); + let mut entries: Vec = Vec::with_capacity(event.changed.len()); + for change in &event.changed { entries.push(EquipmentEntry { - slot: slot as i8, - item: item.clone(), + slot: change.idx as i8, + item: change.stack.clone(), }); } - for (entity_id, client, _) in &mut clients { + for (entity_id, client) in &mut clients { // Dont send the packet to the entity that changed equipment. if entity_id.get() == entity_id_changed_equipment { continue; From 65396e2c419ed1791d396c71b64680fc9170610c Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sun, 13 Oct 2024 14:44:36 +0200 Subject: [PATCH 03/23] remove logging --- examples/equipment.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/equipment.rs b/examples/equipment.rs index 6fbbf222e..dae8d444a 100644 --- a/examples/equipment.rs +++ b/examples/equipment.rs @@ -100,8 +100,6 @@ fn randomize_equipment(mut query: Query<&mut Equipment>, server: Res) { for mut equipment in &mut query { equipment.clear(); - tracing::info!("Randomizing equipment for entity"); - let (slot, item_stack) = match rand::thread_rng().gen_range(0..=5) { 0 => ( Equipment::MAIN_HAND_IDX, From d44b907abbc11aab79ac95f5ae2273981d148565 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Mon, 14 Oct 2024 00:51:00 +0200 Subject: [PATCH 04/23] add entity (un)load event --- crates/valence_equipment/src/lib.rs | 106 +++++++++++++--- crates/valence_server/src/client.rs | 97 ++++++++++++++- examples/equipment.rs | 24 ++-- src/tests/equipment.rs | 180 ++++++++++++++++++++++++++-- 4 files changed, 370 insertions(+), 37 deletions(-) diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index 1a21d801b..309570a27 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -1,22 +1,33 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; -use valence_server::client::{Client, FlushPacketsSet, SpawnClientsSet}; -use valence_server::entity::EntityId; +use valence_server::client::{Client, FlushPacketsSet, LoadEntityForClientEvent, SpawnClientsSet}; +use valence_server::entity::living::LivingEntity; +use valence_server::entity::{EntityId, EntityLayerId, Position}; use valence_server::protocol::packets::play::entity_equipment_update_s2c::EquipmentEntry; use valence_server::protocol::packets::play::EntityEquipmentUpdateS2c; use valence_server::protocol::WritePacket; -use valence_server::ItemStack; +use valence_server::{EntityLayer, ItemStack, Layer}; pub struct EquipmentPlugin; impl Plugin for EquipmentPlugin { fn build(&self, app: &mut App) { - app.add_systems(PreUpdate, init_new_client_equipment.after(SpawnClientsSet)) - .add_systems( - PostUpdate, - (emit_equipment_change_event, update_equipment).before(FlushPacketsSet), - ) - .add_event::(); + app.add_systems( + PreUpdate, + ( + init_new_client_equipment.after(SpawnClientsSet), + on_entity_init, + ), + ) + .add_systems( + Update, + ( + update_equipment.before(FlushPacketsSet), + emit_equipment_change_event, + on_entity_load.before(FlushPacketsSet), + ), + ) + .add_event::(); } } @@ -38,6 +49,13 @@ impl Equipment { pub const CHESTPLATE_IDX: u8 = 4; pub const HELMET_IDX: u8 = 5; + pub fn new(equipment: [ItemStack; Self::SLOT_COUNT]) -> Self { + Self { + equipment, + changed: u8::MAX, + } + } + pub fn slot(&self, idx: u8) -> &ItemStack { &self.equipment[idx as usize] } @@ -106,6 +124,10 @@ impl Equipment { self.set_slot(slot, ItemStack::EMPTY); } } + + pub fn is_default(&self) -> bool { + self.equipment.iter().all(|item| item.is_empty()) + } } fn init_new_client_equipment(clients: Query>, mut commands: Commands) { @@ -155,16 +177,22 @@ fn emit_equipment_change_event( } fn update_equipment( - mut clients: Query<(&EntityId, Option<&mut Client>)>, + mut clients: Query<(&EntityId, &EntityLayerId, &Position)>, + mut entity_layer_query: Query<&mut EntityLayer>, mut events: EventReader, ) { for event in events.read() { - let Ok((entity_id, _)) = clients.get(event.client) else { + let Ok((entity_id, entity_layer_id, position)) = clients.get(event.client) else { + continue; + }; + + let Ok(mut entity_layer) = entity_layer_query.get_mut(entity_layer_id.0) else { continue; }; // The entity ID of the entity that changed equipment. let entity_id_changed_equipment = entity_id.get(); + let entity_pos_changed_equipment = *position; let mut entries: Vec = Vec::with_capacity(event.changed.len()); for change in &event.changed { @@ -174,18 +202,64 @@ fn update_equipment( }); } - for (entity_id, client) in &mut clients { + for (entity_id, _, _) in &mut clients { // Dont send the packet to the entity that changed equipment. if entity_id.get() == entity_id_changed_equipment { continue; } - if let Some(mut client) = client { - client.write_packet(&EntityEquipmentUpdateS2c { + entity_layer + .view_writer(entity_pos_changed_equipment.0) + .write_packet(&EntityEquipmentUpdateS2c { entity_id: entity_id_changed_equipment.into(), equipment: entries.clone(), - }); - } + }) + } + } +} + +/// Gets called when the player loads an entity, for example +/// when the player gets in range of the entity. +fn on_entity_load( + mut clients: Query<&mut Client>, + entities: Query<(&EntityId, &Equipment)>, + mut events: EventReader, +) { + for event in events.read() { + let Ok(mut client) = clients.get_mut(event.client) else { + continue; + }; + + let Ok((entity_id, equipment)) = entities.get(event.entity_loaded) else { + continue; + }; + + if equipment.is_default() { + continue; + } + + let mut entries: Vec = Vec::with_capacity(Equipment::SLOT_COUNT); + for (idx, stack) in equipment.equipment.iter().enumerate() { + entries.push(EquipmentEntry { + slot: idx as i8, + item: stack.clone(), + }); } + + client.write_packet(&EntityEquipmentUpdateS2c { + entity_id: entity_id.get().into(), + equipment: entries, + }); + } +} + +/// Add a default equipment component to all living entities when they are +/// initialized. +fn on_entity_init( + mut commands: Commands, + mut entities: Query, Without)>, +) { + for entity in &mut entities { + commands.entity(entity).insert(Equipment::default()); } } diff --git a/crates/valence_server/src/client.rs b/crates/valence_server/src/client.rs index e4dbf41e7..39da06e77 100644 --- a/crates/valence_server/src/client.rs +++ b/crates/valence_server/src/client.rs @@ -100,7 +100,9 @@ impl Plugin for ClientPlugin { ClearEntityChangesSet.after(UpdateClientsSet), FlushPacketsSet, ), - ); + ) + .add_event::() + .add_event::(); } } @@ -878,6 +880,26 @@ fn handle_layer_messages( ); } +/// This event will be emitted when a entity is unloaded for a client (e.g when +/// moving out of range of the entity). +#[derive(Debug, Clone, PartialEq, Event)] +pub struct UnloadEntityForClientEvent { + /// The client to unload the entity for. + pub client: Entity, + /// The entity ID of the entity that will be unloaded. + pub entity_unloaded: Entity, +} + +/// This event will be emitted when a entity is loaded for a client (e.g when +/// moving into range of the entity). +#[derive(Debug, Clone, PartialEq, Event)] +pub struct LoadEntityForClientEvent { + /// The client to load the entity for. + pub client: Entity, + /// The entity that will be loaded. + pub entity_loaded: Entity, +} + pub(crate) fn update_view_and_layers( mut clients: Query< ( @@ -904,8 +926,19 @@ pub(crate) fn update_view_and_layers( entity_layers: Query<&EntityLayer>, entity_ids: Query<&EntityId>, entity_init: Query<(EntityInitQuery, &Position)>, + + mut unload_entity_writer: EventWriter, + mut load_entity_writer: EventWriter, ) { - clients.par_iter_mut().for_each( + // Wrap the events in this, so we only need one channel. + enum ChannelEvent { + UnloadEntity(UnloadEntityForClientEvent), + LoadEntity(LoadEntityForClientEvent), + } + + let (tx, rx) = std::sync::mpsc::channel(); + + (clients).par_iter_mut().for_each( |( self_entity, mut client, @@ -962,6 +995,14 @@ pub(crate) fn update_view_and_layers( for entity in layer.entities_at(pos) { if self_entity != entity { if let Ok(id) = entity_ids.get(entity) { + tx.send(ChannelEvent::UnloadEntity( + UnloadEntityForClientEvent { + client: self_entity, + entity_unloaded: entity, + }, + )) + .unwrap(); + remove_buf.push(id.get()); } } @@ -979,6 +1020,14 @@ pub(crate) fn update_view_and_layers( for entity in layer.entities_at(pos) { if self_entity != entity { if let Ok((init, pos)) = entity_init.get(entity) { + tx.send(ChannelEvent::LoadEntity( + LoadEntityForClientEvent { + client: self_entity, + entity_loaded: entity, + }, + )) + .unwrap(); + init.write_init_packets(pos.get(), &mut *client); } } @@ -999,6 +1048,14 @@ pub(crate) fn update_view_and_layers( for entity in layer.entities_at(pos) { if self_entity != entity { if let Ok(id) = entity_ids.get(entity) { + tx.send(ChannelEvent::UnloadEntity( + UnloadEntityForClientEvent { + client: self_entity, + entity_unloaded: entity, + }, + )) + .unwrap(); + remove_buf.push(id.get()); } } @@ -1019,6 +1076,14 @@ pub(crate) fn update_view_and_layers( for entity in layer.entities_at(pos) { if self_entity != entity { if let Ok((init, pos)) = entity_init.get(entity) { + tx.send(ChannelEvent::LoadEntity( + LoadEntityForClientEvent { + client: self_entity, + entity_loaded: entity, + }, + )) + .unwrap(); + init.write_init_packets(pos.get(), &mut *client); } } @@ -1061,6 +1126,14 @@ pub(crate) fn update_view_and_layers( for entity in layer.entities_at(pos) { if self_entity != entity { if let Ok(id) = entity_ids.get(entity) { + tx.send(ChannelEvent::UnloadEntity( + UnloadEntityForClientEvent { + client: self_entity, + entity_unloaded: entity, + }, + )) + .unwrap(); + remove_buf.push(id.get()); } } @@ -1076,6 +1149,14 @@ pub(crate) fn update_view_and_layers( for entity in layer.entities_at(pos) { if self_entity != entity { if let Ok((init, pos)) = entity_init.get(entity) { + tx.send(ChannelEvent::LoadEntity( + LoadEntityForClientEvent { + client: self_entity, + entity_loaded: entity, + }, + )) + .unwrap(); + init.write_init_packets(pos.get(), &mut *client); } } @@ -1097,6 +1178,18 @@ pub(crate) fn update_view_and_layers( } }, ); + + // Send the events. + for event in rx.try_iter() { + match event { + ChannelEvent::UnloadEntity(event) => { + unload_entity_writer.send(event); + } + ChannelEvent::LoadEntity(event) => { + load_entity_writer.send(event); + } + }; + } } pub(crate) fn update_game_mode(mut clients: Query<(&mut Client, &GameMode), Changed>) { diff --git a/examples/equipment.rs b/examples/equipment.rs index dae8d444a..504220612 100644 --- a/examples/equipment.rs +++ b/examples/equipment.rs @@ -3,6 +3,7 @@ const SPAWN_Y: i32 = 64; use rand::Rng; +use valence::entity::armor_stand::ArmorStandEntityBundle; use valence::entity::zombie::ZombieEntityBundle; use valence::prelude::*; use valence_inventory::player_inventory::PlayerInventory; @@ -47,7 +48,19 @@ fn setup( } } - commands.spawn(layer); + let layer_id = commands.spawn(layer).id(); + + commands.spawn(ZombieEntityBundle { + position: Position::new(DVec3::new(0.0, f64::from(SPAWN_Y) + 1.0, 0.0)), + layer: EntityLayerId(layer_id), + ..Default::default() + }); + + commands.spawn(ArmorStandEntityBundle { + position: Position::new(DVec3::new(1.0, f64::from(SPAWN_Y) + 1.0, 0.0)), + layer: EntityLayerId(layer_id), + ..Default::default() + }); } fn init_clients( @@ -62,7 +75,6 @@ fn init_clients( Added, >, layers: Query, With)>, - mut commands: Commands, ) { for ( mut pos, @@ -79,14 +91,6 @@ fn init_clients( visible_chunk_layer.0 = layer; visible_entity_layers.0.insert(layer); *game_mode = GameMode::Survival; - - commands - .spawn(ZombieEntityBundle { - position: *pos, - layer: *layer_id, - ..Default::default() - }) - .insert(Equipment::default()); } } diff --git a/src/tests/equipment.rs b/src/tests/equipment.rs index 9bf87322c..4644582e1 100644 --- a/src/tests/equipment.rs +++ b/src/tests/equipment.rs @@ -1,7 +1,9 @@ -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; use valence_equipment::Equipment; +use valence_server::entity::armor_stand::ArmorStandEntityBundle; +use valence_server::entity::item::ItemEntityBundle; use valence_server::entity::zombie::ZombieEntityBundle; +use valence_server::entity::{EntityLayerId, Position}; +use valence_server::math::DVec3; use valence_server::protocol::packets::play::EntityEquipmentUpdateS2c; use valence_server::{ItemKind, ItemStack}; @@ -40,8 +42,8 @@ fn test_only_send_update_to_other_players() { fn test_multiple_entities() { let ScenarioSingleClient { mut app, - client, mut helper, + layer, .. } = ScenarioSingleClient::new(); @@ -50,17 +52,12 @@ fn test_multiple_entities() { helper.clear_received(); let zombie_bundle = ZombieEntityBundle { + layer: EntityLayerId(layer), ..Default::default() }; let zombie = app.world_mut().spawn(zombie_bundle).id(); - // add equipment to the zombie,its not attached to the zombie by default - app.world_mut() - .commands() - .entity(zombie) - .insert(Equipment::default()); - app.update(); let mut equipment = app @@ -94,3 +91,168 @@ fn test_multiple_entities() { let sent_packets = helper.collect_received(); sent_packets.assert_count::(0); } + +#[test] +fn test_update_on_load_entity() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + let zombie_bundle = ZombieEntityBundle { + layer: EntityLayerId(layer), + position: Position::new(DVec3::new(1000.0, 0.0, 1000.0)), + ..Default::default() + }; + + let zombie = app + .world_mut() + .spawn(zombie_bundle) + .insert(Equipment::default()) + .id(); + + app.update(); + + let mut equipment = app + .world_mut() + .get_mut::(zombie) + .expect("could not get entity equipment"); + + equipment.set_chestplate(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + equipment.set_helmet(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); + + app.update(); + + // Make assertions + let sent_packets = helper.collect_received(); + // The zombie is not in range of the player + sent_packets.assert_count::(0); + + // Move the player to the zombie + let mut player_pos = app + .world_mut() + .get_mut::(client) + .expect("could not get player position"); + + player_pos.0 = DVec3::new(1000.0, 0.0, 1000.0); + + // 1 tick for the tp, 1 tick for loading the entity (i think) + app.update(); + app.update(); + + // Make assertions + let sent_packets = helper.collect_received(); + // Once the player is in range, we send the equipment update + sent_packets.assert_count::(1); +} + +#[test] +fn test_skip_update_for_empty_equipment() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer, + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + let zombie_bundle = ZombieEntityBundle { + layer: EntityLayerId(layer), + position: Position::new(DVec3::new(1000.0, 0.0, 1000.0)), + ..Default::default() + }; + + app.world_mut() + .spawn(zombie_bundle) + .insert(Equipment::default()); + + app.update(); + app.update(); + + // Make assertions + let sent_packets = helper.collect_received(); + // The zombie is not in range of the player + sent_packets.assert_count::(0); + + // Move the player to the zombie + let mut player_pos = app + .world_mut() + .get_mut::(client) + .expect("could not get player position"); + + player_pos.0 = DVec3::new(1000.0, 0.0, 1000.0); + + // 1 tick for the tp, 1 tick for loading the entity (i think) + app.update(); + app.update(); + + // Make assertions + let sent_packets = helper.collect_received(); + // We skip the packet, because the equipment is empty + sent_packets.assert_count::(0); +} + +#[test] +fn test_ensure_living_entities_only() { + let ScenarioSingleClient { + mut app, + mut helper, + layer, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + let zombie_bundle = ZombieEntityBundle { + layer: EntityLayerId(layer), + ..Default::default() + }; + + let armor_stand_bundle = ArmorStandEntityBundle { + layer: EntityLayerId(layer), + ..Default::default() + }; + + let item_bundle = ItemEntityBundle { + layer: EntityLayerId(layer), + ..Default::default() + }; + + let zombie = app + .world_mut() + .spawn(zombie_bundle) + .insert(Equipment::default()) + .id(); + let armor_stand = app + .world_mut() + .spawn(armor_stand_bundle) + .insert(Equipment::default()) + .id(); + let item = app + .world_mut() + .spawn(item_bundle) + .insert(Equipment::default()) + .id(); + + app.update(); + + let zombie_equipment = app.world_mut().get_mut::(zombie); + assert!(zombie_equipment.is_some()); + + let armor_stand_equipment = app.world_mut().get_mut::(armor_stand); + assert!(armor_stand_equipment.is_some()); + + let item_equipment = app.world_mut().get_mut::(item); + assert!(item_equipment.is_none()); +} From 8b27d179923efc38f74c33be05c6b85d696a3dd7 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Tue, 15 Oct 2024 19:06:26 +0200 Subject: [PATCH 05/23] refactor + depgraph --- assets/depgraph.svg | 362 ++++++++++++++-------------- crates/valence_equipment/src/lib.rs | 115 ++++----- src/tests/equipment.rs | 20 +- 3 files changed, 236 insertions(+), 261 deletions(-) diff --git a/assets/depgraph.svg b/assets/depgraph.svg index 8060e3d12..0a9174593 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -4,16 +4,16 @@ - - + + %3 - + 0 - -java_string + +java_string @@ -24,140 +24,140 @@ 2 - -valence_server + +valence_server 1->2 - - + + 3 - -valence_entity + +valence_entity 2->3 - - + + 12 - -valence_registry + +valence_registry 2->12 - - + + 11 - -valence_server_common + +valence_server_common 3->11 - - + + 12->11 - - + + 9 - -valence_protocol + +valence_protocol 11->9 - - + + 4 - -valence_generated + +valence_generated 5 - -valence_ident + +valence_ident 4->5 - - + + 6 - -valence_math + +valence_math 4->6 - - + + 7 - -valence_build_utils + +valence_build_utils 8 - -valence_nbt + +valence_nbt 9->4 - - + + 10 - -valence_text + +valence_text 9->10 - - + + 10->5 - - + + 10->8 - - + + @@ -168,8 +168,8 @@ 13->2 - - + + @@ -180,8 +180,8 @@ 14->2 - - + + @@ -192,212 +192,224 @@ 15->2 - - + + 16 + +valence_equipment + + + +17 valence_inventory - + -16->2 - - +16->17 + + - - -17 - -valence_lang + + +17->2 + + 18 - -valence_network - - - -18->2 - - - - - -18->17 - - + +valence_lang 19 - -valence_player_list + +valence_network - + 19->2 - - + + + + + +19->18 + + 20 - -valence_scoreboard + +valence_player_list 20->2 - - + + 21 - -valence_spatial + +valence_scoreboard + + + +21->2 + + 22 - -valence_weather - - - -22->2 - - + +valence_spatial 23 - -valence_world_border + +valence_weather 23->2 - - + + 24 - -dump_schedule + +valence_world_border + + + +24->2 + + 25 - -valence + +dump_schedule - - -24->25 - - + + +26 + +valence - + -25->1 - - +25->26 + + - + -25->13 - - +26->1 + + - + -25->14 - - +26->13 + + - + -25->15 - - +26->14 + + - + -25->16 - - +26->15 + + - + -25->18 - - +26->16 + + - + -25->19 - - +26->19 + + - + -25->20 - - +26->20 + + - + -25->22 - - +26->21 + + - + -25->23 - - - - - -26 - -packet_inspector +26->23 + + - + -26->9 - - +26->24 + + 27 - -playground + +packet_inspector - + -27->25 - - +27->9 + + 28 - -stresser + +playground - + -28->9 - - +28->26 + + + + + +29 + +stresser + + + +29->9 + + diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index 309570a27..abf740137 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -1,6 +1,6 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; -use valence_server::client::{Client, FlushPacketsSet, LoadEntityForClientEvent, SpawnClientsSet}; +use valence_server::client::{Client, FlushPacketsSet, LoadEntityForClientEvent}; use valence_server::entity::living::LivingEntity; use valence_server::entity::{EntityId, EntityLayerId, Position}; use valence_server::protocol::packets::play::entity_equipment_update_s2c::EquipmentEntry; @@ -12,22 +12,15 @@ pub struct EquipmentPlugin; impl Plugin for EquipmentPlugin { fn build(&self, app: &mut App) { - app.add_systems( - PreUpdate, - ( - init_new_client_equipment.after(SpawnClientsSet), - on_entity_init, - ), - ) - .add_systems( - Update, - ( - update_equipment.before(FlushPacketsSet), - emit_equipment_change_event, - on_entity_load.before(FlushPacketsSet), - ), - ) - .add_event::(); + app.add_systems(PreUpdate, (on_entity_init,)) + .add_systems( + Update, + ( + update_equipment.before(FlushPacketsSet), + on_entity_load.before(FlushPacketsSet), + ), + ) + .add_event::(); } } @@ -49,10 +42,17 @@ impl Equipment { pub const CHESTPLATE_IDX: u8 = 4; pub const HELMET_IDX: u8 = 5; - pub fn new(equipment: [ItemStack; Self::SLOT_COUNT]) -> Self { + pub fn new( + main_hand: ItemStack, + off_hand: ItemStack, + boots: ItemStack, + leggings: ItemStack, + chestplate: ItemStack, + helmet: ItemStack, + ) -> Self { Self { - equipment, - changed: u8::MAX, + equipment: [main_hand, off_hand, boots, leggings, chestplate, helmet], + changed: 0, } } @@ -130,12 +130,6 @@ impl Equipment { } } -fn init_new_client_equipment(clients: Query>, mut commands: Commands) { - for entity in &clients { - commands.entity(entity).insert(Equipment::default()); - } -} - #[derive(Debug, Clone)] pub struct EquipmentSlotChange { idx: u8, @@ -148,11 +142,19 @@ pub struct EquipmentChangeEvent { pub changed: Vec, } -fn emit_equipment_change_event( - mut clients: Query<(Entity, &mut Equipment), Changed>, +fn update_equipment( + mut clients: Query< + (Entity, &EntityId, &EntityLayerId, &Position, &mut Equipment), + Changed, + >, mut event_writer: EventWriter, + mut entity_layer: Query<&mut EntityLayer>, ) { - for (entity, mut equipment) in &mut clients { + for (entity, entity_id, entity_layer_id, position, mut equipment) in &mut clients { + let Ok(mut entity_layer) = entity_layer.get_mut(entity_layer_id.0) else { + continue; + }; + if equipment.changed != 0 { let mut slots_changed: Vec = Vec::with_capacity(Equipment::SLOT_COUNT); @@ -166,6 +168,19 @@ fn emit_equipment_change_event( } } + entity_layer + .view_except_writer(position.0, entity) + .write_packet(&EntityEquipmentUpdateS2c { + entity_id: entity_id.get().into(), + equipment: slots_changed + .iter() + .map(|change| EquipmentEntry { + slot: change.idx as i8, + item: change.stack.clone(), + }) + .collect(), + }); + event_writer.send(EquipmentChangeEvent { client: entity, changed: slots_changed, @@ -176,48 +191,6 @@ fn emit_equipment_change_event( } } -fn update_equipment( - mut clients: Query<(&EntityId, &EntityLayerId, &Position)>, - mut entity_layer_query: Query<&mut EntityLayer>, - mut events: EventReader, -) { - for event in events.read() { - let Ok((entity_id, entity_layer_id, position)) = clients.get(event.client) else { - continue; - }; - - let Ok(mut entity_layer) = entity_layer_query.get_mut(entity_layer_id.0) else { - continue; - }; - - // The entity ID of the entity that changed equipment. - let entity_id_changed_equipment = entity_id.get(); - let entity_pos_changed_equipment = *position; - - let mut entries: Vec = Vec::with_capacity(event.changed.len()); - for change in &event.changed { - entries.push(EquipmentEntry { - slot: change.idx as i8, - item: change.stack.clone(), - }); - } - - for (entity_id, _, _) in &mut clients { - // Dont send the packet to the entity that changed equipment. - if entity_id.get() == entity_id_changed_equipment { - continue; - } - - entity_layer - .view_writer(entity_pos_changed_equipment.0) - .write_packet(&EntityEquipmentUpdateS2c { - entity_id: entity_id_changed_equipment.into(), - equipment: entries.clone(), - }) - } - } -} - /// Gets called when the player loads an entity, for example /// when the player gets in range of the entity. fn on_entity_load( diff --git a/src/tests/equipment.rs b/src/tests/equipment.rs index 4644582e1..b8a942181 100644 --- a/src/tests/equipment.rs +++ b/src/tests/equipment.rs @@ -229,21 +229,11 @@ fn test_ensure_living_entities_only() { ..Default::default() }; - let zombie = app - .world_mut() - .spawn(zombie_bundle) - .insert(Equipment::default()) - .id(); - let armor_stand = app - .world_mut() - .spawn(armor_stand_bundle) - .insert(Equipment::default()) - .id(); - let item = app - .world_mut() - .spawn(item_bundle) - .insert(Equipment::default()) - .id(); + let zombie = app.world_mut().spawn(zombie_bundle).id(); + + let armor_stand = app.world_mut().spawn(armor_stand_bundle).id(); + + let item = app.world_mut().spawn(item_bundle).id(); app.update(); From 4ca80bc1bbf98ac65a9c44d5d98fcb9c542b28c4 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:20:37 +0200 Subject: [PATCH 06/23] add use equipment to lib.rs --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 7c651f4c4..a8e7d12df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,8 @@ pub use valence_boss_bar as boss_bar; pub use valence_command as command; #[cfg(feature = "command")] pub use valence_command_macros as command_macros; +#[cfg(feature = "equipment")] +pub use valence_equipment as equipment; #[cfg(feature = "inventory")] pub use valence_inventory as inventory; pub use valence_lang as lang; From afc0f2ab2ec8d9e4195c15d2b4f2fecc5bc4a790 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:43:40 +0200 Subject: [PATCH 07/23] remove unused valence_inventory dep --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 237b5efb6..aea1cee38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ default = [ advancement = ["dep:valence_advancement"] anvil = ["dep:valence_anvil"] boss_bar = ["dep:valence_boss_bar"] -equipment = ["dep:valence_equipment", "dep:valence_inventory"] +equipment = ["dep:valence_equipment"] inventory = ["dep:valence_inventory"] log = ["dep:bevy_log"] network = ["dep:valence_network"] From c73588777a9958e742f7804d8b89a4002f49ab2b Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 16 Oct 2024 00:07:39 +0200 Subject: [PATCH 08/23] remove inventory dep frfr --- crates/valence_equipment/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/valence_equipment/Cargo.toml b/crates/valence_equipment/Cargo.toml index 37e62d153..2193ffa59 100644 --- a/crates/valence_equipment/Cargo.toml +++ b/crates/valence_equipment/Cargo.toml @@ -17,4 +17,3 @@ bevy_ecs.workspace = true derive_more.workspace = true tracing.workspace = true valence_server.workspace = true -valence_inventory.workspace = true From c96634bb9a588d79570f27708c265d9a1324eab2 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 16 Oct 2024 00:30:15 +0200 Subject: [PATCH 09/23] update depgraph --- assets/depgraph.svg | 290 ++++++++++++++++++++++---------------------- 1 file changed, 148 insertions(+), 142 deletions(-) diff --git a/assets/depgraph.svg b/assets/depgraph.svg index 0a9174593..b03e2a5a8 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -4,16 +4,16 @@ - - + + %3 - + 0 - -java_string + +java_string @@ -24,140 +24,140 @@ 2 - -valence_server + +valence_server 1->2 - - + + 3 - -valence_entity + +valence_entity 2->3 - - + + 12 - -valence_registry + +valence_registry 2->12 - - + + 11 - -valence_server_common + +valence_server_common 3->11 - - + + 12->11 - - + + 9 - -valence_protocol + +valence_protocol 11->9 - - + + 4 - -valence_generated + +valence_generated 5 - -valence_ident + +valence_ident 4->5 - - + + 6 - -valence_math + +valence_math 4->6 - - + + 7 - -valence_build_utils + +valence_build_utils 8 - -valence_nbt + +valence_nbt 9->4 - - + + 10 - -valence_text + +valence_text 9->10 - - + + 10->5 - - + + 10->8 - - + + @@ -168,8 +168,8 @@ 13->2 - - + + @@ -180,8 +180,8 @@ 14->2 - - + + @@ -192,224 +192,230 @@ 15->2 - - + + 16 - -valence_equipment + +valence_equipment + + + +16->2 + + 17 - -valence_inventory - - - -16->17 - - + +valence_inventory 17->2 - - + + 18 - -valence_lang + +valence_lang 19 - -valence_network + +valence_network 19->2 - - + + 19->18 - - + + 20 - -valence_player_list + +valence_player_list 20->2 - - + + 21 - -valence_scoreboard + +valence_scoreboard 21->2 - - + + 22 - -valence_spatial + +valence_spatial 23 - -valence_weather + +valence_weather 23->2 - - + + 24 - -valence_world_border + +valence_world_border 24->2 - - + + 25 - -dump_schedule + +dump_schedule 26 - -valence + +valence 25->26 - - + + 26->1 - - + + 26->13 - - + + 26->14 - - + + 26->15 - - + + 26->16 - - + + - + +26->17 + + + + + 26->19 - - + + - + 26->20 - - + + - + 26->21 - - + + - + 26->23 - - + + - + 26->24 - - + + 27 - -packet_inspector + +packet_inspector - + 27->9 - - + + 28 - -playground + +playground - + 28->26 - - + + 29 - -stresser + +stresser - + 29->9 - - + + From 849e1162f5d66df821816f6e09f7959d02a4825e Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:32:32 +0200 Subject: [PATCH 10/23] add equipment & inventory syncing for players --- crates/valence_equipment/Cargo.toml | 7 ++ crates/valence_equipment/README.md | 4 + crates/valence_equipment/src/lib.rs | 135 +++++++++++++++++----- examples/equipment.rs | 32 ++---- src/tests/equipment.rs | 171 +++++++++++++++++++++++++++- 5 files changed, 293 insertions(+), 56 deletions(-) create mode 100644 crates/valence_equipment/README.md diff --git a/crates/valence_equipment/Cargo.toml b/crates/valence_equipment/Cargo.toml index 2193ffa59..e90dde468 100644 --- a/crates/valence_equipment/Cargo.toml +++ b/crates/valence_equipment/Cargo.toml @@ -11,9 +11,16 @@ license.workspace = true [lints] workspace = true +[features] +default = [ + "inventory" +] +inventory = ["dep:valence_inventory"] + [dependencies] bevy_app.workspace = true bevy_ecs.workspace = true derive_more.workspace = true tracing.workspace = true valence_server.workspace = true +valence_inventory = { workspace = true, optional = true } diff --git a/crates/valence_equipment/README.md b/crates/valence_equipment/README.md new file mode 100644 index 000000000..1b0c45368 --- /dev/null +++ b/crates/valence_equipment/README.md @@ -0,0 +1,4 @@ +# `valence_equipment` +Manages Minecraft's entity equipment (armor, held items) via the `Equipment` component. +By default this is separated from an entities `Inventory` (which means that changes are only visible to other players), but it can be synced by attaching the `EquipmentInventorySync` +component to a entity (currently only Players). diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index abf740137..0261ec61a 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -1,5 +1,7 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; +#[cfg(feature = "inventory")] +pub use inventory_sync::EquipmentInventorySync; use valence_server::client::{Client, FlushPacketsSet, LoadEntityForClientEvent}; use valence_server::entity::living::LivingEntity; use valence_server::entity::{EntityId, EntityLayerId, Position}; @@ -12,15 +14,24 @@ pub struct EquipmentPlugin; impl Plugin for EquipmentPlugin { fn build(&self, app: &mut App) { - app.add_systems(PreUpdate, (on_entity_init,)) - .add_systems( - Update, - ( - update_equipment.before(FlushPacketsSet), - on_entity_load.before(FlushPacketsSet), - ), - ) - .add_event::(); + app.add_systems( + PreUpdate, + ( + on_entity_init, + #[cfg(feature = "inventory")] + inventory_sync::on_attach_inventory_sync, + ), + ) + .add_systems( + Update, + ( + update_equipment.before(FlushPacketsSet), + on_entity_load.before(FlushPacketsSet), + #[cfg(feature = "inventory")] + inventory_sync::equipment_inventory_sync, + ), + ) + .add_event::(); } } @@ -37,10 +48,10 @@ impl Equipment { pub const MAIN_HAND_IDX: u8 = 0; pub const OFF_HAND_IDX: u8 = 1; - pub const BOOTS_IDX: u8 = 2; - pub const LEGGINGS_IDX: u8 = 3; - pub const CHESTPLATE_IDX: u8 = 4; - pub const HELMET_IDX: u8 = 5; + pub const FEET_IDX: u8 = 2; + pub const LEGS_IDX: u8 = 3; + pub const CHEST_IDX: u8 = 4; + pub const HEAD_IDX: u8 = 5; pub fn new( main_hand: ItemStack, @@ -79,20 +90,20 @@ impl Equipment { self.slot(Self::OFF_HAND_IDX) } - pub fn boots(&self) -> &ItemStack { - self.slot(Self::BOOTS_IDX) + pub fn feet(&self) -> &ItemStack { + self.slot(Self::FEET_IDX) } - pub fn leggings(&self) -> &ItemStack { - self.slot(Self::LEGGINGS_IDX) + pub fn legs(&self) -> &ItemStack { + self.slot(Self::LEGS_IDX) } - pub fn chestplate(&self) -> &ItemStack { - self.slot(Self::CHESTPLATE_IDX) + pub fn chest(&self) -> &ItemStack { + self.slot(Self::CHEST_IDX) } - pub fn helmet(&self) -> &ItemStack { - self.slot(Self::HELMET_IDX) + pub fn head(&self) -> &ItemStack { + self.slot(Self::HEAD_IDX) } pub fn set_main_hand(&mut self, item: ItemStack) { @@ -103,20 +114,20 @@ impl Equipment { self.set_slot(Self::OFF_HAND_IDX, item); } - pub fn set_boots(&mut self, item: ItemStack) { - self.set_slot(Self::BOOTS_IDX, item); + pub fn set_feet(&mut self, item: ItemStack) { + self.set_slot(Self::FEET_IDX, item); } - pub fn set_leggings(&mut self, item: ItemStack) { - self.set_slot(Self::LEGGINGS_IDX, item); + pub fn set_legs(&mut self, item: ItemStack) { + self.set_slot(Self::LEGS_IDX, item); } - pub fn set_chestplate(&mut self, item: ItemStack) { - self.set_slot(Self::CHESTPLATE_IDX, item); + pub fn set_chest(&mut self, item: ItemStack) { + self.set_slot(Self::CHEST_IDX, item); } pub fn set_helmet(&mut self, item: ItemStack) { - self.set_slot(Self::HELMET_IDX, item); + self.set_slot(Self::HEAD_IDX, item); } pub fn clear(&mut self) { @@ -236,3 +247,71 @@ fn on_entity_init( commands.entity(entity).insert(Equipment::default()); } } + +#[cfg(feature = "inventory")] +mod inventory_sync { + use valence_inventory::player_inventory::PlayerInventory; + use valence_inventory::{HeldItem, Inventory}; + use valence_server::entity::player::PlayerEntity; + + use super::*; + #[derive(Debug, Default, Clone, Component)] + pub struct EquipmentInventorySync; + + /// Syncs the player [`Equipment`] with the [`Inventory`]. + /// If a change in the player's inventory and in the equipment occurs in the + /// same tick, the equipment change has priority. + pub(crate) fn equipment_inventory_sync( + mut clients: Query< + (&mut Equipment, &mut Inventory, &mut HeldItem), + ( + Or<(Changed, Changed, Changed)>, + With, + With, + ), + >, + ) { + for (mut equipment, mut inventory, held_item) in &mut clients { + // Equipment change has priority over held item changes + if equipment.changed & (1 << Equipment::MAIN_HAND_IDX) != 0 { + let item = equipment.main_hand().clone(); + inventory.set_slot(held_item.slot(), item); + } else if held_item.is_changed() { + let item = inventory.slot(held_item.slot()).clone(); + equipment.set_main_hand(item); + } + + let slots = [ + (Equipment::OFF_HAND_IDX, PlayerInventory::SLOT_OFFHAND), + (Equipment::HEAD_IDX, PlayerInventory::SLOT_HEAD), + (Equipment::CHEST_IDX, PlayerInventory::SLOT_CHEST), + (Equipment::LEGS_IDX, PlayerInventory::SLOT_LEGS), + (Equipment::FEET_IDX, PlayerInventory::SLOT_FEET), + ]; + + for (equipment_slot, inventory_slot) in slots { + // Equipment has priority over inventory changes + if equipment.changed & (1 << equipment_slot) != 0 { + let item = equipment.slot(equipment_slot).clone(); + inventory.set_slot(inventory_slot, item); + } else if inventory.is_changed() { + let item = inventory.slot(inventory_slot).clone(); + equipment.set_slot(equipment_slot, item); + } + } + } + } + + pub(crate) fn on_attach_inventory_sync( + entities: Query, (Added, With)>, + ) { + for entity in &entities { + if entity.is_none() { + tracing::warn!( + "EquipmentInventorySync attached to non-player entity, this will have no \ + effect" + ); + } + } + } +} diff --git a/examples/equipment.rs b/examples/equipment.rs index 504220612..4450c2052 100644 --- a/examples/equipment.rs +++ b/examples/equipment.rs @@ -6,8 +6,7 @@ use rand::Rng; use valence::entity::armor_stand::ArmorStandEntityBundle; use valence::entity::zombie::ZombieEntityBundle; use valence::prelude::*; -use valence_inventory::player_inventory::PlayerInventory; -use valence_inventory::HeldItem; +use valence_equipment::EquipmentInventorySync; pub fn main() { App::new() @@ -20,7 +19,6 @@ pub fn main() { init_clients, despawn_disconnected_clients, randomize_equipment, - update_player_inventory, ), ) .run(); @@ -64,8 +62,10 @@ fn setup( } fn init_clients( + mut commands: Commands, mut clients: Query< ( + Entity, &mut Position, &mut EntityLayerId, &mut VisibleChunkLayer, @@ -77,6 +77,7 @@ fn init_clients( layers: Query, With)>, ) { for ( + player, mut pos, mut layer_id, mut visible_chunk_layer, @@ -91,6 +92,8 @@ fn init_clients( visible_chunk_layer.0 = layer; visible_entity_layers.0.insert(layer); *game_mode = GameMode::Survival; + + commands.entity(player).insert(EquipmentInventorySync); } } @@ -114,19 +117,19 @@ fn randomize_equipment(mut query: Query<&mut Equipment>, server: Res) { ItemStack::new(ItemKind::Shield, 1, None), ), 2 => ( - Equipment::BOOTS_IDX, + Equipment::FEET_IDX, ItemStack::new(ItemKind::DiamondBoots, 1, None), ), 3 => ( - Equipment::LEGGINGS_IDX, + Equipment::LEGS_IDX, ItemStack::new(ItemKind::DiamondLeggings, 1, None), ), 4 => ( - Equipment::CHESTPLATE_IDX, + Equipment::CHEST_IDX, ItemStack::new(ItemKind::DiamondChestplate, 1, None), ), 5 => ( - Equipment::HELMET_IDX, + Equipment::HEAD_IDX, ItemStack::new(ItemKind::DiamondHelmet, 1, None), ), _ => unreachable!(), @@ -135,18 +138,3 @@ fn randomize_equipment(mut query: Query<&mut Equipment>, server: Res) { equipment.set_slot(slot, item_stack); } } - -/// Updating the Equipment will only be visible for other players, -/// so we need to update the player's inventory as well. -fn update_player_inventory( - mut clients: Query<(&mut Inventory, &Equipment, &HeldItem), Changed>, -) { - for (mut inv, equipment, held_item) in &mut clients { - inv.set_slot(PlayerInventory::SLOT_HEAD, equipment.helmet().clone()); - inv.set_slot(PlayerInventory::SLOT_CHEST, equipment.chestplate().clone()); - inv.set_slot(PlayerInventory::SLOT_LEGS, equipment.leggings().clone()); - inv.set_slot(PlayerInventory::SLOT_FEET, equipment.boots().clone()); - inv.set_slot(PlayerInventory::SLOT_OFFHAND, equipment.off_hand().clone()); - inv.set_slot(held_item.slot(), equipment.main_hand().clone()); - } -} diff --git a/src/tests/equipment.rs b/src/tests/equipment.rs index b8a942181..c2fb7bdcb 100644 --- a/src/tests/equipment.rs +++ b/src/tests/equipment.rs @@ -1,4 +1,6 @@ -use valence_equipment::Equipment; +use valence_equipment::{Equipment, EquipmentInventorySync}; +use valence_inventory::player_inventory::PlayerInventory; +use valence_inventory::Inventory; use valence_server::entity::armor_stand::ArmorStandEntityBundle; use valence_server::entity::item::ItemEntityBundle; use valence_server::entity::zombie::ZombieEntityBundle; @@ -27,7 +29,7 @@ fn test_only_send_update_to_other_players() { .get_mut::(client) .expect("could not get player equipment"); - player_equipment.set_chestplate(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + player_equipment.set_chest(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); app.update(); @@ -65,7 +67,7 @@ fn test_multiple_entities() { .get_mut::(zombie) .expect("could not get entity equipment"); - equipment.set_chestplate(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + equipment.set_chest(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); equipment.set_helmet(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); app.update(); @@ -82,7 +84,7 @@ fn test_multiple_entities() { .expect("could not get entity equipment"); // Set the zombie's equipment to the same items - equipment.set_chestplate(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + equipment.set_chest(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); equipment.set_helmet(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); app.update(); @@ -124,7 +126,7 @@ fn test_update_on_load_entity() { .get_mut::(zombie) .expect("could not get entity equipment"); - equipment.set_chestplate(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + equipment.set_chest(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); equipment.set_helmet(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); app.update(); @@ -176,7 +178,6 @@ fn test_skip_update_for_empty_equipment() { .insert(Equipment::default()); app.update(); - app.update(); // Make assertions let sent_packets = helper.collect_received(); @@ -246,3 +247,161 @@ fn test_ensure_living_entities_only() { let item_equipment = app.world_mut().get_mut::(item); assert!(item_equipment.is_none()); } + +#[test] +fn test_inventory_sync_from_equipment() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + app.world_mut() + .entity_mut(client) + .insert(EquipmentInventorySync); + + let mut player_equipment = app + .world_mut() + .get_mut::(client) + .expect("could not get player equipment"); + + player_equipment.set_chest(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + + app.update(); + + let player_inventory = app + .world() + .get::(client) + .expect("could not get player equipment"); + + let player_equipment = app + .world() + .get::(client) + .expect("could not get player equipment"); + + // The inventory should have been updated + // after the equipment change + assert_eq!( + *player_inventory.slot(PlayerInventory::SLOT_CHEST), + ItemStack::new(ItemKind::DiamondChestplate, 1, None) + ); + + assert_eq!( + *player_equipment.chest(), + ItemStack::new(ItemKind::DiamondChestplate, 1, None) + ); +} + +#[test] +fn test_inventory_sync_from_inventory() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + app.world_mut() + .entity_mut(client) + .insert(EquipmentInventorySync); + + let mut player_inventory = app + .world_mut() + .get_mut::(client) + .expect("could not get player equipment"); + + player_inventory.set_slot( + PlayerInventory::SLOT_CHEST, + ItemStack::new(ItemKind::DiamondChestplate, 1, None), + ); + + app.update(); + + let player_inventory = app + .world() + .get::(client) + .expect("could not get player equipment"); + + let player_equipment = app + .world() + .get::(client) + .expect("could not get player equipment"); + + // The equipment should have been updated + // after the inventory change + assert_eq!( + *player_inventory.slot(PlayerInventory::SLOT_CHEST), + ItemStack::new(ItemKind::DiamondChestplate, 1, None) + ); + + assert_eq!( + *player_equipment.chest(), + ItemStack::new(ItemKind::DiamondChestplate, 1, None) + ); +} + +#[test] +fn test_equipment_priority_over_inventory() { + let ScenarioSingleClient { + mut app, client, .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + + app.world_mut() + .entity_mut(client) + .insert(EquipmentInventorySync); + + let mut player_inventory = app + .world_mut() + .get_mut::(client) + .expect("could not get player equipment"); + + // Set the slot in the inventory as well as in the equipment in the same tick + + player_inventory.set_slot( + PlayerInventory::SLOT_CHEST, + ItemStack::new(ItemKind::DiamondChestplate, 1, None), + ); + + let mut player_equipment = app + .world_mut() + .get_mut::(client) + .expect("could not get player equipment"); + + player_equipment.set_chest(ItemStack::new(ItemKind::GoldenChestplate, 1, None)); + + app.update(); + + // The equipment change should have priority, the inventory change is ignored + + let player_inventory = app + .world() + .get::(client) + .expect("could not get player equipment"); + + let player_equipment = app + .world() + .get::(client) + .expect("could not get player equipment"); + + assert_eq!( + *player_inventory.slot(PlayerInventory::SLOT_CHEST), + ItemStack::new(ItemKind::GoldenChestplate, 1, None) + ); + + assert_eq!( + *player_equipment.chest(), + ItemStack::new(ItemKind::GoldenChestplate, 1, None) + ); +} From 76b9664ddec01b579bd2a48f67cb574cf3571bc3 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:36:28 +0200 Subject: [PATCH 11/23] only care about relevant inventory slot --- crates/valence_equipment/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index 0261ec61a..8252682af 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -294,7 +294,7 @@ mod inventory_sync { if equipment.changed & (1 << equipment_slot) != 0 { let item = equipment.slot(equipment_slot).clone(); inventory.set_slot(inventory_slot, item); - } else if inventory.is_changed() { + } else if inventory.changed & (1 << inventory_slot) != 0 { let item = inventory.slot(inventory_slot).clone(); equipment.set_slot(equipment_slot, item); } From a7adbb1fbd6a0e1d47ca45951d2d3508e42f13c9 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:55:11 +0200 Subject: [PATCH 12/23] update depgraph --- assets/depgraph.svg | 290 ++++++++++++++++++++++---------------------- 1 file changed, 142 insertions(+), 148 deletions(-) diff --git a/assets/depgraph.svg b/assets/depgraph.svg index b03e2a5a8..e7d3d86da 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -4,16 +4,16 @@ - - + + %3 - + 0 - -java_string + +java_string @@ -24,140 +24,140 @@ 2 - -valence_server + +valence_server 1->2 - - + + 3 - -valence_entity + +valence_entity 2->3 - - + + 12 - -valence_registry + +valence_registry 2->12 - - + + 11 - -valence_server_common + +valence_server_common 3->11 - - + + 12->11 - - + + 9 - -valence_protocol + +valence_protocol 11->9 - - + + 4 - -valence_generated + +valence_generated 5 - -valence_ident + +valence_ident 4->5 - - + + 6 - -valence_math + +valence_math 4->6 - - + + 7 - -valence_build_utils + +valence_build_utils 8 - -valence_nbt + +valence_nbt 9->4 - - + + 10 - -valence_text + +valence_text 9->10 - - + + 10->5 - - + + 10->8 - - + + @@ -168,8 +168,8 @@ 13->2 - - + + @@ -180,8 +180,8 @@ 14->2 - - + + @@ -192,230 +192,224 @@ 15->2 - - + + 16 - -valence_equipment - - - -16->2 - - + +valence_equipment 17 - -valence_inventory + +valence_inventory + + + +16->17 + + 17->2 - - + + 18 - -valence_lang + +valence_lang 19 - -valence_network + +valence_network 19->2 - - + + 19->18 - - + + 20 - -valence_player_list + +valence_player_list 20->2 - - + + 21 - -valence_scoreboard + +valence_scoreboard 21->2 - - + + 22 - -valence_spatial + +valence_spatial 23 - -valence_weather + +valence_weather 23->2 - - + + 24 - -valence_world_border + +valence_world_border 24->2 - - + + 25 - -dump_schedule + +dump_schedule 26 - -valence + +valence 25->26 - - + + 26->1 - - + + 26->13 - - + + 26->14 - - + + 26->15 - - + + 26->16 - - - - - -26->17 - - + + - + 26->19 - - + + - + 26->20 - - + + - + 26->21 - - + + - + 26->23 - - + + - + 26->24 - - + + 27 - -packet_inspector + +packet_inspector - + 27->9 - - + + 28 - -playground + +playground - + 28->26 - - + + 29 - -stresser + +stresser - + 29->9 - - + + From 45707374911601fced58b232cd0e02a17e8b3cc0 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:43:09 +0200 Subject: [PATCH 13/23] improvde docs + remove inventory feature from equipment --- crates/valence_equipment/Cargo.toml | 10 +-- crates/valence_equipment/README.md | 56 +++++++++++++ .../valence_equipment/src/inventory_sync.rs | 63 +++++++++++++++ crates/valence_equipment/src/lib.rs | 78 ++----------------- 4 files changed, 126 insertions(+), 81 deletions(-) create mode 100644 crates/valence_equipment/src/inventory_sync.rs diff --git a/crates/valence_equipment/Cargo.toml b/crates/valence_equipment/Cargo.toml index e90dde468..81b1a3770 100644 --- a/crates/valence_equipment/Cargo.toml +++ b/crates/valence_equipment/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "valence_equipment" -description = "Equipment syncing support for Valence" +description = "Equipment support for Valence" readme = "README.md" version.workspace = true edition.workspace = true @@ -11,16 +11,10 @@ license.workspace = true [lints] workspace = true -[features] -default = [ - "inventory" -] -inventory = ["dep:valence_inventory"] - [dependencies] bevy_app.workspace = true bevy_ecs.workspace = true derive_more.workspace = true tracing.workspace = true valence_server.workspace = true -valence_inventory = { workspace = true, optional = true } +valence_inventory.workspace = true diff --git a/crates/valence_equipment/README.md b/crates/valence_equipment/README.md index 1b0c45368..05a3733d0 100644 --- a/crates/valence_equipment/README.md +++ b/crates/valence_equipment/README.md @@ -2,3 +2,59 @@ Manages Minecraft's entity equipment (armor, held items) via the `Equipment` component. By default this is separated from an entities `Inventory` (which means that changes are only visible to other players), but it can be synced by attaching the `EquipmentInventorySync` component to a entity (currently only Players). + +## Example + +```rust +use valence::prelude::*; +use valence_equipment::*; + +// Spawn a player with full armor, a sword and a shield. +fn init_clients( + mut commands: Commands, + mut clients: Query< + ( + Entity, + &mut Position, + &mut EntityLayerId, + &mut VisibleChunkLayer, + &mut VisibleEntityLayers, + &mut GameMode, + ), + Added, + >, + layers: Query, With)>, +) { + for ( + player, + mut pos, + mut layer_id, + mut visible_chunk_layer, + mut visible_entity_layers, + mut game_mode, + ) in &mut clients + { + let layer = layers.single(); + + pos.0 = [0.0, f64::from(SPAWN_Y) + 1.0, 0.0].into(); + layer_id.0 = layer; + visible_chunk_layer.0 = layer; + visible_entity_layers.0.insert(layer); + *game_mode = GameMode::Survival; + + let equipment = Equipment::new( + ItemStack::new(ItemKind::DiamondSword, 1, None), // Main hand + ItemStack::new(ItemKind::Shield, 1, None), // Off hand + ItemStack::new(ItemKind::DiamondBoots, 1, None), // Feet + ItemStack::new(ItemKind::DiamondLeggings, 1, None), // Legs + ItemStack::new(ItemKind::DiamondChestplate, 1, None), // Chest + ItemStack::new(ItemKind::DiamondHelmet, 1, None), // Head + ); + + commands.entity(player).insert(equipment); // Add the equipment to the player + + commands.entity(player).insert(EquipmentInventorySync); // Sync the equipment with the player's inventory. This is not required if you want to only show equipment for other players. + } +} +``` + diff --git a/crates/valence_equipment/src/inventory_sync.rs b/crates/valence_equipment/src/inventory_sync.rs new file mode 100644 index 000000000..65df9aba3 --- /dev/null +++ b/crates/valence_equipment/src/inventory_sync.rs @@ -0,0 +1,63 @@ +use valence_inventory::player_inventory::PlayerInventory; +use valence_inventory::{HeldItem, Inventory}; +use valence_server::entity::player::PlayerEntity; + +use super::*; +#[derive(Debug, Default, Clone, Component)] +pub struct EquipmentInventorySync; + +/// Syncs the player [`Equipment`] with the [`Inventory`]. +/// If a change in the player's inventory and in the equipment occurs in the +/// same tick, the equipment change has priority. +pub(crate) fn equipment_inventory_sync( + mut clients: Query< + (&mut Equipment, &mut Inventory, &mut HeldItem), + ( + Or<(Changed, Changed, Changed)>, + With, + With, + ), + >, +) { + for (mut equipment, mut inventory, held_item) in &mut clients { + // Equipment change has priority over held item changes + if equipment.changed & (1 << Equipment::MAIN_HAND_IDX) != 0 { + let item = equipment.main_hand().clone(); + inventory.set_slot(held_item.slot(), item); + } else if held_item.is_changed() { + let item = inventory.slot(held_item.slot()).clone(); + equipment.set_main_hand(item); + } + + let slots = [ + (Equipment::OFF_HAND_IDX, PlayerInventory::SLOT_OFFHAND), + (Equipment::HEAD_IDX, PlayerInventory::SLOT_HEAD), + (Equipment::CHEST_IDX, PlayerInventory::SLOT_CHEST), + (Equipment::LEGS_IDX, PlayerInventory::SLOT_LEGS), + (Equipment::FEET_IDX, PlayerInventory::SLOT_FEET), + ]; + + for (equipment_slot, inventory_slot) in slots { + // Equipment has priority over inventory changes + if equipment.changed & (1 << equipment_slot) != 0 { + let item = equipment.slot(equipment_slot).clone(); + inventory.set_slot(inventory_slot, item); + } else if inventory.changed & (1 << inventory_slot) != 0 { + let item = inventory.slot(inventory_slot).clone(); + equipment.set_slot(equipment_slot, item); + } + } + } +} + +pub(crate) fn on_attach_inventory_sync( + entities: Query, (Added, With)>, +) { + for entity in &entities { + if entity.is_none() { + tracing::warn!( + "EquipmentInventorySync attached to non-player entity, this will have no effect" + ); + } + } +} diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index 8252682af..1c6d9f0bd 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -1,6 +1,8 @@ +#![doc = include_str!("../README.md")] + use bevy_app::prelude::*; use bevy_ecs::prelude::*; -#[cfg(feature = "inventory")] +mod inventory_sync; pub use inventory_sync::EquipmentInventorySync; use valence_server::client::{Client, FlushPacketsSet, LoadEntityForClientEvent}; use valence_server::entity::living::LivingEntity; @@ -18,17 +20,15 @@ impl Plugin for EquipmentPlugin { PreUpdate, ( on_entity_init, - #[cfg(feature = "inventory")] inventory_sync::on_attach_inventory_sync, + inventory_sync::equipment_inventory_sync, ), ) .add_systems( - Update, + PostUpdate, ( update_equipment.before(FlushPacketsSet), on_entity_load.before(FlushPacketsSet), - #[cfg(feature = "inventory")] - inventory_sync::equipment_inventory_sync, ), ) .add_event::(); @@ -247,71 +247,3 @@ fn on_entity_init( commands.entity(entity).insert(Equipment::default()); } } - -#[cfg(feature = "inventory")] -mod inventory_sync { - use valence_inventory::player_inventory::PlayerInventory; - use valence_inventory::{HeldItem, Inventory}; - use valence_server::entity::player::PlayerEntity; - - use super::*; - #[derive(Debug, Default, Clone, Component)] - pub struct EquipmentInventorySync; - - /// Syncs the player [`Equipment`] with the [`Inventory`]. - /// If a change in the player's inventory and in the equipment occurs in the - /// same tick, the equipment change has priority. - pub(crate) fn equipment_inventory_sync( - mut clients: Query< - (&mut Equipment, &mut Inventory, &mut HeldItem), - ( - Or<(Changed, Changed, Changed)>, - With, - With, - ), - >, - ) { - for (mut equipment, mut inventory, held_item) in &mut clients { - // Equipment change has priority over held item changes - if equipment.changed & (1 << Equipment::MAIN_HAND_IDX) != 0 { - let item = equipment.main_hand().clone(); - inventory.set_slot(held_item.slot(), item); - } else if held_item.is_changed() { - let item = inventory.slot(held_item.slot()).clone(); - equipment.set_main_hand(item); - } - - let slots = [ - (Equipment::OFF_HAND_IDX, PlayerInventory::SLOT_OFFHAND), - (Equipment::HEAD_IDX, PlayerInventory::SLOT_HEAD), - (Equipment::CHEST_IDX, PlayerInventory::SLOT_CHEST), - (Equipment::LEGS_IDX, PlayerInventory::SLOT_LEGS), - (Equipment::FEET_IDX, PlayerInventory::SLOT_FEET), - ]; - - for (equipment_slot, inventory_slot) in slots { - // Equipment has priority over inventory changes - if equipment.changed & (1 << equipment_slot) != 0 { - let item = equipment.slot(equipment_slot).clone(); - inventory.set_slot(inventory_slot, item); - } else if inventory.changed & (1 << inventory_slot) != 0 { - let item = inventory.slot(inventory_slot).clone(); - equipment.set_slot(equipment_slot, item); - } - } - } - } - - pub(crate) fn on_attach_inventory_sync( - entities: Query, (Added, With)>, - ) { - for entity in &entities { - if entity.is_none() { - tracing::warn!( - "EquipmentInventorySync attached to non-player entity, this will have no \ - effect" - ); - } - } - } -} From 5d97106a87d2a6dfe0b9ff1ed2e9aef019e667ae Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:59:00 +0200 Subject: [PATCH 14/23] update depgraph --- assets/depgraph.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/depgraph.svg b/assets/depgraph.svg index e7d3d86da..0a9174593 100644 --- a/assets/depgraph.svg +++ b/assets/depgraph.svg @@ -210,7 +210,7 @@ 16->17 - + From 442eecd0cb237f966d294fc8fde3a0a93860793a Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Thu, 17 Oct 2024 12:46:08 +0200 Subject: [PATCH 15/23] fix doctest + rename set_helmet -> set_head --- crates/valence_equipment/README.md | 63 ++++++++++------------------- crates/valence_equipment/src/lib.rs | 2 +- src/tests/equipment.rs | 6 +-- 3 files changed, 26 insertions(+), 45 deletions(-) diff --git a/crates/valence_equipment/README.md b/crates/valence_equipment/README.md index 05a3733d0..651c26f21 100644 --- a/crates/valence_equipment/README.md +++ b/crates/valence_equipment/README.md @@ -6,55 +6,36 @@ component to a entity (currently only Players). ## Example ```rust -use valence::prelude::*; +use bevy_ecs::prelude::*; use valence_equipment::*; - -// Spawn a player with full armor, a sword and a shield. -fn init_clients( - mut commands: Commands, +use valence_server::{ + ItemStack, ItemKind, + entity::player::PlayerEntity, +}; +// Add equipment to players when they are added to the world. +fn init_equipment( mut clients: Query< + &mut Equipment, ( - Entity, - &mut Position, - &mut EntityLayerId, - &mut VisibleChunkLayer, - &mut VisibleEntityLayers, - &mut GameMode, + Added, + With, ), - Added, >, - layers: Query, With)>, ) { - for ( - player, - mut pos, - mut layer_id, - mut visible_chunk_layer, - mut visible_entity_layers, - mut game_mode, - ) in &mut clients + for mut equipment in &mut clients { - let layer = layers.single(); - - pos.0 = [0.0, f64::from(SPAWN_Y) + 1.0, 0.0].into(); - layer_id.0 = layer; - visible_chunk_layer.0 = layer; - visible_entity_layers.0.insert(layer); - *game_mode = GameMode::Survival; - - let equipment = Equipment::new( - ItemStack::new(ItemKind::DiamondSword, 1, None), // Main hand - ItemStack::new(ItemKind::Shield, 1, None), // Off hand - ItemStack::new(ItemKind::DiamondBoots, 1, None), // Feet - ItemStack::new(ItemKind::DiamondLeggings, 1, None), // Legs - ItemStack::new(ItemKind::DiamondChestplate, 1, None), // Chest - ItemStack::new(ItemKind::DiamondHelmet, 1, None), // Head - ); - - commands.entity(player).insert(equipment); // Add the equipment to the player - - commands.entity(player).insert(EquipmentInventorySync); // Sync the equipment with the player's inventory. This is not required if you want to only show equipment for other players. + equipment.set_main_hand(ItemStack::new(ItemKind::DiamondSword, 1, None)); + equipment.set_off_hand(ItemStack::new(ItemKind::Shield, 1, None)); + equipment.set_feet(ItemStack::new(ItemKind::DiamondBoots, 1, None)); + equipment.set_legs(ItemStack::new(ItemKind::DiamondLeggings, 1, None)); + equipment.set_chest(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + equipment.set_head(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); } } ``` +### See also + +Examples related to inventories in the `valence/examples/` directory: +- `equipment` + diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index 1c6d9f0bd..e4f2ef571 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -126,7 +126,7 @@ impl Equipment { self.set_slot(Self::CHEST_IDX, item); } - pub fn set_helmet(&mut self, item: ItemStack) { + pub fn set_head(&mut self, item: ItemStack) { self.set_slot(Self::HEAD_IDX, item); } diff --git a/src/tests/equipment.rs b/src/tests/equipment.rs index c2fb7bdcb..43b285173 100644 --- a/src/tests/equipment.rs +++ b/src/tests/equipment.rs @@ -68,7 +68,7 @@ fn test_multiple_entities() { .expect("could not get entity equipment"); equipment.set_chest(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); - equipment.set_helmet(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); + equipment.set_head(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); app.update(); @@ -85,7 +85,7 @@ fn test_multiple_entities() { // Set the zombie's equipment to the same items equipment.set_chest(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); - equipment.set_helmet(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); + equipment.set_head(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); app.update(); @@ -127,7 +127,7 @@ fn test_update_on_load_entity() { .expect("could not get entity equipment"); equipment.set_chest(ItemStack::new(ItemKind::DiamondChestplate, 1, None)); - equipment.set_helmet(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); + equipment.set_head(ItemStack::new(ItemKind::DiamondHelmet, 1, None)); app.update(); From 3924bd00e829b7543c04a9a3db66214bcc08f3f1 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 19 Oct 2024 16:33:48 +0200 Subject: [PATCH 16/23] more tests + fix client changes --- .../valence_equipment/src/inventory_sync.rs | 29 +++- crates/valence_equipment/src/lib.rs | 1 + crates/valence_inventory/src/lib.rs | 3 + src/tests/equipment.rs | 142 +++++++++++++++++- 4 files changed, 170 insertions(+), 5 deletions(-) diff --git a/crates/valence_equipment/src/inventory_sync.rs b/crates/valence_equipment/src/inventory_sync.rs index 65df9aba3..672a3a7e2 100644 --- a/crates/valence_equipment/src/inventory_sync.rs +++ b/crates/valence_equipment/src/inventory_sync.rs @@ -1,5 +1,5 @@ use valence_inventory::player_inventory::PlayerInventory; -use valence_inventory::{HeldItem, Inventory}; +use valence_inventory::{HeldItem, Inventory, UpdateSelectedSlotEvent}; use valence_server::entity::player::PlayerEntity; use super::*; @@ -9,6 +9,8 @@ pub struct EquipmentInventorySync; /// Syncs the player [`Equipment`] with the [`Inventory`]. /// If a change in the player's inventory and in the equipment occurs in the /// same tick, the equipment change has priority. +/// Note: This system only handles direct changes to the held item (not actual +/// changes from the client) see [`equipment_held_item_sync_from_client`] pub(crate) fn equipment_inventory_sync( mut clients: Query< (&mut Equipment, &mut Inventory, &mut HeldItem), @@ -18,6 +20,7 @@ pub(crate) fn equipment_inventory_sync( With, ), >, + // mut change_slot_events ) { for (mut equipment, mut inventory, held_item) in &mut clients { // Equipment change has priority over held item changes @@ -25,6 +28,8 @@ pub(crate) fn equipment_inventory_sync( let item = equipment.main_hand().clone(); inventory.set_slot(held_item.slot(), item); } else if held_item.is_changed() { + // This will only be called if we change the held item from valence, + // the client change is handled in `equipment_held_item_sync_from_client` let item = inventory.slot(held_item.slot()).clone(); equipment.set_main_hand(item); } @@ -37,12 +42,15 @@ pub(crate) fn equipment_inventory_sync( (Equipment::FEET_IDX, PlayerInventory::SLOT_FEET), ]; + // We cant rely on the changed bitfield of inventory here + // because that gets reset when the client changes the inventory + for (equipment_slot, inventory_slot) in slots { // Equipment has priority over inventory changes if equipment.changed & (1 << equipment_slot) != 0 { let item = equipment.slot(equipment_slot).clone(); inventory.set_slot(inventory_slot, item); - } else if inventory.changed & (1 << inventory_slot) != 0 { + } else if inventory.is_changed() { let item = inventory.slot(inventory_slot).clone(); equipment.set_slot(equipment_slot, item); } @@ -50,6 +58,23 @@ pub(crate) fn equipment_inventory_sync( } } +/// Handles the case where the client changes the slot (the bevy change is +/// suppressed for this). See +/// [`valence_inventory::handle_update_selected_slot`]. +pub(crate) fn equipment_held_item_sync_from_client( + mut clients: Query<(&HeldItem, &Inventory, &mut Equipment), With>, + mut events: EventReader, +) { + for event in events.read() { + let Ok((held_item, inventory, mut equipment)) = clients.get_mut(event.client) else { + continue; + }; + + let item = inventory.slot(held_item.slot()).clone(); + equipment.set_main_hand(item); + } +} + pub(crate) fn on_attach_inventory_sync( entities: Query, (Added, With)>, ) { diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index e4f2ef571..be01b8864 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -22,6 +22,7 @@ impl Plugin for EquipmentPlugin { on_entity_init, inventory_sync::on_attach_inventory_sync, inventory_sync::equipment_inventory_sync, + inventory_sync::equipment_held_item_sync_from_client, ), ) .add_systems( diff --git a/crates/valence_inventory/src/lib.rs b/crates/valence_inventory/src/lib.rs index 19841bfa0..5918ce0b2 100644 --- a/crates/valence_inventory/src/lib.rs +++ b/crates/valence_inventory/src/lib.rs @@ -1408,6 +1408,9 @@ fn handle_update_selected_slot( for packet in packets.read() { if let Some(pkt) = packet.decode::() { if let Ok(mut mut_held) = clients.get_mut(packet.client) { + // We bypass the change detection here because the server listens for changes + // of `HeldItem` in order to send the update to the client. + // This is not required here because the update is coming from the client. let held = mut_held.bypass_change_detection(); if pkt.slot > 8 { // The client is trying to interact with a slot that does not exist, ignore. diff --git a/src/tests/equipment.rs b/src/tests/equipment.rs index 43b285173..7460b78f2 100644 --- a/src/tests/equipment.rs +++ b/src/tests/equipment.rs @@ -1,12 +1,14 @@ use valence_equipment::{Equipment, EquipmentInventorySync}; use valence_inventory::player_inventory::PlayerInventory; -use valence_inventory::Inventory; +use valence_inventory::{ClickMode, ClientInventoryState, Inventory, SlotChange}; use valence_server::entity::armor_stand::ArmorStandEntityBundle; use valence_server::entity::item::ItemEntityBundle; use valence_server::entity::zombie::ZombieEntityBundle; use valence_server::entity::{EntityLayerId, Position}; use valence_server::math::DVec3; -use valence_server::protocol::packets::play::EntityEquipmentUpdateS2c; +use valence_server::protocol::packets::play::{ + ClickSlotC2s, EntityEquipmentUpdateS2c, UpdateSelectedSlotC2s, +}; use valence_server::{ItemKind, ItemStack}; use crate::testing::ScenarioSingleClient; @@ -298,7 +300,7 @@ fn test_inventory_sync_from_equipment() { } #[test] -fn test_inventory_sync_from_inventory() { +fn test_equipment_sync_from_inventory() { let ScenarioSingleClient { mut app, client, @@ -405,3 +407,137 @@ fn test_equipment_priority_over_inventory() { ItemStack::new(ItemKind::GoldenChestplate, 1, None) ); } + +#[test] +fn test_equipment_change_from_player() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + app.world_mut() + .entity_mut(client) + .insert(EquipmentInventorySync); + + let mut player_inventory = app + .world_mut() + .get_mut::(client) + .expect("could not get player equipment"); + + player_inventory.set_slot(36, ItemStack::new(ItemKind::DiamondChestplate, 1, None)); + app.update(); + helper.clear_received(); + + let state_id = app + .world() + .get::(client) + .expect("could not get player equipment") + .state_id(); + + app.update(); + + helper.send(&ClickSlotC2s { + window_id: 0, + button: 0, + mode: ClickMode::Hotbar, + state_id: state_id.0.into(), + slot_idx: 36, + slot_changes: vec![ + SlotChange { + idx: 36, + stack: ItemStack::EMPTY, + }, + SlotChange { + idx: PlayerInventory::SLOT_CHEST as i16, + stack: ItemStack::new(ItemKind::DiamondChestplate, 1, None), + }, + ] + .into(), + carried_item: ItemStack::EMPTY, + }); + + app.update(); + app.update(); + + let player_inventory = app + .world() + .get::(client) + .expect("could not get player equipment"); + + let player_equipment = app + .world() + .get::(client) + .expect("could not get player equipment"); + + assert_eq!( + player_inventory.slot(PlayerInventory::SLOT_CHEST), + &ItemStack::new(ItemKind::DiamondChestplate, 1, None) + ); + + assert_eq!(player_inventory.slot(36), &ItemStack::EMPTY); + + assert_eq!( + player_equipment.chest(), + &ItemStack::new(ItemKind::DiamondChestplate, 1, None) + ); +} + +#[test] +fn test_held_item_change_from_client() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + .. + } = ScenarioSingleClient::new(); + + // Process a tick to get past the "on join" logic. + app.update(); + helper.clear_received(); + + app.world_mut() + .entity_mut(client) + .insert(EquipmentInventorySync); + + let mut player_inventory = app + .world_mut() + .get_mut::(client) + .expect("could not get player equipment"); + + player_inventory.set_slot(36, ItemStack::new(ItemKind::DiamondSword, 1, None)); + player_inventory.set_slot(37, ItemStack::new(ItemKind::IronSword, 1, None)); + + app.update(); + + let player_equipment = app + .world() + .get::(client) + .expect("could not get player equipment"); + + assert_eq!( + player_equipment.main_hand(), + &ItemStack::new(ItemKind::DiamondSword, 1, None) + ); + + // Change the held item from the client + helper.send(&UpdateSelectedSlotC2s { slot: 1 }); + + app.update(); // handle change slot + app.update(); // handle change equipment + + let player_equipment = app + .world() + .get::(client) + .expect("could not get player equipment"); + + assert_eq!( + player_equipment.main_hand(), + &ItemStack::new(ItemKind::IronSword, 1, None) + ); +} From 59bea026fa840b37e32b09d434e61eb996938adf Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 19 Oct 2024 17:23:29 +0200 Subject: [PATCH 17/23] fix case where selected slot would not change, but held item would --- crates/valence_equipment/src/inventory_sync.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/valence_equipment/src/inventory_sync.rs b/crates/valence_equipment/src/inventory_sync.rs index 672a3a7e2..b97edfcdd 100644 --- a/crates/valence_equipment/src/inventory_sync.rs +++ b/crates/valence_equipment/src/inventory_sync.rs @@ -27,7 +27,10 @@ pub(crate) fn equipment_inventory_sync( if equipment.changed & (1 << Equipment::MAIN_HAND_IDX) != 0 { let item = equipment.main_hand().clone(); inventory.set_slot(held_item.slot(), item); - } else if held_item.is_changed() { + } else { + // If we change the inventory (e.g by pickung up an item) + // then the HeldItem slot wont be changed + // This will only be called if we change the held item from valence, // the client change is handled in `equipment_held_item_sync_from_client` let item = inventory.slot(held_item.slot()).clone(); From f371f4428593b6d9ce68ccd9c2dd20770a71f407 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:03:27 +0200 Subject: [PATCH 18/23] remove link to private function --- crates/valence_equipment/src/inventory_sync.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/valence_equipment/src/inventory_sync.rs b/crates/valence_equipment/src/inventory_sync.rs index b97edfcdd..fb416ea40 100644 --- a/crates/valence_equipment/src/inventory_sync.rs +++ b/crates/valence_equipment/src/inventory_sync.rs @@ -62,8 +62,7 @@ pub(crate) fn equipment_inventory_sync( } /// Handles the case where the client changes the slot (the bevy change is -/// suppressed for this). See -/// [`valence_inventory::handle_update_selected_slot`]. +/// suppressed for this) pub(crate) fn equipment_held_item_sync_from_client( mut clients: Query<(&HeldItem, &Inventory, &mut Equipment), With>, mut events: EventReader, From 86c22d144e7ad7f59d3058300797db6d923fe34c Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Tue, 22 Oct 2024 02:40:18 +0200 Subject: [PATCH 19/23] remove comment --- crates/valence_equipment/src/inventory_sync.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/valence_equipment/src/inventory_sync.rs b/crates/valence_equipment/src/inventory_sync.rs index fb416ea40..fdbc61177 100644 --- a/crates/valence_equipment/src/inventory_sync.rs +++ b/crates/valence_equipment/src/inventory_sync.rs @@ -20,7 +20,6 @@ pub(crate) fn equipment_inventory_sync( With, ), >, - // mut change_slot_events ) { for (mut equipment, mut inventory, held_item) in &mut clients { // Equipment change has priority over held item changes From da87c748897f87351ef2a80a565abaf11a872a6b Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:48:13 +0200 Subject: [PATCH 20/23] add doc comment --- crates/valence_equipment/src/inventory_sync.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/valence_equipment/src/inventory_sync.rs b/crates/valence_equipment/src/inventory_sync.rs index fdbc61177..1d1abf2c1 100644 --- a/crates/valence_equipment/src/inventory_sync.rs +++ b/crates/valence_equipment/src/inventory_sync.rs @@ -3,6 +3,9 @@ use valence_inventory::{HeldItem, Inventory, UpdateSelectedSlotEvent}; use valence_server::entity::player::PlayerEntity; use super::*; + +/// This component will sync a player's [`Equipment`], which is visible to other +/// players, with the player [`Inventory`]. #[derive(Debug, Default, Clone, Component)] pub struct EquipmentInventorySync; From 7a9ce5d9e1a89e751bde699532fa26d7b66efdfc Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:56:14 +0200 Subject: [PATCH 21/23] add doc comments --- crates/valence_equipment/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index be01b8864..f7f4f493f 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -36,6 +36,12 @@ impl Plugin for EquipmentPlugin { } } +/// Contains the visible equipment of a [`LivingEntity`], such as armor and held +/// items. By default this is not synced with a player's [`Inventory`], so the +/// armor the player has equipped in their inventory, will not be visible by +/// other players. You would have to change the equipment in this component here +/// or attach the [`EquipmentInventorySync`] component to the player +/// entity to sync the equipment with the inventory. #[derive(Debug, Default, Clone, Component)] pub struct Equipment { equipment: [ItemStack; Self::SLOT_COUNT], From 8de9c05c47bf6556db8ca217ba13b6d0c8c8ce5f Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:04:12 +0200 Subject: [PATCH 22/23] fix link to inventory --- crates/valence_equipment/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index f7f4f493f..e79c720fa 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -37,7 +37,7 @@ impl Plugin for EquipmentPlugin { } /// Contains the visible equipment of a [`LivingEntity`], such as armor and held -/// items. By default this is not synced with a player's [`Inventory`], so the +/// items. By default this is not synced with a player's [`valence_inventory::Inventory`], so the /// armor the player has equipped in their inventory, will not be visible by /// other players. You would have to change the equipment in this component here /// or attach the [`EquipmentInventorySync`] component to the player From 5336925a1279cbe14f8c0acf36585337b8200046 Mon Sep 17 00:00:00 2001 From: maxomatic458 <104733404+maxomatic458@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:07:30 +0200 Subject: [PATCH 23/23] format --- crates/valence_equipment/src/lib.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/valence_equipment/src/lib.rs b/crates/valence_equipment/src/lib.rs index e79c720fa..a7e6b07c7 100644 --- a/crates/valence_equipment/src/lib.rs +++ b/crates/valence_equipment/src/lib.rs @@ -37,11 +37,12 @@ impl Plugin for EquipmentPlugin { } /// Contains the visible equipment of a [`LivingEntity`], such as armor and held -/// items. By default this is not synced with a player's [`valence_inventory::Inventory`], so the -/// armor the player has equipped in their inventory, will not be visible by -/// other players. You would have to change the equipment in this component here -/// or attach the [`EquipmentInventorySync`] component to the player -/// entity to sync the equipment with the inventory. +/// items. By default this is not synced with a player's +/// [`valence_inventory::Inventory`], so the armor the player has equipped in +/// their inventory, will not be visible by other players. You would have to +/// change the equipment in this component here or attach the +/// [`EquipmentInventorySync`] component to the player entity to sync the +/// equipment with the inventory. #[derive(Debug, Default, Clone, Component)] pub struct Equipment { equipment: [ItemStack; Self::SLOT_COUNT],