use std::{collections::BTreeMap, fs}; use heck::ToShoutySnakeCase; use proc_macro2::TokenStream; use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextContent::Translate; use quote::{format_ident, quote}; use serde::Deserialize; /// Raw deserialization shape for a single enchantment entry from `enchantments.json`. #[derive(Deserialize)] pub struct Enchantment { /// Numeric registry ID for this enchantment. pub id: u8, /// Anvil repair cost multiplier added when applying this enchantment. pub anvil_cost: u32, /// Tag path (prefixed with `#`) of items that support this enchantment. pub supported_items: String, /// Display name component for this enchantment (typically a translation key). pub description: TextComponent, /// Optional exclusive-set tag; enchantments in the same set are mutually incompatible. pub exclusive_set: Option, /// Maximum level this enchantment can reach. pub max_level: i32, /// Equipment slots this enchantment's attribute modifiers apply to. pub slots: Vec, // TODO: add more /// The weight of this enchantment (used for random selection). pub weight: i32, /// The minimum cost to get this enchantment. pub min_cost: Cost, /// The maximum cost to get this enchantment. pub max_cost: Cost, } #[derive(Deserialize, Clone, Copy)] pub struct Cost { pub base: i32, pub per_level_above_first: i32, } /// Equipment slot category that an enchantment's attribute modifier applies to. #[derive(Deserialize, Clone)] #[serde(rename_all = "lowercase")] pub enum AttributeModifierSlot { /// Applies in any equipment slot. Any, /// Applies only when held in the main hand. MainHand, /// Applies only when held in the offhand. OffHand, /// Applies when held in either hand. Hand, /// Applies when worn on the feet. Feet, /// Applies when worn on the legs. Legs, /// Applies when worn on the chest. Chest, /// Applies when worn on the head. Head, /// Applies when wearing any piece of armor. Armor, /// Applies when worn as a body armor piece (e.g. on horses or wolves). Body, /// Applies when placed in a saddle slot. Saddle, } impl AttributeModifierSlot { /// Converts this slot variant into a `TokenStream` for use in generated code. pub fn to_tokens(&self) -> TokenStream { match self { Self::Any => quote! { AttributeModifierSlot::Any }, Self::MainHand => quote! { AttributeModifierSlot::MainHand }, Self::OffHand => quote! { AttributeModifierSlot::OffHand }, Self::Hand => quote! { AttributeModifierSlot::Hand }, Self::Feet => quote! { AttributeModifierSlot::Feet }, Self::Legs => quote! { AttributeModifierSlot::Legs }, Self::Chest => quote! { AttributeModifierSlot::Chest }, Self::Head => quote! { AttributeModifierSlot::Head }, Self::Armor => quote! { AttributeModifierSlot::Armor }, Self::Body => quote! { AttributeModifierSlot::Body }, Self::Saddle => quote! { AttributeModifierSlot::Saddle }, } } } /// Generates the `TokenStream` for the `Enchantment` struct, `AttributeModifierSlot` enum, /// and `from_name`/`from_id` lookup methods. pub fn build() -> TokenStream { let enchantments: BTreeMap = serde_json::from_str(&fs::read_to_string("../assets/enchantments.json").unwrap()) .expect("Failed to parse enchantments.json"); let mut variants = TokenStream::new(); let mut all_variants = TokenStream::new(); let mut name_to_type = TokenStream::new(); let mut id_to_type = TokenStream::new(); for (name, enchantment) in enchantments { let id = enchantment.id; let raw_name = name.strip_prefix("minecraft:").unwrap(); let format_name = format_ident!("{}", raw_name.to_shouty_snake_case()); all_variants.extend(quote! { &Self::#format_name, }); let anvil_cost = enchantment.anvil_cost; let supported_items = format_ident!( "{}", enchantment .supported_items .strip_prefix("#") .unwrap() .replace([':', '/'], "_") .to_uppercase() ); let max_level = enchantment.max_level; let weight = enchantment.weight; let min_cost_base = enchantment.min_cost.base; let min_cost_per_level = enchantment.min_cost.per_level_above_first; let max_cost_base = enchantment.max_cost.base; let max_cost_per_level = enchantment.max_cost.per_level_above_first; let slots = enchantment.slots; let slots = slots.iter().map(AttributeModifierSlot::to_tokens); let Translate { translate, bedrock_translate: _, with: _ } = &*enchantment.description.0.content else { panic!() }; let translate = translate.to_string(); if let Some(exclusive_set) = &enchantment.exclusive_set { let exclusive_set = format_ident!( "{}", exclusive_set .strip_prefix("#") .unwrap() .replace([':', '/'], "_") .to_uppercase() ); variants.extend([quote! { pub const #format_name: Self = Self { id: #id, name: #name, registry_key: #raw_name, description: #translate, anvil_cost: #anvil_cost, supported_items: &ItemTag::#supported_items, exclusive_set: Some(&EnchantmentTag::#exclusive_set), max_level: #max_level, slots: &[#(#slots),*], weight: #weight, min_cost: Cost { base: #min_cost_base, per_level_above_first: #min_cost_per_level, }, max_cost: Cost { base: #max_cost_base, per_level_above_first: #max_cost_per_level, }, }; }]); } else { variants.extend([quote! { pub const #format_name: Self = Self { id: #id, name: #name, description: #translate, registry_key: #raw_name, anvil_cost: #anvil_cost, supported_items: &ItemTag::#supported_items, exclusive_set: None, max_level: #max_level, slots: &[#(#slots),*], weight: #weight, min_cost: Cost { base: #min_cost_base, per_level_above_first: #min_cost_per_level, }, max_cost: Cost { base: #max_cost_base, per_level_above_first: #max_cost_per_level, }, }; }]); } name_to_type.extend(quote! { #name => Some(&Self::#format_name), }); id_to_type.extend(quote! { #id => Some(&Self::#format_name), }); } quote! { use crate::item::Item; use crate::tag::Enchantment as EnchantmentTag; use crate::tag::Item as ItemTag; use crate::tag::{RegistryKey, Tag, Taggable}; use crate::data_component_impl::EnchantmentsImpl; use pumpkin_util::text::TextComponent; use pumpkin_util::text::color::NamedColor; use std::hash::{Hash, Hasher}; use std::slice::Iter; pub struct Enchantment { pub id: u8, pub name: &'static str, pub registry_key: &'static str, pub description: &'static str, // TODO use TextComponent pub anvil_cost: u32, pub supported_items: &'static Tag, pub exclusive_set: Option<&'static Tag>, pub max_level: i32, pub slots: &'static [AttributeModifierSlot], pub weight: i32, pub min_cost: Cost, pub max_cost: Cost, // TODO: add more } #[derive(Clone, Copy, Debug)] pub struct Cost { pub base: i32, pub per_level_above_first: i32, } impl Cost { pub fn calculate(&self, level: i32) -> i32 { self.base + self.per_level_above_first * (level - 1) } } impl Taggable for Enchantment { #[inline] fn tag_key() -> RegistryKey { RegistryKey::Enchantment } #[inline] fn registry_key(&self) -> &str { self.registry_key } #[inline] fn registry_id(&self) -> u16 { self.id as u16 } } impl PartialEq for Enchantment { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for Enchantment {} impl Hash for Enchantment { fn hash(&self, state: &mut H) { self.id.hash(state); } } #[derive(Debug, Clone, Hash, PartialEq)] pub enum AttributeModifierSlot { Any, MainHand, OffHand, Hand, Feet, Legs, Chest, Head, Armor, Body, Saddle, } impl Enchantment { pub const ALL: &'static [&'static Self] = &[#all_variants]; pub fn all() -> Iter<'static, &'static Self> { Self::ALL.iter() } #variants pub fn from_name(name: &str) -> Option<&'static Self> { match name { #name_to_type _ => None } } pub fn from_id(id: u8) -> Option<&'static Self> { match id { #id_to_type _ => None } } pub fn can_enchant(&self, item: &'static Item) -> bool { self.supported_items.1.contains(&item.id) } pub fn are_compatible(&self, other: &'static Enchantment) -> bool { if self == other { return false; } if let Some(tag) = self.exclusive_set && tag.1.contains(&(other.id as u16)) { return false; } if let Some(tag) = other.exclusive_set && tag.1.contains(&(self.id as u16)) { return false; } true } pub fn is_enchantment_compatible(&self, other: &EnchantmentsImpl) -> bool { for (i, _) in other.enchantment.iter() { if !self.are_compatible(i) { return false; } } true } pub fn get_fullname(&self, level: i32) -> TextComponent { let mut ret = TextComponent::translate(self.description, []).color_named( if self.has_tag(&EnchantmentTag::MINECRAFT_CURSE) { NamedColor::Red } else { NamedColor::Gray } ); if level != 1 || self.max_level != 1 { ret = ret.add_text(" ") .add_child(TextComponent::translate("enchantment.level.".to_string() + &level.to_string(), [])); } ret } } } }