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::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
6use std::time::Duration;
7
8use samp_sdk::amx::Amx;
9use samp_sdk::cell::AmxCell;
10
11use crate::macros::sdk_warn;
12use crate::runtime::Runtime;
13
14#[doc(hidden)]
15pub fn initialize<F, T>(constructor: F)
16where
17    F: FnOnce() -> T + 'static,
18    T: SampPlugin + 'static,
19{
20    let rt = Runtime::initialize();
21    let plugin = constructor();
22
23    rt.set_plugin(plugin);
24    rt.post_initialize();
25}
26
27/// Tells the SDK how often [`SampPlugin::on_tick`] should fire on each
28/// server.
29///
30/// The two servers schedule periodic callbacks differently:
31///
32/// - **SA-MP** exports `ProcessTick`. The server's main loop invokes it on
33///   every iteration — the cadence is whatever the server is configured for.
34///   The SDK has no say over the interval; the [`sa_mp`] flag only decides
35///   whether the export is advertised at all.
36/// - **native Open Multiplayer** has no built-in `ProcessTick` equivalent.
37///   The SDK installs a repeating timer on the server's `ITimersComponent`
38///   in `on_ready` and dispatches the timeout into [`on_tick`]. The
39///   interval is the [`omp_interval`] field.
40///
41/// [`sa_mp`]: TickConfig::sa_mp
42/// [`omp_interval`]: TickConfig::omp_interval
43/// [`on_tick`]: SampPlugin::on_tick
44#[derive(Debug, Clone, Copy)]
45pub struct TickConfig {
46    /// Enable the tick on SA-MP. When `false`, the plugin does not advertise
47    /// `Supports::PROCESS_TICK` and the export becomes inert.
48    pub sa_mp: bool,
49    /// Enable the tick on native Open Multiplayer. When `false`, the SDK
50    /// does not create the `ITimersComponent` timer in `on_ready`.
51    pub omp: bool,
52    /// Interval the SDK uses when creating the Open Multiplayer timer.
53    /// Ignored when [`omp`] is `false`. Ignored entirely on SA-MP (the
54    /// server controls the cadence).
55    ///
56    /// [`omp`]: TickConfig::omp
57    pub omp_interval: Duration,
58}
59
60impl Default for TickConfig {
61    /// Default: enabled on both servers, 5 ms timer on Open Multiplayer.
62    fn default() -> Self {
63        Self {
64            sa_mp: true,
65            omp: true,
66            omp_interval: Duration::from_millis(5),
67        }
68    }
69}
70
71impl TickConfig {
72    /// Equivalent to `TickConfig::default()`.
73    #[must_use]
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Builder: sets [`sa_mp`].
79    ///
80    /// [`sa_mp`]: TickConfig::sa_mp
81    #[must_use]
82    pub fn sa_mp(mut self, enabled: bool) -> Self {
83        self.sa_mp = enabled;
84        self
85    }
86
87    /// Builder: sets [`omp`].
88    ///
89    /// [`omp`]: TickConfig::omp
90    #[must_use]
91    pub fn omp(mut self, enabled: bool) -> Self {
92        self.omp = enabled;
93        self
94    }
95
96    /// Builder: sets [`omp_interval`].
97    ///
98    /// [`omp_interval`]: TickConfig::omp_interval
99    #[must_use]
100    pub fn omp_interval(mut self, interval: Duration) -> Self {
101        self.omp_interval = interval;
102        self
103    }
104
105    /// Shortcut: tick only on SA-MP. Equivalent to
106    /// `TickConfig::new().omp(false)`.
107    ///
108    /// Use when the plugin has no meaningful work to do on the Open
109    /// Multiplayer tick — for example, a pure SA-MP plugin running in
110    /// legacy mode under Open Multiplayer.
111    #[must_use]
112    pub fn sa_mp_only() -> Self {
113        Self::default().omp(false)
114    }
115
116    /// Shortcut: tick only on native Open Multiplayer, at the supplied
117    /// interval. Equivalent to
118    /// `TickConfig::new().sa_mp(false).omp_interval(interval)`.
119    ///
120    /// Use when the plugin needs a controlled cadence specifically on
121    /// Open Multiplayer and should stay silent on SA-MP — for example,
122    /// a component that drives a long-poll loop only meaningful when
123    /// the component API is reachable.
124    #[must_use]
125    pub fn omp_only(interval: Duration) -> Self {
126        Self::default().sa_mp(false).omp_interval(interval)
127    }
128}
129
130/// Origin of the current [`SampPlugin::on_tick`] invocation.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum TickSource {
133    /// Fired by SA-MP's `ProcessTick` export, on every iteration of the
134    /// server's main loop.
135    SaMp,
136    /// Fired by the SDK-owned repeating timer on native Open Multiplayer
137    /// (created via `ITimersComponent` in `on_ready`). Matches the
138    /// `omp` / `Omp*` identifier convention used elsewhere in the SDK
139    /// (`OmpComponent`, `OmpComponentHandle`, …).
140    OmpTimer,
141}
142
143/// Per-call context delivered to [`SampPlugin::on_tick`].
144#[derive(Debug, Clone, Copy)]
145pub struct TickContext {
146    /// Wall-clock time elapsed since the previous `on_tick` dispatch in
147    /// this plugin instance. `Duration::ZERO` on the very first call.
148    pub elapsed: Duration,
149    /// Which server scheduled this dispatch.
150    pub source: TickSource,
151}
152
153/// Enables [`SampPlugin::on_tick`] with default settings: tick on both
154/// servers, 5 ms interval on Open Multiplayer.
155///
156/// Call inside `initialize_plugin!`. Without this opt-in the tick stays
157/// inert — useful for purely reactive plugins that do not need the cycle.
158pub fn enable_tick() {
159    enable_tick_with(TickConfig::default());
160}
161
162/// Enables [`SampPlugin::on_tick`] with an explicit [`TickConfig`].
163///
164/// Use this form to disable the tick on one server, or to choose a
165/// different Open Multiplayer timer interval.
166///
167/// # Example
168/// ```rust,no_run
169/// # use std::time::Duration;
170/// # use samp::plugin::{enable_tick_with, TickConfig};
171/// // Tick every 50 ms on Open Multiplayer; rely on SA-MP's default cadence.
172/// enable_tick_with(TickConfig::new().omp_interval(Duration::from_millis(50)));
173/// ```
174pub fn enable_tick_with(config: TickConfig) {
175    Runtime::get().set_tick_config(config);
176}
177
178/// Installs the SDK's debug hook on `amx`, routing every executed line into
179/// [`SampPlugin::on_debug_break`]. Call from [`SampPlugin::on_amx_load`] for
180/// each AMX you want to debug (typically the gamemode).
181///
182/// The `.amx` must have been compiled with `-d2`/`-d3` for the VM to invoke the
183/// hook. To stop receiving callbacks, call [`disable_debug_hook`].
184///
185/// This is the turnkey alternative to [`Amx::install_debug_hook`]: instead of
186/// managing a raw `extern "C"` callback and global state yourself, the SDK owns
187/// a panic-guarded trampoline and dispatches into your plugin instance.
188///
189/// # Example
190/// ```rust,ignore
191/// impl SampPlugin for MyDebugger {
192///     fn on_amx_load(&mut self, amx: &Amx) {
193///         samp::plugin::enable_debug_hook(amx);
194///     }
195///     fn on_debug_break(&mut self, amx: &Amx) {
196///         let line = amx.cip();
197///         // inspect / pause / forward to a DAP client...
198///     }
199/// }
200/// ```
201pub fn enable_debug_hook(amx: &Amx) {
202    amx.install_debug_hook(debug_hook_trampoline);
203}
204
205/// Removes the SDK debug hook previously installed by [`enable_debug_hook`] on
206/// `amx`, so [`SampPlugin::on_debug_break`] stops firing for it.
207pub fn disable_debug_hook(amx: &Amx) {
208    amx.remove_debug_hook();
209}
210
211/// SDK-owned debug hook callback. The VM calls this on every source line of an
212/// AMX that opted in via [`enable_debug_hook`]. It wraps the raw `*mut AMX` and
213/// dispatches into the plugin's [`SampPlugin::on_debug_break`].
214///
215/// Crosses the FFI boundary, so it must never unwind: the dispatch is wrapped in
216/// `catch_unwind` and always returns `AMX_ERR_NONE` (0).
217extern "C" fn debug_hook_trampoline(amx: *mut samp_sdk::raw::types::AMX) -> i32 {
218    let _ = crate::panic_guard::catch(|| {
219        let Some(rt) = Runtime::try_get() else { return };
220        let wrapped = Amx::new(amx, rt.amx_exports());
221        Runtime::plugin().on_debug_break(&wrapped);
222    });
223    0 // AMX_ERR_NONE
224}
225
226/// Returns a [`fern::Dispatch`] already chained into the server's log system,
227/// disabling the SDK's default routing.
228///
229/// Lets the plugin customize format, level, sink (file, console) without
230/// giving up delivery to the server (SA-MP `logprintf` or
231/// `ICore::logLnU8`). The `log` crate level is mapped automatically to
232/// [`samp_sdk::omp::LogLevel`] in Open Multiplayer mode.
233///
234/// # Example
235/// ```rust,ignore
236/// initialize_plugin!({
237///     let _ = fern::Dispatch::new()
238///         .format(|cb, msg, rec| cb.finish(format_args!("[MyPlugin][{}]: {}", rec.level(), msg)))
239///         .level(log::LevelFilter::Info)
240///         .chain(samp::plugin::logger())
241///         .apply();
242///     MyPlugin
243/// });
244/// ```
245pub fn logger() -> fern::Dispatch {
246    let rt = Runtime::get();
247    rt.disable_default_logger();
248
249    // Through the logger's queue: a line logged on another thread reaches the
250    // server on the main thread, since neither server's log is thread-safe.
251    fern::Dispatch::new().chain(fern::Output::call(|record| {
252        crate::logger::to_server(record.level(), record.args().to_string());
253    }))
254}
255
256#[doc(hidden)]
257#[must_use]
258pub fn get<T: SampPlugin + 'static>() -> NonNull<T> {
259    Runtime::plugin_cast()
260}
261
262/// What `#[native]` uses to reach the plugin: `None` instead of a panic when
263/// there is no plugin yet, since a native can be called by another plugin
264/// outside the order the server keeps.
265#[doc(hidden)]
266#[must_use]
267pub fn try_get<T: SampPlugin + 'static>() -> Option<NonNull<T>> {
268    Runtime::try_plugin_cast()
269}
270
271// ---------------------------------------------------------------------------
272// Reaching the plugin from outside a native
273// ---------------------------------------------------------------------------
274
275/// Set while a [`with_instance`] closure runs.
276static PLUGIN_BORROWED: AtomicBool = AtomicBool::new(false);
277
278/// Depth of native calls in progress. A native already holds `&mut self`, so
279/// handing out another borrow underneath one would alias it.
280///
281/// Natives run on the main thread only, so this is the only thread that writes
282/// it: a load and a store do, without the locked read-modify-write a
283/// `fetch_add` costs on every native call.
284static NATIVE_DEPTH: AtomicUsize = AtomicUsize::new(0);
285
286/// Marks a native call for the duration of its frame.
287///
288/// Generated by `#[native]`; not part of the stable surface.
289#[doc(hidden)]
290pub struct NativeFrame;
291
292impl NativeFrame {
293    /// Enters a native frame, warning once if a `with_instance` borrow is alive.
294    ///
295    /// That combination means a main-thread job called into Pawn and the script
296    /// called back into this plugin, so the native's `&mut self` aliases the
297    /// job's `&mut T`. The SDK cannot refuse — the native must still answer the
298    /// script — so it names the problem instead of leaving it silent.
299    #[must_use]
300    pub fn enter() -> Self {
301        if PLUGIN_BORROWED.load(Ordering::Acquire) {
302            static WARNED: AtomicBool = AtomicBool::new(false);
303            if !WARNED.swap(true, Ordering::Relaxed) {
304                sdk_warn!(
305                    "a native ran while a main-thread job held the plugin: the two \
306                     `&mut` borrows alias. Call into Pawn after the job's closure \
307                     returns — collect what to send, end the closure, then send it."
308                );
309            }
310        }
311        let depth = NATIVE_DEPTH.load(Ordering::Acquire);
312        NATIVE_DEPTH.store(depth + 1, Ordering::Release);
313        Self
314    }
315}
316
317impl Drop for NativeFrame {
318    fn drop(&mut self) {
319        let depth = NATIVE_DEPTH.load(Ordering::Acquire);
320        NATIVE_DEPTH.store(depth.saturating_sub(1), Ordering::Release);
321    }
322}
323
324/// Runs `f` with the plugin instance, on the main thread.
325///
326/// This is the general way to reach plugin state from code the plugin does not
327/// own the call site of — a main-thread job, a callback helper, a timer — where
328/// there is no `&mut self` in scope.
329///
330/// ```rust,no_run
331/// # use samp::prelude::*;
332/// # struct MyPlugin { sent: u32 }
333/// # impl SampPlugin for MyPlugin {}
334/// let sent = samp::plugin::with_instance::<MyPlugin, _>(|plugin| {
335///     plugin.sent += 1;
336///     plugin.sent
337/// });
338/// ```
339///
340/// Returns `None`, having logged why, when the plugin cannot be lent out:
341///
342/// - `T` is not the type the plugin was created as;
343/// - the plugin does not exist yet, or no longer does;
344/// - a borrow is already alive — a nested `with_instance`, or a call from
345///   inside a native, which already holds `&mut self`. Use that `self`.
346///
347/// Must be called from the main thread. There is nothing to synchronize with
348/// off it: the plugin is not `Sync`, and the server touches it from one thread.
349/// Work arriving on another thread belongs in [`crate::mainthread::post_with`],
350/// which calls through here at the right moment.
351pub fn with_instance<T, R>(f: impl FnOnce(&mut T) -> R) -> Option<R>
352where
353    T: SampPlugin + 'static,
354{
355    if NATIVE_DEPTH.load(Ordering::Acquire) > 0 {
356        sdk_warn!(
357            "with_instance() called from inside a native, which already holds \
358             `&mut self` — use that instead; nothing was run"
359        );
360        return None;
361    }
362    if PLUGIN_BORROWED.swap(true, Ordering::AcqRel) {
363        sdk_warn!("with_instance() called while the plugin was already borrowed; nothing was run");
364        return None;
365    }
366
367    // Released on every path, including a panic in `f`, so one bad job does not
368    // lock the plugin away for the rest of the process.
369    struct Release;
370    impl Drop for Release {
371        fn drop(&mut self) {
372            PLUGIN_BORROWED.store(false, Ordering::Release);
373        }
374    }
375    let _release = Release;
376
377    let Some(plugin) = Runtime::plugin_as::<T>() else {
378        if Runtime::plugin_is_set() {
379            sdk_warn!(
380                "with_instance::<{}>() does not name the plugin's type; nothing was run",
381                std::any::type_name::<T>()
382            );
383        } else {
384            sdk_warn!("with_instance() called before the plugin was created; nothing was run");
385        }
386        return None;
387    };
388
389    Some(f(plugin))
390}
391
392/// Whether a [`with_instance`] borrow is alive right now.
393///
394/// Exposed for a plugin deciding whether to reach for state or defer.
395#[must_use]
396pub fn is_borrowed() -> bool {
397    PLUGIN_BORROWED.load(Ordering::Acquire)
398}
399
400/// Returns the Open Multiplayer server's `ICore*` pointer received in `on_load`.
401///
402/// Available only in native Open Multiplayer mode (without the `samp-only` feature).
403/// Returns `None` if the plugin was loaded via SA-MP or if `on_load` has not
404/// been called yet.
405#[cfg(not(feature = "samp-only"))]
406#[must_use]
407pub fn omp_core() -> Option<*mut samp_sdk::omp::component::ICore> {
408    crate::runtime::Runtime::get().omp_core()
409}
410
411/// Looks up an Open Multiplayer component by UID in the list received in `on_init`.
412///
413/// Returns `None` if the server has not yet called `on_init` or if the component
414/// is not registered.
415///
416/// # Example
417/// ```rust,no_run
418/// use samp::plugin::omp_query_component;
419/// use samp_sdk::omp::server::PAWN_COMPONENT_UID;
420///
421/// if let Some(_pawn) = omp_query_component(PAWN_COMPONENT_UID) {
422///     // IPawnComponent available
423/// }
424/// ```
425#[cfg(not(feature = "samp-only"))]
426#[must_use]
427pub fn omp_query_component(
428    uid: samp_sdk::omp::types::UID,
429) -> Option<*mut samp_sdk::omp::server::ServerComponent> {
430    crate::runtime::Runtime::get().omp_query_component(uid)
431}
432
433/// Looks up an Open Multiplayer component via its typed wrapper.
434///
435/// Typed version of `omp_query_component`: uses the `UID` declared in the type's
436/// `OmpComponentHandle` trait, returns a wrapper that exposes specific methods.
437///
438/// # Example
439/// ```rust,no_run
440/// use samp_sdk::omp::PawnComponent;
441///
442/// if let Some(pawn) = samp::plugin::omp_query::<PawnComponent>() {
443///     if let Some(version) = pawn.version() {
444///         println!("Pawn component: {}.{}.{}", version.major, version.minor, version.patch);
445///     }
446/// }
447/// ```
448#[cfg(not(feature = "samp-only"))]
449#[must_use]
450pub fn omp_query<T>() -> Option<T>
451where
452    T: samp_sdk::omp::OmpComponentHandle,
453{
454    let raw = omp_query_component(T::UID)?;
455    let nonnull_ptr = std::ptr::NonNull::new(raw)?;
456    Some(unsafe { T::from_raw(nonnull_ptr) })
457}
458
459/// Plugin lifecycle. All methods are optional — the trait provides empty
460/// implementations so the plugin only overrides the relevant ones.
461///
462/// Instead of implementing manually, use `#[derive(SampPlugin)]` if no
463/// method needs custom logic.
464pub trait SampPlugin {
465    /// Server has finished loading the plugin (`Load()` on SA-MP /
466    /// `onLoad(ICore*)` on Open Multiplayer). Good moment to initialize state.
467    fn on_load(&mut self) {}
468
469    /// Server is unloading the plugin. Release external resources here.
470    fn on_unload(&mut self) {}
471
472    /// A Pawn script (`.amx`) was loaded. On SA-MP it is called by the
473    /// `AmxLoad` export; on Open Multiplayer by `IEventDispatcher<PawnEventHandler>`.
474    fn on_amx_load(&mut self, amx: &Amx) {
475        let _ = amx;
476    }
477
478    /// A Pawn script is being unloaded. Clean per-AMX state here.
479    fn on_amx_unload(&mut self, amx: &Amx) {
480        let _ = amx;
481    }
482
483    /// The VM's debug hook fired on a source line. Only called for AMXs the
484    /// plugin opted in via [`enable_debug_hook`], and only when the `.amx` was
485    /// compiled with `-d2`/`-d3`.
486    ///
487    /// This runs on the VM thread, synchronously, on every executed line — keep
488    /// it cheap, and block here (e.g. waiting for a debugger client) only if you
489    /// intend to freeze the server. Use the VM accessors on [`Amx`]
490    /// (`cip`, `frame`, `read_cell`/`write_cell`) to read the paused state, and
491    /// pair them with `samp::debug` (feature `debug`) to map addresses to source
492    /// lines and symbols.
493    fn on_debug_break(&mut self, amx: &Amx) {
494        let _ = amx;
495    }
496
497    /// Periodic callback. Fires only when the plugin opted in via
498    /// [`enable_tick`] (or [`enable_tick_with`]).
499    ///
500    /// The two servers schedule this differently:
501    /// - **SA-MP**: the server invokes the `ProcessTick` export on every
502    ///   iteration of its main loop. The cadence is whatever the server is
503    ///   configured for — the SDK has no control over it.
504    /// - **native Open Multiplayer**: there is no native equivalent of
505    ///   `ProcessTick` for components. The SDK installs a repeating timer
506    ///   on the server's `ITimersComponent` in `on_ready` and dispatches
507    ///   its timeout here. The interval is whatever [`TickConfig::omp_interval`]
508    ///   was set to (default: 5 ms).
509    ///
510    /// `ctx.source` tells which server scheduled the call; `ctx.elapsed`
511    /// is the wall-clock time since the previous dispatch (zero on the
512    /// first call).
513    fn on_tick(&mut self, ctx: TickContext) {
514        let _ = ctx;
515    }
516
517    /// Called when all Open Multiplayer components have finished initializing.
518    ///
519    /// This is the safe moment to interact with other server components,
520    /// since all of them have already gone through their `on_init`.
521    ///
522    /// Available only in native Open Multiplayer mode (without the `samp-only` feature).
523    #[cfg(not(feature = "samp-only"))]
524    fn on_omp_ready(&mut self) {}
525
526    /// Called when any Open Multiplayer component is being unloaded.
527    ///
528    /// Use together with `samp::plugin::omp_query_component()` to check
529    /// which components are still available after the notification.
530    ///
531    /// Available only in native Open Multiplayer mode (without the `samp-only` feature).
532    #[cfg(not(feature = "samp-only"))]
533    fn on_component_free(&mut self) {}
534}
535
536#[doc(hidden)]
537pub fn convert_return_value<T: AmxCell<'static>>(value: T) -> i32 {
538    value.as_cell()
539}
540
541// ---------------------------------------------------------------------------
542// Pawn include generation
543// ---------------------------------------------------------------------------
544
545/// Pawn `.inc` source for every native this plugin registers.
546///
547/// The declarations are derived by `#[native]` from the Rust signatures, so the
548/// include cannot drift from the code: rename an argument or change a type and
549/// the next build produces the new line.
550///
551/// Types map as follows — `f32` to `Float:`, `bool` to `bool:`, `AmxString` to
552/// `const name[]`, `Ref<T>` to `&name` keeping `T`'s tag, and buffers to
553/// `name[]`. A type the macro does not recognize becomes a plain cell, which is
554/// what the AMX passes anyway; only the Pawn tag is lost. Natives declared
555/// `raw` come back commented out, since their arity is not in the signature.
556///
557/// Returns an empty include (header only) before the plugin is loaded.
558#[must_use]
559pub fn pawn_include() -> String {
560    let rt = Runtime::get();
561    render_include(rt.plugin_name(), rt.native_decls())
562}
563
564/// The Pawn declaration of every registered native, as `#[native]` derived it.
565///
566/// A `raw` native appears commented out, its arity not being in the signature.
567pub(crate) fn native_decls() -> Vec<&'static str> {
568    Runtime::get().native_decls().to_vec()
569}
570
571/// Renders the include from a name and a list of declarations.
572///
573/// Public through [`crate::pawn_include::render`], which is where it is
574/// documented; kept here next to the rest of the include plumbing.
575pub(crate) fn render_include(name: &str, decls: &[&str]) -> String {
576    // A crate name may carry characters Pawn does not accept in an identifier
577    // (`email-samp`), so the include guard uses a sanitized form.
578    let guard: String = name
579        .chars()
580        .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
581        .collect();
582    let mut out = format!(
583        "// Generated by rust-samp from the #[native] signatures of {name}.\n\
584         // Edits are lost on the next build — change the Rust side instead.\n\n\
585         #if defined _{guard}_included\n    #endinput\n#endif\n#define _{guard}_included\n\n"
586    );
587    for decl in decls {
588        out.push_str(decl);
589        out.push('\n');
590    }
591    out
592}
593
594/// Writes [`pawn_include`] to `path`, creating or truncating the file.
595///
596/// # Errors
597/// Returns the underlying [`std::io::Error`] when the path cannot be written.
598pub fn write_pawn_include(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
599    std::fs::write(path, pawn_include())
600}
601
602/// Writes the include when `SAMP_PAWN_INCLUDE` holds a path.
603///
604/// Called by the SDK once the natives are known, so a plugin author gets the
605/// `.inc` by starting the server once with the variable set, on either server
606/// and either mode. A failure is logged and otherwise ignored: producing a
607/// development artifact must never take the server down.
608pub(crate) fn write_pawn_include_if_requested() {
609    let Some(path) = crate::pawn_include::path_for_plugin("SAMP_PAWN_INCLUDE") else {
610        return;
611    };
612
613    // With a template, the include is that template with the declarations
614    // filled in; without one, it is the plain generated file.
615    if let Some(template) = crate::pawn_include::path_for_plugin("SAMP_PAWN_INCLUDE_TEMPLATE") {
616        crate::pawn_include::write_from_template(&template, &path);
617        return;
618    }
619
620    match write_pawn_include(&path) {
621        Ok(()) => crate::macros::sdk_info!("Pawn include written to {}", path.to_string_lossy()),
622        Err(e) => crate::macros::sdk_warn!(
623            "could not write the Pawn include to {}: {e}",
624            path.to_string_lossy()
625        ),
626    }
627}
628
629#[cfg(test)]
630mod pawn_include_tests {
631    use super::render_include;
632
633    #[test]
634    fn header_names_the_plugin_and_declarations_follow() {
635        let out = render_include("counter", &["native Counter_Get(&out);"]);
636        assert!(out.contains("signatures of counter"));
637        assert!(out.ends_with("native Counter_Get(&out);\n"));
638    }
639
640    #[test]
641    fn include_guard_drops_characters_pawn_rejects() {
642        // `email-samp` would produce `_email-samp_included`, which the Pawn
643        // preprocessor reads as a subtraction.
644        let out = render_include("email-samp", &[]);
645        assert!(out.contains("#define _email_samp_included"));
646        assert!(!out.contains("email-samp_included"));
647    }
648
649    #[test]
650    fn a_plugin_without_natives_still_produces_a_valid_include() {
651        let out = render_include("hello", &[]);
652        assert!(out.contains("#if defined _hello_included"));
653    }
654}
655
656#[cfg(test)]
657mod instance_tests {
658    use super::*;
659    use crate::test_support::{TestPlugin, exclusive, value};
660
661    /// A second plugin type, never installed: naming it must reach nothing.
662    struct Other;
663    impl SampPlugin for Other {}
664
665    #[test]
666    fn the_closure_reaches_the_plugin_and_returns_its_value() {
667        let _g = exclusive();
668
669        let before = value();
670        let after = with_instance::<TestPlugin, _>(|p| {
671            p.value += 7;
672            p.value
673        })
674        .unwrap();
675
676        assert_eq!(after, before + 7);
677        assert_eq!(value(), after);
678    }
679
680    #[test]
681    fn naming_the_wrong_type_runs_nothing() {
682        let _g = exclusive();
683
684        let mut ran = false;
685        let out = with_instance::<Other, _>(|_| {
686            ran = true;
687        });
688
689        assert!(out.is_none());
690        assert!(
691            !ran,
692            "the plugin's bytes must not be reinterpreted as another type"
693        );
694    }
695
696    #[test]
697    fn a_nested_borrow_is_refused_rather_than_aliased() {
698        let _g = exclusive();
699
700        let inner = with_instance::<TestPlugin, _>(|_outer| {
701            assert!(is_borrowed());
702            with_instance::<TestPlugin, _>(|_| "ran anyway")
703        })
704        .unwrap();
705
706        assert_eq!(inner, None);
707        assert!(!is_borrowed(), "the borrow is released on the way out");
708    }
709
710    #[test]
711    fn a_native_frame_holds_the_borrow_off() {
712        let _g = exclusive();
713
714        let frame = NativeFrame::enter();
715        assert!(
716            with_instance::<TestPlugin, _>(|p| p.value).is_none(),
717            "a native already holds `&mut self`"
718        );
719        drop(frame);
720
721        assert!(with_instance::<TestPlugin, _>(|p| p.value).is_some());
722    }
723
724    #[test]
725    fn a_panic_inside_the_closure_does_not_lock_the_plugin_away() {
726        let _g = exclusive();
727
728        let panicked = std::panic::catch_unwind(|| {
729            with_instance::<TestPlugin, _>(|_| panic!("closure blew up"));
730        });
731
732        assert!(panicked.is_err());
733        assert!(!is_borrowed());
734        assert!(with_instance::<TestPlugin, _>(|p| p.value).is_some());
735    }
736}