1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

add StartUseItemEvent and improve code related to interactions

This commit is contained in:
mat 2025-05-10 06:22:08 +03:30
parent e9b3128103
commit e1d3b902ba
16 changed files with 325 additions and 144 deletions

View file

@ -25,7 +25,8 @@ is breaking anyways, semantic versioning is not followed.
- [BREAKING] The `BlockState::id` field is now private, use `.id()` instead.
- [BREAKING] Update to [Bevy 0.16](https://bevyengine.org/news/bevy-0-16/).
- [BREAKING] Rename `InstanceContainer::insert` to `get_or_insert`.
- ClientBuilder and SwarmBuilder are now Send.
- [BREAKING] Replace `BlockInteractEvent` with the more general-purpose `StartUseItemEvent`, and add `client.start_use_item()`.
- `ClientBuilder` and `SwarmBuilder` are now Send.
### Fixed

View file

@ -17,7 +17,7 @@ use bevy_ecs::prelude::*;
use tracing::{error, trace};
use crate::{
InstanceHolder, interact::handle_block_interact_event, inventory::InventorySet,
InstanceHolder, interact::handle_start_use_item_queued, inventory::InventorySet,
packet::game::SendPacketEvent, respawn::perform_respawn,
};
@ -33,7 +33,7 @@ impl Plugin for ChunksPlugin {
)
.chain()
.before(InventorySet)
.before(handle_block_interact_event)
.before(handle_start_use_item_queued)
.before(perform_respawn),
)
.add_event::<ReceiveChunkEvent>()

View file

