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,
15 attributes::{
16 creative_block_interaction_range_modifier, creative_entity_interaction_range_modifier,
17 },
18 clamp_look_direction,
19};
20use azalea_inventory::{ItemStack, ItemStackData, components};
21use azalea_physics::{
22 PhysicsSet, collision::entity_collisions::update_last_bounding_box, local_player::PhysicsState,
23};
24use azalea_protocol::packets::game::{
25 ServerboundInteract, ServerboundUseItem,
26 s_interact::{self, InteractionHand},
27 s_swing::ServerboundSwing,
28 s_use_item_on::ServerboundUseItemOn,
29};
30use azalea_world::{Instance, MinecraftEntityId};
31use bevy_app::{App, Plugin, Update};
32use bevy_ecs::prelude::*;
33use tracing::warn;
34
35use super::mining::Mining;
36use crate::{
37 Client,
38 attack::handle_attack_event,
39 interact::pick::{HitResultComponent, update_hit_result_component},
40 inventory::{Inventory, InventorySet},
41 local_player::{LocalGameMode, PermissionLevel},
42 movement::MoveEventsSet,
43 packet::game::SendPacketEvent,
44 respawn::perform_respawn,
45};
46
47pub struct InteractPlugin;
49impl Plugin for InteractPlugin {
50 fn build(&self, app: &mut App) {
51 app.add_event::<StartUseItemEvent>()
52 .add_event::<SwingArmEvent>()
53 .add_systems(
54 Update,
55 (
56 (
57 update_attributes_for_held_item,
58 update_attributes_for_gamemode,
59 )
60 .in_set(UpdateAttributesSet)
61 .chain(),
62 handle_start_use_item_event,
63 update_hit_result_component
64 .after(clamp_look_direction)
65 .after(update_last_bounding_box),
66 handle_swing_arm_event,
67 )
68 .after(InventorySet)
69 .after(MoveEventsSet)
70 .after(perform_respawn)
71 .after(handle_attack_event)
72 .chain(),
73 )
74 .add_systems(GameTick, handle_start_use_item_queued.before(PhysicsSet))
75 .add_observer(handle_swing_arm_trigger);
76 }
77}
78
79#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
80pub struct UpdateAttributesSet;
81
82impl Client {
83 pub fn block_interact(&self, position: BlockPos) {
92 self.ecs.lock().send_event(StartUseItemEvent {
93 entity: self.entity,
94 hand: InteractionHand::MainHand,
95 force_block: Some(position),
96 });
97 }
98
99 pub fn start_use_item(&self) {
107 self.ecs.lock().send_event(StartUseItemEvent {
108 entity: self.entity,
109 hand: InteractionHand::MainHand,
110 force_block: None,
111 });
112 }
113}
114
115#[derive(Component, Clone, Debug, Default)]
118pub struct BlockStatePredictionHandler {
119 seq: u32,
121 server_state: HashMap<BlockPos, ServerVerifiedState>,
122}
123#[derive(Clone, Debug)]
124struct ServerVerifiedState {
125 seq: u32,
126 block_state: BlockState,
127 #[allow(unused)]
130 player_pos: Vec3,
131}
132
133impl BlockStatePredictionHandler {
134 pub fn start_predicting(&mut self) -> u32 {
137 self.seq += 1;
138 self.seq
139 }
140
141 pub fn retain_known_server_state(
150 &mut self,
151 pos: BlockPos,
152 old_state: BlockState,
153 player_pos: Vec3,
154 ) {
155 self.server_state
156 .entry(pos)
157 .and_modify(|s| s.seq = self.seq)
158 .or_insert(ServerVerifiedState {
159 seq: self.seq,
160 block_state: old_state,
161 player_pos,
162 });
163 }
164
165 pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool {
172 if let Some(s) = self.server_state.get_mut(&pos) {
173 s.block_state = state;
174 true
175 } else {
176 false
177 }
178 }
179
180 pub fn end_prediction_up_to(&mut self, seq: u32, world: &Instance) {
181 let mut to_remove = Vec::new();
182 for (pos, state) in &self.server_state {
183 if state.seq > seq {
184 continue;
185 }
186 to_remove.push(*pos);
187
188 let client_block_state = world.get_block_state(*pos).unwrap_or_default();
190 let server_block_state = state.block_state;
191 if client_block_state == server_block_state {
192 continue;
193 }
194 world.set_block_state(*pos, server_block_state);
195 }
200
201 for pos in to_remove {
202 self.server_state.remove(&pos);
203 }
204 }
205}
206
207#[doc(alias("right click"))]
212#[derive(Event)]
213pub struct StartUseItemEvent {
214 pub entity: Entity,
215 pub hand: InteractionHand,
216 pub force_block: Option<BlockPos>,
218}
219pub fn handle_start_use_item_event(
220 mut commands: Commands,
221 mut events: EventReader<StartUseItemEvent>,
222) {
223 for event in events.read() {
224 commands.entity(event.entity).insert(StartUseItemQueued {
225 hand: event.hand,
226 force_block: event.force_block,
227 });
228 }
229}
230
231#[derive(Component, Debug)]
239pub struct StartUseItemQueued {
240 pub hand: InteractionHand,
241 pub force_block: Option<BlockPos>,
247}
248#[allow(clippy::type_complexity)]
249pub fn handle_start_use_item_queued(
250 mut commands: Commands,
251 query: Query<(
252 Entity,
253 &StartUseItemQueued,
254 &mut BlockStatePredictionHandler,
255 &HitResultComponent,
256 &LookDirection,
257 &PhysicsState,
258 Option<&Mining>,
259 )>,
260 entity_id_query: Query<&MinecraftEntityId>,
261) {
262 for (
263 entity,
264 start_use_item,
265 mut prediction_handler,
266 hit_result,
267 look_direction,
268 physics_state,
269 mining,
270 ) in query
271 {
272 commands.entity(entity).remove::<StartUseItemQueued>();
273
274 if mining.is_some() {
275 warn!("Got a StartUseItemEvent for a client that was mining");
276 }
277
278 let mut hit_result = (**hit_result).clone();
282
283 if let Some(force_block) = start_use_item.force_block {
284 let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
285 block_hit_result.block_pos == force_block
286 } else {
287 false
288 };
289
290 if !hit_result_matches {
291 hit_result = HitResult::Block(BlockHitResult {
293 location: force_block.center(),
294 direction: Direction::Up,
295 block_pos: force_block,
296 inside: false,
297 world_border: false,
298 miss: false,
299 });
300 }
301 }
302
303 match &hit_result {
304 HitResult::Block(r) => {
305 let seq = prediction_handler.start_predicting();
306 if r.miss {
307 commands.trigger(SendPacketEvent::new(
308 entity,
309 ServerboundUseItem {
310 hand: start_use_item.hand,
311 seq,
312 x_rot: look_direction.x_rot(),
313 y_rot: look_direction.y_rot(),
314 },
315 ));
316 } else {
317 commands.trigger(SendPacketEvent::new(
318 entity,
319 ServerboundUseItemOn {
320 hand: start_use_item.hand,
321 block_hit: r.into(),
322 seq,
323 },
324 ));
325 }
330 }
331 HitResult::Entity(r) => {
332 let Ok(entity_id) = entity_id_query.get(r.entity).copied() else {
335 warn!("tried to interact with an entity that doesn't have MinecraftEntityId");
336 continue;
337 };
338
339 let mut interact = ServerboundInteract {
340 entity_id,
341 action: s_interact::ActionType::InteractAt {
342 location: r.location,
343 hand: InteractionHand::MainHand,
344 },
345 using_secondary_action: physics_state.trying_to_crouch,
346 };
347 commands.trigger(SendPacketEvent::new(entity, interact.clone()));
348 let consumes_action = false;
351 if !consumes_action {
352 interact.action = s_interact::ActionType::Interact {
355 hand: InteractionHand::MainHand,
356 };
357 commands.trigger(SendPacketEvent::new(entity, interact));
358 }
359 }
360 }
361 }
362}
363
364pub fn check_is_interaction_restricted(
370 instance: &Instance,
371 block_pos: BlockPos,
372 game_mode: &GameMode,
373 inventory: &Inventory,
374) -> bool {
375 match game_mode {
376 GameMode::Adventure => {
377 let held_item = inventory.held_item();
381 match &held_item {
382 ItemStack::Present(item) => {
383 let block = instance.chunks.get_block_state(block_pos);
384 let Some(block) = block else {
385 return true;
387 };
388 check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
389 }
390 _ => true,
391 }
392 }
393 GameMode::Spectator => true,
394 _ => false,
395 }
396}
397
398pub fn check_block_can_be_broken_by_item_in_adventure_mode(
400 item: &ItemStackData,
401 _block: &BlockState,
402) -> bool {
403 if item.get_component::<components::CanBreak>().is_none() {
407 return false;
409 };
410
411 false
412
413 }
420
421pub fn can_use_game_master_blocks(
422 abilities: &PlayerAbilities,
423 permission_level: &PermissionLevel,
424) -> bool {
425 abilities.instant_break && **permission_level >= 2
426}
427
428#[derive(Event, Clone, Debug)]
431pub struct SwingArmEvent {
432 pub entity: Entity,
433}
434pub fn handle_swing_arm_trigger(trigger: Trigger<SwingArmEvent>, mut commands: Commands) {
435 commands.trigger(SendPacketEvent::new(
436 trigger.event().entity,
437 ServerboundSwing {
438 hand: InteractionHand::MainHand,
439 },
440 ));
441}
442pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) {
443 for event in events.read() {
444 commands.trigger(event.clone());
445 }
446}
447
448#[allow(clippy::type_complexity)]
449fn update_attributes_for_held_item(
450 mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
451) {
452 for (mut attributes, inventory) in &mut query {
453 let held_item = inventory.held_item();
454
455 use azalea_registry::Item;
456 let added_attack_speed = match held_item.kind() {
457 Item::WoodenSword => -2.4,
458 Item::WoodenShovel => -3.0,
459 Item::WoodenPickaxe => -2.8,
460 Item::WoodenAxe => -3.2,
461 Item::WoodenHoe => -3.0,
462
463 Item::StoneSword => -2.4,
464 Item::StoneShovel => -3.0,
465 Item::StonePickaxe => -2.8,
466 Item::StoneAxe => -3.2,
467 Item::StoneHoe => -2.0,
468
469 Item::GoldenSword => -2.4,
470 Item::GoldenShovel => -3.0,
471 Item::GoldenPickaxe => -2.8,
472 Item::GoldenAxe => -3.0,
473 Item::GoldenHoe => -3.0,
474
475 Item::IronSword => -2.4,
476 Item::IronShovel => -3.0,
477 Item::IronPickaxe => -2.8,
478 Item::IronAxe => -3.1,
479 Item::IronHoe => -1.0,
480
481 Item::DiamondSword => -2.4,
482 Item::DiamondShovel => -3.0,
483 Item::DiamondPickaxe => -2.8,
484 Item::DiamondAxe => -3.0,
485 Item::DiamondHoe => 0.0,
486
487 Item::NetheriteSword => -2.4,
488 Item::NetheriteShovel => -3.0,
489 Item::NetheritePickaxe => -2.8,
490 Item::NetheriteAxe => -3.0,
491 Item::NetheriteHoe => 0.0,
492
493 Item::Trident => -2.9,
494 _ => 0.,
495 };
496 attributes
497 .attack_speed
498 .insert(azalea_entity::attributes::base_attack_speed_modifier(
499 added_attack_speed,
500 ));
501 }
502}
503
504#[allow(clippy::type_complexity)]
505fn update_attributes_for_gamemode(
506 query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
507) {
508 for (mut attributes, game_mode) in query {
509 if game_mode.current == GameMode::Creative {
510 attributes
511 .block_interaction_range
512 .insert(creative_block_interaction_range_modifier());
513 attributes
514 .entity_interaction_range
515 .insert(creative_entity_interaction_range_modifier());
516 } else {
517 attributes
518 .block_interaction_range
519 .remove(&creative_block_interaction_range_modifier().id);
520 attributes
521 .entity_interaction_range
522 .remove(&creative_entity_interaction_range_modifier().id);
523 }
524 }
525}