feat: Support for eating food

This commit is contained in:
Alexander Medvedev
2025-08-19 20:46:52 +02:00
parent 314547eb5f
commit 684db059e5
7 changed files with 182 additions and 10 deletions

View File

@@ -35,6 +35,10 @@ pub struct ItemComponents {
pub food: Option<FoodComponent>,
#[serde(rename = "minecraft:equippable")]
pub equippable: Option<EquippableComponent>,
#[serde(rename = "minecraft:consumable")]
pub consumable: Option<Consumable>,
#[serde(rename = "minecraft:blocks_attacks")]
pub blocks_attacks: Option<BlocksAttacks>,
}
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<f32>, // TODO
}
#[derive(Deserialize, Clone, Debug)]
pub struct BlocksAttacks {
// TODO
}
#[allow(dead_code)]
#[derive(Deserialize, Clone, Debug)]
pub struct EquippableComponent {

View File

@@ -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<H: std::hash::Hasher>(&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)]

View File

@@ -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<i32> 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),
}

View File

@@ -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::<ConsumableImpl>() {
return value.consume_ticks();
}
// TODO: this causes a panic
// if self.get_data_component::<BlocksAttacksImpl>().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);
}

View File

@@ -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));

View File

@@ -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<f32>,
/// The current health level of the entity.
pub health: AtomicCell<f32>,
pub item_use_time: AtomicI32,
pub item_in_use: Mutex<Option<ItemStack>>,
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<Option<BlockPos>>,
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::<FoodImpl>()
&& 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);
}

View File

@@ -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::<ConsumableImpl>().is_some() {
// If its food we want to make sure we can actually consume it
if let Some(food) = held.get_data_component::<FoodImpl>() {
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;