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/// Returns a [`fern::Dispatch`] already chained into the server's log system,
177/// disabling the SDK's default routing.
178///
179/// Lets the plugin customize format, level, sink (file, console) without
180/// giving up delivery to the server (SA-MP `logprintf` or
181/// `ICore::logLnU8`). The `log` crate level is mapped automatically to
182/// [`samp_sdk::omp::LogLevel`] in Open Multiplayer mode.
183///
184/// # Example
185/// ```rust,ignore
186/// initialize_plugin!({
187///     let _ = fern::Dispatch::new()
188///         .format(|cb, msg, rec| cb.finish(format_args!("[MyPlugin][{}]: {}", rec.level(), msg)))
189///         .level(log::LevelFilter::Info)
190///         .chain(samp::plugin::logger())
191///         .apply();
192///     MyPlugin
193/// });
194/// ```
195pub fn logger() -> fern::Dispatch {
196    let rt = Runtime::get();
197    rt.disable_default_logger();
198
199    fern::Dispatch::new().chain(fern::Output::call(|record| {
200        let rt = Runtime::get();
201        // In Open Multiplayer mode, maps log::Level → LogLevel and routes via ICore::logLn.
202        // In SA-MP mode, log_level falls back to the standard log() (logprintf has no level).
203        #[cfg(not(feature = "samp-only"))]
204        {
205            let level = match record.level() {
206                log::Level::Error => samp_sdk::omp::LogLevel::Error,
207                log::Level::Warn => samp_sdk::omp::LogLevel::Warning,
208                log::Level::Info => samp_sdk::omp::LogLevel::Message,
209                log::Level::Debug | log::Level::Trace => samp_sdk::omp::LogLevel::Debug,
210            };
211            rt.log_level(level, record.args());
212        }
213        #[cfg(feature = "samp-only")]
214        rt.log(record.args());
215    }))
216}
217
218#[doc(hidden)]
219#[must_use]
220pub fn get<T: SampPlugin + 'static>() -> NonNull<T> {
221    Runtime::plugin_cast()
222}
223
224/// Returns the Open Multiplayer server's `ICore*` pointer received in `on_load`.
225///
226/// Available only in native Open Multiplayer mode (without the `samp-only` feature).
227/// Returns `None` if the plugin was loaded via SA-MP or if `on_load` has not
228/// been called yet.
229#[cfg(not(feature = "samp-only"))]
230#[must_use]
231pub fn omp_core() -> Option<*mut samp_sdk::omp::component::ICore> {
232    crate::runtime::Runtime::get().omp_core()
233}
234
235/// Looks up an Open Multiplayer component by UID in the list received in `on_init`.
236///
237/// Returns `None` if the server has not yet called `on_init` or if the component
238/// is not registered.
239///
240/// # Example
241/// ```rust,no_run
242/// use samp::plugin::omp_query_component;
243/// use samp_sdk::omp::server::PAWN_COMPONENT_UID;
244///
245/// if let Some(_pawn) = omp_query_component(PAWN_COMPONENT_UID) {
246///     // IPawnComponent available
247/// }
248/// ```
249#[cfg(not(feature = "samp-only"))]
250#[must_use]
251pub fn omp_query_component(
252    uid: samp_sdk::omp::types::UID,
253) -> Option<*mut samp_sdk::omp::server::ServerComponent> {
254    crate::runtime::Runtime::get().omp_query_component(uid)
255}
256
257/// Looks up an Open Multiplayer component via its typed wrapper.
258///
259/// Typed version of `omp_query_component`: uses the `UID` declared in the type's
260/// `OmpComponentHandle` trait, returns a wrapper that exposes specific methods.
261///
262/// # Example
263/// ```rust,no_run
264/// use samp_sdk::omp::PawnComponent;
265///
266/// if let Some(pawn) = samp::plugin::omp_query::<PawnComponent>() {
267///     if let Some(version) = pawn.version() {
268///         println!("Pawn component: {}.{}.{}", version.major, version.minor, version.patch);
269///     }
270/// }
271/// ```
272#[cfg(not(feature = "samp-only"))]
273#[must_use]
274pub fn omp_query<T>() -> Option<T>
275where
276    T: samp_sdk::omp::OmpComponentHandle,
277{
278    let raw = omp_query_component(T::UID)?;
279    let nonnull_ptr = std::ptr::NonNull::new(raw)?;
280    Some(unsafe { T::from_raw(nonnull_ptr) })
281}
282
283/// Plugin lifecycle. All methods are optional — the trait provides empty
284/// implementations so the plugin only overrides the relevant ones.
285///
286/// Instead of implementing manually, use `#[derive(SampPlugin)]` if no
287/// method needs custom logic.
288pub trait SampPlugin {
289    /// Server has finished loading the plugin (`Load()` on SA-MP /
290    /// `onLoad(ICore*)` on Open Multiplayer). Good moment to initialize state.
291    fn on_load(&mut self) {}
292
293    /// Server is unloading the plugin. Release external resources here.
294    fn on_unload(&mut self) {}
295
296    /// A Pawn script (`.amx`) was loaded. On SA-MP it is called by the
297    /// `AmxLoad` export; on Open Multiplayer by `IEventDispatcher<PawnEventHandler>`.
298    fn on_amx_load(&mut self, amx: &Amx) {
299        let _ = amx;
300    }
301
302    /// A Pawn script is being unloaded. Clean per-AMX state here.
303    fn on_amx_unload(&mut self, amx: &Amx) {
304        let _ = amx;
305    }
306
307    /// Periodic callback. Fires only when the plugin opted in via
308    /// [`enable_tick`] (or [`enable_tick_with`]).
309    ///
310    /// The two servers schedule this differently:
311    /// - **SA-MP**: the server invokes the `ProcessTick` export on every
312    ///   iteration of its main loop. The cadence is whatever the server is
313    ///   configured for — the SDK has no control over it.
314    /// - **native Open Multiplayer**: there is no native equivalent of
315    ///   `ProcessTick` for components. The SDK installs a repeating timer
316    ///   on the server's `ITimersComponent` in `on_ready` and dispatches
317    ///   its timeout here. The interval is whatever [`TickConfig::omp_interval`]
318    ///   was set to (default: 5 ms).
319    ///
320    /// `ctx.source` tells which server scheduled the call; `ctx.elapsed`
321    /// is the wall-clock time since the previous dispatch (zero on the
322    /// first call).
323    fn on_tick(&mut self, ctx: TickContext) {
324        let _ = ctx;
325    }
326
327    /// Called when all Open Multiplayer components have finished initializing.
328    ///
329    /// This is the safe moment to interact with other server components,
330    /// since all of them have already gone through their `on_init`.
331    ///
332    /// Available only in native Open Multiplayer mode (without the `samp-only` feature).
333    #[cfg(not(feature = "samp-only"))]
334    fn on_omp_ready(&mut self) {}
335
336    /// Called when any Open Multiplayer component is being unloaded.
337    ///
338    /// Use together with `samp::plugin::omp_query_component()` to check
339    /// which components are still available after the notification.
340    ///
341    /// Available only in native Open Multiplayer mode (without the `samp-only` feature).
342    #[cfg(not(feature = "samp-only"))]
343    fn on_component_free(&mut self) {}
344}
345
346#[doc(hidden)]
347pub fn convert_return_value<T: AmxCell<'static>>(value: T) -> i32 {
348    value.as_cell()
349}