Skip to main content

samp/
plugin.rs

1//! API the Rust plugin uses: trait [`SampPlugin`] (lifecycle) + global
2//! functions to enable features (`enable_tick`, `logger`, `omp_query`).
3
4use std::ptr::NonNull;
5use std::time::Duration;
6
7use samp_sdk::amx::Amx;
8use samp_sdk::cell::AmxCell;
9
10use crate::runtime::Runtime;
11
12#[doc(hidden)]
13pub fn initialize<F, T>(constructor: F)
14where
15    F: FnOnce() -> T + 'static,
16    T: SampPlugin + 'static,
17{
18    let rt = Runtime::initialize();
19    let plugin = constructor();
20
21    rt.set_plugin(plugin);
22    rt.post_initialize();
23}
24
25/// Tells the SDK how often [`SampPlugin::on_tick`] should fire on each
26/// server.
27///
28/// The two servers schedule periodic callbacks differently:
29///
30/// - **SA-MP** exports `ProcessTick`. The server's main loop invokes it on
31///   every iteration — the cadence is whatever the server is configured for.
32///   The SDK has no say over the interval; the [`sa_mp`] flag only decides
33///   whether the export is advertised at all.
34/// - **native Open Multiplayer** has no built-in `ProcessTick` equivalent.
35///   The SDK installs a repeating timer on the server's `ITimersComponent`
36///   in `on_ready` and dispatches the timeout into [`on_tick`]. The
37///   interval is the [`omp_interval`] field.
38///
39/// [`sa_mp`]: TickConfig::sa_mp
40/// [`omp_interval`]: TickConfig::omp_interval
41/// [`on_tick`]: SampPlugin::on_tick
42#[derive(Debug, Clone, Copy)]
43pub struct TickConfig {
44    /// Enable the tick on SA-MP. When `false`, the plugin does not advertise
45    /// `Supports::PROCESS_TICK` and the export becomes inert.
46    pub sa_mp: bool,
47    /// Enable the tick on native Open Multiplayer. When `false`, the SDK
48    /// does not create the `ITimersComponent` timer in `on_ready`.
49    pub omp: bool,
50    /// Interval the SDK uses when creating the Open Multiplayer timer.
51    /// Ignored when [`omp`] is `false`. Ignored entirely on SA-MP (the
52    /// server controls the cadence).
53    ///
54    /// [`omp`]: TickConfig::omp
55    pub omp_interval: Duration,
56}
57
58impl Default for TickConfig {
59    /// Default: enabled on both servers, 5 ms timer on Open Multiplayer.
60    fn default() -> Self {
61        Self {
62            sa_mp: true,
63            omp: true,
64            omp_interval: Duration::from_millis(5),
65        }
66    }
67}
68
69impl TickConfig {
70    /// Equivalent to `TickConfig::default()`.
71    #[must_use]
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Builder: sets [`sa_mp`].
77    ///
78    /// [`sa_mp`]: TickConfig::sa_mp
79    #[must_use]
80    pub fn sa_mp(mut self, enabled: bool) -> Self {
81        self.sa_mp = enabled;
82        self
83    }
84
85    /// Builder: sets [`omp`].
86    ///
87    /// [`omp`]: TickConfig::omp
88    #[must_use]
89    pub fn omp(mut self, enabled: bool) -> Self {
90        self.omp = enabled;
91        self
92    }
93
94    /// Builder: sets [`omp_interval`].
95    ///
96    /// [`omp_interval`]: TickConfig::omp_interval
97    #[must_use]
98    pub fn omp_interval(mut self, interval: Duration) -> Self {
99        self.omp_interval = interval;
100        self
101    }
102
103    /// Shortcut: tick only on SA-MP. Equivalent to
104    /// `TickConfig::new().omp(false)`.
105    ///
106    /// Use when the plugin has no meaningful work to do on the Open
107    /// Multiplayer tick — for example, a pure SA-MP plugin running in
108    /// legacy mode under Open Multiplayer.
109    #[must_use]
110    pub fn sa_mp_only() -> Self {
111        Self::default().omp(false)
112    }
113
114    /// Shortcut: tick only on native Open Multiplayer, at the supplied
115    /// interval. Equivalent to
116    /// `TickConfig::new().sa_mp(false).omp_interval(interval)`.
117    ///
118    /// Use when the plugin needs a controlled cadence specifically on
119    /// Open Multiplayer and should stay silent on SA-MP — for example,
120    /// a component that drives a long-poll loop only meaningful when
121    /// the component API is reachable.
122    #[must_use]
123    pub fn omp_only(interval: Duration) -> Self {
124        Self::default().sa_mp(false).omp_interval(interval)
125    }
126}
127
128/// Origin of the current [`SampPlugin::on_tick`] invocation.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum TickSource {
131    /// Fired by SA-MP's `ProcessTick` export, on every iteration of the
132    /// server's main loop.
133    SaMp,
134    /// Fired by the SDK-owned repeating timer on native Open Multiplayer
135    /// (created via `ITimersComponent` in `on_ready`). Matches the
136    /// `omp` / `Omp*` identifier convention used elsewhere in the SDK
137    /// (`OmpComponent`, `OmpComponentHandle`, …).
138    OmpTimer,
139}
140
141/// Per-call context delivered to [`SampPlugin::on_tick`].
142#[derive(Debug, Clone, Copy)]
143pub struct TickContext {
144    /// Wall-clock time elapsed since the previous `on_tick` dispatch in
145    /// this plugin instance. `Duration::ZERO` on the very first call.
146    pub elapsed: Duration,
147    /// Which server scheduled this dispatch.
148    pub source: TickSource,
149}
150
151/// Enables [`SampPlugin::on_tick`] with default settings: tick on both
152/// servers, 5 ms interval on Open Multiplayer.
153///
154/// Call inside `initialize_plugin!`. Without this opt-in the tick stays
155/// inert — useful for purely reactive plugins that do not need the cycle.
156pub fn enable_tick() {
157    enable_tick_with(TickConfig::default());
158}
159
160/// Enables [`SampPlugin::on_tick`] with an explicit [`TickConfig`].
161///
162/// Use this form to disable the tick on one server, or to choose a
163/// different Open Multiplayer timer interval.
164///
165/// # Example
166/// ```rust,no_run
167/// # use std::time::Duration;
168/// # use samp::plugin::{enable_tick_with, TickConfig};
169/// // Tick every 50 ms on Open Multiplayer; rely on SA-MP's default cadence.
170/// enable_tick_with(TickConfig::new().omp_interval(Duration::from_millis(50)));
171/// ```
172pub fn enable_tick_with(config: TickConfig) {
173    Runtime::get().set_tick_config(config);
174}
175
176/// Installs the SDK's debug hook on `amx`, routing every executed line into
177/// [`SampPlugin::on_debug_break`]. Call from [`SampPlugin::on_amx_load`] for
178/// each AMX you want to debug (typically the gamemode).
179///
180/// The `.amx` must have been compiled with `-d2`/`-d3` for the VM to invoke the
181/// hook. To stop receiving callbacks, call [`disable_debug_hook`].
182///
183/// This is the turnkey alternative to [`Amx::install_debug_hook`]: instead of
184/// managing a raw `extern "C"` callback and global state yourself, the SDK owns
185/// a panic-guarded trampoline and dispatches into your plugin instance.
186///
187/// # Example
188/// ```rust,ignore
189/// impl SampPlugin for MyDebugger {
190///     fn on_amx_load(&mut self, amx: &Amx) {
191///         samp::plugin::enable_debug_hook(amx);
192///     }
193///     fn on_debug_break(&mut self, amx: &Amx) {
194///         let line = amx.cip();
195///         // inspect / pause / forward to a DAP client...
196///     }
197/// }
198/// ```
199pub fn enable_debug_hook(amx: &Amx) {
200    amx.install_debug_hook(debug_hook_trampoline);
201}
202
203/// Removes the SDK debug hook previously installed by [`enable_debug_hook`] on
204/// `amx`, so [`SampPlugin::on_debug_break`] stops firing for it.
205pub fn disable_debug_hook(amx: &Amx) {
206    amx.remove_debug_hook();
207}
208
209/// SDK-owned debug hook callback. The VM calls this on every source line of an
210/// AMX that opted in via [`enable_debug_hook`]. It wraps the raw `*mut AMX` and
211/// dispatches into the plugin's [`SampPlugin::on_debug_break`].
212///
213/// Crosses the FFI boundary, so it must never unwind: the dispatch is wrapped in
214/// `catch_unwind` and always returns `AMX_ERR_NONE` (0).
215extern "C" fn debug_hook_trampoline(amx: *mut samp_sdk::raw::types::AMX) -> i32 {
216    let _ = std::panic::catch_unwind(|| {
217        let Some(rt) = Runtime::try_get() else { return };
218        let wrapped = Amx::new(amx, rt.amx_exports());
219        Runtime::plugin().on_debug_break(&wrapped);
220    });
221    0 // AMX_ERR_NONE
222}
223
224/// Returns a [`fern::Dispatch`] already chained into the server's log system,
225/// disabling the SDK's default routing.
226///
227/// Lets the plugin customize format, level, sink (file, console) without
228/// giving up delivery to the server (SA-MP `logprintf` or
229/// `ICore::logLnU8`). The `log` crate level is mapped automatically to
230/// [`samp_sdk::omp::LogLevel`] in Open Multiplayer mode.
231///
232/// # Example
233/// ```rust,ignore
234/// initialize_plugin!({
235///     let _ = fern::Dispatch::new()
236///         .format(|cb, msg, rec| cb.finish(format_args!("[MyPlugin][{}]: {}", rec.level(), msg)))
237///         .level(log::LevelFilter::Info)
238///         .chain(samp::plugin::logger())
239///         .apply();
240///     MyPlugin
241/// });
242/// ```
243pub fn logger() -> fern::Dispatch {
244    let rt = Runtime::get();
245    rt.disable_default_logger();
246
247    fern::Dispatch::new().chain(fern::Output::call(|record| {
248        let rt = Runtime::get();
249        // In Open Multiplayer mode, maps log::Level → LogLevel and routes via ICore::logLn.
250        // In SA-MP mode, log_level falls back to the standard log() (logprintf has no level).
251        #[cfg(not(feature = "samp-only"))]
252        {
253            let level = match record.level() {
254                log::Level::Error => samp_sdk::omp::LogLevel::Error,
255                log::Level::Warn => samp_sdk::omp::LogLevel::Warning,
256                log::Level::Info => samp_sdk::omp::LogLevel::Message,
257                log::Level::Debug | log::Level::Trace => samp_sdk::omp::LogLevel::Debug,
258            };
259            rt.log_level(level, record.args());
260        }
261        #[cfg(feature = "samp-only")]
262        rt.log(record.args());
263    }))
264}
265
266#[doc(hidden)]
267#[must_use]
268pub fn get<T: SampPlugin + 'static>() -> NonNull<T> {
269    Runtime::plugin_cast()
270}
271
272/// Returns the Open Multiplayer server's `ICore*` pointer received in `on_load`.
273///
274/// Available only in native Open Multiplayer mode (without the `samp-only` feature).
275/// Returns `None` if the plugin was loaded via SA-MP or if `on_load` has not
276/// been called yet.
277#[cfg(not(feature = "samp-only"))]
278#[must_use]
279pub fn omp_core() -> Option<*mut samp_sdk::omp::component::ICore> {
280    crate::runtime::Runtime::get().omp_core()
281}
282
283/// Looks up an Open Multiplayer component by UID in the list received in `on_init`.
284///
285/// Returns `None` if the server has not yet called `on_init` or if the component
286/// is not registered.
287///
288/// # Example
289/// ```rust,no_run
290/// use samp::plugin::omp_query_component;
291/// use samp_sdk::omp::server::PAWN_COMPONENT_UID;
292///
293/// if let Some(_pawn) = omp_query_component(PAWN_COMPONENT_UID) {
294///     // IPawnComponent available
295/// }
296/// ```
297#[cfg(not(feature = "samp-only"))]
298#[must_use]
299pub fn omp_query_component(
300    uid: samp_sdk::omp::types::UID,
301) -> Option<*mut samp_sdk::omp::server::ServerComponent> {
302    crate::runtime::Runtime::get().omp_query_component(uid)
303}
304
305/// Looks up an Open Multiplayer component via its typed wrapper.
306///
307/// Typed version of `omp_query_component`: uses the `UID` declared in the type's
308/// `OmpComponentHandle` trait, returns a wrapper that exposes specific methods.
309///
310/// # Example
311/// ```rust,no_run
312/// use samp_sdk::omp::PawnComponent;
313///
314/// if let Some(pawn) = samp::plugin::omp_query::<PawnComponent>() {
315///     if let Some(version) = pawn.version() {
316///         println!("Pawn component: {}.{}.{}", version.major, version.minor, version.patch);
317///     }
318/// }
319/// ```
320#[cfg(not(feature = "samp-only"))]
321#[must_use]
322pub fn omp_query<T>() -> Option<T>
323where
324    T: samp_sdk::omp::OmpComponentHandle,
325{
326    let raw = omp_query_component(T::UID)?;
327    let nonnull_ptr = std::ptr::NonNull::new(raw)?;
328    Some(unsafe { T::from_raw(nonnull_ptr) })
329}
330
331/// Plugin lifecycle. All methods are optional — the trait provides empty
332/// implementations so the plugin only overrides the relevant ones.
333///
334/// Instead of implementing manually, use `#[derive(SampPlugin)]` if no
335/// method needs custom logic.
336pub trait SampPlugin {
337    /// Server has finished loading the plugin (`Load()` on SA-MP /
338    /// `onLoad(ICore*)` on Open Multiplayer). Good moment to initialize state.
339    fn on_load(&mut self) {}
340
341    /// Server is unloading the plugin. Release external resources here.
342    fn on_unload(&mut self) {}
343
344    /// A Pawn script (`.amx`) was loaded. On SA-MP it is called by the
345    /// `AmxLoad` export; on Open Multiplayer by `IEventDispatcher<PawnEventHandler>`.
346    fn on_amx_load(&mut self, amx: &Amx) {
347        let _ = amx;
348    }
349
350    /// A Pawn script is being unloaded. Clean per-AMX state here.
351    fn on_amx_unload(&mut self, amx: &Amx) {
352        let _ = amx;
353    }
354
355    /// The VM's debug hook fired on a source line. Only called for AMXs the
356    /// plugin opted in via [`enable_debug_hook`], and only when the `.amx` was
357    /// compiled with `-d2`/`-d3`.
358    ///
359    /// This runs on the VM thread, synchronously, on every executed line — keep
360    /// it cheap, and block here (e.g. waiting for a debugger client) only if you
361    /// intend to freeze the server. Use the VM accessors on [`Amx`]
362    /// (`cip`, `frame`, `read_cell`/`write_cell`) to read the paused state, and
363    /// pair them with `samp::debug` (feature `debug`) to map addresses to source
364    /// lines and symbols.
365    fn on_debug_break(&mut self, amx: &Amx) {
366        let _ = amx;
367    }
368
369    /// Periodic callback. Fires only when the plugin opted in via
370    /// [`enable_tick`] (or [`enable_tick_with`]).
371    ///
372    /// The two servers schedule this differently:
373    /// - **SA-MP**: the server invokes the `ProcessTick` export on every
374    ///   iteration of its main loop. The cadence is whatever the server is
375    ///   configured for — the SDK has no control over it.
376    /// - **native Open Multiplayer**: there is no native equivalent of
377    ///   `ProcessTick` for components. The SDK installs a repeating timer
378    ///   on the server's `ITimersComponent` in `on_ready` and dispatches
379    ///   its timeout here. The interval is whatever [`TickConfig::omp_interval`]
380    ///   was set to (default: 5 ms).
381    ///
382    /// `ctx.source` tells which server scheduled the call; `ctx.elapsed`
383    /// is the wall-clock time since the previous dispatch (zero on the
384    /// first call).
385    fn on_tick(&mut self, ctx: TickContext) {
386        let _ = ctx;
387    }
388
389    /// Called when all Open Multiplayer components have finished initializing.
390    ///
391    /// This is the safe moment to interact with other server components,
392    /// since all of them have already gone through their `on_init`.
393    ///
394    /// Available only in native Open Multiplayer mode (without the `samp-only` feature).
395    #[cfg(not(feature = "samp-only"))]
396    fn on_omp_ready(&mut self) {}
397
398    /// Called when any Open Multiplayer component is being unloaded.
399    ///
400    /// Use together with `samp::plugin::omp_query_component()` to check
401    /// which components are still available after the notification.
402    ///
403    /// Available only in native Open Multiplayer mode (without the `samp-only` feature).
404    #[cfg(not(feature = "samp-only"))]
405    fn on_component_free(&mut self) {}
406}
407
408#[doc(hidden)]
409pub fn convert_return_value<T: AmxCell<'static>>(value: T) -> i32 {
410    value.as_cell()
411}