azalea_client/plugins/interact/
mod.rs

1pub mod pick;
2
3use std::collections::HashMap;
4
5use azalea_block::BlockState;
6use azalea_core::{
7    direction::Direction,
8    game_type::GameMode,
9    hit_result::{BlockHitResult, HitResult},
10    position::{BlockPos, Vec3},
11    tick::GameTick,
12};
13use azalea_entity::{
14    Attributes, LocalEntity, LookDirection, PlayerAbilities, Position,
15    attributes::{
16        creative_block_interaction_range_modifier, creative_entity_interaction_range_modifier,
17    },
18    clamp_look_direction,
19    indexing::EntityIdIndex,
20    inventory::Inventory,
21};
22use azalea_inventory::{ItemStack, ItemStackData, components};
23use azalea_physics::{
24    PhysicsSystems, collision::entity_collisions::update_last_bounding_box,
25    local_player::PhysicsState,
26};
27use azalea_protocol::packets::game::{
28    ServerboundInteract, ServerboundUseItem,
29    s_interact::{self, InteractionHand},
30    s_swing::ServerboundSwing,
31    s_use_item_on::ServerboundUseItemOn,
32};
33use azalea_registry::builtin::ItemKind;
34use azalea_world::Instance;
35use bevy_app::{App, Plugin, Update};
36use bevy_ecs::prelude::*;
37use tracing::warn;
38
39use super::mining::Mining;
40use crate::{
41    Client,
42    attack::handle_attack_event,
43    interact::pick::{HitResultComponent, update_hit_result_component},
44    inventory::InventorySystems,
45    local_player::{LocalGameMode, PermissionLevel},
46    movement::MoveEventsSystems,
47    packet::game::SendGamePacketEvent,
48    respawn::perform_respawn,
49};
50
51/// A plugin that allows clients to interact with blocks in the world.
52pub struct InteractPlugin;
53impl Plugin for InteractPlugin {
54    fn build(&self, app: &mut App) {
55        app.add_message::<StartUseItemEvent>()
56            .add_systems(
57                Update,
58                (
59                    (
60                        update_attributes_for_held_item,
61                        update_attributes_for_gamemode,
62                    )
63                        .in_set(UpdateAttributesSystems)
64                        .chain(),
65                    handle_start_use_item_event,
66                    update_hit_result_component
67                        .after(clamp_look_direction)
68                        .after(update_last_bounding_box),
69                )
70                    .after(InventorySystems)
71                    .after(MoveEventsSystems)
72                    .after(perform_respawn)
73                    .after(handle_attack_event)
74                    .chain(),
75            )
76            .add_systems(
77                GameTick,
78                handle_start_use_item_queued.before(PhysicsSystems),
79            )
80            .add_observer(handle_entity_interact)
81            .add_observer(handle_swing_arm_trigger);
82    }
83}
84
85#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
86pub struct UpdateAttributesSystems;
87
88impl Client {
89    /// Right-click a block.
90    ///
91    /// The behavior of this depends on the target block,
92    /// and it'll either place the block you're holding in your hand or use the
93    /// block you clicked (like toggling a lever).
94    ///
95    /// Note that this may trigger anticheats as it doesn't take into account
96    /// whether you're actually looking at the block.
97    pub fn block_interact(&self, position: BlockPos) {
98        self.ecs.lock().write_message(StartUseItemEvent {
99            entity: self.entity,
100            hand: InteractionHand::MainHand,
101            force_block: Some(position),
102        });
103    }
104
105    /// Right-click an entity.
106    ///
107    /// This can click through walls, which may trigger anticheats. If that
108    /// behavior isn't desired, consider using [`Client::start_use_item`]
109    /// instead.
110    pub fn entity_interact(&self, entity: Entity) {
111        self.ecs.lock().trigger(EntityInteractEvent {
112            client: self.entity,
113            target: entity,
114            location: None,
115        });
116    }
117
118    /// Right-click the currently held item.
119    ///
120    /// If the item is consumable, then it'll act as if right-click was held
121    /// until the item finishes being consumed. You can use this to eat food.
122    ///
123    /// If we're looking at a block or entity, then it will be clicked. Also see
124    /// [`Client::block_interact`] and [`Client::entity_interact`].
125    pub fn start_use_item(&self) {
126        self.ecs.lock().write_message(StartUseItemEvent {
127            entity: self.entity,
128            hand: InteractionHand::MainHand,
129            force_block: None,
130        });
131    }
132}
133
134/// A component that contains information about our local block state
135/// predictions.
136#[derive(Component, Clone, Debug, Default)]
137pub struct BlockStatePredictionHandler {
138    /// The total number of changes that this client has made to blocks.
139    seq: u32,
140    server_state: HashMap<BlockPos, ServerVerifiedState>,
141}
142#[derive(Clone, Debug)]
143struct ServerVerifiedState {
144    seq: u32,
145    block_state: BlockState,
146    /// Used for teleporting the player back if we're colliding with the block
147    /// that got placed back.
148    #[allow(unused)]
149    player_pos: Vec3,
150}
151
152impl BlockStatePredictionHandler {
153    /// Get the next sequence number that we're going to use and increment the
154    /// value.
155    pub fn start_predicting(&mut self) -> u32 {
156        self.seq += 1;
157        self.seq
158    }
159
160    /// Should be called right before the client updates a block with its
161    /// prediction.
162    ///
163    /// This is used to make sure that we can rollback to this state if the
164    /// server acknowledges the sequence number (with
165    /// [`ClientboundBlockChangedAck`]) without having sent a block update.
166    ///
167    /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
168    pub fn retain_known_server_state(
169        &mut self,
170        pos: BlockPos,
171        old_state: BlockState,
172        player_pos: Vec3,
173    ) {
174        self.server_state
175            .entry(pos)
176            .and_modify(|s| s.seq = self.seq)
177            .or_insert(ServerVerifiedState {
178                seq: self.seq,
179                block_state: old_state,
180                player_pos,
181            });
182    }
183
184    /// Save this update as the correct server state so when the server sends a
185    /// [`ClientboundBlockChangedAck`] we don't roll back this new update.
186    ///
187    /// This should be used when we receive a block update from the server.
188    ///
189    /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
190    pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool {
191        if let Some(s) = self.server_state.get_mut(&pos) {
192            s.block_state = state;
193            true
194        } else {
195            false
196        }
197    }
198
199    pub fn end_prediction_up_to(&mut self, seq: u32, world: &Instance) {
200        let mut to_remove = Vec::new();
201        for (pos, state) in &self.server_state {
202            if state.seq > seq {
203                continue;
204            }
205            to_remove.push(*pos);
206
207            // syncBlockState
208            let client_block_state = world.get_block_state(*pos).unwrap_or_default();
209            let server_block_state = state.block_state;
210            if client_block_state == server_block_state {
211                continue;
212            }
213            world.set_block_state(*pos, server_block_state);
214            // TODO: implement these two functions
215            // if is_colliding(player, *pos, server_block_state) {
216            //     abs_snap_to(state.player_pos);
217            // }
218        }
219
220        for pos in to_remove {
221            self.server_state.remove(&pos);
222        }
223    }
224}
225
226/// An event that makes one of our clients simulate a right-click.
227///
228/// This event just inserts the [`StartUseItemQueued`] component on the given
229/// entity.
230#[doc(alias("right click"))]
231#[derive(Message)]
232pub struct StartUseItemEvent {
233    pub entity: Entity,
234    pub hand: InteractionHand,
235    /// See [`StartUseItemQueued::force_block`].
236    pub force_block: Option<BlockPos>,
237}
238pub fn handle_start_use_item_event(
239    mut commands: Commands,
240    mut events: MessageReader<StartUseItemEvent>,
241) {
242    for event in events.read() {
243        commands.entity(event.entity).insert(StartUseItemQueued {
244            hand: event.hand,
245            force_block: event.force_block,
246        });
247    }
248}
249
250/// A component that makes our client simulate a right-click on the next
251/// [`GameTick`]. It's removed after that tick.
252///
253/// You may find it more convenient to use [`StartUseItemEvent`] instead, which
254/// just inserts this component for you.
255///
256/// [`GameTick`]: azalea_core::tick::GameTick
257#[derive(Component, Debug)]
258pub struct StartUseItemQueued {
259    pub hand: InteractionHand,
260    /// Optionally force us to send a [`ServerboundUseItemOn`] on the given
261    /// block.
262    ///
263    /// This is useful if you want to interact with a block without looking at
264    /// it, but should be avoided to stay compatible with anticheats.
265    pub force_block: Option<BlockPos>,
266}
267#[allow(clippy::type_complexity)]
268pub fn handle_start_use_item_queued(
269    mut commands: Commands,
270    query: Query<(
271        Entity,
272        &StartUseItemQueued,
273        &mut BlockStatePredictionHandler,
274        &HitResultComponent,
275        &LookDirection,
276        Option<&Mining>,
277    )>,
278) {
279    for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
280        query
281    {
282        commands.entity(entity).remove::<StartUseItemQueued>();
283
284        if mining.is_some() {
285            warn!("Got a StartUseItemEvent for a client that was mining");
286        }
287
288        // TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
289        // rowing a boat
290
291        let mut hit_result = (**hit_result).clone();
292
293        if let Some(force_block) = start_use_item.force_block {
294            let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
295                block_hit_result.block_pos == force_block
296            } else {
297                false
298            };
299
300            if !hit_result_matches {
301                // we're not looking at the block, so make up some numbers
302                hit_result = HitResult::Block(BlockHitResult {
303                    location: force_block.center(),
304                    direction: Direction::Up,
305                    block_pos: force_block,
306                    inside: false,
307                    world_border: false,
308                    miss: false,
309                });
310            }
311        }
312
313        match &hit_result {
314            HitResult::Block(r) => {
315                let seq = prediction_handler.start_predicting();
316                if r.miss {
317                    commands.trigger(SendGamePacketEvent::new(
318                        entity,
319                        ServerboundUseItem {
320                            hand: start_use_item.hand,
321                            seq,
322                            x_rot: look_direction.x_rot(),
323                            y_rot: look_direction.y_rot(),
324                        },
325                    ));
326                } else {
327                    commands.trigger(SendGamePacketEvent::new(
328                        entity,
329                        ServerboundUseItemOn {
330                            hand: start_use_item.hand,
331                            block_hit: r.into(),
332                            seq,
333                        },
334                    ));
335                    // TODO: depending on the result of useItemOn, this might
336                    // also need to send a SwingArmEvent.
337                    // basically, this TODO is for simulating block
338                    // interactions/placements on the client-side.
339                }
340            }
341            HitResult::Entity(r) => {
342                commands.trigger(EntityInteractEvent {
343                    client: entity,
344                    target: r.entity,
345                    location: Some(r.location),
346                });
347            }
348        }
349    }
350}
351
352/// An ECS `Event` that makes the client tell the server that we right-clicked
353/// an entity.
354#[derive(EntityEvent, Clone, Debug)]
355pub struct EntityInteractEvent {
356    #[event_target]
357    pub client: Entity,
358    pub target: Entity,
359    /// The position on the entity that we'll tell the server that we clicked
360    /// on.
361    ///
362    /// This doesn't matter for most entities. If it's set to `None` but we're
363    /// looking at the target, it'll use the correct value. If it's `None` and
364    /// we're not looking at the entity, then it'll arbitrary send the target's
365    /// exact position.
366    pub location: Option<Vec3>,
367}
368
369pub fn handle_entity_interact(
370    trigger: On<EntityInteractEvent>,
371    mut commands: Commands,
372    client_query: Query<(&PhysicsState, &EntityIdIndex, &HitResultComponent)>,
373    target_query: Query<&Position>,
374) {
375    let Some((physics_state, entity_id_index, hit_result)) = client_query.get(trigger.client).ok()
376    else {
377        warn!(
378            "tried to interact with an entity but the client didn't have the required components"
379        );
380        return;
381    };
382
383    // TODO: worldborder check
384
385    let Some(entity_id) = entity_id_index.get_by_ecs_entity(trigger.target) else {
386        warn!("tried to interact with an entity that isn't known by the client");
387        return;
388    };
389
390    let location = if let Some(l) = trigger.location {
391        l
392    } else {
393        // if we're looking at the entity, use that
394        if let Some(entity_hit_result) = hit_result.as_entity_hit_result()
395            && entity_hit_result.entity == trigger.target
396        {
397            entity_hit_result.location
398        } else {
399            // if we're not looking at the entity, make up a value that's good enough by
400            // using the entity's position
401            let Ok(target_position) = target_query.get(trigger.target) else {
402                warn!("tried to look at an entity without the entity having a position");
403                return;
404            };
405            **target_position
406        }
407    };
408
409    let mut interact = ServerboundInteract {
410        entity_id,
411        action: s_interact::ActionType::InteractAt {
412            location,
413            hand: InteractionHand::MainHand,
414        },
415        using_secondary_action: physics_state.trying_to_crouch,
416    };
417    commands.trigger(SendGamePacketEvent::new(trigger.client, interact.clone()));
418
419    // TODO: this is true if the interaction failed, which i think can only happen
420    // in certain cases when interacting with armor stands
421    let consumes_action = false;
422    if !consumes_action {
423        // but yes, most of the time vanilla really does send two interact packets like
424        // this
425        interact.action = s_interact::ActionType::Interact {
426            hand: InteractionHand::MainHand,
427        };
428        commands.trigger(SendGamePacketEvent::new(trigger.client, interact));
429    }
430}
431
432/// Whether we can't interact with the block, based on your gamemode.
433///
434/// If this is false, then we can interact with the block.
435///
436/// Passing the inventory, block position, and instance is necessary for the
437/// adventure mode check.
438pub fn check_is_interaction_restricted(
439    instance: &Instance,
440    block_pos: BlockPos,
441    game_mode: &GameMode,
442    inventory: &Inventory,
443) -> bool {
444    match game_mode {
445        GameMode::Adventure => {
446            // vanilla checks for abilities.mayBuild here but servers have no
447            // way of modifying that
448
449            let held_item = inventory.held_item();
450            match &held_item {
451                ItemStack::Present(item) => {
452                    let block = instance.chunks.get_block_state(block_pos);
453                    let Some(block) = block else {
454                        // block isn't loaded so just say that it is restricted
455                        return true;
456                    };
457                    check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
458                }
459                _ => true,
460            }
461        }
462        GameMode::Spectator => true,
463        _ => false,
464    }
465}
466
467/// Check if the item has the `CanDestroy` tag for the block.
468pub fn check_block_can_be_broken_by_item_in_adventure_mode(
469    item: &ItemStackData,
470    _block: &BlockState,
471) -> bool {
472    // minecraft caches the last checked block but that's kind of an unnecessary
473    // optimization and makes the code too complicated
474
475    if item.get_component::<components::CanBreak>().is_none() {
476        // no CanDestroy tag
477        return false;
478    };
479
480    false
481
482    // for block_predicate in can_destroy {
483    //     // TODO
484    //     // defined in BlockPredicateArgument.java
485    // }
486
487    // true
488}
489
490pub fn can_use_game_master_blocks(
491    abilities: &PlayerAbilities,
492    permission_level: &PermissionLevel,
493) -> bool {
494    abilities.instant_break && **permission_level >= 2
495}
496
497/// Swing your arm.
498///
499/// This is purely a visual effect and won't interact with anything in the
500/// world.
501#[derive(EntityEvent, Clone, Debug)]
502pub struct SwingArmEvent {
503    pub entity: Entity,
504}
505pub fn handle_swing_arm_trigger(swing_arm: On<SwingArmEvent>, mut commands: Commands) {
506    commands.trigger(SendGamePacketEvent::new(
507        swing_arm.entity,
508        ServerboundSwing {
509            hand: InteractionHand::MainHand,
510        },
511    ));
512}
513
514#[allow(clippy::type_complexity)]
515fn update_attributes_for_held_item(
516    mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
517) {
518    for (mut attributes, inventory) in &mut query {
519        let held_item = inventory.held_item();
520
521        let added_attack_speed = added_attack_speed_for_item(held_item.kind());
522        attributes
523            .attack_speed
524            .insert(azalea_entity::attributes::base_attack_speed_modifier(
525                added_attack_speed,
526            ));
527    }
528}
529
530fn added_attack_speed_for_item(item: ItemKind) -> f64 {
531    match item {
532        ItemKind::WoodenSword => -2.4,
533        ItemKind::WoodenShovel => -3.0,
534        ItemKind::WoodenPickaxe => -2.8,
535        ItemKind::WoodenAxe => -3.2,
536        ItemKind::WoodenHoe => -3.0,
537
538        ItemKind::StoneSword => -2.4,
539        ItemKind::StoneShovel => -3.0,
540        ItemKind::StonePickaxe => -2.8,
541        ItemKind::StoneAxe => -3.2,
542        ItemKind::StoneHoe => -2.0,
543
544        ItemKind::GoldenSword => -2.4,
545        ItemKind::GoldenShovel => -3.0,
546        ItemKind::GoldenPickaxe => -2.8,
547        ItemKind::GoldenAxe => -3.0,
548        ItemKind::GoldenHoe => -3.0,
549
550        ItemKind::IronSword => -2.4,
551        ItemKind::IronShovel => -3.0,
552        ItemKind::IronPickaxe => -2.8,
553        ItemKind::IronAxe => -3.1,
554        ItemKind::IronHoe => -1.0,
555
556        ItemKind::DiamondSword => -2.4,
557        ItemKind::DiamondShovel => -3.0,
558        ItemKind::DiamondPickaxe => -2.8,
559        ItemKind::DiamondAxe => -3.0,
560        ItemKind::DiamondHoe => 0.0,
561
562        ItemKind::NetheriteSword => -2.4,
563        ItemKind::NetheriteShovel => -3.0,
564        ItemKind::NetheritePickaxe => -2.8,
565        ItemKind::NetheriteAxe => -3.0,
566        ItemKind::NetheriteHoe => 0.0,
567
568        ItemKind::Trident => -2.9,
569        _ => 0.,
570    }
571}
572
573#[allow(clippy::type_complexity)]
574fn update_attributes_for_gamemode(
575    query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
576) {
577    for (mut attributes, game_mode) in query {
578        if game_mode.current == GameMode::Creative {
579            attributes
580                .block_interaction_range
581                .insert(creative_block_interaction_range_modifier());
582            attributes
583                .entity_interaction_range
584                .insert(creative_entity_interaction_range_modifier());
585        } else {
586            attributes
587                .block_interaction_range
588                .remove(&creative_block_interaction_range_modifier().id);
589            attributes
590                .entity_interaction_range
591                .remove(&creative_entity_interaction_range_modifier().id);
592        }
593    }
594}