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};
21use azalea_inventory::{ItemStack, ItemStackData, components};
22use azalea_physics::{
23    PhysicsSystems, collision::entity_collisions::update_last_bounding_box,
24    local_player::PhysicsState,
25};
26use azalea_protocol::packets::game::{
27    ServerboundInteract, ServerboundUseItem,
28    s_interact::{self, InteractionHand},
29    s_swing::ServerboundSwing,
30    s_use_item_on::ServerboundUseItemOn,
31};
32use azalea_world::Instance;
33use bevy_app::{App, Plugin, Update};
34use bevy_ecs::prelude::*;
35use tracing::warn;
36
37use super::mining::Mining;
38use crate::{
39    Client,
40    attack::handle_attack_event,
41    interact::pick::{HitResultComponent, update_hit_result_component},
42    inventory::{Inventory, InventorySystems},
43    local_player::{LocalGameMode, PermissionLevel},
44    movement::MoveEventsSystems,
45    packet::game::SendGamePacketEvent,
46    respawn::perform_respawn,
47};
48
49/// A plugin that allows clients to interact with blocks in the world.
50pub struct InteractPlugin;
51impl Plugin for InteractPlugin {
52    fn build(&self, app: &mut App) {
53        app.add_message::<StartUseItemEvent>()
54            .add_message::<EntityInteractEvent>()
55            .add_systems(
56                Update,
57                (
58                    (
59                        update_attributes_for_held_item,
60                        update_attributes_for_gamemode,
61                    )
62                        .in_set(UpdateAttributesSystems)
63                        .chain(),
64                    handle_start_use_item_event,
65                    update_hit_result_component
66                        .after(clamp_look_direction)
67                        .after(update_last_bounding_box),
68                )
69                    .after(InventorySystems)
70                    .after(MoveEventsSystems)
71                    .after(perform_respawn)
72                    .after(handle_attack_event)
73                    .chain(),
74            )
75            .add_systems(
76                GameTick,
77                (handle_start_use_item_queued, handle_entity_interact)
78                    .chain()
79                    .before(PhysicsSystems),
80            )
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().write_message(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    mut entity_interact: MessageWriter<EntityInteractEvent>,
279) {
280    for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
281        query
282    {
283        commands.entity(entity).remove::<StartUseItemQueued>();
284
285        if mining.is_some() {
286            warn!("Got a StartUseItemEvent for a client that was mining");
287        }
288
289        // TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
290        // rowing a boat
291
292        let mut hit_result = (**hit_result).clone();
293
294        if let Some(force_block) = start_use_item.force_block {
295            let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
296                block_hit_result.block_pos == force_block
297            } else {
298                false
299            };
300
301            if !hit_result_matches {
302                // we're not looking at the block, so make up some numbers
303                hit_result = HitResult::Block(BlockHitResult {
304                    location: force_block.center(),
305                    direction: Direction::Up,
306                    block_pos: force_block,
307                    inside: false,
308                    world_border: false,
309                    miss: false,
310                });
311            }
312        }
313
314        match &hit_result {
315            HitResult::Block(r) => {
316                let seq = prediction_handler.start_predicting();
317                if r.miss {
318                    commands.trigger(SendGamePacketEvent::new(
319                        entity,
320                        ServerboundUseItem {
321                            hand: start_use_item.hand,
322                            seq,
323                            x_rot: look_direction.x_rot(),
324                            y_rot: look_direction.y_rot(),
325                        },
326                    ));
327                } else {
328                    commands.trigger(SendGamePacketEvent::new(
329                        entity,
330                        ServerboundUseItemOn {
331                            hand: start_use_item.hand,
332                            block_hit: r.into(),
333                            seq,
334                        },
335                    ));
336                    // TODO: depending on the result of useItemOn, this might
337                    // also need to send a SwingArmEvent.
338                    // basically, this TODO is for simulating block
339                    // interactions/placements on the client-side.
340                }
341            }
342            HitResult::Entity(r) => {
343                entity_interact.write(EntityInteractEvent {
344                    client: entity,
345                    target: r.entity,
346                    location: Some(r.location),
347                });
348            }
349        }
350    }
351}
352
353/// An ECS `Message` that makes the client tell the server that we right-clicked
354/// an entity.
355#[derive(Message)]
356pub struct EntityInteractEvent {
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    mut events: MessageReader<EntityInteractEvent>,
371    mut commands: Commands,
372    client_query: Query<(&PhysicsState, &EntityIdIndex, &HitResultComponent)>,
373    target_query: Query<&Position>,
374) {
375    for event in events.read() {
376        let Some((physics_state, entity_id_index, hit_result)) =
377            client_query.get(event.target).ok()
378        else {
379            warn!(
380                "tried to interact with an entity but the client didn't have the required components"
381            );
382            continue;
383        };
384
385        // TODO: worldborder check
386
387        let Some(entity_id) = entity_id_index.get_by_ecs_entity(event.target) else {
388            warn!("tried to interact with an entity that isn't known by the client");
389            continue;
390        };
391
392        let location = if let Some(l) = event.location {
393            l
394        } else {
395            // if we're looking at the entity, use that
396            if let Some(entity_hit_result) = hit_result.as_entity_hit_result()
397                && entity_hit_result.entity == event.target
398            {
399                entity_hit_result.location
400            } else {
401                // if we're not looking at the entity, make up a value that's good enough by
402                // using the entity's position
403                let Ok(target_position) = target_query.get(event.target) else {
404                    warn!("tried to look at an entity without the entity having a position");
405                    continue;
406                };
407                **target_position
408            }
409        };
410
411        let mut interact = ServerboundInteract {
412            entity_id,
413            action: s_interact::ActionType::InteractAt {
414                location,
415                hand: InteractionHand::MainHand,
416            },
417            using_secondary_action: physics_state.trying_to_crouch,
418        };
419        commands.trigger(SendGamePacketEvent::new(event.client, interact.clone()));
420        // TODO: this is true if the interaction failed, which i think can only happen
421        // in certain cases when interacting with armor stands
422        let consumes_action = false;
423        if !consumes_action {
424            // but yes, most of the time vanilla really does send two interact packets like
425            // this
426            interact.action = s_interact::ActionType::Interact {
427                hand: InteractionHand::MainHand,
428            };
429            commands.trigger(SendGamePacketEvent::new(event.client, interact));
430        }
431    }
432}
433
434/// Whether we can't interact with the block, based on your gamemode.
435///
436/// If this is false, then we can interact with the block.
437///
438/// Passing the inventory, block position, and instance is necessary for the
439/// adventure mode check.
440pub fn check_is_interaction_restricted(
441    instance: &Instance,
442    block_pos: BlockPos,
443    game_mode: &GameMode,
444    inventory: &Inventory,
445) -> bool {
446    match game_mode {
447        GameMode::Adventure => {
448            // vanilla checks for abilities.mayBuild here but servers have no
449            // way of modifying that
450
451            let held_item = inventory.held_item();
452            match &held_item {
453                ItemStack::Present(item) => {
454                    let block = instance.chunks.get_block_state(block_pos);
455                    let Some(block) = block else {
456                        // block isn't loaded so just say that it is restricted
457                        return true;
458                    };
459                    check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
460                }
461                _ => true,
462            }
463        }
464        GameMode::Spectator => true,
465        _ => false,
466    }
467}
468
469/// Check if the item has the `CanDestroy` tag for the block.
470pub fn check_block_can_be_broken_by_item_in_adventure_mode(
471    item: &ItemStackData,
472    _block: &BlockState,
473) -> bool {
474    // minecraft caches the last checked block but that's kind of an unnecessary
475    // optimization and makes the code too complicated
476
477    if item.get_component::<components::CanBreak>().is_none() {
478        // no CanDestroy tag
479        return false;
480    };
481
482    false
483
484    // for block_predicate in can_destroy {
485    //     // TODO
486    //     // defined in BlockPredicateArgument.java
487    // }
488
489    // true
490}
491
492pub fn can_use_game_master_blocks(
493    abilities: &PlayerAbilities,
494    permission_level: &PermissionLevel,
495) -> bool {
496    abilities.instant_break && **permission_level >= 2
497}
498
499/// Swing your arm.
500///
501/// This is purely a visual effect and won't interact with anything in the
502/// world.
503#[derive(EntityEvent, Clone, Debug)]
504pub struct SwingArmEvent {
505    pub entity: Entity,
506}
507pub fn handle_swing_arm_trigger(swing_arm: On<SwingArmEvent>, mut commands: Commands) {
508    commands.trigger(SendGamePacketEvent::new(
509        swing_arm.entity,
510        ServerboundSwing {
511            hand: InteractionHand::MainHand,
512        },
513    ));
514}
515
516#[allow(clippy::type_complexity)]
517fn update_attributes_for_held_item(
518    mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
519) {
520    for (mut attributes, inventory) in &mut query {
521        let held_item = inventory.held_item();
522
523        use azalea_registry::Item;
524        let added_attack_speed = match held_item.kind() {
525            Item::WoodenSword => -2.4,
526            Item::WoodenShovel => -3.0,
527            Item::WoodenPickaxe => -2.8,
528            Item::WoodenAxe => -3.2,
529            Item::WoodenHoe => -3.0,
530
531            Item::StoneSword => -2.4,
532            Item::StoneShovel => -3.0,
533            Item::StonePickaxe => -2.8,
534            Item::StoneAxe => -3.2,
535            Item::StoneHoe => -2.0,
536
537            Item::GoldenSword => -2.4,
538            Item::GoldenShovel => -3.0,
539            Item::GoldenPickaxe => -2.8,
540            Item::GoldenAxe => -3.0,
541            Item::GoldenHoe => -3.0,
542
543            Item::IronSword => -2.4,
544            Item::IronShovel => -3.0,
545            Item::IronPickaxe => -2.8,
546            Item::IronAxe => -3.1,
547            Item::IronHoe => -1.0,
548
549            Item::DiamondSword => -2.4,
550            Item::DiamondShovel => -3.0,
551            Item::DiamondPickaxe => -2.8,
552            Item::DiamondAxe => -3.0,
553            Item::DiamondHoe => 0.0,
554
555            Item::NetheriteSword => -2.4,
556            Item::NetheriteShovel => -3.0,
557            Item::NetheritePickaxe => -2.8,
558            Item::NetheriteAxe => -3.0,
559            Item::NetheriteHoe => 0.0,
560
561            Item::Trident => -2.9,
562            _ => 0.,
563        };
564        attributes
565            .attack_speed
566            .insert(azalea_entity::attributes::base_attack_speed_modifier(
567                added_attack_speed,
568            ));
569    }
570}
571
572#[allow(clippy::type_complexity)]
573fn update_attributes_for_gamemode(
574    query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
575) {
576    for (mut attributes, game_mode) in query {
577        if game_mode.current == GameMode::Creative {
578            attributes
579                .block_interaction_range
580                .insert(creative_block_interaction_range_modifier());
581            attributes
582                .entity_interaction_range
583                .insert(creative_entity_interaction_range_modifier());
584        } else {
585            attributes
586                .block_interaction_range
587                .remove(&creative_block_interaction_range_modifier().id);
588            attributes
589                .entity_interaction_range
590                .remove(&creative_entity_interaction_range_modifier().id);
591        }
592    }
593}