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,
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;
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, PlayerAbilities},
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        Option<&Mining>,
254    )>,
255    entity_id_query: Query<&MinecraftEntityId>,
256) {
257    for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
258        query
259    {
260        commands.entity(entity).remove::<StartUseItemQueued>();
261
262        if mining.is_some() {
263            warn!("Got a StartUseItemEvent for a client that was mining");
264        }
265
266        // TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
267        // rowing a boat
268
269        let mut hit_result = (**hit_result).clone();
270
271        if let Some(force_block) = start_use_item.force_block {
272            let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
273                block_hit_result.block_pos == force_block
274            } else {
275                false
276            };
277
278            if !hit_result_matches {
279                // we're not looking at the block, so make up some numbers
280                hit_result = HitResult::Block(BlockHitResult {
281                    location: force_block.center(),
282                    direction: Direction::Up,
283                    block_pos: force_block,
284                    inside: false,
285                    world_border: false,
286                    miss: false,
287                });
288            }
289        }
290
291        match &hit_result {
292            HitResult::Block(r) => {
293                let seq = prediction_handler.start_predicting();
294                if r.miss {
295                    commands.trigger(SendPacketEvent::new(
296                        entity,
297                        ServerboundUseItem {
298                            hand: start_use_item.hand,
299                            seq,
300                            x_rot: look_direction.x_rot,
301                            y_rot: look_direction.y_rot,
302                        },
303                    ));
304                } else {
305                    commands.trigger(SendPacketEvent::new(
306                        entity,
307                        ServerboundUseItemOn {
308                            hand: start_use_item.hand,
309                            block_hit: r.into(),
310                            seq,
311                        },
312                    ));
313                    // TODO: depending on the result of useItemOn, this might
314                    // also need to send a SwingArmEvent.
315                    // basically, this TODO is for simulating block
316                    // interactions/placements on the client-side.
317                }
318            }
319            HitResult::Entity(r) => {
320                // TODO: worldborder check
321
322                let Ok(entity_id) = entity_id_query.get(r.entity).copied() else {
323                    warn!("tried to interact with an entity that doesn't have MinecraftEntityId");
324                    continue;
325                };
326
327                commands.trigger(SendPacketEvent::new(
328                    entity,
329                    ServerboundInteract {
330                        entity_id,
331                        action: s_interact::ActionType::InteractAt {
332                            location: r.location,
333                            hand: InteractionHand::MainHand,
334                        },
335                        // TODO: sneaking
336                        using_secondary_action: false,
337                    },
338                ));
339            }
340        }
341    }
342}
343
344/// Whether we can't interact with the block, based on your gamemode. If
345/// this is false, then we can interact with the block.
346///
347/// Passing the inventory, block position, and instance is necessary for the
348/// adventure mode check.
349pub fn check_is_interaction_restricted(
350    instance: &Instance,
351    block_pos: BlockPos,
352    game_mode: &GameMode,
353    inventory: &Inventory,
354) -> bool {
355    match game_mode {
356        GameMode::Adventure => {
357            // vanilla checks for abilities.mayBuild here but servers have no
358            // way of modifying that
359
360            let held_item = inventory.held_item();
361            match &held_item {
362                ItemStack::Present(item) => {
363                    let block = instance.chunks.get_block_state(block_pos);
364                    let Some(block) = block else {
365                        // block isn't loaded so just say that it is restricted
366                        return true;
367                    };
368                    check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
369                }
370                _ => true,
371            }
372        }
373        GameMode::Spectator => true,
374        _ => false,
375    }
376}
377
378/// Check if the item has the `CanDestroy` tag for the block.
379pub fn check_block_can_be_broken_by_item_in_adventure_mode(
380    item: &ItemStackData,
381    _block: &BlockState,
382) -> bool {
383    // minecraft caches the last checked block but that's kind of an unnecessary
384    // optimization and makes the code too complicated
385
386    if !item.components.has::<components::CanBreak>() {
387        // no CanDestroy tag
388        return false;
389    };
390
391    false
392
393    // for block_predicate in can_destroy {
394    //     // TODO
395    //     // defined in BlockPredicateArgument.java
396    // }
397
398    // true
399}
400
401pub fn can_use_game_master_blocks(
402    abilities: &PlayerAbilities,
403    permission_level: &PermissionLevel,
404) -> bool {
405    abilities.instant_break && **permission_level >= 2
406}
407
408/// Swing your arm. This is purely a visual effect and won't interact with
409/// anything in the world.
410#[derive(Event, Clone, Debug)]
411pub struct SwingArmEvent {
412    pub entity: Entity,
413}
414pub fn handle_swing_arm_trigger(trigger: Trigger<SwingArmEvent>, mut commands: Commands) {
415    commands.trigger(SendPacketEvent::new(
416        trigger.event().entity,
417        ServerboundSwing {
418            hand: InteractionHand::MainHand,
419        },
420    ));
421}
422pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) {
423    for event in events.read() {
424        commands.trigger(event.clone());
425    }
426}
427
428#[allow(clippy::type_complexity)]
429fn update_attributes_for_held_item(
430    mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
431) {
432    for (mut attributes, inventory) in &mut query {
433        let held_item = inventory.held_item();
434
435        use azalea_registry::Item;
436        let added_attack_speed = match held_item.kind() {
437            Item::WoodenSword => -2.4,
438            Item::WoodenShovel => -3.0,
439            Item::WoodenPickaxe => -2.8,
440            Item::WoodenAxe => -3.2,
441            Item::WoodenHoe => -3.0,
442
443            Item::StoneSword => -2.4,
444            Item::StoneShovel => -3.0,
445            Item::StonePickaxe => -2.8,
446            Item::StoneAxe => -3.2,
447            Item::StoneHoe => -2.0,
448
449            Item::GoldenSword => -2.4,
450            Item::GoldenShovel => -3.0,
451            Item::GoldenPickaxe => -2.8,
452            Item::GoldenAxe => -3.0,
453            Item::GoldenHoe => -3.0,
454
455            Item::IronSword => -2.4,
456            Item::IronShovel => -3.0,
457            Item::IronPickaxe => -2.8,
458            Item::IronAxe => -3.1,
459            Item::IronHoe => -1.0,
460
461            Item::DiamondSword => -2.4,
462            Item::DiamondShovel => -3.0,
463            Item::DiamondPickaxe => -2.8,
464            Item::DiamondAxe => -3.0,
465            Item::DiamondHoe => 0.0,
466
467            Item::NetheriteSword => -2.4,
468            Item::NetheriteShovel => -3.0,
469            Item::NetheritePickaxe => -2.8,
470            Item::NetheriteAxe => -3.0,
471            Item::NetheriteHoe => 0.0,
472
473            Item::Trident => -2.9,
474            _ => 0.,
475        };
476        attributes
477            .attack_speed
478            .insert(azalea_entity::attributes::base_attack_speed_modifier(
479                added_attack_speed,
480            ));
481    }
482}
483
484#[allow(clippy::type_complexity)]
485fn update_attributes_for_gamemode(
486    query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
487) {
488    for (mut attributes, game_mode) in query {
489        if game_mode.current == GameMode::Creative {
490            attributes
491                .block_interaction_range
492                .insert(creative_block_interaction_range_modifier());
493            attributes
494                .entity_interaction_range
495                .insert(creative_entity_interaction_range_modifier());
496        } else {
497            attributes
498                .block_interaction_range
499                .remove(&creative_block_interaction_range_modifier().id);
500            attributes
501                .entity_interaction_range
502                .remove(&creative_entity_interaction_range_modifier().id);
503        }
504    }
505}