From 9254f388c11e3f340c0c1f2a8e0058c325076671 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 13 Mar 2023 21:53:53 -0500 Subject: [PATCH] container handle --- azalea-client/src/client.rs | 24 +++--- azalea-client/src/inventory_plugin.rs | 115 ++++++++++++++++++++++++-- azalea/examples/testbot.rs | 16 ++-- 3 files changed, 128 insertions(+), 27 deletions(-) diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 301a77db..53b27f62 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -43,7 +43,7 @@ use azalea_world::{ entity::{EntityPlugin, EntityUpdateSet, Local, Position, WorldName}, Instance, InstanceContainer, PartialInstance, }; -use bevy_app::{App, CoreSchedule, Plugin, PluginGroup, PluginGroupBuilder}; +use bevy_app::{App, CoreSchedule, IntoSystemAppConfig, Plugin, PluginGroup, PluginGroupBuilder}; use bevy_ecs::{ bundle::Bundle, component::Component, @@ -632,32 +632,32 @@ pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<( } /// A resource that contains a [`broadcast::Sender`] that will be sent every -/// time the ECS schedule is run. +/// Minecraft tick. /// /// This is useful for running code every schedule from async user code. /// /// ```no_run /// let mut receiver = { /// let ecs = client.ecs.lock(); -/// let schedule_broadcast = ecs.resource::(); -/// schedule_broadcast.subscribe() +/// let tick_broadcast = ecs.resource::(); +/// tick_broadcast.subscribe() /// }; /// while receiver.recv().await.is_ok() { /// // do something /// } /// ``` #[derive(Resource, Deref)] -pub struct RanScheduleBroadcast(broadcast::Sender<()>); +pub struct TickBroadcast(broadcast::Sender<()>); -fn send_ran_schedule_event(ran_schedule_broadcast: ResMut) { - let _ = ran_schedule_broadcast.0.send(()); +fn send_tick_broadcast(tick_broadcast: ResMut) { + let _ = tick_broadcast.0.send(()); } /// A plugin that makes the [`RanScheduleBroadcast`] resource available. -pub struct RanSchedulePlugin; -impl Plugin for RanSchedulePlugin { +pub struct TickBroadcastPlugin; +impl Plugin for TickBroadcastPlugin { fn build(&self, app: &mut App) { - app.insert_resource(RanScheduleBroadcast(broadcast::channel(1).0)) - .add_system(send_ran_schedule_event); + app.insert_resource(TickBroadcast(broadcast::channel(1).0)) + .add_system(send_tick_broadcast.in_schedule(CoreSchedule::FixedUpdate)); } } @@ -681,6 +681,6 @@ impl PluginGroup for DefaultPlugins { .add(DisconnectPlugin) .add(PlayerMovePlugin) .add(InteractPlugin) - .add(RanSchedulePlugin) + .add(TickBroadcastPlugin) } } diff --git a/azalea-client/src/inventory_plugin.rs b/azalea-client/src/inventory_plugin.rs index 0dc9a49b..c610dd13 100644 --- a/azalea-client/src/inventory_plugin.rs +++ b/azalea-client/src/inventory_plugin.rs @@ -1,29 +1,39 @@ +use std::fmt::{Debug, Formatter}; + use azalea_chat::FormattedText; use azalea_core::BlockPos; use azalea_inventory::{ItemSlot, Menu}; +use azalea_protocol::packets::game::serverbound_container_close_packet::ServerboundContainerClosePacket; use azalea_registry::MenuKind; use bevy_app::{App, Plugin}; use bevy_ecs::{ component::Component, entity::Entity, event::EventReader, - schedule::IntoSystemConfigs, + prelude::EventWriter, + schedule::{IntoSystemConfig, IntoSystemConfigs}, system::{Commands, Query}, }; -use crate::{client::RanScheduleBroadcast, Client}; +use crate::{client::TickBroadcast, local_player::handle_send_packet_event, Client, LocalPlayer}; pub struct InventoryPlugin; impl Plugin for InventoryPlugin { fn build(&self, app: &mut App) { app.add_event::() .add_event::() + .add_event::() .add_systems( ( handle_menu_opened_event, handle_client_side_close_container_event, ) .chain(), + ) + .add_system( + handle_container_close_event + .before(handle_send_packet_event) + .before(handle_client_side_close_container_event), ); } } @@ -32,7 +42,7 @@ impl Plugin for InventoryPlugin { pub struct WaitingForInventoryOpen; impl Client { - pub async fn open_container(&mut self, pos: BlockPos) -> Option { + pub async fn open_container(&mut self, pos: BlockPos) -> Option { self.ecs .lock() .entity_mut(self.entity) @@ -41,8 +51,8 @@ impl Client { let mut receiver = { let ecs = self.ecs.lock(); - let schedule_broadcast = ecs.resource::(); - schedule_broadcast.subscribe() + let tick_broadcast = ecs.resource::(); + tick_broadcast.subscribe() }; while receiver.recv().await.is_ok() { let ecs = self.ecs.lock(); @@ -52,13 +62,69 @@ impl Client { } let ecs = self.ecs.lock(); - let inventory = ecs.get::(self.entity); - if let Some(inventory) = inventory { - inventory.container_menu.clone() + let inventory = ecs + .get::(self.entity) + .expect("no inventory"); + if inventory.id == 0 { + None + } else { + Some(ContainerHandle { + id: inventory.id, + client: self.clone(), + }) + } + } + + /// Return the menu that is currently open. If no menu is open, this will + /// have the player's inventory. + pub fn menu(&self) -> Menu { + let mut ecs = self.ecs.lock(); + let inventory = self.query::<&InventoryComponent>(&mut ecs); + inventory.menu().clone() + } +} + +/// A handle to the open container. The container will be closed once this is +/// dropped. +pub struct ContainerHandle { + pub id: i8, + client: Client, +} +impl Drop for ContainerHandle { + fn drop(&mut self) { + self.client.ecs.lock().send_event(CloseContainerEvent { + entity: self.client.entity, + id: self.id, + }); + } +} +impl Debug for ContainerHandle { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ContainerHandle") + .field("id", &self.id) + .finish() + } +} +impl ContainerHandle { + /// Returns the menu of the container. If the container is closed, this + /// will return `None`. + pub fn menu(&self) -> Option { + let ecs = self.client.ecs.lock(); + let inventory = ecs + .get::(self.client.entity) + .expect("no inventory"); + if inventory.id == self.id { + Some(inventory.container_menu.clone().unwrap()) } else { None } } + + /// Returns the item slots in the container. If the container is closed, + /// this will return `None`. + pub fn contents(&self) -> Option> { + self.menu().map(|menu| menu.contents()) + } } /// A component present on all local players that have an inventory. @@ -145,6 +211,39 @@ fn handle_menu_opened_event( } } +/// Tell the server that we want to close a container. +/// +/// Note that this is also sent when the client closes its own inventory, even +/// though there is no packet for opening its inventory. +pub struct CloseContainerEvent { + pub entity: Entity, + /// The ID of the container to close. 0 for the player's inventory. If this + /// is not the same as the currently open inventory, nothing will happen. + pub id: i8, +} +fn handle_container_close_event( + mut events: EventReader, + mut client_side_events: EventWriter, + query: Query<(&LocalPlayer, &InventoryComponent)>, +) { + for event in events.iter() { + let (local_player, inventory) = query.get(event.entity).unwrap(); + if event.id != inventory.id { + continue; + } + + local_player.write_packet( + ServerboundContainerClosePacket { + container_id: inventory.id as u8, + } + .get(), + ); + client_side_events.send(ClientSideCloseContainerEvent { + entity: event.entity, + }); + } +} + /// Close a container without notifying the server. /// /// Note that this also gets fired when we get a [`CloseContainerEvent`]. diff --git a/azalea/examples/testbot.rs b/azalea/examples/testbot.rs index f4908295..3fe9253c 100644 --- a/azalea/examples/testbot.rs +++ b/azalea/examples/testbot.rs @@ -7,7 +7,6 @@ use azalea::entity::metadata::Player; use azalea::entity::{EyeHeight, Position}; use azalea::interact::HitResultComponent; use azalea::inventory::ItemSlot; -use azalea::inventory_plugin::InventoryComponent; use azalea::pathfinder::BlockPosGoal; use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection}; use azalea::{Account, Client, Event}; @@ -145,9 +144,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result< std::thread::sleep(Duration::from_millis(1000)); } "inventory" => { - let mut ecs = bot.ecs.lock(); - let inventory = bot.query::<&InventoryComponent>(&mut ecs); - println!("inventory: {:?}", inventory.menu()); + println!("inventory: {:?}", bot.menu()); } "findblock" => { let target_pos = bot @@ -196,11 +193,16 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result< }; bot.look_at(target_pos.center()); let container = bot.open_container(target_pos).await; + println!("container: {:?}", container); if let Some(container) = container { - for item in container.contents() { - if let ItemSlot::Present(item) = item { - println!("item: {:?}", item); + if let Some(contents) = container.contents() { + for item in contents { + if let ItemSlot::Present(item) = item { + println!("item: {:?}", item); + } } + } else { + println!("container was immediately closed"); } } else { println!("no container found");