Skip to content

Commit 3924bd0

Browse files
committed
more tests + fix client changes
1 parent 442eecd commit 3924bd0

File tree

4 files changed

+170
-5
lines changed

4 files changed

+170
-5
lines changed

crates/valence_equipment/src/inventory_sync.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use valence_inventory::player_inventory::PlayerInventory;
2-
use valence_inventory::{HeldItem, Inventory};
2+
use valence_inventory::{HeldItem, Inventory, UpdateSelectedSlotEvent};
33
use valence_server::entity::player::PlayerEntity;
44

55
use super::*;
@@ -9,6 +9,8 @@ pub struct EquipmentInventorySync;
99
/// Syncs the player [`Equipment`] with the [`Inventory`].
1010
/// If a change in the player's inventory and in the equipment occurs in the
1111
/// same tick, the equipment change has priority.
12+
/// Note: This system only handles direct changes to the held item (not actual
13+
/// changes from the client) see [`equipment_held_item_sync_from_client`]
1214
pub(crate) fn equipment_inventory_sync(
1315
mut clients: Query<
1416
(&mut Equipment, &mut Inventory, &mut HeldItem),
@@ -18,13 +20,16 @@ pub(crate) fn equipment_inventory_sync(
1820
With<PlayerEntity>,
1921
),
2022
>,
23+
// mut change_slot_events
2124
) {
2225
for (mut equipment, mut inventory, held_item) in &mut clients {
2326
// Equipment change has priority over held item changes
2427
if equipment.changed & (1 << Equipment::MAIN_HAND_IDX) != 0 {
2528
let item = equipment.main_hand().clone();
2629
inventory.set_slot(held_item.slot(), item);
2730
} else if held_item.is_changed() {
31+
// This will only be called if we change the held item from valence,
32+
// the client change is handled in `equipment_held_item_sync_from_client`
2833
let item = inventory.slot(held_item.slot()).clone();
2934
equipment.set_main_hand(item);
3035
}
@@ -37,19 +42,39 @@ pub(crate) fn equipment_inventory_sync(
3742
(Equipment::FEET_IDX, PlayerInventory::SLOT_FEET),
3843
];
3944

45+
// We cant rely on the changed bitfield of inventory here
46+
// because that gets reset when the client changes the inventory
47+
4048
for (equipment_slot, inventory_slot) in slots {
4149
// Equipment has priority over inventory changes
4250
if equipment.changed & (1 << equipment_slot) != 0 {
4351
let item = equipment.slot(equipment_slot).clone();
4452
inventory.set_slot(inventory_slot, item);
45-
} else if inventory.changed & (1 << inventory_slot) != 0 {
53+
} else if inventory.is_changed() {
4654
let item = inventory.slot(inventory_slot).clone();
4755
equipment.set_slot(equipment_slot, item);
4856
}
4957
}
5058
}
5159
}
5260

