diff --git a/pumpkin-data/build/item.rs b/pumpkin-data/build/item.rs index 344034426..65bc79584 100644 --- a/pumpkin-data/build/item.rs +++ b/pumpkin-data/build/item.rs @@ -1,6 +1,7 @@ use heck::ToShoutySnakeCase; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, format_ident, quote}; +use std::fmt; use syn::{Ident, LitBool, LitFloat, LitInt, LitStr}; include!("../src/tag.rs"); @@ -28,6 +29,12 @@ pub struct ItemComponents { pub attribute_modifiers: Option, #[serde(rename = "minecraft:tool")] pub tool: Option, + #[serde(rename = "minecraft:food")] + pub food: Option, + #[serde(rename = "minecraft:consumable")] + pub consumable: Option, + #[serde(rename = "minecraft:use_remainder")] + pub use_remainder: Option, } impl ToTokens for ItemComponents { @@ -90,6 +97,117 @@ impl ToTokens for ItemComponents { None => quote! { None }, }; + let food = match &self.food { + Some(food) => { + let nutrition = food.nutrition; + let saturation = food.saturation; + let can_always_eat = food + .can_always_eat + .is_some_and(|can_always_eat| can_always_eat); + quote! { Some(Food { nutrition: #nutrition, saturation: #saturation, can_always_eat: #can_always_eat }) } + } + None => quote! { None }, + }; + + let use_remainder = match &self.use_remainder { + Some(remainder) => { + let id = LitStr::new(&remainder.id, Span::call_site()); + let count = remainder.count.map_or(1, |count| count); + quote! { Some(Remainder { id: #id, count: #count }) } + } + None => quote! { None }, + }; + + let consumable = match &self.consumable { + Some(consumable) => { + let consume_seconds = consumable + .consume_seconds + .map_or(1.6, |consume_seconds| consume_seconds); + let sound = consumable + .sound + .as_ref() + .map_or("entity.generic.eat", |s| s) + .to_string(); + let animation = consumable + .animation + .as_ref() + .map_or("eat", |s| s) + .to_string(); + let has_consume_particles = consumable + .has_consume_particles + .is_some_and(|has_consume_particles| has_consume_particles); + + let on_consume_effects = match &consumable.on_consume_effects { + Some(on_consume_effects) => { + let on_consume_effects = on_consume_effects.iter().map(|consume_effect| { + let r#type = LitStr::new(&consume_effect.r#type, Span::call_site()); + let effects = match &consume_effect.effects { + Some(effects) => match &effects { + Effects::Single(s) => { + let s = LitStr::new(s, Span::call_site()); + quote! { Some(&Effects::Single(#s)) } + } + Effects::List(effects) => { + let effects = effects.iter().map(|effect| { + let id = LitStr::new(&effect.id, Span::call_site()); + let amplifier = + effect.amplifier.map_or(0, |amplifier| amplifier); + let duration = + effect.duration.map_or(1, |duration| duration); + let ambient = + effect.ambient.is_some_and(|ambient| ambient); + let show_particles = effect + .show_particles + .is_none_or(|show_particles| show_particles); + let show_icon = + effect.show_icon.is_none_or(|show_icon| show_icon); + quote! { + Effect { + id: #id, + amplifier: #amplifier, + duration: #duration, + ambient: #ambient, + show_particles: #show_particles, + show_icon: #show_icon, + } + } + }); + quote! { Some(&Effects::List(&[#(#effects),*])) } + } + }, + None => quote! { None }, + }; + let probability = consume_effect + .probability + .map_or(1f32, |probability| probability); + let diameter = + consume_effect.diameter.map_or(16f32, |diameter| diameter); + quote! { + ConsumeEffect { + r#type: #r#type, + effects: #effects, + probability: #probability, + diameter: #diameter, + } + } + }); + quote! { Some(&[#(#on_consume_effects),*]) } + } + None => quote! { None }, + }; + quote! { + Some(Consumable { + consume_seconds: #consume_seconds, + sound: #sound, + animation: #animation, + has_consume_particles: #has_consume_particles, + on_consume_effects: #on_consume_effects, + }) + } + } + None => quote! { None }, + }; + let tool = match &self.tool { Some(tool) => { let rules_code = tool.rules.iter().map(|rule| { @@ -154,7 +272,10 @@ impl ToTokens for ItemComponents { damage: #damage, max_damage: #max_damage, attribute_modifiers: #attribute_modifiers, - tool: #tool + tool: #tool, + food: #food, + consumable: #consumable, + use_remainder: #use_remainder, } }); } @@ -193,6 +314,53 @@ pub struct Modifier { pub slot: String, } +#[derive(Deserialize, Clone, Debug)] +pub struct Food { + pub nutrition: u32, + pub saturation: f32, + pub can_always_eat: Option, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct Remainder { + pub id: String, + pub count: Option, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct Consumable { + pub consume_seconds: Option, + pub animation: Option, + pub sound: Option, + pub has_consume_particles: Option, + pub on_consume_effects: Option>, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct ConsumeEffect { + pub r#type: String, + // effects can either be Vec or a Single String + pub effects: Option, + pub probability: Option, + pub diameter: Option, +} + +#[derive(Clone, Debug)] +pub enum Effects { + List(Vec), + Single(String), +} + +#[derive(Deserialize, Clone, Debug)] +pub struct Effect { + pub id: String, + pub amplifier: Option, + pub duration: Option, + pub ambient: Option, + pub show_particles: Option, + pub show_icon: Option, +} + #[derive(Deserialize, Clone, Debug, PartialEq)] #[serde(rename_all = "snake_case")] #[allow(clippy::enum_variant_names)] @@ -202,6 +370,40 @@ pub enum Operation { AddMultipliedTotal, } +impl<'de> Deserialize<'de> for Effects { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct EffectsVisitor; + + impl<'de> Visitor<'de> for EffectsVisitor { + type Value = Effects; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a list of Effect objects or a single String") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(Effects::Single(value.to_string())) + } + + fn visit_seq(self, seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let vec = Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))?; + Ok(Effects::List(vec)) + } + } + + deserializer.deserialize_any(EffectsVisitor) + } +} + pub(crate) fn build() -> TokenStream { println!("cargo:rerun-if-changed=../assets/items.json"); @@ -261,7 +463,56 @@ pub(crate) fn build() -> TokenStream { pub damage: Option, pub max_damage: Option, pub attribute_modifiers: Option, - pub tool: Option + pub tool: Option, + pub food: Option, + pub consumable: Option, + pub use_remainder: Option, + } + + #[derive(Clone, Copy, Debug)] + pub struct Food { + pub nutrition: u32, + pub saturation: f32, + pub can_always_eat: bool, + } + + #[derive(Clone, Copy, Debug)] + pub struct Remainder { + pub id: &'static str, + pub count: u8, + } + + #[derive(Clone, Copy, Debug)] + pub struct Consumable { + pub consume_seconds: f32, + pub sound: &'static str, + pub animation: &'static str, + pub has_consume_particles: bool, + pub on_consume_effects: Option<&'static [ConsumeEffect]>, + } + + #[derive(Clone, Copy, Debug)] + pub struct ConsumeEffect { + pub r#type: &'static str, + pub effects: Option<&'static Effects>, + pub probability: f32, + pub diameter: f32, + } + + #[derive(Clone, Debug)] + pub enum Effects { + List(&'static [Effect]), + Single(&'static str), + } + + #[derive(Clone, Copy, Debug)] + pub struct Effect { + pub id: &'static str, + pub amplifier: i8, + pub duration: i32, + pub ambient: bool, + pub show_particles: bool, + pub show_icon: bool, } #[derive(Clone, Copy, Debug)] diff --git a/pumpkin-data/src/lib.rs b/pumpkin-data/src/lib.rs index a14580a1d..e2fcc4906 100644 --- a/pumpkin-data/src/lib.rs +++ b/pumpkin-data/src/lib.rs @@ -55,3 +55,7 @@ pub mod damage { pub mod fluid { include!(concat!(env!("OUT_DIR"), "/fluid.rs")); } + +pub fn parse_registry_name(name: &str) -> String { + name.replace("minecraft:", "") +} diff --git a/pumpkin-inventory/src/player.rs b/pumpkin-inventory/src/player.rs index 47ac7aafc..72c0de079 100644 --- a/pumpkin-inventory/src/player.rs +++ b/pumpkin-inventory/src/player.rs @@ -266,6 +266,19 @@ impl PlayerInventory { let (items, hotbar) = self.items.split_at_mut(27); hotbar.iter_mut().chain(items) } + + pub fn add_item(&mut self, item: ItemStack) -> bool { + if let Some(slot) = self.collect_item_slot(item.item.id) { + let slot_item = self.items[slot]; + if let Some(mut slot_item) = slot_item { + slot_item.item_count += item.item_count; + } else { + self.items[slot] = Some(item); + } + return true; + } + false + } } impl Container for PlayerInventory { diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index 55f5958fa..ede0a4def 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -43,6 +43,7 @@ mod player_info_update; mod player_position; mod player_remove; mod remove_entities; +mod remove_mob_effect; mod reset_score; mod respawn; mod server_links; @@ -122,6 +123,7 @@ pub use player_info_update::*; pub use player_position::*; pub use player_remove::*; pub use remove_entities::*; +pub use remove_mob_effect::*; pub use reset_score::*; pub use respawn::*; pub use server_links::*; diff --git a/pumpkin-protocol/src/client/play/remove_mob_effect.rs b/pumpkin-protocol/src/client/play/remove_mob_effect.rs new file mode 100644 index 000000000..1329f6508 --- /dev/null +++ b/pumpkin-protocol/src/client/play/remove_mob_effect.rs @@ -0,0 +1,21 @@ +use pumpkin_data::packet::clientbound::PLAY_REMOVE_MOB_EFFECT; +use pumpkin_macros::packet; +use serde::Serialize; + +use crate::codec::var_int::VarInt; + +#[derive(Serialize)] +#[packet(PLAY_REMOVE_MOB_EFFECT)] +pub struct CRemoveMobEffect { + entity_id: VarInt, + effect_id: VarInt, +} + +impl CRemoveMobEffect { + pub fn new(entity_id: VarInt, effect_id: VarInt) -> Self { + Self { + entity_id, + effect_id, + } + } +} diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index da9847337..e61feb1d8 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -151,6 +151,23 @@ impl LivingEntity { // TODO broadcast metadata } + pub async fn remove_effect(&self, effect: EffectType) { + let mut effects = self.active_effects.lock().await; + effects.remove(&effect); + // TODO broadcast metadata + } + + pub async fn clear_effects(&self) { + let mut effects = self.active_effects.lock().await; + effects.clear(); + // TODO broadcast metadata + } + + pub async fn get_effects(&self) -> Vec { + let effects = self.active_effects.lock().await; + effects.values().cloned().collect() + } + pub async fn has_effect(&self, effect: EffectType) -> bool { let effects = self.active_effects.lock().await; effects.contains_key(&effect) diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 4edaa4b0b..1d2d95c2a 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -8,13 +8,32 @@ use std::{ time::{Duration, Instant}, }; +use super::living::LivingEntity; +use super::{ + Entity, EntityBase, EntityId, NBTStorage, + combat::{self, AttackType, player_attack_sound}, + effect::Effect, + hunger::HungerManager, + item::ItemEntity, +}; +use crate::{ + block, + command::{client_suggestions, dispatcher::CommandDispatcher}, + data::op_data::OPERATOR_CONFIG, + net::{Client, PlayerConfig}, + server::Server, + world::World, +}; +use crate::{error::PumpkinError, net::GameProfile}; use async_trait::async_trait; use crossbeam::atomic::AtomicCell; use pumpkin_config::{ADVANCED_CONFIG, BASIC_CONFIG}; +use pumpkin_data::item::{Consumable, ConsumeEffect, Effects}; use pumpkin_data::{ damage::DamageType, entity::{EffectType, EntityStatus, EntityType}, item::Operation, + parse_registry_name, particle::Particle, sound::{Sound, SoundCategory}, }; @@ -26,8 +45,9 @@ use pumpkin_protocol::{ client::play::{ CAcknowledgeBlockChange, CActionBar, CCombatDeath, CDisguisedChatMessage, CGameEvent, CKeepAlive, CParticle, CPlayDisconnect, CPlayerAbilities, CPlayerInfoUpdate, - CPlayerPosition, CRespawn, CSetExperience, CSetHealth, CSubtitle, CSystemChatMessage, - CTitleText, CUnloadChunk, CUpdateMobEffect, GameEvent, MetaDataType, PlayerAction, + CPlayerPosition, CRemoveMobEffect, CRespawn, CSetExperience, CSetHealth, CSubtitle, + CSystemChatMessage, CTitleText, CUnloadChunk, CUpdateMobEffect, GameEvent, MetaDataType, + PlayerAction, }, server::play::{ SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, SClientTickEnd, @@ -63,24 +83,29 @@ use pumpkin_util::{ use pumpkin_world::{cylindrical_chunk_iterator::Cylindrical, item::ItemStack}; use tokio::sync::{Mutex, Notify, RwLock}; -use super::{ - Entity, EntityBase, EntityId, NBTStorage, - combat::{self, AttackType, player_attack_sound}, - effect::Effect, - hunger::HungerManager, - item::ItemEntity, -}; -use crate::{ - block, - command::{client_suggestions, dispatcher::CommandDispatcher}, - data::op_data::OPERATOR_CONFIG, - net::{Client, PlayerConfig}, - server::Server, - world::World, -}; -use crate::{error::PumpkinError, net::GameProfile}; +pub enum PlayerUseItemState { + None, + Eating { + start_time: Instant, + eat_duration: Duration, + item_id: u16, + }, + Drinking { + start_time: Instant, + drink_duration: Duration, + item_id: u16, + }, + Drawing { + start_time: Instant, + draw_duration: Duration, + }, +} -use super::living::LivingEntity; +impl Default for PlayerUseItemState { + fn default() -> Self { + Self::None + } +} /// Represents a Minecraft player entity. /// @@ -148,6 +173,7 @@ pub struct Player { /// The player's total experience points pub experience_points: AtomicI32, pub experience_pick_up_delay: Mutex, + pub use_item_state: Mutex, } impl Player { @@ -238,6 +264,7 @@ impl Player { experience_level: AtomicI32::new(0), experience_progress: AtomicCell::new(0.0), experience_points: AtomicI32::new(0), + use_item_state: Mutex::new(PlayerUseItemState::default()), } } @@ -490,6 +517,7 @@ impl Player { self.living_entity.tick(server).await; self.hunger_manager.tick(self).await; + self.tick_eating(server).await; // timeout/keep alive handling self.tick_client_load_timeout(); @@ -1113,6 +1141,29 @@ impl Player { self.living_entity.add_effect(effect).await; } + pub async fn remove_effect(&self, effect: EffectType) { + self.client + .send_packet(&CRemoveMobEffect::new( + self.entity_id().into(), + VarInt(effect as i32), + )) + .await; + self.living_entity.remove_effect(effect).await; + } + + pub async fn clear_effects(&self) { + let vec = self.living_entity.get_effects().await; + for effect in vec { + self.client + .send_packet(&CRemoveMobEffect::new( + self.entity_id().into(), + VarInt(effect.r#type as i32), + )) + .await; + } + self.living_entity.clear_effects().await; + } + /// Add experience levels to the player pub async fn add_experience_levels(&self, added_levels: i32) { let current_level = self.experience_level.load(Ordering::Relaxed); @@ -1151,6 +1202,270 @@ impl Player { let progress = experience::progress_in_level(new_points, new_level); self.set_experience(new_level, progress, new_points).await; } + + pub async fn set_eating(&self, item_id: u16, duration: Duration) { + let mut state = self.use_item_state.lock().await; + *state = PlayerUseItemState::Eating { + start_time: Instant::now(), + eat_duration: duration, + item_id, + }; + + // Update metadata to show eating animation + self.living_entity + .entity + .send_meta_data(&[Metadata::new( + 8, + MetaDataType::Byte, + 1i8, // 1 for active hand + )]) + .await; + } + + pub async fn stop_eating(&self) { + let mut state = self.use_item_state.lock().await; + *state = PlayerUseItemState::None; + + // Update metadata to stop showing eating animation + self.living_entity + .entity + .send_meta_data(&[Metadata::new( + 8, + MetaDataType::Byte, + 0i8, // 0 for no active hand + )]) + .await; + } + + pub async fn tick_eating(&self, server: &Server) { + // 1. Check if currently eating and if timer has completed + let (should_consume, item_id) = { + let state = self.use_item_state.lock().await; + match *state { + PlayerUseItemState::Eating { + start_time, + eat_duration, + item_id, + } => { + if start_time.elapsed() >= eat_duration { + (true, Some(item_id)) + } else { + // Still eating - play particles and sounds periodically + let elapsed = start_time.elapsed(); + if elapsed.as_millis() % 200 < 20 { + // Every ~200ms + // Release lock before async calls + drop(state); + + // Play eating sound + self.world() + .await + .play_sound( + Sound::EntityGenericEat, + SoundCategory::Players, + &self.living_entity.entity.pos.load(), + ) + .await; + + // Spawn particles + // TODO: investigate this make the client disconnect + /*let position = self.position(); + self.spawn_particle( + position, + Vector3::new(0.1, 0.1, 0.1), + 0.05, + 5, + Particle::Item + ).await;*/ + } + (false, None) + } + } + _ => (false, None), + } + }; + + // 2. Process completed eating + if let Some(item_id) = item_id.filter(|_| should_consume) { + // Get the item data from registry + if let Ok(item) = server.item_registry.get_item_from_id(item_id) { + // Verify player still has the item + let has_item = { + let inventory = self.inventory.lock().await; + inventory + .held_item() + .is_some_and(|stack| stack.item.id == item_id) + }; + + if has_item { + // Stop eating animation + self.stop_eating().await; + + // Play finishing sound + self.world() + .await + .play_sound( + Sound::EntityPlayerBurp, + SoundCategory::Players, + &self.living_entity.entity.pos.load(), + ) + .await; + + // Apply food effects + if let Some(food) = item.components.food { + // Add food and saturation + self.hunger_manager + .level + .store((self.hunger_manager.level.load() + food.nutrition).min(20)); + self.hunger_manager.saturation.store( + (self.hunger_manager.saturation.load() + food.saturation) + .min(self.hunger_manager.level.load() as f32), + ); + + // Apply effects if present + if let Some(consumable) = item.components.consumable { + if let Some(effects) = consumable.on_consume_effects { + self.apply_food_effects(&consumable, effects).await; + } + } + + // Update client with new hunger values + self.send_health().await; + } + + let mut inventory = self.inventory.lock().await; + // Handle container items (like bowls from stew) + if let Some(remainder) = item.components.use_remainder { + if let Ok(remainder_item) = + server.item_registry.get_item_from_name(remainder.id) + { + inventory.add_item(ItemStack::new(remainder.count, remainder_item)); + } + } + + // Decrease item stack + inventory.decrease_current_stack(1); + } + } + } + + // 3. Check for eating interruptions + let should_interrupt = { + let state = self.use_item_state.lock().await; + let inventory = self.inventory.lock().await; + if let PlayerUseItemState::Eating { item_id, .. } = *state { + // Check conditions that would interrupt eating + let health_too_low = self.living_entity.health.load() <= 0.0; + + // Check if item changed + let current_held_item = inventory.held_item(); + let item_changed = current_held_item.is_none_or(|stack| stack.item.id != item_id); + + health_too_low || item_changed + } else { + false + } + }; + + // Handle interruption if needed + if should_interrupt { + self.stop_eating().await; + } + } + + async fn apply_food_effects(&self, consumable: &Consumable, effects: &[ConsumeEffect]) { + for consume_effect in effects { + let roll = rand::random::(); + + if roll <= consume_effect.probability { + // TODO: convert this magic string to an enum + match consume_effect.r#type { + "minecraft:apply_effects" => { + if let Some(Effects::List(effect_list)) = consume_effect.effects { + for effect in *effect_list { + self.apply_single_effect(effect).await; + } + } + } + "minecraft:remove_effects" => { + if let Some(Effects::Single(effect_id)) = &consume_effect.effects { + if let Some(effect_type) = + EffectType::from_name(&parse_registry_name(effect_id)) + { + self.remove_effect(effect_type).await; + } + } + } + "minecraft:clear_all_effects" => { + self.clear_effects().await; + } + "minecraft:teleport_randomly" => { + self.random_teleport_close_to_player().await; + } + "minecraft:play_sound" => { + if let Some(sound) = + Sound::from_name(&parse_registry_name(consumable.sound)) + { + self.world() + .await + .play_sound(sound, SoundCategory::Players, &self.position()) + .await; + } + } + _ => {} + } + } + } + } + + async fn apply_single_effect(&self, effect: &pumpkin_data::item::Effect) { + if let Some(effect_type) = EffectType::from_name(&parse_registry_name(effect.id)) { + self.add_effect( + Effect { + r#type: effect_type, + duration: effect.duration, + amplifier: effect.amplifier as u8, + ambient: effect.ambient, + show_particles: effect.show_particles, + show_icon: effect.show_icon, + }, + false, + ) + .await; + } + } + + async fn random_teleport_close_to_player(&self) { + let offset_x = rand::random::() * 32.0 - 16.0; // -16.0 to 16.0 + let offset_y = rand::random::() * 32.0 - 16.0; + let offset_z = rand::random::() * 32.0 - 16.0; + + let current_pos = self.position(); + + // TODO: safe check for teleport location + let target_pos = Vector3::new( + current_pos.x + offset_x, + current_pos.y + offset_y, + current_pos.z + offset_z, + ); + + let world = self.world().await; + self.request_teleport( + target_pos, + self.living_entity.entity.yaw.load(), + self.living_entity.entity.pitch.load(), + ) + .await; + + // Play teleport sound at new location + world + .play_sound( + Sound::ItemChorusFruitTeleport, + SoundCategory::Players, + &target_pos, + ) + .await; + } } #[async_trait] diff --git a/pumpkin/src/item/items/food.rs b/pumpkin/src/item/items/food.rs new file mode 100644 index 000000000..daf6e485a --- /dev/null +++ b/pumpkin/src/item/items/food.rs @@ -0,0 +1,251 @@ +// pumpkin/src/item/items/food.rs + +use crate::item::items::pumpkin_food::PumpkinFood; +use async_trait::async_trait; +use pumpkin_macros::pumpkin_item; + +// Basic food items +#[pumpkin_item("apple")] +pub struct AppleItem; + +#[async_trait] +impl PumpkinFood for AppleItem {} + +#[pumpkin_item("bread")] +pub struct BreadItem; + +#[async_trait] +impl PumpkinFood for BreadItem {} + +#[pumpkin_item("cooked_beef")] +pub struct CookedBeefItem; + +#[async_trait] +impl PumpkinFood for CookedBeefItem {} + +#[pumpkin_item("cooked_chicken")] +pub struct CookedChickenItem; + +#[async_trait] +impl PumpkinFood for CookedChickenItem {} + +#[pumpkin_item("cooked_cod")] +pub struct CookedCodItem; + +#[async_trait] +impl PumpkinFood for CookedCodItem {} + +#[pumpkin_item("cooked_mutton")] +pub struct CookedMuttonItem; + +#[async_trait] +impl PumpkinFood for CookedMuttonItem {} + +#[pumpkin_item("cooked_porkchop")] +pub struct CookedPorkchopItem; + +#[async_trait] +impl PumpkinFood for CookedPorkchopItem {} + +#[pumpkin_item("cooked_rabbit")] +pub struct CookedRabbitItem; + +#[async_trait] +impl PumpkinFood for CookedRabbitItem {} + +#[pumpkin_item("cooked_salmon")] +pub struct CookedSalmonItem; + +#[async_trait] +impl PumpkinFood for CookedSalmonItem {} + +// Raw foods +#[pumpkin_item("beef")] +pub struct BeefItem; + +#[async_trait] +impl PumpkinFood for BeefItem {} + +#[pumpkin_item("chicken")] +pub struct ChickenItem; + +#[async_trait] +impl PumpkinFood for ChickenItem {} + +#[pumpkin_item("cod")] +pub struct CodItem; + +#[async_trait] +impl PumpkinFood for CodItem {} + +#[pumpkin_item("mutton")] +pub struct MuttonItem; + +#[async_trait] +impl PumpkinFood for MuttonItem {} + +#[pumpkin_item("porkchop")] +pub struct PorkchopItem; + +#[async_trait] +impl PumpkinFood for PorkchopItem {} + +#[pumpkin_item("rabbit")] +pub struct RabbitItem; + +#[async_trait] +impl PumpkinFood for RabbitItem {} + +#[pumpkin_item("salmon")] +pub struct SalmonItem; + +#[async_trait] +impl PumpkinFood for SalmonItem {} + +// Special Foods +#[pumpkin_item("golden_apple")] +pub struct GoldenAppleItem; + +#[async_trait] +impl PumpkinFood for GoldenAppleItem {} + +#[pumpkin_item("enchanted_golden_apple")] +pub struct EnchantedGoldenAppleItem; + +#[async_trait] +impl PumpkinFood for EnchantedGoldenAppleItem {} + +#[pumpkin_item("golden_carrot")] +pub struct GoldenCarrotItem; + +#[async_trait] +impl PumpkinFood for GoldenCarrotItem {} + +#[pumpkin_item("rotten_flesh")] +pub struct RottenFleshItem; + +#[async_trait] +impl PumpkinFood for RottenFleshItem {} + +#[pumpkin_item("spider_eye")] +pub struct SpiderEyeItem; + +#[async_trait] +impl PumpkinFood for SpiderEyeItem {} + +#[pumpkin_item("chorus_fruit")] +pub struct ChorusFruitItem; + +#[async_trait] +impl PumpkinFood for ChorusFruitItem { + // Chorus fruit has its teleport handled through the effects system +} + +#[pumpkin_item("suspicious_stew")] +pub struct SuspiciousStewItem; + +#[async_trait] +impl PumpkinFood for SuspiciousStewItem {} + +#[pumpkin_item("dried_kelp")] +pub struct DriedKelpItem; + +#[async_trait] +impl PumpkinFood for DriedKelpItem {} + +#[pumpkin_item("sweet_berries")] +pub struct SweetBerriesItem; + +#[async_trait] +impl PumpkinFood for SweetBerriesItem {} + +#[pumpkin_item("honey_bottle")] +pub struct HoneyBottleItem; + +#[async_trait] +impl PumpkinFood for HoneyBottleItem {} + +// Additional foods +#[pumpkin_item("cookie")] +pub struct CookieItem; + +#[async_trait] +impl PumpkinFood for CookieItem {} + +#[pumpkin_item("melon_slice")] +pub struct MelonSliceItem; + +#[async_trait] +impl PumpkinFood for MelonSliceItem {} + +#[pumpkin_item("beetroot")] +pub struct BeetrootItem; + +#[async_trait] +impl PumpkinFood for BeetrootItem {} + +#[pumpkin_item("beetroot_soup")] +pub struct BeetrootSoupItem; + +#[async_trait] +impl PumpkinFood for BeetrootSoupItem {} + +#[pumpkin_item("mushroom_stew")] +pub struct MushroomStewItem; + +#[async_trait] +impl PumpkinFood for MushroomStewItem {} + +#[pumpkin_item("rabbit_stew")] +pub struct RabbitStewItem; + +#[async_trait] +impl PumpkinFood for RabbitStewItem {} + +#[pumpkin_item("carrot")] +pub struct CarrotItem; + +#[async_trait] +impl PumpkinFood for CarrotItem {} + +#[pumpkin_item("potato")] +pub struct PotatoItem; + +#[async_trait] +impl PumpkinFood for PotatoItem {} + +#[pumpkin_item("baked_potato")] +pub struct BakedPotatoItem; + +#[async_trait] +impl PumpkinFood for BakedPotatoItem {} + +#[pumpkin_item("poisonous_potato")] +pub struct PoisonousPotatoItem; + +#[async_trait] +impl PumpkinFood for PoisonousPotatoItem {} + +#[pumpkin_item("pumpkin_pie")] +pub struct PumpkinPieItem; + +#[async_trait] +impl PumpkinFood for PumpkinPieItem {} + +#[pumpkin_item("tropical_fish")] +pub struct TropicalFishItem; + +#[async_trait] +impl PumpkinFood for TropicalFishItem {} + +#[pumpkin_item("pufferfish")] +pub struct PufferfishItem; + +#[async_trait] +impl PumpkinFood for PufferfishItem {} + +#[pumpkin_item("glow_berries")] +pub struct GlowBerriesItem; + +#[async_trait] +impl PumpkinFood for GlowBerriesItem {} diff --git a/pumpkin/src/item/items/mod.rs b/pumpkin/src/item/items/mod.rs index cca21c512..025b4ca21 100644 --- a/pumpkin/src/item/items/mod.rs +++ b/pumpkin/src/item/items/mod.rs @@ -1,2 +1,4 @@ pub mod egg; +pub mod food; +pub mod pumpkin_food; pub mod snowball; diff --git a/pumpkin/src/item/items/pumpkin_food.rs b/pumpkin/src/item/items/pumpkin_food.rs new file mode 100644 index 000000000..40f03d78b --- /dev/null +++ b/pumpkin/src/item/items/pumpkin_food.rs @@ -0,0 +1,71 @@ +use async_trait::async_trait; +use std::time::Duration; + +use crate::entity::player::Player; +use crate::item::pumpkin_item::PumpkinItem; +use crate::server::Server; +use pumpkin_data::item::Item; +use pumpkin_data::parse_registry_name; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::block::registry::Block; + +#[async_trait] +pub trait PumpkinFood: PumpkinItem { + async fn can_consume(&self, item: &Item, player: &Player) -> bool { + item.components + .food + .is_some_and(|food| food.can_always_eat || player.hunger_manager.level.load() < 20) + } + + fn get_eat_time(&self, item: &Item) -> Duration { + item.components.consumable.map_or_else( + || Duration::from_millis(1600), + |consumable| Duration::from_secs_f32(consumable.consume_seconds), + ) + } + + async fn begin_eating(&self, item: &Item, player: &Player) { + // Play eating sound + + let sound = item + .components + .consumable + .and_then(|consumable| Sound::from_name(&parse_registry_name(consumable.sound))); + + player + .world() + .await + .play_sound( + sound.map_or(Sound::EntityGenericEat, |sound| sound), + SoundCategory::Players, + &player.living_entity.entity.pos.load(), + ) + .await; + + // Set metadata to show eating animation + player.set_eating(item.id, self.get_eat_time(item)).await; + } +} + +#[async_trait] +impl PumpkinItem for T { + async fn normal_use(&self, item: &Item, player: &Player, _server: &Server) { + if self.can_consume(item, player).await { + // Start eating animation and set player state with timer + self.begin_eating(item, player).await; + } + } + + async fn use_on_block( + &self, + item: &Item, + player: &Player, + _location: BlockPos, + _block: &Block, + server: &Server, + ) { + // For most food items, using on a block is the same as normal use + self.normal_use(item, player, server).await; + } +} diff --git a/pumpkin/src/item/mod.rs b/pumpkin/src/item/mod.rs index db0f38e01..e331fb7c4 100644 --- a/pumpkin/src/item/mod.rs +++ b/pumpkin/src/item/mod.rs @@ -1,9 +1,19 @@ use items::{egg::EggItem, snowball::SnowBallItem}; use registry::ItemRegistry; +use crate::item::items::food::{ + AppleItem, BakedPotatoItem, BeefItem, BeetrootItem, BeetrootSoupItem, BreadItem, CarrotItem, + ChickenItem, ChorusFruitItem, CodItem, CookedBeefItem, CookedChickenItem, CookedCodItem, + CookedMuttonItem, CookedPorkchopItem, CookedRabbitItem, CookedSalmonItem, CookieItem, + DriedKelpItem, EnchantedGoldenAppleItem, GlowBerriesItem, GoldenAppleItem, GoldenCarrotItem, + HoneyBottleItem, MelonSliceItem, MushroomStewItem, MuttonItem, PoisonousPotatoItem, + PorkchopItem, PotatoItem, PufferfishItem, PumpkinPieItem, RabbitItem, RabbitStewItem, + RottenFleshItem, SalmonItem, SpiderEyeItem, SuspiciousStewItem, SweetBerriesItem, + TropicalFishItem, +}; use std::sync::Arc; -mod items; +pub mod items; pub mod pumpkin_item; pub mod registry; @@ -14,5 +24,48 @@ pub fn default_registry() -> Arc { manager.register(SnowBallItem); manager.register(EggItem); + // Register food items + manager.register(AppleItem); + manager.register(BreadItem); + manager.register(CookedBeefItem); + manager.register(CookedChickenItem); + manager.register(CookedChickenItem); + manager.register(CookedCodItem); + manager.register(CookedMuttonItem); + manager.register(CookedPorkchopItem); + manager.register(CookedRabbitItem); + manager.register(CookedSalmonItem); + manager.register(BeefItem); + manager.register(ChickenItem); + manager.register(CodItem); + manager.register(MuttonItem); + manager.register(PorkchopItem); + manager.register(RabbitItem); + manager.register(SalmonItem); + manager.register(GoldenAppleItem); + manager.register(EnchantedGoldenAppleItem); + manager.register(GoldenCarrotItem); + manager.register(RottenFleshItem); + manager.register(SpiderEyeItem); + manager.register(ChorusFruitItem); + manager.register(SuspiciousStewItem); + manager.register(DriedKelpItem); + manager.register(SweetBerriesItem); + manager.register(HoneyBottleItem); + manager.register(CookieItem); + manager.register(MelonSliceItem); + manager.register(BeetrootItem); + manager.register(BeetrootSoupItem); + manager.register(MushroomStewItem); + manager.register(RabbitStewItem); + manager.register(CarrotItem); + manager.register(PotatoItem); + manager.register(BakedPotatoItem); + manager.register(PoisonousPotatoItem); + manager.register(PumpkinPieItem); + manager.register(TropicalFishItem); + manager.register(PufferfishItem); + manager.register(GlowBerriesItem); + Arc::new(manager) } diff --git a/pumpkin/src/item/registry.rs b/pumpkin/src/item/registry.rs index 4d7394d8b..dadb29f6d 100644 --- a/pumpkin/src/item/registry.rs +++ b/pumpkin/src/item/registry.rs @@ -1,13 +1,13 @@ +use super::pumpkin_item::{ItemMetadata, PumpkinItem}; use crate::entity::player::Player; use crate::server::Server; use pumpkin_data::item::Item; +use pumpkin_data::parse_registry_name; use pumpkin_util::math::position::BlockPos; use pumpkin_world::block::registry::Block; use std::collections::HashMap; use std::sync::Arc; -use super::pumpkin_item::{ItemMetadata, PumpkinItem}; - #[derive(Default)] pub struct ItemRegistry { items: HashMap>, @@ -45,4 +45,12 @@ impl ItemRegistry { pub fn get_pumpkin_item(&self, item_id: u16) -> Option<&Arc> { self.items.get(&item_id) } + + pub fn get_item_from_id(&self, id: u16) -> Result { + Item::from_id(id).ok_or("Item not found") + } + + pub fn get_item_from_name(&self, name: &str) -> Result { + Item::from_name(&parse_registry_name(name)).ok_or("Item not found") + } }