azalea_client/plugins/
interact.rs

1use std::ops::AddAssign;
2
3use azalea_block::BlockState;
4use azalea_core::{
5    block_hit_result::BlockHitResult,
6    direction::Direction,
7    game_type::GameMode,
8    position::{BlockPos, Vec3},
9};
10use azalea_entity::{
11    Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector,
12};
13use azalea_inventory::{ItemStack, ItemStackData, components};
14use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
15use azalea_protocol::packets::game::{
16    s_interact::InteractionHand,
17    s_swing::ServerboundSwing,
18    s_use_item_on::{BlockHit, ServerboundUseItemOn},
19};
20use azalea_world::{Instance, InstanceContainer, InstanceName};
21use bevy_app::{App, Plugin, Update};
22use bevy_ecs::prelude::*;
23use derive_more::{Deref, DerefMut};
24use tracing::warn;
25
26use crate::{
27    Client,
28    attack::handle_attack_event,
29    inventory::{Inventory, InventorySet},
30    local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
31    movement::MoveEventsSet,
32    packet::game::SendPacketEvent,
33    respawn::perform_respawn,
34};
35
36/// A plugin that allows clients to interact with blocks in the world.
37pub struct InteractPlugin;
38impl Plugin for InteractPlugin {
39    fn build(&self, app: &mut App) {
40        app.add_event::<BlockInteractEvent>()
41            .add_event::<SwingArmEvent>()
42            .add_systems(
43                Update,
44                (
45                    (
46                        update_hit_result_component.after(clamp_look_direction),
47                        handle_block_interact_event,
48                        handle_swing_arm_event,
49                    )
50                        .after(InventorySet)
51                        .after(perform_respawn)
52                        .after(handle_attack_event)
53                        .chain(),
54                    update_modifiers_for_held_item
55                        .after(InventorySet)
56                        .after(MoveEventsSet),
57                ),
58            )
59            .add_observer(handle_swing_arm_trigger);
60    }
61}
62
63impl Client {
64    /// Right click a block. The behavior of this depends on the target block,
65    /// and it'll either place the block you're holding in your hand or use the
66    /// block you clicked (like toggling a lever).
67    ///
68    /// Note that this may trigger anticheats as it doesn't take into account
69    /// whether you're actually looking at the block.
70    pub fn block_interact(&self, position: BlockPos) {
71        self.ecs.lock().send_event(BlockInteractEvent {
72            entity: self.entity,
73            position,
74        });
75    }
76}
77
78/// Right click a block. The behavior of this depends on the target block,
79/// and it'll either place the block you're holding in your hand or use the
80/// block you clicked (like toggling a lever).
81#[derive(Event)]
82pub struct BlockInteractEvent {
83    /// The local player entity that's opening the container.
84    pub entity: Entity,
85    /// The coordinates of the container.
86    pub position: BlockPos,
87}
88
89/// A component that contains the number of changes this client has made to
90/// blocks.
91#[derive(Component, Copy, Clone, Debug, Default, Deref)]
92pub struct CurrentSequenceNumber(u32);
93
94impl AddAssign<u32> for CurrentSequenceNumber {
95    fn add_assign(&mut self, rhs: u32) {
96        self.0 += rhs;
97    }
98}
99
100/// A component that contains the block that the player is currently looking at.
101#[derive(Component, Clone, Debug, Deref, DerefMut)]
102pub struct HitResultComponent(BlockHitResult);
103
104pub fn handle_block_interact_event(
105    mut events: EventReader<BlockInteractEvent>,
106    mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>,
107    mut commands: Commands,
108) {
109    for event in events.read() {
110        let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else {
111            warn!("Sent BlockInteractEvent for entity that doesn't have the required components");
112            continue;
113        };
114
115        // TODO: check to make sure we're within the world border
116
117        *sequence_number += 1;
118
119        // minecraft also does the interaction client-side (so it looks like clicking a
120        // button is instant) but we don't really need that
121
122        // the block_hit data will depend on whether we're looking at the block and
123        // whether we can reach it
124
125        let block_hit = if hit_result.block_pos == event.position {
126            // we're looking at the block :)
127            BlockHit {
128                block_pos: hit_result.block_pos,
129                direction: hit_result.direction,
130                location: hit_result.location,
131                inside: hit_result.inside,
132                world_border: hit_result.world_border,
133            }
134        } else {
135            // we're not looking at the block, so make up some numbers
136            BlockHit {
137                block_pos: event.position,
138                direction: Direction::Up,
139                location: event.position.center(),
140                inside: false,
141                world_border: false,
142            }
143        };
144
145        commands.trigger(SendPacketEvent::new(
146            entity,
147            ServerboundUseItemOn {
148                hand: InteractionHand::MainHand,
149                block_hit,
150                sequence: sequence_number.0,
151            },
152        ));
153    }
154}
155
156#[allow(clippy::type_complexity)]
157pub fn update_hit_result_component(
158    mut commands: Commands,
159    mut query: Query<(
160        Entity,
161        Option<&mut HitResultComponent>,
162        &LocalGameMode,
163        &Position,
164        &EyeHeight,
165        &LookDirection,
166        &InstanceName,
167    )>,
168    instance_container: Res<InstanceContainer>,
169) {
170    for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in
171        &mut query
172    {
173        let pick_range = if game_mode.current == GameMode::Creative {
174            6.
175        } else {
176            4.5
177        };
178        let eye_position = Vec3 {
179            x: position.x,
180            y: position.y + **eye_height as f64,
181            z: position.z,
182        };
183
184        let Some(instance_lock) = instance_container.get(world_name) else {
185            continue;
186        };
187        let instance = instance_lock.read();
188
189        let hit_result = pick(look_direction, &eye_position, &instance.chunks, pick_range);
190        if let Some(mut hit_result_ref) = hit_result_ref {
191            **hit_result_ref = hit_result;
192        } else {
193            commands
194                .entity(entity)
195                .insert(HitResultComponent(hit_result));
196        }
197    }
198}
199
200/// Get the block that a player would be looking at if their eyes were at the
201/// given direction and position.
202///
203/// If you need to get the block the player is looking at right now, use
204/// [`HitResultComponent`].
205pub fn pick(
206    look_direction: &LookDirection,
207    eye_position: &Vec3,
208    chunks: &azalea_world::ChunkStorage,
209    pick_range: f64,
210) -> BlockHitResult {
211    let view_vector = view_vector(look_direction);
212    let end_position = eye_position + &(view_vector * pick_range);
213    azalea_physics::clip::clip(
214        chunks,
215        ClipContext {
216            from: *eye_position,
217            to: end_position,
218            block_shape_type: BlockShapeType::Outline,
219            fluid_pick_type: FluidPickType::None,
220        },
221    )
222}
223
224/// Whether we can't interact with the block, based on your gamemode. If
225/// this is false, then we can interact with the block.
226///
227/// Passing the inventory, block position, and instance is necessary for the
228/// adventure mode check.
229pub fn check_is_interaction_restricted(
230    instance: &Instance,
231    block_pos: &BlockPos,
232    game_mode: &GameMode,
233    inventory: &Inventory,
234) -> bool {
235    match game_mode {
236        GameMode::Adventure => {
237            // vanilla checks for abilities.mayBuild here but servers have no
238            // way of modifying that
239
240            let held_item = inventory.held_item();
241            match &held_item {
242                ItemStack::Present(item) => {
243                    let block = instance.chunks.get_block_state(block_pos);
244                    let Some(block) = block else {
245                        // block isn't loaded so just say that it is restricted
246                        return true;
247                    };
248                    check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
249                }
250                _ => true,
251            }
252        }
253        GameMode::Spectator => true,
254        _ => false,
255    }
256}
257
258/// Check if the item has the `CanDestroy` tag for the block.
259pub fn check_block_can_be_broken_by_item_in_adventure_mode(
260    item: &ItemStackData,
261    _block: &BlockState,
262) -> bool {
263    // minecraft caches the last checked block but that's kind of an unnecessary
264    // optimization and makes the code too complicated
265
266    if !item.components.has::<components::CanBreak>() {
267        // no CanDestroy tag
268        return false;
269    };
270
271    false
272
273    // for block_predicate in can_destroy {
274    //     // TODO
275    //     // defined in BlockPredicateArgument.java
276    // }
277
278    // true
279}
280
281pub fn can_use_game_master_blocks(
282    abilities: &PlayerAbilities,
283    permission_level: &PermissionLevel,
284) -> bool {
285    abilities.instant_break && **permission_level >= 2
286}
287
288/// Swing your arm. This is purely a visual effect and won't interact with
289/// anything in the world.
290#[derive(Event, Clone, Debug)]
291pub struct SwingArmEvent {
292    pub entity: Entity,
293}
294pub fn handle_swing_arm_trigger(trigger: Trigger<SwingArmEvent>, mut commands: Commands) {
295    commands.trigger(SendPacketEvent::new(
296        trigger.event().entity,
297        ServerboundSwing {
298            hand: InteractionHand::MainHand,
299        },
300    ));
301}
302pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) {
303    for event in events.read() {
304        commands.trigger(event.clone());
305    }
306}
307
308#[allow(clippy::type_complexity)]
309fn update_modifiers_for_held_item(
310    mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
311) {
312    for (mut attributes, inventory) in &mut query {
313        let held_item = inventory.held_item();
314
315        use azalea_registry::Item;
316        let added_attack_speed = match held_item.kind() {
317            Item::WoodenSword => -2.4,
318            Item::WoodenShovel => -3.0,
319            Item::WoodenPickaxe => -2.8,
320            Item::WoodenAxe => -3.2,
321            Item::WoodenHoe => -3.0,
322
323            Item::StoneSword => -2.4,
324            Item::StoneShovel => -3.0,
325            Item::StonePickaxe => -2.8,
326            Item::StoneAxe => -3.2,
327            Item::StoneHoe => -2.0,
328
329            Item::GoldenSword => -2.4,
330            Item::GoldenShovel => -3.0,
331            Item::GoldenPickaxe => -2.8,
332            Item::GoldenAxe => -3.0,
333            Item::GoldenHoe => -3.0,
334
335            Item::IronSword => -2.4,
336            Item::IronShovel => -3.0,
337            Item::IronPickaxe => -2.8,
338            Item::IronAxe => -3.1,
339            Item::IronHoe => -1.0,
340
341            Item::DiamondSword => -2.4,
342            Item::DiamondShovel => -3.0,
343            Item::DiamondPickaxe => -2.8,
344            Item::DiamondAxe => -3.0,
345            Item::DiamondHoe => 0.0,
346
347            Item::NetheriteSword => -2.4,
348            Item::NetheriteShovel => -3.0,
349            Item::NetheritePickaxe => -2.8,
350            Item::NetheriteAxe => -3.0,
351            Item::NetheriteHoe => 0.0,
352
353            Item::Trident => -2.9,
354            _ => 0.,
355        };
356        attributes
357            .attack_speed
358            .insert(azalea_entity::attributes::base_attack_speed_modifier(
359                added_attack_speed,
360            ));
361    }
362}