Add EquippableComponent; Handle armour items more elegantly (#1052)

* Partially revert #1008

commit a378e48a04

* feat: equippable component

* chore

* chore

* fix

* fix

* fix: slot & equip_sound

* fix: reg
This commit is contained in:
Liyan Zhao
2025-07-31 04:31:13 +08:00
committed by GitHub
parent dbaf240da5
commit 43534cbffd
13 changed files with 390 additions and 205 deletions

View File

@@ -33,6 +33,8 @@ pub struct ItemComponents {
pub tool: Option<ToolComponent>,
#[serde(rename = "minecraft:food")]
pub food: Option<FoodComponent>,
#[serde(rename = "minecraft:equippable")]
pub equippable: Option<EquippableComponent>,
}
impl ToTokens for ItemComponents {
@@ -215,6 +217,104 @@ impl ToTokens for ItemComponents {
can_always_eat: #can_always_eat,
}), });
};
if let Some(equippable) = &self.equippable {
let slot = match equippable.slot.as_str() {
"mainhand" => quote! { &EquipmentSlot::MAIN_HAND },
"offhand" => quote! { &EquipmentSlot::OFF_HAND },
"head" => quote! { &EquipmentSlot::HEAD },
"chest" => quote! { &EquipmentSlot::CHEST },
"legs" => quote! { &EquipmentSlot::LEGS },
"feet" => quote! { &EquipmentSlot::FEET },
"body" => quote! { &EquipmentSlot::BODY },
"saddle" => quote! { &EquipmentSlot::SADDLE },
_ => panic!("Unknown equippable slot: {}", equippable.slot),
};
let equip_sound = equippable
.equip_sound
.as_ref()
.map(|s| {
let equip_sound = LitStr::new(s, Span::call_site());
quote! { #equip_sound }
})
.unwrap_or(quote! { "item.armor.equip_generic" });
let asset_id = equippable
.asset_id
.as_ref()
.map(|s| {
let asset_id = LitStr::new(s, Span::call_site());
quote! { Some(#asset_id) }
})
.unwrap_or(quote! { None });
let camera_overlay = equippable
.camera_overlay
.as_ref()
.map(|s| {
let camera_overlay = LitStr::new(s, Span::call_site());
quote! { Some(#camera_overlay) }
})
.unwrap_or(quote! { None });
let allowed_entities = equippable
.allowed_entities
.clone()
.map(|list| {
let vec: Vec<_> = list
.get_values()
.iter()
.map(|reg| {
match reg {
TagType::Item(item) => {
let ident = format_ident!(
"{}",
item.strip_prefix("minecraft:").unwrap().to_uppercase()
);
quote! { EntityTypeOrTag::Single(&crate::entity_type::EntityType::#ident) }
},
TagType::Tag(tag) => {
let ident = format_ident!(
"{}",
tag.replace(":", "_").replace("/", "_").to_uppercase()
);
quote! { EntityTypeOrTag::Tag(&crate::tag::EntityType::#ident) }
}
}
})
.collect();
quote! {
Some(&[#(#vec),*])
}
})
.unwrap_or(quote! { None });
let dispensable = LitBool::new(equippable.dispensable, Span::call_site());
let swappable = LitBool::new(equippable.swappable, Span::call_site());
let damage_on_hurt = LitBool::new(equippable.damage_on_hurt, Span::call_site());
let equip_on_interact = LitBool::new(equippable.equip_on_interact, Span::call_site());
let can_be_sheared = LitBool::new(equippable.can_be_sheared, Span::call_site());
let shearing_sound = equippable
.shearing_sound
.as_ref()
.map(|s| {
let shearing_sound = LitStr::new(s, Span::call_site());
quote! {
Some(#shearing_sound)
}
})
.unwrap_or(quote! { None });
tokens.extend(quote! { (Equippable, &EquippableImpl {
slot: #slot,
equip_sound: #equip_sound,
asset_id: #asset_id,
camera_overlay: #camera_overlay,
allowed_entities: #allowed_entities,
dispensable: #dispensable,
swappable: #swappable,
damage_on_hurt: #damage_on_hurt,
equip_on_interact: #equip_on_interact,
can_be_sheared: #can_be_sheared,
shearing_sound: #shearing_sound
}), });
};
}
}
@@ -269,6 +369,31 @@ pub struct Modifier {
pub slot: AttributeModifierSlot,
}
fn _true() -> bool {
true
}
#[allow(dead_code)]
#[derive(Deserialize, Clone, Debug)]
pub struct EquippableComponent {
pub slot: String,
pub equip_sound: Option<String>,
pub asset_id: Option<String>,
pub camera_overlay: Option<String>,
pub allowed_entities: Option<RegistryEntryList>,
#[serde(default = "_true")]
pub dispensable: bool,
#[serde(default = "_true")]
pub swappable: bool,
#[serde(default = "_true")]
pub damage_on_hurt: bool,
#[serde(default)]
pub equip_on_interact: bool,
#[serde(default)]
pub can_be_sheared: bool,
pub shearing_sound: Option<String>,
}
#[derive(Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
#[allow(clippy::enum_variant_names)]
@@ -368,6 +493,7 @@ pub(crate) fn build() -> TokenStream {
#[doc = "Try to parse an item from a resource location string."]
pub fn from_registry_key(name: &str) -> Option<&'static Self> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
match name {
#type_from_name
_ => None

View File

@@ -3,8 +3,10 @@
use crate::attributes::Attributes;
use crate::data_component::DataComponent;
use crate::data_component::DataComponent::*;
use crate::entity_type::EntityType;
use crate::tag::Tag;
use crate::{AttributeModifierSlot, Block};
use pumpkin_util::registry::RegistryEntryList;
use pumpkin_util::text::TextComponent;
use std::any::Any;
use std::borrow::Cow;
@@ -320,10 +322,218 @@ impl Hash for ToolImpl {
}
#[derive(Clone, Debug, Hash)]
pub struct WeaponImpl;
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum EquipmentType {
Hand,
HumanoidArmor,
AnimalArmor,
Saddle,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct EquipmentSlotData {
pub slot_type: EquipmentType,
pub entity_id: i32,
pub max_count: i32,
pub index: i32,
pub name: Cow<'static, str>,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
#[repr(i8)]
pub enum EquipmentSlot {
MainHand(EquipmentSlotData),
OffHand(EquipmentSlotData),
Feet(EquipmentSlotData),
Legs(EquipmentSlotData),
Chest(EquipmentSlotData),
Head(EquipmentSlotData),
Body(EquipmentSlotData),
Saddle(EquipmentSlotData),
}
impl EquipmentSlot {
pub const MAIN_HAND: Self = Self::MainHand(EquipmentSlotData {
slot_type: EquipmentType::Hand,
entity_id: 0,
index: 0,
max_count: 0,
name: Cow::Borrowed("mainhand"),
});
pub const OFF_HAND: Self = Self::OffHand(EquipmentSlotData {
slot_type: EquipmentType::Hand,
entity_id: 1,
index: 5,
max_count: 0,
name: Cow::Borrowed("offhand"),
});
pub const FEET: Self = Self::Feet(EquipmentSlotData {
slot_type: EquipmentType::HumanoidArmor,
entity_id: 0,
index: 1,
max_count: 1,
name: Cow::Borrowed("feet"),
});
pub const LEGS: Self = Self::Legs(EquipmentSlotData {
slot_type: EquipmentType::HumanoidArmor,
entity_id: 1,
index: 2,
max_count: 1,
name: Cow::Borrowed("legs"),
});
pub const CHEST: Self = Self::Chest(EquipmentSlotData {
slot_type: EquipmentType::HumanoidArmor,
entity_id: 2,
index: 3,
max_count: 1,
name: Cow::Borrowed("chest"),
});
pub const HEAD: Self = Self::Head(EquipmentSlotData {
slot_type: EquipmentType::HumanoidArmor,
entity_id: 3,
index: 4,
max_count: 1,
name: Cow::Borrowed("head"),
});
pub const BODY: Self = Self::Body(EquipmentSlotData {
slot_type: EquipmentType::AnimalArmor,
entity_id: 0,
index: 6,
max_count: 1,
name: Cow::Borrowed("body"),
});
pub const SADDLE: Self = Self::Saddle(EquipmentSlotData {
slot_type: EquipmentType::Saddle,
entity_id: 0,
index: 7,
max_count: 1,
name: Cow::Borrowed("saddle"),
});
pub fn get_entity_slot_id(&self) -> i32 {
match self {
Self::MainHand(data) => data.entity_id,
Self::OffHand(data) => data.entity_id,
Self::Feet(data) => data.entity_id,
Self::Legs(data) => data.entity_id,
Self::Chest(data) => data.entity_id,
Self::Head(data) => data.entity_id,
Self::Body(data) => data.entity_id,
Self::Saddle(data) => data.entity_id,
}
}
pub fn get_from_name(name: &str) -> Option<Self> {
match name {
"mainhand" => Some(Self::MAIN_HAND),
"offhand" => Some(Self::OFF_HAND),
"feet" => Some(Self::FEET),
"legs" => Some(Self::LEGS),
"chest" => Some(Self::CHEST),
"head" => Some(Self::HEAD),
"body" => Some(Self::BODY),
"saddle" => Some(Self::SADDLE),
_ => None,
}
}
pub fn get_offset_entity_slot_id(&self, offset: i32) -> i32 {
self.get_entity_slot_id() + offset
}
pub fn slot_type(&self) -> EquipmentType {
match self {
Self::MainHand(data) => data.slot_type,
Self::OffHand(data) => data.slot_type,
Self::Feet(data) => data.slot_type,
Self::Legs(data) => data.slot_type,
Self::Chest(data) => data.slot_type,
Self::Head(data) => data.slot_type,
Self::Body(data) => data.slot_type,
Self::Saddle(data) => data.slot_type,
}
}
pub fn is_armor_slot(&self) -> bool {
matches!(
self.slot_type(),
EquipmentType::HumanoidArmor | EquipmentType::AnimalArmor
)
}
pub const fn discriminant(&self) -> i8 {
match self {
Self::MainHand(_) => 0,
Self::OffHand(_) => 1,
Self::Feet(_) => 2,
Self::Legs(_) => 3,
Self::Chest(_) => 4,
Self::Head(_) => 5,
Self::Body(_) => 6,
Self::Saddle(_) => 7,
}
}
}
#[derive(Clone, Debug)]
pub enum EntityTypeOrTag {
Tag(&'static Tag),
Single(&'static EntityType),
}
impl Hash for EntityTypeOrTag {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
EntityTypeOrTag::Tag(tag) => {
for x in tag.0 {
x.hash(state);
}
}
EntityTypeOrTag::Single(entity_type) => {
entity_type.id.hash(state);
}
}
}
}
#[derive(Clone, Debug, Hash)]
pub struct EnchantableImpl;
#[derive(Clone, Debug, Hash)]
pub struct EquippableImpl;
pub struct EquippableImpl {
pub slot: &'static EquipmentSlot,
pub equip_sound: &'static str,
pub asset_id: Option<&'static str>,
pub camera_overlay: Option<&'static str>,
// pub allowed_entities: Option<&'static [&'static str]>,
pub allowed_entities: Option<&'static [EntityTypeOrTag]>,
pub dispensable: bool,
pub swappable: bool,
pub damage_on_hurt: bool,
pub equip_on_interact: bool,
pub can_be_sheared: bool,
pub shearing_sound: Option<&'static str>,
}
impl DataComponentImpl for EquippableImpl {
fn get_enum() -> DataComponent
where
Self: Sized,
{
Equippable
}
fn clone_dyn(&self) -> Box<dyn DataComponentImpl> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_mut_any(&mut self) -> &mut dyn Any {
self
}
}
#[derive(Clone, Debug, Hash)]
pub struct RepairableImpl;
#[derive(Clone, Debug, Hash)]