Events¶
Events are Pawn callbacks — OnPlayerConnect, OnPlayerSpawn,
OnPlayerText, and friends — observed directly in Rust. Where a
native is a function the gamemode calls into, a callback runs in
the gamemode's own script and the plugin only gets to watch it. The #[event]
attribute is what lets a Rust plugin react to them, closing the gap between
writing a plugin and writing a whole gamemode in Rust.
How it differs from a native¶
The two macros share their argument-marshalling machinery, but the direction of the call is opposite:
#[native] |
#[event] |
|
|---|---|---|
| Direction | Pawn → Rust | Pawn callback → Rust observer |
| Registered under | natives: [...] |
events: [...] |
| Return value | Returned to the caller | Ignored (observer — see below) |
| Delivery | Server invokes the exported FFI wrapper | SDK intercepts amx_Exec |
Basic shape¶
impl MyPlugin {
#[event(name = "OnPlayerConnect")]
fn on_player_connect(&mut self, _amx: &Amx, playerid: i32) -> AmxResult<i32> {
log::info!("player {playerid} connected");
Ok(1)
}
}
// The gamemode still defines the callback as usual — the Rust handler
// runs *before* this public executes.
public OnPlayerConnect(playerid) {
return 1;
}
Signature rules¶
The signature rules are identical to #[native]:
- The first parameter is
&mut selffor plugin methods. Associated functions (noself) are also accepted. - The next parameter is
&Amx— the AMX the callback is running in. Use_amx: &Amxwhen it is not needed. - Subsequent parameters are the callback arguments, parsed via the
AmxCelltrait exactly like native arguments (see the argument type table). - Returns either
AmxResult<T>/Result<T, E: Display>(the wrapper logs the error onErr) orTdirectly for infallible handlers.
name is validated at proc-macro time — interior NUL bytes fail compilation
rather than panic at server load.
Registering the handler¶
Every handler must appear in the events: [...] list of initialize_plugin!,
alongside the existing natives: [...]:
initialize_plugin!(
natives: [
MyPlugin::some_native,
],
events: [
MyPlugin::on_player_connect,
MyPlugin::on_player_spawn,
],
{
MyPlugin::default()
}
);
Both lists are optional and independent — a plugin may declare only natives, only events, or both.
Observer vs. suppression¶
By default handlers are observers: a handler returning AmxResult<T> / T
has its value ignored and the gamemode's own public always runs afterwards.
Registering an observer never changes the gamemode's control flow.
To cancel a callback, return [EventReturn] instead:
use samp::prelude::*; // brings EventReturn into scope
#[event(name = "OnPlayerCommandText")]
fn on_command(&mut self, _amx: &Amx, playerid: i32, cmd: &AmxString) -> EventReturn {
if self.is_banned(playerid) {
EventReturn::Suppress(1) // skip the gamemode's public; callback returns 1
} else {
EventReturn::Continue // run the public as usual
}
}
EventReturn::Continue— run the gamemode's public (same as an observer).EventReturn::Suppress(value)— skip the public entirely; the callback returnsvalue(a raw AMX cell) to its caller.EventReturn::suppress(value)— the same, but encodes a typed value for you:EventReturn::suppress(1.5_f32)for aFloat:callback,EventReturn::suppress(true)for abool:one.
Multiple handlers may observe the same callback; they run in registration order,
each receiving the same argument list. The first handler to return
Suppress cancels the callback and the remaining handlers are skipped.
Raw handlers¶
For a variadic or protocol-specific callback, add raw to receive the Args
cursor and parse it yourself — the same escape hatch #[native(raw)] offers:
use samp::args::Args;
#[event(name = "OnPlayerCommandText", raw)]
fn on_command(&mut self, _amx: &Amx, args: &mut Args) -> EventReturn {
let _playerid: Option<i32> = args.next_arg();
let cmd: Option<AmxString> = args.next_arg();
if let Some(cmd) = cmd {
log::info!("command: {}", &*cmd);
}
EventReturn::Continue
}
Reentrancy¶
If a handler re-enters the VM (e.g. calls a public via exec_public!) on the
same callback it is currently handling, the SDK runs that public directly
instead of dispatching into the handler again — so a handler cannot recurse into
itself without bound. Different callbacks still nest normally.
How dispatch works¶
To receive a callback the SDK detours the VM's amx_Exec (via the
retour detour library). Every executed
public is inspected and, when its index matches a registered event on that AMX,
the handler runs before the original public. The argument list is rebuilt from
the VM stack into the same shape a native receives, so the same AmxCell
parsing applies.
The detour is installed lazily — only when the plugin registered at least
one #[event] handler and the AMX function table is available. A plugin with
no events never touches amx_Exec.
Platform scope¶
The detour is x86 / x86_64 only — the architectures SA-MP and open.mp
actually run on. On other targets (for example a 64-bit ARM host used to build
samp::debug tooling) the events API still compiles, but the detour is a no-op
and handlers never fire. This is a hard limitation of running on those servers,
not of the SDK.
The same detour drives both SA-MP and native open.mp (it hooks the AMX
amx_Exec obtained from the server either way). Event delivery has been verified
end-to-end on a live SA-MP server; the open.mp path uses the identical
mechanism but has not yet been validated on a live open.mp server.
Panic safety¶
Like #[native], the generated wrapper invokes the handler body inside
std::panic::catch_unwind. A panic that would otherwise cross the extern "C"
boundary back into the VM is captured, logged with the callback name plus
payload, and swallowed — the original public still runs.