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}