feat: add Anvil

This commit is contained in:
Alexander Medvedev 2026-05-01 16:30:14 +02:00
parent 81cffe19a2
commit 8060283970
14 changed files with 394 additions and 13 deletions

View File

@ -225,10 +225,18 @@ impl DataComponentImpl for UnbreakableImpl {
}
#[derive(Clone, Hash, PartialEq, Eq)]
pub struct CustomNameImpl {
// TODO make TextComponent const
pub name: &'static str,
// TODO make TextComponent
pub name: String,
}
impl DataComponentImpl for CustomNameImpl {
fn write_data(&self) -> NbtTag {
NbtTag::String(self.name.clone())
}
fn get_hash(&self) -> i32 {
get_str_hash(self.name.as_str()) as i32
}
default_impl!(CustomName);
}
#[derive(Clone, Hash, PartialEq, Eq)]

View File

@ -288,6 +288,20 @@ impl ItemStack {
self.item_count == 0 || self.item.id == Item::AIR.id
}
pub fn set_custom_name(&mut self, name: String) {
use crate::data_component_impl::CustomNameImpl;
let component = Some(CustomNameImpl { name }.to_dyn());
if let Some(pos) = self
.patch
.iter()
.position(|(id, _)| *id == DataComponent::CustomName)
{
self.patch[pos].1 = component;
} else {
self.patch.push((DataComponent::CustomName, component));
}
}
#[must_use]
pub fn split(&mut self, amount: u8) -> Self {
let min = amount.min(self.item_count);

View File

@ -0,0 +1,231 @@
use std::any::Any;
use std::sync::Arc;
use pumpkin_data::{item_stack::ItemStack, screen::WindowType};
use pumpkin_world::inventory::Inventory;
use crate::{
player::player_inventory::PlayerInventory,
screen_handler::{
InventoryPlayer, ItemStackFuture, ScreenHandler, ScreenHandlerBehaviour,
ScreenHandlerFuture,
},
slot::NormalSlot,
window_property::{Anvil, WindowProperty},
};
pub struct AnvilScreenHandler {
pub inventory: Arc<dyn Inventory>,
behaviour: ScreenHandlerBehaviour,
pub rename_text: String,
pub repair_cost: i16,
}
impl AnvilScreenHandler {
#[expect(clippy::needless_pass_by_value)]
pub fn new(
sync_id: u8,
player_inventory: &Arc<PlayerInventory>,
inventory: Arc<dyn Inventory>,
) -> Self {
let mut handler = Self {
inventory: inventory.clone(),
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Anvil)),
rename_text: String::new(),
repair_cost: 0,
};
// Anvil specific slots: 2 input, 1 output
for i in 0..3 {
handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), i)));
}
let player_inventory: Arc<dyn Inventory> = player_inventory.clone();
handler.add_player_slots(&player_inventory);
handler
}
pub async fn update_item_name(&mut self, name: String) {
self.rename_text = name;
self.update_result_slot().await;
self.send_content_updates().await;
}
pub async fn update_result_slot(&mut self) {
let input_a = {
let lock = self.inventory.get_stack(0).await;
lock.lock().await.clone()
};
if input_a.is_empty() {
self.inventory.set_stack(2, ItemStack::EMPTY.clone()).await;
self.set_repair_cost(0).await;
return;
}
let mut result_item = input_a.clone();
let mut cost = 0;
// Basic renaming logic for now
if !self.rename_text.is_empty() {
result_item.set_custom_name(self.rename_text.clone());
cost += 1;
}
// If combining with another item... we'll skip complex anvil logic for now
// and just support renaming.
if cost > 0 {
self.inventory.set_stack(2, result_item).await;
self.set_repair_cost(cost).await;
} else {
self.inventory.set_stack(2, ItemStack::EMPTY.clone()).await;
self.set_repair_cost(0).await;
}
}
pub async fn set_repair_cost(&mut self, cost: i16) {
self.repair_cost = cost;
if let Some(sync_handler) = self.behaviour.sync_handler.as_ref() {
let (property_id, property_value) =
WindowProperty::new(Anvil::RepairCost, cost).into_tuple();
sync_handler
.update_property(&self.behaviour, property_id as i32, property_value as i32)
.await;
}
}
}
impl ScreenHandler for AnvilScreenHandler {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
&self.behaviour
}
fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour {
&mut self.behaviour
}
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
self.default_on_closed(player).await;
self.inventory.on_close().await;
// Drop inputs from anvil
for i in 0..2 {
let stack = self.inventory.remove_stack(i).await;
if !stack.is_empty() {
player.drop_item(stack, false).await;
}
}
self.inventory.set_stack(2, ItemStack::EMPTY.clone()).await;
})
}
fn quick_move<'a>(
&'a mut self,
_player: &'a dyn InventoryPlayer,
slot_index: i32,
) -> ItemStackFuture<'a> {
Box::pin(async move {
let mut stack_left = ItemStack::EMPTY.clone();
let slot = self.get_behaviour().slots[slot_index as usize].clone();
if slot.has_stack().await {
let slot_stack_lock = slot.get_stack().await;
let slot_stack_guard = slot_stack_lock.lock().await;
stack_left = slot_stack_guard.clone();
drop(slot_stack_guard);
let mut slot_stack_mut = slot_stack_lock.lock().await;
if slot_index < 3 {
// From anvil to player
if !self
.insert_item(
&mut slot_stack_mut,
3,
self.get_behaviour().slots.len() as i32,
true,
)
.await
{
return ItemStack::EMPTY.clone();
}
} else {
// From player to anvil input 0 and 1
if !self.insert_item(&mut slot_stack_mut, 0, 2, false).await {
return ItemStack::EMPTY.clone();
}
}
if slot_stack_mut.is_empty() {
drop(slot_stack_mut);
slot.set_stack(ItemStack::EMPTY.clone()).await;
} else {
drop(slot_stack_mut);
slot.mark_dirty().await;
}
}
stack_left
})
}
fn on_slot_click<'a>(
&'a mut self,
slot_index: i32,
button: i32,
action_type: pumpkin_protocol::java::server::play::SlotActionType,
player: &'a dyn InventoryPlayer,
) -> ScreenHandlerFuture<'a, ()> {
Box::pin(async move {
if slot_index == 2 {
// Taking from output slot
let result_slot = self.get_behaviour().slots[2].clone();
if result_slot.has_stack().await {
let result_stack = result_slot.get_cloned_stack().await;
if !result_stack.is_empty() {
if player.experience_level() >= self.repair_cost as i32
|| player.is_creative()
{
// Consume experience
if !player.is_creative() {
player
.add_experience_levels(-(self.repair_cost as i32))
.await;
}
// Consume inputs
let input_a = self.inventory.get_stack(0).await;
let mut input_a = input_a.lock().await;
input_a.decrement(1); // Usually consumes 1
if input_a.is_empty() {
*input_a = ItemStack::EMPTY.clone();
}
drop(input_a);
self.get_behaviour().slots[0].mark_dirty().await;
} else {
// Cancel click
self.send_content_updates().await;
return;
}
}
}
}
self.internal_on_slot_click(slot_index, button, action_type, player)
.await;
if slot_index == 0 || slot_index == 1 || slot_index == 2 {
self.update_result_slot().await;
self.send_content_updates().await;
}
})
}
}

