Every rust-samp plugin follows the same shape: a Rust type that
implements SampPlugin, methods annotated with #[native], and a
single initialize_plugin! invocation that wires it all together.
SampPlugin traitSampPlugin defines the plugin’s lifecycle. Every method is optional —
the trait provides empty defaults.
pub trait SampPlugin {
/// Called once after the server loads the plugin
/// (`Load()` on SA-MP, `onLoad(ICore*)` on native Open Multiplayer).
fn on_load(&mut self) {}
/// Called when the server unloads the plugin.
fn on_unload(&mut self) {}
/// A Pawn script (`.amx`) was loaded.
fn on_amx_load(&mut self, amx: &Amx) {}
/// A Pawn script is being unloaded.
fn on_amx_unload(&mut self, amx: &Amx) {}
/// Periodic callback. Requires opting in via
/// `samp::plugin::enable_tick()` (or `enable_tick_with(...)`).
/// `TickContext::source` distinguishes SA-MP's main-loop tick
/// from the SDK-owned Open Multiplayer timer; `TickContext::elapsed`
/// is the wall-clock time since the previous dispatch.
fn on_tick(&mut self, ctx: TickContext) {}
/// Every Open Multiplayer component has finished initializing.
/// Compiled only when the `samp-only` feature is **not** active.
#[cfg(not(feature = "samp-only"))]
fn on_omp_ready(&mut self) {}
/// An Open Multiplayer component is being released.
/// Compiled only when the `samp-only` feature is **not** active.
#[cfg(not(feature = "samp-only"))]
fn on_component_free(&mut self) {}
}
The two Open Multiplayer-only hooks exist only when the samp-only
feature is not set. Plugins that must compile both with and without
that feature should gate their overrides with
#[cfg(not(feature = "samp-only"))].
See Native Open Multiplayer support for the full Open Multiplayer lifecycle and feature-flag matrix.
Because every method has a default, a plugin without overrides can use the derive instead of writing the impl by hand:
#[derive(SampPlugin, Default)]
struct MyPlugin;
#[derive(SampPlugin)]emits exactlyimpl SampPlugin for T {}. If a method needs custom logic (on_load,on_tick, …), write the impl by hand and drop the derive.
The plugin struct is mutable (&mut self), so it can hold state across
calls:
struct MyPlugin {
players_online: u32,
sessions: Vec<String>,
}
impl SampPlugin for MyPlugin {
fn on_load(&mut self) {
self.players_online = 0;
println!("Plugin ready.");
}
}
initialize_plugin! { ... } — instantiate the plugin.on_load — once, after the server loads the plugin.on_amx_load — each time a Pawn script is loaded.on_tick — repeatedly, while enabled.on_amx_unload — each time a Pawn script is unloaded.on_unload — once, before shutdown.On native Open Multiplayer, on_omp_ready fires between on_load and
the first on_amx_load, and on_component_free fires when any other
component is released.
initialize_plugin! macroinitialize_plugin! does three things:
Load,
Unload, Supports, AmxLoad, AmxUnload, ProcessTick) and, by
default, the Open Multiplayer ComponentEntryPoint.type: TFor plugins without initialization logic. Uses Default::default() as
the constructor:
#[derive(SampPlugin, Default)]
struct MyPlugin;
initialize_plugin!(
type: MyPlugin,
natives: [
MyPlugin::function_a,
MyPlugin::function_b,
],
);
For plugins that need to set up logging, encoding, the server tick, or that build initial state:
initialize_plugin!(
natives: [
MyPlugin::function_a,
MyPlugin::function_b,
],
{
samp::plugin::enable_tick();
samp::encoding::set_default_encoding(samp::encoding::WINDOWS_1251);
return MyPlugin {
players_online: 0,
sessions: Vec::new(),
};
}
);
The constructor block must end with
return <instance>;. Any code preceding thereturnruns exactly once, when the server loads the plugin.
Native Open Multiplayer mode is the default: every build without the
samp-only feature emits both the SA-MP exports and the
ComponentEntryPoint. No extra configuration is required.
The component UID is resolved from three sources, in priority order:
uid: 0x..._u64 declared inside initialize_plugin!.[package.metadata.samp] uid = "0x..." in Cargo.toml.CARGO_PKG_NAME@CARGO_PKG_VERSION. The
computed value is written back into Cargo.toml under
[package.metadata.samp] on the next build, so subsequent builds
reuse the same value.To declare the metadata directly in Cargo.toml:
[package.metadata.samp]
uid = "0x4D455550CAFEBABE"
name = "MyPlugin" # optional — default: CARGO_PKG_NAME
version = "1.0.0" # optional — default: CARGO_PKG_VERSION
To declare it directly in code (macro values take precedence over
Cargo.toml):
initialize_plugin!(
uid: 0x4D455550CAFEBABE_u64,
component_name: "MyPlugin", // optional
component_version: (1, 0, 0), // optional
natives: [MyPlugin::function_a],
{ return MyPlugin::new(); }
);
See Native Open Multiplayer support for the full explanation.
If the plugin only reacts to events:
// Short form
initialize_plugin!(type: MyPlugin, natives: []);
// Full form
initialize_plugin!(
natives: [],
{ return MyPlugin; }
);
By default on_tick is not called. Opt in inside the constructor
block:
initialize_plugin!(
natives: [],
{
samp::plugin::enable_tick();
return MyPlugin::default();
}
);
The two servers schedule the callback differently:
Supports::PROCESS_TICK is advertised and the server
calls ProcessTick on every iteration of its main loop. The
cadence is whatever the server is configured for; the SDK has no
control over it.ProcessTick
equivalent. The SDK queries ITimersComponent in on_ready and
installs a repeating timer; its timeout dispatches on_tick. The
interval is configurable.For custom cadence or per-server control, use the explicit form:
use std::time::Duration;
use samp::plugin::{enable_tick_with, TickConfig};
// Both servers, but 100 ms on Open Multiplayer instead of 5 ms:
enable_tick_with(TickConfig::new().omp_interval(Duration::from_millis(100)));
// Shortcut: SA-MP only, no Open Multiplayer timer.
enable_tick_with(TickConfig::sa_mp_only());
// Shortcut: Open Multiplayer only, at the given interval.
enable_tick_with(TickConfig::omp_only(Duration::from_millis(50)));
TickContext::source (passed to on_tick) reports
TickSource::SaMp or TickSource::OmpTimer, so the same
method can branch on origin when needed.
Server start
└─ Plugin load
├─ initialize_plugin! { ... } ← construct the instance
├─ on_load()
├─ Gamemode loaded → on_amx_load(amx)
├─ [loop] on_tick(ctx) (if enabled — cadence dictated by the server)
├─ Gamemode unloaded → on_amx_unload(amx)
└─ on_unload()
Server shutdown
Server start
└─ Plugin load (ComponentEntryPoint)
├─ initialize_plugin! { ... } ← construct the instance
├─ on_load() ← from comp_on_load(ICore*)
├─ on_omp_ready() ← every component initialized
├─ Script loaded → on_amx_load(amx)
├─ [loop] on_tick(ctx) (if enabled — SDK-owned ITimer at configured interval)
├─ on_component_free() ← another component being released
├─ Script unloaded → on_amx_unload(amx)
└─ on_unload() ← from comp_free
Server shutdown