Pumpkin/crates/pumpkin-macros/src/lib.rs
Alexander Medvedev 1ee7be3f8c chore: add pumpkin_macros::translate_cross! macro
validates translations at compile time, also update bedrock translations to latest 26.40.5
2026-08-10 18:11:00 +02:00

841 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![allow(clippy::unwrap_used, clippy::expect_used)]
use heck::ToShoutySnakeCase;
use proc_macro::TokenStream;
use proc_macro_error2::{abort, abort_call_site};
use pumpkin_data::tag::{RegistryKey, get_tag_ids};
use pumpkin_data::{Block, BlockId};
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::{self, Attribute, DeriveInput, LitStr, Type, parse_quote};
use syn::{Block as SynBlock, Expr, Field, Fields, ItemStruct, Stmt, parse_macro_input};
/// Derives the `Payload` trait for an event struct, enabling it to be used in the plugin system.
///
/// # Arguments
/// - `item` – The input `TokenStream` representing the struct to derive `Event` for.
#[proc_macro_derive(Event)]
pub fn event(item: TokenStream) -> TokenStream {
let ast = parse_macro_input!(item as DeriveInput);
let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
quote! {
impl #impl_generics crate::plugin::Payload for #name #ty_generics #where_clause {
fn get_name_static() -> &'static str {
stringify!(#name)
}
fn get_name(&self) -> &'static str {
stringify!(#name)
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
}
.into()
}
/// Marks a struct as cancellable by adding a `cancelled: bool` field and
/// implementing the `Cancellable` trait.
///
/// # Arguments
/// - `_args` – `TokenStream` of arguments passed to the attribute (unused).
/// - `input` – The input `TokenStream` representing the struct to modify.
#[proc_macro_attribute]
pub fn cancellable(_args: TokenStream, input: TokenStream) -> TokenStream {
let mut item_struct = parse_macro_input!(input as ItemStruct);
let name = &item_struct.ident;
let (impl_generics, ty_generics, where_clause) = item_struct.generics.split_for_impl();
match &mut item_struct.fields {
Fields::Named(fields) => {
if fields
.named
.iter()
.any(|f| f.ident.as_ref().is_some_and(|i| i == "cancelled"))
{
abort!(fields.span(), "Struct already has a `cancelled` field");
}
let field: Field = parse_quote! {
pub cancelled: bool
};
fields.named.push(field);
}
_ => abort!(
item_struct.span(),
"#[cancellable] can only be used on structs with named fields"
),
}
quote! {
#item_struct
impl #impl_generics crate::plugin::Cancellable for #name #ty_generics #where_clause {
fn cancelled(&self) -> bool {
self.cancelled
}
fn set_cancelled(&mut self, cancelled: bool) {
self.cancelled = cancelled;
}
}
}
.into()
}
/// Sends a cancellable event through the plugin manager.
///
/// # Syntax
/// ```ignore
/// send_cancellable! {{
/// <server_expr>;
/// <event_expr>;
/// 'after: { <after_stmts> }
/// 'cancelled: { <cancelled_stmts> }
/// }}
/// ```
#[proc_macro]
pub fn send_cancellable(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as SynBlock);
let mut stmts_iter = input.stmts.into_iter();
let Some(Stmt::Expr(server_stmt, _)) = stmts_iter.next() else {
abort_call_site!("expected server expression as first statement")
};
let Some(Stmt::Expr(event_stmt, _)) = stmts_iter.next() else {
abort_call_site!("expected event expression as second statement")
};
let event_expr = if let Expr::Reference(syn::ExprReference {
expr,
mutability: Some(_),
..
}) = event_stmt
{
*expr
} else {
event_stmt
};
let mut after_block = None;
let mut cancelled_block = None;
for stmt in stmts_iter {
if let Stmt::Expr(Expr::Block(b), _) = stmt
&& let Some(ref label) = b.label
{
if label.name.ident == "after" {
after_block = Some(b.block);
} else if label.name.ident == "cancelled" {
cancelled_block = Some(b.block);
}
}
}
let execution = match (after_block, cancelled_block) {
(Some(after), Some(cancelled)) => quote! {
if !is_cancelled {
#after
} else {
#cancelled
}
},
(Some(after), None) => quote! {
if !is_cancelled {
#after
}
},
(None, Some(cancelled)) => quote! {
if is_cancelled {
#cancelled
}
},
(None, None) => quote! {},
};
let expanded = quote! {
{
let mut event = #event_expr;
let server_ref: &std::sync::Arc<crate::server::Server> = {
use std::borrow::Borrow;
(#server_stmt).borrow()
};
server_ref.plugin_manager.fire(server_ref, &mut event).await;
let is_cancelled = {
use crate::plugin::Cancellable;
event.cancelled()
};
#execution
}
};
expanded.into()
}
/// Attaches a fixed packet ID to a struct implementing `Packet`.
///
/// # Arguments
/// - `args` – The `TokenStream` representing the packet ID expression.
/// - `item` – The input `TokenStream` representing the struct to implement `Packet` for.
#[proc_macro_attribute]
pub fn packet(args: TokenStream, item: TokenStream) -> TokenStream {
let packet_id_expr = parse_macro_input!(args as Expr);
let ast = parse_macro_input!(item as DeriveInput);
let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
quote! {
#ast
impl #impl_generics crate::packet::Packet for #name #ty_generics #where_clause {
const PACKET_ID: i32 = #packet_id_expr;
}
}
.into()
}
/// Attaches a multi-version packet ID to a struct implementing `MultiVersionJavaPacket`.
///
/// # Arguments
/// - `args` – The `TokenStream` representing the packet ID expression.
/// - `item` – The input `TokenStream` representing the struct to implement the trait for.
#[proc_macro_attribute]
pub fn java_packet(args: TokenStream, item: TokenStream) -> TokenStream {
let packet_id_expr = parse_macro_input!(args as Expr);
let ast = parse_macro_input!(item as DeriveInput);
let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
quote! {
#ast
impl #impl_generics crate::packet::MultiVersionJavaPacket for #name #ty_generics #where_clause {
#[must_use]
#[inline]
fn to_id(version: pumpkin_util::version::JavaMinecraftVersion) -> i32 {
#packet_id_expr.to_id(version)
}
}
}
.into()
}
/// Marks a struct as representing a specific block by its name.
///
/// # Arguments
/// - `args` – The `TokenStream` representing the block name literal.
/// - `item` – The input `TokenStream` representing the struct to implement `BlockMetadata` for.
#[proc_macro_attribute]
pub fn pumpkin_block(args: TokenStream, item: TokenStream) -> TokenStream {
let input_item = item.clone();
let arg_lit = parse_macro_input!(args as LitStr);
let arg_value = arg_lit.value();
let block_name = arg_value.strip_prefix("minecraft:").unwrap_or(&arg_value);
let Some(block) = Block::from_name(block_name) else {
return syn::Error::new(arg_lit.span(), "Invalid block name")
.to_compile_error()
.into();
};
let const_ident = format_ident!("{}", block.name.to_shouty_snake_case());
let ast = parse_macro_input!(item as DeriveInput);
let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let generated = quote! {
impl #impl_generics crate::block::BlockMetadata for #name #ty_generics #where_clause {
fn ids() -> Box<[pumpkin_data::BlockId]> {
[pumpkin_data::BlockId::#const_ident].into()
}
}
};
// Combine the original item and new impl.
let mut output = input_item;
output.extend(TokenStream::from(generated));
output
}
/// Marks a struct as representing a set of blocks from a given tag.
///
/// # Arguments
/// - `args` – The `TokenStream` representing the block tag literal.
/// - `item` – The input `TokenStream` representing the struct to implement `BlockMetadata` for.
#[proc_macro_attribute]
pub fn pumpkin_block_from_tag(args: TokenStream, item: TokenStream) -> TokenStream {
let original_item = item.clone();
let arg_lit = parse_macro_input!(args as LitStr);
let ast = parse_macro_input!(item as DeriveInput);
let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let full_tag = arg_lit.value();
let Some(values) = get_tag_ids(RegistryKey::Block, &full_tag) else {
return syn::Error::new(arg_lit.span(), format!("Failed to get tag IDs: {full_tag}"))
.to_compile_error()
.into();
};
let const_values: Vec<_> = values
.iter()
.map(|v| {
let block = BlockId::new_or_air(*v).to_block();
format_ident!("{}", block.name.to_shouty_snake_case())
})
.collect();
let expanded = quote! {
impl #impl_generics crate::block::BlockMetadata for #name #ty_generics #where_clause {
fn ids() -> Box<[pumpkin_data::BlockId]> {
Box::new([ #(pumpkin_data::BlockId::#const_values),* ])
}
}
};
let mut output = original_item;
output.extend(TokenStream::from(expanded));
output
}
// #[proc_macro_error]
// #[proc_macro_attribute]
// pub fn block_property(input: TokenStream, item: TokenStream) -> TokenStream {
// let ast: syn::DeriveInput = syn::parse(item.clone()).unwrap();
// let name = &ast.ident;
// let (impl_generics, ty_generics, _) = ast.generics.split_for_impl();
// let input_string = input.to_string();
// let input_parts: Vec<&str> = input_string.split("[").collect();
// let property_name = input_parts[0].trim_ascii().trim_matches(&['"', ','][..]);
// let mut property_values: Vec<&str> = Vec::new();
// if input_parts.len() > 1 {
// property_values = input_parts[1]
// .trim_matches(']')
// .split(", ")
// .map(|p| p.trim_ascii().trim_matches(&['"', ','][..]))
// .collect::<Vec<&str>>();
// }
// let item: proc_macro2::TokenStream = item.into();
// let (variants, is_enum): (Vec<proc_macro2::Ident>, bool) = match ast.data {
// syn::Data::Enum(enum_item) => (
// enum_item.variants.into_iter().map(|v| v.ident).collect(),
// true,
// ),
// syn::Data::Struct(s) => {
// let fields = match s.fields {
// Fields::Named(f) => abort!(f.span(), "Block properties can't have named fields"),
// Fields::Unnamed(fields) => fields.unnamed,
// Fields::Unit => abort!(s.fields.span(), "Block properties must have fields"),
// };
// if fields.len() != 1 {
// abort!(
// fields.span(),
// "Block properties `struct's must have exactly one field"
// );
// }
// let field = fields.first().unwrap();
// let ty = &field.ty;
// let struct_type = match field.ty {
// syn::Type::Path(ref type_path) => {
// type_path.path.segments.first().unwrap().ident.to_string()
// }
// ref other => abort!(
// other.span(),
// "Block properties can only have primitive types"
// ),
// };
// match struct_type.as_str() {
// "bool" => (
// vec![
// proc_macro2::Ident::new("true", proc_macro2::Span::call_site()),
// proc_macro2::Ident::new("false", proc_macro2::Span::call_site()),
// ],
// false,
// ),
// other => abort!(
// ty.span(),
// format!("`{other}` is not supported (why not implement it yourself?)")
// ),
// }
// }
// _ => abort_call_site!("Block properties can only be `enum`s or `struct's"),
// };
// let values = variants.iter().enumerate().map(|(i, v)| match is_enum {
// true => {
// let mut value = v.to_string().to_snake_case();
// if !property_values.is_empty() && i < property_values.len() {
// value = property_values[i].to_string();
// }
// quote! {
// Self::#v => #value.to_string(),
// }
// }
// false => {
// let value = v.to_string();
// quote! {
// Self(#v) => #value.to_string(),
// }
// }
// });
// let from_values = variants.iter().enumerate().map(|(i, v)| match is_enum {
// true => {
// let mut value = v.to_string().to_snake_case();
// if !property_values.is_empty() && i < property_values.len() {
// value = property_values[i].to_string();
// }
// quote! {
// #value => Self::#v,
// }
// }
// false => {
// let value = v.to_string();
// quote! {
// #value => Self(#v),
// }
// }
// });
// let extra_fns = variants.iter().map(|v| {
// let title = proc_macro2::Ident::new(
// &v.to_string().to_pascal_case(),
// proc_macro2::Span::call_site(),
// );
// quote! {
// pub fn #title() -> Self {
// Self(#v)
// }
// }
// });
// let extra = if is_enum {
// quote! {}
// } else {
// quote! {
// impl #name {
// #(#extra_fns)*
// }
// }
// };
// let code = quote! {
// #item
// impl #impl_generics pumpkin_world::block::properties::BlockPropertyMetadata for #name #ty_generics {
// fn name(&self) -> &'static str {
// #property_name
// }
// fn value(&self) -> String {
// match self {
// #(#values)*
// }
// }
// fn from_value(value: String) -> Self {
// match value.as_str() {
// #(#from_values)*
// _ => panic!("Invalid value for block property"),
// }
// }
// }
// #extra
// };
// code.into()
// }
/// Derives the `PacketWrite` trait for a struct, enabling serialization.
///
/// # Arguments
/// - `input` – The input `TokenStream` representing the struct to derive `PacketWrite` for.
#[rustfmt::skip]
#[proc_macro_derive(PacketWrite, attributes(serial))]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields = if let syn::Data::Struct(data) = &input.data {
data.fields.iter().map(|f| {
let ident = f.ident.as_ref().unwrap();
let (is_big_endian, no_prefix) = check_serial_attributes(&f.attrs);
let is_vec = is_vec(&f.ty);
if is_vec && !no_prefix {
// Vec with prefix: write VarUInt length, then data
if is_big_endian {
quote! {
crate::codec::var_uint::VarUInt(self.#ident.len() as u32).write(writer)?;
self.#ident.write_be(writer)?;
}
} else {
quote! {
crate::codec::var_uint::VarUInt(self.#ident.len() as u32).write(writer)?;
self.#ident.write(writer)?;
}
}
} else {
// Non-Vec or Vec with no_prefix: write directly
if is_big_endian {
quote! {
self.#ident.write_be(writer)?;
}
} else {
quote! {
self.#ident.write(writer)?;
}
}
}
})
} else {
return syn::Error::new(name.span(), "Only structs are supported")
.to_compile_error()
.into();
};
let expanded = quote! {
impl PacketWrite for #name {
fn write<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
#(#fields)*
Ok(())
}
}
};
expanded.into()
}
/// Derives the `PacketRead` trait for a struct, enabling deserialization.
///
/// # Arguments
/// - `input` – The input `TokenStream` representing the struct to derive `PacketRead` for.
#[rustfmt::skip]
#[proc_macro_derive(PacketRead, attributes(serial))]
pub fn derive_deserialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields = if let syn::Data::Struct(data) = &input.data {
data.fields.iter().map(|f| {
let ident = f.ident.as_ref().unwrap();
let (is_big_endian, no_prefix) = check_serial_attributes(&f.attrs);
let is_vec = is_vec(&f.ty);
if is_vec && !no_prefix {
// Vec with prefix: read VarUInt length, then data
quote! {
#ident: {
let len = crate::codec::var_uint::VarUInt::read(reader)?.0 as usize;
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf)?;
buf
}
}
} else {
// Non-Vec or Vec with no_prefix: read directly
if is_big_endian {
quote! {
#ident: PacketRead::read_be(reader)?
}
} else {
quote! {
#ident: PacketRead::read(reader)?
}
}
}
})
} else {
return syn::Error::new(name.span(), "Only structs are supported")
.to_compile_error()
.into();
};
let expanded = quote! {
impl PacketRead for #name {
fn read<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
Ok(Self {
#(#fields),*
})
}
}
};
expanded.into()
}
/// Checks a field's `#[serial(...)]` attributes.
///
/// # Arguments
/// - `attrs` – Slice of `Attribute`s to inspect for serial-specific metadata.
///
/// # Returns
/// Tuple `(is_big_endian, no_prefix)` indicating whether the field is big-endian
/// and/or has no length prefix.
fn check_serial_attributes(attrs: &[Attribute]) -> (bool, bool) {
let mut is_big_endian = false;
let mut no_prefix = false;
for attr in attrs {
if attr.path().is_ident("serial") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("big_endian") {
is_big_endian = true;
} else if meta.path.is_ident("no_prefix") {
no_prefix = true;
}
Ok(())
});
}
}
(is_big_endian, no_prefix)
}
/// Returns true if the type is a `Vec<_>`.
///
/// # Arguments
/// - `ty` – The `Type` to check.
///
/// # Returns
/// `true` if the type is a `Vec`, otherwise `false`.
fn is_vec(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
type_path
.path
.segments
.iter()
.next_back()
.is_some_and(|segment| segment.ident == "Vec")
} else {
false
}
}
struct TranslateCrossInput {
java_expr: syn::Expr,
bedrock_expr: syn::Expr,
args: Vec<syn::Expr>,
}
impl syn::parse::Parse for TranslateCrossInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let java_expr: syn::Expr = input.parse()?;
let _ = input.parse::<syn::Token![,]>()?;
let bedrock_expr: syn::Expr = input.parse()?;
let mut args = Vec::new();
while !input.is_empty() {
let _ = input.parse::<syn::Token![,]>()?;
if input.is_empty() {
break;
}
args.push(input.parse()?);
}
Ok(Self {
java_expr,
bedrock_expr,
args,
})
}
}
fn eval_translation_key_expr(expr: &syn::Expr) -> Option<(&'static str, proc_macro2::Span)> {
match expr {
syn::Expr::Path(expr_path) => {
let segments: Vec<String> = expr_path
.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect();
let seg_refs: Vec<&str> = segments.iter().map(String::as_str).collect();
let (is_java, const_ident) = match seg_refs.as_slice() {
["translation", "java", ident]
| ["pumpkin_data" | "crate", "translation", "java", ident] => (true, *ident),
["translation", "bedrock", ident]
| ["pumpkin_data" | "crate", "translation", "bedrock", ident] => (false, *ident),
_ => return None,
};
let key = if is_java {
pumpkin_data::translation::java::get(const_ident)
.and_then(pumpkin_data::translation::java::get_value)
} else {
pumpkin_data::translation::bedrock::get(const_ident)
.and_then(pumpkin_data::translation::bedrock::get_value)
};
key.map(|k| (k, expr.span()))
}
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) => {
// Leaking string slice here is fine since this runs during compilation inside proc macro
let s = Box::leak(lit_str.value().into_boxed_str());
Some((s, lit_str.span()))
}
_ => None,
}
}
fn count_placeholders(format_str: &str) -> usize {
let mut count = 0;
let bytes = format_str.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 1 < bytes.len() {
if bytes[i + 1] == b'%' {
i += 2;
continue;
}
// Handle positional specifiers like %1$s, %2$d
let mut j = i + 1;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j < bytes.len() && bytes[j] == b'$' {
count += 1;
i = j + 2; // skip past specifier character following $ (e.g., $s)
continue;
}
// Handle optional width/flags before specifier type like %s, %d, %f, %1s
count += 1;
i = j + 1; // skip past specifier character
continue;
}
i += 1;
}
count
}
/// Validates translation keys and arguments at compile-time and returns a `TextComponent`.
#[proc_macro]
pub fn translate_cross(input: TokenStream) -> TokenStream {
let TranslateCrossInput {
java_expr,
bedrock_expr,
args,
} = parse_macro_input!(input as TranslateCrossInput);
if let Some((java_str, span)) = eval_translation_key_expr(&java_expr) {
let expected = count_placeholders(java_str);
// Java translation keys from generated constants might differ from Bedrock key placeholder formats
if expected != args.len() && matches!(java_expr, syn::Expr::Lit(_)) {
return syn::Error::new(
span,
format!(
"Java translation key `{}` expects {} argument(s), but {} were provided",
java_str,
expected,
args.len()
),
)
.to_compile_error()
.into();
}
}
if let Some((bedrock_str, span)) = eval_translation_key_expr(&bedrock_expr) {
let expected = count_placeholders(bedrock_str);
if expected != args.len() && matches!(bedrock_expr, syn::Expr::Lit(_)) {
return syn::Error::new(
span,
format!(
"Bedrock translation key `{}` expects {} argument(s), but {} were provided",
bedrock_str,
expected,
args.len()
),
)
.to_compile_error()
.into();
}
}
let expanded = quote! {
{
#[allow(deprecated)]
pumpkin_util::text::TextComponent::translate_cross(#java_expr, #bedrock_expr, vec![#(#args),*])
}
};
expanded.into()
}
struct TranslateJavaInput {
java_expr: syn::Expr,
args: Vec<syn::Expr>,
}
impl syn::parse::Parse for TranslateJavaInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let java_expr: syn::Expr = input.parse()?;
let mut args = Vec::new();
while !input.is_empty() {
let _ = input.parse::<syn::Token![,]>()?;
if input.is_empty() {
break;
}
args.push(input.parse()?);
}
Ok(Self { java_expr, args })
}
}
/// Validates Java translation keys and arguments at compile-time and returns a `TextComponent`.
#[deprecated(
since = "0.1.0",
note = "Use `pumpkin_macros::translate_cross!` macro instead for cross-platform (Java & Bedrock) translation support."
)]
#[proc_macro]
pub fn translate_java(input: TokenStream) -> TokenStream {
let TranslateJavaInput { java_expr, args } = parse_macro_input!(input as TranslateJavaInput);
if let Some((java_str, span)) = eval_translation_key_expr(&java_expr) {
let expected = count_placeholders(java_str);
if expected != args.len() {
return syn::Error::new(
span,
format!(
"Java translation key `{}` expects {} argument(s), but {} were provided",
java_str,
expected,
args.len()
),
)
.to_compile_error()
.into();
}
}
let expanded = quote! {
{
#[allow(deprecated)]
pumpkin_util::text::TextComponent::translate(#java_expr, vec![#(#args),*])
}
};
expanded.into()
}