From 32a62392cebf268fc6b156bc4c9a24db5da3efed Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 25 Sep 2025 16:52:45 -1030 Subject: ClientsideCloseContainerEvent, MenuOpenedEvent, and CloseContainerEvent are now triggers instead of events --- azalea-client/src/plugins/inventory.rs | 151 +++++++++++++++------------ azalea-client/src/plugins/packet/game/mod.rs | 47 ++++----- 2 files changed, 105 insertions(+), 93 deletions(-) (limited to 'azalea-client/src') diff --git a/azalea-client/src/plugins/inventory.rs b/azalea-client/src/plugins/inventory.rs index 7dfe42c4..e6481577 100644 --- a/azalea-client/src/plugins/inventory.rs +++ b/azalea-client/src/plugins/inventory.rs @@ -28,7 +28,7 @@ use crate::{Client, packet::game::SendPacketEvent, respawn::perform_respawn}; pub struct InventoryPlugin; impl Plugin for InventoryPlugin { fn build(&self, app: &mut App) { - app.add_event::() + app.add_event::() .add_event::() .add_event::() .add_event::() @@ -39,7 +39,6 @@ impl Plugin for InventoryPlugin { ( handle_set_selected_hotbar_slot_event, handle_menu_opened_event, - handle_set_container_content_event, handle_container_click_event, handle_container_close_event, handle_client_side_close_container_event, @@ -51,7 +50,10 @@ impl Plugin for InventoryPlugin { .add_systems( GameTick, ensure_has_sent_carried_item.after(super::mining::handle_mining_queued), - ); + ) + .add_observer(handle_client_side_close_container_trigger) + .add_observer(handle_menu_opened_trigger) + .add_observer(handle_set_container_content_trigger); } } @@ -728,24 +730,28 @@ impl Default for Inventory { } } -/// Sent from the server when a menu (like a chest or crafting table) was -/// opened by the client. -#[derive(Event, Debug)] +/// A Bevy trigger that's fired when our client should show a new screen (like a +/// chest or crafting table). +/// +/// To watch for the menu being closed, you could use +/// [`ClientSideCloseContainerEvent`]. To close it manually, use +/// [`CloseContainerEvent`]. +#[derive(Event, Debug, Clone)] pub struct MenuOpenedEvent { pub entity: Entity, pub window_id: i32, pub menu_type: MenuKind, pub title: FormattedText, } -fn handle_menu_opened_event( - mut events: EventReader, - mut query: Query<&mut Inventory>, -) { +fn handle_menu_opened_trigger(event: Trigger, mut query: Query<&mut Inventory>) { + let mut inventory = query.get_mut(event.entity).unwrap(); + inventory.id = event.window_id; + inventory.container_menu = Some(Menu::from_kind(event.menu_type)); + inventory.container_menu_title = Some(event.title.clone()); +} +pub fn handle_menu_opened_event(mut events: EventReader, mut commands: Commands) { for event in events.read() { - let mut inventory = query.get_mut(event.entity).unwrap(); - inventory.id = event.window_id; - inventory.container_menu = Some(Menu::from_kind(event.menu_type)); - inventory.container_menu_title = Some(event.title.clone()); + commands.trigger(event.clone()); } } @@ -763,7 +769,7 @@ pub struct CloseContainerEvent { fn handle_container_close_event( query: Query<(Entity, &Inventory)>, mut events: EventReader, - mut client_side_events: EventWriter, + mut client_side_events: EventWriter, mut commands: Commands, ) { for event in events.read() { @@ -782,52 +788,62 @@ fn handle_container_close_event( container_id: inventory.id, }, )); - client_side_events.write(ClientSideCloseContainerEvent { + client_side_events.write(ClientsideCloseContainerEvent { entity: event.entity, }); } } -/// Close a container without notifying the server. +/// A Bevy trigger that's fired when our client closed a container. /// -/// Note that this also gets fired when we get a [`CloseContainerEvent`]. -#[derive(Event)] -pub struct ClientSideCloseContainerEvent { +/// This can also be triggered directly to close a container silently without +/// sending any packets to the server. You probably don't want that though, and +/// should instead use [`CloseContainerEvent`]. +/// +/// If you want to watch for a container being opened, you should use +/// [`MenuOpenedEvent`]. +#[derive(Event, Clone)] +pub struct ClientsideCloseContainerEvent { pub entity: Entity, } -pub fn handle_client_side_close_container_event( - mut events: EventReader, +pub fn handle_client_side_close_container_trigger( + event: Trigger, mut query: Query<&mut Inventory>, ) { - for event in events.read() { - let mut inventory = query.get_mut(event.entity).unwrap(); - - // copy the Player part of the container_menu to the inventory_menu - if let Some(inventory_menu) = inventory.container_menu.take() { - // this isn't the same as what vanilla does. i believe vanilla synchronizes the - // slots between inventoryMenu and containerMenu by just having the player slots - // point to the same ItemStack in memory, but emulating this in rust would - // require us to wrap our `ItemStack`s as `Arc>` which would - // have kinda terrible ergonomics. - - // the simpler solution i chose to go with here is to only copy the player slots - // when the container is closed. this is perfectly fine for vanilla, but it - // might cause issues if a server modifies id 0 while we have a container - // open... - - // if we do encounter this issue in the wild then the simplest solution would - // probably be to just add logic for updating the container_menu when the server - // tries to modify id 0 for slots within `inventory`. not implemented for now - // because i'm not sure if that's worth worrying about. - - let new_inventory = - inventory_menu.slots()[inventory_menu.player_slots_range()].to_vec(); - let new_inventory = <[ItemStack; 36]>::try_from(new_inventory).unwrap(); - *inventory.inventory_menu.as_player_mut().inventory = new_inventory; - } + let mut inventory = query.get_mut(event.entity).unwrap(); + + // copy the Player part of the container_menu to the inventory_menu + if let Some(inventory_menu) = inventory.container_menu.take() { + // this isn't the same as what vanilla does. i believe vanilla synchronizes the + // slots between inventoryMenu and containerMenu by just having the player slots + // point to the same ItemStack in memory, but emulating this in rust would + // require us to wrap our `ItemStack`s as `Arc>` which would + // have kinda terrible ergonomics. + + // the simpler solution i chose to go with here is to only copy the player slots + // when the container is closed. this is perfectly fine for vanilla, but it + // might cause issues if a server modifies id 0 while we have a container + // open... + + // if we do encounter this issue in the wild then the simplest solution would + // probably be to just add logic for updating the container_menu when the server + // tries to modify id 0 for slots within `inventory`. not implemented for now + // because i'm not sure if that's worth worrying about. + + let new_inventory = inventory_menu.slots()[inventory_menu.player_slots_range()].to_vec(); + let new_inventory = <[ItemStack; 36]>::try_from(new_inventory).unwrap(); + *inventory.inventory_menu.as_player_mut().inventory = new_inventory; + } - inventory.id = 0; - inventory.container_menu_title = None; + inventory.id = 0; + inventory.container_menu_title = None; +} +pub fn handle_client_side_close_container_event( + mut commands: Commands, + mut events: EventReader, +) { + for event in events.read() { + commands.trigger(event.clone()); } } @@ -900,34 +916,33 @@ pub fn handle_container_click_event( } } -/// Sent from the server when the contents of a container are replaced. Usually -/// triggered by the `ContainerSetContent` packet. +/// Sent from the server when the contents of a container are replaced. +/// +/// Usually triggered by the `ContainerSetContent` packet. #[derive(Event)] pub struct SetContainerContentEvent { pub entity: Entity, pub slots: Vec, pub container_id: i32, } -fn handle_set_container_content_event( - mut events: EventReader, +pub fn handle_set_container_content_trigger( + event: Trigger, mut query: Query<&mut Inventory>, ) { - for event in events.read() { - let mut inventory = query.get_mut(event.entity).unwrap(); + let mut inventory = query.get_mut(event.entity).unwrap(); - if event.container_id != inventory.id { - warn!( - "Got SetContainerContentEvent for container with ID {}, but the current container ID is {}", - event.container_id, inventory.id - ); - continue; - } + if event.container_id != inventory.id { + warn!( + "Got SetContainerContentEvent for container with ID {}, but the current container ID is {}", + event.container_id, inventory.id + ); + return; + } - let menu = inventory.menu_mut(); - for (i, slot) in event.slots.iter().enumerate() { - if let Some(slot_mut) = menu.slot_mut(i) { - *slot_mut = slot.clone(); - } + let menu = inventory.menu_mut(); + for (i, slot) in event.slots.iter().enumerate() { + if let Some(slot_mut) = menu.slot_mut(i) { + *slot_mut = slot.clone(); } } } diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index 49523002..bc1d1752 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -31,7 +31,7 @@ use crate::{ disconnect::DisconnectEvent, interact::BlockStatePredictionHandler, inventory::{ - ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent, + ClientsideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent, }, local_player::{Hunger, InstanceHolder, LocalGameMode, TabList}, movement::{KnockbackEvent, KnockbackType}, @@ -1136,28 +1136,25 @@ impl GamePacketHandler<'_> { pub fn container_set_content(&mut self, p: &ClientboundContainerSetContent) { debug!("Got container set content packet {p:?}"); - as_system::<(Query<&mut Inventory>, EventWriter<_>)>( - self.ecs, - |(mut query, mut events)| { - let mut inventory = query.get_mut(self.player).unwrap(); - - // container id 0 is always the player's inventory - if p.container_id == 0 { - // this is just so it has the same type as the `else` block - for (i, slot) in p.items.iter().enumerate() { - if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) { - *slot_mut = slot.clone(); - } + as_system::<(Commands, Query<&mut Inventory>)>(self.ecs, |(mut commands, mut query)| { + let mut inventory = query.get_mut(self.player).unwrap(); + + // container id 0 is always the player's inventory + if p.container_id == 0 { + // this is just so it has the same type as the `else` block + for (i, slot) in p.items.iter().enumerate() { + if let Some(slot_mut) = inventory.inventory_menu.slot_mut(i) { + *slot_mut = slot.clone(); } - } else { - events.write(SetContainerContentEvent { - entity: self.player, - slots: p.items.clone(), - container_id: p.container_id, - }); } - }, - ); + } else { + commands.trigger(SetContainerContentEvent { + entity: self.player, + slots: p.items.clone(), + container_id: p.container_id, + }); + } + }); } pub fn container_set_data(&mut self, p: &ClientboundContainerSetData) { @@ -1215,8 +1212,8 @@ impl GamePacketHandler<'_> { debug!("Got container close packet {p:?}"); - as_system::>(self.ecs, |mut events| { - events.write(ClientSideCloseContainerEvent { + as_system::(self.ecs, |mut commands| { + commands.trigger(ClientsideCloseContainerEvent { entity: self.player, }); }); @@ -1266,8 +1263,8 @@ impl GamePacketHandler<'_> { pub fn open_screen(&mut self, p: &ClientboundOpenScreen) { debug!("Got open screen packet {p:?}"); - as_system::>(self.ecs, |mut events| { - events.write(MenuOpenedEvent { + as_system::(self.ecs, |mut commands| { + commands.trigger(MenuOpenedEvent { entity: self.player, window_id: p.container_id, menu_type: p.menu_type, -- cgit v1.2.3