feat: implemented item/armor durability (#1343)

* Added durability for items and armor

* Run rustfmt
This commit is contained in:
Jinx
2026-01-20 20:16:56 +02:00
committed by GitHub
parent 1cc35e482d
commit a6a09a9eca
12 changed files with 401 additions and 18 deletions

View File

@@ -43,6 +43,7 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option<Box<dyn DataCompone
MaxStackSize => Some(MaxStackSizeImpl::read_data(data)?.to_dyn()),
Enchantments => Some(EnchantmentsImpl::read_data(data)?.to_dyn()),
Damage => Some(DamageImpl::read_data(data)?.to_dyn()),
Unbreakable => Some(UnbreakableImpl::read_data(data)?.to_dyn()),
_ => None,
}
}
@@ -150,6 +151,20 @@ impl DataComponentImpl for DamageImpl {
}
#[derive(Clone, Hash, PartialEq)]
pub struct UnbreakableImpl;
impl UnbreakableImpl {
fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for UnbreakableImpl {
fn write_data(&self) -> NbtTag {
NbtTag::Compound(NbtCompound::new())
}
fn get_hash(&self) -> i32 {
0
}
default_impl!(Unbreakable);
}
#[derive(Clone, Hash, PartialEq)]
pub struct CustomNameImpl {
// TODO make TextComponent const

View File

@@ -2,7 +2,7 @@ use crate::codec::var_int::VarInt;
use pumpkin_data::Enchantment;
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{
DamageImpl, DataComponentImpl, EnchantmentsImpl, MaxStackSizeImpl, get,
DamageImpl, DataComponentImpl, EnchantmentsImpl, MaxStackSizeImpl, UnbreakableImpl, get,
};
use serde::de;
use serde::de::SeqAccess;
@@ -79,6 +79,15 @@ impl DataComponentCodec<Self> for EnchantmentsImpl {
}
}
impl DataComponentCodec<Self> for UnbreakableImpl {
fn serialize<T: SerializeStruct>(&self, _seq: &mut T) -> Result<(), T::Error> {
Ok(())
}
fn deserialize<'a, A: SeqAccess<'a>>(_seq: &mut A) -> Result<Self, A::Error> {
Ok(Self)
}
}
pub fn deserialize<'a, A: SeqAccess<'a>>(
id: DataComponent,
seq: &mut A,
@@ -87,6 +96,7 @@ pub fn deserialize<'a, A: SeqAccess<'a>>(
DataComponent::MaxStackSize => Ok(MaxStackSizeImpl::deserialize(seq)?.to_dyn()),
DataComponent::Enchantments => Ok(EnchantmentsImpl::deserialize(seq)?.to_dyn()),
DataComponent::Damage => Ok(DamageImpl::deserialize(seq)?.to_dyn()),
DataComponent::Unbreakable => Ok(UnbreakableImpl::deserialize(seq)?.to_dyn()),
_ => todo!("{} not yet implemented", id.to_name()),
}
}
@@ -99,6 +109,7 @@ pub fn serialize<T: SerializeStruct>(
DataComponent::MaxStackSize => get::<MaxStackSizeImpl>(value).serialize(seq),
DataComponent::Enchantments => get::<EnchantmentsImpl>(value).serialize(seq),
DataComponent::Damage => get::<DamageImpl>(value).serialize(seq),
DataComponent::Unbreakable => get::<UnbreakableImpl>(value).serialize(seq),
_ => todo!("{} not yet implemented", id.to_name()),
}
}

View File

