azalea_client/plugins/interact/
mod.rs1pub 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
49pub 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#[derive(Clone, Component, Debug, Default)]
81pub struct BlockStatePredictionHandler {
82 seq: u32,
84 server_state: HashMap<BlockPos, ServerVerifiedState>,
85}
86#[derive(Clone, Debug)]
87struct ServerVerifiedState {
88 seq: u32,
89 block_state: BlockState,
90 #[allow(unused)]
93 player_pos: Vec3,
94}
95
96impl BlockStatePredictionHandler {
97 pub fn start_predicting(&mut self) -> u32 {
100 self.seq += 1;
101 self.seq
102 }
103
104 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 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 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 }
163
164 for pos in to_remove {
165 self.server_state.remove(&pos);
166 }
167 }
168}
169
170#[doc(alias("right click"))]
175#[derive(Message)]
176pub struct StartUseItemEvent {
177 pub entity: Entity,
178 pub hand: InteractionHand,
179 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#[derive(Component, Debug)]
202pub struct StartUseItemQueued {
203 pub hand: InteractionHand,
204 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 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 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 }
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#[derive(Clone, Debug, EntityEvent)]
299pub struct EntityInteractEvent {
300 #[event_target]
301 pub client: Entity,
302 pub target: Entity,
303 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 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 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 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 let consumes_action = false;
366 if !consumes_action {
367 interact.action = s_interact::ActionType::Interact {
370 hand: InteractionHand::MainHand,
371 };
372 commands.trigger(SendGamePacketEvent::new(trigger.client, interact));
373 }
374}
375
376pub 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 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 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
411pub fn check_block_can_be_broken_by_item_in_adventure_mode(
413 item: &ItemStackData,
414 _block: &BlockState,
415) -> bool {
416 if item.get_component::<components::CanBreak>().is_none() {
420 return false;
422 };
423
424 false
425
426 }
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#[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}