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,
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;
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, PlayerAbilities},
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 Option<&Mining>,
254 )>,
255 entity_id_query: Query<&MinecraftEntityId>,
256) {
257 for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
258 query
259 {
260 commands.entity(entity).remove::<StartUseItemQueued>();
261
262 if mining.is_some() {
263 warn!("Got a StartUseItemEvent for a client that was mining");
264 }
265
266 let mut hit_result = (**hit_result).clone();
270
271 if let Some(force_block) = start_use_item.force_block {
272 let hit_result_matches = if let HitResult::Block(block_hit_result) = &hit_result {
273 block_hit_result.block_pos == force_block
274 } else {
275 false
276 };
277
278 if !hit_result_matches {
279 hit_result = HitResult::Block(BlockHitResult {
281 location: force_block.center(),
282 direction: Direction::Up,
283 block_pos: force_block,
284 inside: false,
285 world_border: false,
286 miss: false,
287 });
288 }
289 }
290
291 match &hit_result {
292 HitResult::Block(r) => {
293 let seq = prediction_handler.start_predicting();
294 if r.miss {
295 commands.trigger(SendPacketEvent::new(
296 entity,
297 ServerboundUseItem {
298 hand: start_use_item.hand,
299 seq,
300 x_rot: look_direction.x_rot,
301 y_rot: look_direction.y_rot,
302 },
303 ));
304 } else {
305 commands.trigger(SendPacketEvent::new(
306 entity,
307 ServerboundUseItemOn {
308 hand: start_use_item.hand,
309 block_hit: r.into(),
310 seq,
311 },
312 ));
313 }
318 }
319 HitResult::Entity(r) => {
320 let Ok(entity_id) = entity_id_query.get(r.entity).copied() else {
323 warn!("tried to interact with an entity that doesn't have MinecraftEntityId");
324 continue;
325 };
326
327 commands.trigger(SendPacketEvent::new(
328 entity,
329 ServerboundInteract {
330 entity_id,
331 action: s_interact::ActionType::InteractAt {
332 location: r.location,
333 hand: InteractionHand::MainHand,
334 },
335 using_secondary_action: false,
337 },
338 ));
339 }
340 }
341 }
342}
343
344pub fn check_is_interaction_restricted(
350 instance: &Instance,
351 block_pos: BlockPos,
352 game_mode: &GameMode,
353 inventory: &Inventory,
354) -> bool {
355 match game_mode {
356 GameMode::Adventure => {
357 let held_item = inventory.held_item();
361 match &held_item {
362 ItemStack::Present(item) => {
363 let block = instance.chunks.get_block_state(block_pos);
364 let Some(block) = block else {
365 return true;
367 };
368 check_block_can_be_broken_by_item_in_adventure_mode(item, &block)
369 }
370 _ => true,
371 }
372 }
373 GameMode::Spectator => true,
374 _ => false,
375 }
376}
377
378pub fn check_block_can_be_broken_by_item_in_adventure_mode(
380 item: &ItemStackData,
381 _block: &BlockState,
382) -> bool {
383 if !item.components.has::<components::CanBreak>() {
387 return false;
389 };
390
391 false
392
393 }
400
401pub fn can_use_game_master_blocks(
402 abilities: &PlayerAbilities,
403 permission_level: &PermissionLevel,
404) -> bool {
405 abilities.instant_break && **permission_level >= 2
406}
407
408#[derive(Event, Clone, Debug)]
411pub struct SwingArmEvent {
412 pub entity: Entity,
413}
414pub fn handle_swing_arm_trigger(trigger: Trigger<SwingArmEvent>, mut commands: Commands) {
415 commands.trigger(SendPacketEvent::new(
416 trigger.event().entity,
417 ServerboundSwing {
418 hand: InteractionHand::MainHand,
419 },
420 ));
421}
422pub fn handle_swing_arm_event(mut events: EventReader<SwingArmEvent>, mut commands: Commands) {
423 for event in events.read() {
424 commands.trigger(event.clone());
425 }
426}
427
428#[allow(clippy::type_complexity)]
429fn update_attributes_for_held_item(
430 mut query: Query<(&mut Attributes, &Inventory), (With<LocalEntity>, Changed<Inventory>)>,
431) {
432 for (mut attributes, inventory) in &mut query {
433 let held_item = inventory.held_item();
434
435 use azalea_registry::Item;
436 let added_attack_speed = match held_item.kind() {
437 Item::WoodenSword => -2.4,
438 Item::WoodenShovel => -3.0,
439 Item::WoodenPickaxe => -2.8,
440 Item::WoodenAxe => -3.2,
441 Item::WoodenHoe => -3.0,
442
443 Item::StoneSword => -2.4,
444 Item::StoneShovel => -3.0,
445 Item::StonePickaxe => -2.8,
446 Item::StoneAxe => -3.2,
447 Item::StoneHoe => -2.0,
448
449 Item::GoldenSword => -2.4,
450 Item::GoldenShovel => -3.0,
451 Item::GoldenPickaxe => -2.8,
452 Item::GoldenAxe => -3.0,
453 Item::GoldenHoe => -3.0,
454
455 Item::IronSword => -2.4,
456 Item::IronShovel => -3.0,
457 Item::IronPickaxe => -2.8,
458 Item::IronAxe => -3.1,
459 Item::IronHoe => -1.0,
460
461 Item::DiamondSword => -2.4,
462 Item::DiamondShovel => -3.0,
463 Item::DiamondPickaxe => -2.8,
464 Item::DiamondAxe => -3.0,
465 Item::DiamondHoe => 0.0,
466
467 Item::NetheriteSword => -2.4,
468 Item::NetheriteShovel => -3.0,
469 Item::NetheritePickaxe => -2.8,
470 Item::NetheriteAxe => -3.0,
471 Item::NetheriteHoe => 0.0,
472
473 Item::Trident => -2.9,
474 _ => 0.,
475 };
476 attributes
477 .attack_speed
478 .insert(azalea_entity::attributes::base_attack_speed_modifier(
479 added_attack_speed,
480 ));
481 }
482}
483
484#[allow(clippy::type_complexity)]
485fn update_attributes_for_gamemode(
486 query: Query<(&mut Attributes, &LocalGameMode), (With<LocalEntity>, Changed<LocalGameMode>)>,
487) {
488 for (mut attributes, game_mode) in query {
489 if game_mode.current == GameMode::Creative {
490 attributes
491 .block_interaction_range
492 .insert(creative_block_interaction_range_modifier());
493 attributes
494 .entity_interaction_range
495 .insert(creative_entity_interaction_range_modifier());
496 } else {
497 attributes
498 .block_interaction_range
499 .remove(&creative_block_interaction_range_modifier().id);
500 attributes
501 .entity_interaction_range
502 .remove(&creative_entity_interaction_range_modifier().id);
503 }
504 }
505}