feat: add more items

This commit is contained in:
Alexander Medvedev 2026-08-09 21:36:03 +02:00
parent b18c26ccb4
commit 6720ac265e
33 changed files with 2138 additions and 72 deletions

8
.github/CODEOWNERS vendored
View File

@ -10,16 +10,16 @@ crates/pumpkin/src/net/ @Snowiiii @xRookieFight
# Bedrock related changes # Bedrock related changes
assets/bedrock/ @ZlordHUN @xRookieFight assets/bedrock/ @ZlordHUN @xRookieFight
crates/pumpkin-protocol/src/bedrock/ @ZlordHUN @xRookieFight crates/pumpkin-protocol/src/bedrock/ @ZlordHUN @xRookieFight @QuantumSegfault
crates/pumpkin/src/net/bedrock/ @ZlordHUN @xRookieFight crates/pumpkin/src/net/bedrock/ @ZlordHUN @xRookieFight @QuantumSegfault
# Command related changes # Command related changes
crates/pumpkin/src/command/ @aure31 crates/pumpkin/src/command/ @aure31
# Plugin related changes & WASM Host # 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-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 crates/pumpkin/src/plugin/ @vyPal @yunuservices @QuantumSegfault
# Documentation related changes # Documentation related changes

View File

@ -1,4 +1,5 @@
use crate::data_component_impl::DataComponentImpl; use crate::data_component_impl::DataComponentImpl;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag; use pumpkin_nbt::tag::NbtTag;
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@ -11,35 +12,79 @@ impl WritableBookContentImpl {
if let NbtTag::Compound(c) = tag if let NbtTag::Compound(c) = tag
&& let Some(NbtTag::List(l)) = c.get("pages") && let Some(NbtTag::List(l)) = c.get("pages")
{ {
for _ in l { for item in l {
pages.push(String::new()); 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 }) Some(Self { pages })
} }
} }
impl DataComponentImpl for WritableBookContentImpl { impl DataComponentImpl for WritableBookContentImpl {
fn write_data(&self) -> NbtTag {
let mut compound = NbtCompound::new();
let pages_tags: Vec<NbtTag> = 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); default_impl!(WritableBookContent);
} }
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct WrittenBookContentImpl { pub struct WrittenBookContentImpl {
pub title: String,
pub author: String,
pub pages: Vec<String>, pub pages: Vec<String>,
} }
impl WrittenBookContentImpl { impl WrittenBookContentImpl {
pub fn read_data(tag: &NbtTag) -> Option<Self> { pub fn read_data(tag: &NbtTag) -> Option<Self> {
let mut pages = Vec::new(); let mut pages = Vec::new();
if let NbtTag::Compound(c) = tag let mut title = String::new();
&& let Some(NbtTag::List(l)) = c.get("pages") let mut author = String::new();
{ if let NbtTag::Compound(c) = tag {
for _ in l { if let Some(s) = c.get_string("title") {
pages.push(String::new()); 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 { 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<NbtTag> = 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); default_impl!(WrittenBookContent);
} }

View File

@ -44,6 +44,7 @@ mod login;
mod map_item_data; mod map_item_data;
mod merchant_offers; mod merchant_offers;
mod multi_block_update; mod multi_block_update;
mod open_book;
mod open_screen; mod open_screen;
mod open_sign_editor; mod open_sign_editor;
mod particle; mod particle;
@ -152,6 +153,7 @@ pub use login::*;
pub use map_item_data::*; pub use map_item_data::*;
pub use merchant_offers::*; pub use merchant_offers::*;
pub use multi_block_update::*; pub use multi_block_update::*;
pub use open_book::*;
pub use open_screen::*; pub use open_screen::*;
pub use open_sign_editor::*; pub use open_sign_editor::*;
pub use particle::*; pub use particle::*;
@ -219,5 +221,8 @@ pub use update_objectives::*;
pub use update_score::*; pub use update_score::*;
pub use worldevent::*; pub use worldevent::*;
mod waypoint;
pub use waypoint::*;
mod debug_sample; mod debug_sample;
pub use debug_sample::*; pub use debug_sample::*;

View File

@ -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(())
}
}

View File

@ -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<WaypointIcon<'a>>,
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<WaypointIcon<'_>>,
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<WaypointIcon<'a>>,
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<WaypointIcon<'a>>,
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(())
}
}

View File

@ -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<Self, ReadingError> {
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 })
}
}

View File

