Skip to content

Commit

Permalink
add equipment & inventory syncing for players
Browse files Browse the repository at this point in the history
  • Loading branch information
maxomatic458 committed Oct 16, 2024
1 parent c96634b commit 849e116
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 56 deletions.
7 changes: 7 additions & 0 deletions crates/valence_equipment/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
4 changes: 4 additions & 0 deletions crates/valence_equipment/README.md
Original file line number Diff line number Diff line change
@@ -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).
135 changes: 107 additions & 28 deletions crates/valence_equipment/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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::<EquipmentChangeEvent>();
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::<EquipmentChangeEvent>();
}
}

Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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<Equipment>, Changed<Inventory>, Changed<HeldItem>)>,
With<EquipmentInventorySync>,
With<PlayerEntity>,
),
>,
) {
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<Option<&PlayerEntity>, (Added<EquipmentInventorySync>, With<Inventory>)>,
) {
for entity in &entities {
if entity.is_none() {
tracing::warn!(
"EquipmentInventorySync attached to non-player entity, this will have no \
effect"
);
}
}
}
}
32 changes: 10 additions & 22 deletions examples/equipment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -20,7 +19,6 @@ pub fn main() {
init_clients,
despawn_disconnected_clients,
randomize_equipment,
update_player_inventory,
),
)
.run();
Expand Down Expand Up @@ -64,8 +62,10 @@ fn setup(
}

fn init_clients(
mut commands: Commands,
mut clients: Query<
(
Entity,
&mut Position,
&mut EntityLayerId,
&mut VisibleChunkLayer,
Expand All @@ -77,6 +77,7 @@ fn init_clients(
layers: Query<Entity, (With<ChunkLayer>, With<EntityLayer>)>,
) {
for (
player,
mut pos,
mut layer_id,
mut visible_chunk_layer,
Expand All @@ -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);
}
}

Expand All @@ -114,19 +117,19 @@ fn randomize_equipment(mut query: Query<&mut Equipment>, server: Res<Server>) {
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!(),
Expand All @@ -135,18 +138,3 @@ fn randomize_equipment(mut query: Query<&mut Equipment>, server: Res<Server>) {
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<Equipment>>,
) {
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());
}
}
Loading

0 comments on commit 849e116

Please sign in to comment.