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