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}