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