#[cfg(test)]
#[path = "../tests/tf2/class_targets.rs"]
mod tests;
use crate::entities::Entity;
use crate::{Game, Server};
use sdk_raw::abi::VTABLE_SLOT_SIZE;
use sdk_raw::tf2::class_targets::{ClassVtables, OBJECT_CLASSES, PLAYER_CLASSES};
use sdk_raw::util;
use sdk_raw::util::vtable::vtable_pointer;
use std::ffi::{CStr, c_void};
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::ptr::NonNull;
#[doc(alias("CBaseEntity"))]
#[derive(Debug)]
pub enum BaseEntity {}
#[doc(alias("CBaseObject"))]
#[derive(Debug)]
pub enum BaseObject {}
pub trait ClassKind: sealed::Sealed + Debug + 'static {
const NAME: &'static CStr;
const VTABLE_SLOTS: usize;
}
#[doc(alias("CBaseCombatCharacter"))]
#[derive(Debug)]
pub enum CombatCharacter {}
#[doc(alias("CBaseCombatWeapon"))]
#[derive(Debug)]
pub enum CombatWeapon {}
pub trait DerivesFrom<B: ClassKind>: ClassKind {}
impl<K: ClassKind> DerivesFrom<K> for K {}
#[doc(alias("CTFWeaponBaseMelee"))]
#[derive(Debug)]
pub enum TfMeleeWeapon {}
#[doc(alias("CTFPlayer", "CTFBot"))]
#[derive(Debug)]
pub enum TfPlayer {}
#[doc(alias("CTFWeaponBase"))]
#[derive(Debug)]
pub enum TfWeapon {}
macro_rules! kinds {
($($kind:ident: $name:literal, $vtable:ty, [$($base:ident),*];)*) => {$(
impl sealed::Sealed for $kind {}
impl ClassKind for $kind {
const NAME: &'static CStr = $name;
const VTABLE_SLOTS: usize = size_of::<$vtable>() / VTABLE_SLOT_SIZE;
}
$(impl DerivesFrom<$base> for $kind {})*
)*};
}
kinds! {
BaseEntity: c"CBaseEntity", sys::CBaseEntity__bindgen_vtable, [];
BaseObject: c"CBaseObject", sys::CBaseObject__bindgen_vtable, [BaseEntity, CombatCharacter];
CombatCharacter: c"CBaseCombatCharacter", sys::CBaseCombatCharacter__bindgen_vtable, [BaseEntity];
CombatWeapon: c"CBaseCombatWeapon", sys::CBaseCombatWeapon__bindgen_vtable, [BaseEntity];
TfMeleeWeapon: c"CTFWeaponBaseMelee", sys::CTFWeaponBaseMelee__bindgen_vtable, [BaseEntity, CombatWeapon, TfWeapon];
TfPlayer: c"CTFPlayer", sys::CTFPlayer__bindgen_vtable, [BaseEntity, CombatCharacter];
TfWeapon: c"CTFWeaponBase", sys::CTFWeaponBase__bindgen_vtable, [BaseEntity, CombatWeapon];
}
pub struct ClassTarget<'s, K: ClassKind> {
vtable: NonNull<*mut c_void>,
_kind: PhantomData<fn() -> K>,
_scope: PhantomData<&'s Server<'s>>,
}
impl<'s, K: ClassKind> ClassTarget<'s, K> {
pub const unsafe fn from_raw(vtable: NonNull<*mut c_void>) -> Self {
Self {
vtable,
_kind: PhantomData,
_scope: PhantomData,
}
}
pub const unsafe fn from_vtable(vtable: ClassVtable<'s>) -> Self {
unsafe { Self::from_raw(vtable.as_ptr()) }
}
pub fn of(server: Server<'s>, entity: Entity<'s>) -> Option<Self> {
if server.game() != Game::TeamFortress2 || !entity.has_data_map_class(K::NAME) {
return None;
}
let vtable = unsafe { vtable_pointer::<*mut c_void>(entity.as_ptr()) };
NonNull::new(vtable.cast_mut()).map(|vtable| Self {
vtable,
_kind: PhantomData,
_scope: PhantomData,
})
}
pub const fn as_ptr(self) -> NonNull<*mut c_void> {
self.vtable
}
pub fn is_class_of(self, entity: Entity<'_>) -> bool {
let vtable = unsafe { vtable_pointer::<*mut c_void>(entity.as_ptr()) };
vtable.cast_mut() == self.vtable.as_ptr()
}
pub const fn upcast<B: ClassKind>(self) -> ClassTarget<'s, B>
where
K: DerivesFrom<B>,
{
ClassTarget {
vtable: self.vtable,
_kind: PhantomData,
_scope: PhantomData,
}
}
}
impl<K: ClassKind> Clone for ClassTarget<'_, K> {
fn clone(&self) -> Self {
*self
}
}
impl<K: ClassKind> Copy for ClassTarget<'_, K> {}
impl<K: ClassKind> Debug for ClassTarget<'_, K> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ClassTarget")
.field("kind", &K::NAME)
.field("vtable", &self.vtable)
.finish()
}
}
impl<K: ClassKind> Eq for ClassTarget<'_, K> {}
impl<K: ClassKind> Hash for ClassTarget<'_, K> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.vtable.hash(state);
}
}
impl<K: ClassKind> PartialEq for ClassTarget<'_, K> {
fn eq(&self, other: &Self) -> bool {
self.vtable == other.vtable
}
}
#[derive(Debug, thiserror::Error)]
pub enum ClassTargetError {
#[error("class targets require Team Fortress 2")]
WrongGame,
#[error("the game module could not be inspected")]
Image(#[from] std::io::Error),
#[error("the game module has an unsupported executable image")]
InvalidImage,
#[error("no unique primary vtable found for TF2's class `{0}`")]
NotFound(&'static str),
}
impl From<util::Error> for ClassTargetError {
fn from(error: util::Error) -> Self {
match error {
util::Error::InvalidImage => Self::InvalidImage,
util::Error::Io(error) => Self::Image(error),
}
}
}
#[derive(Debug, Clone)]
pub struct ClassTargets<'s> {
vtables: ClassVtables,
_scope: PhantomData<&'s Server<'s>>,
}
impl<'s> ClassTargets<'s> {
pub fn load(server: Server<'s>) -> Result<Self, ClassTargetError> {
if server.game() != Game::TeamFortress2 {
return Err(ClassTargetError::WrongGame);
}
let vtables = unsafe { ClassVtables::load(server.game_server_factory().as_raw()) }?;
Ok(Self {
vtables,
_scope: PhantomData,
})
}
pub fn find(&self, class: &str, slot: usize) -> Option<ClassVtable<'s>> {
self.vtables.find(class, slot).map(|vtable| ClassVtable {
vtable,
_scope: PhantomData,
})
}
pub fn find_all(&self, classes: &[&str], slot: usize) -> Vec<Option<ClassVtable<'s>>> {
self.vtables
.find_all(classes, slot)
.into_iter()
.map(|vtable| {
vtable.map(|vtable| ClassVtable {
vtable,
_scope: PhantomData,
})
})
.collect()
}
pub unsafe fn find_kind<K: ClassKind>(
&self,
classes: &[&str],
) -> Vec<Option<ClassTarget<'s, K>>> {
self.find_all(classes, K::VTABLE_SLOTS - 1)
.into_iter()
.map(|vtable| vtable.map(|vtable| unsafe { ClassTarget::from_vtable(vtable) }))
.collect()
}
pub fn objects(
&self,
) -> Result<[ClassTarget<'s, BaseObject>; OBJECT_CLASSES.len()], ClassTargetError> {
let found = unsafe { self.find_kind(&OBJECT_CLASSES) };
known(found, &OBJECT_CLASSES)
}
pub fn players(
&self,
) -> Result<[ClassTarget<'s, TfPlayer>; PLAYER_CLASSES.len()], ClassTargetError> {
let found = unsafe { self.find_kind(&PLAYER_CLASSES) };
known(found, &PLAYER_CLASSES)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ClassVtable<'s> {
vtable: NonNull<*mut c_void>,
_scope: PhantomData<&'s Server<'s>>,
}
impl ClassVtable<'_> {
pub const fn as_ptr(self) -> NonNull<*mut c_void> {
self.vtable
}
}
#[derive(Debug, Clone)]
pub struct SlotTargets<'s, const SLOT: usize>(ClassTargets<'s>);
impl<'s, const SLOT: usize> SlotTargets<'s, SLOT> {
pub fn load(server: Server<'s>) -> Result<Self, ClassTargetError> {
ClassTargets::load(server).map(Self)
}
pub fn find(&self, class: &str) -> Option<ClassVtable<'s>> {
self.0.find(class, SLOT)
}
pub fn find_all(&self, classes: &[&str]) -> Vec<Option<ClassVtable<'s>>> {
self.0.find_all(classes, SLOT)
}
}
impl<'s, const SLOT: usize> From<ClassTargets<'s>> for SlotTargets<'s, SLOT> {
fn from(targets: ClassTargets<'s>) -> Self {
Self(targets)
}
}
fn known<'s, K: ClassKind, const N: usize>(
found: Vec<Option<ClassTarget<'s, K>>>,
names: &[&'static str; N],
) -> Result<[ClassTarget<'s, K>; N], ClassTargetError> {
let found: Vec<_> = found
.into_iter()
.zip(names)
.map(|(target, name)| target.ok_or(ClassTargetError::NotFound(name)))
.collect::<Result<_, _>>()?;
Ok(found
.try_into()
.unwrap_or_else(|_| unreachable!("one class is found for each name")))
}
mod sealed {
pub trait Sealed {}
}