diff --git a/pumpkin-data/src/data_component_impl.rs b/pumpkin-data/src/data_component_impl.rs index 9a357a9a0..b3e7a5109 100644 --- a/pumpkin-data/src/data_component_impl.rs +++ b/pumpkin-data/src/data_component_impl.rs @@ -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)] diff --git a/pumpkin-data/src/item_stack/mod.rs b/pumpkin-data/src/item_stack/mod.rs index db1b03373..28386f2dd 100644 --- a/pumpkin-data/src/item_stack/mod.rs +++ b/pumpkin-data/src/item_stack/mod.rs @@ -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); diff --git a/pumpkin-inventory/src/anvil/anvil_screen_handler.rs b/pumpkin-inventory/src/anvil/anvil_screen_handler.rs new file mode 100644 index 000000000..c758b39d8 --- /dev/null +++ b/pumpkin-inventory/src/anvil/anvil_screen_handler.rs @@ -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, + 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, + inventory: Arc, + ) -> 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 = 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; + } + }) + } +} diff --git a/pumpkin-inventory/src/anvil/mod.rs b/pumpkin-inventory/src/anvil/mod.rs new file mode 100644 index 000000000..7abf2b5a0 --- /dev/null +++ b/pumpkin-inventory/src/anvil/mod.rs @@ -0,0 +1,3 @@ +pub mod anvil_screen_handler; + +pub use anvil_screen_handler::*; diff --git a/pumpkin-inventory/src/lib.rs b/pumpkin-inventory/src/lib.rs index 378cd6964..80af27a15 100644 --- a/pumpkin-inventory/src/lib.rs +++ b/pumpkin-inventory/src/lib.rs @@ -30,6 +30,7 @@ //! [`PlayerInventory`]: PlayerInventory //! [`SyncHandler`]: sync_handler::SyncHandler +pub mod anvil; pub mod brewing; pub mod container_click; pub mod crafting; diff --git a/pumpkin-inventory/src/screen_handler.rs b/pumpkin-inventory/src/screen_handler.rs index 504553570..a715e9064 100644 --- a/pumpkin-inventory/src/screen_handler.rs +++ b/pumpkin-inventory/src/screen_handler.rs @@ -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, diff --git a/pumpkin-inventory/src/window_property.rs b/pumpkin-inventory/src/window_property.rs index 27d5388e9..48203034f 100644 --- a/pumpkin-inventory/src/window_property.rs +++ b/pumpkin-inventory/src/window_property.rs @@ -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). diff --git a/pumpkin-protocol/src/codec/data_component.rs b/pumpkin-protocol/src/codec/data_component.rs index 2f03caf4f..08e78af00 100644 --- a/pumpkin-protocol/src/codec/data_component.rs +++ b/pumpkin-protocol/src/codec/data_component.rs @@ -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 for ItemModelImpl { } } +impl DataComponentCodec for CustomNameImpl { + fn serialize(&self, seq: &mut T) -> Result<(), T::Error> { + seq.serialize_field::("", &self.name) + } + + fn deserialize<'a, A: SeqAccess<'a>>(seq: &mut A) -> Result { + let name = seq + .next_element::()? + .ok_or(de::Error::custom("No CustomNameImpl name string!"))?; + Ok(Self { name }) + } +} + impl DataComponentCodec for ConsumableImpl { fn serialize(&self, seq: &mut T) -> Result<(), T::Error> { seq.serialize_field::("", &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( DataComponent::FireworkExplosion => get::(value).serialize(seq), DataComponent::Fireworks => get::(value).serialize(seq), DataComponent::ItemModel => get::(value).serialize(seq), + DataComponent::CustomName => get::(value).serialize(seq), DataComponent::Consumable => get::(value).serialize(seq), DataComponent::Equippable => get::(value).serialize(seq), DataComponent::StoredEnchantments => get::(value).serialize(seq), diff --git a/pumpkin-protocol/src/java/server/play/mod.rs b/pumpkin-protocol/src/java/server/play/mod.rs index 4b2e89dc5..eb8d58c92 100644 --- a/pumpkin-protocol/src/java/server/play/mod.rs +++ b/pumpkin-protocol/src/java/server/play/mod.rs @@ -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::*; diff --git a/pumpkin-protocol/src/java/server/play/rename_item.rs b/pumpkin-protocol/src/java/server/play/rename_item.rs new file mode 100644 index 000000000..1e1e4159a --- /dev/null +++ b/pumpkin-protocol/src/java/server/play/rename_item.rs @@ -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, +} diff --git a/pumpkin/src/block/blocks/anvil.rs b/pumpkin/src/block/blocks/anvil.rs index de6c7532c..6dae5a51d 100644 --- a/pumpkin/src/block/blocks/anvil.rs +++ b/pumpkin/src/block/blocks/anvil.rs @@ -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, + _player: &'a dyn InventoryPlayer, + ) -> BoxFuture<'a, Option> { 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, &[]) + } } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 6ad7cad2c..e8dbca80d 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -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, 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::() + { + 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 { self.inventory.clone() } diff --git a/pumpkin/src/item/items/name_tag.rs b/pumpkin/src/item/items/name_tag.rs index 57dbf4951..149a20bc8 100644 --- a/pumpkin/src/item/items/name_tag.rs +++ b/pumpkin/src/item/items/name_tag.rs @@ -30,7 +30,9 @@ impl ItemBehaviour for NameTagItem { && let Some(name) = item.get_data_component::() { // 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); } }) diff --git a/pumpkin/src/net/java/mod.rs b/pumpkin/src/net/java/mod.rs index b8f0960d8..ab0c40fbb 100644 --- a/pumpkin/src/net/java/mod.rs +++ b/pumpkin/src/net/java/mod.rs @@ -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;