rust-samp 3.2.0

Write SA-MP and open.mp plugins in safe Rust instead of C++. A single binary runs natively on both servers, with proc macros (`#[native]`, `initialize_plugin!`) that hide the FFI boilerplate and ABI-correct marshalling for Linux (Itanium) and Windows (MSVC).
Documentation
//! Glue layer between the exports generated by `samp-codegen` and the
//! [`Runtime`]/`SampPlugin`.
//!
//! Each public function here is the destination of a server callback:
//!
//! - `supports`/`load`/`unload`/`amx_load`/`amx_unload`/`server_tick` —
//!   called by SA-MP exports (`Supports`, `Load`, `Unload`, etc).
//! - `omp_initialize`/`omp_store_natives`/`omp_load`/`omp_on_init`/
//!   `omp_on_ready`/`omp_on_free`/`omp_cleanup` — called by the generated
//!   `ComponentEntryPoint` and by the Rust `IComponent` vtable.
//!
//! Marked `#[doc(hidden)]` in `lib.rs` — not part of the plugin's public API.

#[cfg(not(feature = "samp-only"))]
use crate::macros::sdk_warn;
use crate::runtime::Runtime;
use samp_sdk::raw::types::{AMX, AMX_NATIVE_INFO};

#[cfg(not(feature = "samp-only"))]
use samp_sdk::omp::component::ICore;
#[cfg(not(feature = "samp-only"))]
use samp_sdk::omp::events::{PawnEventHandler, PawnEventHandlerVTable};
#[cfg(not(feature = "samp-only"))]
use samp_sdk::omp::server::{
    IPawnScript, PAWN_COMPONENT_UID, ServerComponentList, add_pawn_event_handler,
    get_amx_from_script, get_amx_functions, get_pawn_event_dispatcher, query_component,
    remove_pawn_event_handler,
};
#[cfg(not(feature = "samp-only"))]
use samp_sdk::omp::timers::{
    ITimer, TimerHandlerVTable, TimerTimeOutHandler, create_repeating_timer, kill_timer,
    query_timers_component,
};

/// Static vtable of our `PawnEventHandler`.
#[cfg(not(feature = "samp-only"))]
static PAWN_HANDLER_VTABLE: PawnEventHandlerVTable = PawnEventHandlerVTable {
    on_amx_load: pawn_on_amx_load,
    on_amx_unload: pawn_on_amx_unload,
};

/// Vtable of our `TimerTimeOutHandler` to deliver `on_tick` on Open Multiplayer.
#[cfg(not(feature = "samp-only"))]
static TICK_HANDLER_VTABLE: TimerHandlerVTable = TimerHandlerVTable {
    timeout: tick_handler_timeout,
    free: tick_handler_free,
};

/// Shared timeout logic — fires the plugin's tick with
/// [`TickSource::OmpTimer`] as the source.
///
/// [`TickSource::OmpTimer`]: crate::plugin::TickSource::OmpTimer
#[cfg(not(feature = "samp-only"))]
#[inline]
fn inner_tick_timeout() {
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        tick(crate::plugin::TickSource::OmpTimer);
    }));
}

/// Timer callback — called by the server on every timeout (~5ms).
#[cfg(all(not(feature = "samp-only"), not(target_env = "msvc")))]
unsafe extern "C" fn tick_handler_timeout(_handler: *mut TimerTimeOutHandler, _timer: *mut ITimer) {
    inner_tick_timeout();
}

#[cfg(all(not(feature = "samp-only"), target_env = "msvc"))]
unsafe extern "thiscall" fn tick_handler_timeout(
    _handler: *mut TimerTimeOutHandler,
    _timer: *mut ITimer,
) {
    inner_tick_timeout();
}

/// `free` callback — called once when the server destroys the timer.
/// Releases the handler we allocated in `Box::into_raw`.
#[cfg(all(not(feature = "samp-only"), not(target_env = "msvc")))]
unsafe extern "C" fn tick_handler_free(handler: *mut TimerTimeOutHandler, _timer: *mut ITimer) {
    if !handler.is_null() {
        let _ = unsafe { Box::from_raw(handler) };
    }
}

