use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{
Error, FnArg, Ident, ItemFn, LitStr, Pat, Result as SynResult, ReturnType, Token, Type,
parse_macro_input,
};
use crate::{EVENT_PREFIX, EVENT_REG_PREFIX};
struct EventName {
name: String,
raw: bool,
}
impl Parse for EventName {
fn parse(input: ParseStream) -> SynResult<Self> {
let mut name = String::new();
let mut raw = false;
while !input.is_empty() {
let ident: Ident = input.parse()?;
if ident == "name" {
let _: Token![=] = input.parse()?;
let callback_name: LitStr = input.parse()?;
let value = callback_name.value();
if value.contains('\0') {
return Err(Error::new(
callback_name.span(),
"event name cannot contain null bytes ('\\0')",
));
}
name = value;
} else if ident == "raw" {
raw = true;
} else {
return Err(Error::new(
ident.span(),
"Unexpected argument name. `#[event]` supports only \"name\" and \"raw\".",
));
}
let _: Option<Token![,]> = input.parse()?;
}
if name.is_empty() {
return Err(input.error("`#[event]` requires `name = \"OnSomething\"`"));
}
Ok(EventName { name, raw })
}
}
pub fn create_event(args: TokenStream, input: TokenStream) -> TokenStream {
let event = parse_macro_input!(args as EventName);
let origin_fn = parse_macro_input!(input as ItemFn);
let vis = &origin_fn.vis;
let origin_name = &origin_fn.sig.ident;
let wrapper_name = prepend(origin_name, EVENT_PREFIX);
let reg_name = prepend(origin_name, EVENT_REG_PREFIX);
let callback_name = &event.name;
let has_self = matches!(origin_fn.sig.inputs.first(), Some(FnArg::Receiver(_)));
let skip_count = if has_self { 2 } else { 1 };
let fn_input_idents = gen_fn_input_idents(&origin_fn, skip_count);
let args_parsing = gen_args_parsing(&origin_fn, skip_count, event.raw, callback_name);
let plugin_binding = gen_plugin_binding(has_self);
let call_origin = gen_call_origin(origin_name, has_self, event.raw, &fn_input_idents);
let invocation = gen_invocation(&origin_fn, &call_origin, callback_name);
let wrapper = quote! {
#vis fn #wrapper_name(
amx: &samp::amx::Amx,
args: &mut samp::args::Args,
) -> samp::events::EventReturn {
#plugin_binding
#args_parsing
unsafe {
#invocation
}
}
};
let reg = gen_reg_event(vis, ®_name, &wrapper_name, callback_name);
let generated = quote! {
#origin_fn
#reg
#wrapper
};
generated.into()
}
fn gen_fn_input_idents(origin_fn: &ItemFn, skip_count: usize) -> Vec<proc_macro2::TokenStream> {
origin_fn
.sig
.inputs
.iter()
.skip(skip_count)
.filter_map(|arg| match arg {
FnArg::Typed(pat_type) => {
let Pat::Ident(pat_ident) = &*pat_type.pat else {
return None;
};
let ident = &pat_ident.ident;
let by_ref = matches!(&*pat_type.ty, Type::Reference(_));
Some(if by_ref {
quote_spanned!(pat_type.span() => &#ident)
} else {
quote_spanned!(pat_type.span() => #ident)
})
}
FnArg::Receiver(_) => None,
})
.collect()
}
fn gen_args_parsing(
origin_fn: &ItemFn,
skip_count: usize,
raw: bool,
callback_name: &str,
) -> proc_macro2::TokenStream {
if raw {
return proc_macro2::TokenStream::new();
}
origin_fn
.sig
.inputs
.iter()
.skip(skip_count)
.enumerate()
.filter_map(|(idx, arg)| match arg {
FnArg::Typed(pat_type) => {
let Pat::Ident(pat_ident) = &*pat_type.pat else {
return None;
};
let ident = &pat_ident.ident;
let ty = &pat_type.ty;
Some(quote_spanned! {
pat_type.span() =>
let Some(#ident) = args.next_arg() else {
samp::log::error!(
"[{}] failed to parse event argument #{} '{}' (expected type: {})",
#callback_name,
#idx,
stringify!(#ident),
stringify!(#ty),
);
return samp::events::EventReturn::Continue;
};
})
}
FnArg::Receiver(_) => None,
})
.collect()
}
fn gen_plugin_binding(has_self: bool) -> proc_macro2::TokenStream {
if has_self {
quote!(let mut plugin = samp::plugin::get::<Self>();)
} else {
proc_macro2::TokenStream::new()
}
}
fn gen_call_origin(
origin_name: &Ident,
has_self: bool,
raw: bool,
fn_input_idents: &[proc_macro2::TokenStream],
) -> proc_macro2::TokenStream {
if raw {
if has_self {
quote!(plugin.as_mut().#origin_name(amx, args))
} else {
quote!(Self::#origin_name(amx, args))
}
} else if has_self {
quote!(plugin.as_mut().#origin_name(amx, #(#fn_input_idents),*))
} else {
quote!(Self::#origin_name(amx, #(#fn_input_idents),*))
}
}
fn gen_invocation(
origin_fn: &ItemFn,
call_origin: &proc_macro2::TokenStream,
callback_name: &str,
) -> proc_macro2::TokenStream {
let handle_user_return = if returns_event_return(&origin_fn.sig.output) {
quote! {
return user_return;
}
} else if returns_result(&origin_fn.sig.output) {
quote! {
if let Err(err) = user_return {
samp::log::error!("[{}] {}", #callback_name, err);
}
return samp::events::EventReturn::Continue;
}
} else {
quote! {
let _ = user_return;
return samp::events::EventReturn::Continue;
}
};
quote! {
let user_return = match ::std::panic::catch_unwind(
::std::panic::AssertUnwindSafe(|| #call_origin)
) {
Ok(v) => v,
Err(panic) => {
let msg = panic.downcast_ref::<&str>()
.copied()
.or_else(|| panic.downcast_ref::<String>().map(String::as_str))
.unwrap_or("(non-string payload)");
samp::log::error!("[{}] panic in event handler: {}", #callback_name, msg);
return samp::events::EventReturn::Continue;
}
};
#handle_user_return
}
}
fn gen_reg_event(
vis: &syn::Visibility,
reg_name: &Ident,
wrapper_name: &Ident,
callback_name: &str,
) -> proc_macro2::TokenStream {
quote! {
#vis fn #reg_name() -> samp::events::EventInfo {
samp::events::EventInfo {
name: #callback_name,
handler: Self::#wrapper_name,
}
}
}
}
fn prepend(ident: &Ident, prefix: &str) -> Ident {
Ident::new(&format!("{prefix}{ident}"), ident.span())
}
fn returns_result(output: &ReturnType) -> bool {
let ReturnType::Type(_, ty) = output else {
return false;
};
let Type::Path(tp) = &**ty else {
return false;
};
let Some(last) = tp.path.segments.last() else {
return false;
};
last.ident == "Result" || last.ident == "AmxResult"
}
fn returns_event_return(output: &ReturnType) -> bool {
let ReturnType::Type(_, ty) = output else {
return false;
};
let Type::Path(tp) = &**ty else {
return false;
};
tp.path
.segments
.last()
.is_some_and(|last| last.ident == "EventReturn")
}