use crate::MetamodApi;
use crate::sys::plugin::{self as raw, HookStatus};
use source_sdk_2013::interfaces::ServerGameDll;
use source_sdk_2013::net::incoming::{
HookTargetError, IncomingHandler, Verdict, hook_target, route_incoming,
};
use source_sdk_2013::{Server, ServerBinding};
use std::cell::Cell;
use std::ffi::{CStr, c_char, c_int, c_void};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::{self, NonNull};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum HookError {
#[error(
"hooks can only be installed while Metamod runs the plugin, and with a hooking library"
)]
NotBound,
#[error("the hook is already installed")]
AlreadyInstalled,
#[error("Metamod's hooking library refused the hook")]
Refused,
#[error("the hook was given invalid arguments")]
InvalidArgument,
#[error("this Metamod version has no plugin shell")]
Unsupported,
}
impl HookError {
pub(crate) fn check(status: HookStatus) -> Result<(), Self> {
match status {
HookStatus::INSTALLED => Ok(()),
HookStatus::NOT_BOUND => Err(Self::NotBound),
HookStatus::ALREADY_INSTALLED => Err(Self::AlreadyInstalled),
HookStatus::REFUSED => Err(Self::Refused),
HookStatus::INVALID_ARGUMENT => Err(Self::InvalidArgument),
_ => Err(Self::Unsupported),
}
}
}
pub type GameFrameFn = fn(server: Server<'_>, simulating: bool);
#[derive(Debug, Clone, Copy, Default)]
pub struct LevelEvents {
pub init: Option<fn(server: Server<'_>, map: &CStr)>,
pub shutdown: Option<fn(server: Server<'_>)>,
}
struct Route<T>(Cell<Option<(ServerBinding, T)>>);
unsafe impl<T> Sync for Route<T> {}
impl<T: Copy> Route<T> {
const fn new() -> Self {
Self(Cell::new(None))
}
fn context(&'static self) -> *mut c_void {
ptr::from_ref(self).cast_mut().cast()
}
unsafe fn from_context(context: *mut c_void) -> Option<(ServerBinding, T)> {
unsafe { &*context.cast::<Self>() }.0.get()
}
}
static GAME_FRAME: Route<GameFrameFn> = Route::new();
static LEVELS: Route<LevelEvents> = Route::new();
static NET_MESSAGES: Route<&'static dyn IncomingHandler> = Route::new();
#[derive(Debug, thiserror::Error)]
pub enum NetMessageHookError {
#[error(transparent)]
Target(#[from] HookTargetError),
#[error(transparent)]
Hook(#[from] HookError),
}
impl MetamodApi<'_> {
pub fn hook_game_frame(
self,
game_dll: ServerGameDll<'_>,
binding: ServerBinding,
callback: GameFrameFn,
) -> Result<(), HookError> {
GAME_FRAME.0.set(Some((binding, callback)));
let status = unsafe {
raw::cpp_metamod_hook_game_frame(
self.version().plugin_api_version(),
game_dll.as_ptr().cast(),
game_frame,
GAME_FRAME.context(),
)
};
HookError::check(status)
}
pub fn listen_level_events(
self,
binding: ServerBinding,
events: LevelEvents,
) -> Result<(), HookError> {
LEVELS.0.set(Some((binding, events)));
let status = unsafe {
raw::cpp_metamod_listen_levels(
self.version().plugin_api_version(),
events.init.map(|_| level_init as raw::LevelInitCallback),
events
.shutdown
.map(|_| level_shutdown as raw::LevelShutdownCallback),
LEVELS.context(),
)
};
HookError::check(status)
}
pub fn hook_net_messages(
self,
server: Server<'_>,
binding: ServerBinding,
handler: &'static dyn IncomingHandler,
) -> Result<(), NetMessageHookError> {
let target = hook_target(server)?;
NET_MESSAGES.0.set(Some((binding, handler)));
let status = unsafe {
raw::cpp_metamod_hook_net_messages(
self.version().plugin_api_version(),
target.handler.as_ptr(),
&target.slots,
net_message,
NET_MESSAGES.context(),
)
};
Ok(HookError::check(status)?)
}
}
fn with_server(binding: ServerBinding, f: impl FnOnce(Server<'_>)) {
let scope = ();
let server = unsafe { binding.server(&scope) };
catch_unwind(AssertUnwindSafe(|| f(server))).ok();
}
unsafe extern "C" fn game_frame(context: *mut c_void, simulating: bool) {
if let Some((binding, callback)) = unsafe { Route::<GameFrameFn>::from_context(context) } {
with_server(binding, |server| callback(server, simulating));
}
}
unsafe extern "C" fn level_init(context: *mut c_void, map: *const c_char) {
let Some((binding, events)) = (unsafe { Route::<LevelEvents>::from_context(context) }) else {
return;
};
let (Some(init), false) = (events.init, map.is_null()) else {
return;
};
let map = unsafe { CStr::from_ptr(map) };
with_server(binding, |server| init(server, map));
}
unsafe extern "C" fn level_shutdown(context: *mut c_void) {
if let Some((
binding,
LevelEvents {
shutdown: Some(shutdown),
..
},
)) = unsafe { Route::<LevelEvents>::from_context(context) }
{
with_server(binding, shutdown);
}
}
unsafe extern "C" fn net_message(
context: *mut c_void,
kind: c_int,
handler: *mut c_void,
message: *mut c_void,
) -> bool {
let route = unsafe { Route::<&'static dyn IncomingHandler>::from_context(context) };
let (Some((binding, incoming)), Some(handler), Some(message)) =
(route, NonNull::new(handler), NonNull::new(message))
else {
return false;
};
unsafe { route_incoming(&binding, incoming, kind, handler, message) == Verdict::Block }
}