use proc_macro::TokenStream; use quote::quote; use syn::{ Attribute, Ident, LitStr, Token, braced, parse::{Parse, ParseStream, Result}, parse_macro_input, punctuated::Punctuated, }; struct Registry { name: Ident, items: Vec, attrs: Vec, } struct RegistryItem { attrs: Vec, name: Ident, id: String, } impl Parse for RegistryItem { // Air => "minecraft:air" fn parse(input: ParseStream) -> Result { // parse annotations like #[default] let attrs = input.call(Attribute::parse_outer).unwrap_or_default(); let name = input.parse()?; input.parse::]>()?; let id = input.parse::()?.value(); Ok(RegistryItem { attrs, name, id }) } } impl Parse for Registry { fn parse(input: ParseStream) -> Result { // enum BlockKind { // Air => "minecraft:air", // Stone => "minecraft:stone" // } // this also includes docs let attrs = input.call(Attribute::parse_outer).unwrap_or_default(); input.parse::()?; let name = input.parse()?; let content; braced!(content in input); let items: Punctuated = content.parse_terminated(RegistryItem::parse, Token![,])?; Ok(Registry { name, items: items.into_iter().collect(), attrs, }) } } #[proc_macro] pub fn registry(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as Registry); let name = input.name; let mut generated = quote! {}; // enum BlockKind { // Air = 0, // Stone, // } let mut enum_items = quote! {}; for (i, item) in input.items.iter().enumerate() { let attrs = &item.attrs; let name = &item.name; let protocol_id = i as u32; enum_items.extend(quote! { #(#attrs)* #name = #protocol_id, }); } let attributes = input.attrs; generated.extend(quote! { #(#attributes)* #[derive(azalea_buf::AzBuf, Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[repr(u32)] pub enum #name { #enum_items } }); let max_id = input.items.len() as u32 - 1; let doc_0 = format!("Transmutes a u32 to a {name}."); let doc_1 = format!("The `id` should be at most {max_id}."); generated.extend(quote! { impl #name { #[doc = #doc_0] /// /// # Safety #[doc = #doc_1] #[inline] pub unsafe fn from_u32_unchecked(id: u32) -> Self { std::mem::transmute::(id) } #[inline] pub const fn is_valid_id(id: u32) -> bool { id <= #max_id } } impl crate::Registry for #name { fn from_u32(value: u32) -> Option { if Self::is_valid_id(value) { Some(unsafe { Self::from_u32_unchecked(value) }) } else { None } } fn to_u32(&self) -> u32 { *self as u32 } } }); let doc_0 = format!("Safely transmutes a u32 to a {name}."); generated.extend(quote! { impl TryFrom for #name { type Error = (); #[doc = #doc_0] fn try_from(id: u32) -> Result { if let Some(value) = crate::Registry::from_u32(id) { Ok(value) } else { Err(()) } } } }); // Display that uses registry ids let mut to_str_items = quote! {}; let mut from_str_items = quote! {}; for item in &input.items { let name = &item.name; let id = &item.id; to_str_items.extend(quote! { Self::#name => concat!("minecraft:", #id), }); from_str_items.extend(quote! { #id => Ok(Self::#name), }); } generated.extend(quote! { impl #name { /// Convert the value to a stringified identifier, formatted like /// `"minecraft:air"`. pub fn to_str(&self) -> &'static str { match self { #to_str_items } } } impl std::fmt::Display for #name { /// Convert the value to a stringified identifier, formatted like /// `"minecraft:air"`. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_str()) } } impl<'a> TryFrom<&'a crate::Identifier> for #name { type Error = (); fn try_from(ident: &'a crate::Identifier) -> Result { if ident.namespace() != "minecraft" { return Err(()) } match ident.path() { #from_str_items _ => return Err(()), } } } impl<'a> From<#name> for crate::Identifier { fn from(value: #name) -> Self { Self::new(value.to_str()) } } /// Parse the value from a stringified identifier, formatted like /// either `"air"` or `"minecraft:air"`. impl std::str::FromStr for #name { type Err = (); fn from_str(s: &str) -> Result { Self::try_from(&crate::Identifier::new(s)) } } #[cfg(feature = "serde")] impl serde::Serialize for #name { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&self.to_string()) } } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for #name { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; s.parse().map_err(|_| serde::de::Error::custom( format!("{s:?} is not a valid {name}", s = s, name = stringify!(#name)) )) } } impl simdnbt::FromNbtTag for #name { fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option { let v = tag.string()?; std::str::FromStr::from_str(&v.to_str()).ok() } } impl simdnbt::ToNbtTag for #name { fn to_nbt_tag(self) -> simdnbt::owned::NbtTag { simdnbt::owned::NbtTag::String(self.to_string().into()) } } }); generated.into() }