@ -1,21 +1,22 @@
use std::ops::AddAssign;
use azalea_block::BlockState;
use azalea_core::{
block_hit_result::BlockHitResult,
direction::Direction,
game_type::GameMode,
hit_result::{BlockHitResult, HitResult},
position::{BlockPos, Vec3},
tick::GameTick,
};
use azalea_entity::{
Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector,
};
use azalea_inventory::{ItemStack, ItemStackData, components};
use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
use azalea_physics::{
PhysicsSet,
clip::{BlockShapeType, ClipContext, FluidPickType},
};
use azalea_protocol::packets::game::{
s_interact::InteractionHand,
s_swing::ServerboundSwing,
s_use_item_on::{BlockHit, ServerboundUseItemOn},
ServerboundUseItem, s_interact::InteractionHand, s_swing::ServerboundSwing,
s_use_item_on::ServerboundUseItemOn,
};
use azalea_world::{Instance, InstanceContainer, InstanceName};
use bevy_app::{App, Plugin, Update};
@ -23,6 +24,7 @@ use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use tracing::warn;
use super::mining::{Mining, MiningSet};
use crate::{
Client,
attack::handle_attack_event,
@ -37,14 +39,14 @@ use crate::{
pub struct InteractPlugin;
impl Plugin for InteractPlugin {
fn build(&self, app: &mut App) {
app.add_event::<BlockInteractEvent>()
app.add_event::<StartUseItemEvent>()
.add_event::<SwingArmEvent>()
.add_systems(
Update,
(
(
handle_start_use_item_event,
update_hit_result_component.after(clamp_look_direction),
handle_block_interact_event,
handle_swing_arm_event,
)
.after(InventorySet)
@ -56,34 +58,47 @@ impl Plugin for InteractPlugin {
.after(MoveEventsSet),
),
)
.add_systems(
GameTick,
handle_start_use_item_queued
.after(MiningSet)
.before(PhysicsSet),
)
.add_observer(handle_swing_arm_trigger);
}
}
impl Client {
/// Right click a block. The behavior of this depends on the target block,
/// Right-click a block.
///
/// The behavior of this depends on the target block,
/// and it'll either place the block you're holding in your hand or use the
/// block you clicked (like toggling a lever).
///
/// Note that this may trigger anticheats as it doesn't take into account
/// whether you're actually looking at the block.
pub fn block_interact(&self, position: BlockPos) {
self.ecs.lock().send_event(BlockInteractEvent {
self.ecs.lock().send_event(StartUseItemEvent {
entity: self.entity,
position,
hand: InteractionHand::MainHand,
force_block: Some(position),
});
}
}
/// Right click a block. The behavior of this depends on the target block,
/// and it'll either place the block you're holding in your hand or use the
/// block you clicked (like toggling a lever).
#[derive(Event)]
pub struct BlockInteractEvent {
/// The local player entity that's opening the container.
pub entity: Entity,
/// The coordinates of the container.
pub position: BlockPos,
/// Use the current item.
///
/// If the item is consumable, then it'll act as if right-click was held
/// until the item finished being consumed. You can use this to eat food.
///
/// If we're looking at a block or entity, then it will be clicked. Also see
/// [`Client::block_interact`].
pub fn start_use_item(&self) {
self.ecs.lock().send_event(StartUseItemEvent {
entity: self.entity,
hand: InteractionHand::MainHand,
force_block: None,
});
}
}
/// A component that contains the number of changes this client has made to
@ -91,66 +106,149 @@ pub struct BlockInteractEvent {
#[derive(Component, Copy, Clone, Debug, Default, Deref)]
pub struct CurrentSequenceNumber(u32);
impl AddAssign<u32> for CurrentSequenceNumber {
fn add_assign(&mut self, rhs: u32) {
self.0 += rhs;
impl CurrentSequenceNumber {
/// Get the next sequence number that we're going to use and increment the
/// value.
pub fn get_and_increment(&mut self) -> u32 {
let cur = self.0;
self.0 += 1;
cur
}
}
/// A component that contains the block that the player is currently looking at.
/// A component that contains the block or entity that the player is currently
/// looking at.
#[doc(alias("looking at", "looking at block", "crosshair"))]
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct HitResultComponent(BlockHitResult);
pub struct HitResultComponent(HitResult);
pub fn handle_block_interact_event(
mut events: EventReader<BlockInteractEvent>,
mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>,
/// An event that makes one of our clients simulate a right-click.
///
/// This event just inserts the [`StartUseItemQueued`] component on the given
/// entity.
#[doc(alias("right click"))]
#[derive(Event)]
pub struct StartUseItemEvent {
pub entity: Entity,
pub hand: InteractionHand,
/// See [`QueuedStartUseItem::force_block`].
pub force_block: Option<BlockPos>,
}
pub fn handle_start_use_item_event(
mut commands: Commands,
mut events: EventReader<StartUseItemEvent>,
) {
for event in events.read() {
let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else {
warn!("Sent BlockInteractEvent for entity that doesn't have the required components");
continue;
};
commands.entity(event.entity).insert(StartUseItemQueued {
hand: event.hand,
force_block: event.force_block,
});
}
}
// TODO: check to make sure we're within the world border
/// A component that makes our client simulate a right-click on the next
/// [`GameTick`]. It's removed after that tick.
///
/// You may find it more convenient to use [`StartUseItemEvent`] instead, which
/// just inserts this component for you.
///
/// [`GameTick`]: azalea_core::tick::GameTick
#[derive(Component)]
pub struct StartUseItemQueued {
pub hand: InteractionHand,
/// Optionally force us to send a [`ServerboundUseItemOn`] on the given
/// block.
///
/// This is useful if you want to interact with a block without looking at
/// it, but should be avoided to stay compatible with anticheats.
pub force_block: Option<BlockPos>,
}
pub fn handle_start_use_item_queued(
mut commands: Commands,
query: Query<(
Entity,
&StartUseItemQueued,
&mut CurrentSequenceNumber,
&HitResultComponent,
&LookDirection,
Option<&Mining>,
)>,
) {
for (entity, start_use_item, mut sequence_number, hit_result, look_direction, mining) in query {
commands.entity(entity).remove::<StartUseItemQueued>();
*sequence_number += 1;
if mining.is_some() {
warn!("Got a StartUseItemEvent for a client that was mining");
}
// minecraft also does the interaction client-side (so it looks like clicking a
// button is instant) but we don't really need that
// TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
// rowing a boat
// the block_hit data will depend on whether we're looking at the block and
// whether we can reach it
let mut hit_result = hit_result.0.clone();
let block_hit = if hit_result.block_pos == event.position {
// we're looking at the block :)
BlockHit {
block_pos: hit_result.block_pos,
direction: hit_result.direction,
location: hit_result.location,
inside: hit_result.inside,
world_border: hit_result.world_border,
if let Some(force_block) = start_use_item.force_block {
let hit_result_matches = if let HitResult::Block(block_hit_result) = hit_result {
block_hit_result.block_pos == force_block
} else {
false
};
if !hit_result_matches {
// we're not looking at the block, so make up some numbers
hit_result = HitResult::Block(BlockHitResult {
location: force_block.center(),
direction: Direction::Up,
block_pos: force_block,
inside: false,
world_border: false,
miss: false,
});
}
} else {
// we're not looking at the block, so make up some numbers
BlockHit {
block_pos: event.position,
direction: Direction::Up,
location: event.position.center(),
inside: false,
world_border: false,
}
};
}
commands.trigger(SendPacketEvent::new(
entity,
ServerboundUseItemOn {
hand: InteractionHand::MainHand,
block_hit,
sequence: sequence_number.0,
},
));
match hit_result {
HitResult::Block(block_hit_result) => {
if block_hit_result.miss {
commands.trigger(SendPacketEvent::new(
entity,
ServerboundUseItem {
hand: start_use_item.hand,
sequence: sequence_number.get_and_increment(),
x_rot: look_direction.x_rot,
y_rot: look_direction.y_rot,
},
));
} else {
commands.trigger(SendPacketEvent::new(
entity,
ServerboundUseItemOn {
hand: start_use_item.hand,
block_hit: block_hit_result.into(),
sequence: sequence_number.get_and_increment(),
},
));
// TODO: depending on the result of useItemOn, this might
// also need to send a SwingArmEvent.
// basically, this TODO is for
// simulating block interactions/placements on the
// client-side.
}
}
HitResult::Entity => {
// TODO: implement HitResult::Entity
// TODO: worldborder check
// commands.trigger(SendPacketEvent::new(
// entity,
// ServerboundInteract {
// entity_id: todo!(),
// action: todo!(),
// using_secondary_action: todo!(),
// },
// ));
}
}
}
}
@ -198,12 +296,32 @@ pub fn update_hit_result_component(
}
}
/// Get the block or entity that a player would be looking at if their eyes were
/// at the given direction and position.
///
/// If you need to get the block/entity the player is looking at right now, use
/// [`HitResultComponent`].
///
/// Also see [`pick_block`].
///
/// TODO: does not currently check for entities
pub fn pick(
look_direction: &LookDirection,
eye_position: &Vec3,
chunks: &azalea_world::ChunkStorage,
pick_range: f64,
) -> HitResult {
// TODO
// let entity_hit_result = ;
HitResult::Block(pick_block(look_direction, eye_position, chunks, pick_range))
}
/// Get the block that a player would be looking at if their eyes were at the
/// given direction and position.
///
/// If you need to get the block the player is looking at right now, use
/// [`HitResultComponent`].
pub fn pick(
/// Also see [`pick`].
pub fn pick_block(
look_direction: &LookDirection,
eye_position: &Vec3,
chunks: &azalea_world::ChunkStorage,
@ -211,6 +329,7 @@ pub fn pick(
) -> BlockHitResult {
let view_vector = view_vector(look_direction);
let end_position = eye_position + &(view_vector * pick_range);
azalea_physics::clip::clip(
chunks,
ClipContext {

View file

@ -39,7 +39,8 @@ impl Plugin for MiningPlugin {
handle_mining_queued,
)
.chain()
.before(PhysicsSet),
.before(PhysicsSet)
.in_set(MiningSet),
)
.add_systems(
Update,
@ -56,7 +57,7 @@ impl Plugin for MiningPlugin {
.after(azalea_entity::update_fluid_on_eyes)
.after(crate::interact::update_hit_result_component)
.after(crate::attack::handle_attack_event)
.after(crate::interact::handle_block_interact_event)
.after(crate::interact::handle_start_use_item_queued)
.before(crate::interact::handle_swing_arm_event),
);
}
@ -121,22 +122,25 @@ fn handle_auto_mine(
current_mining_item,
) in &mut query.iter_mut()
{
let block_pos = hit_result_component.block_pos;
let block_pos = hit_result_component
.as_block_hit_result_if_not_miss()
.map(|b| b.block_pos);
if (mining.is_none()
|| !is_same_mining_target(
block_pos,
inventory,
current_mining_pos,
current_mining_item,
))
&& !hit_result_component.miss
// start mining if we're looking at a block and we're not already mining it
if let Some(block_pos) = block_pos
&& (mining.is_none()
|| !is_same_mining_target(
block_pos,
inventory,
current_mining_pos,
current_mining_item,
))
{
start_mining_block_event.write(StartMiningBlockEvent {
entity,
position: block_pos,
});
} else if mining.is_some() && hit_result_component.miss {
} else if mining.is_some() && hit_result_component.is_miss() {
stop_mining_block_event.write(StopMiningBlockEvent { entity });
}
}
@ -166,9 +170,11 @@ fn handle_start_mining_block_event(
) {
for event in events.read() {
let hit_result = query.get_mut(event.entity).unwrap();
let direction = if hit_result.block_pos == event.position {
let direction = if let Some(block_hit_result) = hit_result.as_block_hit_result_if_not_miss()
&& block_hit_result.block_pos == event.position
{
// we're looking at the block
hit_result.direction
block_hit_result.direction
} else {
// we're not looking at the block, arbitrary direction
Direction::Down
@ -241,7 +247,6 @@ fn handle_mining_queued(
// is outside of the worldborder
if game_mode.current == GameMode::Creative {
*sequence_number += 1;
finish_mining_events.write(FinishMiningBlockEvent {
entity,
position: mining_queued.position,
@ -318,14 +323,13 @@ fn handle_mining_queued(
});
}
*sequence_number += 1;
commands.trigger(SendPacketEvent::new(
entity,
ServerboundPlayerAction {
action: s_player_action::Action::StartDestroyBlock,
pos: mining_queued.position,
direction: mining_queued.direction,
sequence: **sequence_number,
sequence: sequence_number.get_and_increment(),
},
));
commands.trigger(SwingArmEvent { entity });
@ -558,14 +562,13 @@ pub fn continue_mining_block(
entity,
position: mining.pos,
});
*sequence_number += 1;
commands.trigger(SendPacketEvent::new(
entity,
ServerboundPlayerAction {
action: s_player_action::Action::StartDestroyBlock,
pos: mining.pos,
direction: mining.dir,
sequence: **sequence_number,
sequence: sequence_number.get_and_increment(),
},
));
commands.trigger(SwingArmEvent { entity });
@ -602,7 +605,6 @@ pub fn continue_mining_block(
if **mine_progress >= 1. {
commands.entity(entity).remove::<Mining>();
*sequence_number += 1;
println!("finished mining block at {:?}", mining.pos);
finish_mining_events.write(FinishMiningBlockEvent {
entity,
@ -614,7 +616,7 @@ pub fn continue_mining_block(
action: s_player_action::Action::StopDestroyBlock,
pos: mining.pos,
direction: mining.dir,
sequence: **sequence_number,
sequence: sequence_number.get_and_increment(),
},
));
**mine_progress = 0.;
@ -638,16 +640,16 @@ pub fn continue_mining_block(
}
}
fn update_mining_component(
pub fn update_mining_component(
mut commands: Commands,
mut query: Query<(Entity, &mut Mining, &HitResultComponent)>,
) {
for (entity, mut mining, hit_result_component) in &mut query.iter_mut() {
if hit_result_component.miss {
commands.entity(entity).remove::<Mining>();
if let Some(block_hit_result) = hit_result_component.as_block_hit_result_if_not_miss() {
mining.pos = block_hit_result.block_pos;
mining.dir = block_hit_result.direction;
} else {
mining.pos = hit_result_component.block_pos;
mining.dir = hit_result_component.direction;
commands.entity(entity).remove::<Mining>();
}
}
}

View file

@ -1,6 +1,6 @@
use crate::{
block_hit_result::BlockHitResult,
direction::{Axis, Direction},
hit_result::BlockHitResult,
math::EPSILON,
position::{BlockPos, Vec3},
};

View file

@ -1,34 +0,0 @@
use crate::{
direction::Direction,
position::{BlockPos, Vec3},
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BlockHitResult {
pub location: Vec3,
pub direction: Direction,
pub block_pos: BlockPos,
pub miss: bool,
pub inside: bool,
pub world_border: bool,
}
impl BlockHitResult {
pub fn miss(location: Vec3, direction: Direction, block_pos: BlockPos) -> Self {
Self {
location,
direction,
block_pos,
miss: true,
inside: false,
world_border: false,
}
}
pub fn with_direction(&self, direction: Direction) -> Self {
Self { direction, ..*self }
}
pub fn with_position(&self, block_pos: BlockPos) -> Self {
Self { block_pos, ..*self }
}
}

View file

@ -0,0 +1,68 @@
use crate::{
direction::Direction,
position::{BlockPos, Vec3},
};
/// The block or entity that our player is looking at and can interact with.
///
/// If there's nothing, it'll be a [`BlockHitResult`] with `miss` set to true.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum HitResult {
Block(BlockHitResult),
/// TODO
Entity,
}
impl HitResult {
pub fn is_miss(&self) -> bool {
match self {
HitResult::Block(block_hit_result) => block_hit_result.miss,
HitResult::Entity => false,
}
}
pub fn is_block_hit_and_not_miss(&self) -> bool {
match self {
HitResult::Block(block_hit_result) => !block_hit_result.miss,
HitResult::Entity => false,
}
}
/// Returns the [`BlockHitResult`], if we were looking at a block and it
/// wasn't a miss.
pub fn as_block_hit_result_if_not_miss(&self) -> Option<&BlockHitResult> {
match self {
HitResult::Block(block_hit_result) if !block_hit_result.miss => Some(block_hit_result),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BlockHitResult {
pub location: Vec3,
pub direction: Direction,
pub block_pos: BlockPos,
pub inside: bool,
pub world_border: bool,
pub miss: bool,
}
impl BlockHitResult {
pub fn miss(location: Vec3, direction: Direction, block_pos: BlockPos) -> Self {
Self {
location,
direction,
block_pos,
miss: true,
inside: false,
world_border: false,
}
}
pub fn with_direction(&self, direction: Direction) -> Self {
Self { direction, ..*self }
}
pub fn with_position(&self, block_pos: BlockPos) -> Self {
Self { block_pos, ..*self }
}
}

View file

@ -2,7 +2,6 @@
pub mod aabb;
pub mod bitset;
pub mod block_hit_result;
pub mod color;
pub mod cursor3d;
pub mod data_registry;
@ -11,6 +10,7 @@ pub mod difficulty;
pub mod direction;
pub mod filterable;
pub mod game_type;
pub mod hit_result;
pub mod math;
pub mod objectives;
pub mod position;

View file

@ -218,9 +218,9 @@ pub struct Jumping(pub bool);
/// A component that contains the direction an entity is looking.
#[derive(Debug, Component, Copy, Clone, Default, PartialEq, AzBuf)]
pub struct LookDirection {
/// Left and right. Aka yaw.
/// Left and right. AKA yaw.
pub y_rot: f32,
/// Up and down. Aka pitch.
/// Up and down. AKA pitch.
pub x_rot: f32,
}

View file

@ -6,8 +6,8 @@ use azalea_block::{
};
use azalea_core::{
aabb::AABB,
block_hit_result::BlockHitResult,
direction::{Axis, Direction},
hit_result::BlockHitResult,
math::{self, EPSILON, lerp},
position::{BlockPos, Vec3},
};

View file

@ -1,8 +1,8 @@
use std::{cmp, num::NonZeroU32, sync::LazyLock};
use azalea_core::{
block_hit_result::BlockHitResult,
direction::{Axis, AxisCycle, Direction},
hit_result::BlockHitResult,
math::{EPSILON, binary_search},
position::{BlockPos, Vec3},
};

View file

@ -80,8 +80,9 @@ impl AzaleaRead for ActionType {
}
}
#[derive(AzBuf, Clone, Copy, Debug)]
#[derive(AzBuf, Clone, Copy, Debug, Default)]
pub enum InteractionHand {
#[default]
MainHand = 0,
OffHand = 1,
}

View file

@ -8,6 +8,6 @@ pub struct ServerboundUseItem {
pub hand: InteractionHand,
#[var]
pub sequence: u32,
pub yaw: f32,
pub pitch: f32,
pub y_rot: f32,
pub x_rot: f32,
}

View file

@ -3,6 +3,7 @@ use std::io::{Cursor, Write};
use azalea_buf::{AzBuf, AzaleaRead, AzaleaWrite, BufReadError};
use azalea_core::{
direction::Direction,
hit_result::BlockHitResult,
position::{BlockPos, Vec3},
};
use azalea_protocol_macros::ServerboundGamePacket;
@ -77,3 +78,19 @@ impl AzaleaRead for BlockHit {
})
}
}
impl From<BlockHitResult> for BlockHit {
/// Converts a [`BlockHitResult`] to a [`BlockHit`].
///
/// The only difference is that the `miss` field is not present in
/// [`BlockHit`].
fn from(hit_result: BlockHitResult) -> Self {
Self {
block_pos: hit_result.block_pos,
direction: hit_result.direction,
location: hit_result.location,
inside: hit_result.inside,
world_border: hit_result.world_border,
}
}
}

View file

@ -104,10 +104,10 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
let hit_result = *source.bot.component::<HitResultComponent>();
if hit_result.miss {
let Some(hit_result) = hit_result.as_block_hit_result_if_not_miss() else {
source.reply("I'm not looking at anything");
return 1;
}
};
let block_pos = hit_result.block_pos;
let block = source.bot.world().read().get_block_state(&block_pos);
@ -174,6 +174,13 @@ pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
1
}));
commands.register(literal("startuseitem").executes(|ctx: &Ctx| {
let source = ctx.source.lock();
source.bot.start_use_item();
source.reply("Ok!");
1
}));
commands.register(literal("debugecsleak").executes(|ctx: &Ctx| {
let source = ctx.source.lock();

View file

@ -212,7 +212,7 @@ impl Goal for ReachBlockPosGoal {
let eye_position = n.to_vec3_floored() + Vec3::new(0.5, 1.62, 0.5);
let look_direction = crate::direction_looking_at(&eye_position, &self.pos.center());
let block_hit_result = azalea_client::interact::pick(
let block_hit_result = azalea_client::interact::pick_block(
&look_direction,
&eye_position,
&self.chunk_storage,