#[cfg(all(not(feature = "samp-only"), target_env = "msvc"))]
unsafe extern "thiscall" fn tick_handler_free(
    handler: *mut TimerTimeOutHandler,
    _timer: *mut ITimer,
) {
    if !handler.is_null() {
        let _ = unsafe { Box::from_raw(handler) };
    }
}

/// Shared logic of `pawn_on_amx_load` — ABI independent.
///
/// Open Multiplayer may fire `on_amx_load` for pre-loaded gamemodes BEFORE `on_ready`
/// is called — at which point `getAmxFunctions()` still returns 0.
/// In that case we enqueue the AMX and process it later in `omp_on_ready`.
#[cfg(not(feature = "samp-only"))]
fn inner_amx_load(script: *mut IPawnScript) {
    let amx_ptr = unsafe { get_amx_from_script(script) };
    if amx_ptr.is_null() {
        return;
    }
    let rt = Runtime::get();
    if rt.omp_has_amx_exports() {
        let natives = rt.omp_natives();
        amx_load(amx_ptr, natives);
    } else {
        rt.enqueue_pending_amx(amx_ptr);
    }
}

/// Shared logic of `pawn_on_amx_unload` — ABI independent.
#[cfg(not(feature = "samp-only"))]
fn inner_amx_unload(script: *mut IPawnScript) {
    let amx_ptr = unsafe { get_amx_from_script(script) };
    if !amx_ptr.is_null() {
        amx_unload(amx_ptr);
    }
}

/// Callback: Pawn script loaded (native Open Multiplayer mode) — Itanium ABI.
///
/// # Safety
/// `script` must be a valid pointer to the Open Multiplayer server's `IPawnScript`.
#[cfg(all(not(feature = "samp-only"), not(target_env = "msvc")))]
unsafe extern "C" fn pawn_on_amx_load(_this: *mut PawnEventHandler, script: *mut IPawnScript) {
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| inner_amx_load(script)));
}

/// Callback: Pawn script loaded (native Open Multiplayer mode) — MSVC ABI.
///
/// # Safety
/// `script` must be a valid pointer to the Open Multiplayer server's `IPawnScript`.
#[cfg(all(not(feature = "samp-only"), target_env = "msvc"))]
unsafe extern "thiscall" fn pawn_on_amx_load(
    _this: *mut PawnEventHandler,
    script: *mut IPawnScript,
) {
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| inner_amx_load(script)));
}

/// Callback: Pawn script unloaded (native Open Multiplayer mode) — Itanium ABI.
///
/// # Safety
/// `script` must be a valid pointer to the Open Multiplayer server's `IPawnScript`.
#[cfg(all(not(feature = "samp-only"), not(target_env = "msvc")))]
unsafe extern "C" fn pawn_on_amx_unload(_this: *mut PawnEventHandler, script: *mut IPawnScript) {
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| inner_amx_unload(script)));
}

/// Callback: Pawn script unloaded (native Open Multiplayer mode) — MSVC ABI.
///
/// # Safety
/// `script` must be a valid pointer to the Open Multiplayer server's `IPawnScript`.
#[cfg(all(not(feature = "samp-only"), target_env = "msvc"))]
unsafe extern "thiscall" fn pawn_on_amx_unload(
    _this: *mut PawnEventHandler,
    script: *mut IPawnScript,
) {
    let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| inner_amx_unload(script)));
}

#[must_use]
pub fn supports() -> u32 {
    let rt = Runtime::get();
    let supports = rt.supports();

    supports.bits()
}

pub fn load(server_exports: *const usize) {
    let rt = Runtime::get();
    let plugin = Runtime::plugin();

    rt.set_server_exports(server_exports);
    plugin.on_load();
}

pub fn unload() {
    let plugin = Runtime::plugin();
    plugin.on_unload();
}

pub fn amx_load(amx: *mut AMX, natives: &[AMX_NATIVE_INFO]) {
    let rt = Runtime::get();
    let plugin = Runtime::plugin();

    let amx = rt.insert_amx(amx);
    let _ = amx.register(natives); // don't care about errors, that function always raises errors.

    plugin.on_amx_load(amx);
}

pub fn amx_unload(amx: *mut AMX) {
    let rt = Runtime::get();
    let plugin = Runtime::plugin();

    if let Some(amx) = rt.remove_amx(amx) {
        plugin.on_amx_unload(&amx);
    }
}

