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_registry::builtin::ItemKind;
34use azalea_world::Instance;
35use bevy_app::{App, Plugin, Update};
36use bevy_ecs::prelude::*;
37use tracing::warn;
38
39use super::mining::Mining;
40use crate::{
41 Client,
42 attack::handle_attack_event,
43 interact::pick::{HitResultComponent, update_hit_result_component},
44 inventory::InventorySystems,
45 local_player::{LocalGameMode, PermissionLevel},
46 movement::MoveEventsSystems,
47 packet::game::SendGamePacketEvent,
48 respawn::perform_respawn,
49};
50
51pub struct InteractPlugin;
53impl Plugin for InteractPlugin {
54 fn build(&self, app: &mut App) {
55 app.add_message::<StartUseItemEvent>()
56 .add_systems(
57 Update,
58 (
59 (
60 update_attributes_for_held_item,
61 update_attributes_for_gamemode,
62 )
63 .in_set(UpdateAttributesSystems)
64 .chain(),
65 handle_start_use_item_event,
66 update_hit_result_component
67 .after(clamp_look_direction)
68 .after(update_last_bounding_box),
69 )
70 .after(InventorySystems)
71 .after(MoveEventsSystems)
72 .after(perform_respawn)
73 .after(handle_attack_event)
74 .chain(),
75 )
76 .add_systems(
77 GameTick,
78 handle_start_use_item_queued.before(PhysicsSystems),
79 )
80 .add_observer(handle_entity_interact)
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().trigger(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) {
279 for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
280 query
281 {
282 commands.entity(entity).remove::<StartUseItemQueued>();
283
284 if mining.is_some() {
285 warn!("Got a StartUseItemEvent for a client that was mining");
286 }
287
288 let mut hit_result = (**hit_result).clone();
292
293 if let Some(force_block) = start_use_item.force_block {
294 let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
295 block_hit_result.block_pos == force_block
296 } else {
297 false
298 };
299
300 if !hit_result_matches {
301 hit_result = HitResult::Block(BlockHitResult {
303 location: force_block.center(),
304 direction: Direction::Up,
305 block_pos: force_block,
306 inside: false,
307 world_border: false,
308 miss: false,
309 });
310 }
311 }
312
313 match &hit_result {
314 HitResult::Block(r) => {
315 let seq = prediction_handler.start_predicting();
316 if r.miss {
317 commands.trigger(SendGamePacketEvent::new(
318 entity,
319 ServerboundUseItem {
320 hand: start_use_item.hand,
321 seq,
322 x_rot: look_direction.x_rot(),
323 y_rot: look_direction.y_rot(),
324 },
325 ));
326 } else {
327 commands.trigger(SendGamePacketEvent::new(
328 entity,
329 ServerboundUseItemOn {
330 hand: start_use_item.hand,
331 block_hit: r.into(),
332 seq,
333 },
334 ));
335 }
340 }
341 HitResult::Entity(r) => {
342 commands.trigger(EntityInteractEvent {
343 client: entity,
344 target: r.entity,
345 location: Some(r.location),
346 });
347 }
348 }
349 }
350}
351
352#[derive(EntityEvent, Clone, Debug)]
355pub struct EntityInteractEvent {
356 #[event_target]
357 pub client: Entity,
358 pub target: Entity,
359 pub location: Option<Vec3>,
367}
368
369pub fn handle_entity_interact(
370 trigger: On<EntityInteractEvent>,
371 mut commands: Commands,
372 client_query: Query<(&PhysicsState, &EntityIdIndex, &HitResultComponent)>,
373 target_query: Query<&Position>,
374) {
375 let Some((physics_state, entity_id_index, hit_result)) = client_query.get(trigger.client).ok()
376 else {
377 warn!(
378 "tried to interact with an entity but the client didn't have the required components"
379 );
380 return;
381 };
382
383 let Some(entity_id) = entity_id_index.get_by_ecs_entity(trigger.target) else {
386 warn!("tried to interact with an entity that isn't known by the client");
387 return;
388 };
389
390 let location = if let Some(l) = trigger.location {
391 l
392 } else {
393 if let Some(entity_hit_result) = hit_result.as_entity_hit_result()
395 && entity_hit_result.entity == trigger.target
396 {
397 entity_hit_result.location
398 } else {
399 let Ok(target_position) = target_query.get(trigger.target) else {
402 warn!("tried to look at an entity without the entity having a position");
403 return;
404 };
405 **target_position
406 }
407 };
408
409 let mut interact = ServerboundInteract {
410 entity_id,
411 action: s_interact::ActionType::InteractAt {
412 location,
413 hand: InteractionHand::MainHand,
414 },
415 using_secondary_action: physics_state.trying_to_crouch,
416 };
417 commands.trigger(SendGamePacketEvent::new(trigger.client, interact.clone()));
418
419 let consumes_action = false;
422 if !consumes_action {
423 interact.action = s_interact::ActionType::Interact {
426 hand: InteractionHand::MainHand,
427 };
428 commands.trigger(SendGamePacketEvent::new(trigger.client, interact));
429 }
430}
431
432pub fn check_is_interaction_restricted(
439 instance: &Instance,
440 block_pos: BlockPos,
441 game_mode: &GameMode,
442 inventory: &Inventory,
443) -> bool {
444 match game_mode {
445 GameMode::Adventure => {
446 let held_item = inventory.held_item();
450 match &held_item {
451 ItemStack::Present(item) => {
452 let block = instance.chunks.get_block_state(block_pos);
453 let Some(block) = block else {
454 return true;
456 };
457 check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
458 }
459 _ => true,
460 }
461 }
462 GameMode::Spectator => true,
463 _ => false,
464 }
465}
466
467pub fn check_block_can_be_broken_by_item_in_adventure_mode(
469 item: &ItemStackData,
470 _block: &BlockState,
471) -> bool {
472 if item.get_component::<components::CanBreak>().is_none() {
476 return false;
478 };
479
480 false
481
482 }
489
490pub fn can_use_game_master_blocks(
491 abilities: &PlayerAbilities,
492 permission_level: &PermissionLevel,
493) -> bool {
494 abilities.instant_break && **permission_level >= 2
495}
496
497#[derive(EntityEvent, Clone, Debug)]
502pub struct SwingArmEvent {
503 pub entity: Entity,
504}
505pub fn handle_swing_arm_trigger(swing_arm: On<SwingArmEvent>, mut commands: Commands) {
506 commands.trigger(SendGamePacketEvent::new(
507 swing_arm.entity,
508 ServerboundSwing {
509 hand: InteractionHand::MainHand,
510 },
511 ));
512}
513
514#[allow(clippy::type_complexity)]
515fn update_attributes_for_held_item(
516 mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
517) {
518 for (mut attributes, inventory) in &mut query {
519 let held_item = inventory.held_item();
520
521 let added_attack_speed = added_attack_speed_for_item(held_item.kind());
522 attributes
523 .attack_speed
524 .insert(azalea_entity::attributes::base_attack_speed_modifier(
525 added_attack_speed,
526 ));
527 }
528}
529
530fn added_attack_speed_for_item(item: ItemKind) -> f64 {
531 match item {
532 ItemKind::WoodenSword => -2.4,
533 ItemKind::WoodenShovel => -3.0,
534 ItemKind::WoodenPickaxe => -2.8,
535 ItemKind::WoodenAxe => -3.2,
536 ItemKind::WoodenHoe => -3.0,
537
538 ItemKind::StoneSword => -2.4,
539 ItemKind::StoneShovel => -3.0,
540 ItemKind::StonePickaxe => -2.8,
541 ItemKind::StoneAxe => -3.2,
542 ItemKind::StoneHoe => -2.0,
543
544 ItemKind::GoldenSword => -2.4,
545 ItemKind::GoldenShovel => -3.0,
546 ItemKind::GoldenPickaxe => -2.8,
547 ItemKind::GoldenAxe => -3.0,
548 ItemKind::GoldenHoe => -3.0,
549
550 ItemKind::IronSword => -2.4,
551 ItemKind::IronShovel => -3.0,
552 ItemKind::IronPickaxe => -2.8,
553 ItemKind::IronAxe => -3.1,
554 ItemKind::IronHoe => -1.0,
555
556 ItemKind::DiamondSword => -2.4,
557 ItemKind::DiamondShovel => -3.0,
558 ItemKind::DiamondPickaxe => -2.8,
559 ItemKind::DiamondAxe => -3.0,
560 ItemKind::DiamondHoe => 0.0,
561
562 ItemKind::NetheriteSword => -2.4,
563 ItemKind::NetheriteShovel => -3.0,
564 ItemKind::NetheritePickaxe => -2.8,
565 ItemKind::NetheriteAxe => -3.0,
566 ItemKind::NetheriteHoe => 0.0,
567
568 ItemKind::Trident => -2.9,
569 _ => 0.,
570 }
571}
572
573#[allow(clippy::type_complexity)]
574fn update_attributes_for_gamemode(
575 query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
576) {
577 for (mut attributes, game_mode) in query {
578 if game_mode.current == GameMode::Creative {
579 attributes
580 .block_interaction_range
581 .insert(creative_block_interaction_range_modifier());
582 attributes
583 .entity_interaction_range
584 .insert(creative_entity_interaction_range_modifier());
585 } else {
586 attributes
587 .block_interaction_range
588 .remove(&creative_block_interaction_range_modifier().id);
589 attributes
590 .entity_interaction_range
591 .remove(&creative_entity_interaction_range_modifier().id);
592 }
593 }
594}