1use azalea_block::BlockState;
2use azalea_core::{
3 direction::Direction,
4 game_type::GameMode,
5 hit_result::{BlockHitResult, HitResult},
6 position::{BlockPos, Vec3},
7 tick::GameTick,
8};
9use azalea_entity::{
10 Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector,
11};
12use azalea_inventory::{ItemStack, ItemStackData, components};
13use azalea_physics::{
14 PhysicsSet,
15 clip::{BlockShapeType, ClipContext, FluidPickType},
16};
17use azalea_protocol::packets::game::{
18 ServerboundUseItem, s_interact::InteractionHand, s_swing::ServerboundSwing,
19 s_use_item_on::ServerboundUseItemOn,
20};
21use azalea_world::{Instance, InstanceContainer, InstanceName};
22use bevy_app::{App, Plugin, Update};
23use bevy_ecs::prelude::*;
24use derive_more::{Deref, DerefMut};
25use tracing::warn;
26
27use super::mining::{Mining, MiningSet};
28use crate::{
29 Client,
30 attack::handle_attack_event,
31 inventory::{Inventory, InventorySet},
32 local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
33 movement::MoveEventsSet,
34 packet::game::SendPacketEvent,
35 respawn::perform_respawn,
36};
37
38pub struct InteractPlugin;
40impl Plugin for InteractPlugin {
41 fn build(&self, app: &mut App) {
42 app.add_event::<StartUseItemEvent>()
43 .add_event::<SwingArmEvent>()
44 .add_systems(
45 Update,
46 (
47 (
48 handle_start_use_item_event,
49 update_hit_result_component.after(clamp_look_direction),
50 handle_swing_arm_event,
51 )
52 .after(InventorySet)
53 .after(perform_respawn)
54 .after(handle_attack_event)
55 .chain(),
56 update_modifiers_for_held_item
57 .after(InventorySet)
58 .after(MoveEventsSet),
59 ),
60 )
61 .add_systems(
62 GameTick,
63 handle_start_use_item_queued
64 .after(MiningSet)
65 .before(PhysicsSet),
66 )
67 .add_observer(handle_swing_arm_trigger);
68 }
69}
70
71impl Client {
72 pub fn block_interact(&self, position: BlockPos) {
81 self.ecs.lock().send_event(StartUseItemEvent {
82 entity: self.entity,
83 hand: InteractionHand::MainHand,
84 force_block: Some(position),
85 });
86 }
87
88 pub fn start_use_item(&self) {
96 self.ecs.lock().send_event(StartUseItemEvent {
97 entity: self.entity,
98 hand: InteractionHand::MainHand,
99 force_block: None,
100 });
101 }
102}
103
104#[derive(Component, Copy, Clone, Debug, Default, Deref)]
107pub struct CurrentSequenceNumber(u32);
108
109impl CurrentSequenceNumber {
110 pub fn get_and_increment(&mut self) -> u32 {
113 let cur = self.0;
114 self.0 += 1;
115 cur
116 }
117}
118
119#[doc(alias("looking at", "looking at block", "crosshair"))]
122#[derive(Component, Clone, Debug, Deref, DerefMut)]
123pub struct HitResultComponent(HitResult);
124
125#[doc(alias("right click"))]
130#[derive(Event)]
131pub struct StartUseItemEvent {
132 pub entity: Entity,
133 pub hand: InteractionHand,
134 pub force_block: Option<BlockPos>,
136}
137pub fn handle_start_use_item_event(
138 mut commands: Commands,
139 mut events: EventReader<StartUseItemEvent>,
140) {
141 for event in events.read() {
142 commands.entity(event.entity).insert(StartUseItemQueued {
143 hand: event.hand,
144 force_block: event.force_block,
145 });
146 }
147}
148
149#[derive(Component)]
157pub struct StartUseItemQueued {
158 pub hand: InteractionHand,
159 pub force_block: Option<BlockPos>,
165}
166#[allow(clippy::type_complexity)]
167pub fn handle_start_use_item_queued(
168 mut commands: Commands,
169 query: Query<(
170 Entity,
171 &StartUseItemQueued,
172 &mut CurrentSequenceNumber,
173 &HitResultComponent,
174 &LookDirection,
175 Option<&Mining>,
176 )>,
177) {
178 for (entity, start_use_item, mut sequence_number, hit_result, look_direction, mining) in query {
179 commands.entity(entity).remove::<StartUseItemQueued>();
180
181 if mining.is_some() {
182 warn!("Got a StartUseItemEvent for a client that was mining");
183 }
184
185 let mut hit_result = hit_result.0.clone();
189
190 if let Some(force_block) = start_use_item.force_block {
191 let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
192 block_hit_result.block_pos == force_block
193 } else {
194 false
195 };
196
197 if !hit_result_matches {
198 hit_result = HitResult::Block(BlockHitResult {
200 location: force_block.center(),
201 direction: Direction::Up,
202 block_pos: force_block,
203 inside: false,
204 world_border: false,
205 miss: false,
206 });
207 }
208 }
209
210 match &hit_result {
211 HitResult::Block(block_hit_result) => {
212 if block_hit_result.miss {
213 commands.trigger(SendPacketEvent::new(
214 entity,
215 ServerboundUseItem {
216 hand: start_use_item.hand,
217 sequence: sequence_number.get_and_increment(),
218 x_rot: look_direction.x_rot,
219 y_rot: look_direction.y_rot,
220 },
221 ));
222 } else {
223 commands.trigger(SendPacketEvent::new(
224 entity,
225 ServerboundUseItemOn {
226 hand: start_use_item.hand,
227 block_hit: block_hit_result.into(),
228 sequence: sequence_number.get_and_increment(),
229 },
230 ));
231 }
237 }
238 HitResult::Entity => {
239 }
252 }
253 }
254}
255
256#[allow(clippy::type_complexity)]
257pub fn update_hit_result_component(
258 mut commands: Commands,
259 mut query: Query<(
260 Entity,
261 Option<&mut HitResultComponent>,
262 &LocalGameMode,
263 &Position,
264 &EyeHeight,
265 &LookDirection,
266 &InstanceName,
267 )>,
268 instance_container: Res<InstanceContainer>,
269) {
270 for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in
271 &mut query
272 {
273 let pick_range = if game_mode.current == GameMode::Creative {
274 6.
275 } else {
276 4.5
277 };
278 let eye_position = Vec3 {
279 x: position.x,
280 y: position.y + **eye_height as f64,
281 z: position.z,
282 };
283
284 let Some(instance_lock) = instance_container.get(world_name) else {
285 continue;
286 };
287 let instance = instance_lock.read();
288
289 let hit_result = pick(look_direction, &eye_position, &instance.chunks, pick_range);
290 if let Some(mut hit_result_ref) = hit_result_ref {
291 **hit_result_ref = hit_result;
292 } else {
293 commands
294 .entity(entity)
295 .insert(HitResultComponent(hit_result));
296 }
297 }
298}
299
300pub fn pick(
310 look_direction: &LookDirection,
311 eye_position: &Vec3,
312 chunks: &azalea_world::ChunkStorage,
313 pick_range: f64,
314) -> HitResult {
315 HitResult::Block(pick_block(look_direction, eye_position, chunks, pick_range))
319}
320
321pub fn pick_block(
326 look_direction: &LookDirection,
327 eye_position: &Vec3,
328 chunks: &azalea_world::ChunkStorage,
329 pick_range: f64,
330) -> BlockHitResult {
331 let view_vector = view_vector(look_direction);
332 let end_position = eye_position + &(view_vector * pick_range);
333
334 azalea_physics::clip::clip(
335 chunks,
336 ClipContext {
337 from: *eye_position,
338 to: end_position,
339 block_shape_type: BlockShapeType::Outline,
340 fluid_pick_type: FluidPickType::None,
341 },
342 )
343}
344
345pub fn check_is_interaction_restricted(
351 instance: &Instance,
352 block_pos: &BlockPos,
353 game_mode: &GameMode,
354 inventory: &Inventory,
355) -> bool {
356 match game_mode {
357 GameMode::Adventure => {
358 let held_item = inventory.held_item();
362 match &held_item {
363 ItemStack::Present(item) => {
364 let block = instance.chunks.get_block_state(block_pos);
365 let Some(block) = block else {
366 return true;
368 };
369 check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
370 }
371 _ => true,
372 }
373 }
374 GameMode::Spectator => true,
375 _ => false,
376 }
377}
378
379pub fn check_block_can_be_broken_by_item_in_adventure_mode(
381 item: &ItemStackData,
382 _block: &BlockState,
383) -> bool {
384 if !item.components.has::<components::CanBreak>() {
388 return false;
390 };
391
392 false
393
394 }
401
402pub fn can_use_game_master_blocks(
403 abilities: &PlayerAbilities,
404 permission_level: &PermissionLevel,
405) -> bool {
406 abilities.instant_break && **permission_level >= 2
407}
408
409#[derive(Event, Clone, Debug)]
412pub struct SwingArmEvent {
413 pub entity: Entity,
414}
415pub fn handle_swing_arm_trigger(trigger: Trigger<SwingArmEvent>, mut commands: Commands) {
416 commands.trigger(SendPacketEvent::new(
417 trigger.event().entity,
418 ServerboundSwing {
419 hand: InteractionHand::MainHand,
420 },
421 ));
422}
423pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) {
424 for event in events.read() {
425 commands.trigger(event.clone());
426 }
427}
428
429#[allow(clippy::type_complexity)]
430fn update_modifiers_for_held_item(
431 mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
432) {
433 for (mut attributes, inventory) in &mut query {
434 let held_item = inventory.held_item();
435
436 use azalea_registry::Item;
437 let added_attack_speed = match held_item.kind() {
438 Item::WoodenSword => -2.4,
439 Item::WoodenShovel => -3.0,
440 Item::WoodenPickaxe => -2.8,
441 Item::WoodenAxe => -3.2,
442 Item::WoodenHoe => -3.0,
443
444 Item::StoneSword => -2.4,
445 Item::StoneShovel => -3.0,
446 Item::StonePickaxe => -2.8,
447 Item::StoneAxe => -3.2,
448 Item::StoneHoe => -2.0,
449
450 Item::GoldenSword => -2.4,
451 Item::GoldenShovel => -3.0,
452 Item::GoldenPickaxe => -2.8,
453 Item::GoldenAxe => -3.0,
454 Item::GoldenHoe => -3.0,
455
456 Item::IronSword => -2.4,
457 Item::IronShovel => -3.0,
458 Item::IronPickaxe => -2.8,
459 Item::IronAxe => -3.1,
460 Item::IronHoe => -1.0,
461
462 Item::DiamondSword => -2.4,
463 Item::DiamondShovel => -3.0,
464 Item::DiamondPickaxe => -2.8,
465 Item::DiamondAxe => -3.0,
466 Item::DiamondHoe => 0.0,
467
468 Item::NetheriteSword => -2.4,
469 Item::NetheriteShovel => -3.0,
470 Item::NetheritePickaxe => -2.8,
471 Item::NetheriteAxe => -3.0,
472 Item::NetheriteHoe => 0.0,
473
474 Item::Trident => -2.9,
475 _ => 0.,
476 };
477 attributes
478 .attack_speed
479 .insert(azalea_entity::attributes::base_attack_speed_modifier(
480 added_attack_speed,
481 ));
482 }
483}