use super::signature::{HOOK_MANAGERS, Signature};
use super::{Handler, HookAction, HookCall, HookError, HookId, HookTiming, khook, sourcehook};
use crate::MetamodApi;
use crate::api::MetamodVersion;
use crate::sys::plugin::{PluginStatus, cpp_metamod_plugin_status};
use crate::sys::sourcehook::ISourceHook;
use std::any::{Any, TypeId};
use std::cell::{Cell, RefCell, RefMut};
use std::ffi::{c_int, c_void};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::{self, NonNull};
static REGISTRY: MainThread<RefCell<Registry>> = MainThread(RefCell::new(Registry::new()));
thread_local! {
static MAIN_THREAD: Cell<bool> = const { Cell::new(false) };
}
trait AnySite: Any {
fn as_any(&self) -> &dyn Any;
fn contains(&self, id: HookId) -> bool;
fn remove(&self, id: HookId) -> bool;
fn slot(&self) -> (NonNull<*mut c_void>, usize);
}
#[derive(Debug, Clone, Copy)]
enum Backend {
KHook,
SourceHook {
sourcehook: NonNull<ISourceHook>,
plugin: c_int,
},
}
#[derive(Clone, Copy)]
struct Entry<S: Signature> {
id: HookId,
timing: HookTiming,
instance: Option<NonNull<c_void>>,
handler: &'static dyn Handler<S>,
removed: bool,
}
struct MainThread<T>(T);
unsafe impl<T> Sync for MainThread<T> {}
pub(super) struct Registry {
api_version: c_int,
backend: Option<Backend>,
generation: Option<u64>,
hook_managers: [Option<(TypeId, c_int)>; HOOK_MANAGERS],
sites: Vec<&'static dyn AnySite>,
}
impl Registry {
const fn new() -> Self {
Self {
api_version: 0,
backend: None,
generation: None,
hook_managers: [None; HOOK_MANAGERS],
sites: Vec::new(),
}
}
pub(super) fn bind(api: MetamodApi<'_>) -> Result<RefMut<'static, Self>, HookError> {
let api_version = api.version().plugin_api_version();
let status = plugin_status(api_version);
if !status.loaded {
return Err(HookError::NotBound);
}
MAIN_THREAD.with(|main_thread| main_thread.set(true));
let mut registry = REGISTRY
.0
.try_borrow_mut()
.map_err(|_| HookError::NotBound)?;
if registry.generation != Some(status.generation) {
*registry = Self::new();
registry.api_version = api_version;
registry.generation = Some(status.generation);
sourcehook::reset_hook_managers();
}
if registry.backend.is_none() {
registry.backend = Some(match api.version() {
MetamodVersion::Stable1226 => Backend::SourceHook {
sourcehook: sourcehook::bind(api)?,
plugin: status.id,
},
MetamodVersion::Dev1469 => {
khook::bind(api, status.id)?;
Backend::KHook
}
});
}
Ok(registry)
}
pub(super) fn current(api: MetamodApi<'_>) -> Option<RefMut<'static, Self>> {
let status = plugin_status(api.version().plugin_api_version());
let registry = REGISTRY.0.try_borrow_mut().ok()?;
(status.loaded && registry.generation == Some(status.generation)).then_some(registry)
}
pub(super) fn contains(&self, id: HookId) -> bool {
self.sites.iter().any(|site| site.contains(id))
}
pub(super) unsafe fn install<S: Signature>(
&mut self,
site: &'static Site<S>,
timing: HookTiming,
) -> Result<(), HookError> {
match self.backend {
Some(Backend::KHook) => unsafe { khook::install(site, timing) },
Some(Backend::SourceHook { sourcehook, plugin }) => {
let manager = sourcehook::hook_manager::<S>(&mut self.hook_managers, site.index)?;
unsafe { sourcehook::install(sourcehook, plugin, manager, site, timing) }
}
None => Err(HookError::NotBound),
}
}
pub(super) unsafe fn original(
&self,
vtable: NonNull<*mut c_void>,
index: c_int,
) -> *mut c_void {
match self.backend {
Some(Backend::KHook) => unsafe { khook::original(vtable, index) },
Some(Backend::SourceHook { sourcehook, .. }) => unsafe {
sourcehook::original(sourcehook, vtable, index)
},
None => ptr::null_mut(),
}
}
pub(super) fn remove(&self, id: HookId) -> bool {
self.sites.iter().any(|site| site.remove(id))
}
pub(super) fn site<S: Signature>(
&mut self,
vtable: NonNull<*mut c_void>,
index: usize,
) -> Result<&'static Site<S>, HookError> {
if c_int::try_from(index).is_err() {
return Err(HookError::InvalidArgument);
}
let found = self
.sites
.iter()
.find(|site| site.slot() == (vtable, index));
if let Some(&site) = found {
return site
.as_any()
.downcast_ref::<Site<S>>()
.ok_or(HookError::SignatureMismatch);
}
let site: &'static Site<S> = Box::leak(Box::new(Site {
api_version: self.api_version,
generation: self.generation.unwrap_or_default(),
vtable,
index,
entries: RefCell::new(Vec::new()),
depth: Cell::new(0),
installed: [Cell::new(false), Cell::new(false)],
}));
self.sites.push(site);
Ok(site)
}
}
pub(super) struct Site<S: Signature> {
api_version: c_int,
generation: u64,
vtable: NonNull<*mut c_void>,
index: usize,
entries: RefCell<Vec<Entry<S>>>,
depth: Cell<u32>,
installed: [Cell<bool>; 2],
}
impl<S: Signature> Site<S> {
pub(super) fn active(&self) -> bool {
let status = plugin_status(self.api_version);
status.loaded && !status.paused && status.generation == self.generation
}
pub(super) fn dispatch(&self, call: &HookCall<'_, S>) {
self.depth.set(self.depth.get() + 1);
let mut position = 0;
loop {
let entry = self.entries.borrow().get(position).copied();
let Some(entry) = entry else { break };
position += 1;
if entry.removed
|| entry.timing != call.timing()
|| entry
.instance
.is_some_and(|instance| instance.as_ptr() != call.this().cast())
{
continue;
}
let action = catch_unwind(AssertUnwindSafe(|| entry.handler.call(call)))
.unwrap_or(HookAction::Ignore);
call.record(action);
}
let depth = self.depth.get() - 1;
self.depth.set(depth);
if depth == 0 {
self.entries.borrow_mut().retain(|entry| !entry.removed);
}
}
pub(super) const fn index(&self) -> usize {
self.index
}
pub(super) fn installed(&self, timing: HookTiming) -> bool {
self.installed[timing as usize].get()
}
pub(super) fn push(
&self,
id: HookId,
timing: HookTiming,
instance: Option<NonNull<c_void>>,
handler: &'static dyn Handler<S>,
) {
self.entries.borrow_mut().push(Entry {
id,
timing,
instance,
handler,
removed: false,
});
}
pub(super) fn set_installed(&self, timing: HookTiming) {
self.installed[timing as usize].set(true);
}
pub(super) const fn vtable(&self) -> NonNull<*mut c_void> {
self.vtable
}
}
impl<S: Signature> AnySite for Site<S> {
fn as_any(&self) -> &dyn Any {
self
}
fn contains(&self, id: HookId) -> bool {
self.entries
.borrow()
.iter()
.any(|entry| entry.id == id && !entry.removed)
}
fn remove(&self, id: HookId) -> bool {
let mut entries = self.entries.borrow_mut();
let Some(position) = entries
.iter()
.position(|entry| entry.id == id && !entry.removed)
else {
return false;
};
if self.depth.get() == 0 {
entries.remove(position);
} else {
entries[position].removed = true;
}
true
}
fn slot(&self) -> (NonNull<*mut c_void>, usize) {
(self.vtable, self.index)
}
}
pub(super) fn on_main_thread() -> bool {
MAIN_THREAD.try_with(Cell::get).unwrap_or(false)
}
fn plugin_status(api_version: c_int) -> PluginStatus {
#[cfg(test)]
if let Some(status) = crate::test_support::harness::plugin_status() {
return status;
}
cpp_metamod_plugin_status(api_version)
}