Advanced examples¶
This chapter walks through the larger plugins shipped under
examples/. They build on top of Plugin
anatomy and Natives and exercise the
more advanced features of the SDK.
examples/counter — stateful plugin with the unified tick¶
examples/counter demonstrates:
- A struct that holds plugin state (
count,max,ticks). - A handwritten
impl SampPluginoverridingon_load,on_unload, andon_tick. - The full
initialize_plugin!form with a constructor block that enables the unified tick and a customferndispatch. - Multiple natives, including one that writes through
Ref<i32>.
use log::info;
use samp::plugin::TickContext;
use samp::prelude::*;
use samp::{initialize_plugin, native};
struct Counter {
count: i32,
max: i32,
ticks: u32,
}
impl SampPlugin for Counter {
fn on_load(&mut self) {
info!("Counter plugin loaded. Max={}", self.max);
}
fn on_unload(&mut self) {
info!("Counter plugin unloaded. Final value={}", self.count);
}
fn on_tick(&mut self, _ctx: TickContext) {
self.ticks += 1;
if self.ticks.is_multiple_of(1000) {
info!("Counter tick={} count={}/{}", self.ticks, self.count, self.max);
}
}
}
impl Counter {
#[native(name = "Counter_Get")]
fn get(&mut self, _amx: &Amx, mut out: Ref<i32>) -> bool {
*out = self.count;
true
}
#[native(name = "Counter_Increment")]
fn increment(&mut self, _amx: &Amx) -> i32 {
if self.count >= self.max { return -1; }
self.count += 1;
self.count
}
// … decrement, reset, set_max, is_at_max
}
initialize_plugin!(
natives: [
Counter::increment, Counter::decrement, Counter::reset,
Counter::get, Counter::set_max, Counter::is_at_max,
],
{
samp::plugin::enable_tick();
let _ = fern::Dispatch::new()
.level(log::LevelFilter::Info)
.chain(samp::plugin::logger())
.apply();
return Counter { count: 0, max: 100, ticks: 0 };
}
);
Pawn-side declarations:
native Counter_Increment();
native Counter_Decrement();
native Counter_Reset();
native Counter_Get(&out);
native Counter_SetMax(max);
native bool:Counter_IsAtMax();
examples/advanced — memcache client with custom types¶
examples/advanced demonstrates:
- A custom return type implementing
AmxCell. - Persistent plugin state (
Vec<memcache::Client>). - Multiple native shapes — strings, refs, output buffers.
- The
encodingfeature in use (Windows-1251 explicit). - A layered
ferndispatch: server log + custom file atTracelevel.
Custom return type¶
#[derive(Debug, Clone, Copy)]
enum MemcacheResult {
Success(i32),
NoData,
NoClient,
NoKey,
}
impl AmxCell<'_> for MemcacheResult {
fn as_cell(&self) -> i32 {
match self {
MemcacheResult::Success(v) => *v,
MemcacheResult::NoData => -1,
MemcacheResult::NoClient => -2,
MemcacheResult::NoKey => -3,
}
}
}
From Pawn the result is read as a plain integer:
new id = Memcached_Connect("memcache://127.0.0.1:11211");
if (id >= 0) {
// id is the connection slot
} else if (id == -2) {
// connection failed
}
Working with &AmxString in generic contexts¶
Client::connect is generic over Connectable, which is implemented
for &str but not for &AmxString. Rust does not apply deref
coercion on generic bounds, so the explicit &** is required:
#[native(name = "Memcached_Connect")]
pub fn connect(&mut self, _: &Amx, address: &AmxString) -> MemcacheResult {
match Client::connect(&**address) {
Ok(client) => {
self.clients.push(client);
let idx = i32::try_from(self.clients.len()).unwrap_or(i32::MAX);
MemcacheResult::Success(idx - 1)
}
Err(_) => MemcacheResult::NoClient,
}
}
When the parameter has a concrete &str type (no generic bound), the
deref coercion happens automatically and name is enough.
Output through Ref<i32>¶
#[native(name = "Memcached_Get")]
pub fn get(
&mut self,
_: &Amx,
con: usize,
key: &AmxString,
mut value: Ref<i32>,
) -> MemcacheResult {
if con < self.clients.len() {
match self.clients[con].get(key) {
Ok(Some(data)) => { *value = data; MemcacheResult::Success(1) }
Ok(None) => MemcacheResult::NoData,
Err(_) => MemcacheResult::NoKey,
}
} else {
MemcacheResult::NoClient
}
}
Writing a string back through UnsizedBuffer¶
#[native(name = "Memcached_GetString")]
pub fn get_string(
&mut self,
_: &Amx,
con: usize,
key: &AmxString,
buffer: UnsizedBuffer,
size: usize,
) -> AmxResult<MemcacheResult> {
if con < self.clients.len() {
match self.clients[con].get::<String>(key) {
Ok(Some(data)) => {
buffer.write_str(size, &data)?;
Ok(MemcacheResult::Success(1))
}
Ok(None) => Ok(MemcacheResult::NoData),
Err(_) => Ok(MemcacheResult::NoKey),
}
} else {
Ok(MemcacheResult::NoClient)
}
}
Layered fern dispatch¶
initialize_plugin!(
natives: [
Memcached::connect, Memcached::get, Memcached::set,
Memcached::get_string, Memcached::set_string,
Memcached::increment, Memcached::delete,
],
{
samp::plugin::enable_tick();
samp::encoding::set_default_encoding(samp::encoding::WINDOWS_1251);
let samp_logger = samp::plugin::logger()
.level(log::LevelFilter::Info);
let log_file = fern::log_file("myplugin.log")
.expect("failed to open log file");
let trace_level = fern::Dispatch::new()
.level(log::LevelFilter::Trace)
.chain(log_file);
let _ = fern::Dispatch::new()
.format(|callback, message, record| {
callback.finish(format_args!(
"memcached {}: {}",
record.level().to_string().to_lowercase(),
message
));
})
.chain(samp_logger)
.chain(trace_level)
.apply();
return Memcached { clients: Vec::new() };
}
);
Useful patterns¶
Per-AMX state¶
When the server hosts both a gamemode and one or more filterscripts,
each gets its own Amx. Keep per-script state keyed by AmxIdent:
use std::collections::HashMap;
use samp::amx::AmxExt;
struct MyPlugin {
by_amx: HashMap<samp::amx::AmxIdent, Vec<String>>,
}
impl SampPlugin for MyPlugin {
fn on_amx_load(&mut self, amx: &Amx) {
self.by_amx.insert(amx.ident(), Vec::new());
}
fn on_amx_unload(&mut self, amx: &Amx) {
self.by_amx.remove(&amx.ident());
}
}
Throttling work inside the tick¶
use samp::plugin::TickContext;
struct MyPlugin { tick_count: u64 }
impl SampPlugin for MyPlugin {
fn on_tick(&mut self, _ctx: TickContext) {
self.tick_count += 1;
if self.tick_count.is_multiple_of(1000) {
self.periodic_work();
}
}
}
A 5 ms tick × 1000 iterations ≈ 5 seconds between runs.