aboutsummaryrefslogtreecommitdiff
path: root/azalea
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2023-05-03 20:57:27 -0500
committerGitHub <noreply@github.com>2023-05-03 20:57:27 -0500
commit634cb8d72c6608512aedba19e5cd669104bc35ea (patch)
treef8e76ce9eb43403d29cc0cbcf9a4f51522419dc2 /azalea
parent1fb4418f2c9cbd004c64c2f23d2d0352ee12c0e5 (diff)
downloadazalea-drasl-634cb8d72c6608512aedba19e5cd669104bc35ea.tar.xz
Inventory (#48)
* start adding azalea-inventory * design more of how inventories are defined * start working on az-inv-macros * inventory macro works * start adding inventory codegen * update some deps * add inventory codegen * manually write inventory menus * put the inventories in Client * start on containersetcontent * inventory menu should hopefully work * checks in containersetcontent * format a comment * move some variant matches * inventory.rs * inventory stuff * more inventory stuff * inventory/container tracking works * start adding interact function * sequence number * start adding HitResultComponent * implement traverse_blocks * start adding clip * add clip function * update_hit_result_component * start trying to fix * fix * make some stuff simpler * clippy * lever * chest * container handle * fix ambiguity * fix some doc tests * move some container stuff from az-client to azalea * clicking container * start implementing simulate_click * keep working on simulate click * implement more of simulate_click this is really boring * inventory fixes * start implementing shift clicking * fix panic in azalea-chat i hope * shift clicking implemented * more inventory stuff * fix items not showing in containers sometimes * fix test * fix all warnings * remove a println --------- Co-authored-by: mat <git@matdoes.dev>
Diffstat (limited to 'azalea')
-rw-r--r--azalea/Cargo.toml1
-rw-r--r--azalea/examples/steal.rs76
-rw-r--r--azalea/examples/testbot.rs81
-rw-r--r--azalea/examples/todo/craft_dig_straight_down.rs8
-rw-r--r--azalea/src/bot.rs18
-rw-r--r--azalea/src/container.rs140
-rw-r--r--azalea/src/lib.rs5
-rw-r--r--azalea/src/pathfinder/mod.rs4
-rw-r--r--azalea/src/prelude.rs5
-rw-r--r--azalea/src/swarm/mod.rs6
10 files changed, 310 insertions, 34 deletions
diff --git a/azalea/Cargo.toml b/azalea/Cargo.toml
index 678d4a3b..5dbc7556 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/steal.rs b/azalea/examples/steal.rs
new file mode 100644
index 00000000..3302079a
--- /dev/null
+++ b/azalea/examples/steal.rs
@@ -0,0 +1,76 @@
+//! Steal all the diamonds from all the nearby chests.
+
+use azalea::{prelude::*, BlockPos};
+use azalea_inventory::operations::QuickMoveClick;
+use azalea_inventory::ItemSlot;
+use parking_lot::Mutex;
+use std::sync::Arc;
+
+#[tokio::main]
+async fn main() {
+ let account = Account::offline("bot");
+ // or let bot = Account::microsoft("email").await;
+
+ ClientBuilder::new()
+ .set_handler(handle)
+ .start(account, "localhost")
+ .await
+ .unwrap();
+}
+
+#[derive(Default, Clone, Component)]
+struct State {
+ pub checked_chests: Arc<Mutex<Vec<BlockPos>>>,
+}
+
+async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> {
+ match event {
+ Event::Chat(m) => {
+ if m.username() == Some(bot.profile.name.clone()) {
+ return Ok(());
+ };
+ if m.content() != "go" {
+ return Ok(());
+ }
+ {
+ state.checked_chests.lock().clear();
+ }
+
+ let chest_block = bot
+ .world()
+ .read()
+ .find_block(bot.position(), &azalea::Block::Chest.into());
+ // TODO: update this when find_blocks is implemented
+ let Some(chest_block) = chest_block else {
+ bot.chat("No chest found");
+ return Ok(());
+ };
+ // bot.goto(BlockPosGoal::from(chest_block));
+ let Some(chest) = bot.open_container(chest_block).await else {
+ println!("Couldn't open chest");
+ return Ok(());
+ };
+
+ println!("Getting contents");
+ for (index, slot) in chest
+ .contents()
+ .expect("we just opened the chest")
+ .iter()
+ .enumerate()
+ {
+ println!("Checking slot {index}: {slot:?}");
+ if let ItemSlot::Present(item) = slot {
+ if item.kind == azalea::Item::Diamond {
+ println!("clicking slot ^");
+ chest.click(QuickMoveClick::Left { slot: index as u16 });
+ }
+ }
+ }
+
+ println!("Done");
+ }
+ _ => {}
+ }
+
+ Ok(())
+}
diff --git a/azalea/examples/testbot.rs b/azalea/examples/testbot.rs
index a25b28e3..3fe9253c 100644
--- a/azalea/examples/testbot.rs
+++ b/azalea/examples/testbot.rs
@@ -4,7 +4,9 @@
use azalea::ecs::query::With;
use azalea::entity::metadata::Player;
-use azalea::entity::Position;
+use azalea::entity::{EyeHeight, Position};
+use azalea::interact::HitResultComponent;
+use azalea::inventory::ItemSlot;
use azalea::pathfinder::BlockPosGoal;
use azalea::{prelude::*, swarm::prelude::*, BlockPos, GameProfileComponent, WalkDirection};
use azalea::{Account, Client, Event};
@@ -46,7 +48,7 @@ async fn main() -> anyhow::Result<()> {
let mut accounts = Vec::new();
let mut states = Vec::new();
- for i in 0..5 {
+ for i in 0..1 {
accounts.push(Account::offline(&format!("bot{i}")));
states.push(State::default());
}
@@ -112,7 +114,7 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.chat(&format!("You're at {pos:?}",));
}
"whereareyou" => {
- let pos = bot.component::<Position>();
+ let pos = bot.position();
bot.chat(&format!("I'm at {pos:?}",));
}
"goto" => {
@@ -122,10 +124,11 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.goto(BlockPosGoal::from(target_pos));
}
"look" => {
- let entity_pos = bot.entity_component::<Position>(entity);
- let target_pos: BlockPos = entity_pos.into();
- println!("target_pos: {target_pos:?}");
- bot.look_at(target_pos.center());
+ let entity_pos = bot
+ .entity_component::<Position>(entity)
+ .up(bot.entity_component::<EyeHeight>(entity).into());
+ println!("entity_pos: {entity_pos:?}");
+ bot.look_at(entity_pos);
}
"jump" => {
bot.set_jumping(true);
@@ -140,18 +143,21 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
"lag" => {
std::thread::sleep(Duration::from_millis(1000));
}
+ "inventory" => {
+ println!("inventory: {:?}", bot.menu());
+ }
"findblock" => {
- let target_pos = bot.world().read().find_block(
- bot.component::<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.component::<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)));
@@ -159,6 +165,49 @@ async fn handle(mut bot: Client, event: Event, _state: State) -> anyhow::Result<
bot.chat("no diamond block found");
}
}
+ "lever" => {
+ let target_pos = bot
+ .world()
+ .read()
+ .find_block(bot.position(), &azalea::Block::Lever.into());
+ let Some(target_pos) = target_pos else {
+ bot.chat("no lever found");
+ return Ok(())
+ };
+ bot.goto(BlockPosGoal::from(target_pos));
+ bot.look_at(target_pos.center());
+ bot.block_interact(target_pos);
+ }
+ "hitresult" => {
+ 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;
+ println!("container: {:?}", container);
+ if let Some(container) = container {
+ 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");
+ }
+ }
_ => {}
}
}
@@ -196,7 +245,7 @@ async fn swarm_handle(
SwarmEvent::Chat(m) => {
println!("swarm chat message: {}", m.message().to_ansi());
if m.message().to_string() == "<py5> world" {
- for (name, world) in &swarm.world_container.read().worlds {
+ for (name, world) in &swarm.instance_container.read().worlds {
println!("world name: {name}");
if let Some(w) = world.upgrade() {
for chunk_pos in w.read().chunks.chunks.values() {
diff --git a/azalea/examples/todo/craft_dig_straight_down.rs b/azalea/examples/todo/craft_dig_straight_down.rs
index 4c980ccf..116cbcc2 100644
--- a/azalea/examples/todo/craft_dig_straight_down.rs
+++ b/azalea/examples/todo/craft_dig_straight_down.rs
@@ -38,17 +38,15 @@ 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(&chest, 5, |i| i.id == "#minecraft:planks")
+ bot.take_amount_from_container(&chest, 5, |i| i.id == "#minecraft:planks")
.await;
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/bot.rs b/azalea/src/bot.rs
index a45ae28d..e5ea4c28 100644
--- a/azalea/src/bot.rs
+++ b/azalea/src/bot.rs
@@ -1,4 +1,5 @@
use crate::app::{App, CoreSchedule, IntoSystemAppConfig, Plugin, PluginGroup, PluginGroupBuilder};
+use crate::container::ContainerPlugin;
use crate::ecs::{
component::Component,
entity::Entity,
@@ -9,7 +10,8 @@ use crate::ecs::{
};
use azalea_core::Vec3;
use azalea_physics::{force_jump_listener, PhysicsSet};
-use azalea_world::entity::{metadata::Player, set_rotation, Jumping, Local, Physics, Position};
+use azalea_world::entity::{clamp_look_direction, EyeHeight, LookDirection};
+use azalea_world::entity::{metadata::Player, Jumping, Local, Position};
use std::f64::consts::PI;
use crate::pathfinder::PathfinderPlugin;
@@ -22,7 +24,9 @@ impl Plugin for BotPlugin {
.add_event::<JumpEvent>()
.add_systems((
insert_bot,
- look_at_listener.before(force_jump_listener),
+ look_at_listener
+ .before(force_jump_listener)
+ .before(clamp_look_direction),
jump_listener,
stop_jumping
.in_schedule(CoreSchedule::FixedUpdate)
@@ -99,12 +103,13 @@ pub struct LookAtEvent {
}
fn look_at_listener(
mut events: EventReader<LookAtEvent>,
- mut query: Query<(&Position, &mut Physics)>,
+ mut query: Query<(&Position, &EyeHeight, &mut LookDirection)>,
) {
for event in events.iter() {
- if let Ok((position, mut physics)) = query.get_mut(event.entity) {
- let (y_rot, x_rot) = direction_looking_at(position, &event.position);
- set_rotation(&mut physics, y_rot, x_rot);
+ if let Ok((position, eye_height, mut look_direction)) = query.get_mut(event.entity) {
+ let (y_rot, x_rot) =
+ direction_looking_at(&position.up(eye_height.into()), &event.position);
+ (look_direction.y_rot, look_direction.x_rot) = (y_rot, x_rot);
}
}
}
@@ -129,5 +134,6 @@ impl PluginGroup for DefaultBotPlugins {
PluginGroupBuilder::start::<Self>()
.add(BotPlugin)
.add(PathfinderPlugin)
+ .add(ContainerPlugin)
}
}
diff --git a/azalea/src/container.rs b/azalea/src/container.rs
new file mode 100644
index 00000000..0016caad
--- /dev/null
+++ b/azalea/src/container.rs
@@ -0,0 +1,140 @@
+use std::fmt::Formatter;
+
+use azalea_client::{
+ inventory::{CloseContainerEvent, ContainerClickEvent, InventoryComponent},
+ packet_handling::PacketEvent,
+ Client, TickBroadcast,
+};
+use azalea_core::BlockPos;
+use azalea_inventory::{operations::ClickOperation, ItemSlot, Menu};
+use azalea_protocol::packets::game::ClientboundGamePacket;
+use bevy_app::{App, Plugin};
+use bevy_ecs::{component::Component, prelude::EventReader, system::Commands};
+use std::fmt::Debug;
+
+pub struct ContainerPlugin;
+impl Plugin for ContainerPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_system(handle_menu_opened_event);
+ }
+}
+
+pub trait ContainerClientExt {
+ async fn open_container(&mut self, pos: BlockPos) -> Option<ContainerHandle>;
+}
+
+impl ContainerClientExt for Client {
+ /// Open a container in the world, like a chest.
+ ///
+ /// ```
+ /// # use azalea::prelude::*;
+ /// # async fn example(mut bot: azalea::Client) {
+ /// 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;
+ /// };
+ /// let container = bot.open_container(target_pos).await;
+ /// # }
+ /// ```
+ async fn open_container(&mut self, pos: BlockPos) -> Option<ContainerHandle> {
+ self.ecs
+ .lock()
+ .entity_mut(self.entity)
+ .insert(WaitingForInventoryOpen);
+ self.block_interact(pos);
+
+ let mut receiver = {
+ let ecs = self.ecs.lock();
+ let tick_broadcast = ecs.resource::<TickBroadcast>();
+ tick_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)
+ .expect("no inventory");
+ if inventory.id == 0 {
+ None
+ } else {
+ Some(ContainerHandle {
+ id: inventory.id,
+ client: self.clone(),
+ })
+ }
+ }
+}
+
+/// A handle to the open container. The container will be closed once this is
+/// dropped.
+pub struct ContainerHandle {
+ pub id: u8,
+ 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<Menu> {
+ let ecs = self.client.ecs.lock();
+ let inventory = ecs
+ .get::<InventoryComponent>(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, not including the player's
+ /// inventory. If the container is closed, this will return `None`.
+ pub fn contents(&self) -> Option<Vec<ItemSlot>> {
+ self.menu().map(|menu| menu.contents())
+ }
+
+ pub fn click(&self, operation: impl Into<ClickOperation>) {
+ let operation = operation.into();
+ self.client.ecs.lock().send_event(ContainerClickEvent {
+ entity: self.client.entity,
+ window_id: self.id,
+ operation,
+ });
+ }
+}
+
+#[derive(Component, Debug)]
+pub struct WaitingForInventoryOpen;
+
+fn handle_menu_opened_event(mut commands: Commands, mut events: EventReader<PacketEvent>) {
+ for event in events.iter() {
+ if let ClientboundGamePacket::ContainerSetContent { .. } = event.packet {
+ commands
+ .entity(event.entity)
+ .remove::<WaitingForInventoryOpen>();
+ }
+ }
+}
diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs
index bd1d356a..2e8e4fa1 100644
--- a/azalea/src/lib.rs
+++ b/azalea/src/lib.rs
@@ -1,7 +1,10 @@
#![doc = include_str!("../README.md")]
#![feature(async_closure)]
+#![allow(incomplete_features)]
+#![feature(async_fn_in_trait)]
mod bot;
+mod container;
pub mod pathfinder;
pub mod prelude;
pub mod swarm;
@@ -12,7 +15,7 @@ pub use azalea_block as blocks;
pub use azalea_client::*;
pub use azalea_core::{BlockPos, Vec3};
pub use azalea_protocol as protocol;
-pub use azalea_registry::EntityKind;
+pub use azalea_registry::{Block, EntityKind, Item};
pub use azalea_world::{entity, Instance};
use bot::DefaultBotPlugins;
use ecs::component::Component;
diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs
index 9c06ebb8..56c8e0ce 100644
--- a/azalea/src/pathfinder/mod.rs
+++ b/azalea/src/pathfinder/mod.rs
@@ -93,7 +93,7 @@ fn goto_listener(
mut commands: Commands,
mut events: EventReader<GotoEvent>,
mut query: Query<(&Position, &WorldName)>,
- world_container: Res<InstanceContainer>,
+ instance_container: Res<InstanceContainer>,
) {
let thread_pool = AsyncComputeTaskPool::get();
@@ -106,7 +106,7 @@ fn goto_listener(
vertical_vel: VerticalVel::None,
};
- let world_lock = world_container
+ let world_lock = instance_container
.get(world_name)
.expect("Entity tried to pathfind but the entity isn't in a valid world");
let end = event.goal.goal_node();
diff --git a/azalea/src/prelude.rs b/azalea/src/prelude.rs
index b1a1fed3..87cb0b53 100644
--- a/azalea/src/prelude.rs
+++ b/azalea/src/prelude.rs
@@ -1,7 +1,10 @@
//! The Azalea prelude. Things that are necessary for a bare-bones bot are
//! re-exported here.
-pub use crate::{bot::BotClientExt, pathfinder::PathfinderClientExt, ClientBuilder};
+pub use crate::{
+ bot::BotClientExt, container::ContainerClientExt, pathfinder::PathfinderClientExt,
+ ClientBuilder,
+};
pub use azalea_client::{Account, Client, Event};
// this is necessary to make the macros that reference bevy_ecs work
pub use crate::ecs as bevy_ecs;
diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs
index 6fe11b7d..2253f5bd 100644
--- a/azalea/src/swarm/mod.rs
+++ b/azalea/src/swarm/mod.rs
@@ -37,7 +37,7 @@ pub struct Swarm {
// bot_datas: Arc<Mutex<Vec<(Client, S)>>>,
resolved_address: SocketAddr,
address: ServerAddress,
- pub world_container: Arc<RwLock<InstanceContainer>>,
+ pub instance_container: Arc<RwLock<InstanceContainer>>,
bots_tx: mpsc::UnboundedSender<(Option<Event>, Client)>,
swarm_tx: mpsc::UnboundedSender<SwarmEvent>,
@@ -248,7 +248,7 @@ where
// resolve the address
let resolved_address = resolver::resolve_address(&address).await?;
- let world_container = Arc::new(RwLock::new(InstanceContainer::default()));
+ let instance_container = Arc::new(RwLock::new(InstanceContainer::default()));
// we can't modify the swarm plugins after this
let (bots_tx, mut bots_rx) = mpsc::unbounded_channel();
@@ -263,7 +263,7 @@ where
resolved_address,
address,
- world_container,
+ instance_container,
bots_tx,