use crate::enchantments::AttributeModifierSlot; use heck::ToShoutySnakeCase; use proc_macro2::{Span, TokenStream}; use pumpkin_util::registry::TagType; use pumpkin_util::text::TextContent; use pumpkin_util::{registry::RegistryEntryList, text::TextComponent}; use quote::{ToTokens, format_ident, quote}; use serde::Deserialize; use std::{collections::BTreeMap, fs}; use syn::{Ident, LitBool, LitFloat, LitInt, LitStr}; #[derive(Deserialize)] pub struct Item { pub id: u16, pub components: ItemComponents, } #[derive(Deserialize)] pub struct ItemComponents { #[serde(rename = "minecraft:item_name")] pub item_name: TextComponent, #[serde(rename = "minecraft:max_stack_size")] pub max_stack_size: u8, #[serde(rename = "minecraft:jukebox_playable")] pub jukebox_playable: Option, #[serde(rename = "minecraft:damage")] pub damage: Option, #[serde(rename = "minecraft:max_damage")] pub max_damage: Option, #[serde(rename = "minecraft:attribute_modifiers")] pub attribute_modifiers: Option>, #[serde(rename = "minecraft:tool")] pub tool: Option, #[serde(rename = "minecraft:food")] pub food: Option, #[serde(rename = "minecraft:equippable")] pub equippable: Option, #[serde(rename = "minecraft:consumable")] pub consumable: Option, #[serde(rename = "minecraft:blocks_attacks")] pub blocks_attacks: Option, #[serde(rename = "minecraft:death_protection")] pub death_protection: Option, } impl ToTokens for ItemComponents { fn to_tokens(&self, tokens: &mut TokenStream) { let max_stack_size = LitInt::new(&self.max_stack_size.to_string(), Span::call_site()); tokens.extend(quote! { (MaxStackSize, &MaxStackSizeImpl { size: #max_stack_size, }), }); if let Some(playable) = &self.jukebox_playable { let song = LitStr::new(playable, Span::call_site()); tokens.extend(quote! { (JukeboxPlayable, &JukeboxPlayableImpl{ song: #song, }), }); } let TextContent::Translate { translate: text, with: _, } = *self.item_name.clone().0.content else { unreachable!() }; let item_name = LitStr::new(&text, Span::call_site()); tokens.extend(quote! { (ItemName, &ItemNameImpl { name: #item_name, }), }); if let Some(d) = self.damage { let damage_lit = LitInt::new(&d.to_string(), Span::call_site()); tokens.extend(quote! { (Damage, &DamageImpl { damage: #damage_lit, }), }); } if let Some(md) = self.max_damage { let max_damage_lit = LitInt::new(&md.to_string(), Span::call_site()); tokens.extend(quote! { (MaxDamage, &MaxDamageImpl { max_damage: #max_damage_lit, }), }); } if let Some(modifiers) = &self.attribute_modifiers { let modifier_code = modifiers.iter().map(|modifier| { let r#type = format_ident!( "{}", modifier .r#type .strip_prefix("minecraft:") .unwrap() .to_uppercase() ); let id = LitStr::new(&modifier.id, Span::call_site()); let amount = modifier.amount; let operation = Ident::new(&format!("{:?}", modifier.operation), Span::call_site()); let slot = modifier.slot.to_tokens(); quote! { Modifier { r#type: &Attributes::#r#type, id: #id, amount: #amount, operation: Operation::#operation, slot: #slot, } } }); tokens.extend(quote! { (AttributeModifiers, &AttributeModifiersImpl { attribute_modifiers: Cow::Borrowed(&[#(#modifier_code),*]) }), }); } if let Some(tool) = &self.tool { let rules_code = tool.rules.iter().map(|rule| { let block_array; if let RegistryEntryList::Single(t) = &rule.blocks { if let TagType::Item(str) = t { let ident = format_ident!( "{}", str.strip_prefix("minecraft:").unwrap().to_uppercase() ); block_array = quote! { Blocks(Cow::Borrowed(&[&Block::#ident])) } } else if let TagType::Tag(str) = t { let ident = format_ident!("{}", str.replace([':', '/'], "_").to_uppercase()); block_array = quote! { Tag(&tag::Block::#ident) } } else { unreachable!(); } } else if let RegistryEntryList::Many(t) = &rule.blocks { let mut array = vec![]; for i in t { let TagType::Item(str) = i else { unreachable!(); }; let ident = format_ident!( "{}", str.strip_prefix("minecraft:").unwrap().to_uppercase() ); array.push(quote! { &Block::#ident }); } block_array = quote! { Blocks(Cow::Borrowed(&[#(#array),*])) } } else { unreachable!(); } let speed = if let Some(speed) = rule.speed { quote! { Some(#speed) } } else { quote! { None } }; let correct_for_drops = if let Some(correct_for_drops) = rule.correct_for_drops { let correct_for_drops = LitBool::new(correct_for_drops, Span::call_site()); quote! { Some(#correct_for_drops) } } else { quote! { None } }; quote! { ToolRule { blocks: #block_array, speed: #speed, correct_for_drops: #correct_for_drops } } }); let damage_per_block = { let speed = LitInt::new(&tool.damage_per_block.to_string(), Span::call_site()); quote! { #speed } }; let default_mining_speed = { let speed = LitFloat::new( &format!("{:.1}", tool.default_mining_speed), Span::call_site(), ); quote! { #speed } }; let can_destroy_blocks_in_creative = LitBool::new(tool.can_destroy_blocks_in_creative, Span::call_site()); tokens.extend(quote! { (Tool, &ToolImpl { rules: Cow::Borrowed(&[#(#rules_code),*]), default_mining_speed: #default_mining_speed, damage_per_block: #damage_per_block, can_destroy_blocks_in_creative: #can_destroy_blocks_in_creative }), }); } if let Some(food) = &self.food { let nutrition = LitInt::new(&food.nutrition.to_string(), Span::call_site()); let saturation = LitFloat::new(&format!("{:.1}", food.saturation), Span::call_site()); let can_always_eat = { let can = LitBool::new(food.can_always_eat, Span::call_site()); quote! { #can } }; tokens.extend(quote! { (Food, &FoodImpl { nutrition: #nutrition, saturation: #saturation, can_always_eat: #can_always_eat, }), }); } if let Some(consumable) = &self.consumable { let consume_seconds = LitFloat::new( &format!("{:.1}", consumable.consume_seconds.unwrap_or(1.6)), Span::call_site(), ); tokens.extend(quote! { (Consumable, &ConsumableImpl { consume_seconds: #consume_seconds, }), }); } if self.blocks_attacks.is_some() { tokens.extend(quote! { (BlocksAttacks, &BlocksAttacksImpl), }); } if self.death_protection.is_some() { tokens.extend(quote! { (DeathProtection, &DeathProtectionImpl), }); } 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 .into_vec() .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([':', '/'], "_").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 }), }); } } } const fn return_1u32() -> u32 { 1 } const fn return_1f32() -> f32 { 1. } const fn return_true() -> bool { true } #[derive(Deserialize)] pub struct ToolComponent { rules: Vec, #[serde(default = "return_1f32")] default_mining_speed: f32, #[serde(default = "return_1u32")] damage_per_block: u32, #[serde(default = "return_true")] can_destroy_blocks_in_creative: bool, } const fn return_false() -> bool { false } #[derive(Deserialize, Copy, Clone)] pub struct FoodComponent { nutrition: u8, saturation: f32, #[serde(default = "return_false")] can_always_eat: bool, } #[derive(Deserialize, Clone)] pub struct ToolRule { blocks: RegistryEntryList, speed: Option, correct_for_drops: Option, } #[derive(Deserialize, Clone)] pub struct Modifier { pub r#type: String, pub id: String, pub amount: f64, pub operation: Operation, // TODO: Make this an enum pub slot: AttributeModifierSlot, } const fn _true() -> bool { true } #[derive(Deserialize, Clone)] pub struct Consumable { consume_seconds: Option, // TODO } #[derive(Deserialize, Clone)] pub struct DeathProtection { // TODO } #[derive(Deserialize, Clone)] pub struct BlocksAttacks { // TODO } #[derive(Deserialize, Clone)] pub struct EquippableComponent { pub slot: String, pub equip_sound: Option, pub asset_id: Option, pub camera_overlay: Option, pub allowed_entities: Option, #[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, } #[derive(Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] #[expect(clippy::enum_variant_names)] pub enum Operation { AddValue, AddMultipliedBase, AddMultipliedTotal, } pub fn build() -> TokenStream { let items: BTreeMap = serde_json::from_str(&fs::read_to_string("../assets/items.json").unwrap()) .expect("Failed to parse items.json"); let mut type_from_raw_id_arms = TokenStream::new(); let mut type_from_name = TokenStream::new(); let mut constants = TokenStream::new(); for (name, item) in items { let const_ident = format_ident!("{}", name.to_shouty_snake_case()); let components = &item.components; let components_tokens = components.to_token_stream(); let id_lit = LitInt::new(&item.id.to_string(), Span::call_site()); constants.extend(quote! { pub const #const_ident: Item = Item { id: #id_lit, registry_key: #name, components: &[#components_tokens], }; }); type_from_raw_id_arms.extend(quote! { #id_lit => Some(&Self::#const_ident), }); type_from_name.extend(quote! { #name => Some(&Self::#const_ident), }); } quote! { use crate::data_component::DataComponent::*; use crate::data_component_impl::*; use crate::tag::{RegistryKey, Taggable}; use pumpkin_util::text::TextComponent; use std::borrow::Cow; use std::hash::{Hash, Hasher}; use crate::{tag, AttributeModifierSlot}; use crate::attributes::Attributes; use crate::data_component_impl::IDSet::{Blocks, Tag}; use crate::data_component::DataComponent; use crate::Block; #[derive(Clone)] pub struct Item { pub id: u16, pub registry_key: &'static str, pub components: &'static [(DataComponent, &'static dyn DataComponentImpl)], } impl PartialEq for Item { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for Item {} impl Hash for Item { fn hash(&self, state: &mut H) { self.id.hash(state); } } impl Item { #constants pub fn translated_name(&self) -> TextComponent { TextComponent::translate( self.components .iter() .find_map(|(id, data)| if id == &ItemName { Some(data.as_any().downcast_ref::().unwrap().name) } else { None } ).unwrap(), &[], ) } #[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 } } #[doc = "Try to parse an item from a raw id."] pub const fn from_id(id: u16) -> Option<&'static Self> { match id { #type_from_raw_id_arms _ => None } } } impl Taggable for Item { #[inline] fn tag_key() -> RegistryKey { RegistryKey::Item } #[inline] fn registry_key(&self) -> &str { self.registry_key } #[inline] fn registry_id(&self) -> u16 { self.id } } } }