61+
/// Handles the case where the client changes the slot (the bevy change is
62+
/// suppressed for this). See
63+
/// [`valence_inventory::handle_update_selected_slot`].
64+
pub(crate) fn equipment_held_item_sync_from_client(
65+
mut clients: Query<(&HeldItem, &Inventory, &mut Equipment), With<EquipmentInventorySync>>,
66+
mut events: EventReader<UpdateSelectedSlotEvent>,
67+
) {
68+
for event in events.read() {
69+
let Ok((held_item, inventory, mut equipment)) = clients.get_mut(event.client) else {
70+
continue;
71+
};
72+
73+
let item = inventory.slot(held_item.slot()).clone();
74+
equipment.set_main_hand(item);
75+
}
76+
}
77+
5378
pub(crate) fn on_attach_inventory_sync(
5479
entities: Query<Option<&PlayerEntity>, (Added<EquipmentInventorySync>, With<Inventory>)>,
5580
) {

crates/valence_equipment/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ impl Plugin for EquipmentPlugin {
2222
on_entity_init,
2323
inventory_sync::on_attach_inventory_sync,
2424
inventory_sync::equipment_inventory_sync,
25+
inventory_sync::equipment_held_item_sync_from_client,
2526
),
2627
)
2728
.add_systems(

crates/valence_inventory/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,9 @@ fn handle_update_selected_slot(
14081408
for packet in packets.read() {
14091409
if let Some(pkt) = packet.decode::<UpdateSelectedSlotC2s>() {
14101410
if let Ok(mut mut_held) = clients.get_mut(packet.client) {
1411+
// We bypass the change detection here because the server listens for changes
1412+
// of `HeldItem` in order to send the update to the client.
1413+
// This is not required here because the update is coming from the client.
14111414
let held = mut_held.bypass_change_detection();
14121415
if pkt.slot > 8 {
14131416
// The client is trying to interact with a slot that does not exist, ignore.

src/tests/equipment.rs

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use valence_equipment::{Equipment, EquipmentInventorySync};
22
use valence_inventory::player_inventory::PlayerInventory;
3-
use valence_inventory::Inventory;
3+
use valence_inventory::{ClickMode, ClientInventoryState, Inventory, SlotChange};
44
use valence_server::entity::armor_stand::ArmorStandEntityBundle;
55
use valence_server::entity::item::ItemEntityBundle;
66
use valence_server::entity::zombie::ZombieEntityBundle;
77
use valence_server::entity::{EntityLayerId, Position};
88
use valence_server::math::DVec3;
9-
use valence_server::protocol::packets::play::EntityEquipmentUpdateS2c;
9+
use valence_server::protocol::packets::play::{
10+
ClickSlotC2s, EntityEquipmentUpdateS2c, UpdateSelectedSlotC2s,
11+
};
1012
use valence_server::{ItemKind, ItemStack};
1113

1214
use crate::testing::ScenarioSingleClient;
@@ -298,7 +300,7 @@ fn test_inventory_sync_from_equipment() {
298300
}
299301

300302
#[test]
301-
fn test_inventory_sync_from_inventory() {
303+
fn test_equipment_sync_from_inventory() {
302304
let ScenarioSingleClient {
303305
mut app,
304306
client,
@@ -405,3 +407,137 @@ fn test_equipment_priority_over_inventory() {
405407
ItemStack::new(ItemKind::GoldenChestplate, 1, None)
406408
);
407409
}
410+
411+
#[test]
412+
fn test_equipment_change_from_player() {
413+
let ScenarioSingleClient {
414+
mut app,
415+
client,
416+
mut helper,
417+
..
418+
} = ScenarioSingleClient::new();
419+
420+
// Process a tick to get past the "on join" logic.
421+
app.update();
422+
helper.clear_received();
423+
424+
app.world_mut()
425+
.entity_mut(client)
426+
.insert(EquipmentInventorySync);
427+
428+
let mut player_inventory = app
429+
.world_mut()
430+
.get_mut::<Inventory>(client)
431+
.expect("could not get player equipment");
432+
433+
player_inventory.set_slot(36, ItemStack::new(ItemKind::DiamondChestplate, 1, None));
434+
app.update();
435+
helper.clear_received();
436+
437+
let state_id = app
438+
.world()
439+
.get::<ClientInventoryState>(client)
440+
.expect("could not get player equipment")
441+
.state_id();
442+
443+
app.update();
444+
445+
helper.send(&ClickSlotC2s {
446+
window_id: 0,
447+
button: 0,
448+
mode: ClickMode::Hotbar,
449+
state_id: state_id.0.into(),
450+
slot_idx: 36,
451+
slot_changes: vec![
452+
SlotChange {
453+
idx: 36,
454+
stack: ItemStack::EMPTY,
455+
},
456+
SlotChange {
457+
idx: PlayerInventory::SLOT_CHEST as i16,
458+
stack: ItemStack::new(ItemKind::DiamondChestplate, 1, None),
459+
},
460+
]
461+
.into(),
462+
carried_item: ItemStack::EMPTY,
463+
});
464+
465+
app.update();
466+
app.update();
467+
468+
let player_inventory = app
469+
.world()
470+
.get::<Inventory>(client)
471+
.expect("could not get player equipment");
472+
473+
let player_equipment = app
474+
.world()
475+
.get::<Equipment>(client)
476+
.expect("could not get player equipment");
477+
478+
assert_eq!(
479+
player_inventory.slot(PlayerInventory::SLOT_CHEST),
480+
&ItemStack::new(ItemKind::DiamondChestplate, 1, None)
481+
);
482+
483+
assert_eq!(player_inventory.slot(36), &ItemStack::EMPTY);
484+
485+
assert_eq!(
486+
player_equipment.chest(),
487+
&ItemStack::new(ItemKind::DiamondChestplate, 1, None)
488+
);
489+
}
490+
491+
#[test]
492+
fn test_held_item_change_from_client() {
493+
let ScenarioSingleClient {
494+
mut app,
495+
client,
496+
mut helper,
497+
..
498+
} = ScenarioSingleClient::new();
499+
500+
// Process a tick to get past the "on join" logic.
501+
app.update();
502+
helper.clear_received();
503+
504+
app.world_mut()
505+
.entity_mut(client)
506+
.insert(EquipmentInventorySync);
507+
508+
let mut player_inventory = app
509+
.world_mut()
510+
.get_mut::<Inventory>(client)
511+
.expect("could not get player equipment");
512+
513+
player_inventory.set_slot(36, ItemStack::new(ItemKind::DiamondSword, 1, None));
514+
player_inventory.set_slot(37, ItemStack::new(ItemKind::IronSword, 1, None));
515+
516+
app.update();
517+
518+
let player_equipment = app
519+
.world()
520+
.get::<Equipment>(client)
521+
.expect("could not get player equipment");
522+
523+
assert_eq!(
524+
player_equipment.main_hand(),
525+
&ItemStack::new(ItemKind::DiamondSword, 1, None)
526+
);
527+
528+
// Change the held item from the client
529+
helper.send(&UpdateSelectedSlotC2s { slot: 1 });
530+
531+
app.update(); // handle change slot
532+
app.update(); // handle change equipment
533+
534+
let player_equipment = app
535+
.world()
536+
.get::<Equipment>(client)
537+
.expect("could not get player equipment");
538+
539+
assert_eq!(
540+
player_equipment.main_hand(),
541+
&ItemStack::new(ItemKind::IronSword, 1, None)
542+
);
543+
}

0 commit comments

Comments
 (0)