azalea_client/plugins/
interact.rs

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