@ -15,6 +15,7 @@ mod container_button_click;
mod cookie_response; mod cookie_response;
mod custom_click_action; mod custom_click_action;
mod custom_payload; mod custom_payload;
mod edit_book;
mod interact; mod interact;
mod jigsaw_generate; mod jigsaw_generate;
mod keep_alive; mod keep_alive;
@ -66,6 +67,7 @@ pub use container_button_click::*;
pub use cookie_response::*; pub use cookie_response::*;
pub use custom_click_action::*; pub use custom_click_action::*;
pub use custom_payload::*; pub use custom_payload::*;
pub use edit_book::*;
pub use interact::*; pub use interact::*;
pub use jigsaw_generate::*; pub use jigsaw_generate::*;
pub use keep_alive::*; pub use keep_alive::*;

View File

@ -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::BlockId;
use pumpkin_data::block_properties::{BlockProperties, WaterCauldronLikeProperties}; 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; pub struct CauldronBlock;
@ -17,6 +25,178 @@ impl BlockMetadata for CauldronBlock {
} }
impl BlockBehaviour 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>( fn get_comparator_output<'a>(
&'a self, &'a self,
args: GetComparatorOutputArgs<'a>, args: GetComparatorOutputArgs<'a>,

View File

@ -227,7 +227,7 @@ impl BlockBehaviour for LecternBlock {
Box::pin(async move { Box::pin(async move {
let item_stack = &mut *args.item_stack; let item_stack = &mut *args.item_stack;
if !item_stack.item.has_tag(&tag::Item::MINECRAFT_LECTERN_BOOKS) { if !item_stack.item.has_tag(&tag::Item::MINECRAFT_LECTERN_BOOKS) {
return BlockActionResult::Pass; return BlockActionResult::PassToDefaultBlockAction;
} }
let props = LecternLikeProperties::from_state_id( let props = LecternLikeProperties::from_state_id(
@ -236,14 +236,14 @@ impl BlockBehaviour for LecternBlock {
); );
if props.has_book { if props.has_book {
// Fall through so `normal_use` opens the reading screen. // 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 { 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::<LecternBlockEntity>() else { let Some(lectern) = lectern.as_any().downcast_ref::<LecternBlockEntity>() else {
return BlockActionResult::Pass; return BlockActionResult::PassToDefaultBlockAction;
}; };
let book = item_stack.split_unless_creative(args.player.gamemode.load(), 1); let book = item_stack.split_unless_creative(args.player.gamemode.load(), 1);

View File

@ -1,13 +1,16 @@
use pumpkin_data::translation; use pumpkin_data::translation;
use pumpkin_protocol::java::client::play::{CWaypoint, WaypointIcon};
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use crate::command::CommandResult;
use crate::command::args::{ use crate::command::args::{
FindArg, entity::EntityArgumentConsumer, hex_color::HexColorArgumentConsumer, FindArg, entity::EntityArgumentConsumer, hex_color::HexColorArgumentConsumer,
resource_location::ResourceLocationArgumentConsumer, team_color::TeamColorArgumentConsumer, resource_location::ResourceLocationArgumentConsumer, team_color::TeamColorArgumentConsumer,
}; };
use crate::command::tree::builder::{argument, literal}; 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 NAMES: [&str; 1] = ["waypoint"];
const DESCRIPTION: &str = "List or modify waypoints."; const DESCRIPTION: &str = "List or modify waypoints.";
@ -31,7 +34,6 @@ impl CommandExecutor for ListExecutor {
.expect("There should always be at least one world"); .expect("There should always be at least one world");
let dimension = world.dimension.minecraft_name.to_string(); let dimension = world.dimension.minecraft_name.to_string();
// Currently no active waypoints are tracked in the level
sender sender
.send_message(TextComponent::translate( .send_message(TextComponent::translate(
translation::java::COMMANDS_WAYPOINT_LIST_EMPTY, translation::java::COMMANDS_WAYPOINT_LIST_EMPTY,
@ -59,7 +61,38 @@ impl CommandExecutor for ColorExecutor {
args: &'a ConsumedArgs<'a>, args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> { ) -> CommandResult<'a> {
Box::pin(async move { 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 { match self.0 {
ColorAction::Named => { ColorAction::Named => {
@ -111,13 +144,35 @@ impl CommandExecutor for StyleExecutor {
args: &'a ConsumedArgs<'a>, args: &'a ConsumedArgs<'a>,
) -> CommandResult<'a> { ) -> CommandResult<'a> {
Box::pin(async move { 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 => { 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 sender

View File

@ -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::<u32>() 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
}
}
}

View File

@ -2825,31 +2825,57 @@ impl LivingEntity {
}; };
for consume_effect in consumable.effects.iter() { for consume_effect in consumable.effects.iter() {
let ConsumeEffect::ApplyEffects((effects, probability)) = consume_effect else { match consume_effect {
continue; ConsumeEffect::ApplyEffects((effects, probability)) => {
}; if !consume_effect_probability_applies(*probability, rand::random()) {
if !consume_effect_probability_applies(*probability, rand::random()) { continue;
continue; }
}
for effect in effects.iter() { for effect in effects.iter() {
let Some(effect_type) = StatusEffect::from_minecraft_name(&effect.effect_id) else { let Some(effect_type) =
continue; StatusEffect::from_minecraft_name(&effect.effect_id)
}; else {
let Ok(amplifier) = u8::try_from(effect.amplifier) else { continue;
continue; };
}; let Ok(amplifier) = u8::try_from(effect.amplifier) else {
continue;
};
self.add_effect(Effect { self.add_effect(Effect {
effect_type, effect_type,
duration: effect.duration, duration: effect.duration,
amplifier, amplifier,
ambient: effect.ambient, ambient: effect.ambient,
show_particles: effect.show_particles, show_particles: effect.show_particles,
show_icon: effect.show_icon, show_icon: effect.show_icon,
blend: false, blend: false,
}) })
.await; .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::<f64>() - 0.5) * radius * 2.0;
let target_y = center.y + (rand::random::<f64>() - 0.5) * radius;
let target_z = center.z + (rand::random::<f64>() - 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(_) => {}
} }
} }
} }

View File

@ -508,6 +508,20 @@ pub trait Mob: EntityBase + Send + Sync {
None 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. /// Per-mob tick hook called each tick before AI runs. Override for mob-specific logic.
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> { fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async {}) Box::pin(async {})
@ -605,12 +619,20 @@ pub trait Mob: EntityBase + Send + Sync {
} }
fn mob_set_variant_name(&self, _name: &str) {} fn mob_set_variant_name(&self, _name: &str) {}
fn get_sheep(&self) -> Option<&crate::entity::passive::sheep::SheepEntity> {
None
}
} }
impl<T: Mob + Send + 'static> EntityBase for T { impl<T: Mob + Send + 'static> EntityBase for T {
fn get_mob(&self) -> Option<&dyn Mob> { fn get_mob(&self) -> Option<&dyn Mob> {
Some(self) 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<'_, ()> { fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move { Box::pin(async move {
self.mob_init_data_tracker().await; self.mob_init_data_tracker().await;

View File

@ -92,6 +92,7 @@ pub mod experience_orb;
pub mod falling; pub mod falling;
pub mod hunger; pub mod hunger;
pub mod item; pub mod item;
pub mod item_steerable;
pub mod living; pub mod living;
pub mod mob; pub mod mob;
pub mod passive; pub mod passive;
@ -165,6 +166,10 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any {
self self
} }
fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> {
None
}
fn get_eye_pos(&self) -> Vector3<f64> { fn get_eye_pos(&self) -> Vector3<f64> {
self.get_entity().get_eye_pos() self.get_entity().get_eye_pos()
} }

View File

@ -25,12 +25,17 @@ const PIG_FOOD: &[&Item] = &[
&Item::CARROT_ON_A_STICK, &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. /// Represents a Pig, a common passive mob that provides porkchops.
/// ///
/// Wiki: <https://minecraft.wiki/w/Pig> /// Wiki: <https://minecraft.wiki/w/Pig>
pub struct PigEntity { pub struct PigEntity {
pub mob_entity: MobEntity, pub mob_entity: MobEntity,
pub ageable_data: crate::entity::ageable::AgeableData, pub ageable_data: crate::entity::ageable::AgeableData,
pub steering: ItemBasedSteering,
pub saddled: std::sync::atomic::AtomicBool,
} }
impl PigEntity { impl PigEntity {
@ -39,6 +44,8 @@ impl PigEntity {
let pig = Self { let pig = Self {
mob_entity, mob_entity,
ageable_data: crate::entity::ageable::AgeableData::default(), 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_arc = Arc::new(pig);
let mob_weak: Weak<dyn Mob> = { let mob_weak: Weak<dyn Mob> = {
@ -82,6 +89,7 @@ impl NBTStorage for PigEntity {
self.mob_entity.living_entity.write_nbt(nbt).await; self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt); self.write_ageable_nbt(nbt);
self.write_animal_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.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt); self.read_ageable_nbt(nbt);
self.read_animal_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 &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>( fn mob_interact<'a>(
&'a self, &'a self,
player: &'a Arc<Player>, player: &'a Arc<Player>,
item_stack: &'a mut ItemStack, item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> { ) -> EntityBaseFuture<'a, bool> {
use super::animal::Animal; 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<dyn EntityBase>)
.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
} }
} }

View File

@ -148,6 +148,10 @@ impl Mob for SheepEntity {
}) })
} }
fn get_sheep(&self) -> Option<&SheepEntity> {
Some(self)
}
fn mob_interact<'a>( fn mob_interact<'a>(
&'a self, &'a self,
player: &'a Arc<Player>, player: &'a Arc<Player>,

View File

@ -11,14 +11,22 @@ use crate::entity::{
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
use crate::entity::item_steerable::{ItemBasedSteering, ItemSteerable};
pub struct StriderEntity { pub struct StriderEntity {
pub mob_entity: MobEntity, pub mob_entity: MobEntity,
pub steering: ItemBasedSteering,
pub saddled: std::sync::atomic::AtomicBool,
} }
impl StriderEntity { impl StriderEntity {
pub fn new(entity: Entity) -> Arc<Self> { pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity); 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_arc = Arc::new(strider);
let mob_weak: Weak<dyn Mob> = { let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone(); let mob_arc: Arc<dyn Mob> = 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 { impl Mob for StriderEntity {
fn get_mob_entity(&self) -> &MobEntity { fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity &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<crate::entity::player::Player>,
_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<dyn EntityBase>)
.await;
return true;
}
}
false
})
}
}
impl ItemSteerable for StriderEntity {
fn boost(&self) -> bool {
self.steering.boost()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
} }

View File

@ -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<f32>,
block: &'a Block,
_server: &'a Server,
) -> Pin<Box<dyn Future<Output = ()> + 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<dyn BlockProperties> 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::<u8>().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::<u32>() % 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<dyn BlockProperties> 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::<u8>().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::<u32>().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::<u32>().is_multiple_of(8) {
Block::DANDELION.default_state.id
} else if (rand::random::<u32>() + 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
}
}

View File

@ -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::<u8>().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::<u32>() 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<Box<dyn Future<Output = ()> + 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<f32>,
block: &'a Block,
_server: &'a Server,
) -> Pin<Box<dyn Future<Output = ()> + 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<dyn EntityBase>,
) -> Pin<Box<dyn Future<Output = ()> + 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;
}

View File

@ -22,6 +22,7 @@ use crate::world::World;
pub struct EmptyBucketItem; pub struct EmptyBucketItem;
pub struct FilledBucketItem; pub struct FilledBucketItem;
pub struct MilkBucketItem;
impl ItemMetadata for EmptyBucketItem { impl ItemMetadata for EmptyBucketItem {
fn ids() -> Box<[u16]> { fn ids() -> Box<[u16]> {
@ -46,11 +47,11 @@ impl ItemMetadata for FilledBucketItem {
} }
} }
// impl ItemMetadata for MilkBucketItem { impl ItemMetadata for MilkBucketItem {
// fn ids() -> Box<[u16]> { fn ids() -> Box<[u16]> {
// [Item::MILK_BUCKET.id].into() [Item::MILK_BUCKET.id].into()
// } }
// } }
fn get_start_and_end_pos(player: &Player) -> (Vector3<f64>, Vector3<f64>) { fn get_start_and_end_pos(player: &Player) -> (Vector3<f64>, Vector3<f64>) {
let start_pos = player.eye_position(); 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<Box<dyn Future<Output = ()> + 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<Box<dyn Future<Output = ()> + 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
}
}

View File

@ -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<Box<dyn Future<Output = ()> + 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
}
}

View File

@ -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<f32>,
block: &'a Block,
_server: &'a Server,
) -> Pin<Box<dyn Future<Output = ()> + 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
}
}

View File

@ -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<Box<dyn Future<Output = ()> + 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::<u32>() % 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
}
}

View File

@ -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<f32>,
block: &'a Block,
_server: &'a Server,
) -> Pin<Box<dyn Future<Output = ()> + 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::<u8>().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
}
}

View File

@ -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<Box<dyn Future<Output = ()> + 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;
}

View File

@ -2,47 +2,70 @@ pub mod armor_stand;
pub mod arrow; pub mod arrow;
pub mod axe; pub mod axe;
pub mod boat; pub mod boat;
pub mod bone_meal;
pub mod bow; pub mod bow;
pub mod brush;
pub mod bucket; pub mod bucket;
pub mod bundle; pub mod bundle;
pub mod clock;
pub mod compass;
pub mod crossbow; pub mod crossbow;
pub mod dye; pub mod dye;
pub mod egg; pub mod egg;
pub mod end_crystal; pub mod end_crystal;
pub mod ender_eye; pub mod ender_eye;
pub mod ender_pearl; pub mod ender_pearl;
pub mod experience_bottle;
pub mod firework_rocket; pub mod firework_rocket;
pub mod fishing_rod; pub mod fishing_rod;
pub mod glass_bottle;
pub mod glowing_ink_sac; pub mod glowing_ink_sac;
pub mod goat_horn;
pub mod hoe; pub mod hoe;
pub mod honeycomb; pub mod honeycomb;
pub mod ignite; pub mod ignite;
pub mod ink_sac; pub mod ink_sac;
pub mod lead;
pub mod mace; pub mod mace;
pub mod map; pub mod map;
pub mod minecart; pub mod minecart;
pub mod name_tag; pub mod name_tag;
pub mod on_a_stick;
pub mod potions; pub mod potions;
pub mod saddle;
pub mod shears;
pub mod shovel; pub mod shovel;
pub mod snowball; pub mod snowball;
pub mod spawn_egg; pub mod spawn_egg;
pub mod spyglass;
pub mod swords; pub mod swords;
pub mod trident; pub mod trident;
pub mod wind_charge; pub mod wind_charge;
pub mod writable_book;
pub mod lead;
use crate::item::items::armor_stand::ArmorStandItem; use crate::item::items::armor_stand::ArmorStandItem;
use crate::item::items::boat::BoatItem; 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::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::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::lead::LeadItem;
use crate::item::items::map::MapItem; use crate::item::items::map::MapItem;
use crate::item::items::minecart::MinecartItem; use crate::item::items::minecart::MinecartItem;
use crate::item::items::name_tag::NameTagItem; 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::spawn_egg::SpawnEggItem;
use crate::item::items::spyglass::SpyglassItem;
use crate::item::items::wind_charge::WindChargeItem; use crate::item::items::wind_charge::WindChargeItem;
use crate::item::items::writable_book::WritableBookItem;
use firework_rocket::FireworkRocketItem; use firework_rocket::FireworkRocketItem;
use fishing_rod::FishingRodItem; use fishing_rod::FishingRodItem;
use glowing_ink_sac::GlowingInkSacItem; use glowing_ink_sac::GlowingInkSacItem;
@ -52,7 +75,7 @@ use crate::item::items::potions::{LingeringPotionItem, PotionItem, SplashPotionI
use arrow::ArrowItem; use arrow::ArrowItem;
use axe::AxeItem; use axe::AxeItem;
use bow::BowItem; use bow::BowItem;
use bucket::{EmptyBucketItem, FilledBucketItem}; use bucket::{EmptyBucketItem, FilledBucketItem, MilkBucketItem};
use crossbow::CrossbowItem; use crossbow::CrossbowItem;
use dye::DyeItem; use dye::DyeItem;
use egg::EggItem; use egg::EggItem;
@ -85,11 +108,22 @@ pub fn default_registry() -> Arc<ItemRegistry> {
manager.register(MaceItem); manager.register(MaceItem);
manager.register(TridentItem); manager.register(TridentItem);
manager.register(FishingRodItem); manager.register(FishingRodItem);
// TODO: Register CrossbowItem with per-shot durability cost. manager.register(BrushItem);
// TODO: Register BrushItem with per-stroke durability cost. manager.register(CarrotOnAStickItem);
// TODO: Register CarrotOnAStickItem and WarpedFungusOnAStickItem with boost durability costs. 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(EmptyBucketItem);
manager.register(FilledBucketItem); manager.register(FilledBucketItem);
manager.register(MilkBucketItem);
manager.register(ShovelItem); manager.register(ShovelItem);
manager.register(SpawnEggItem); manager.register(SpawnEggItem);
manager.register(AxeItem); manager.register(AxeItem);

View File

@ -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<Box<dyn Future<Output = ()> + 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<Box<dyn Future<Output = ()> + 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
}
}

View File

@ -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<dyn EntityBase>,
) -> Pin<Box<dyn Future<Output = ()> + 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
}
}

View File

@ -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<f32>,
block: &'a Block,
_server: &'a Server,
) -> Pin<Box<dyn Future<Output = ()> + 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<dyn EntityBase>,
) -> Pin<Box<dyn Future<Output = ()> + 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::<u8>() % 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::<u8>().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;
}
}