/// Fires the plugin's [`on_tick`] callback. Called by the `ProcessTick()`
/// export on SA-MP and by the `ITimersComponent` handler on native Open
/// Multiplayer. The caller passes the [`TickSource`] of the dispatch so
/// the plugin can tell the two apart through `TickContext::source`.
///
/// [`on_tick`]: crate::plugin::SampPlugin::on_tick
/// [`TickSource`]: crate::plugin::TickSource
#[inline]
pub fn tick(source: crate::plugin::TickSource) {
    let rt = Runtime::get();
    let elapsed = rt.record_tick();
    let ctx = crate::plugin::TickContext { elapsed, source };
    Runtime::plugin().on_tick(ctx);
}

/// Called by the generated `ComponentEntryPoint` — initializes the runtime in native Open Multiplayer mode.
///
/// Equivalent to SA-MP's `Supports()`: creates the Runtime and instantiates the plugin.
#[cfg(not(feature = "samp-only"))]
pub fn omp_initialize<F, T>(constructor: F)
where
    F: FnOnce() -> T + 'static,
    T: crate::plugin::SampPlugin + 'static,
{
    crate::plugin::initialize(constructor);
}

/// Stores the list of natives for later use in `pawn_on_amx_load` (native Open Multiplayer mode).
///
/// Must be called by the generated `ComponentEntryPoint` immediately after `omp_initialize`,
/// ensuring natives are available before any Pawn script is loaded.
#[cfg(not(feature = "samp-only"))]
pub fn omp_store_natives(natives: Vec<AMX_NATIVE_INFO>) {
    Runtime::get().set_omp_natives(natives);
}

/// Called by the vtable's `on_load` handler — equivalent to SA-MP's `Load()`.
///
/// Stores the `ICore*` in the runtime (available via `samp::plugin::omp_core()`)
/// and invokes `plugin.on_load()`.
#[cfg(not(feature = "samp-only"))]
pub fn omp_load(core: *mut ICore) {
    if core.is_null() {
        sdk_warn!("null ICore* in on_load — samp::plugin::omp_core() will return None");
    }
    Runtime::get().set_omp_core(core);
    Runtime::plugin().on_load();
}

/// Called by the vtable's `on_init` handler.
///
/// Looks up `IPawnComponent` in the component list and stores the AMX function
/// table in the runtime, enabling native registration via `AmxLoad`.
///
/// # Safety
/// `components` must be a valid pointer to the Open Multiplayer server's `IComponentList`.
#[cfg(not(feature = "samp-only"))]
pub unsafe fn omp_on_init(components: *mut ServerComponentList) {
    let rt = Runtime::get();

    rt.set_omp_component_list(components);

    let pawn = unsafe { query_component(components, PAWN_COMPONENT_UID) };
    if pawn.is_null() {
        sdk_warn!("IPawnComponent not found in on_init — Pawn natives unavailable");
        return;
    }

    // Adaptive attempt: in the current Open Multiplayer version (1.5.x), getAmxFunctions()
    // returns 0 in on_init and is only valid in on_ready. But we test here anyway —
    // if future versions start providing it as early as on_init, we take advantage
    // automatically. omp_on_ready below checks whether we already have exports before
    // retrying, keeping retro/forward compat.
    let exports = unsafe { get_amx_functions(pawn) };
    if exports != 0 {
        rt.set_omp_amx_exports(exports);
    }

    // Register the dispatcher to receive on_amx_load/on_amx_unload.
    let dispatcher = unsafe { get_pawn_event_dispatcher(pawn) };
    if dispatcher.is_null() {
        sdk_warn!(
            "null IEventDispatcher<PawnEventHandler> in on_init — on_amx_load/on_amx_unload will not be called"
        );
    } else {
        let handler = Box::into_raw(Box::new(PawnEventHandler::new(
            &raw const PAWN_HANDLER_VTABLE,
        )));
        rt.set_pawn_event_handler(handler);
        unsafe { add_pawn_event_handler(dispatcher, handler) };
    }
}

