Migration guide¶
This page collects the breaking changes (and the new defaults) between the supported releases. Pick the section that matches your current state.
v2.1.0 → v2.2.0¶
No breaking changes — only ergonomic improvements. Old code keeps compiling; the new patterns are simpler and worth adopting.
1. Plugin construction — the short form¶
Before:
struct MyPlugin;
impl SampPlugin for MyPlugin {}
initialize_plugin!(
natives: [MyPlugin::my_native],
{ return MyPlugin; }
);
Now:
#[derive(SampPlugin, Default)]
struct MyPlugin;
initialize_plugin!(
type: MyPlugin,
natives: [MyPlugin::my_native],
);
#[derive(SampPlugin)] emits impl SampPlugin for T {} for stateless
plugins. As soon as a lifecycle hook (on_load, on_tick, …)
needs an override, drop the derive and write the impl by hand — and
switch back to the constructor-block form of initialize_plugin!.
| Situation | Recommended form |
|---|---|
| Stateless plugin | initialize_plugin!(type: T, natives: [...]) |
Setup logic (on_load, logging, …) |
initialize_plugin!(natives: [...], { ... }) |
| Initial struct state | initialize_plugin!(natives: [...], { return T { ... }; }) |
2. AmxString — use Deref instead of .to_string()¶
AmxString implements Deref<Target = str>, so every &str method is
available without an extra allocation.
Before:
fn say_hello(&mut self, _amx: &Amx, name: AmxString) -> AmxResult<bool> {
let name = name.to_string();
println!("Hello, {name}!");
Ok(true)
}
Now:
fn say_hello(&mut self, _amx: &Amx, name: &AmxString) -> AmxResult<bool> {
println!("Hello, {}!", &**name);
Ok(true)
}
Other usage patterns:
if name.starts_with("Admin") { /* ... */ }
if name.contains("vip") { /* ... */ }
let msg = format!("Welcome, {}!", &**name);
connect_to_server(&**name);
The decoding is lazy: the underlying
Stringis only built on the firstDerefaccess, then cached in aOnceCell<String>. If the native never touches the content, noStringis allocated.
3. Output strings — write_str¶
Before:
fn get_value(_amx: &Amx, buffer: UnsizedBuffer, size: usize) -> AmxResult<bool> {
let mut buf = buffer.into_sized_buffer(size);
let _ = samp::cell::string::put_in_buffer(&mut buf, "value");
Ok(true)
}
Now:
fn get_value(_amx: &Amx, buffer: UnsizedBuffer, size: usize) -> AmxResult<bool> {
buffer.write_str(size, "value")?;
Ok(true)
}
The previous helper silenced the error with let _ = …. write_str
propagates AmxError::General through ? when the encoded string is
too long for the buffer.
Available on both Buffer and UnsizedBuffer:
Internally
put_in_bufferstill exists but ispub(crate)— every public surface goes throughwrite_str.
4. Typed Pawn arrays — get_as / set_as¶
Float:arr[] and bool:arr[] no longer require manual bit
manipulation.
Before:
fn process_floats(_amx: &Amx, array: UnsizedBuffer, len: usize) -> AmxResult<bool> {
let buf = array.into_sized_buffer(len);
for i in 0..buf.len() {
let value = f32::from_bits(buf[i] as u32); // manual conversion
println!("{value}");
}
Ok(true)
}
Now:
fn process_floats(_amx: &Amx, array: UnsizedBuffer, len: usize) -> AmxResult<bool> {
let buf = array.into_sized_buffer(len);
for i in 0..buf.len() {
if let Some(value) = buf.get_as::<f32>(i) {
println!("{value}");
}
}
Ok(true)
}
Types supported by get_as / set_as / iter_as: i8, u8, i16,
u16, i32, u32, isize, usize, f32, bool.
get_asandset_asrely on theCellConverttrait, not onAmxCell.AmxCellconverts native arguments;CellConvertconverts individual cells of a buffer. They live in different layers intentionally —CellConvertdoes not need an&Amx.
v2.x → v3.0.0 — native Open Multiplayer support¶
No source-level breaking changes. Existing code compiles unchanged. What changes is what the SDK generates by default.
Summary¶
Starting with v3.0.0, every build that does not enable the
samp-only feature emits the SA-MP exports and the Open
Multiplayer ComponentEntryPoint. The same binary loads on SA-MP and
is treated as a first-class component on Open Multiplayer.
| Version | Generated binary |
|---|---|
| v2.x | SA-MP exports only. |
| v3.0.0 (default) | SA-MP exports and ComponentEntryPoint. |
v3.0.0 with samp-only |
SA-MP exports only (identical to v2.x). |
Unified on_tick¶
The trait method previously called process_tick is now
on_tick(&mut self, ctx: TickContext). The opt-in switched from
samp::plugin::enable_process_tick() to
samp::plugin::enable_tick() (or enable_tick_with(TickConfig) for
custom interval / per-server control).
The unified callback fires on both servers:
- SA-MP — the
ProcessTickexport forwards toon_tickwithctx.source == TickSource::SaMp. Cadence is whatever the server's main loop is configured for. - Native Open Multiplayer — the SDK queries
ITimersComponentand creates a repeating timer whose timeout dispatches the same callback withctx.source == TickSource::OmpTimer. Interval defaults to 5 ms; configurable throughTickConfig::omp_interval.
ctx.elapsed is the wall-clock time since the previous dispatch
(zero on the first call), useful for delta-based logic without
calling Instant::now() in the plugin.
Common TickConfig patterns come with builder shortcuts:
use std::time::Duration;
use samp::plugin::{enable_tick_with, TickConfig};
// SA-MP only — Open Multiplayer timer disabled.
enable_tick_with(TickConfig::sa_mp_only());
// Open Multiplayer only, at a custom interval — SA-MP export stays inert.
enable_tick_with(TickConfig::omp_only(Duration::from_millis(50)));
// Full builder when you need both servers with tweaked Open Multiplayer cadence.
enable_tick_with(TickConfig::new().omp_interval(Duration::from_millis(20)));
Targets¶
| Platform | Target | SA-MP | Native Open Multiplayer |
|---|---|---|---|
| Linux | i686-unknown-linux-gnu |
✅ | ✅ |
| Windows | i686-pc-windows-msvc |
✅ | ✅ |
| Windows | i686-pc-windows-gnu |
✅ | ❌ |
For Windows builds with native Open Multiplayer support, cross-compile
from Linux through cargo-xwin:
i686-pc-windows-gnu does not support native Open Multiplayer —
use it only for SA-MP-only builds (--features samp-only).
Compile-time error after upgrading¶
error[E0080]: evaluation panicked: OmpComponent: invalid size for the
Itanium ABI. Use --target i686-unknown-linux-gnu to compile with
native Open Multiplayer support.
Cause: the build target is x86_64 instead of i686. The SDK
validates OmpComponent's layout at compile time against the i686
Itanium ABI, and on x86_64 pointers are 8 bytes.
Fix: create .cargo/config.toml at the project root:
[build]
# Linux — SA-MP + native Open Multiplayer (default)
target = "i686-unknown-linux-gnu"
# Windows — SA-MP + native Open Multiplayer (requires cargo-xwin)
# target = "i686-pc-windows-msvc"
# Windows — SA-MP only (combine with the samp-only feature)
# target = "i686-pc-windows-gnu"
Option A — keep v2.x behavior (SA-MP only)¶
Enable the samp-only feature. The ComponentEntryPoint is not
emitted; the plugin behaves exactly like in v2.x:
[dependencies]
samp = { git = "https://github.com/NullSablex/rust-samp.git", tag = "v3.0.0", features = ["samp-only"] }
No other change is required.
Option B — adopt native Open Multiplayer support¶
Update the dependency without samp-only. The SDK emits the
ComponentEntryPoint and, when the UID is missing, derives one via
FNV-1a and writes it back to Cargo.toml:
After the first build the Cargo.toml ends up with a new section:
That is the only required change. The same binary loads on SA-MP and is recognized as a native component by Open Multiplayer.
Option B+ — react to Open Multiplayer-only events¶
To hook into events specific to native Open Multiplayer, add the optional methods to the trait impl:
impl SampPlugin for MyPlugin {
fn on_load(&mut self) {
// called on SA-MP and on Open Multiplayer
}
// Every Open Multiplayer component finished initializing.
// The #[cfg] is required only if the plugin must compile both
// with and without the samp-only feature.
#[cfg(not(feature = "samp-only"))]
fn on_omp_ready(&mut self) {
if let Some(_core) = samp::plugin::omp_core() {
log::info!("running on native Open Multiplayer");
}
}
#[cfg(not(feature = "samp-only"))]
fn on_component_free(&mut self) {
log::info!("an Open Multiplayer component was released");
}
}
Migration checklist¶
- Update the dependency tag to
v3.0.0. - Configure
.cargo/config.tomlwith the correct i686 target (if not yet done). - Decide: keep
samp-onlyfor SA-MP-only behavior, or drop the feature to enable dual support. - If using dual support: rename any
process_tickoverrides toon_tick(&mut self, ctx: TickContext), and the opt-in call toenable_tick()(orenable_tick_with(...)for custom interval / per-server control). TheTickContextparameter is mandatory — use_ctxif you don't read it. - Build once and verify the UID was written into
[package.metadata.samp].
Legacy samp_sdk → current API¶
This section covers the move from the original samp_sdk (pre-v1)
to the current samp crate.
Summary¶
| Before | Now |
|---|---|
samp_sdk = "*" |
samp = { git = "https://github.com/NullSablex/rust-samp.git", tag = "v3.0.0" } |
new_plugin!(Plugin) |
initialize_plugin!(type: T, natives: [...]) or constructor-block form |
define_native!(name, args) |
#[native(name = "Name")] |
impl Default for Plugin |
#[derive(Default)] or a constructor block |
AMX (raw) |
Amx (safe wrapper) |
Cell |
i32, Ref<T>, AmxString, custom AmxCell impls |
| Manual native registration | Automatic, through initialize_plugin! |
string.to_string() |
&*string via Deref<Target = str> |
process_tick |
on_tick(ctx: TickContext) (unified across servers; opt in via enable_tick() / enable_tick_with(TickConfig)) |
1. Update Cargo.toml¶
- [dependencies]
- samp_sdk = "*"
+ [dependencies]
+ samp = { git = "https://github.com/NullSablex/rust-samp.git", tag = "v3.0.0" }
2. Update imports¶
- use samp_sdk::new_plugin;
- use samp_sdk::...;
+ use samp::prelude::*;
+ use samp::{native, initialize_plugin, SampPlugin};
3. Replace define_native! with #[native]¶
Before:
Now:
#[native(name = "MyNative")]
fn my_native(&mut self, _amx: &Amx, text: &AmxString) -> AmxResult<bool> {
println!("{}", &**text);
Ok(true)
}
#[native(name = "RawNative", raw)]
fn raw_native(&mut self, amx: &Amx, args: Args) -> AmxResult<f32> {
Ok(1.0)
}
4. Replace new_plugin! with initialize_plugin!¶
Before:
Now (short form):
#[derive(SampPlugin, Default)]
struct Plugin;
initialize_plugin!(
type: Plugin,
natives: [Plugin::my_native],
);
Now (full form):