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}