@@ -25,8 +25,9 @@ use azalea_physics::{
2525 local_player:: PhysicsState ,
2626} ;
2727use azalea_protocol:: packets:: game:: {
28- ServerboundInteract , ServerboundUseItem ,
28+ ServerboundInteract , ServerboundPlayerAction , ServerboundUseItem ,
2929 s_interact:: { self , InteractionHand } ,
30+ s_player_action:: Action ,
3031 s_swing:: ServerboundSwing ,
3132 s_use_item_on:: ServerboundUseItemOn ,
3233} ;
@@ -44,6 +45,7 @@ use crate::{
4445 movement:: MoveEventsSystems ,
4546 packet:: game:: SendGamePacketEvent ,
4647 respawn:: perform_respawn,
48+ tick_counter:: TicksConnected ,
4749} ;
4850
4951/// A plugin that allows clients to interact with blocks in the world.
@@ -68,7 +70,10 @@ impl Plugin for InteractPlugin {
6870 )
6971 . add_systems (
7072 GameTick ,
71- handle_start_use_item_queued. before ( PhysicsSystems ) ,
73+ (
74+ handle_start_use_item_queued. before ( PhysicsSystems ) ,
75+ handle_using_item. before ( PhysicsSystems ) ,
76+ ) ,
7277 )
7378 . add_observer ( handle_entity_interact)
7479 . add_observer ( handle_swing_arm_trigger) ;
@@ -208,6 +213,21 @@ pub struct StartUseItemQueued {
208213 /// it, but should be avoided to stay compatible with anticheats.
209214 pub force_block : Option < BlockPos > ,
210215}
216+ /// A component that tracks ongoing item use, like eating food.
217+ ///
218+ /// While this component exists, the client will send [`ServerboundUseItem`]
219+ /// packets every tick. When the use duration expires, it sends
220+ /// [`ServerboundPlayerAction`] with [`ReleaseUseItem`] and removes this
221+ /// component.
222+ #[ derive( Component , Debug ) ]
223+ pub struct UsingItem {
224+ pub hand : InteractionHand ,
225+ /// The tick when the use started.
226+ pub start_tick : u64 ,
227+ /// The duration in seconds.
228+ pub duration_seconds : f32 ,
229+ }
230+
211231#[ allow( clippy:: type_complexity) ]
212232pub fn handle_start_use_item_queued (
213233 mut commands : Commands ,
@@ -217,11 +237,21 @@ pub fn handle_start_use_item_queued(
217237 & mut BlockStatePredictionHandler ,
218238 & HitResultComponent ,
219239 & LookDirection ,
240+ & Inventory ,
241+ & TicksConnected ,
220242 Option < & Mining > ,
221243 ) > ,
222244) {
223- for ( entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in
224- query
245+ for (
246+ entity,
247+ start_use_item,
248+ mut prediction_handler,
249+ hit_result,
250+ look_direction,
251+ inventory,
252+ ticks_connected,
253+ mining,
254+ ) in query
225255 {
226256 commands. entity ( entity) . remove :: < StartUseItemQueued > ( ) ;
227257
@@ -258,6 +288,23 @@ pub fn handle_start_use_item_queued(
258288 HitResult :: Block ( r) => {
259289 let seq = prediction_handler. start_predicting ( ) ;
260290 if r. miss {
291+ // Check if the held item is consumable (like food)
292+ let held_item = inventory. held_item ( ) ;
293+ let is_consumable = matches ! ( held_item, ItemStack :: Present ( item) if item. get_component:: <components:: Consumable >( ) . is_some( ) ) ;
294+
295+ if is_consumable {
296+ // Start consuming the item
297+ let consume_seconds = held_item
298+ . get_component :: < components:: Consumable > ( )
299+ . map ( |c| c. consume_seconds )
300+ . unwrap_or ( 1.6 ) ;
301+ commands. entity ( entity) . insert ( UsingItem {
302+ hand : start_use_item. hand ,
303+ start_tick : ticks_connected. 0 ,
304+ duration_seconds : consume_seconds,
305+ } ) ;
306+ }
307+
261308 commands. trigger ( SendGamePacketEvent :: new (
262309 entity,
263310 ServerboundUseItem {
@@ -293,6 +340,49 @@ pub fn handle_start_use_item_queued(
293340 }
294341}
295342
343+ pub fn handle_using_item (
344+ mut commands : Commands ,
345+ query : Query < (
346+ Entity ,
347+ & UsingItem ,
348+ & mut BlockStatePredictionHandler ,
349+ & LookDirection ,
350+ & TicksConnected ,
351+ ) > ,
352+ ) {
353+ for ( entity, using_item, mut prediction_handler, look_direction, ticks_connected) in query {
354+ let ticks_elapsed = ticks_connected. 0 - using_item. start_tick ;
355+ let duration_ticks = ( using_item. duration_seconds * 20.0 ) as u64 ; // 20 ticks per second
356+
357+ if ticks_elapsed >= duration_ticks {
358+ // Finished using the item, send release packet
359+ let seq = prediction_handler. start_predicting ( ) ;
360+ commands. trigger ( SendGamePacketEvent :: new (
361+ entity,
362+ ServerboundPlayerAction {
363+ action : Action :: ReleaseUseItem ,
364+ pos : BlockPos :: new ( 0 , 0 , 0 ) , // Not used for ReleaseUseItem
365+ direction : Direction :: Down , // Not used for ReleaseUseItem
366+ seq,
367+ } ,
368+ ) ) ;
369+ commands. entity ( entity) . remove :: < UsingItem > ( ) ;
370+ } else {
371+ // Continue using the item, send use packet
372+ let seq = prediction_handler. start_predicting ( ) ;
373+ commands. trigger ( SendGamePacketEvent :: new (
374+ entity,
375+ ServerboundUseItem {
376+ hand : using_item. hand ,
377+ seq,
378+ x_rot : look_direction. x_rot ( ) ,
379+ y_rot : look_direction. y_rot ( ) ,
380+ } ,
381+ ) ) ;
382+ }
383+ }
384+ }
385+
296386/// An ECS `Event` that makes the client tell the server that we right-clicked
297387/// an entity.
298388#[ derive( Clone , Debug , EntityEvent ) ]
0 commit comments