/// Called by the vtable's `on_ready` handler — all server components have
/// finished initializing.
#[cfg(not(feature = "samp-only"))]
pub fn omp_on_ready() {
    let rt = Runtime::get();

    // If we already have exports (in case `on_init` succeeded in a future
    // Open Multiplayer version), do not re-query. Otherwise, try now — that is the
    // expected behavior in the current version.
    if !rt.omp_has_amx_exports() {
        if let Some(pawn) = rt.omp_query_component(PAWN_COMPONENT_UID) {
            let exports = unsafe { get_amx_functions(pawn) };
            if exports != 0 {
                rt.set_omp_amx_exports(exports);
            } else {
                sdk_warn!("getAmxFunctions() returned 0 in on_ready — Pawn natives unavailable");
            }
        } else {
            sdk_warn!("on_ready: IPawnComponent not found");
        }
    }

    // Process AMXs that arrived before we had the fn_table (always — regardless
    // of when the exports were obtained, on_init or on_ready).
    if rt.omp_has_amx_exports() {
        let pending = rt.take_pending_amx();
        if !pending.is_empty() {
            let natives = rt.omp_natives().to_vec();
            for amx in pending {
                amx_load(amx, &natives);
            }
        }
    }

    // Tick abstraction: if the plugin opted in to the tick on the Open
    // Multiplayer side via `enable_tick` / `enable_tick_with`, create a
    // repeating timer in `ITimersComponent` at the configured interval and
    // route its timeout into `SampPlugin::on_tick`.
    if let Some(interval) = rt.omp_tick_interval()
        && let Some(components) = rt.omp_component_list()
    {
        let timers = unsafe { query_timers_component(components) };
        if timers.is_null() {
            sdk_warn!(
                "ITimersComponent not found — on_tick will not be called on Open Multiplayer"
            );
        } else {
            let handler = Box::into_raw(Box::new(TimerTimeOutHandler {
                vtable: &raw const TICK_HANDLER_VTABLE,
            }));
            // `ITimersComponent::create` takes the interval as i64
            // milliseconds. Clamp to i64::MAX as a defense against absurd
            // values; in practice intervals are at most a few seconds.
            let interval_ms = i64::try_from(interval.as_millis()).unwrap_or(i64::MAX);
            let timer = unsafe { create_repeating_timer(timers, handler, interval_ms) };
            if timer.is_null() {
                sdk_warn!(
                    "failed to create timer on ITimersComponent — on_tick will not be called on Open Multiplayer"
                );
                let _ = unsafe { Box::from_raw(handler) };
            } else {
                rt.set_omp_tick(timer, handler);
            }
        }
    }

    Runtime::plugin().on_omp_ready();
}

/// Called by the vtable's `on_free` handler — notifies the plugin that a
/// server component is being unloaded.
#[cfg(not(feature = "samp-only"))]
pub fn omp_on_free() {
    Runtime::plugin().on_component_free();
}

/// Open Multiplayer cleanup — disables SDK resources before shutdown:
///   1. Kills the `on_tick` timer (if it was created in `on_ready`).
///   2. Removes the `PawnEventHandler` from the dispatcher.
///
/// Called by `comp_free` before `unload()`. Avoids use-after-free in case the
/// server tries to fire Pawn events or ticks after the component is released.
#[cfg(not(feature = "samp-only"))]
pub fn omp_cleanup() {
    let rt = Runtime::get();

    // 1) Kill the tick timer. The server invokes `tick_handler_free` in response,
    //    which releases the heap handler. We clear the handler pointer here only
    //    to drop the reference — the Box is dropped in the `free` callback.
    if let Some(timer) = rt.take_omp_tick_timer() {
        unsafe { kill_timer(timer) };
        let _ = rt.take_omp_tick_handler(); // ownership already passed to the free callback
    }

    // 2) Unregister the PawnEventHandler from the dispatcher.
    if let Some(handler) = rt.take_pawn_event_handler() {
        if let Some(pawn) = rt.omp_query_component(samp_sdk::omp::server::PAWN_COMPONENT_UID) {
            let dispatcher = unsafe { get_pawn_event_dispatcher(pawn) };
            if !dispatcher.is_null() {
                unsafe { remove_pawn_event_handler(dispatcher, handler) };
            }
        }
        drop(unsafe { Box::from_raw(handler) });
    }
}