Skip to main content

azalea_client/plugins/interact/
mod.rs

1pub mod pick;
2
3use std::collections::HashMap;
4
5use azalea_block::BlockState;
6use azalea_core::{
7    delta::LpVec3,
8    direction::Direction,
9    game_type::GameMode,
10    hit_result::{BlockHitResult, HitResult},
11    position::{BlockPos, Vec3},
12    tick::GameTick,
13};
14use azalea_entity::{
15    Attributes, LocalEntity, LookDirection, PlayerAbilities, Position,
16    attributes::{
17        creative_block_interaction_range_modifier, creative_entity_interaction_range_modifier,
18    },
19    clamp_look_direction,
20    indexing::EntityIdIndex,
21    inventory::Inventory,
22};
23use azalea_inventory::{ItemStack, ItemStackData, components};
24use azalea_physics::{
25    PhysicsSystems, client_movement::ClientMovementState,
26    collision::entity_collisions::update_last_bounding_box,
27};
28use azalea_protocol::packets::game::{
29    ServerboundInteract, ServerboundUseItem, s_interact::InteractionHand,
30    s_swing::ServerboundSwing, s_use_item_on::ServerboundUseItemOn,
31};
32use azalea_world::World;
33use bevy_app::{App, Plugin, Update};
34use bevy_ecs::prelude::*;
35use tracing::warn;
36
37use super::mining::Mining;
38use crate::{
39    attack::handle_attack_event,
40    interact::pick::{HitResultComponent, update_hit_result_component},
41    inventory::InventorySystems,
42    local_player::{LocalGameMode, PermissionLevel},
43    movement::MoveEventsSystems,
44    packet::game::SendGamePacketEvent,
45    respawn::perform_respawn,
46};
47
48/// A plugin that allows clients to interact with blocks in the world.
49pub struct InteractPlugin;
50impl Plugin for InteractPlugin {
51    fn build(&self, app: &mut App) {
52        app.add_message::<StartUseItemEvent>()
53            .add_systems(
54                Update,
55                (
56                    update_attributes_for_gamemode,
57                    handle_start_use_item_event,
58                    update_hit_result_component
59                        .after(clamp_look_direction)
60                        .after(update_last_bounding_box),
61                )
62                    .after(InventorySystems)
63                    .after(MoveEventsSystems)
64                    .after(perform_respawn)
65                    .after(handle_attack_event)
66                    .chain(),
67            )
68            .add_systems(
69                GameTick,
70                handle_start_use_item_queued.before(PhysicsSystems),
71            )
72            .add_observer(handle_entity_interact)
73            .add_observer(handle_swing_arm_trigger);
74    }
75}
76
77/// A component that contains information about our local block state
78/// predictions.
79#[derive(Clone, Component, Debug, Default)]
80pub struct BlockStatePredictionHandler {
81    /// The total number of changes that this client has made to blocks.
82    seq: u32,
83    server_state: HashMap<BlockPos, ServerVerifiedState>,
84}
85#[derive(Clone, Debug)]
86struct ServerVerifiedState {
87    seq: u32,
88    block_state: BlockState,
89    /// Used for teleporting the player back if we're colliding with the block
90    /// that got placed back.
91    #[allow(unused)]
92    player_pos: Vec3,
93}
94
95impl BlockStatePredictionHandler {
96    /// Get the next sequence number that we're going to use and increment the
97    /// value.
98    pub fn start_predicting(&mut self) -> u32 {
99        self.seq += 1;
100        self.seq
101    }
102
103    /// Should be called right before the client updates a block with its
104    /// prediction.
105    ///
106    /// This is used to make sure that we can rollback to this state if the
107    /// server acknowledges the sequence number (with
108    /// [`ClientboundBlockChangedAck`]) without having sent a block update.
109    ///
110    /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
111    pub fn retain_known_server_state(
112        &mut self,
113        pos: BlockPos,
114        old_state: BlockState,
115        player_pos: Vec3,
116    ) {
117        self.server_state
118            .entry(pos)
119            .and_modify(|s| s.seq = self.seq)
120            .or_insert(ServerVerifiedState {
121                seq: self.seq,
122                block_state: old_state,
123                player_pos,
124            });
125    }
126
127    /// Save this update as the correct server state so when the server sends a
128    /// [`ClientboundBlockChangedAck`] we don't roll back this new update.
129    ///
130    /// This should be used when we receive a block update from the server.
131    ///
132    /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck
133    pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool {
134        if let Some(s) = self.server_state.get_mut(&pos) {
135            s.block_state = state;
136            true
137        } else {
138            false
139        }
140    }
141
142    pub fn end_prediction_up_to(&mut self, seq: u32, world: &World) {
143        let mut to_remove = Vec::new();
144        for (pos, state) in &self.server_state {
145            if state.seq > seq {
146                continue;
147            }
148            to_remove.push(*pos);
149
150            // syncBlockState
151            let client_block_state = world.get_block_state(*pos).unwrap_or_default();
152            let server_block_state = state.block_state;
153            if client_block_state == server_block_state {
154                continue;
155            }
156            world.set_block_state(*pos, server_block_state);
157            // TODO: implement these two functions
158            // if is_colliding(player, *pos, server_block_state) {
159            //     abs_snap_to(state.player_pos);
160            // }
161        }
162
163        for pos in to_remove {
164            self.server_state.remove(&pos);
165        }
166    }
167}
168
169/// An event that makes one of our clients simulate a right-click.
170///
171/// This event just inserts the [`StartUseItemQueued`] component on the given
172/// entity.
173#[doc(alias("right click"))]
174#[derive(Message)]
175pub struct StartUseItemEvent {
176    pub entity: Entity,
177    pub hand: InteractionHand,
178    /// See [`StartUseItemQueued::force_block`].
179    pub force_block: Option<BlockPos>,
180}
181pub fn handle_start_use_item_event(
182    mut commands: Commands,
183    mut events: MessageReader<StartUseItemEvent>,
184) {
185    for event in events.read() {
186        commands.entity(event.entity).insert(StartUseItemQueued {
187            hand: event.hand,
188            force_block: event.force_block,
189        });
190    }
191}
192
193/// A component that makes our client simulate a right-click on the next
194/// [`GameTick`]. It's removed after that tick.
195///
196/// You may find it more convenient to use [`StartUseItemEvent`] instead, which
197/// just inserts this component for you.
198///
199/// [`GameTick`]: azalea_core::tick::GameTick
200#[derive(Component, Debug)]
201pub struct StartUseItemQueued {
202    pub hand: InteractionHand,
203    /// Optionally force us to send a [`ServerboundUseItemOn`] on the given
204    /// block.
205    ///
206    /// This is useful if you want to interact with a block without looking at
207    /// it, but should be avoided to stay compatible with anticheats.
208    pub force_block: Option<BlockPos>,
209}
210#[allow(clippy::type_complexity)]
211pub fn handle_start_use_item_queued(
212    mut commands: Commands,
213    query: Query<(
214        Entity,
215        &StartUseItemQueued,
216        &mut BlockStatePredictionHandler,
217        &HitResultComponent,
218        &LookDirection,
219        Option<&Mining>,
220    )>,
221) {
222    for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
223        query
224    {
225        commands.entity(entity).remove::<StartUseItemQueued>();
226
227        if mining.is_some() {
228            warn!("Got a StartUseItemEvent for a client that was mining");
229        }
230
231        // TODO: this also skips if LocalPlayer.handsBusy is true, which is used when
232        // rowing a boat
233
234        let mut hit_result = (**hit_result).clone();
235
236        if let Some(force_block) = start_use_item.force_block {
237            let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
238                block_hit_result.block_pos == force_block
239            } else {
240                false
241            };
242
243            if !hit_result_matches {
244                // we're not looking at the block, so make up some numbers
245                hit_result = HitResult::Block(BlockHitResult {
246                    location: force_block.center(),
247                    direction: Direction::Up,
248                    block_pos: force_block,
249                    inside: false,
250                    world_border: false,
251                    miss: false,
252                });
253            }
254        }
255
256        match &hit_result {
257            HitResult::Block(r) => {
258                let seq = prediction_handler.start_predicting();
259                if r.miss {
260                    commands.trigger(SendGamePacketEvent::new(
261                        entity,
262                        ServerboundUseItem {
263                            hand: start_use_item.hand,
264                            seq,
265                            x_rot: look_direction.x_rot(),
266                            y_rot: look_direction.y_rot(),
267                        },
268                    ));
269                } else {
270                    commands.trigger(SendGamePacketEvent::new(
271                        entity,
272                        ServerboundUseItemOn {
273                            hand: start_use_item.hand,
274                            block_hit: r.into(),
275                            seq,
276                        },
277                    ));
278                    // TODO: depending on the result of useItemOn, this might
279                    // also need to send a SwingArmEvent.
280                    // basically, this TODO is for simulating block
281                    // interactions/placements on the client-side.
282                }
283            }
284            HitResult::Entity(r) => {
285                commands.trigger(EntityInteractEvent {
286                    client: entity,
287                    target: r.entity,
288                    location: Some(r.location),
289                });
290            }
291        }
292    }
293}
294
295/// An ECS `Event` that makes the client tell the server that we right-clicked
296/// an entity.
297#[derive(Clone, Debug, EntityEvent)]
298pub struct EntityInteractEvent {
299    #[event_target]
300    pub client: Entity,
301    pub target: Entity,
302    /// The position on the entity that we'll tell the server that we clicked
303    /// on.
304    ///
305    /// This doesn't matter for most entities. If it's set to `None` but we're
306    /// looking at the target, it'll use the correct value. If it's `None` and
307    /// we're not looking at the entity, then it'll arbitrary send the target's
308    /// exact position.
309    pub location: Option<Vec3>,
310}
311
312pub fn handle_entity_interact(
313    trigger: On<EntityInteractEvent>,
314    mut commands: Commands,
315    client_query: Query<(&ClientMovementState, &EntityIdIndex, &HitResultComponent)>,
316    target_query: Query<&Position>,
317) {
318    let Some((physics_state, entity_id_index, hit_result)) = client_query.get(trigger.client).ok()
319    else {
320        warn!(
321            "tried to interact with an entity but the client didn't have the required components"
322        );
323        return;
324    };
325
326    // TODO: worldborder check
327
328    let Some(entity_id) = entity_id_index.get_by_ecs_entity(trigger.target) else {
329        warn!("tried to interact with an entity that isn't known by the client");
330        return;
331    };
332
333    let location = if let Some(l) = trigger.location {
334        l
335    } else {
336        // if we're looking at the entity, use that
337        if let Some(entity_hit_result) = hit_result.as_entity_hit_result()
338            && entity_hit_result.entity == trigger.target
339        {
340            entity_hit_result.location
341        } else {
342            // if we're not looking at the entity, make up a value that's good enough by
343            // using the entity's position
344            let Ok(target_position) = target_query.get(trigger.target) else {
345                warn!("tried to look at an entity without the entity having a position");
346                return;
347            };
348            **target_position
349        }
350    };
351
352    let interact = ServerboundInteract {
353        entity_id,
354        hand: InteractionHand::MainHand,
355        location: LpVec3::from(location),
356        using_secondary_action: physics_state.trying_to_crouch,
357    };
358    commands.trigger(SendGamePacketEvent::new(trigger.client, interact.clone()));
359
360    // TODO: this is true if the interaction failed, which i think can only happen
361    // in certain cases when interacting with armor stands
362    let consumes_action = false;
363    if !consumes_action {
364        // but yes, most of the time vanilla really does send two identical interact
365        // packets like this
366        commands.trigger(SendGamePacketEvent::new(trigger.client, interact));
367    }
368}
369
370/// Whether we can't interact with the block, based on your gamemode.
371///
372/// If this is false, then we can interact with the block.
373///
374/// The world, block position, and inventory are used for the adventure mode
375/// check.
376pub fn check_is_interaction_restricted(
377    world: &World,
378    block_pos: BlockPos,
379    game_mode: &GameMode,
380    inventory: &Inventory,
381) -> bool {
382    match game_mode {
383        GameMode::Adventure => {
384            // vanilla checks for abilities.mayBuild here but servers have no
385            // way of modifying that
386
387            let held_item = inventory.held_item();
388            match &held_item {
389                ItemStack::Present(item) => {
390                    let block = world.chunks.get_block_state(block_pos);
391                    let Some(block) = block else {
392                        // block isn't loaded so just say that it is restricted
393                        return true;
394                    };
395                    check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
396                }
397                _ => true,
398            }
399        }
400        GameMode::Spectator => true,
401        _ => false,
402    }
403}
404
405/// Check if the item has the `CanDestroy` tag for the block.
406pub fn check_block_can_be_broken_by_item_in_adventure_mode(
407    item: &ItemStackData,
408    _block: &BlockState,
409) -> bool {
410    // minecraft caches the last checked block but that's kind of an unnecessary
411    // optimization and makes the code too complicated
412
413    if item.get_component::<components::CanBreak>().is_none() {
414        // no CanDestroy tag
415        return false;
416    };
417
418    false
419
420    // for block_predicate in can_destroy {
421    //     // TODO
422    //     // defined in BlockPredicateArgument.java
423    // }
424
425    // true
426}
427
428pub fn can_use_game_master_blocks(
429    abilities: &PlayerAbilities,
430    permission_level: &PermissionLevel,
431) -> bool {
432    abilities.instant_break && **permission_level >= 2
433}
434
435/// Swing your arm.
436///
437/// This is purely a visual effect and won't interact with anything in the
438/// world.
439#[derive(Clone, Debug, EntityEvent)]
440pub struct SwingArmEvent {
441    pub entity: Entity,
442}
443pub fn handle_swing_arm_trigger(swing_arm: On<SwingArmEvent>, mut commands: Commands) {
444    commands.trigger(SendGamePacketEvent::new(
445        swing_arm.entity,
446        ServerboundSwing {
447            hand: InteractionHand::MainHand,
448        },
449    ));
450}
451
452#[allow(clippy::type_complexity)]
453fn update_attributes_for_gamemode(
454    query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
455) {
456    for (mut attributes, game_mode) in query {
457        if game_mode.current == GameMode::Creative {
458            attributes
459                .block_interaction_range
460                .insert(creative_block_interaction_range_modifier());
461            attributes
462                .entity_interaction_range
463                .insert(creative_entity_interaction_range_modifier());
464        } else {
465            attributes
466                .block_interaction_range
467                .remove(&creative_block_interaction_range_modifier().id);
468            attributes
469                .entity_interaction_range
470                .remove(&creative_entity_interaction_range_modifier().id);
471        }
472    }
473}