use samp_sdk::amx::Amx;
use samp_sdk::args::Args;
use samp_sdk::raw::types::AMX;
use crate::amx::AmxIdent;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use crate::macros::sdk_warn;
use crate::runtime::Runtime;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use std::sync::OnceLock;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use retour::GenericDetour;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use samp_sdk::consts::AmxExecIdx;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use samp_sdk::exports::{Exec, Export};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventReturn {
Continue,
Suppress(i32),
}
impl EventReturn {
#[must_use]
pub fn suppress<T: samp_sdk::cell::CellConvert>(value: T) -> Self {
EventReturn::Suppress(value.into_cell())
}
}
pub type EventHandler = fn(&Amx, &mut Args) -> EventReturn;
pub(crate) type EventTable = Vec<Option<std::rc::Rc<[EventHandler]>>>;
#[derive(Clone, Copy)]
pub struct EventInfo {
pub name: &'static str,
pub handler: EventHandler,
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
type ExecFn = unsafe extern "C" fn(*mut AMX, *mut i32, i32) -> i32;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
struct ExecDetour(GenericDetour<ExecFn>);
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
unsafe impl Sync for ExecDetour {}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
unsafe impl Send for ExecDetour {}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
static EXEC_DETOUR: OnceLock<ExecDetour> = OnceLock::new();
pub(crate) fn on_amx_load(rt: &Runtime, amx: &Amx) {
if !rt.has_events() {
return;
}
resolve_events_for_amx(rt, amx);
install_exec_hook(rt.amx_exports());
}
pub(crate) fn on_amx_unload(rt: &Runtime, amx_ptr: *mut AMX) {
if rt.has_events() {
rt.remove_resolved_events(AmxIdent::from(amx_ptr));
}
}
fn resolve_events_for_amx(rt: &Runtime, amx: &Amx) {
let Some(ptr) = amx.amx() else {
return;
};
let mut by_index: Vec<Vec<EventHandler>> = Vec::new();
for event in rt.events_snapshot() {
let Ok(index) = amx.find_public(event.name) else {
continue;
};
let Ok(index) = usize::try_from(i32::from(index)) else {
continue;
};
if by_index.len() <= index {
by_index.resize_with(index + 1, Vec::new);
}
by_index[index].push(event.handler);
}
let table: EventTable = by_index
.into_iter()
.map(|handlers| (!handlers.is_empty()).then(|| handlers.into()))
.collect();
rt.set_resolved_events(AmxIdent::from(ptr.as_ptr()), table);
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn install_exec_hook(fn_table: usize) {
if EXEC_DETOUR.get().is_some() || fn_table == 0 {
return;
}
let Some(exec) = Exec::try_from_table(fn_table) else {
sdk_warn!("the AMX function table has no amx_Exec; #[event] handlers will not fire");
return;
};
let target: ExecFn = exec;
let detour = match unsafe { GenericDetour::new(target, exec_detour) } {
Ok(detour) => detour,
Err(err) => {
sdk_warn!("failed to build amx_Exec detour: {err}; events will not fire");
return;
}
};
let cell = EXEC_DETOUR.get_or_init(|| ExecDetour(detour));
if let Err(err) = unsafe { cell.0.enable() } {
sdk_warn!("failed to enable amx_Exec detour: {err}; events will not fire");
}
}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
fn install_exec_hook(_fn_table: usize) {}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
unsafe extern "C" fn exec_detour(amx: *mut AMX, retval: *mut i32, index: i32) -> i32 {
let suppressed = crate::panic_guard::catch(|| dispatch(amx, index)).unwrap_or(None);
if let Some(value) = suppressed {
unsafe { consume_arguments(amx) };
if !retval.is_null() {
unsafe { *retval = value };
}
return 0;
}
match EXEC_DETOUR.get() {
Some(cell) => unsafe { cell.0.call(amx, retval, index) },
None => 0,
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
unsafe fn consume_arguments(amx: *mut AMX) {
if amx.is_null() {
return;
}
unsafe {
let paramcount = std::ptr::addr_of!((*amx).paramcount).read_unaligned();
let stk = std::ptr::addr_of!((*amx).stk).read_unaligned();
let consumed = paramcount
.max(0)
.checked_mul(4)
.and_then(|bytes| stk.checked_add(bytes));
if let Some(stk) = consumed {
std::ptr::addr_of_mut!((*amx).stk).write_unaligned(stk);
}
std::ptr::addr_of_mut!((*amx).paramcount).write_unaligned(0);
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
struct ActiveGuard<'rt> {
rt: &'rt Runtime,
key: (AmxIdent, i32),
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
impl<'rt> ActiveGuard<'rt> {
fn acquire(rt: &'rt Runtime, ident: AmxIdent, index: i32) -> Option<Self> {
rt.enter_dispatch(ident, index).then_some(ActiveGuard {
rt,
key: (ident, index),
})
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
impl Drop for ActiveGuard<'_> {
fn drop(&mut self) {
self.rt.leave_dispatch(self.key.0, self.key.1);
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn dispatch(amx_ptr: *mut AMX, index: i32) -> Option<i32> {
let AmxExecIdx::UserDef(idx) = AmxExecIdx::from(index) else {
return None;
};
if amx_ptr.is_null() {
return None;
}
let rt = Runtime::get();
let ident = AmxIdent::from(amx_ptr);
let handlers = rt.resolved_handlers(ident, idx)?;
let _guard = ActiveGuard::acquire(rt, ident, idx)?;
let amx = crate::amx::get(ident)?;
let mut inline = [0i32; INLINE_PARAMS + 1];
let mut spilled = Vec::new();
let params = read_stack_params(amx_ptr, amx, &mut inline, &mut spilled)?;
let mut args = Args::new(amx, params.as_ptr());
for handler in handlers.iter() {
args.reset();
if let EventReturn::Suppress(value) = handler(amx, &mut args) {
return Some(value);
}
}
None
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
const INLINE_PARAMS: usize = 16;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn read_stack_params<'b>(
amx_ptr: *mut AMX,
amx: &Amx,
inline: &'b mut [i32; INLINE_PARAMS + 1],
spilled: &'b mut Vec<i32>,
) -> Option<&'b mut [i32]> {
let (paramcount, stk) = unsafe {
(
std::ptr::addr_of!((*amx_ptr).paramcount).read_unaligned(),
std::ptr::addr_of!((*amx_ptr).stk).read_unaligned(),
)
};
let count = usize::try_from(paramcount).ok()?;
let params: &mut [i32] = if count <= INLINE_PARAMS {
&mut inline[..=count]
} else {
spilled.resize(count + 1, 0);
spilled
};
params[0] = paramcount.checked_mul(4)?;
let mut addr = stk;
for slot in &mut params[1..] {
*slot = amx.read_cell(addr)?;
addr = addr.checked_add(4)?;
}
Some(params)
}
#[cfg(test)]
mod tests {
use super::*;
use samp_sdk::cell::Ref;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[test]
fn a_suppressed_public_consumes_its_arguments_like_amx_exec() {
extern "C" fn callback(_: *mut AMX, _: i32, _: *mut i32, _: *mut i32) -> i32 {
0
}
extern "C" fn debug(_: *mut AMX) -> i32 {
0
}
let mut amx = AMX {
base: std::ptr::null_mut(),
data: std::ptr::null_mut(),
callback,
debug,
cip: 0,
frm: 0,
hea: 0,
hlw: 0,
stk: 4096 - 8,
stp: 4096,
flags: 0,
usertags: [0; 4],
userdata: [std::ptr::null_mut(); 4],
error: 0,
paramcount: 2,
pri: 0,
alt: 0,
reset_stk: 0,
reset_hea: 0,
sysreq_d: 0,
};
unsafe { consume_arguments(&raw mut amx) };
assert_eq!({ amx.stk }, 4096);
assert_eq!({ amx.paramcount }, 0);
unsafe { consume_arguments(&raw mut amx) };
assert_eq!({ amx.stk }, 4096);
unsafe { consume_arguments(std::ptr::null_mut()) };
}
fn handler_stub(_amx: &Amx, _args: &mut Args) -> EventReturn {
EventReturn::Continue
}
#[test]
fn event_info_is_copy_and_holds_fields() {
let info = EventInfo {
name: "OnPlayerConnect",
handler: handler_stub,
};
let copy = info;
assert_eq!(copy.name, "OnPlayerConnect");
}
#[test]
fn event_return_suppress_carries_value() {
assert_eq!(EventReturn::Suppress(1), EventReturn::Suppress(1));
assert_ne!(EventReturn::Continue, EventReturn::Suppress(0));
}
#[test]
fn event_return_typed_suppress_encodes_cells() {
assert_eq!(EventReturn::suppress(42_i32), EventReturn::Suppress(42));
assert_eq!(EventReturn::suppress(true), EventReturn::Suppress(1));
assert_eq!(EventReturn::suppress(false), EventReturn::Suppress(0));
assert_eq!(
EventReturn::suppress(1.5_f32),
EventReturn::Suppress(1.5_f32.to_bits().cast_signed())
);
}
#[test]
fn synthetic_params_parse_back_through_args() {
let params: [i32; 3] = [2 * 4, 7, 42];
let amx = Amx::new(std::ptr::null_mut(), 0);
let mut args = Args::new(&amx, params.as_ptr());
assert_eq!(args.count(), 2);
assert_eq!(args.next_arg::<i32>(), Some(7));
assert_eq!(args.next_arg::<i32>(), Some(42));
assert_eq!(args.next_arg::<i32>(), None);
}
#[test]
fn zero_arg_public_yields_empty_arg_list() {
let params: [i32; 1] = [0];
let amx = Amx::new(std::ptr::null_mut(), 0);
let args = Args::new(&amx, params.as_ptr());
assert_eq!(args.count(), 0);
assert!(args.get::<Ref<i32>>(0).is_none());
}
}