use std::ptr::NonNull;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use samp_sdk::amx::Amx;
use samp_sdk::cell::AmxCell;
use crate::macros::sdk_warn;
use crate::runtime::Runtime;
#[doc(hidden)]
pub fn initialize<F, T>(constructor: F)
where
F: FnOnce() -> T + 'static,
T: SampPlugin + 'static,
{
let rt = Runtime::initialize();
let plugin = constructor();
rt.set_plugin(plugin);
rt.post_initialize();
}
#[derive(Debug, Clone, Copy)]
pub struct TickConfig {
pub sa_mp: bool,
pub omp: bool,
pub omp_interval: Duration,
}
impl Default for TickConfig {
fn default() -> Self {
Self {
sa_mp: true,
omp: true,
omp_interval: Duration::from_millis(5),
}
}
}
impl TickConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn sa_mp(mut self, enabled: bool) -> Self {
self.sa_mp = enabled;
self
}
#[must_use]
pub fn omp(mut self, enabled: bool) -> Self {
self.omp = enabled;
self
}
#[must_use]
pub fn omp_interval(mut self, interval: Duration) -> Self {
self.omp_interval = interval;
self
}
#[must_use]
pub fn sa_mp_only() -> Self {
Self::default().omp(false)
}
#[must_use]
pub fn omp_only(interval: Duration) -> Self {
Self::default().sa_mp(false).omp_interval(interval)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TickSource {
SaMp,
OmpTimer,
}
#[derive(Debug, Clone, Copy)]
pub struct TickContext {
pub elapsed: Duration,
pub source: TickSource,
}
pub fn enable_tick() {
enable_tick_with(TickConfig::default());
}
pub fn enable_tick_with(config: TickConfig) {
Runtime::get().set_tick_config(config);
}
pub fn enable_debug_hook(amx: &Amx) {
amx.install_debug_hook(debug_hook_trampoline);
}
pub fn disable_debug_hook(amx: &Amx) {
amx.remove_debug_hook();
}
extern "C" fn debug_hook_trampoline(amx: *mut samp_sdk::raw::types::AMX) -> i32 {
let _ = crate::panic_guard::catch(|| {
let Some(rt) = Runtime::try_get() else { return };
let wrapped = Amx::new(amx, rt.amx_exports());
Runtime::plugin().on_debug_break(&wrapped);
});
0 }
pub fn logger() -> fern::Dispatch {
let rt = Runtime::get();
rt.disable_default_logger();
fern::Dispatch::new().chain(fern::Output::call(|record| {
crate::logger::to_server(record.level(), record.args().to_string());
}))
}
#[doc(hidden)]
#[must_use]
pub fn get<T: SampPlugin + 'static>() -> NonNull<T> {
Runtime::plugin_cast()
}
#[doc(hidden)]
#[must_use]
pub fn try_get<T: SampPlugin + 'static>() -> Option<NonNull<T>> {
Runtime::try_plugin_cast()
}
static PLUGIN_BORROWED: AtomicBool = AtomicBool::new(false);
static NATIVE_DEPTH: AtomicUsize = AtomicUsize::new(0);
#[doc(hidden)]
pub struct NativeFrame;
impl NativeFrame {
#[must_use]
pub fn enter() -> Self {
if PLUGIN_BORROWED.load(Ordering::Acquire) {
static WARNED: AtomicBool = AtomicBool::new(false);
if !WARNED.swap(true, Ordering::Relaxed) {
sdk_warn!(
"a native ran while a main-thread job held the plugin: the two \
`&mut` borrows alias. Call into Pawn after the job's closure \
returns — collect what to send, end the closure, then send it."
);
}
}
let depth = NATIVE_DEPTH.load(Ordering::Acquire);
NATIVE_DEPTH.store(depth + 1, Ordering::Release);
Self
}
}
impl Drop for NativeFrame {
fn drop(&mut self) {
let depth = NATIVE_DEPTH.load(Ordering::Acquire);
NATIVE_DEPTH.store(depth.saturating_sub(1), Ordering::Release);
}
}
pub fn with_instance<T, R>(f: impl FnOnce(&mut T) -> R) -> Option<R>
where
T: SampPlugin + 'static,
{
if NATIVE_DEPTH.load(Ordering::Acquire) > 0 {
sdk_warn!(
"with_instance() called from inside a native, which already holds \
`&mut self` — use that instead; nothing was run"
);
return None;
}
if PLUGIN_BORROWED.swap(true, Ordering::AcqRel) {
sdk_warn!("with_instance() called while the plugin was already borrowed; nothing was run");
return None;
}
struct Release;
impl Drop for Release {
fn drop(&mut self) {
PLUGIN_BORROWED.store(false, Ordering::Release);
}
}
let _release = Release;
let Some(plugin) = Runtime::plugin_as::<T>() else {
if Runtime::plugin_is_set() {
sdk_warn!(
"with_instance::<{}>() does not name the plugin's type; nothing was run",
std::any::type_name::<T>()
);
} else {
sdk_warn!("with_instance() called before the plugin was created; nothing was run");
}
return None;
};
Some(f(plugin))
}
#[must_use]
pub fn is_borrowed() -> bool {
PLUGIN_BORROWED.load(Ordering::Acquire)
}
#[cfg(not(feature = "samp-only"))]
#[must_use]
pub fn omp_core() -> Option<*mut samp_sdk::omp::component::ICore> {
crate::runtime::Runtime::get().omp_core()
}
#[cfg(not(feature = "samp-only"))]
#[must_use]
pub fn omp_query_component(
uid: samp_sdk::omp::types::UID,
) -> Option<*mut samp_sdk::omp::server::ServerComponent> {
crate::runtime::Runtime::get().omp_query_component(uid)
}
#[cfg(not(feature = "samp-only"))]
#[must_use]
pub fn omp_query<T>() -> Option<T>
where
T: samp_sdk::omp::OmpComponentHandle,
{
let raw = omp_query_component(T::UID)?;
let nonnull_ptr = std::ptr::NonNull::new(raw)?;
Some(unsafe { T::from_raw(nonnull_ptr) })
}
pub trait SampPlugin {
fn on_load(&mut self) {}
fn on_unload(&mut self) {}
fn on_amx_load(&mut self, amx: &Amx) {
let _ = amx;
}
fn on_amx_unload(&mut self, amx: &Amx) {
let _ = amx;
}
fn on_debug_break(&mut self, amx: &Amx) {
let _ = amx;
}
fn on_tick(&mut self, ctx: TickContext) {
let _ = ctx;
}
#[cfg(not(feature = "samp-only"))]
fn on_omp_ready(&mut self) {}
#[cfg(not(feature = "samp-only"))]
fn on_component_free(&mut self) {}
}
#[doc(hidden)]
pub fn convert_return_value<T: AmxCell<'static>>(value: T) -> i32 {
value.as_cell()
}
#[must_use]
pub fn pawn_include() -> String {
let rt = Runtime::get();
render_include(rt.plugin_name(), rt.native_decls())
}
pub(crate) fn native_decls() -> Vec<&'static str> {
Runtime::get().native_decls().to_vec()
}
pub(crate) fn render_include(name: &str, decls: &[&str]) -> String {
let guard: String = name
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
let mut out = format!(
"// Generated by rust-samp from the #[native] signatures of {name}.\n\
// Edits are lost on the next build — change the Rust side instead.\n\n\
#if defined _{guard}_included\n #endinput\n#endif\n#define _{guard}_included\n\n"
);
for decl in decls {
out.push_str(decl);
out.push('\n');
}
out
}
pub fn write_pawn_include(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
std::fs::write(path, pawn_include())
}
pub(crate) fn write_pawn_include_if_requested() {
let Some(path) = crate::pawn_include::path_for_plugin("SAMP_PAWN_INCLUDE") else {
return;
};
if let Some(template) = crate::pawn_include::path_for_plugin("SAMP_PAWN_INCLUDE_TEMPLATE") {
crate::pawn_include::write_from_template(&template, &path);
return;
}
match write_pawn_include(&path) {
Ok(()) => crate::macros::sdk_info!("Pawn include written to {}", path.to_string_lossy()),
Err(e) => crate::macros::sdk_warn!(
"could not write the Pawn include to {}: {e}",
path.to_string_lossy()
),
}
}
#[cfg(test)]
mod pawn_include_tests {
use super::render_include;
#[test]
fn header_names_the_plugin_and_declarations_follow() {
let out = render_include("counter", &["native Counter_Get(&out);"]);
assert!(out.contains("signatures of counter"));
assert!(out.ends_with("native Counter_Get(&out);\n"));
}
#[test]
fn include_guard_drops_characters_pawn_rejects() {
let out = render_include("email-samp", &[]);
assert!(out.contains("#define _email_samp_included"));
assert!(!out.contains("email-samp_included"));
}
#[test]
fn a_plugin_without_natives_still_produces_a_valid_include() {
let out = render_include("hello", &[]);
assert!(out.contains("#if defined _hello_included"));
}
}
#[cfg(test)]
mod instance_tests {
use super::*;
use crate::test_support::{TestPlugin, exclusive, value};
struct Other;
impl SampPlugin for Other {}
#[test]
fn the_closure_reaches_the_plugin_and_returns_its_value() {
let _g = exclusive();
let before = value();
let after = with_instance::<TestPlugin, _>(|p| {
p.value += 7;
p.value
})
.unwrap();
assert_eq!(after, before + 7);
assert_eq!(value(), after);
}
#[test]
fn naming_the_wrong_type_runs_nothing() {
let _g = exclusive();
let mut ran = false;
let out = with_instance::<Other, _>(|_| {
ran = true;
});
assert!(out.is_none());
assert!(
!ran,
"the plugin's bytes must not be reinterpreted as another type"
);
}
#[test]
fn a_nested_borrow_is_refused_rather_than_aliased() {
let _g = exclusive();
let inner = with_instance::<TestPlugin, _>(|_outer| {
assert!(is_borrowed());
with_instance::<TestPlugin, _>(|_| "ran anyway")
})
.unwrap();
assert_eq!(inner, None);
assert!(!is_borrowed(), "the borrow is released on the way out");
}
#[test]
fn a_native_frame_holds_the_borrow_off() {
let _g = exclusive();
let frame = NativeFrame::enter();
assert!(
with_instance::<TestPlugin, _>(|p| p.value).is_none(),
"a native already holds `&mut self`"
);
drop(frame);
assert!(with_instance::<TestPlugin, _>(|p| p.value).is_some());
}
#[test]
fn a_panic_inside_the_closure_does_not_lock_the_plugin_away() {
let _g = exclusive();
let panicked = std::panic::catch_unwind(|| {
with_instance::<TestPlugin, _>(|_| panic!("closure blew up"));
});
assert!(panicked.is_err());
assert!(!is_borrowed());
assert!(with_instance::<TestPlugin, _>(|p| p.value).is_some());
}
}