605 lines
20 KiB
Rust
605 lines
20 KiB
Rust
use proc_macro::TokenStream;
|
||
use proc_macro_error2::{abort, abort_call_site, proc_macro_error};
|
||
use pumpkin_data::Block;
|
||
use pumpkin_data::tag::{RegistryKey, get_tag_ids};
|
||
use quote::quote;
|
||
use syn::spanned::Spanned;
|
||
use syn::{self, Attribute, DeriveInput, LitStr, Type, parse_quote};
|
||
use syn::{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 asynchronously.
|
||
///
|
||
/// # Arguments
|
||
/// - `input` – The input `TokenStream` representing the labelled blocks and event expressions.
|
||
#[proc_macro_error]
|
||
#[proc_macro]
|
||
pub fn send_cancellable(input: TokenStream) -> TokenStream {
|
||
let block = parse_macro_input!(input as syn::Block);
|
||
|
||
let mut server_expr = None;
|
||
let mut event_expr = None;
|
||
let mut after_block = None;
|
||
let mut cancelled_block = None;
|
||
|
||
for stmt in block.stmts {
|
||
match stmt {
|
||
Stmt::Expr(expr, _) => {
|
||
// Check if it is a labelled block first.
|
||
let mut is_special_block = false;
|
||
if let Expr::Block(ref b) = expr
|
||
&& let Some(ref label) = b.label
|
||
{
|
||
let label_name = label.name.ident.to_string();
|
||
if label_name == "after" {
|
||
after_block = Some(b.clone()); // Clone strictly necessary here as we split AST.
|
||
is_special_block = true;
|
||
} else if label_name == "cancelled" {
|
||
cancelled_block = Some(b.clone());
|
||
is_special_block = true;
|
||
}
|
||
}
|
||
|
||
// If it wasn't a special block, it must be the event expression.
|
||
if !is_special_block {
|
||
if server_expr.is_none() {
|
||
server_expr = Some(expr);
|
||
} else if event_expr.is_none() {
|
||
event_expr = Some(expr);
|
||
} else {
|
||
abort!(
|
||
expr.span(),
|
||
"Multiple event expressions found. Only one event expression allowed."
|
||
);
|
||
}
|
||
}
|
||
}
|
||
// Abort on other statements (like `let x = ...`) if strictness is desired.
|
||
_ => abort!(
|
||
stmt.span(),
|
||
"Only event expressions and labeled blocks allowed in `send_cancellable!`"
|
||
),
|
||
}
|
||
}
|
||
|
||
let Some(server) = server_expr else {
|
||
abort_call_site!("Server expression must be specified");
|
||
};
|
||
|
||
let Some(event) = event_expr else {
|
||
abort_call_site!("Event expression must be specified");
|
||
};
|
||
|
||
// Construct the if/else logic.
|
||
let logic = match (after_block, cancelled_block) {
|
||
(Some(after), Some(cancelled)) => quote! {
|
||
if !event.cancelled { #after } else { #cancelled }
|
||
},
|
||
(Some(after), None) => quote! {
|
||
if !event.cancelled { #after }
|
||
},
|
||
(None, Some(cancelled)) => quote! {
|
||
if event.cancelled { #cancelled }
|
||
},
|
||
(None, None) => quote! {},
|
||
};
|
||
|
||
quote! {
|
||
let event = #server.plugin_manager.fire(#event).await;
|
||
#logic
|
||
}
|
||
.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::MinecraftVersion) -> 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 block_id = block.id;
|
||
|
||
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<[u16]> {
|
||
[#block_id].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 expanded = quote! {
|
||
impl #impl_generics crate::block::BlockMetadata for #name #ty_generics #where_clause {
|
||
fn ids() -> Box<[u16]> {
|
||
Box::new([ #(#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 {
|
||
unimplemented!()
|
||
};
|
||
|
||
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 {
|
||
unimplemented!()
|
||
};
|
||
|
||
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
|
||
}
|
||
}
|