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};
21use azalea_inventory::{ItemStack, ItemStackData, components};
22use azalea_physics::{
23 PhysicsSystems, collision::entity_collisions::update_last_bounding_box,
24 local_player::PhysicsState,
25};
26use azalea_protocol::packets::game::{
27 ServerboundInteract, ServerboundUseItem,
28 s_interact::{self, InteractionHand},
29 s_swing::ServerboundSwing,
30 s_use_item_on::ServerboundUseItemOn,
31};
32use azalea_world::Instance;
33use bevy_app::{App, Plugin, Update};
34use bevy_ecs::prelude::*;
35use tracing::warn;
36
37use super::mining::Mining;
38use crate::{
39 Client,
40 attack::handle_attack_event,
41 interact::pick::{HitResultComponent, update_hit_result_component},
42 inventory::{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_message::<EntityInteractEvent>()
55 .add_systems(
56 Update,
57 (
58 (
59 update_attributes_for_held_item,
60 update_attributes_for_gamemode,
61 )
62 .in_set(UpdateAttributesSystems)
63 .chain(),
64 handle_start_use_item_event,
65 update_hit_result_component
66 .after(clamp_look_direction)
67 .after(update_last_bounding_box),
68 )
69 .after(InventorySystems)
70 .after(MoveEventsSystems)
71 .after(perform_respawn)
72 .after(handle_attack_event)
73 .chain(),
74 )
75 .add_systems(
76 GameTick,
77 (handle_start_use_item_queued, handle_entity_interact)
78 .chain()
79 .before(PhysicsSystems),
80 )
81 .add_observer(handle_swing_arm_trigger);
82 }
83}
84
85#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
86pub struct UpdateAttributesSystems;
87
88impl Client {
89 pub fn block_interact(&self, position: BlockPos) {
98 self.ecs.lock().write_message(StartUseItemEvent {
99 entity: self.entity,
100 hand: InteractionHand::MainHand,
101 force_block: Some(position),
102 });
103 }
104
105 pub fn entity_interact(&self, entity: Entity) {
111 self.ecs.lock().write_message(EntityInteractEvent {
112 client: self.entity,
113 target: entity,
114 location: None,
115 });
116 }
117
118 pub fn start_use_item(&self) {
126 self.ecs.lock().write_message(StartUseItemEvent {
127 entity: self.entity,
128 hand: InteractionHand::MainHand,
129 force_block: None,
130 });
131 }
132}
133
134#[derive(Component, Clone, Debug, Default)]
137pub struct BlockStatePredictionHandler {
138 seq: u32,
140 server_state: HashMap<BlockPos, ServerVerifiedState>,
141}
142#[derive(Clone, Debug)]
143struct ServerVerifiedState {
144 seq: u32,
145 block_state: BlockState,
146 #[allow(unused)]
149 player_pos: Vec3,
150}
151
152impl BlockStatePredictionHandler {
153 pub fn start_predicting(&mut self) -> u32 {
156 self.seq += 1;
157 self.seq
158 }
159
160 pub fn retain_known_server_state(
169 &mut self,
170 pos: BlockPos,
171 old_state: BlockState,
172 player_pos: Vec3,
173 ) {
174 self.server_state
175 .entry(pos)
176 .and_modify(|s| s.seq = self.seq)
177 .or_insert(ServerVerifiedState {
178 seq: self.seq,
179 block_state: old_state,
180 player_pos,
181 });
182 }
183
184 pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool {
191 if let Some(s) = self.server_state.get_mut(&pos) {
192 s.block_state = state;
193 true
194 } else {
195 false
196 }
197 }
198
199 pub fn end_prediction_up_to(&mut self, seq: u32, world: &Instance) {
200 let mut to_remove = Vec::new();
201 for (pos, state) in &self.server_state {
202 if state.seq > seq {
203 continue;
204 }
205 to_remove.push(*pos);
206
207 let client_block_state = world.get_block_state(*pos).unwrap_or_default();
209 let server_block_state = state.block_state;
210 if client_block_state == server_block_state {
211 continue;
212 }
213 world.set_block_state(*pos, server_block_state);
214 }
219
220 for pos in to_remove {
221 self.server_state.remove(&pos);
222 }
223 }
224}
225
226#[doc(alias("right click"))]
231#[derive(Message)]
232pub struct StartUseItemEvent {
233 pub entity: Entity,
234 pub hand: InteractionHand,
235 pub force_block: Option<BlockPos>,
237}
238pub fn handle_start_use_item_event(
239 mut commands: Commands,
240 mut events: MessageReader<StartUseItemEvent>,
241) {
242 for event in events.read() {
243 commands.entity(event.entity).insert(StartUseItemQueued {
244 hand: event.hand,
245 force_block: event.force_block,
246 });
247 }
248}
249
250#[derive(Component, Debug)]
258pub struct StartUseItemQueued {
259 pub hand: InteractionHand,
260 pub force_block: Option<BlockPos>,
266}
267#[allow(clippy::type_complexity)]
268pub fn handle_start_use_item_queued(
269 mut commands: Commands,
270 query: Query<(
271 Entity,
272 &StartUseItemQueued,
273 &mut BlockStatePredictionHandler,
274 &HitResultComponent,
275 &LookDirection,
276 Option<&Mining>,
277 )>,
278 mut entity_interact: MessageWriter<EntityInteractEvent>,
279) {
280 for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
281 query
282 {
283 commands.entity(entity).remove::<StartUseItemQueued>();
284
285 if mining.is_some() {
286 warn!("Got a StartUseItemEvent for a client that was mining");
287 }
288
289 let mut hit_result = (**hit_result).clone();
293
294 if let Some(force_block) = start_use_item.force_block {
295 let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
296 block_hit_result.block_pos == force_block
297 } else {
298 false
299 };
300
301 if !hit_result_matches {
302 hit_result = HitResult::Block(BlockHitResult {
304 location: force_block.center(),
305 direction: Direction::Up,
306 block_pos: force_block,
307 inside: false,
308 world_border: false,
309 miss: false,
310 });
311 }
312 }
313
314 match &hit_result {
315 HitResult::Block(r) => {
316 let seq = prediction_handler.start_predicting();
317 if r.miss {
318 commands.trigger(SendGamePacketEvent::new(
319 entity,
320 ServerboundUseItem {
321 hand: start_use_item.hand,
322 seq,
323 x_rot: look_direction.x_rot(),
324 y_rot: look_direction.y_rot(),
325 },
326 ));
327 } else {
328 commands.trigger(SendGamePacketEvent::new(
329 entity,
330 ServerboundUseItemOn {
331 hand: start_use_item.hand,
332 block_hit: r.into(),
333 seq,
334 },
335 ));
336 }
341 }
342 HitResult::Entity(r) => {
343 entity_interact.write(EntityInteractEvent {
344 client: entity,
345 target: r.entity,
346 location: Some(r.location),
347 });
348 }
349 }
350 }
351}
352
353#[derive(Message)]
356pub struct EntityInteractEvent {
357 pub client: Entity,
358 pub target: Entity,
359 pub location: Option<Vec3>,
367}
368
369pub fn handle_entity_interact(
370 mut events: MessageReader<EntityInteractEvent>,
371 mut commands: Commands,
372 client_query: Query<(&PhysicsState, &EntityIdIndex, &HitResultComponent)>,
373 target_query: Query<&Position>,
374) {
375 for event in events.read() {
376 let Some((physics_state, entity_id_index, hit_result)) =
377 client_query.get(event.target).ok()
378 else {
379 warn!(
380 "tried to interact with an entity but the client didn't have the required components"
381 );
382 continue;
383 };
384
385 let Some(entity_id) = entity_id_index.get_by_ecs_entity(event.target) else {
388 warn!("tried to interact with an entity that isn't known by the client");
389 continue;
390 };
391
392 let location = if let Some(l) = event.location {
393 l
394 } else {
395 if let Some(entity_hit_result) = hit_result.as_entity_hit_result()
397 && entity_hit_result.entity == event.target
398 {
399 entity_hit_result.location
400 } else {
401 let Ok(target_position) = target_query.get(event.target) else {
404 warn!("tried to look at an entity without the entity having a position");
405 continue;
406 };
407 **target_position
408 }
409 };
410
411 let mut interact = ServerboundInteract {
412 entity_id,
413 action: s_interact::ActionType::InteractAt {
414 location,
415 hand: InteractionHand::MainHand,
416 },
417 using_secondary_action: physics_state.trying_to_crouch,
418 };
419 commands.trigger(SendGamePacketEvent::new(event.client, interact.clone()));
420 let consumes_action = false;
423 if !consumes_action {
424 interact.action = s_interact::ActionType::Interact {
427 hand: InteractionHand::MainHand,
428 };
429 commands.trigger(SendGamePacketEvent::new(event.client, interact));
430 }
431 }
432}
433
434pub fn check_is_interaction_restricted(
441 instance: &Instance,
442 block_pos: BlockPos,
443 game_mode: &GameMode,
444 inventory: &Inventory,
445) -> bool {
446 match game_mode {
447 GameMode::Adventure => {
448 let held_item = inventory.held_item();
452 match &held_item {
453 ItemStack::Present(item) => {
454 let block = instance.chunks.get_block_state(block_pos);
455 let Some(block) = block else {
456 return true;
458 };
459 check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
460 }
461 _ => true,
462 }
463 }
464 GameMode::Spectator => true,
465 _ => false,
466 }
467}
468
469pub fn check_block_can_be_broken_by_item_in_adventure_mode(
471 item: &ItemStackData,
472 _block: &BlockState,
473) -> bool {
474 if item.get_component::<components::CanBreak>().is_none() {
478 return false;
480 };
481
482 false
483
484 }
491
492pub fn can_use_game_master_blocks(
493 abilities: &PlayerAbilities,
494 permission_level: &PermissionLevel,
495) -> bool {
496 abilities.instant_break && **permission_level >= 2
497}
498
499#[derive(EntityEvent, Clone, Debug)]
504pub struct SwingArmEvent {
505 pub entity: Entity,
506}
507pub fn handle_swing_arm_trigger(swing_arm: On<SwingArmEvent>, mut commands: Commands) {
508 commands.trigger(SendGamePacketEvent::new(
509 swing_arm.entity,
510 ServerboundSwing {
511 hand: InteractionHand::MainHand,
512 },
513 ));
514}
515
516#[allow(clippy::type_complexity)]
517fn update_attributes_for_held_item(
518 mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
519) {
520 for (mut attributes, inventory) in &mut query {
521 let held_item = inventory.held_item();
522
523 use azalea_registry::Item;
524 let added_attack_speed = match held_item.kind() {
525 Item::WoodenSword => -2.4,
526 Item::WoodenShovel => -3.0,
527 Item::WoodenPickaxe => -2.8,
528 Item::WoodenAxe => -3.2,
529 Item::WoodenHoe => -3.0,
530
531 Item::StoneSword => -2.4,
532 Item::StoneShovel => -3.0,
533 Item::StonePickaxe => -2.8,
534 Item::StoneAxe => -3.2,
535 Item::StoneHoe => -2.0,
536
537 Item::GoldenSword => -2.4,
538 Item::GoldenShovel => -3.0,
539 Item::GoldenPickaxe => -2.8,
540 Item::GoldenAxe => -3.0,
541 Item::GoldenHoe => -3.0,
542
543 Item::IronSword => -2.4,
544 Item::IronShovel => -3.0,
545 Item::IronPickaxe => -2.8,
546 Item::IronAxe => -3.1,
547 Item::IronHoe => -1.0,
548
549 Item::DiamondSword => -2.4,
550 Item::DiamondShovel => -3.0,
551 Item::DiamondPickaxe => -2.8,
552 Item::DiamondAxe => -3.0,
553 Item::DiamondHoe => 0.0,
554
555 Item::NetheriteSword => -2.4,
556 Item::NetheriteShovel => -3.0,
557 Item::NetheritePickaxe => -2.8,
558 Item::NetheriteAxe => -3.0,
559 Item::NetheriteHoe => 0.0,
560
561 Item::Trident => -2.9,
562 _ => 0.,
563 };
564 attributes
565 .attack_speed
566 .insert(azalea_entity::attributes::base_attack_speed_modifier(
567 added_attack_speed,
568 ));
569 }
570}
571
572#[allow(clippy::type_complexity)]
573fn update_attributes_for_gamemode(
574 query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
575) {
576 for (mut attributes, game_mode) in query {
577 if game_mode.current == GameMode::Creative {
578 attributes
579 .block_interaction_range
580 .insert(creative_block_interaction_range_modifier());
581 attributes
582 .entity_interaction_range
583 .insert(creative_entity_interaction_range_modifier());
584 } else {
585 attributes
586 .block_interaction_range
587 .remove(&creative_block_interaction_range_modifier().id);
588 attributes
589 .entity_interaction_range
590 .remove(&creative_entity_interaction_range_modifier().id);
591 }
592 }
593}