azalea_client/plugins/interact/
mod.rs1pub 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
48pub 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#[derive(Clone, Component, Debug, Default)]
80pub struct BlockStatePredictionHandler {
81 seq: u32,
83 server_state: HashMap<BlockPos, ServerVerifiedState>,
84}
85#[derive(Clone, Debug)]
86struct ServerVerifiedState {
87 seq: u32,
88 block_state: BlockState,
89 #[allow(unused)]
92 player_pos: Vec3,
93}
94
95impl BlockStatePredictionHandler {
96 pub fn start_predicting(&mut self) -> u32 {
99 self.seq += 1;
100 self.seq
101 }
102
103 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 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 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 }
162
163 for pos in to_remove {
164 self.server_state.remove(&pos);
165 }
166 }
167}
168
169#[doc(alias("right click"))]
174#[derive(Message)]
175pub struct StartUseItemEvent {
176 pub entity: Entity,
177 pub hand: InteractionHand,
178 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#[derive(Component, Debug)]
201pub struct StartUseItemQueued {
202 pub hand: InteractionHand,
203 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 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 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 }
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#[derive(Clone, Debug, EntityEvent)]
298pub struct EntityInteractEvent {
299 #[event_target]
300 pub client: Entity,
301 pub target: Entity,
302 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 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 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 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 let consumes_action = false;
363 if !consumes_action {
364 commands.trigger(SendGamePacketEvent::new(trigger.client, interact));
367 }
368}
369
370pub 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 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 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
405pub fn check_block_can_be_broken_by_item_in_adventure_mode(
407 item: &ItemStackData,
408 _block: &BlockState,
409) -> bool {
410 if item.get_component::<components::CanBreak>().is_none() {
414 return false;
416 };
417
418 false
419
420 }
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#[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}