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