1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00
This commit is contained in:
mat 2023-03-13 21:10:21 -05:00
parent b44dc94274
commit 6286e953a6
17 changed files with 282 additions and 44 deletions

1
Cargo.lock generated
View file

@ -181,6 +181,7 @@ dependencies = [
"azalea-chat",
"azalea-client",
"azalea-core",
"azalea-inventory",
"azalea-physics",
"azalea-protocol",
"azalea-registry",

View file

@ -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::<RanScheduleBroadcast>();
/// 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<RanScheduleBroadcast>) {
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)
}
}

View file

@ -30,10 +30,11 @@ impl Plugin for InteractPlugin {
fn build(&self, app: &mut App) {
app.add_event::<BlockInteractEvent>().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(),
);
}
}

View file

@ -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::<ClientSideCloseContainerEvent>()
.add_system(handle_client_side_close_container_event);
.add_event::<MenuOpenedEvent>()
.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<Menu> {
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::<RanScheduleBroadcast>();
schedule_broadcast.subscribe()
};
while receiver.recv().await.is_ok() {
let ecs = self.ecs.lock();
if ecs.get::<WaitingForInventoryOpen>(self.entity).is_none() {
break;
}
}
let ecs = self.ecs.lock();
let inventory = ecs.get::<InventoryComponent>(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<ItemStack> 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<MenuOpenedEvent>,
mut query: Query<&mut InventoryComponent>,
) {
for event in events.iter() {
commands
.entity(event.entity)
.remove::<WaitingForInventoryOpen>();
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,
}

View file

@ -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;

View file

@ -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<EventWriter<MenuOpenedEvent>> =
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(_) => {}

View file

@ -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<ItemSlot> {
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,
&quote! {
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 {

View file

@ -27,6 +27,21 @@ impl<const N: usize> Default for SlotList<N> {
}
}
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`.")
}
}
}

View file

@ -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)]

View file

@ -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,
}

View file

@ -3010,7 +3010,7 @@ enum MemoryModuleKind {
}
registry! {
enum Menu {
enum MenuKind {
Generic9x1 => "minecraft:generic_9x1",
Generic9x2 => "minecraft:generic_9x2",
Generic9x3 => "minecraft:generic_9x3",

View file

@ -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" }

View file

@ -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;

View file

@ -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::<HitResultComponent>();
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");
}
}
_ => {}
}
}

View file

@ -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"))

View file

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

View file

@ -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} {{')