Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Equipment support #663

Merged
merged 24 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ default = [
"advancement",
"anvil",
"boss_bar",
"equipment",
"inventory",
"log",
"network",
Expand All @@ -32,6 +33,7 @@ default = [
advancement = ["dep:valence_advancement"]
anvil = ["dep:valence_anvil"]
boss_bar = ["dep:valence_boss_bar"]
equipment = ["dep:valence_equipment"]
inventory = ["dep:valence_inventory"]
log = ["dep:bevy_log"]
network = ["dep:valence_network"]
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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" }
Expand Down
366 changes: 192 additions & 174 deletions assets/depgraph.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions crates/valence_equipment/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[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
238 changes: 238 additions & 0 deletions crates/valence_equipment/src/lib.rs
maxomatic458 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
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;
use valence_server::protocol::packets::play::EntityEquipmentUpdateS2c;
use valence_server::protocol::WritePacket;
use valence_server::{EntityLayer, ItemStack, Layer};

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

#[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 new(
main_hand: ItemStack,
off_hand: ItemStack,
boots: ItemStack,
leggings: ItemStack,
chestplate: ItemStack,
helmet: ItemStack,
) -> Self {
Self {
equipment: [main_hand, off_hand, boots, leggings, chestplate, helmet],
changed: 0,
}
}

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);
}
}

pub fn is_default(&self) -> bool {
self.equipment.iter().all(|item| item.is_empty())
}
}

#[derive(Debug, Clone)]
pub struct EquipmentSlotChange {
idx: u8,
stack: ItemStack,
}

#[derive(Debug, Clone, Event)]
pub struct EquipmentChangeEvent {
pub client: Entity,
pub changed: Vec<EquipmentSlotChange>,
}

fn update_equipment(
mut clients: Query<
(Entity, &EntityId, &EntityLayerId, &Position, &mut Equipment),
Changed<Equipment>,
>,
mut event_writer: EventWriter<EquipmentChangeEvent>,
mut entity_layer: Query<&mut EntityLayer>,
) {
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<EquipmentSlotChange> =
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(),
});
}
}

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,
});

equipment.changed = 0;
}
}
}

/// 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<LoadEntityForClientEvent>,
) {
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<EquipmentEntry> = 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<Entity, (Added<LivingEntity>, Without<Equipment>)>,
) {
for entity in &mut entities {
commands.entity(entity).insert(Equipment::default());
}
}
Loading
Loading