#[cfg(test)]
#[path = "tests/death_hooks.rs"]
mod tests;
use crate::MetamodApi;
use crate::hook::{
Handler, HookAction, HookCall, HookError, HookId, HookTarget, HookTiming, VirtualFunction,
};
use source_sdk_2013::entities::Entity;
use source_sdk_2013::raw::entities::health::{EVENT_KILLED_SLOT, EventKilledFn as EventKilled};
use source_sdk_2013::raw::util::vtable::vtable_pointer;
use source_sdk_2013::tf2::damage::{DamageEvent, DamageInfo};
use source_sdk_2013::{Game, Server, ServerBinding, sys};
use std::cell::Cell;
use std::ffi::c_void;
use std::ptr::NonNull;
pub type KilledFn = for<'s> fn(Server<'s>, Entity<'s>, &DamageInfo);
const EVENT_KILLED: VirtualFunction<EventKilled> = VirtualFunction::new(EVENT_KILLED_SLOT);
static ROUTES: [KilledRoute; 8] = [const { KilledRoute::new() }; 8];
#[derive(Debug, thiserror::Error)]
pub enum DeathHookError {
#[error("death hooks require a TF2 server and a CTFPlayer entity")]
NotTfPlayer,
#[error(transparent)]
Hook(#[from] HookError),
}
struct KilledRoute {
state: Cell<Option<RoutedDeath>>,
}
impl KilledRoute {
const fn new() -> Self {
Self {
state: Cell::new(None),
}
}
}
impl Handler<EventKilled> for KilledRoute {
fn call(&self, call: &HookCall<'_, EventKilled>) -> HookAction<()> {
let Some(route) = self.state.get() else {
return HookAction::Ignore;
};
let (info,) = call.args();
let (Some(victim), Some(info)) = (NonNull::new(call.this()), NonNull::new(info.cast_mut()))
else {
return HookAction::Ignore;
};
let scope = ();
let server = unsafe { route.binding.server(&scope) };
unsafe { dispatch(server, route.callback, victim, info) };
HookAction::Ignore
}
}
unsafe impl Sync for KilledRoute {}
#[derive(Clone, Copy)]
struct RoutedDeath {
binding: ServerBinding,
callback: KilledFn,
hook: HookId,
vtable: usize,
}
impl MetamodApi<'_> {
pub fn hook_player_killed(
self,
player: Entity<'_>,
binding: ServerBinding,
callback: KilledFn,
) -> Result<HookId, DeathHookError> {
if binding.game() != Game::TeamFortress2
|| !player
.server_class()
.is_some_and(|class| class.name() == c"CTFPlayer")
{
return Err(DeathHookError::NotTfPlayer);
}
unsafe { self.install_killed(NonNull::new(player.as_ptr()).unwrap(), binding, callback) }
}
unsafe fn install_killed(
self,
object: NonNull<sys::CBaseEntity>,
binding: ServerBinding,
callback: KilledFn,
) -> Result<HookId, DeathHookError> {
let vtable = unsafe { vtable_pointer::<c_void>(object.as_ptr()) }.addr();
if ROUTES.iter().any(|route| {
route
.state
.get()
.is_some_and(|state| state.vtable == vtable && self.has_hook(state.hook))
}) {
return Err(HookError::AlreadyInstalled.into());
}
let route = ROUTES
.iter()
.find(|route| {
route
.state
.get()
.is_none_or(|state| !self.has_hook(state.hook))
})
.ok_or(HookError::TooManyFunctions)?;
let hook = unsafe {
self.add_hook(
EVENT_KILLED,
HookTarget::class_of(object),
HookTiming::Post,
route,
)
}?;
route.state.set(Some(RoutedDeath {
binding,
callback,
hook,
vtable,
}));
Ok(hook)
}
}
unsafe fn dispatch(
server: Server<'_>,
callback: KilledFn,
victim: NonNull<sys::CBaseEntity>,
info: NonNull<sys::CTakeDamageInfo>,
) {
let event = unsafe { DamageEvent::from_raw(server, victim, info) };
callback(server, event.victim, &event.info);
}