azalea_client/plugins/interact/
mod.rs

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