From 684db059e55bc6908f63ecfd128ca1c211a217f8 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Tue, 19 Aug 2025 20:46:52 +0200 Subject: [PATCH] feat: Support for eating food --- pumpkin-data/build/item.rs | 29 ++++++++ pumpkin-data/src/data_component_impl.rs | 26 ++++++- .../src/java/server/play/player_action.rs | 4 +- pumpkin-world/src/item/mod.rs | 19 ++++- pumpkin/src/entity/hunger.rs | 14 ++++ pumpkin/src/entity/living.rs | 73 ++++++++++++++++++- pumpkin/src/net/java/play.rs | 27 ++++++- 7 files changed, 182 insertions(+), 10 deletions(-) diff --git a/pumpkin-data/build/item.rs b/pumpkin-data/build/item.rs index 91733e746..ba68b3bbd 100644 --- a/pumpkin-data/build/item.rs +++ b/pumpkin-data/build/item.rs @@ -35,6 +35,10 @@ pub struct ItemComponents { pub food: Option, #[serde(rename = "minecraft:equippable")] pub equippable: Option, + #[serde(rename = "minecraft:consumable")] + pub consumable: Option, + #[serde(rename = "minecraft:blocks_attacks")] + pub blocks_attacks: Option, } impl ToTokens for ItemComponents { @@ -218,6 +222,21 @@ impl ToTokens for ItemComponents { }), }); }; + if let Some(consumable) = &self.consumable { + let consume_seconds = LitFloat::new( + &format!("{:.1}", consumable.consume_seconds.unwrap_or(1.6)), + Span::call_site(), + ); + + tokens.extend(quote! { (Consumable, &ConsumableImpl { + consume_seconds: #consume_seconds, + }), }); + }; + + if self.blocks_attacks.is_some() { + tokens.extend(quote! { (Consumable, &BlocksAttacksImpl), }); + }; + if let Some(equippable) = &self.equippable { let slot = match equippable.slot.as_str() { "mainhand" => quote! { &EquipmentSlot::MAIN_HAND }, @@ -373,6 +392,16 @@ fn _true() -> bool { true } +#[derive(Deserialize, Clone, Debug)] +pub struct Consumable { + consume_seconds: Option, // TODO +} + +#[derive(Deserialize, Clone, Debug)] +pub struct BlocksAttacks { + // TODO +} + #[allow(dead_code)] #[derive(Deserialize, Clone, Debug)] pub struct EquippableComponent { diff --git a/pumpkin-data/src/data_component_impl.rs b/pumpkin-data/src/data_component_impl.rs index e6d92e129..128ab9f10 100644 --- a/pumpkin-data/src/data_component_impl.rs +++ b/pumpkin-data/src/data_component_impl.rs @@ -288,8 +288,26 @@ impl Hash for FoodImpl { self.can_always_eat.hash(state); } } -#[derive(Clone, Debug, Hash, PartialEq)] -pub struct ConsumableImpl; +#[derive(Clone, Debug, PartialEq)] +pub struct ConsumableImpl { + pub consume_seconds: f32, + // TODO: more +} + +impl ConsumableImpl { + pub fn consume_ticks(&self) -> i32 { + (self.consume_seconds * 20.0) as i32 + } +} + +impl DataComponentImpl for ConsumableImpl { + default_impl!(Consumable); +} +impl Hash for ConsumableImpl { + fn hash(&self, state: &mut H) { + unsafe { (*(&self.consume_seconds as *const f32 as *const u32)).hash(state) }; + } +} #[derive(Clone, Debug, Hash, PartialEq)] pub struct UseRemainderImpl; #[derive(Clone, Debug, Hash, PartialEq)] @@ -545,6 +563,10 @@ pub struct TooltipStyleImpl; pub struct DeathProtectionImpl; #[derive(Clone, Debug, Hash, PartialEq)] pub struct BlocksAttacksImpl; + +impl DataComponentImpl for BlocksAttacksImpl { + default_impl!(BlocksAttacks); +} #[derive(Clone, Debug, Hash, PartialEq)] pub struct StoredEnchantmentsImpl; #[derive(Clone, Debug, Hash, PartialEq)] diff --git a/pumpkin-protocol/src/java/server/play/player_action.rs b/pumpkin-protocol/src/java/server/play/player_action.rs index 26aad0027..b997589cc 100644 --- a/pumpkin-protocol/src/java/server/play/player_action.rs +++ b/pumpkin-protocol/src/java/server/play/player_action.rs @@ -26,7 +26,7 @@ pub enum Status { DropItem, /// I didn't make that up /// Indicates that the currently held item should have its state updated such as eating food, pulling back bows, using buckets, etc. Location is always set to 0/0/0, Face is always set to -Y. Sequence is always set to 0. - ShootArrowOrFinishEating, + ReleaseItemInUse, /// Used to swap or assign an item to the second hand. Location is always set to 0/0/0, Face is always set to -Y. Sequence is always set to 0. SwapItem, } @@ -43,7 +43,7 @@ impl TryFrom for Status { 2 => Ok(Self::FinishedDigging), 3 => Ok(Self::DropItemStack), 4 => Ok(Self::DropItem), - 5 => Ok(Self::ShootArrowOrFinishEating), + 5 => Ok(Self::ReleaseItemInUse), 6 => Ok(Self::SwapItem), _ => Err(InvalidStatus), } diff --git a/pumpkin-world/src/item/mod.rs b/pumpkin-world/src/item/mod.rs index c1c92c55f..fdcd21d44 100644 --- a/pumpkin-world/src/item/mod.rs +++ b/pumpkin-world/src/item/mod.rs @@ -1,7 +1,7 @@ use pumpkin_data::Block; use pumpkin_data::data_component::DataComponent; use pumpkin_data::data_component_impl::{ - DataComponentImpl, IDSet, MaxStackSizeImpl, ToolImpl, get, read_data, + ConsumableImpl, DataComponentImpl, IDSet, MaxStackSizeImpl, ToolImpl, get, read_data, }; use pumpkin_data::item::Item; use pumpkin_data::recipes::RecipeResultStruct; @@ -87,6 +87,17 @@ impl ItemStack { } } + pub fn get_max_use_time(&self) -> i32 { + if let Some(value) = self.get_data_component::() { + return value.consume_ticks(); + } + // TODO: this causes a panic + // if self.get_data_component::().is_some() { + // return 72000; + // } + 0 + } + pub fn get_item(&self) -> &Item { if self.is_empty() { &Item::AIR @@ -129,6 +140,12 @@ impl ItemStack { self.item_count = count; } + pub fn decrement_unless_creative(&mut self, gamemode: GameMode, amount: u8) { + if gamemode != GameMode::Creative { + self.item_count = self.item_count.saturating_sub(amount); + } + } + pub fn decrement(&mut self, amount: u8) { self.item_count = self.item_count.saturating_sub(amount); } diff --git a/pumpkin/src/entity/hunger.rs b/pumpkin/src/entity/hunger.rs index 600c2a8e3..571aa7d7e 100644 --- a/pumpkin/src/entity/hunger.rs +++ b/pumpkin/src/entity/hunger.rs @@ -77,6 +77,20 @@ impl HungerManager { } } + pub async fn add_modifier(&self, player: &Player, food: u8, saturation_modifier: f32) { + let saturation = f32::from(food) * saturation_modifier * 2.0; + self.level.store(food + self.level.load()); + self.saturation.store(saturation + self.saturation.load()); + player.send_health().await; + } + + pub async fn eat(&self, player: &Player, food: u8, saturation: f32) { + self.level.store(food + self.level.load()); + self.saturation.store(saturation + self.saturation.load()); + + player.send_health().await; + } + pub fn add_exhaustion(&self, exhaustion: f32) { self.exhaustion .store((self.exhaustion.load() + exhaustion).min(40.0)); diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index fb220c1af..8429b7e41 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -1,6 +1,7 @@ use pumpkin_data::potion::Effect; use pumpkin_util::math::position::BlockPos; use std::sync::Arc; +use std::sync::atomic::Ordering; use std::sync::atomic::{ AtomicBool, AtomicU8, Ordering::{Relaxed, SeqCst}, @@ -9,6 +10,7 @@ use std::{collections::HashMap, sync::atomic::AtomicI32}; use super::{Entity, NBTStorage}; use super::{EntityBase, NBTStorageInit}; +use crate::entity::player::Hand; use crate::server::Server; use crate::world::loot::{LootContextParameters, LootTableExt}; use async_trait::async_trait; @@ -16,7 +18,7 @@ use crossbeam::atomic::AtomicCell; use pumpkin_config::advanced_config; use pumpkin_data::Block; use pumpkin_data::damage::DeathMessageType; -use pumpkin_data::data_component_impl::EquipmentSlot; +use pumpkin_data::data_component_impl::{EquipmentSlot, FoodImpl}; use pumpkin_data::effect::StatusEffect; use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType}; use pumpkin_data::sound::SoundCategory; @@ -47,6 +49,8 @@ pub struct LivingEntity { pub last_damage_taken: AtomicCell, /// The current health level of the entity. pub health: AtomicCell, + pub item_use_time: AtomicI32, + pub item_in_use: Mutex>, pub death_time: AtomicU8, /// Indicates whether the entity is dead. (`on_death` called) pub dead: AtomicBool, @@ -68,6 +72,7 @@ pub struct LivingEntity { pub climbing_pos: AtomicCell>, water_movement_speed_multiplier: f32, + livings_flags: AtomicU8, } #[async_trait] @@ -78,6 +83,11 @@ pub trait LivingEntityTrait: EntityBase { } impl LivingEntity { + const USING_ITEM_FLAG: i32 = 1; + const OFF_HAND_ACTIVE_FLAG: i32 = 2; + #[allow(dead_code)] + const USING_RIPTIDE_FLAG: i32 = 4; + pub fn new(entity: Entity) -> Self { let water_movement_speed_multiplier = if entity.entity_type == &EntityType::POLAR_BEAR { 0.98 @@ -97,6 +107,9 @@ impl LivingEntity { fall_distance: AtomicCell::new(0.0), death_time: AtomicU8::new(0), dead: AtomicBool::new(false), + item_use_time: AtomicI32::new(0), + item_in_use: Mutex::new(None), + livings_flags: AtomicU8::new(0), active_effects: Mutex::new(HashMap::new()), entity_equipment: Arc::new(Mutex::new(EntityEquipment::new())), jumping: AtomicBool::new(false), @@ -141,6 +154,35 @@ impl LivingEntity { .await; } + /// Sends the Hand animation to all others, used when Eating for example + pub async fn set_active_hand(&self, hand: Hand, stack: ItemStack) { + self.item_use_time + .store(stack.get_max_use_time(), Ordering::Relaxed); + *self.item_in_use.lock().await = Some(stack); + self.set_living_flag(Self::USING_ITEM_FLAG, true).await; + self.set_living_flag(Self::OFF_HAND_ACTIVE_FLAG, hand == Hand::Left) + .await; + } + + async fn set_living_flag(&self, flag: i32, value: bool) { + let index = flag as u8; + let mut b = self.livings_flags.load(Ordering::Relaxed); + if value { + b |= index; + } else { + b &= !index; + } + self.livings_flags.store(b, Ordering::Relaxed); + self.entity + .send_meta_data(&[Metadata::new(8, MetaDataType::Byte, b)]) + .await; + } + + pub async fn clear_active_hand(&self) { + self.set_living_flag(Self::USING_ITEM_FLAG, false).await; + self.item_use_time.store(0, Ordering::Relaxed); + } + pub async fn heal(&self, additional_health: f32) { assert!(additional_health > 0.0); self.set_health(self.health.load() + additional_health) @@ -973,6 +1015,35 @@ impl EntityBase for LivingEntity { self.entity.send_velocity().await; } self.tick_effects().await; + // Current active item + { + let mut item_in_use = self.item_in_use.lock().await; + if let Some(item) = item_in_use.as_ref() + && self.item_use_time.fetch_sub(1, Ordering::Relaxed) <= 0 + { + // Consume item + if let Some(food) = item.get_data_component::() + && let Some(player) = caller.get_player() + { + player + .hunger_manager + .eat(player, food.nutrition as u8, food.saturation) + .await; + } + if let Some(player) = caller.get_player() { + player + .inventory + .held_item() + .lock() + .await + .decrement_unless_creative(player.gamemode.load(), 1); + } + + self.clear_active_hand().await; + *item_in_use = None; + } + } + if self.hurt_cooldown.load(Relaxed) > 0 { self.hurt_cooldown.fetch_sub(1, Relaxed); } diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index bad563d06..d22f1c9bf 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -27,7 +27,7 @@ use crate::server::{Server, seasonal_events}; use crate::world::{World, chunker}; use pumpkin_config::{BASIC_CONFIG, advanced_config}; use pumpkin_data::block_properties::{BlockProperties, WaterLikeProperties}; -use pumpkin_data::data_component_impl::EquipmentSlot; +use pumpkin_data::data_component_impl::{ConsumableImpl, EquipmentSlot, FoodImpl}; use pumpkin_data::entity::{EntityType, entity_from_egg}; use pumpkin_data::item::Item; use pumpkin_data::sound::{Sound, SoundCategory}; @@ -1296,8 +1296,8 @@ impl JavaClient { Status::DropItemStack => { player.drop_held_item(true).await; } - Status::ShootArrowOrFinishEating => { - log::debug!("todo"); + Status::ReleaseItemInUse => { + player.living_entity.clear_active_hand().await; } Status::SwapItem => { player.swap_item().await; @@ -1603,11 +1603,30 @@ impl JavaClient { None, ) }; + let held = item_in_hand.lock().await; + if held.get_data_component::().is_some() { + // If its food we want to make sure we can actually consume it + if let Some(food) = held.get_data_component::() { + if player.abilities.lock().await.invulnerable + || food.can_always_eat + || player.hunger_manager.level.load() < 20 + { + player + .living_entity + .set_active_hand(hand, held.clone()) + .await; + } + } else { + player + .living_entity + .set_active_hand(hand, held.clone()) + .await; + } + } send_cancellable! {{ event; 'after: { - let held = item_in_hand.lock().await; let item = held.item; drop(held); server.item_registry.on_use(item, player).await;