use samp_sdk::amx::Amx;
use samp_sdk::args::Args;
use samp_sdk::raw::types::AMX;
use crate::amx::AmxIdent;
use crate::runtime::Runtime;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use std::cell::RefCell;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use std::collections::HashSet;
#[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;
#[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 ident = AmxIdent::from(ptr.as_ptr());
rt.remove_resolved_events(ident);
for event in rt.events_snapshot() {
if let Ok(idx) = amx.find_public(event.name) {
rt.push_resolved_event(ident, i32::from(idx), event.handler);
}
}
}
#[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 target: ExecFn = Exec::from_table(fn_table);
let detour = match unsafe { GenericDetour::new(target, exec_detour) } {
Ok(detour) => detour,
Err(err) => {
log::warn!("[rust-samp] 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() } {
log::warn!("[rust-samp] 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 =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| dispatch(amx, index)))
.unwrap_or(None);
if let Some(value) = suppressed {
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"))]
thread_local! {
static ACTIVE: RefCell<HashSet<(usize, i32)>> = RefCell::new(HashSet::new());
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
struct ActiveGuard(usize, i32);
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
impl ActiveGuard {
fn acquire(key: (usize, i32)) -> Option<Self> {
ACTIVE.with(|active| {
active
.borrow_mut()
.insert(key)
.then_some(ActiveGuard(key.0, key.1))
})
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
impl Drop for ActiveGuard {
fn drop(&mut self) {
ACTIVE.with(|active| {
active.borrow_mut().remove(&(self.0, self.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);
if handlers.is_empty() {
return None;
}
let _guard = ActiveGuard::acquire((amx_ptr as usize, idx))?;
let amx = crate::amx::get(ident)?;
let params = read_stack_params(amx_ptr, amx)?;
let mut args = Args::new(amx, params.as_ptr());
for handler in handlers {
args.reset();
if let EventReturn::Suppress(value) = handler(amx, &mut args) {
return Some(value);
}
}
None
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn read_stack_params(amx_ptr: *mut AMX, amx: &Amx) -> Option<Vec<i32>> {
let (paramcount, stk) = unsafe {
(
std::ptr::addr_of!((*amx_ptr).paramcount).read_unaligned(),
std::ptr::addr_of!((*amx_ptr).stk).read_unaligned(),
)
};
if paramcount < 0 {
return None;
}
let count = paramcount as usize;
let mut params = Vec::with_capacity(count + 1);
params.push(paramcount.checked_mul(4)?);
for k in 0..count {
let offset = i32::try_from(k).ok()?.checked_mul(4)?;
let addr = stk.checked_add(offset)?;
params.push(amx.read_cell(addr)?);
}
Some(params)
}
#[cfg(test)]
mod tests {
use super::*;
use samp_sdk::cell::Ref;
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());
}
}