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