From 6720ac265e61921656c199edb736f1d55972a190 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 9 Aug 2026 21:36:03 +0200 Subject: [PATCH] feat: add more items --- .github/CODEOWNERS | 8 +- .../src/data_component_impl/book.rs | 61 ++++- .../src/java/client/play/mod.rs | 5 + .../src/java/client/play/open_book.rs | 30 +++ .../src/java/client/play/waypoint.rs | 167 ++++++++++++ .../src/java/server/play/edit_book.rs | 35 +++ .../src/java/server/play/mod.rs | 2 + crates/pumpkin/src/block/blocks/cauldron.rs | 182 ++++++++++++- crates/pumpkin/src/block/blocks/lectern.rs | 8 +- .../pumpkin/src/command/commands/waypoint.rs | 71 ++++- crates/pumpkin/src/entity/item_steerable.rs | 57 ++++ crates/pumpkin/src/entity/living.rs | 72 +++-- crates/pumpkin/src/entity/mob/mod.rs | 22 ++ crates/pumpkin/src/entity/mod.rs | 5 + crates/pumpkin/src/entity/passive/pig.rs | 55 +++- crates/pumpkin/src/entity/passive/sheep.rs | 4 + crates/pumpkin/src/entity/passive/strider.rs | 84 +++++- crates/pumpkin/src/item/items/bone_meal.rs | 172 ++++++++++++ crates/pumpkin/src/item/items/brush.rs | 240 +++++++++++++++++ crates/pumpkin/src/item/items/bucket.rs | 46 +++- crates/pumpkin/src/item/items/clock.rs | 37 +++ crates/pumpkin/src/item/items/compass.rs | 71 +++++ .../src/item/items/experience_bottle.rs | 46 ++++ crates/pumpkin/src/item/items/glass_bottle.rs | 107 ++++++++ crates/pumpkin/src/item/items/goat_horn.rs | 49 ++++ crates/pumpkin/src/item/items/mod.rs | 46 +++- crates/pumpkin/src/item/items/on_a_stick.rs | 67 +++++ crates/pumpkin/src/item/items/saddle.rs | 51 ++++ crates/pumpkin/src/item/items/shears.rs | 246 ++++++++++++++++++ crates/pumpkin/src/item/items/spyglass.rs | 63 +++++ .../pumpkin/src/item/items/writable_book.rs | 44 ++++ crates/pumpkin/src/net/java/mod.rs | 6 +- crates/pumpkin/src/net/java/play.rs | 51 +++- 33 files changed, 2138 insertions(+), 72 deletions(-) create mode 100644 crates/pumpkin-protocol/src/java/client/play/open_book.rs create mode 100644 crates/pumpkin-protocol/src/java/client/play/waypoint.rs create mode 100644 crates/pumpkin-protocol/src/java/server/play/edit_book.rs create mode 100644 crates/pumpkin/src/entity/item_steerable.rs create mode 100644 crates/pumpkin/src/item/items/bone_meal.rs create mode 100644 crates/pumpkin/src/item/items/brush.rs create mode 100644 crates/pumpkin/src/item/items/clock.rs create mode 100644 crates/pumpkin/src/item/items/compass.rs create mode 100644 crates/pumpkin/src/item/items/experience_bottle.rs create mode 100644 crates/pumpkin/src/item/items/glass_bottle.rs create mode 100644 crates/pumpkin/src/item/items/goat_horn.rs create mode 100644 crates/pumpkin/src/item/items/on_a_stick.rs create mode 100644 crates/pumpkin/src/item/items/saddle.rs create mode 100644 crates/pumpkin/src/item/items/shears.rs create mode 100644 crates/pumpkin/src/item/items/spyglass.rs create mode 100644 crates/pumpkin/src/item/items/writable_book.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0da6ecd21..316cd472b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,16 +10,16 @@ crates/pumpkin/src/net/ @Snowiiii @xRookieFight # Bedrock related changes assets/bedrock/ @ZlordHUN @xRookieFight -crates/pumpkin-protocol/src/bedrock/ @ZlordHUN @xRookieFight -crates/pumpkin/src/net/bedrock/ @ZlordHUN @xRookieFight +crates/pumpkin-protocol/src/bedrock/ @ZlordHUN @xRookieFight @QuantumSegfault +crates/pumpkin/src/net/bedrock/ @ZlordHUN @xRookieFight @QuantumSegfault # Command related changes crates/pumpkin/src/command/ @aure31 # Plugin related changes & WASM Host -crates/pumpkin-plugin-api/ @vyPal @yunuservices @QuantumSegfault +crates/pumpkin-plugin-api/ @vyPal @yunuservices crates/pumpkin-plugin-wit/ @vyPal @yunuservices @QuantumSegfault -crates/pumpkin-api-macros/ @vyPal @yunuservices @QuantumSegfault +crates/pumpkin-api-macros/ @vyPal @yunuservices crates/pumpkin/src/plugin/ @vyPal @yunuservices @QuantumSegfault # Documentation related changes diff --git a/crates/pumpkin-data/src/data_component_impl/book.rs b/crates/pumpkin-data/src/data_component_impl/book.rs index caffc9015..13af2b414 100644 --- a/crates/pumpkin-data/src/data_component_impl/book.rs +++ b/crates/pumpkin-data/src/data_component_impl/book.rs @@ -1,4 +1,5 @@ use crate::data_component_impl::DataComponentImpl; +use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::tag::NbtTag; #[derive(Clone, Debug, Hash, PartialEq, Eq)] @@ -11,35 +12,79 @@ impl WritableBookContentImpl { if let NbtTag::Compound(c) = tag && let Some(NbtTag::List(l)) = c.get("pages") { - for _ in l { - pages.push(String::new()); + for item in l { + if let NbtTag::String(s) = item { + pages.push(s.to_string()); + } else if let NbtTag::Compound(comp) = item { + if let Some(s) = comp.get_string("raw") { + pages.push(s.to_string()); + } + } } } Some(Self { pages }) } } impl DataComponentImpl for WritableBookContentImpl { + fn write_data(&self) -> NbtTag { + let mut compound = NbtCompound::new(); + let pages_tags: Vec = self + .pages + .iter() + .map(|p| NbtTag::String(p.clone().into_boxed_str())) + .collect(); + compound.put("pages", NbtTag::List(pages_tags)); + NbtTag::Compound(compound) + } default_impl!(WritableBookContent); } #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct WrittenBookContentImpl { + pub title: String, + pub author: String, pub pages: Vec, } impl WrittenBookContentImpl { pub fn read_data(tag: &NbtTag) -> Option { let mut pages = Vec::new(); - if let NbtTag::Compound(c) = tag - && let Some(NbtTag::List(l)) = c.get("pages") - { - for _ in l { - pages.push(String::new()); + let mut title = String::new(); + let mut author = String::new(); + if let NbtTag::Compound(c) = tag { + if let Some(s) = c.get_string("title") { + title = s.to_string(); + } + if let Some(s) = c.get_string("author") { + author = s.to_string(); + } + if let Some(NbtTag::List(l)) = c.get("pages") { + for item in l { + if let NbtTag::String(s) = item { + pages.push(s.to_string()); + } + } } } - Some(Self { pages }) + Some(Self { + title, + author, + pages, + }) } } impl DataComponentImpl for WrittenBookContentImpl { + fn write_data(&self) -> NbtTag { + let mut compound = NbtCompound::new(); + compound.put_string("title", self.title.clone()); + compound.put_string("author", self.author.clone()); + let pages_tags: Vec = self + .pages + .iter() + .map(|p| NbtTag::String(p.clone().into_boxed_str())) + .collect(); + compound.put("pages", NbtTag::List(pages_tags)); + NbtTag::Compound(compound) + } default_impl!(WrittenBookContent); } diff --git a/crates/pumpkin-protocol/src/java/client/play/mod.rs b/crates/pumpkin-protocol/src/java/client/play/mod.rs index bceb746a8..3c900e340 100644 --- a/crates/pumpkin-protocol/src/java/client/play/mod.rs +++ b/crates/pumpkin-protocol/src/java/client/play/mod.rs @@ -44,6 +44,7 @@ mod login; mod map_item_data; mod merchant_offers; mod multi_block_update; +mod open_book; mod open_screen; mod open_sign_editor; mod particle; @@ -152,6 +153,7 @@ pub use login::*; pub use map_item_data::*; pub use merchant_offers::*; pub use multi_block_update::*; +pub use open_book::*; pub use open_screen::*; pub use open_sign_editor::*; pub use particle::*; @@ -219,5 +221,8 @@ pub use update_objectives::*; pub use update_score::*; pub use worldevent::*; +mod waypoint; +pub use waypoint::*; + mod debug_sample; pub use debug_sample::*; diff --git a/crates/pumpkin-protocol/src/java/client/play/open_book.rs b/crates/pumpkin-protocol/src/java/client/play/open_book.rs new file mode 100644 index 000000000..e0c39c60a --- /dev/null +++ b/crates/pumpkin-protocol/src/java/client/play/open_book.rs @@ -0,0 +1,30 @@ +use pumpkin_data::packet::clientbound::PLAY_OPEN_BOOK; +use pumpkin_macros::java_packet; +use pumpkin_util::version::JavaMinecraftVersion; + +use crate::ser::NetworkWriteExt; +use crate::{ClientPacket, VarInt}; + +#[derive(Clone, Copy, Debug)] +#[java_packet(PLAY_OPEN_BOOK)] +pub struct COpenBook { + pub hand: VarInt, +} + +impl COpenBook { + #[must_use] + pub const fn new(hand: VarInt) -> Self { + Self { hand } + } +} + +impl ClientPacket for COpenBook { + fn write_packet_data( + &self, + mut write: impl std::io::Write, + _version: &JavaMinecraftVersion, + ) -> Result<(), crate::ser::WritingError> { + write.write_var_int(&self.hand)?; + Ok(()) + } +} diff --git a/crates/pumpkin-protocol/src/java/client/play/waypoint.rs b/crates/pumpkin-protocol/src/java/client/play/waypoint.rs new file mode 100644 index 000000000..b7e6651fa --- /dev/null +++ b/crates/pumpkin-protocol/src/java/client/play/waypoint.rs @@ -0,0 +1,167 @@ +use std::io::Write; + +use crate::ser::NetworkWriteExt; +use crate::{ClientPacket, VarInt, WritingError}; +use pumpkin_data::packet::clientbound::PLAY_WAYPOINT; +use pumpkin_macros::java_packet; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::version::JavaMinecraftVersion; +use uuid::Uuid; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(i32)] +pub enum WaypointOperation { + Track = 0, + Untrack = 1, + Update = 2, +} + +#[derive(Clone, Debug)] +pub enum WaypointTarget { + Position(BlockPos), + Chunk { x: i32, z: i32 }, + Azimuth(f32), + Empty, +} + +#[derive(Clone, Debug)] +pub struct WaypointIcon<'a> { + pub style: Option<&'a str>, + pub color: i32, +} + +#[derive(Clone, Debug)] +pub struct TrackedWaypoint<'a> { + pub identifier: Uuid, + pub icon: Option>, + pub target: WaypointTarget, +} + +impl TrackedWaypoint<'_> { + #[must_use] + pub const fn empty(identifier: Uuid) -> Self { + Self { + identifier, + icon: None, + target: WaypointTarget::Empty, + } + } + + #[must_use] + pub const fn set_position( + identifier: Uuid, + icon: Option>, + position: BlockPos, + ) -> TrackedWaypoint<'_> { + TrackedWaypoint { + identifier, + icon, + target: WaypointTarget::Position(position), + } + } +} + +/// Syncs tracked waypoints (`ClientboundTrackedWaypointPacket`) to client. +#[java_packet(PLAY_WAYPOINT)] +pub struct CWaypoint<'a> { + pub operation: WaypointOperation, + pub waypoint: TrackedWaypoint<'a>, +} + +impl<'a> CWaypoint<'a> { + #[must_use] + pub const fn new(operation: WaypointOperation, waypoint: TrackedWaypoint<'a>) -> Self { + Self { + operation, + waypoint, + } + } + + #[must_use] + pub const fn remove(identifier: Uuid) -> Self { + Self { + operation: WaypointOperation::Untrack, + waypoint: TrackedWaypoint::empty(identifier), + } + } + + #[must_use] + pub const fn add_position( + identifier: Uuid, + icon: Option>, + position: BlockPos, + ) -> Self { + Self { + operation: WaypointOperation::Track, + waypoint: TrackedWaypoint::set_position(identifier, icon, position), + } + } + + #[must_use] + pub const fn update_position( + identifier: Uuid, + icon: Option>, + position: BlockPos, + ) -> Self { + Self { + operation: WaypointOperation::Update, + waypoint: TrackedWaypoint::set_position(identifier, icon, position), + } + } +} + +impl ClientPacket for CWaypoint<'_> { + fn write_packet_data( + &self, + mut write: impl Write, + _version: &JavaMinecraftVersion, + ) -> Result<(), WritingError> { + // 1. Operation (TRACK = 0, UNTRACK = 1, UPDATE = 2) + write.write_var_int(&VarInt(self.operation as i32))?; + + // 2. TrackedWaypoint Identifier (UUID) + write.write_uuid(&self.waypoint.identifier)?; + + // 3. Waypoint Icon (Optional) + if let Some(ref icon) = self.waypoint.icon { + write.write_bool(true)?; + if let Some(style_str) = icon.style { + write.write_bool(true)?; + // Format style as a valid ResourceLocation (e.g., "minecraft:red" or "red") + if style_str.contains(':') { + write.write_string(style_str)?; + } else { + let formatted = format!("minecraft:{style_str}"); + write.write_string(&formatted)?; + } + } else { + write.write_bool(false)?; + } + write.write_i32_be(icon.color)?; + } else { + write.write_bool(false)?; + } + + // 4. Waypoint Target Payload + match &self.waypoint.target { + WaypointTarget::Position(pos) => { + write.write_var_int(&VarInt(0))?; + write.write_block_pos(pos)?; + } + WaypointTarget::Chunk { x, z } => { + write.write_var_int(&VarInt(1))?; + write.write_i32_be(*x)?; + write.write_i32_be(*z)?; + } + WaypointTarget::Azimuth(angle) => { + write.write_var_int(&VarInt(2))?; + write.write_f32_be(*angle)?; + } + WaypointTarget::Empty => { + write.write_var_int(&VarInt(3))?; + } + } + + Ok(()) + } +} diff --git a/crates/pumpkin-protocol/src/java/server/play/edit_book.rs b/crates/pumpkin-protocol/src/java/server/play/edit_book.rs new file mode 100644 index 000000000..8ff8ec4d2 --- /dev/null +++ b/crates/pumpkin-protocol/src/java/server/play/edit_book.rs @@ -0,0 +1,35 @@ +use pumpkin_data::packet::serverbound::PLAY_EDIT_BOOK; +use pumpkin_macros::java_packet; +use pumpkin_util::version::JavaMinecraftVersion; + +use crate::{ + ServerPacket, VarInt, + ser::{NetworkReadExt, NetworkReadSliceExt, ReadingError}, +}; + +#[derive(Debug)] +#[java_packet(PLAY_EDIT_BOOK)] +pub struct SEditBook<'a> { + pub slot: VarInt, + pub pages: Vec<&'a str>, + pub title: Option<&'a str>, +} + +impl<'a> ServerPacket<'a> for SEditBook<'a> { + fn read(read: &mut &'a [u8], _version: &JavaMinecraftVersion) -> Result { + let slot = read.get_var_int()?; + let count = read.get_var_int()?.0 as usize; + let count = count.min(100); + let mut pages = Vec::with_capacity(count); + for _ in 0..count { + pages.push(read.get_str_bounded_borrowed(1024)?); + } + let has_title = read.get_bool()?; + let title = if has_title { + Some(read.get_str_bounded_borrowed(128)?) + } else { + None + }; + Ok(Self { slot, pages, title }) + } +} diff --git a/crates/pumpkin-protocol/src/java/server/play/mod.rs b/crates/pumpkin-protocol/src/java/server/play/mod.rs index 866dba9f8..70540740c 100644 --- a/crates/pumpkin-protocol/src/java/server/play/mod.rs +++ b/crates/pumpkin-protocol/src/java/server/play/mod.rs @@ -15,6 +15,7 @@ mod container_button_click; mod cookie_response; mod custom_click_action; mod custom_payload; +mod edit_book; mod interact; mod jigsaw_generate; mod keep_alive; @@ -66,6 +67,7 @@ pub use container_button_click::*; pub use cookie_response::*; pub use custom_click_action::*; pub use custom_payload::*; +pub use edit_book::*; pub use interact::*; pub use jigsaw_generate::*; pub use keep_alive::*; diff --git a/crates/pumpkin/src/block/blocks/cauldron.rs b/crates/pumpkin/src/block/blocks/cauldron.rs index 96b69fb73..592a9f5eb 100644 --- a/crates/pumpkin/src/block/blocks/cauldron.rs +++ b/crates/pumpkin/src/block/blocks/cauldron.rs @@ -1,6 +1,14 @@ -use crate::block::{BlockBehaviour, BlockFuture, BlockMetadata, GetComparatorOutputArgs}; +use crate::block::registry::BlockActionResult; +use crate::block::{ + BlockBehaviour, BlockFuture, BlockMetadata, GetComparatorOutputArgs, UseWithItemArgs, +}; +use pumpkin_data::Block; use pumpkin_data::BlockId; use pumpkin_data::block_properties::{BlockProperties, WaterCauldronLikeProperties}; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_world::world::BlockFlags; pub struct CauldronBlock; @@ -17,6 +25,178 @@ impl BlockMetadata for CauldronBlock { } impl BlockBehaviour for CauldronBlock { + #[allow(clippy::too_many_lines)] + fn use_with_item<'a>( + &'a self, + args: UseWithItemArgs<'a>, + ) -> BlockFuture<'a, BlockActionResult> { + Box::pin(async move { + let item_id = args.item_stack.item.id; + let block_id = args.block.id; + let gamemode = args.player.gamemode.load(); + + // Filling empty cauldron with buckets + if block_id == BlockId::CAULDRON { + if item_id == Item::WATER_BUCKET.id { + let state_id = Block::WATER_CAULDRON + .from_properties(&[("level", "3")]) + .to_state_id(&Block::WATER_CAULDRON); + args.world + .set_block_state(args.position, state_id, BlockFlags::NOTIFY_ALL) + .await; + args.world.play_sound( + Sound::ItemBucketEmpty, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + args.item_stack.decrement_unless_creative(gamemode, 1); + args.player + .inventory + .offer_or_drop_stack(ItemStack::new(1, &Item::BUCKET), args.player.as_ref()) + .await; + return BlockActionResult::Success; + } else if item_id == Item::LAVA_BUCKET.id { + args.world + .set_block_state( + args.position, + Block::LAVA_CAULDRON.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + args.world.play_sound( + Sound::ItemBucketEmptyLava, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + args.item_stack.decrement_unless_creative(gamemode, 1); + args.player + .inventory + .offer_or_drop_stack(ItemStack::new(1, &Item::BUCKET), args.player.as_ref()) + .await; + return BlockActionResult::Success; + } else if item_id == Item::POWDER_SNOW_BUCKET.id { + let state_id = Block::POWDER_SNOW_CAULDRON + .from_properties(&[("level", "3")]) + .to_state_id(&Block::POWDER_SNOW_CAULDRON); + args.world + .set_block_state(args.position, state_id, BlockFlags::NOTIFY_ALL) + .await; + args.world.play_sound( + Sound::ItemBucketEmptyPowderSnow, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + args.item_stack.decrement_unless_creative(gamemode, 1); + args.player + .inventory + .offer_or_drop_stack(ItemStack::new(1, &Item::BUCKET), args.player.as_ref()) + .await; + return BlockActionResult::Success; + } else if item_id == Item::POTION.id { + let state_id = Block::WATER_CAULDRON + .from_properties(&[("level", "1")]) + .to_state_id(&Block::WATER_CAULDRON); + args.world + .set_block_state(args.position, state_id, BlockFlags::NOTIFY_ALL) + .await; + args.world.play_sound( + Sound::ItemBottleEmpty, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + args.item_stack.decrement_unless_creative(gamemode, 1); + args.player + .inventory + .offer_or_drop_stack( + ItemStack::new(1, &Item::GLASS_BOTTLE), + args.player.as_ref(), + ) + .await; + return BlockActionResult::Success; + } + } + + // Collecting fluid from full cauldrons into empty bucket + if item_id == Item::BUCKET.id { + let state_id = args.world.get_block_state_id(args.position); + let (filled_item, sound) = if block_id == BlockId::WATER_CAULDRON { + let props = WaterCauldronLikeProperties::from_state_id(state_id, args.block); + if props.level == 3 { + (Some(&Item::WATER_BUCKET), Sound::ItemBucketFill) + } else { + (None, Sound::ItemBucketFill) + } + } else if block_id == BlockId::LAVA_CAULDRON { + (Some(&Item::LAVA_BUCKET), Sound::ItemBucketFillLava) + } else if block_id == BlockId::POWDER_SNOW_CAULDRON { + let props = WaterCauldronLikeProperties::from_state_id(state_id, args.block); + if props.level == 3 { + ( + Some(&Item::POWDER_SNOW_BUCKET), + Sound::ItemBucketFillPowderSnow, + ) + } else { + (None, Sound::ItemBucketFillPowderSnow) + } + } else { + (None, Sound::ItemBucketFill) + }; + + if let Some(result_item) = filled_item { + args.world + .set_block_state( + args.position, + Block::CAULDRON.default_state.id, + BlockFlags::NOTIFY_ALL, + ) + .await; + args.world + .play_sound(sound, SoundCategory::Blocks, &args.position.to_f64()); + args.item_stack.decrement_unless_creative(gamemode, 1); + args.player + .inventory + .offer_or_drop_stack(ItemStack::new(1, result_item), args.player.as_ref()) + .await; + return BlockActionResult::Success; + } + } + + // Adding water bottle to non-full water cauldron + if block_id == BlockId::WATER_CAULDRON && item_id == Item::POTION.id { + let state_id = args.world.get_block_state_id(args.position); + let props = WaterCauldronLikeProperties::from_state_id(state_id, args.block); + if props.level < 3 { + let next_level_str = match props.level { + 1 => "2", + _ => "3", + }; + let new_state_id = Block::WATER_CAULDRON + .from_properties(&[("level", next_level_str)]) + .to_state_id(&Block::WATER_CAULDRON); + args.world + .set_block_state(args.position, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + args.world.play_sound( + Sound::ItemBottleEmpty, + SoundCategory::Blocks, + &args.position.to_f64(), + ); + args.item_stack.decrement_unless_creative(gamemode, 1); + args.player + .inventory + .offer_or_drop_stack( + ItemStack::new(1, &Item::GLASS_BOTTLE), + args.player.as_ref(), + ) + .await; + return BlockActionResult::Success; + } + } + + BlockActionResult::PassToDefaultBlockAction + }) + } + fn get_comparator_output<'a>( &'a self, args: GetComparatorOutputArgs<'a>, diff --git a/crates/pumpkin/src/block/blocks/lectern.rs b/crates/pumpkin/src/block/blocks/lectern.rs index a576b1b0d..685097914 100644 --- a/crates/pumpkin/src/block/blocks/lectern.rs +++ b/crates/pumpkin/src/block/blocks/lectern.rs @@ -227,7 +227,7 @@ impl BlockBehaviour for LecternBlock { Box::pin(async move { let item_stack = &mut *args.item_stack; if !item_stack.item.has_tag(&tag::Item::MINECRAFT_LECTERN_BOOKS) { - return BlockActionResult::Pass; + return BlockActionResult::PassToDefaultBlockAction; } let props = LecternLikeProperties::from_state_id( @@ -236,14 +236,14 @@ impl BlockBehaviour for LecternBlock { ); if props.has_book { // Fall through so `normal_use` opens the reading screen. - return BlockActionResult::Pass; + return BlockActionResult::PassToDefaultBlockAction; } let Some(lectern) = args.world.get_block_entity(args.position) else { - return BlockActionResult::Pass; + return BlockActionResult::PassToDefaultBlockAction; }; let Some(lectern) = lectern.as_any().downcast_ref::() else { - return BlockActionResult::Pass; + return BlockActionResult::PassToDefaultBlockAction; }; let book = item_stack.split_unless_creative(args.player.gamemode.load(), 1); diff --git a/crates/pumpkin/src/command/commands/waypoint.rs b/crates/pumpkin/src/command/commands/waypoint.rs index 9b3382a22..c80d737c6 100644 --- a/crates/pumpkin/src/command/commands/waypoint.rs +++ b/crates/pumpkin/src/command/commands/waypoint.rs @@ -1,13 +1,16 @@ use pumpkin_data::translation; +use pumpkin_protocol::java::client::play::{CWaypoint, WaypointIcon}; +use pumpkin_util::math::position::BlockPos; use pumpkin_util::text::TextComponent; -use crate::command::CommandResult; use crate::command::args::{ FindArg, entity::EntityArgumentConsumer, hex_color::HexColorArgumentConsumer, resource_location::ResourceLocationArgumentConsumer, team_color::TeamColorArgumentConsumer, }; use crate::command::tree::builder::{argument, literal}; -use crate::command::{CommandExecutor, CommandSender, ConsumedArgs, tree::CommandTree}; +use crate::command::{ + CommandExecutor, CommandResult, CommandSender, ConsumedArgs, tree::CommandTree, +}; const NAMES: [&str; 1] = ["waypoint"]; const DESCRIPTION: &str = "List or modify waypoints."; @@ -31,7 +34,6 @@ impl CommandExecutor for ListExecutor { .expect("There should always be at least one world"); let dimension = world.dimension.minecraft_name.to_string(); - // Currently no active waypoints are tracked in the level sender .send_message(TextComponent::translate( translation::java::COMMANDS_WAYPOINT_LIST_EMPTY, @@ -59,7 +61,38 @@ impl CommandExecutor for ColorExecutor { args: &'a ConsumedArgs<'a>, ) -> CommandResult<'a> { Box::pin(async move { - let _waypoint_entity = EntityArgumentConsumer::find_arg(args, ARG_WAYPOINT)?; + let waypoint_entity = EntityArgumentConsumer::find_arg(args, ARG_WAYPOINT)?; + let entity = waypoint_entity.get_entity(); + let pos = entity.pos.load(); + let block_pos = BlockPos::new( + pos.x.floor() as i32, + pos.y.floor() as i32, + pos.z.floor() as i32, + ); + let uuid = entity.entity_uuid; + + let color_val = match self.0 { + ColorAction::Named => { + let color = TeamColorArgumentConsumer::find_arg(args, ARG_COLOR)?; + let rgb = color.to_rgb(); + i32::from_be_bytes([0, rgb.red, rgb.green, rgb.blue]) + } + ColorAction::Hex => HexColorArgumentConsumer::find_arg(args, ARG_COLOR)? as i32, + ColorAction::Reset => 0xFFFFFF, + }; + + let packet = CWaypoint::update_position( + uuid, + Some(WaypointIcon { + style: None, + color: color_val, + }), + block_pos, + ); + + if let Some(player) = sender.as_player() { + player.client.enqueue_packet(&packet).await; + } match self.0 { ColorAction::Named => { @@ -111,13 +144,35 @@ impl CommandExecutor for StyleExecutor { args: &'a ConsumedArgs<'a>, ) -> CommandResult<'a> { Box::pin(async move { - let _waypoint_entity = EntityArgumentConsumer::find_arg(args, ARG_WAYPOINT)?; + let waypoint_entity = EntityArgumentConsumer::find_arg(args, ARG_WAYPOINT)?; + let entity = waypoint_entity.get_entity(); + let pos = entity.pos.load(); + let block_pos = BlockPos::new( + pos.x.floor() as i32, + pos.y.floor() as i32, + pos.z.floor() as i32, + ); + let uuid = entity.entity_uuid; - match self.0 { + let style_str = match self.0 { StyleAction::Set => { - let _style = ResourceLocationArgumentConsumer::find_arg(args, ARG_STYLE)?; + let style = ResourceLocationArgumentConsumer::find_arg(args, ARG_STYLE)?; + Some(style) } - StyleAction::Reset => {} + StyleAction::Reset => None, + }; + + let packet = CWaypoint::update_position( + uuid, + Some(WaypointIcon { + style: style_str, + color: 0xFFFFFF, + }), + block_pos, + ); + + if let Some(player) = sender.as_player() { + player.client.enqueue_packet(&packet).await; } sender diff --git a/crates/pumpkin/src/entity/item_steerable.rs b/crates/pumpkin/src/entity/item_steerable.rs new file mode 100644 index 000000000..3e1e8e218 --- /dev/null +++ b/crates/pumpkin/src/entity/item_steerable.rs @@ -0,0 +1,57 @@ +use std::any::Any; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; + +pub trait ItemSteerable: Send + Sync { + /// Attempts to boost speed. Returns `true` if boost was successfully activated. + fn boost(&self) -> bool; + + fn as_any(&self) -> &dyn Any; +} + +#[derive(Default)] +pub struct ItemBasedSteering { + pub boosting: AtomicBool, + pub boost_time: AtomicI32, + pub boost_time_total: AtomicI32, +} + +impl ItemBasedSteering { + pub const MIN_BOOST_TIME: i32 = 140; + pub const MAX_BOOST_TIME: i32 = 700; + + #[must_use] + pub fn boost(&self) -> bool { + if self.boosting.load(Ordering::Relaxed) { + false + } else { + self.boosting.store(true, Ordering::Relaxed); + self.boost_time.store(0, Ordering::Relaxed); + let random_range = Self::MAX_BOOST_TIME - Self::MIN_BOOST_TIME + 1; + let random_time = + (rand::random::() as i32).abs() % random_range + Self::MIN_BOOST_TIME; + self.boost_time_total.store(random_time, Ordering::Relaxed); + true + } + } + + pub fn tick_boost(&self) { + if self.boosting.load(Ordering::Relaxed) { + let current = self.boost_time.fetch_add(1, Ordering::Relaxed) + 1; + let total = self.boost_time_total.load(Ordering::Relaxed); + if current >= total { + self.boosting.store(false, Ordering::Relaxed); + } + } + } + + #[must_use] + pub fn boost_factor(&self) -> f32 { + if self.boosting.load(Ordering::Relaxed) { + let current = self.boost_time.load(Ordering::Relaxed) as f32; + let total = self.boost_time_total.load(Ordering::Relaxed).max(1) as f32; + 1.0 + 1.15 * (current / total * std::f32::consts::PI).sin() + } else { + 1.0 + } + } +} diff --git a/crates/pumpkin/src/entity/living.rs b/crates/pumpkin/src/entity/living.rs index edbaccfbe..a8bb8f023 100644 --- a/crates/pumpkin/src/entity/living.rs +++ b/crates/pumpkin/src/entity/living.rs @@ -2825,31 +2825,57 @@ impl LivingEntity { }; for consume_effect in consumable.effects.iter() { - let ConsumeEffect::ApplyEffects((effects, probability)) = consume_effect else { - continue; - }; - if !consume_effect_probability_applies(*probability, rand::random()) { - continue; - } + match consume_effect { + ConsumeEffect::ApplyEffects((effects, probability)) => { + if !consume_effect_probability_applies(*probability, rand::random()) { + continue; + } - for effect in effects.iter() { - let Some(effect_type) = StatusEffect::from_minecraft_name(&effect.effect_id) else { - continue; - }; - let Ok(amplifier) = u8::try_from(effect.amplifier) else { - continue; - }; + for effect in effects.iter() { + let Some(effect_type) = + StatusEffect::from_minecraft_name(&effect.effect_id) + else { + continue; + }; + let Ok(amplifier) = u8::try_from(effect.amplifier) else { + continue; + }; - self.add_effect(Effect { - effect_type, - duration: effect.duration, - amplifier, - ambient: effect.ambient, - show_particles: effect.show_particles, - show_icon: effect.show_icon, - blend: false, - }) - .await; + self.add_effect(Effect { + effect_type, + duration: effect.duration, + amplifier, + ambient: effect.ambient, + show_particles: effect.show_particles, + show_icon: effect.show_icon, + blend: false, + }) + .await; + } + } + ConsumeEffect::ClearAllEffects => { + self.reset_effects_and_attributes().await; + } + ConsumeEffect::RemoveEffects(idset) => { + if let pumpkin_data::data_component_impl::IDSet::IDs(ids) = idset { + for effect_type in ids.iter() { + self.remove_effect(effect_type).await; + } + } + } + ConsumeEffect::TeleportRandomly(diameter) => { + let center = self.entity.pos.load(); + let radius = f64::from(*diameter) / 2.0; + let target_x = center.x + (rand::random::() - 0.5) * radius * 2.0; + let target_y = center.y + (rand::random::() - 0.5) * radius; + let target_z = center.z + (rand::random::() - 0.5) * radius * 2.0; + let pos = + pumpkin_util::math::vector3::Vector3::new(target_x, target_y, target_z); + let (yaw, pitch) = (self.entity.yaw.load(), self.entity.pitch.load()); + let world = self.entity.world.load().clone(); + self.entity.teleport(pos, Some(yaw), Some(pitch), world); + } + ConsumeEffect::PlaySound(_) => {} } } } diff --git a/crates/pumpkin/src/entity/mob/mod.rs b/crates/pumpkin/src/entity/mob/mod.rs index 137870be8..a31b1fb18 100644 --- a/crates/pumpkin/src/entity/mob/mod.rs +++ b/crates/pumpkin/src/entity/mob/mod.rs @@ -508,6 +508,20 @@ pub trait Mob: EntityBase + Send + Sync { None } + fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> { + None + } + + fn is_saddled(&self) -> bool { + false + } + + fn can_be_saddled(&self) -> bool { + false + } + + fn set_saddled(&self, _saddled: bool) {} + /// Per-mob tick hook called each tick before AI runs. Override for mob-specific logic. fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { Box::pin(async {}) @@ -605,12 +619,20 @@ pub trait Mob: EntityBase + Send + Sync { } fn mob_set_variant_name(&self, _name: &str) {} + + fn get_sheep(&self) -> Option<&crate::entity::passive::sheep::SheepEntity> { + None + } } impl EntityBase for T { fn get_mob(&self) -> Option<&dyn Mob> { Some(self) } + fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> { + Mob::get_item_steerable(self) + } + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async move { self.mob_init_data_tracker().await; diff --git a/crates/pumpkin/src/entity/mod.rs b/crates/pumpkin/src/entity/mod.rs index 00f78ec11..3ffe624f8 100644 --- a/crates/pumpkin/src/entity/mod.rs +++ b/crates/pumpkin/src/entity/mod.rs @@ -92,6 +92,7 @@ pub mod experience_orb; pub mod falling; pub mod hunger; pub mod item; +pub mod item_steerable; pub mod living; pub mod mob; pub mod passive; @@ -165,6 +166,10 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any { self } + fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> { + None + } + fn get_eye_pos(&self) -> Vector3 { self.get_entity().get_eye_pos() } diff --git a/crates/pumpkin/src/entity/passive/pig.rs b/crates/pumpkin/src/entity/passive/pig.rs index 45a73804d..3ea630646 100644 --- a/crates/pumpkin/src/entity/passive/pig.rs +++ b/crates/pumpkin/src/entity/passive/pig.rs @@ -25,12 +25,17 @@ const PIG_FOOD: &[&Item] = &[ &Item::CARROT_ON_A_STICK, ]; +use crate::entity::EntityBase; +use crate::entity::item_steerable::{ItemBasedSteering, ItemSteerable}; + /// Represents a Pig, a common passive mob that provides porkchops. /// /// Wiki: pub struct PigEntity { pub mob_entity: MobEntity, pub ageable_data: crate::entity::ageable::AgeableData, + pub steering: ItemBasedSteering, + pub saddled: std::sync::atomic::AtomicBool, } impl PigEntity { @@ -39,6 +44,8 @@ impl PigEntity { let pig = Self { mob_entity, ageable_data: crate::entity::ageable::AgeableData::default(), + steering: ItemBasedSteering::default(), + saddled: std::sync::atomic::AtomicBool::new(false), }; let mob_arc = Arc::new(pig); let mob_weak: Weak = { @@ -82,6 +89,7 @@ impl NBTStorage for PigEntity { self.mob_entity.living_entity.write_nbt(nbt).await; self.write_ageable_nbt(nbt); self.write_animal_nbt(nbt); + nbt.put_bool("Saddle", self.is_saddled()); }) } @@ -90,6 +98,9 @@ impl NBTStorage for PigEntity { self.mob_entity.living_entity.read_nbt_non_mut(nbt).await; self.read_ageable_nbt(nbt); self.read_animal_nbt(nbt); + if let Some(saddle) = nbt.get_byte("Saddle") { + self.set_saddled(saddle == 1); + } }) } } @@ -105,12 +116,54 @@ impl Mob for PigEntity { &self.mob_entity } + fn get_item_steerable(&self) -> Option<&dyn ItemSteerable> { + Some(self) + } + + fn is_saddled(&self) -> bool { + self.saddled.load(std::sync::atomic::Ordering::Relaxed) + } + + fn can_be_saddled(&self) -> bool { + use crate::entity::ageable::AgeableMob; + self.mob_entity.living_entity.entity.is_alive() && !self.is_baby() + } + + fn set_saddled(&self, saddled: bool) { + self.saddled + .store(saddled, std::sync::atomic::Ordering::Relaxed); + } + fn mob_interact<'a>( &'a self, player: &'a Arc, item_stack: &'a mut ItemStack, ) -> EntityBaseFuture<'a, bool> { use super::animal::Animal; - self.animal_interact(player, item_stack, Sound::EntityPigAmbient) + Box::pin(async move { + if self.is_saddled() && !self.is_food(item_stack) { + let world = player.world(); + if let Some(vehicle) = world.get_entity_by_id(self.get_entity().entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + self.get_entity() + .add_passenger(vehicle, passenger as Arc) + .await; + return true; + } + } + self.animal_interact(player, item_stack, Sound::EntityPigAmbient) + .await + }) + } +} + +impl ItemSteerable for PigEntity { + fn boost(&self) -> bool { + self.steering.boost() + } + + fn as_any(&self) -> &dyn std::any::Any { + self } } diff --git a/crates/pumpkin/src/entity/passive/sheep.rs b/crates/pumpkin/src/entity/passive/sheep.rs index 296829652..f414a7113 100644 --- a/crates/pumpkin/src/entity/passive/sheep.rs +++ b/crates/pumpkin/src/entity/passive/sheep.rs @@ -148,6 +148,10 @@ impl Mob for SheepEntity { }) } + fn get_sheep(&self) -> Option<&SheepEntity> { + Some(self) + } + fn mob_interact<'a>( &'a self, player: &'a Arc, diff --git a/crates/pumpkin/src/entity/passive/strider.rs b/crates/pumpkin/src/entity/passive/strider.rs index 74e9db423..bc7784560 100644 --- a/crates/pumpkin/src/entity/passive/strider.rs +++ b/crates/pumpkin/src/entity/passive/strider.rs @@ -11,14 +11,22 @@ use crate::entity::{ mob::{Mob, MobEntity}, }; +use crate::entity::item_steerable::{ItemBasedSteering, ItemSteerable}; + pub struct StriderEntity { pub mob_entity: MobEntity, + pub steering: ItemBasedSteering, + pub saddled: std::sync::atomic::AtomicBool, } impl StriderEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let strider = Self { mob_entity }; + let strider = Self { + mob_entity, + steering: ItemBasedSteering::default(), + saddled: std::sync::atomic::AtomicBool::new(false), + }; let mob_arc = Arc::new(strider); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -45,10 +53,82 @@ impl StriderEntity { } } -impl NBTStorage for StriderEntity {} +use crate::entity::EntityBase; + +impl NBTStorage for StriderEntity { + fn write_nbt<'a>( + &'a self, + nbt: &'a mut pumpkin_nbt::compound::NbtCompound, + ) -> crate::entity::NbtFuture<'a, ()> { + Box::pin(async move { + self.mob_entity.living_entity.write_nbt(nbt).await; + nbt.put_bool("Saddle", self.is_saddled()); + }) + } + + fn read_nbt_non_mut<'a>( + &'a self, + nbt: &'a pumpkin_nbt::compound::NbtCompound, + ) -> crate::entity::NbtFuture<'a, ()> { + Box::pin(async move { + self.mob_entity.living_entity.read_nbt_non_mut(nbt).await; + if let Some(saddle) = nbt.get_byte("Saddle") { + self.set_saddled(saddle == 1); + } + }) + } +} impl Mob for StriderEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn get_item_steerable(&self) -> Option<&dyn ItemSteerable> { + Some(self) + } + + fn is_saddled(&self) -> bool { + self.saddled.load(std::sync::atomic::Ordering::Relaxed) + } + + fn can_be_saddled(&self) -> bool { + self.mob_entity.living_entity.entity.is_alive() + } + + fn set_saddled(&self, saddled: bool) { + self.saddled + .store(saddled, std::sync::atomic::Ordering::Relaxed); + } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + _item_stack: &'a mut pumpkin_data::item_stack::ItemStack, + ) -> crate::entity::EntityBaseFuture<'a, bool> { + Box::pin(async move { + if self.is_saddled() { + let world = player.world(); + let ent = &self.mob_entity.living_entity.entity; + if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + ent.add_passenger(vehicle, passenger as Arc) + .await; + return true; + } + } + false + }) + } +} + +impl ItemSteerable for StriderEntity { + fn boost(&self) -> bool { + self.steering.boost() + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } } diff --git a/crates/pumpkin/src/item/items/bone_meal.rs b/crates/pumpkin/src/item/items/bone_meal.rs new file mode 100644 index 000000000..b8c843c0d --- /dev/null +++ b/crates/pumpkin/src/item/items/bone_meal.rs @@ -0,0 +1,172 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use crate::server::Server; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::{Block, BlockDirection}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::world::BlockFlags; + +pub struct BoneMealItem; + +impl ItemMetadata for BoneMealItem { + fn ids() -> Box<[u16]> { + Box::new([Item::BONE_MEAL.id]) + } +} + +impl ItemBehaviour for BoneMealItem { + #[allow(clippy::too_many_lines)] + fn use_on_block<'a>( + &'a self, + item: &'a mut ItemStack, + player: &'a Player, + location: BlockPos, + _face: BlockDirection, + _cursor_pos: Vector3, + block: &'a Block, + _server: &'a Server, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let world = player.world(); + let state_id = world.get_block_state_id(&location); + + // Compute crop age progression without holding Box across await + let crop_action = block.properties(state_id).and_then(|props| { + let prop_map = props.to_props(); + prop_map + .iter() + .find(|(k, _)| *k == "age") + .and_then(|(_, age_val)| age_val.parse::().ok()) + .and_then(|current_age| { + let max_age = match block.id { + id if id == Block::BEETROOTS.id || id == Block::SWEET_BERRY_BUSH.id => { + 3 + } + id if id == Block::TORCHFLOWER_CROP.id => 1, + id if id == Block::PITCHER_CROP.id => 4, + _ => 7, + }; + (current_age < max_age).then(|| { + let bonus = (rand::random::() % 4 + 2) as u8; + let new_age = (current_age + bonus).min(max_age).to_string(); + let new_props: Vec<(&str, &str)> = prop_map + .iter() + .map(|(k, v)| { + if *k == "age" { + (*k, new_age.as_str()) + } else { + (*k, *v) + } + }) + .collect(); + block.from_properties(&new_props).to_state_id(block) + }) + }) + }); + + if let Some(new_state_id) = crop_action { + world + .set_block_state(&location, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + world.play_sound( + Sound::ItemBoneMealUse, + SoundCategory::Blocks, + &location.to_f64(), + ); + item.decrement_unless_creative(player.gamemode.load(), 1); + return; + } + + // Compute sapling stage progression without holding Box across await + let sapling_action = block.properties(state_id).and_then(|props| { + let prop_map = props.to_props(); + prop_map + .iter() + .find(|(k, _)| *k == "stage") + .and_then(|(_, stage_val)| stage_val.parse::().ok()) + .filter(|&stage| stage < 1) + .map(|_| { + let new_props: Vec<(&str, &str)> = prop_map + .iter() + .map(|(k, v)| if *k == "stage" { (*k, "1") } else { (*k, *v) }) + .collect(); + block.from_properties(&new_props).to_state_id(block) + }) + }); + + if let Some(new_state_id) = sapling_action { + world + .set_block_state(&location, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + world.play_sound( + Sound::ItemBoneMealUse, + SoundCategory::Blocks, + &location.to_f64(), + ); + item.decrement_unless_creative(player.gamemode.load(), 1); + return; + } + + // Handle Grass Block / Moss Block bone-mealing + if block.id == Block::GRASS_BLOCK.id || block.id == Block::MOSS_BLOCK.id { + let center_x = location.0.x; + let center_y = location.0.y; + let center_z = location.0.z; + + for dx in -2..=2 { + for dz in -2..=2 { + if dx == 0 && dz == 0 { + continue; + } + if rand::random::().is_multiple_of(2) { + let target_pos = + BlockPos::new(center_x + dx, center_y + 1, center_z + dz); + let target_below = + BlockPos::new(center_x + dx, center_y, center_z + dz); + let (below_block, _) = world.get_block_and_state_id(&target_below); + let (target_block, _) = world.get_block_and_state_id(&target_pos); + + if (below_block.id == Block::GRASS_BLOCK.id + || below_block.id == Block::MOSS_BLOCK.id) + && target_block.id == Block::AIR.id + { + let plant_block = if rand::random::().is_multiple_of(8) { + Block::DANDELION.default_state.id + } else if (rand::random::() + 1).is_multiple_of(8) { + Block::POPPY.default_state.id + } else { + Block::SHORT_GRASS.default_state.id + }; + world + .set_block_state( + &target_pos, + plant_block, + BlockFlags::NOTIFY_ALL, + ) + .await; + } + } + } + } + + world.play_sound( + Sound::ItemBoneMealUse, + SoundCategory::Blocks, + &location.to_f64(), + ); + item.decrement_unless_creative(player.gamemode.load(), 1); + } + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/crates/pumpkin/src/item/items/brush.rs b/crates/pumpkin/src/item/items/brush.rs new file mode 100644 index 000000000..9e1d2980c --- /dev/null +++ b/crates/pumpkin/src/item/items/brush.rs @@ -0,0 +1,240 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use crate::entity::item::ItemEntity; +use crate::entity::player::Player; +use crate::entity::{Entity, EntityBase}; +use crate::item::{ItemBehaviour, ItemMetadata}; +use crate::server::Server; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::{Block, BlockDirection, BlockStateId}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::world::BlockFlags; + +pub struct BrushItem; + +impl ItemMetadata for BrushItem { + fn ids() -> Box<[u16]> { + Box::new([Item::BRUSH.id]) + } +} + +fn get_dusted_stage(block: &Block, state_id: BlockStateId) -> u8 { + block + .properties(state_id) + .and_then(|props| { + props + .to_props() + .into_iter() + .find(|(key, _)| *key == "dusted") + .and_then(|(_, val)| val.parse::().ok()) + }) + .unwrap_or(0) +} + +fn set_dusted_stage(block: &Block, state_id: BlockStateId, stage: u8) -> BlockStateId { + let Some(props) = block.properties(state_id) else { + return state_id; + }; + let stage_str = stage.to_string(); + let original_props = props.to_props(); + let updated_props: Vec<(&str, &str)> = original_props + .iter() + .map(|(key, value)| { + if *key == "dusted" { + ("dusted", stage_str.as_str()) + } else { + (*key, *value) + } + }) + .collect(); + block.from_properties(&updated_props).to_state_id(block) +} + +fn get_random_archaeology_loot() -> &'static Item { + let loot_table = [ + &Item::ARCHER_POTTERY_SHERD, + &Item::PRIZE_POTTERY_SHERD, + &Item::SKULL_POTTERY_SHERD, + &Item::ARMS_UP_POTTERY_SHERD, + &Item::ANGLER_POTTERY_SHERD, + &Item::EMERALD, + &Item::DIAMOND, + &Item::SNIFFER_EGG, + &Item::BONE, + &Item::GOLD_NUGGET, + &Item::COAL, + ]; + let idx = (rand::random::() as usize) % loot_table.len(); + loot_table[idx] +} + +impl ItemBehaviour for BrushItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + player.world().play_sound( + Sound::ItemBrushBrushingGeneric, + SoundCategory::Players, + &player.position(), + ); + let stack = player.inventory().held_item().await; + player + .living_entity + .set_active_hand(pumpkin_util::Hand::Right, stack, Self::USE_DURATION) + .await; + }) + } + + fn use_on_block<'a>( + &'a self, + _item: &'a mut ItemStack, + player: &'a Player, + location: BlockPos, + _face: BlockDirection, + _cursor_pos: Vector3, + block: &'a Block, + _server: &'a Server, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let world = player.world(); + let is_sand = block == &Block::SUSPICIOUS_SAND; + let is_gravel = block == &Block::SUSPICIOUS_GRAVEL; + let block_center = Vector3::new( + f64::from(location.0.x) + 0.5, + f64::from(location.0.y) + 0.5, + f64::from(location.0.z) + 0.5, + ); + + if is_sand || is_gravel { + let current_state_id = world.get_block_state_id(&location); + let current_stage = get_dusted_stage(block, current_state_id); + + if current_stage < 3 { + let next_stage_id = + set_dusted_stage(block, current_state_id, current_stage + 1); + world + .set_block_state(&location, next_stage_id, BlockFlags::NOTIFY_ALL) + .await; + + world.play_sound( + if is_sand { + Sound::ItemBrushBrushingSand + } else { + Sound::ItemBrushBrushingGravel + }, + SoundCategory::Blocks, + &block_center, + ); + } else { + let replacement_state_id = if is_sand { + Block::SAND.default_state.id + } else { + Block::GRAVEL.default_state.id + }; + + world + .set_block_state(&location, replacement_state_id, BlockFlags::NOTIFY_ALL) + .await; + + world.play_sound( + if is_sand { + Sound::ItemBrushBrushingSandComplete + } else { + Sound::ItemBrushBrushingGravelComplete + }, + SoundCategory::Blocks, + &block_center, + ); + + let loot_item = get_random_archaeology_loot(); + let spawn_pos = Vector3::new( + f64::from(location.0.x) + 0.5, + f64::from(location.0.y) + 1.0, + f64::from(location.0.z) + 0.5, + ); + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), spawn_pos, &EntityType::ITEM), + ItemStack::new(1, loot_item), + )); + world.spawn_entity(item_entity).await; + } + + player.damage_held_item(1).await; + } else { + world.play_sound( + Sound::ItemBrushBrushingGeneric, + SoundCategory::Blocks, + &block_center, + ); + } + + let stack = player.inventory().held_item().await; + player + .living_entity + .set_active_hand(pumpkin_util::Hand::Right, stack, Self::USE_DURATION) + .await; + }) + } + + fn use_on_entity<'a>( + &'a self, + _item: &'a mut ItemStack, + player: &'a Player, + entity: Arc, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let ent = entity.get_entity(); + if ent.entity_type == &EntityType::ARMADILLO { + let world = player.world(); + world.play_sound( + Sound::EntityArmadilloBrush, + SoundCategory::Neutral, + &ent.pos.load(), + ); + + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), ent.pos.load(), &EntityType::ITEM), + ItemStack::new(1, &Item::ARMADILLO_SCUTE), + )); + world.spawn_entity(item_entity).await; + + player.damage_held_item(16).await; + } else { + let world = player.world(); + world.play_sound( + Sound::ItemBrushBrushingGeneric, + SoundCategory::Neutral, + &ent.pos.load(), + ); + } + + let stack = player.inventory().held_item().await; + player + .living_entity + .set_active_hand(pumpkin_util::Hand::Right, stack, Self::USE_DURATION) + .await; + }) + } + + fn get_use_duration(&self) -> i32 { + Self::USE_DURATION + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl BrushItem { + pub const USE_DURATION: i32 = 200; +} diff --git a/crates/pumpkin/src/item/items/bucket.rs b/crates/pumpkin/src/item/items/bucket.rs index e7391b1e2..bcb3ac456 100644 --- a/crates/pumpkin/src/item/items/bucket.rs +++ b/crates/pumpkin/src/item/items/bucket.rs @@ -22,6 +22,7 @@ use crate::world::World; pub struct EmptyBucketItem; pub struct FilledBucketItem; +pub struct MilkBucketItem; impl ItemMetadata for EmptyBucketItem { fn ids() -> Box<[u16]> { @@ -46,11 +47,11 @@ impl ItemMetadata for FilledBucketItem { } } -// impl ItemMetadata for MilkBucketItem { -// fn ids() -> Box<[u16]> { -// [Item::MILK_BUCKET.id].into() -// } -// } +impl ItemMetadata for MilkBucketItem { + fn ids() -> Box<[u16]> { + [Item::MILK_BUCKET.id].into() + } +} fn get_start_and_end_pos(player: &Player) -> (Vector3, Vector3) { let start_pos = player.eye_position(); @@ -399,4 +400,37 @@ impl ItemBehaviour for FilledBucketItem { } } -//TODO: Implement MilkBucketItem +impl ItemBehaviour for MilkBucketItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let stack = player.inventory().held_item().await; + player + .living_entity + .set_active_hand(pumpkin_util::Hand::Right, stack, 32) + .await; + }) + } + + fn on_stopped_using<'a>( + &'a self, + _stack: &'a ItemStack, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + player.living_entity.reset_effects_and_attributes().await; + give_player_bucket_item(player, &Item::BUCKET).await; + }) + } + + fn get_use_duration(&self) -> i32 { + 32 + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/crates/pumpkin/src/item/items/clock.rs b/crates/pumpkin/src/item/items/clock.rs new file mode 100644 index 000000000..f77ba0f7c --- /dev/null +++ b/crates/pumpkin/src/item/items/clock.rs @@ -0,0 +1,37 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::item::Item; +use pumpkin_data::sound::{Sound, SoundCategory}; + +pub struct ClockItem; + +impl ItemMetadata for ClockItem { + fn ids() -> Box<[u16]> { + Box::new([Item::CLOCK.id]) + } +} + +impl ItemBehaviour for ClockItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let world = player.world(); + world.play_sound( + Sound::UiButtonClick, + SoundCategory::Players, + &player.position(), + ); + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/crates/pumpkin/src/item/items/compass.rs b/crates/pumpkin/src/item/items/compass.rs new file mode 100644 index 000000000..0ead5b5d1 --- /dev/null +++ b/crates/pumpkin/src/item/items/compass.rs @@ -0,0 +1,71 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use crate::server::Server; +use pumpkin_data::data_component::DataComponent; +use pumpkin_data::data_component_impl::{DataComponentImpl, LodestoneTarget, LodestoneTrackerImpl}; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::{Block, BlockDirection}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; + +pub struct CompassItem; + +impl ItemMetadata for CompassItem { + fn ids() -> Box<[u16]> { + Box::new([Item::COMPASS.id, Item::RECOVERY_COMPASS.id]) + } +} + +impl ItemBehaviour for CompassItem { + fn use_on_block<'a>( + &'a self, + item: &'a mut ItemStack, + player: &'a Player, + location: BlockPos, + _face: BlockDirection, + _cursor_pos: Vector3, + block: &'a Block, + _server: &'a Server, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + if block.id == Block::LODESTONE.id && item.item.id == Item::COMPASS.id { + let world = player.world(); + world.play_sound( + Sound::ItemLodestoneCompassLock, + SoundCategory::Players, + &location.to_f64(), + ); + + let mut lodestone_compass = ItemStack::new(1, &Item::COMPASS); + let tracker = LodestoneTrackerImpl { + target: Some(LodestoneTarget { + dimension: world.dimension.minecraft_name.to_string(), + x: location.0.x, + y: location.0.y, + z: location.0.z, + }), + tracked: true, + }; + lodestone_compass + .patch + .push((DataComponent::LodestoneTracker, Some(tracker.to_dyn()))); + + item.decrement_unless_creative(player.gamemode.load(), 1); + player + .inventory + .offer_or_drop_stack(lodestone_compass, player) + .await; + } + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/crates/pumpkin/src/item/items/experience_bottle.rs b/crates/pumpkin/src/item/items/experience_bottle.rs new file mode 100644 index 000000000..cb7c2126f --- /dev/null +++ b/crates/pumpkin/src/item/items/experience_bottle.rs @@ -0,0 +1,46 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use crate::entity::experience_orb::ExperienceOrbEntity; +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::item::Item; +use pumpkin_data::sound::{Sound, SoundCategory}; + +pub struct ExperienceBottleItem; + +impl ItemMetadata for ExperienceBottleItem { + fn ids() -> Box<[u16]> { + Box::new([Item::EXPERIENCE_BOTTLE.id]) + } +} + +impl ItemBehaviour for ExperienceBottleItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let world = player.world(); + let pos = player.eye_position(); + world.play_sound( + Sound::EntityExperienceBottleThrow, + SoundCategory::Players, + &pos, + ); + + let amount = (rand::random::() % 9 + 3) as u32; // 3..=11 exp + ExperienceOrbEntity::spawn(&world, pos, amount).await; + + let mut held = player.inventory().held_item().await; + held.decrement_unless_creative(player.gamemode.load(), 1); + player.inventory().set_held_item(held).await; + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/crates/pumpkin/src/item/items/glass_bottle.rs b/crates/pumpkin/src/item/items/glass_bottle.rs new file mode 100644 index 000000000..c6ce8e2f0 --- /dev/null +++ b/crates/pumpkin/src/item/items/glass_bottle.rs @@ -0,0 +1,107 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use crate::server::Server; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::{Block, BlockDirection}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::world::BlockFlags; + +pub struct GlassBottleItem; + +impl ItemMetadata for GlassBottleItem { + fn ids() -> Box<[u16]> { + Box::new([Item::GLASS_BOTTLE.id]) + } +} + +impl ItemBehaviour for GlassBottleItem { + fn use_on_block<'a>( + &'a self, + item: &'a mut ItemStack, + player: &'a Player, + location: BlockPos, + face: BlockDirection, + _cursor_pos: Vector3, + block: &'a Block, + _server: &'a Server, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let world = player.world(); + + let is_water_target = + block.id == Block::WATER.id || block.id == Block::WATER_CAULDRON.id; + + let check_pos = if is_water_target { + location + } else { + location.offset(face.to_offset()) + }; + + let (check_block, check_state_id) = world.get_block_and_state_id(&check_pos); + + if check_block.id == Block::WATER.id || check_block.id == Block::WATER_CAULDRON.id { + let cauldron_action = (check_block.id == Block::WATER_CAULDRON.id) + .then(|| { + check_block.properties(check_state_id).and_then(|props| { + let prop_map = props.to_props(); + prop_map + .iter() + .find(|(k, _)| *k == "level") + .and_then(|(_, level_str)| level_str.parse::().ok()) + .map(|level| { + if level > 1 { + let new_level = (level - 1).to_string(); + let new_props: Vec<(&str, &str)> = prop_map + .iter() + .map(|(k, v)| { + if *k == "level" { + (*k, new_level.as_str()) + } else { + (*k, *v) + } + }) + .collect(); + check_block + .from_properties(&new_props) + .to_state_id(check_block) + } else { + Block::CAULDRON.default_state.id + } + }) + }) + }) + .flatten(); + + if let Some(new_state_id) = cauldron_action { + world + .set_block_state(&check_pos, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + } + + world.play_sound( + Sound::ItemBottleFill, + SoundCategory::Players, + &check_pos.to_f64(), + ); + + let water_bottle = ItemStack::new(1, &Item::POTION); + item.decrement_unless_creative(player.gamemode.load(), 1); + player + .inventory + .offer_or_drop_stack(water_bottle, player) + .await; + } + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/crates/pumpkin/src/item/items/goat_horn.rs b/crates/pumpkin/src/item/items/goat_horn.rs new file mode 100644 index 000000000..ca3e73516 --- /dev/null +++ b/crates/pumpkin/src/item/items/goat_horn.rs @@ -0,0 +1,49 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::item::Item; +use pumpkin_data::sound::{Sound, SoundCategory}; + +pub struct GoatHornItem; + +impl ItemMetadata for GoatHornItem { + fn ids() -> Box<[u16]> { + Box::new([Item::GOAT_HORN.id]) + } +} + +impl ItemBehaviour for GoatHornItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + player.world().play_sound( + Sound::ItemGoatHornSound0, + SoundCategory::Players, + &player.position(), + ); + let stack = player.inventory().held_item().await; + player + .living_entity + .set_active_hand(pumpkin_util::Hand::Right, stack, Self::USE_DURATION) + .await; + }) + } + + fn get_use_duration(&self) -> i32 { + Self::USE_DURATION + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl GoatHornItem { + pub const USE_DURATION: i32 = 140; +} diff --git a/crates/pumpkin/src/item/items/mod.rs b/crates/pumpkin/src/item/items/mod.rs index 7696133b1..a8ddfbf43 100644 --- a/crates/pumpkin/src/item/items/mod.rs +++ b/crates/pumpkin/src/item/items/mod.rs @@ -2,47 +2,70 @@ pub mod armor_stand; pub mod arrow; pub mod axe; pub mod boat; +pub mod bone_meal; pub mod bow; +pub mod brush; pub mod bucket; pub mod bundle; +pub mod clock; +pub mod compass; pub mod crossbow; pub mod dye; pub mod egg; pub mod end_crystal; pub mod ender_eye; pub mod ender_pearl; +pub mod experience_bottle; pub mod firework_rocket; pub mod fishing_rod; +pub mod glass_bottle; pub mod glowing_ink_sac; +pub mod goat_horn; pub mod hoe; pub mod honeycomb; pub mod ignite; pub mod ink_sac; +pub mod lead; pub mod mace; pub mod map; pub mod minecart; pub mod name_tag; +pub mod on_a_stick; pub mod potions; +pub mod saddle; +pub mod shears; pub mod shovel; pub mod snowball; pub mod spawn_egg; +pub mod spyglass; pub mod swords; pub mod trident; pub mod wind_charge; - -pub mod lead; +pub mod writable_book; use crate::item::items::armor_stand::ArmorStandItem; use crate::item::items::boat::BoatItem; +use crate::item::items::bone_meal::BoneMealItem; +use crate::item::items::brush::BrushItem; use crate::item::items::bundle::BundleItem; +use crate::item::items::clock::ClockItem; +use crate::item::items::compass::CompassItem; use crate::item::items::end_crystal::EndCrystalItem; +use crate::item::items::experience_bottle::ExperienceBottleItem; +use crate::item::items::glass_bottle::GlassBottleItem; +use crate::item::items::goat_horn::GoatHornItem; use crate::item::items::lead::LeadItem; use crate::item::items::map::MapItem; use crate::item::items::minecart::MinecartItem; use crate::item::items::name_tag::NameTagItem; +use crate::item::items::on_a_stick::{CarrotOnAStickItem, WarpedFungusOnAStickItem}; +use crate::item::items::saddle::SaddleItem; +use crate::item::items::shears::ShearsItem; use crate::item::items::spawn_egg::SpawnEggItem; +use crate::item::items::spyglass::SpyglassItem; use crate::item::items::wind_charge::WindChargeItem; +use crate::item::items::writable_book::WritableBookItem; use firework_rocket::FireworkRocketItem; use fishing_rod::FishingRodItem; use glowing_ink_sac::GlowingInkSacItem; @@ -52,7 +75,7 @@ use crate::item::items::potions::{LingeringPotionItem, PotionItem, SplashPotionI use arrow::ArrowItem; use axe::AxeItem; use bow::BowItem; -use bucket::{EmptyBucketItem, FilledBucketItem}; +use bucket::{EmptyBucketItem, FilledBucketItem, MilkBucketItem}; use crossbow::CrossbowItem; use dye::DyeItem; use egg::EggItem; @@ -85,11 +108,22 @@ pub fn default_registry() -> Arc { manager.register(MaceItem); manager.register(TridentItem); manager.register(FishingRodItem); - // TODO: Register CrossbowItem with per-shot durability cost. - // TODO: Register BrushItem with per-stroke durability cost. - // TODO: Register CarrotOnAStickItem and WarpedFungusOnAStickItem with boost durability costs. + manager.register(BrushItem); + manager.register(CarrotOnAStickItem); + manager.register(WarpedFungusOnAStickItem); + manager.register(ShearsItem); + manager.register(SaddleItem); + manager.register(BoneMealItem); + manager.register(GlassBottleItem); + manager.register(ExperienceBottleItem); + manager.register(CompassItem); + manager.register(ClockItem); + manager.register(WritableBookItem); + manager.register(SpyglassItem); + manager.register(GoatHornItem); manager.register(EmptyBucketItem); manager.register(FilledBucketItem); + manager.register(MilkBucketItem); manager.register(ShovelItem); manager.register(SpawnEggItem); manager.register(AxeItem); diff --git a/crates/pumpkin/src/item/items/on_a_stick.rs b/crates/pumpkin/src/item/items/on_a_stick.rs new file mode 100644 index 000000000..c5842cde0 --- /dev/null +++ b/crates/pumpkin/src/item/items/on_a_stick.rs @@ -0,0 +1,67 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use crate::entity::EntityBase; +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::item::Item; + +pub struct CarrotOnAStickItem; +pub struct WarpedFungusOnAStickItem; + +impl ItemMetadata for CarrotOnAStickItem { + fn ids() -> Box<[u16]> { + Box::new([Item::CARROT_ON_A_STICK.id]) + } +} + +impl ItemBehaviour for CarrotOnAStickItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let vehicle_opt = player.get_entity().vehicle.lock().await.clone(); + if let Some(vehicle) = vehicle_opt + && let Some(steerable) = vehicle.get_item_steerable() + && steerable.boost() + { + player.damage_held_item(7).await; + } + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl ItemMetadata for WarpedFungusOnAStickItem { + fn ids() -> Box<[u16]> { + Box::new([Item::WARPED_FUNGUS_ON_A_STICK.id]) + } +} + +impl ItemBehaviour for WarpedFungusOnAStickItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let vehicle_opt = player.get_entity().vehicle.lock().await.clone(); + if let Some(vehicle) = vehicle_opt + && let Some(steerable) = vehicle.get_item_steerable() + && steerable.boost() + { + player.damage_held_item(7).await; + } + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/crates/pumpkin/src/item/items/saddle.rs b/crates/pumpkin/src/item/items/saddle.rs new file mode 100644 index 000000000..063d0e7be --- /dev/null +++ b/crates/pumpkin/src/item/items/saddle.rs @@ -0,0 +1,51 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use crate::entity::EntityBase; +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; + +pub struct SaddleItem; + +impl ItemMetadata for SaddleItem { + fn ids() -> Box<[u16]> { + Box::new([Item::SADDLE.id]) + } +} + +impl ItemBehaviour for SaddleItem { + fn use_on_entity<'a>( + &'a self, + item: &'a mut ItemStack, + player: &'a Player, + entity: Arc, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + if let Some(mob) = entity.get_mob() + && mob.can_be_saddled() + && !mob.is_saddled() + { + mob.set_saddled(true); + let ent = entity.get_entity(); + let sound = if ent.entity_type == &pumpkin_data::entity::EntityType::STRIDER { + Sound::EntityStriderSaddle + } else { + Sound::EntityPigSaddle + }; + player + .world() + .play_sound(sound, SoundCategory::Neutral, &ent.pos.load()); + item.decrement_unless_creative(player.gamemode.load(), 1); + } + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/crates/pumpkin/src/item/items/shears.rs b/crates/pumpkin/src/item/items/shears.rs new file mode 100644 index 000000000..b0cd2b1c7 --- /dev/null +++ b/crates/pumpkin/src/item/items/shears.rs @@ -0,0 +1,246 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use crate::entity::Entity; +use crate::entity::EntityBase; +use crate::entity::item::ItemEntity; +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use crate::server::Server; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::{Block, BlockDirection, BlockStateId}; +use pumpkin_util::GameMode; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::world::BlockFlags; + +pub struct ShearsItem; + +impl ItemMetadata for ShearsItem { + fn ids() -> Box<[u16]> { + Box::new([Item::SHEARS.id]) + } +} + +const fn get_wool_item_for_color(color: u8) -> &'static Item { + match color { + 0 => &Item::WHITE_WOOL, + 1 => &Item::ORANGE_WOOL, + 2 => &Item::MAGENTA_WOOL, + 3 => &Item::LIGHT_BLUE_WOOL, + 4 => &Item::YELLOW_WOOL, + 5 => &Item::LIME_WOOL, + 6 => &Item::PINK_WOOL, + 7 => &Item::GRAY_WOOL, + 8 => &Item::LIGHT_GRAY_WOOL, + 9 => &Item::CYAN_WOOL, + 10 => &Item::PURPLE_WOOL, + 11 => &Item::BLUE_WOOL, + 12 => &Item::BROWN_WOOL, + 13 => &Item::GREEN_WOOL, + 14 => &Item::RED_WOOL, + _ => &Item::BLACK_WOOL, + } +} + +impl ItemBehaviour for ShearsItem { + fn can_mine(&self, player: &Player) -> bool { + player.gamemode.load() != GameMode::Creative + } + + fn use_on_block<'a>( + &'a self, + _item: &'a mut ItemStack, + player: &'a Player, + location: BlockPos, + _face: BlockDirection, + _cursor_pos: Vector3, + block: &'a Block, + _server: &'a Server, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let world = player.world(); + let state_id = world.get_block_state_id(&location); + + if handle_growing_plant(player, &location, block, state_id).await { + return; + } + + if handle_beehive(player, &location, block, state_id).await { + return; + } + + handle_pumpkin(player, &location, block).await; + }) + } + + fn use_on_entity<'a>( + &'a self, + _item: &'a mut ItemStack, + player: &'a Player, + entity: Arc, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + if let Some(sheep) = entity.get_mob().and_then(|m| m.get_sheep()) + && !sheep.is_sheared() + { + sheep.set_sheared(true); + let world = player.world(); + let pos = sheep.mob_entity.living_entity.entity.pos.load(); + world.play_sound(Sound::EntitySheepShear, SoundCategory::Players, &pos); + + let wool_count = (rand::random::() % 3 + 1) as u8; + let wool_item = get_wool_item_for_color(sheep.get_color()); + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), pos, &EntityType::ITEM), + ItemStack::new(wool_count, wool_item), + )); + world.spawn_entity(item_entity).await; + player.damage_held_item(1).await; + } + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +async fn handle_growing_plant( + player: &Player, + location: &BlockPos, + block: &Block, + state_id: BlockStateId, +) -> bool { + let is_growing_plant = block.id == Block::KELP.id + || block.id == Block::CAVE_VINES.id + || block.id == Block::CAVE_VINES_PLANT.id + || block.id == Block::TWISTING_VINES.id + || block.id == Block::WEEPING_VINES.id; + + if !is_growing_plant { + return false; + } + + let world = player.world(); + let action = block.properties(state_id).and_then(|props| { + let prop_map = props.to_props(); + prop_map + .iter() + .find(|(k, _)| *k == "age") + .and_then(|(_, age_str)| age_str.parse::().ok()) + .filter(|&age| age < 25) + .map(|_| { + let new_props: Vec<(&str, &str)> = prop_map + .iter() + .map(|(k, v)| if *k == "age" { (*k, "25") } else { (*k, *v) }) + .collect(); + block.from_properties(&new_props).to_state_id(block) + }) + }); + + if let Some(new_state_id) = action { + world + .set_block_state(location, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + world.play_sound( + Sound::BlockGrowingPlantCrop, + SoundCategory::Blocks, + &location.to_f64(), + ); + player.damage_held_item(1).await; + return true; + } + + false +} + +async fn handle_beehive( + player: &Player, + location: &BlockPos, + block: &Block, + state_id: BlockStateId, +) -> bool { + if block.id != Block::BEEHIVE.id && block.id != Block::BEE_NEST.id { + return false; + } + + let world = player.world(); + let action = block.properties(state_id).and_then(|props| { + let prop_map = props.to_props(); + prop_map + .iter() + .find(|(k, v)| *k == "honey_level" && *v == "5") + .map(|_| { + let new_props: Vec<(&str, &str)> = prop_map + .iter() + .map(|(k, v)| { + if *k == "honey_level" { + (*k, "0") + } else { + (*k, *v) + } + }) + .collect(); + block.from_properties(&new_props).to_state_id(block) + }) + }); + + if let Some(new_state_id) = action { + world + .set_block_state(location, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + world.play_sound( + Sound::BlockBeehiveShear, + SoundCategory::Blocks, + &location.to_f64(), + ); + + let drop_pos = Vector3::new( + f64::from(location.0.x) + 0.5, + f64::from(location.0.y) + 0.5, + f64::from(location.0.z) + 0.5, + ); + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), drop_pos, &EntityType::ITEM), + ItemStack::new(3, &Item::HONEYCOMB), + )); + world.spawn_entity(item_entity).await; + player.damage_held_item(1).await; + return true; + } + + false +} + +async fn handle_pumpkin(player: &Player, location: &BlockPos, block: &Block) { + if block.id == Block::PUMPKIN.id { + let world = player.world(); + let carved_state = Block::CARVED_PUMPKIN.default_state.id; + world + .set_block_state(location, carved_state, BlockFlags::NOTIFY_ALL) + .await; + world.play_sound( + Sound::BlockPumpkinCarve, + SoundCategory::Blocks, + &location.to_f64(), + ); + + let drop_pos = Vector3::new( + f64::from(location.0.x) + 0.5, + f64::from(location.0.y) + 0.5, + f64::from(location.0.z) + 0.5, + ); + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), drop_pos, &EntityType::ITEM), + ItemStack::new(4, &Item::PUMPKIN_SEEDS), + )); + world.spawn_entity(item_entity).await; + player.damage_held_item(1).await; + } +} diff --git a/crates/pumpkin/src/item/items/spyglass.rs b/crates/pumpkin/src/item/items/spyglass.rs new file mode 100644 index 000000000..9139d9316 --- /dev/null +++ b/crates/pumpkin/src/item/items/spyglass.rs @@ -0,0 +1,63 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::item::Item; +use pumpkin_data::sound::{Sound, SoundCategory}; + +pub struct SpyglassItem; + +impl ItemMetadata for SpyglassItem { + fn ids() -> Box<[u16]> { + Box::new([Item::SPYGLASS.id]) + } +} + +impl ItemBehaviour for SpyglassItem { + fn normal_use<'a>( + &'a self, + _item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + player.world().play_sound( + Sound::ItemSpyglassUse, + SoundCategory::Players, + &player.position(), + ); + let stack = player.inventory().held_item().await; + player + .living_entity + .set_active_hand(pumpkin_util::Hand::Right, stack, Self::USE_DURATION) + .await; + }) + } + + fn on_stopped_using<'a>( + &'a self, + _stack: &'a pumpkin_data::item_stack::ItemStack, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + player.world().play_sound( + Sound::ItemSpyglassStopUsing, + SoundCategory::Players, + &player.position(), + ); + }) + } + + fn get_use_duration(&self) -> i32 { + Self::USE_DURATION + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl SpyglassItem { + pub const USE_DURATION: i32 = 1200; +} diff --git a/crates/pumpkin/src/item/items/writable_book.rs b/crates/pumpkin/src/item/items/writable_book.rs new file mode 100644 index 000000000..603a27e2e --- /dev/null +++ b/crates/pumpkin/src/item/items/writable_book.rs @@ -0,0 +1,44 @@ +use std::any::Any; +use std::future::Future; +use std::pin::Pin; + +use crate::entity::player::Player; +use crate::item::{ItemBehaviour, ItemMetadata}; +use pumpkin_data::item::Item; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::COpenBook; + +pub struct WritableBookItem; + +impl ItemMetadata for WritableBookItem { + fn ids() -> Box<[u16]> { + Box::new([Item::WRITABLE_BOOK.id, Item::WRITTEN_BOOK.id]) + } +} + +impl ItemBehaviour for WritableBookItem { + fn normal_use<'a>( + &'a self, + item: &'a Item, + player: &'a Player, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + if item.id == Item::WRITTEN_BOOK.id { + player + .client + .enqueue_packet(&COpenBook::new(VarInt(0))) // 0 = main hand + .await; + player.world().play_sound( + Sound::ItemBookPageTurn, + SoundCategory::Players, + &player.position(), + ); + } + }) + } + + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/crates/pumpkin/src/net/java/mod.rs b/crates/pumpkin/src/net/java/mod.rs index 852e6133b..ca7f8d733 100644 --- a/crates/pumpkin/src/net/java/mod.rs +++ b/crates/pumpkin/src/net/java/mod.rs @@ -16,7 +16,7 @@ use pumpkin_protocol::java::server::play::{ SClickSlot, SClientCommand, SClientInformationPlay, SClientTickEnd, SCloseContainer, SCommandSuggestion, SConfirmTeleport, SContainerButtonClick, SCookieResponse as SPCookieResponse, SCustomPayload, SDebugSampleSubscription, - SDebugSubscriptionRequest, SInteract, SJigsawGenerate, SMoveVehicle, SPaddleBoat, + SDebugSubscriptionRequest, SEditBook, SInteract, SJigsawGenerate, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, SRecipeBookSeenRecipe, SRenameItem, @@ -895,6 +895,10 @@ impl JavaClient { self.handle_sign_update(player, SUpdateSign::read(&mut payload, &version)?) .await; } + id if id == SEditBook::to_id(version) => { + self.handle_edit_book(player, SEditBook::read(&mut payload, &version)?) + .await; + } id if id == SUseItemOn::to_id(version) => { self.handle_use_item_on(player, SUseItemOn::read(&mut payload, &version)?, server) .await?; diff --git a/crates/pumpkin/src/net/java/play.rs b/crates/pumpkin/src/net/java/play.rs index c3b67a1df..1b35e6430 100644 --- a/crates/pumpkin/src/net/java/play.rs +++ b/crates/pumpkin/src/net/java/play.rs @@ -40,8 +40,10 @@ use crate::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent; use crate::server::{Server, seasonal_events}; use crate::world::{BlockBreakingProgress, World, chunker}; use pumpkin_data::block_properties::{BlockProperties, CommandBlockLikeProperties}; +use pumpkin_data::data_component::DataComponent; use pumpkin_data::data_component_impl::{ - BlocksAttacksImpl, ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl, + BlocksAttacksImpl, ConsumableImpl, DataComponentImpl, EquipmentSlot, EquippableImpl, FoodImpl, + WritableBookContentImpl, WrittenBookContentImpl, }; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; @@ -64,13 +66,14 @@ use pumpkin_protocol::java::server::play::{ Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SBundleItemSelected, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClientCommand, SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport, - SCookieResponse as SPCookieResponse, SInteract, SJigsawGenerate, SKeepAlive, SMoveVehicle, - SPaddleBoat, SPickItemFromBlock, SPickItemFromEntity, SPlaceRecipe, SPlayPingRequest, - SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition, - SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, - SRecipeBookSeenRecipe, SSeenAdvancement, SSelectTrade, SSetCommandBlock, SSetCreativeSlot, - SSetHeldItem, SSetJigsawBlock, SSetPlayerGround, SSetTestBlock, SSwingArm, STeleportToEntity, - STestInstanceBlockAction, SUpdateSign, SUseItem, SUseItemOn, Status, + SCookieResponse as SPCookieResponse, SEditBook, SInteract, SJigsawGenerate, SKeepAlive, + SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPickItemFromEntity, SPlaceRecipe, + SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, + SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SPlayerSession, + SRecipeBookChangeSettings, SRecipeBookSeenRecipe, SSeenAdvancement, SSelectTrade, + SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetJigsawBlock, SSetPlayerGround, + SSetTestBlock, SSwingArm, STeleportToEntity, STestInstanceBlockAction, SUpdateSign, SUseItem, + SUseItemOn, Status, }; use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::{polynomial_rolling_hash, position::BlockPos, wrap_degrees}; @@ -2511,6 +2514,38 @@ impl JavaClient { world.update_block_entity(&block_entity); } + pub async fn handle_edit_book(&self, player: &Player, packet: SEditBook<'_>) { + let held_stack = player.inventory().held_item().await; + if held_stack.item.id != Item::WRITABLE_BOOK.id { + return; + } + + let pages: Vec = packet.pages.iter().map(|p| (*p).to_string()).collect(); + + if let Some(title) = packet.title { + let mut written_book = ItemStack::new(1, &Item::WRITTEN_BOOK); + let content = WrittenBookContentImpl { + title: title.to_string(), + author: player.gameprofile.name.clone(), + pages, + }; + written_book + .patch + .push((DataComponent::WrittenBookContent, Some(content.to_dyn()))); + player.inventory().set_held_item(written_book).await; + } else { + let mut writable_book = held_stack; + let content = WritableBookContentImpl { pages }; + writable_book + .patch + .retain(|(component, _)| *component != DataComponent::WritableBookContent); + writable_book + .patch + .push((DataComponent::WritableBookContent, Some(content.to_dyn()))); + player.inventory().set_held_item(writable_book).await; + } + } + pub async fn handle_use_item( &self, player: &Arc,