From 6286e953a678c2f9b0d79c6d54cc2bfafd2ddd13 Mon Sep 17 00:00:00 2001 From: mat Date: Mon, 13 Mar 2023 21:10:21 -0500 Subject: [PATCH] chest --- Cargo.lock | 1 + azalea-client/src/client.rs | 39 ++++++++- azalea-client/src/interact.rs | 5 +- .../src/{inventory.rs => inventory_plugin.rs} | 83 ++++++++++++++++++- azalea-client/src/lib.rs | 2 +- azalea-client/src/packet_handling.rs | 14 +++- .../azalea-inventory-macros/src/menu_impl.rs | 78 +++++++++++++++++ azalea-inventory/src/lib.rs | 30 +++---- azalea-inventory/src/slot.rs | 9 ++ .../game/clientbound_open_screen_packet.rs | 2 +- azalea-registry/src/lib.rs | 2 +- azalea/Cargo.toml | 1 + azalea/examples/inventory_art.rs | 5 +- azalea/examples/testbot.rs | 42 +++++++--- .../examples/todo/craft_dig_straight_down.rs | 6 +- azalea/src/lib.rs | 1 + codegen/lib/code/registry.py | 6 +- 17 files changed, 282 insertions(+), 44 deletions(-) rename azalea-client/src/{inventory.rs => inventory_plugin.rs} (54%) diff --git a/Cargo.lock b/Cargo.lock index 8dd9306a..272e8393 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,7 @@ dependencies = [ "azalea-chat", "azalea-client", "azalea-core", + "azalea-inventory", "azalea-physics", "azalea-protocol", "azalea-registry", diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 7ef1a06b..301a77db 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -3,7 +3,7 @@ use crate::{ disconnect::{DisconnectEvent, DisconnectPlugin}, events::{Event, EventPlugin, LocalPlayerEvents}, interact::{CurrentSequenceNumber, InteractPlugin}, - inventory::{InventoryComponent, InventoryPlugin}, + inventory_plugin::{InventoryComponent, InventoryPlugin}, local_player::{ death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent, LocalPlayer, PhysicsState, SendPacketEvent, @@ -50,6 +50,7 @@ use bevy_ecs::{ entity::Entity, schedule::IntoSystemConfig, schedule::{LogLevel, ScheduleBuildSettings, ScheduleLabel}, + system::{ResMut, Resource}, world::World, }; use bevy_log::LogPlugin; @@ -59,7 +60,10 @@ use log::{debug, error}; use parking_lot::{Mutex, RwLock}; use std::{collections::HashMap, fmt::Debug, io, net::SocketAddr, sync::Arc, time::Duration}; use thiserror::Error; -use tokio::{sync::mpsc, time}; +use tokio::{ + sync::{broadcast, mpsc}, + time, +}; use uuid::Uuid; /// `Client` has the things that a user interacting with the library will want. @@ -627,6 +631,36 @@ 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. +/// +/// 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() +/// }; +/// while receiver.recv().await.is_ok() { +/// // do something +/// } +/// ``` +#[derive(Resource, Deref)] +pub struct RanScheduleBroadcast(broadcast::Sender<()>); + +fn send_ran_schedule_event(ran_schedule_broadcast: ResMut) { + let _ = ran_schedule_broadcast.0.send(()); +} +/// A plugin that makes the [`RanScheduleBroadcast`] resource available. +pub struct RanSchedulePlugin; +impl Plugin for RanSchedulePlugin { + fn build(&self, app: &mut App) { + app.insert_resource(RanScheduleBroadcast(broadcast::channel(1).0)) + .add_system(send_ran_schedule_event); + } +} + /// This plugin group will add all the default plugins necessary for Azalea to /// work. pub struct DefaultPlugins; @@ -647,5 +681,6 @@ impl PluginGroup for DefaultPlugins { .add(DisconnectPlugin) .add(PlayerMovePlugin) .add(InteractPlugin) + .add(RanSchedulePlugin) } } diff --git a/azalea-client/src/interact.rs b/azalea-client/src/interact.rs index 6b8faf27..ec5ed87b 100644 --- a/azalea-client/src/interact.rs +++ b/azalea-client/src/interact.rs @@ -30,10 +30,11 @@ impl Plugin for InteractPlugin { fn build(&self, app: &mut App) { app.add_event::().add_systems( ( - handle_block_interact_event, update_hit_result_component.after(clamp_look_direction), + handle_block_interact_event, ) - .before(handle_send_packet_event), + .before(handle_send_packet_event) + .chain(), ); } } diff --git a/azalea-client/src/inventory.rs b/azalea-client/src/inventory_plugin.rs similarity index 54% rename from azalea-client/src/inventory.rs rename to azalea-client/src/inventory_plugin.rs index bbe693f8..0dc9a49b 100644 --- a/azalea-client/src/inventory.rs +++ b/azalea-client/src/inventory_plugin.rs @@ -1,12 +1,63 @@ +use azalea_chat::FormattedText; +use azalea_core::BlockPos; use azalea_inventory::{ItemSlot, Menu}; +use azalea_registry::MenuKind; use bevy_app::{App, Plugin}; -use bevy_ecs::{component::Component, entity::Entity, event::EventReader, system::Query}; +use bevy_ecs::{ + component::Component, + entity::Entity, + event::EventReader, + schedule::IntoSystemConfigs, + system::{Commands, Query}, +}; + +use crate::{client::RanScheduleBroadcast, Client}; pub struct InventoryPlugin; impl Plugin for InventoryPlugin { fn build(&self, app: &mut App) { app.add_event::() - .add_system(handle_client_side_close_container_event); + .add_event::() + .add_systems( + ( + handle_menu_opened_event, + handle_client_side_close_container_event, + ) + .chain(), + ); + } +} + +#[derive(Component, Debug)] +pub struct WaitingForInventoryOpen; + +impl Client { + pub async fn open_container(&mut self, pos: BlockPos) -> Option { + self.ecs + .lock() + .entity_mut(self.entity) + .insert(WaitingForInventoryOpen); + self.block_interact(pos); + + let mut receiver = { + let ecs = self.ecs.lock(); + let schedule_broadcast = ecs.resource::(); + schedule_broadcast.subscribe() + }; + while receiver.recv().await.is_ok() { + let ecs = self.ecs.lock(); + if ecs.get::(self.entity).is_none() { + break; + } + } + + let ecs = self.ecs.lock(); + let inventory = ecs.get::(self.entity); + if let Some(inventory) = inventory { + inventory.container_menu.clone() + } else { + None + } } } @@ -33,7 +84,7 @@ pub struct InventoryComponent { pub carried: ItemSlot, /// An identifier used by the server to track client inventory desyncs. pub state_id: u32, - // minecraft also has these fields, but i don't need they're necessary?: + // minecraft also has these fields, but i don't think they're necessary?: // private final NonNullList remoteSlots; // private final IntList remoteDataSlots; // private ItemStack remoteCarried; @@ -70,9 +121,33 @@ impl Default for InventoryComponent { } } +/// Sent from the server when a menu (like a chest or crafting table) was +/// opened by the client. +pub struct MenuOpenedEvent { + pub entity: Entity, + pub window_id: u32, + pub menu_type: MenuKind, + pub title: FormattedText, +} +fn handle_menu_opened_event( + mut commands: Commands, + mut events: EventReader, + mut query: Query<&mut InventoryComponent>, +) { + for event in events.iter() { + commands + .entity(event.entity) + .remove::(); + + let mut inventory = query.get_mut(event.entity).unwrap(); + inventory.id = event.window_id as i8; + inventory.container_menu = Some(Menu::from_kind(event.menu_type)); + } +} + /// Close a container without notifying the server. /// -/// Note that this also gets fired from [`CloseContainerEvent`]. +/// Note that this also gets fired when we get a [`CloseContainerEvent`]. pub struct ClientSideCloseContainerEvent { pub entity: Entity, } diff --git a/azalea-client/src/lib.rs b/azalea-client/src/lib.rs index e9d5bd65..310cddc4 100644 --- a/azalea-client/src/lib.rs +++ b/azalea-client/src/lib.rs @@ -19,7 +19,7 @@ mod entity_query; mod events; mod get_mc_dir; pub mod interact; -pub mod inventory; +pub mod inventory_plugin; mod local_player; mod movement; pub mod packet_handling; diff --git a/azalea-client/src/packet_handling.rs b/azalea-client/src/packet_handling.rs index a92bf8d0..c60f58e6 100644 --- a/azalea-client/src/packet_handling.rs +++ b/azalea-client/src/packet_handling.rs @@ -39,7 +39,7 @@ use crate::{ chat::{ChatPacket, ChatReceivedEvent}, client::TabList, disconnect::DisconnectEvent, - inventory::{ClientSideCloseContainerEvent, InventoryComponent}, + inventory_plugin::{ClientSideCloseContainerEvent, InventoryComponent, MenuOpenedEvent}, local_player::{GameProfileComponent, LocalGameMode, LocalPlayer}, ClientInformation, PlayerInfo, }; @@ -971,7 +971,17 @@ fn process_packet_events(ecs: &mut World) { ClientboundGamePacket::MerchantOffers(_) => {} ClientboundGamePacket::MoveVehicle(_) => {} ClientboundGamePacket::OpenBook(_) => {} - ClientboundGamePacket::OpenScreen(_) => {} + ClientboundGamePacket::OpenScreen(p) => { + let mut system_state: SystemState> = + SystemState::new(ecs); + let mut menu_opened_events = system_state.get_mut(ecs); + menu_opened_events.send(MenuOpenedEvent { + entity: player_entity, + window_id: p.container_id, + menu_type: p.menu_type, + title: p.title, + }) + } ClientboundGamePacket::OpenSignEditor(_) => {} ClientboundGamePacket::Ping(_) => {} ClientboundGamePacket::PlaceGhostRecipe(_) => {} diff --git a/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs index b63fd6d6..c704c1f9 100644 --- a/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs +++ b/azalea-inventory/azalea-inventory-macros/src/menu_impl.rs @@ -5,6 +5,8 @@ use quote::quote; pub fn generate(input: &DeclareMenus) -> TokenStream { let mut slot_mut_match_variants = quote! {}; let mut len_match_variants = quote! {}; + let mut kind_match_variants = quote! {}; + let mut contents_match_variants = quote! {}; let mut hotbar_slot_start = 0; let mut hotbar_slot_end = 0; @@ -12,6 +14,8 @@ pub fn generate(input: &DeclareMenus) -> TokenStream { for menu in &input.menus { slot_mut_match_variants.extend(generate_match_variant_for_slot_mut(menu)); len_match_variants.extend(generate_match_variant_for_len(menu)); + kind_match_variants.extend(generate_match_variant_for_kind(menu)); + contents_match_variants.extend(generate_match_variant_for_contents(menu)); // this part is only used to generate `Player::is_hotbar_slot` if menu.name == "Player" { @@ -55,6 +59,19 @@ pub fn generate(input: &DeclareMenus) -> TokenStream { #len_match_variants } } + + pub fn from_kind(kind: azalea_registry::MenuKind) -> Self { + match kind { + #kind_match_variants + } + } + + /// Return the contents of the menu, not including the player's inventory. + pub fn contents(&self) -> Vec { + match self { + #contents_match_variants + } + } } } } @@ -114,6 +131,67 @@ pub fn generate_match_variant_for_len(menu: &Menu) -> TokenStream { ) } +pub fn generate_match_variant_for_kind(menu: &Menu) -> TokenStream { + // azalea_registry::MenuKind::Player => Menu::Player(Player::default()), + // azalea_registry::MenuKind::Generic9x3 => Menu::Generic9x3 { contents: + // Default::default(), player: Default::default() }, + + let menu_name = &menu.name; + let menu_field_names = if menu.name == "Player" { + return quote! {}; + } else { + let mut menu_field_names = quote! {}; + for field in &menu.fields { + let field_name = &field.name; + menu_field_names.extend(quote! { #field_name: Default::default(), }) + } + quote! { { #menu_field_names } } + }; + + quote! { + azalea_registry::MenuKind::#menu_name => Menu::#menu_name #menu_field_names, + } +} + +pub fn generate_match_variant_for_contents(menu: &Menu) -> TokenStream { + // Menu::Generic9x3(m) => { + // let mut contents = Vec::new(); + // contents.extend(player.m.iter().copied()); + // ... + // contents + // }, + // Menu::Generic9x3(m) => { + // let mut contents = Vec::new(); + // contents.extend(m.contents.iter().copied()); + // contents + // }, + + let mut instructions = quote! {}; + let mut length = 0; + for field in &menu.fields { + let field_name = &field.name; + if field_name == "player" { + continue; + } + instructions.extend(if field.length == 1 { + quote! { items.push(#field_name.clone()); } + } else { + quote! { items.extend(#field_name.iter().cloned()); } + }); + length += field.length; + } + + generate_matcher( + menu, + "e! { + let mut items = Vec::with_capacity(#length); + #instructions + items + }, + true, + ) +} + fn generate_matcher(menu: &Menu, match_arms: &TokenStream, needs_fields: bool) -> TokenStream { let menu_name = &menu.name; let menu_field_names = if needs_fields { diff --git a/azalea-inventory/src/lib.rs b/azalea-inventory/src/lib.rs index 84b6282b..7321bf7d 100644 --- a/azalea-inventory/src/lib.rs +++ b/azalea-inventory/src/lib.rs @@ -27,6 +27,21 @@ impl Default for SlotList { } } +impl Menu { + /// Get the [`Player`] from this [`Menu`]. + /// + /// # Panics + /// + /// Will panic if the menu isn't `Menu::Player`. + pub fn as_player(&self) -> &Player { + if let Menu::Player(player) = &self { + player + } else { + unreachable!("Called `Menu::as_player` on a menu that wasn't `Player`.") + } + } +} + // the player inventory part is always the last 36 slots (except in the Player // menu), so we don't have to explicitly specify it @@ -142,18 +157,3 @@ declare_menus! { result: 1, }, } - -impl Menu { - /// Get the [`Player`] from this [`Menu`]. - /// - /// # Panics - /// - /// Will panic if the menu isn't `Menu::Player`. - pub fn as_player(&self) -> &Player { - if let Menu::Player(player) = &self { - player - } else { - unreachable!("Called `Menu::as_player` on a menu that wasn't `Player`.") - } - } -} diff --git a/azalea-inventory/src/slot.rs b/azalea-inventory/src/slot.rs index adebab52..2b8b4acf 100644 --- a/azalea-inventory/src/slot.rs +++ b/azalea-inventory/src/slot.rs @@ -10,6 +10,15 @@ pub enum ItemSlot { Present(ItemSlotData), } +impl ItemSlot { + pub fn is_empty(&self) -> bool { + matches!(self, ItemSlot::Empty) + } + pub fn is_present(&self) -> bool { + matches!(self, ItemSlot::Present(_)) + } +} + /// An item in an inventory, with a count and NBT. Usually you want [`ItemSlot`] /// or [`azalea_registry::Item`] instead. #[derive(Debug, Clone, McBuf)] diff --git a/azalea-protocol/src/packets/game/clientbound_open_screen_packet.rs b/azalea-protocol/src/packets/game/clientbound_open_screen_packet.rs index 9b8b02a1..582cac17 100755 --- a/azalea-protocol/src/packets/game/clientbound_open_screen_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_open_screen_packet.rs @@ -6,6 +6,6 @@ use azalea_protocol_macros::ClientboundGamePacket; pub struct ClientboundOpenScreenPacket { #[var] pub container_id: u32, - pub menu_type: azalea_registry::Menu, + pub menu_type: azalea_registry::MenuKind, pub title: FormattedText, } diff --git a/azalea-registry/src/lib.rs b/azalea-registry/src/lib.rs index 075226b2..97b98e3f 100755 --- a/azalea-registry/src/lib.rs +++ b/azalea-registry/src/lib.rs @@ -3010,7 +3010,7 @@ enum MemoryModuleKind { } registry! { -enum Menu { +enum MenuKind { Generic9x1 => "minecraft:generic_9x1", Generic9x2 => "minecraft:generic_9x2", Generic9x3 => "minecraft:generic_9x3", diff --git a/azalea/Cargo.toml b/azalea/Cargo.toml index dd2bb46c..907920aa 100644 --- a/azalea/Cargo.toml +++ b/azalea/Cargo.toml @@ -18,6 +18,7 @@ azalea-block = { version = "0.6.0", path = "../azalea-block" } azalea-chat = { version = "0.6.0", path = "../azalea-chat" } azalea-client = { version = "0.6.0", path = "../azalea-client" } azalea-core = { version = "0.6.0", path = "../azalea-core" } +azalea-inventory = { version = "0.1.0", path = "../azalea-inventory" } azalea-physics = { version = "0.6.0", path = "../azalea-physics" } azalea-protocol = { version = "0.6.0", path = "../azalea-protocol" } azalea-registry = { version = "0.6.0", path = "../azalea-registry" } diff --git a/azalea/examples/inventory_art.rs b/azalea/examples/inventory_art.rs index b5cce23f..61eb8471 100644 --- a/azalea/examples/inventory_art.rs +++ b/azalea/examples/inventory_art.rs @@ -48,7 +48,10 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { return Ok(()); }; bot.goto(chest_block.into()); - let chest = bot.open_container(&chest_block).await.unwrap(); + let Some(chest) = bot.open_container(&chest_block).await else { + println!("Couldn't open chest"); + return Ok(()); + }; bot.take_amount_from_container(&chest, 5, |i| i.id == "#minecraft:planks") .await; chest.close().await; diff --git a/azalea/examples/testbot.rs b/azalea/examples/testbot.rs index 5931a291..f4908295 100644 --- a/azalea/examples/testbot.rs +++ b/azalea/examples/testbot.rs @@ -6,7 +6,8 @@ use azalea::ecs::query::With; use azalea::entity::metadata::Player; use azalea::entity::{EyeHeight, Position}; use azalea::interact::HitResultComponent; -use azalea::inventory::InventoryComponent; +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}; @@ -149,17 +150,17 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result< println!("inventory: {:?}", inventory.menu()); } "findblock" => { - let target_pos = bot.world().read().find_block( - bot.position(), - &azalea_registry::Block::DiamondBlock.into(), - ); + let target_pos = bot + .world() + .read() + .find_block(bot.position(), &azalea::Block::DiamondBlock.into()); bot.chat(&format!("target_pos: {target_pos:?}",)); } "gotoblock" => { - let target_pos = bot.world().read().find_block( - bot.position(), - &azalea_registry::Block::DiamondBlock.into(), - ); + let target_pos = bot + .world() + .read() + .find_block(bot.position(), &azalea::Block::DiamondBlock.into()); if let Some(target_pos) = target_pos { // +1 to stand on top of the block bot.goto(BlockPosGoal::from(target_pos.up(1))); @@ -171,7 +172,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result< let target_pos = bot .world() .read() - .find_block(bot.position(), &azalea_registry::Block::Lever.into()); + .find_block(bot.position(), &azalea::Block::Lever.into()); let Some(target_pos) = target_pos else { bot.chat("no lever found"); return Ok(()) @@ -184,6 +185,27 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result< let hit_result = bot.get_component::(); bot.chat(&format!("hit_result: {hit_result:?}",)); } + "chest" => { + let target_pos = bot + .world() + .read() + .find_block(bot.position(), &azalea::Block::Chest.into()); + let Some(target_pos) = target_pos else { + bot.chat("no chest found"); + return Ok(()) + }; + bot.look_at(target_pos.center()); + let container = bot.open_container(target_pos).await; + if let Some(container) = container { + for item in container.contents() { + if let ItemSlot::Present(item) = item { + println!("item: {:?}", item); + } + } + } else { + println!("no container found"); + } + } _ => {} } } diff --git a/azalea/examples/todo/craft_dig_straight_down.rs b/azalea/examples/todo/craft_dig_straight_down.rs index 77dee609..116cbcc2 100644 --- a/azalea/examples/todo/craft_dig_straight_down.rs +++ b/azalea/examples/todo/craft_dig_straight_down.rs @@ -38,7 +38,7 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { bot.goto(pathfinder::Goals::NearXZ(5, azalea::BlockXZ(0, 0))) .await; let chest = bot - .open_container(&bot.world().find_block(azalea_registry::Block::Chest)) + .open_container(&bot.world().find_block(azalea::Block::Chest)) .await .unwrap(); bot.take_amount_from_container(&chest, 5, |i| i.id == "#minecraft:planks") @@ -46,9 +46,7 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { chest.close().await; let crafting_table = bot - .open_crafting_table( - &bot.world.find_block(azalea_registry::Block::CraftingTable), - ) + .open_crafting_table(&bot.world.find_block(azalea::Block::CraftingTable)) .await .unwrap(); bot.craft(&crafting_table, &bot.recipe_for("minecraft:sticks")) diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index a6140b56..3fcd0113 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -10,6 +10,7 @@ use app::{App, Plugin, PluginGroup}; pub use azalea_block as blocks; pub use azalea_client::*; pub use azalea_core::{BlockPos, Vec3}; +pub use azalea_inventory as inventory; pub use azalea_protocol as protocol; pub use azalea_registry::{Block, EntityKind}; pub use azalea_world::{entity, Instance}; diff --git a/codegen/lib/code/registry.py b/codegen/lib/code/registry.py index a67b5e4d..aba7ca81 100755 --- a/codegen/lib/code/registry.py +++ b/codegen/lib/code/registry.py @@ -16,12 +16,16 @@ def generate_registries(registries: dict): # Stone => "minecraft:stone" # }); + registry_name = registry_name.split(':')[1] + if registry_name.endswith('_type'): # change _type to _kind because that's Rustier (and because _type # is a reserved keyword) registry_name = registry_name[:-5] + '_kind' + elif registry_name == 'menu': + registry_name = 'menu_kind' - registry_struct_name = to_camel_case(registry_name.split(':')[1]) + registry_struct_name = to_camel_case(registry_name) registry_code = [] registry_code.append(f'enum {registry_struct_name} {{')