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