1use std::ops::AddAssign;
2
3use azalea_block::BlockState;
4use azalea_core::{
5 block_hit_result::BlockHitResult,
6 direction::Direction,
7 game_type::GameMode,
8 position::{BlockPos, Vec3},
9};
10use azalea_entity::{
11 Attributes, EyeHeight, LocalEntity, LookDirection, Position, clamp_look_direction, view_vector,
12};
13use azalea_inventory::{ItemStack, ItemStackData, components};
14use azalea_physics::clip::{BlockShapeType, ClipContext, FluidPickType};
15use azalea_protocol::packets::game::{
16 s_interact::InteractionHand,
17 s_swing::ServerboundSwing,
18 s_use_item_on::{BlockHit, ServerboundUseItemOn},
19};
20use azalea_world::{Instance, InstanceContainer, InstanceName};
21use bevy_app::{App, Plugin, Update};
22use bevy_ecs::{
23 component::Component,
24 entity::Entity,
25 event::{Event, EventReader},
26 query::{Changed, With},
27 schedule::IntoSystemConfigs,
28 system::{Commands, Query, Res},
29};
30use derive_more::{Deref, DerefMut};
31use tracing::warn;
32
33use super::packet::game::handle_outgoing_packets;
34use crate::{
35 Client,
36 attack::handle_attack_event,
37 inventory::{Inventory, InventorySet},
38 local_player::{LocalGameMode, PermissionLevel, PlayerAbilities},
39 movement::MoveEventsSet,
40 packet::game::SendPacketEvent,
41 respawn::perform_respawn,
42};
43
44pub struct InteractPlugin;
46impl Plugin for InteractPlugin {
47 fn build(&self, app: &mut App) {
48 app.add_event::<BlockInteractEvent>()
49 .add_event::<SwingArmEvent>()
50 .add_systems(
51 Update,
52 (
53 (
54 update_hit_result_component.after(clamp_look_direction),
55 handle_block_interact_event,
56 handle_swing_arm_event,
57 )
58 .before(handle_outgoing_packets)
59 .after(InventorySet)
60 .after(perform_respawn)
61 .after(handle_attack_event)
62 .chain(),
63 update_modifiers_for_held_item
64 .after(InventorySet)
65 .after(MoveEventsSet),
66 ),
67 );
68 }
69}
70
71impl Client {
72 pub fn block_interact(&mut self, position: BlockPos) {
79 self.ecs.lock().send_event(BlockInteractEvent {
80 entity: self.entity,
81 position,
82 });
83 }
84}
85
86#[derive(Event)]
90pub struct BlockInteractEvent {
91 pub entity: Entity,
93 pub position: BlockPos,
95}
96
97#[derive(Component, Copy, Clone, Debug, Default, Deref)]
100pub struct CurrentSequenceNumber(u32);
101
102impl AddAssign<u32> for CurrentSequenceNumber {
103 fn add_assign(&mut self, rhs: u32) {
104 self.0 += rhs;
105 }
106}
107
108#[derive(Component, Clone, Debug, Deref, DerefMut)]
110pub struct HitResultComponent(BlockHitResult);
111
112pub fn handle_block_interact_event(
113 mut events: EventReader<BlockInteractEvent>,
114 mut query: Query<(Entity, &mut CurrentSequenceNumber, &HitResultComponent)>,
115 mut commands: Commands,
116) {
117 for event in events.read() {
118 let Ok((entity, mut sequence_number, hit_result)) = query.get_mut(event.entity) else {
119 warn!("Sent BlockInteractEvent for entity that doesn't have the required components");
120 continue;
121 };
122
123 *sequence_number += 1;
126
127 let block_hit = if hit_result.block_pos == event.position {
134 BlockHit {
136 block_pos: hit_result.block_pos,
137 direction: hit_result.direction,
138 location: hit_result.location,
139 inside: hit_result.inside,
140 world_border: hit_result.world_border,
141 }
142 } else {
143 BlockHit {
145 block_pos: event.position,
146 direction: Direction::Up,
147 location: event.position.center(),
148 inside: false,
149 world_border: false,
150 }
151 };
152
153 commands.trigger(SendPacketEvent::new(
154 entity,
155 ServerboundUseItemOn {
156 hand: InteractionHand::MainHand,
157 block_hit,
158 sequence: sequence_number.0,
159 },
160 ));
161 }
162}
163
164#[allow(clippy::type_complexity)]
165pub fn update_hit_result_component(
166 mut commands: Commands,
167 mut query: Query<(
168 Entity,
169 Option<&mut HitResultComponent>,
170 &LocalGameMode,
171 &Position,
172 &EyeHeight,
173 &LookDirection,
174 &InstanceName,
175 )>,
176 instance_container: Res<InstanceContainer>,
177) {
178 for (entity, hit_result_ref, game_mode, position, eye_height, look_direction, world_name) in
179 &mut query
180 {
181 let pick_range = if game_mode.current == GameMode::Creative {
182 6.
183 } else {
184 4.5
185 };
186 let eye_position = Vec3 {
187 x: position.x,
188 y: position.y + **eye_height as f64,
189 z: position.z,
190 };
191
192 let Some(instance_lock) = instance_container.get(world_name) else {
193 continue;
194 };
195 let instance = instance_lock.read();
196
197 let hit_result = pick(look_direction, &eye_position, &instance.chunks, pick_range);
198 if let Some(mut hit_result_ref) = hit_result_ref {
199 **hit_result_ref = hit_result;
200 } else {
201 commands
202 .entity(entity)
203 .insert(HitResultComponent(hit_result));
204 }
205 }
206}
207
208pub fn pick(
214 look_direction: &LookDirection,
215 eye_position: &Vec3,
216 chunks: &azalea_world::ChunkStorage,
217 pick_range: f64,
218) -> BlockHitResult {
219 let view_vector = view_vector(look_direction);
220 let end_position = eye_position + &(view_vector * pick_range);
221 azalea_physics::clip::clip(
222 chunks,
223 ClipContext {
224 from: *eye_position,
225 to: end_position,
226 block_shape_type: BlockShapeType::Outline,
227 fluid_pick_type: FluidPickType::None,
228 },
229 )
230}
231
232pub fn check_is_interaction_restricted(
238 instance: &Instance,
239 block_pos: &BlockPos,
240 game_mode: &GameMode,
241 inventory: &Inventory,
242) -> bool {
243 match game_mode {
244 GameMode::Adventure => {
245 let held_item = inventory.held_item();
249 match &held_item {
250 ItemStack::Present(item) => {
251 let block = instance.chunks.get_block_state(block_pos);
252 let Some(block) = block else {
253 return true;
255 };
256 check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
257 }
258 _ => true,
259 }
260 }
261 GameMode::Spectator => true,
262 _ => false,
263 }
264}
265
266pub fn check_block_can_be_broken_by_item_in_adventure_mode(
268 item: &ItemStackData,
269 _block: &BlockState,
270) -> bool {
271 if !item.components.has::<components::CanBreak>() {
275 return false;
277 };
278
279 false
280
281 }
288
289pub fn can_use_game_master_blocks(
290 abilities: &PlayerAbilities,
291 permission_level: &PermissionLevel,
292) -> bool {
293 abilities.instant_break && **permission_level >= 2
294}
295
296#[derive(Event)]
299pub struct SwingArmEvent {
300 pub entity: Entity,
301}
302pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) {
303 for event in events.read() {
304 commands.trigger(SendPacketEvent::new(
305 event.entity,
306 ServerboundSwing {
307 hand: InteractionHand::MainHand,
308 },
309 ));
310 }
311}
312
313#[allow(clippy::type_complexity)]
314fn update_modifiers_for_held_item(
315 mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
316) {
317 for (mut attributes, inventory) in &mut query {
318 let held_item = inventory.held_item();
319
320 use azalea_registry::Item;
321 let added_attack_speed = match held_item.kind() {
322 Item::WoodenSword => -2.4,
323 Item::WoodenShovel => -3.0,
324 Item::WoodenPickaxe => -2.8,
325 Item::WoodenAxe => -3.2,
326 Item::WoodenHoe => -3.0,
327
328 Item::StoneSword => -2.4,
329 Item::StoneShovel => -3.0,
330 Item::StonePickaxe => -2.8,
331 Item::StoneAxe => -3.2,
332 Item::StoneHoe => -2.0,
333
334 Item::GoldenSword => -2.4,
335 Item::GoldenShovel => -3.0,
336 Item::GoldenPickaxe => -2.8,
337 Item::GoldenAxe => -3.0,
338 Item::GoldenHoe => -3.0,
339
340 Item::IronSword => -2.4,
341 Item::IronShovel => -3.0,
342 Item::IronPickaxe => -2.8,
343 Item::IronAxe => -3.1,
344 Item::IronHoe => -1.0,
345
346 Item::DiamondSword => -2.4,
347 Item::DiamondShovel => -3.0,
348 Item::DiamondPickaxe => -2.8,
349 Item::DiamondAxe => -3.0,
350 Item::DiamondHoe => 0.0,
351
352 Item::NetheriteSword => -2.4,
353 Item::NetheriteShovel => -3.0,
354 Item::NetheritePickaxe => -2.8,
355 Item::NetheriteAxe => -3.0,
356 Item::NetheriteHoe => 0.0,
357
358 Item::Trident => -2.9,
359 _ => 0.,
360 };
361 attributes
362 .attack_speed
363 .insert(azalea_entity::attributes::base_attack_speed_modifier(
364 added_attack_speed,
365 ));
366 }
367}