@@ -1,8 +1,8 @@
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component::DataComponent::Enchantments;
use pumpkin_data::data_component_impl::{
BlocksAttacksImpl, ConsumableImpl, DataComponentImpl, EnchantmentsImpl, IDSet,
MaxStackSizeImpl, ToolImpl, get, get_mut, read_data,
BlocksAttacksImpl, ConsumableImpl, DamageImpl, DataComponentImpl, EnchantmentsImpl, IDSet,
MaxDamageImpl, MaxStackSizeImpl, ToolImpl, UnbreakableImpl, get, get_mut, read_data,
};
use pumpkin_data::item::Item;
use pumpkin_data::recipes::RecipeResultStruct;
@@ -104,6 +104,122 @@ impl ItemStack {
}
}
pub fn get_max_damage(&self) -> Option<i32> {
self.get_data_component::<MaxDamageImpl>()
.map(|value| value.max_damage)
}
pub fn get_damage(&self) -> i32 {
self.get_data_component::<DamageImpl>()
.map(|value| value.damage)
.unwrap_or(0)
}
pub fn get_enchantment_level(&self, enchantment: &'static Enchantment) -> i32 {
let Some(data) = self.get_data_component::<EnchantmentsImpl>() else {
return 0;
};
for (enc, level) in data.enchantment.iter() {
if *enc == enchantment {
return *level;
}
}
0
}
pub fn is_unbreakable(&self) -> bool {
self.get_data_component::<UnbreakableImpl>().is_some()
}
pub fn set_damage(&mut self, damage: i32) {
let damage = damage.max(0);
if damage == 0 {
self.patch.retain(|(id, _)| *id != DataComponent::Damage);
return;
}
for (id, component) in self.patch.iter_mut() {
if *id == DataComponent::Damage {
*component = Some(DamageImpl { damage }.to_dyn());
return;
}
}
self.patch
.push((DataComponent::Damage, Some(DamageImpl { damage }.to_dyn())));
}
pub fn is_damageable(&self) -> bool {
self.get_max_damage().unwrap_or(0) > 0
}
pub fn repair_item(&mut self, amount: i32) -> i32 {
if amount <= 0 {
return 0;
}
let damage = self.get_damage();
if damage <= 0 {
return 0;
}
let repaired = amount.min(damage);
self.set_damage(damage - repaired);
repaired
}
fn should_apply_durability_damage(&self, is_armor: bool) -> bool {
let unbreaking_level = self.get_enchantment_level(&Enchantment::UNBREAKING);
if unbreaking_level <= 0 {
return true;
}
if is_armor {
let chance = 0.6 + (0.4 / (unbreaking_level as f32 + 1.0));
rand::random::<f32>() < chance
} else {
rand::random::<u32>().is_multiple_of(unbreaking_level as u32 + 1)
}
}
pub fn damage_item_with_context(&mut self, amount: i32, is_armor: bool) -> bool {
if amount <= 0 || !self.is_damageable() || self.is_unbreakable() {
return false;
}
let max_damage = self.get_max_damage().unwrap_or(0);
if max_damage <= 0 {
return false;
}
let mut applied = 0;
for _ in 0..amount {
if self.should_apply_durability_damage(is_armor) {
applied += 1;
}
}
if applied <= 0 {
return false;
}
let new_damage = self.get_damage().saturating_add(applied);
if new_damage >= max_damage {
if self.item_count > 1 {
self.item_count = self.item_count.saturating_sub(1);
self.set_damage(0);
} else {
*self = ItemStack::EMPTY.clone();
}
return true;
}
self.set_damage(new_damage);
true
}
pub fn damage_item(&mut self, amount: i32) -> bool {
self.damage_item_with_context(amount, false)
}
pub fn get_max_use_time(&self) -> i32 {
if let Some(value) = self.get_data_component::<ConsumableImpl>() {
return value.consume_ticks();

View File

@@ -123,7 +123,10 @@ impl EntityBase for ExperienceOrbEntity {
if *delay == 0 {
*delay = 2;
player.living_entity.pickup(&self.entity, 1).await;
player.add_experience_points(self.amount as i32).await;
let remaining = player.apply_mending_from_xp(self.amount as i32).await;
if remaining > 0 {
player.add_experience_points(remaining).await;
}
// TODO: pickingCount for merging
self.entity.remove().await;
}

View File

@@ -3,6 +3,7 @@ use pumpkin_data::potion::Effect;
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_inventory::build_equipment_slots;
use pumpkin_inventory::player::player_inventory::PlayerInventory;
use pumpkin_inventory::screen_handler::InventoryPlayer;
use pumpkin_util::Hand;
use pumpkin_util::math::position::BlockPos;
use std::mem;
@@ -32,7 +33,7 @@ use pumpkin_inventory::entity_equipment::EntityEquipment;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::{CHurtAnimation, CTakeItemEntity};
use pumpkin_protocol::java::client::play::{CHurtAnimation, CSetPlayerInventory, CTakeItemEntity};
use pumpkin_protocol::{
codec::item_stack_seralizer::ItemStackSerializer,
java::client::play::{CDamageEvent, CSetEquipment, Metadata},
@@ -899,6 +900,45 @@ impl LivingEntity {
false
}
async fn damage_armor_items(&self, caller: &dyn EntityBase, damage_amount: f32) {
let armor_damage = (damage_amount / 4.0).floor().max(1.0) as i32;
let mut equipment_updates = Vec::new();
for (slot_index, slot) in self.equipment_slots.iter() {
if !slot.is_armor_slot() {
continue;
}
let equipment = self.entity_equipment.lock().await.get(slot);
let updated_stack = {
let mut stack = equipment.lock().await;
if stack.is_empty() {
None
} else if stack.damage_item_with_context(armor_damage, true) {
Some(stack.clone())
} else {
None
}
};
if let Some(updated_stack) = updated_stack {
equipment_updates.push((slot.clone(), updated_stack.clone()));
if let Some(player) = caller.get_player() {
player
.enqueue_slot_set_packet(&CSetPlayerInventory::new(
(*slot_index as i32).into(),
&ItemStackSerializer::from(updated_stack),
))
.await;
}
}
}
if !equipment_updates.is_empty() {
self.send_equipment_changes(&equipment_updates).await;
}
}
pub async fn held_item(&self, caller: &dyn EntityBase) -> Arc<Mutex<ItemStack>> {
if let Some(player) = caller.get_player() {
return player.inventory.held_item();
@@ -1090,6 +1130,10 @@ impl EntityBase for LivingEntity {
self.on_death(damage_type, source, cause).await;
}
if damage_amount > 0.0 {
self.damage_armor_items(caller, damage_amount).await;
}
true
})
}

View File

@@ -30,13 +30,13 @@ use uuid::Uuid;
use pumpkin_data::damage::DamageType;
use pumpkin_data::data_component_impl::{AttributeModifiersImpl, Operation};
use pumpkin_data::data_component_impl::{EquipmentSlot, EquippableImpl};
use pumpkin_data::data_component_impl::{EquipmentSlot, EquippableImpl, ToolImpl};
use pumpkin_data::effect::StatusEffect;
use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType};
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::tag::Taggable;
use pumpkin_data::{Block, BlockState, tag};
use pumpkin_data::{Block, BlockState, Enchantment, tag};
use pumpkin_inventory::player::{
player_inventory::PlayerInventory, player_screen_handler::PlayerScreenHandler,
};
@@ -670,9 +670,79 @@ impl Player {
}
}
self.damage_held_item(1).await;
if config.swing {}
}
pub async fn sync_hand_slot(&self, slot_index: usize, stack: ItemStack) {
self.enqueue_slot_set_packet(&CSetPlayerInventory::new(
(slot_index as i32).into(),
&ItemStackSerializer::from(stack.clone()),
))
.await;
if slot_index == self.inventory.get_selected_slot() as usize {
self.living_entity
.send_equipment_changes(&[(EquipmentSlot::MAIN_HAND, stack)])
.await;
} else if slot_index == PlayerInventory::OFF_HAND_SLOT {
self.living_entity
.send_equipment_changes(&[(EquipmentSlot::OFF_HAND, stack)])
.await;
}
}
pub async fn damage_held_item(&self, amount: i32) -> bool {
if matches!(
self.gamemode.load(),
GameMode::Creative | GameMode::Spectator
) {
return false;
}
let slot_index = self.inventory.get_selected_slot() as usize;
let stack_arc = self.inventory.held_item();
let updated = {
let mut stack = stack_arc.lock().await;
stack
.damage_item_with_context(amount, false)
.then_some(stack.clone())
};
if let Some(updated_stack) = updated {
self.sync_hand_slot(slot_index, updated_stack).await;
return true;
}
false
}
pub async fn apply_tool_damage_for_block_break(&self, state: &BlockState) {
if matches!(
self.gamemode.load(),
GameMode::Creative | GameMode::Spectator
) {
return;
}
if state.hardness <= 0.0 {
return;
}
let damage = {
let stack = self.inventory.held_item();
let stack = stack.lock().await;
stack
.get_data_component::<ToolImpl>()
.map_or(0, |tool| tool.damage_per_block as i32)
};
if damage > 0 {
self.damage_held_item(damage).await;
}
}
pub async fn set_respawn_point(
&self,
dimension: Dimension,
@@ -1922,6 +1992,80 @@ impl Player {
self.set_experience(new_level, progress, new_points).await;
}
pub async fn apply_mending_from_xp(&self, mut xp: i32) -> i32 {
if xp <= 0 {
return xp;
}
let mut candidates: Vec<(usize, EquipmentSlot, Arc<Mutex<ItemStack>>)> = Vec::new();
let selected_slot = self.inventory.get_selected_slot() as usize;
let main_hand = self.inventory.get_stack(selected_slot).await;
let main_hand_eligible = {
let stack = main_hand.lock().await;
stack.get_enchantment_level(&Enchantment::MENDING) > 0 && stack.get_damage() > 0
};
if main_hand_eligible {
candidates.push((selected_slot, EquipmentSlot::MAIN_HAND, main_hand));
}
let offhand_slot = PlayerInventory::OFF_HAND_SLOT;
let off_hand = self.inventory.get_stack(offhand_slot).await;
let off_hand_eligible = {
let stack = off_hand.lock().await;
stack.get_enchantment_level(&Enchantment::MENDING) > 0 && stack.get_damage() > 0
};
if off_hand_eligible {
candidates.push((offhand_slot, EquipmentSlot::OFF_HAND, off_hand));
}
for (slot_index, slot) in self.inventory.equipment_slots.iter() {
if !slot.is_armor_slot() {
continue;
}
let stack = self.inventory.get_stack(*slot_index).await;
let eligible = {
let stack = stack.lock().await;
stack.get_enchantment_level(&Enchantment::MENDING) > 0 && stack.get_damage() > 0
};
if eligible {
candidates.push((*slot_index, slot.clone(), stack));
}
}
if candidates.is_empty() {
return xp;
}
let idx = rand::random::<u32>() as usize % candidates.len();
let (slot_index, equipment_slot, stack) = candidates.swap_remove(idx);
let (updated_stack, repaired) = {
let mut stack = stack.lock().await;
let repaired = stack.repair_item(xp.saturating_mul(2));
(stack.clone(), repaired)
};
if repaired <= 0 {
return xp;
}
let xp_used = (repaired + 1) / 2;
xp = xp.saturating_sub(xp_used);
self.enqueue_slot_set_packet(&CSetPlayerInventory::new(
(slot_index as i32).into(),
&ItemStackSerializer::from(updated_stack.clone()),
))
.await;
self.living_entity
.send_equipment_changes(&[(equipment_slot, updated_stack)])
.await;
xp
}
pub fn increment_screen_handler_sync_id(&self) {
let current_id = self.screen_handler_sync_id.load(Ordering::Relaxed);
self.screen_handler_sync_id

View File

@@ -8,6 +8,7 @@ use pumpkin_data::block_properties::BlockProperties;
use pumpkin_data::block_properties::{OakDoorLikeProperties, PaleOakWoodLikeProperties};
use pumpkin_data::tag::Taggable;
use pumpkin_data::{Block, tag};
use pumpkin_util::GameMode;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::item::ItemStack;
@@ -24,7 +25,7 @@ impl ItemMetadata for AxeItem {
impl ItemBehaviour for AxeItem {
fn use_on_block<'a>(
&'a self,
_item: &'a mut ItemStack,
item: &'a mut ItemStack,
player: &'a Player,
location: BlockPos,
_face: BlockDirection,
@@ -39,7 +40,7 @@ impl ItemBehaviour for AxeItem {
// First we try to strip the block. by getting his equivalent and applying it the axis.
// If there is a strip equivalent.
if replacement_block != 0 {
let changed = if replacement_block != 0 {
let new_block = &Block::from_id(replacement_block);
let new_state_id = if block.has_tag(&tag::Block::MINECRAFT_LOGS) {
let log_information = world.get_block_state_id(&location).await;
@@ -77,6 +78,13 @@ impl ItemBehaviour for AxeItem {
world
.set_block_state(&location, new_state_id, BlockFlags::NOTIFY_ALL)
.await;
true
} else {
false
};
if changed && player.gamemode.load() != GameMode::Creative {
item.damage_item_with_context(1, false);
}
})
}

View File

@@ -7,6 +7,7 @@ use pumpkin_data::BlockDirection;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::{Block, tag};
use pumpkin_util::GameMode;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::item::ItemStack;
@@ -26,7 +27,7 @@ impl ItemMetadata for HoeItem {
impl ItemBehaviour for HoeItem {
fn use_on_block<'a>(
&'a self,
_item: &'a mut ItemStack,
item: &'a mut ItemStack,
player: &'a Player,
location: BlockPos,
face: BlockDirection,
@@ -44,11 +45,13 @@ impl ItemBehaviour for HoeItem {
{
let mut future_block = block;
let world = player.world();
let mut changed = false;
//Only rooted can be right-clicked on the bottom of the block
if face == BlockDirection::Down {
if block == &Block::ROOTED_DIRT {
future_block = &Block::DIRT;
changed = true;
}
} else {
// grass, dirt && dirt path become farmland
@@ -58,10 +61,12 @@ impl ItemBehaviour for HoeItem {
&& world.get_block_state(&location.up()).await.is_air()
{
future_block = &Block::FARMLAND;
changed = true;
}
//Coarse dirt and rooted dirt become dirt
else if block == &Block::COARSE_DIRT || block == &Block::ROOTED_DIRT {
future_block = &Block::DIRT;
changed = true;
}
}
@@ -96,6 +101,10 @@ impl ItemBehaviour for HoeItem {
);
world.spawn_entity(item_entity).await;
}
if changed && player.gamemode.load() != GameMode::Creative {
item.damage_item_with_context(1, false);
}
}
})
}

View File

@@ -26,7 +26,7 @@ impl ItemMetadata for FlintAndSteelItem {
impl ItemBehaviour for FlintAndSteelItem {
fn use_on_block<'a>(
&'a self,
_item: &'a mut ItemStack,
item: &'a mut ItemStack,
player: &'a Player,
location: BlockPos,
face: BlockDirection,
@@ -35,7 +35,7 @@ impl ItemBehaviour for FlintAndSteelItem {
_server: &'a Server,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async move {
Ignition::ignite_block(
let ignited = Ignition::ignite_block(
|world: Arc<World>, pos: BlockPos, new_state_id: u16| async move {
world
.set_block_state(&pos, new_state_id, BlockFlags::NOTIFY_ALL)
@@ -47,6 +47,10 @@ impl ItemBehaviour for FlintAndSteelItem {
block,
)
.await;
if ignited && player.gamemode.load() != pumpkin_util::GameMode::Creative {
item.damage_item_with_context(1, false);
}
})
}

View File

@@ -16,7 +16,8 @@ impl Ignition {
location: BlockPos,
face: BlockDirection,
block: &Block,
) where
) -> bool
where
F: FnOnce(Arc<World>, BlockPos, u16) -> Fut,
Fut: Future<Output = ()>,
{
@@ -24,7 +25,7 @@ impl Ignition {
let pos = location.offset(face.to_offset());
if world.get_fluid(&location).await.name != Fluid::EMPTY.name {
return;
return false;
}
let fire_block = FireBlockBase::get_fire_type(world, &pos).await;
@@ -32,7 +33,7 @@ impl Ignition {
if let Some(new_state_id) = can_be_lit(block, state_id) {
ignite_logic(world.clone(), location, new_state_id).await;
return;
return true;
}
let state_id = FireBlock
@@ -40,7 +41,10 @@ impl Ignition {
.await;
if FireBlockBase::can_place_at(world, &pos).await {
ignite_logic(world.clone(), pos, state_id).await;
return true;
}
false
}
}

View File

@@ -8,6 +8,7 @@ use pumpkin_data::block_properties::{BlockProperties, CampfireLikeProperties};
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::world::WorldEvent;
use pumpkin_data::{Block, tag};
use pumpkin_util::GameMode;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::item::ItemStack;
@@ -25,7 +26,7 @@ impl ItemMetadata for ShovelItem {
impl ItemBehaviour for ShovelItem {
fn use_on_block<'a>(
&'a self,
_item: &'a mut ItemStack,
item: &'a mut ItemStack,
player: &'a Player,
location: BlockPos,
face: BlockDirection,
@@ -36,7 +37,7 @@ impl ItemBehaviour for ShovelItem {
Box::pin(async move {
let world = player.world();
// Yes, Minecraft does hardcode these
if (block == &Block::GRASS_BLOCK
let mut changed = if (block == &Block::GRASS_BLOCK
|| block == &Block::DIRT
|| block == &Block::COARSE_DIRT
|| block == &Block::ROOTED_DIRT
@@ -52,7 +53,10 @@ impl ItemBehaviour for ShovelItem {
BlockFlags::NOTIFY_ALL,
)
.await;
}
true
} else {
false
};
if block == &Block::CAMPFIRE || block == &Block::SOUL_CAMPFIRE {
let mut campfire_props = CampfireLikeProperties::from_state_id(
world.get_block_state(&location).await.id,
@@ -82,8 +86,13 @@ impl ItemBehaviour for ShovelItem {
seed,
)
.await;
changed = true;
}
}
if changed && player.gamemode.load() != GameMode::Creative {
item.damage_item_with_context(1, false);
}
})
}

View File

@@ -1313,6 +1313,7 @@ impl JavaClient {
.block_registry
.broken(world, block, player, &position, server, broken_state)
.await;
player.apply_tool_damage_for_block_break(broken_state).await;
} else {
player.mining.store(true, Ordering::Relaxed);
*player.mining_pos.lock().await = position;
@@ -1381,6 +1382,7 @@ impl JavaClient {
.block_registry
.broken(world, block, player, &location, server, state)
.await;
player.apply_tool_damage_for_block_break(state).await;
self.update_sequence(player, player_action.sequence.0);
}
@@ -1516,6 +1518,12 @@ impl JavaClient {
return Ok(());
}
}
let slot_index = if matches!(hand, Hand::Left) {
inventory.get_selected_slot() as usize
} else {
PlayerInventory::OFF_HAND_SLOT
};
let mut stack = item.lock().await;
if stack.is_empty() {
@@ -1524,6 +1532,8 @@ impl JavaClient {
return Ok(());
}
let before = stack.clone();
server
.item_registry
.use_on_block(
@@ -1547,6 +1557,12 @@ impl JavaClient {
}
}
let after = stack.clone();
drop(stack);
if !after.are_equal(&before) {
player.sync_hand_slot(slot_index, after).await;
}
Ok(())
}