View File

@ -0,0 +1,3 @@
pub mod anvil_screen_handler;
pub use anvil_screen_handler::*;

View File

@ -30,6 +30,7 @@
//! [`PlayerInventory`]: PlayerInventory
//! [`SyncHandler`]: sync_handler::SyncHandler
pub mod anvil;
pub mod brewing;
pub mod container_click;
pub mod crafting;

View File

@ -133,6 +133,15 @@ pub trait InventoryPlayer: Send + Sync {
/// Checks if the player has infinite materials (creative mode).
fn has_infinite_materials(&self) -> bool;
/// Checks if the player is in creative mode.
fn is_creative(&self) -> bool;
/// Gets the player's experience level.
fn experience_level(&self) -> i32;
/// Adds or removes experience levels.
fn add_experience_levels(&self, levels: i32) -> PlayerFuture<'_, ()>;
/// Sends a full container content packet.
fn enqueue_inventory_packet<'a>(
&'a self,

View File

@ -108,6 +108,14 @@ pub enum Anvil {
RepairCost,
}
impl WindowPropertyTrait for Anvil {
fn to_id(self) -> i16 {
match self {
Self::RepairCost => 0,
}
}
}
/// Brewing stand window properties.
pub enum BrewingStand {
/// Brewing progress (0-400).

View File

@ -4,7 +4,7 @@ use crate::codec::var_int::VarInt;
use pumpkin_data::Enchantment;
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{
ConsumableImpl, ConsumeAnimation, ConsumeEffect, DamageImpl, DataComponentImpl,
ConsumableImpl, ConsumeAnimation, ConsumeEffect, CustomNameImpl, DamageImpl, DataComponentImpl,
EnchantmentsImpl, EquipmentSlot, EquippableImpl, FireworkExplosionImpl, FireworkExplosionShape,
FireworksImpl, IDSet, IDSetContent, IdOr, ItemModelImpl, MaxStackSizeImpl, PotionContentsImpl,
SoundEvent, StatusEffectInstance, StoredEnchantmentsImpl, UnbreakableImpl, get,
@ -346,6 +346,19 @@ impl DataComponentCodec<Self> for ItemModelImpl {
}
}
impl DataComponentCodec<Self> for CustomNameImpl {
fn serialize<T: SerializeStruct>(&self, seq: &mut T) -> Result<(), T::Error> {
seq.serialize_field::<String>("", &self.name)
}
fn deserialize<'a, A: SeqAccess<'a>>(seq: &mut A) -> Result<Self, A::Error> {
let name = seq
.next_element::<String>()?
.ok_or(de::Error::custom("No CustomNameImpl name string!"))?;
Ok(Self { name })
}
}
impl DataComponentCodec<Self> for ConsumableImpl {
fn serialize<T: SerializeStruct>(&self, seq: &mut T) -> Result<(), T::Error> {
seq.serialize_field::<f32>("", &self.consume_seconds)?;
@ -820,6 +833,7 @@ pub fn deserialize<'a, A: SeqAccess<'a>>(
DataComponent::FireworkExplosion => Ok(FireworkExplosionImpl::deserialize(seq)?.to_dyn()),
DataComponent::Fireworks => Ok(FireworksImpl::deserialize(seq)?.to_dyn()),
DataComponent::ItemModel => Ok(ItemModelImpl::deserialize(seq)?.to_dyn()),
DataComponent::CustomName => Ok(CustomNameImpl::deserialize(seq)?.to_dyn()),
DataComponent::Consumable => Ok(ConsumableImpl::deserialize(seq)?.to_dyn()),
DataComponent::Equippable => Ok(EquippableImpl::deserialize(seq)?.to_dyn()),
DataComponent::StoredEnchantments => Ok(StoredEnchantmentsImpl::deserialize(seq)?.to_dyn()),
@ -840,6 +854,7 @@ pub fn serialize<T: SerializeStruct>(
DataComponent::FireworkExplosion => get::<FireworkExplosionImpl>(value).serialize(seq),
DataComponent::Fireworks => get::<FireworksImpl>(value).serialize(seq),
DataComponent::ItemModel => get::<ItemModelImpl>(value).serialize(seq),
DataComponent::CustomName => get::<CustomNameImpl>(value).serialize(seq),
DataComponent::Consumable => get::<ConsumableImpl>(value).serialize(seq),
DataComponent::Equippable => get::<EquippableImpl>(value).serialize(seq),
DataComponent::StoredEnchantments => get::<StoredEnchantmentsImpl>(value).serialize(seq),

View File

@ -31,6 +31,7 @@ mod player_rotation;
mod player_session;
mod recipe_book_change_settings;
mod recipe_book_seen_recipe;
mod rename_item;
mod select_trade;
mod set_command_block;
mod set_creative_slot;
@ -73,6 +74,7 @@ pub use player_rotation::*;
pub use player_session::*;
pub use recipe_book_change_settings::*;
pub use recipe_book_seen_recipe::*;
pub use rename_item::*;
pub use select_trade::*;
pub use set_command_block::*;
pub use set_creative_slot::*;

View File

@ -0,0 +1,9 @@
use pumpkin_data::packet::serverbound::PLAY_RENAME_ITEM;
use pumpkin_macros::java_packet;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[java_packet(PLAY_RENAME_ITEM)]
pub struct SRenameItem {
pub item_name: String,
}

View File

@ -1,16 +1,44 @@
use crate::block::blocks::falling::FallingBlock;
use crate::block::registry::BlockActionResult;
use crate::block::{
BlockBehaviour, BlockFuture, GetStateForNeighborUpdateArgs, OnPlaceArgs, OnScheduledTickArgs,
PlacedArgs,
BlockBehaviour, BlockFuture, GetStateForNeighborUpdateArgs, NormalUseArgs, OnPlaceArgs,
OnScheduledTickArgs, PlacedArgs,
};
use pumpkin_data::block_properties::{BlockProperties, WallTorchLikeProperties};
use pumpkin_data::translation;
use pumpkin_inventory::anvil::AnvilScreenHandler;
use pumpkin_inventory::player::player_inventory::PlayerInventory;
use pumpkin_inventory::screen_handler::{
BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler,
};
use pumpkin_macros::pumpkin_block_from_tag;
use pumpkin_util::text::TextComponent;
use pumpkin_world::BlockStateId;
use pumpkin_world::inventory::SimpleInventory;
use std::sync::Arc;
use tokio::sync::Mutex;
#[pumpkin_block_from_tag("minecraft:anvil")]
pub struct AnvilBlock;
impl BlockBehaviour for AnvilBlock {
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
Box::pin(async move {
args.player
.open_handled_screen(&AnvilScreenFactory, Some(*args.position))
.await;
BlockActionResult::Success
})
}
fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> {
Box::pin(async move {
FallingBlock::placed(&FallingBlock, args).await;
})
}
fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
Box::pin(async move {
let dir = args
@ -27,9 +55,9 @@ impl BlockBehaviour for AnvilBlock {
})
}
fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> {
fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> {
Box::pin(async move {
FallingBlock::placed(&FallingBlock, args).await;
FallingBlock::on_scheduled_tick(&FallingBlock, args).await;
})
}
@ -41,9 +69,27 @@ impl BlockBehaviour for AnvilBlock {
async move { FallingBlock::get_state_for_neighbor_update(&FallingBlock, args).await },
)
}
fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> {
}
struct AnvilScreenFactory;
impl ScreenHandlerFactory for AnvilScreenFactory {
fn create_screen_handler<'a>(
&'a self,
sync_id: u8,
player_inventory: &'a Arc<PlayerInventory>,
_player: &'a dyn InventoryPlayer,
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
Box::pin(async move {
FallingBlock::on_scheduled_tick(&FallingBlock, args).await;
let inventory = Arc::new(SimpleInventory::new(3));
let handler = AnvilScreenHandler::new(sync_id, player_inventory, inventory);
let concrete_arc = Arc::new(Mutex::new(handler));
Some(concrete_arc as SharedScreenHandler)
})
}
fn get_display_name(&self) -> TextComponent {
TextComponent::translate(translation::CONTAINER_REPAIR, &[])
}
}

View File

@ -64,7 +64,7 @@ use pumpkin_protocol::java::client::play::{
CTitleAnimation, CTitleText, CUnloadChunk, CUpdateMobEffect, CUpdateTime, GameEvent, Metadata,
PlayerAction, PlayerInfoFlags, PreviousMessage,
};
use pumpkin_protocol::java::server::play::{SClickSlot, SlotActionType};
use pumpkin_protocol::java::server::play::{SClickSlot, SRenameItem, SlotActionType};
use pumpkin_util::math::{
boundingbox::BoundingBox, experience, position::BlockPos, vector2::Vector2, vector3::Vector3,
};
@ -2933,6 +2933,19 @@ impl Player {
.await;
}
pub async fn on_rename_item(self: &Arc<Self>, packet: SRenameItem) {
self.update_last_action_time();
let screen_handler_arc = self.current_screen_handler.lock().await.clone();
let mut screen_handler = screen_handler_arc.lock().await;
if let Some(anvil_handler) = screen_handler
.as_any_mut()
.downcast_mut::<pumpkin_inventory::anvil::AnvilScreenHandler>()
{
anvil_handler.update_item_name(packet.item_name).await;
}
}
pub async fn open_handled_screen(
&self,
screen_handler_factory: &dyn ScreenHandlerFactory,
@ -4060,6 +4073,21 @@ impl InventoryPlayer for Player {
self.gamemode.load() == GameMode::Creative
}
fn is_creative(&self) -> bool {
self.gamemode.load() == GameMode::Creative
}
fn experience_level(&self) -> i32 {
self.experience_level
.load(std::sync::atomic::Ordering::Relaxed)
}
fn add_experience_levels(&self, levels: i32) -> PlayerFuture<'_, ()> {
Box::pin(async move {
self.add_experience_levels(levels).await;
})
}
fn get_inventory(&self) -> Arc<PlayerInventory> {
self.inventory.clone()
}

View File

@ -30,7 +30,9 @@ impl ItemBehaviour for NameTagItem {
&& let Some(name) = item.get_data_component::<CustomNameImpl>()
{
// TODO
entity.set_custom_name(TextComponent::text(name.name)).await;
entity
.set_custom_name(TextComponent::text(name.name.clone()))
.await;
item.decrement_unless_creative(player.gamemode.load(), 1);
}
})

View File

@ -12,8 +12,8 @@ use pumpkin_protocol::java::server::play::{
SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities,
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition,
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings,
SRecipeBookSeenRecipe, SSelectTrade, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem,
SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
SRecipeBookSeenRecipe, SRenameItem, SSelectTrade, SSetCommandBlock, SSetCreativeSlot,
SSetHeldItem, SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
};
use pumpkin_protocol::packet::MultiVersionJavaPacket;
use pumpkin_protocol::{
@ -887,6 +887,11 @@ impl JavaClient {
)
.await;
}
id if id == SRenameItem::to_id(version) => {
player
.on_rename_item(SRenameItem::read(payload, &version)?)
.await;
}
id if id == SPlaceRecipe::to_id(version) => {
self.handle_place_recipe(player, SPlaceRecipe::read(payload, &version)?)
.await;