View File

@ -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<Box<dyn Future<Output = ()> + 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<Box<dyn Future<Output = ()> + 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;
}

View File

@ -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<Box<dyn Future<Output = ()> + 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
}
}

View File

@ -16,7 +16,7 @@ use pumpkin_protocol::java::server::play::{
SClickSlot, SClientCommand, SClientInformationPlay, SClientTickEnd, SCloseContainer, SClickSlot, SClientCommand, SClientInformationPlay, SClientTickEnd, SCloseContainer,
SCommandSuggestion, SConfirmTeleport, SContainerButtonClick, SCommandSuggestion, SConfirmTeleport, SContainerButtonClick,
SCookieResponse as SPCookieResponse, SCustomPayload, SDebugSampleSubscription, SCookieResponse as SPCookieResponse, SCustomPayload, SDebugSampleSubscription,
SDebugSubscriptionRequest, SInteract, SJigsawGenerate, SMoveVehicle, SPaddleBoat, SDebugSubscriptionRequest, SEditBook, SInteract, SJigsawGenerate, SMoveVehicle, SPaddleBoat,
SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities, SPlayerAction,
SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition, SPlayerPositionRotation, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition, SPlayerPositionRotation,
SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, SRecipeBookSeenRecipe, SRenameItem, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, SRecipeBookSeenRecipe, SRenameItem,
@ -895,6 +895,10 @@ impl JavaClient {
self.handle_sign_update(player, SUpdateSign::read(&mut payload, &version)?) self.handle_sign_update(player, SUpdateSign::read(&mut payload, &version)?)
.await; .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) => { id if id == SUseItemOn::to_id(version) => {
self.handle_use_item_on(player, SUseItemOn::read(&mut payload, &version)?, server) self.handle_use_item_on(player, SUseItemOn::read(&mut payload, &version)?, server)
.await?; .await?;

View File

@ -40,8 +40,10 @@ use crate::plugin::player::player_toggle_sprint_event::PlayerToggleSprintEvent;
use crate::server::{Server, seasonal_events}; use crate::server::{Server, seasonal_events};
use crate::world::{BlockBreakingProgress, World, chunker}; use crate::world::{BlockBreakingProgress, World, chunker};
use pumpkin_data::block_properties::{BlockProperties, CommandBlockLikeProperties}; use pumpkin_data::block_properties::{BlockProperties, CommandBlockLikeProperties};
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{ 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::Item;
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
@ -64,13 +66,14 @@ use pumpkin_protocol::java::server::play::{
Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SBundleItemSelected, Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SBundleItemSelected,
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClientCommand, SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClientCommand,
SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport, SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport,
SCookieResponse as SPCookieResponse, SInteract, SJigsawGenerate, SKeepAlive, SMoveVehicle, SCookieResponse as SPCookieResponse, SEditBook, SInteract, SJigsawGenerate, SKeepAlive,
SPaddleBoat, SPickItemFromBlock, SPickItemFromEntity, SPlaceRecipe, SPlayPingRequest, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPickItemFromEntity, SPlaceRecipe,
SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput,
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SPlayerSession,
SRecipeBookSeenRecipe, SSeenAdvancement, SSelectTrade, SSetCommandBlock, SSetCreativeSlot, SRecipeBookChangeSettings, SRecipeBookSeenRecipe, SSeenAdvancement, SSelectTrade,
SSetHeldItem, SSetJigsawBlock, SSetPlayerGround, SSetTestBlock, SSwingArm, STeleportToEntity, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetJigsawBlock, SSetPlayerGround,
STestInstanceBlockAction, SUpdateSign, SUseItem, SUseItemOn, Status, SSetTestBlock, SSwingArm, STeleportToEntity, STestInstanceBlockAction, SUpdateSign, SUseItem,
SUseItemOn, Status,
}; };
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::math::{polynomial_rolling_hash, position::BlockPos, wrap_degrees}; use pumpkin_util::math::{polynomial_rolling_hash, position::BlockPos, wrap_degrees};
@ -2511,6 +2514,38 @@ impl JavaClient {
world.update_block_entity(&block_entity); 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<String> = 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( pub async fn handle_use_item(
&self, &self,
player: &Arc<Player>, player: &Arc<Player>,