#[cfg(test)]
#[path = "tests/entities.rs"]
mod tests;
use crate::NotThreadSafe;
use crate::datatables::ServerClass;
use crate::edicts::Edict;
use crate::math::{QAngle, Vector};
use sdk_raw::entities::datamap::DataMaps;
use sdk_raw::entities::{
EFL_KILLME, ENT_ENTRY_MASK, INVALID_EHANDLE_INDEX, NUM_ENT_ENTRIES, NUM_SERIAL_NUM_SHIFT_BITS,
TeleportSlot,
};
use sdk_raw::util::cstr::borrow_cstr;
use sdk_raw::vcall;
use std::ffi::{CStr, c_int};
use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData;
use std::num::NonZero;
use std::ptr::{self, NonNull};
use std::sync::OnceLock;
#[doc(alias("CBaseEntity"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Entity<'s> {
raw: NonNull<sys::CBaseEntity>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> Entity<'s> {
pub(crate) const unsafe fn from_raw(raw: NonNull<sys::CBaseEntity>) -> Self {
Self {
raw,
_scope: PhantomData,
_not_thread_safe: PhantomData,
}
}
pub(crate) unsafe fn accept_input(
self,
input: &CStr,
mut value: sys::variant_t,
activator: Option<Entity<'_>>,
caller: Option<Entity<'_>>,
) -> bool {
unsafe {
sdk_raw::entities::accept_input(
self.as_ptr(),
input,
activator.map_or(ptr::null_mut(), Entity::as_ptr),
caller.map_or(ptr::null_mut(), Entity::as_ptr),
&raw mut value,
0,
)
}
}
pub const fn as_ptr(self) -> *mut sys::CBaseEntity {
self.raw.as_ptr()
}
#[doc(alias("GetClassname"))]
pub fn class_name(self) -> &'s CStr {
let Some(networkable) = self.networkable() else {
return c"";
};
unsafe { borrow_cstr(vcall!(networkable.as_ptr() => IServerNetworkable_GetClassName())) }
.unwrap_or_default()
}
pub(crate) fn data_maps(self) -> DataMaps<'s> {
unsafe { DataMaps::new(sdk_raw::entities::data_desc_map(self.as_ptr())) }
}
#[doc(alias("GetEdict"))]
pub fn edict(self) -> Option<Edict<'s>> {
let networkable = self.networkable()?;
let edict = unsafe { vcall!(networkable.as_ptr() => IServerNetworkable_GetEdict()) };
NonNull::new(edict).map(|edict| unsafe { Edict::from_raw(edict) })
}
pub(crate) fn find_base_entity_field(
self,
name: &CStr,
field_type: sys::fieldtype_t,
size: usize,
) -> Option<usize> {
sdk_raw::entities::find_base_entity_field(self.data_maps(), name, field_type, size)
}
#[doc(alias("m_iHammerID"))]
pub fn hammer_id(self) -> Option<HammerId> {
static HAMMER_ID_OFFSET: OnceLock<Option<usize>> = OnceLock::new();
let offset = (*HAMMER_ID_OFFSET.get_or_init(|| {
self.find_base_entity_field(
c"m_iHammerID",
sys::_fieldtypes_FIELD_INTEGER,
size_of::<c_int>(),
)
}))?;
HammerId::new(unsafe { self.as_ptr().byte_add(offset).cast::<c_int>().read() })
}
#[doc(alias("GetRefEHandle"))]
pub fn handle(self) -> EntityHandle {
let handle = unsafe { vcall!(self.server_entity() => IServerEntity_GetRefEHandle()) };
NonNull::new(handle.cast_mut()).map_or(EntityHandle::INVALID, |handle| {
EntityHandle(unsafe { (&raw const (*handle.as_ptr()).m_Index).read() })
})
}
#[cfg_attr(
not(feature = "tf2"),
expect(dead_code, reason = "only the tf2 module checks classes so far")
)]
pub(crate) fn has_data_map_class(self, class: &CStr) -> bool {
self.data_maps().any(|map| map.class_name() == Some(class))
}
#[doc(alias("entindex"))]
pub fn index(self) -> Option<usize> {
self.handle().index()
}
#[doc(alias("EFL_KILLME", "IsMarkedForDeletion"))]
pub fn is_marked_for_deletion(self) -> bool {
static EFLAGS_OFFSET: OnceLock<usize> = OnceLock::new();
let offset = *EFLAGS_OFFSET.get_or_init(|| {
self.find_base_entity_field(
c"m_iEFlags",
sys::_fieldtypes_FIELD_INTEGER,
size_of::<c_int>(),
)
.expect("Source's CBaseEntity datamap does not contain m_iEFlags")
});
let flags = unsafe { self.as_ptr().byte_add(offset).cast::<c_int>().read() };
flags & EFL_KILLME != 0
}
pub(crate) fn is_protected(self) -> bool {
self.index() == Some(0)
|| self.data_maps().any(|map| {
matches!(
map.class_name().map(CStr::to_bytes),
Some(b"CBasePlayer" | b"CEnvSoundscape")
)
})
}
pub(crate) fn name_field(self) -> Option<*mut sys::string_t> {
static NAME_OFFSET: OnceLock<Option<usize>> = OnceLock::new();
let offset = (*NAME_OFFSET.get_or_init(|| {
self.find_base_entity_field(
c"m_iName",
sys::_fieldtypes_FIELD_STRING,
size_of::<sys::string_t>(),
)
}))?;
Some(unsafe { self.as_ptr().byte_add(offset).cast() })
}
fn networkable(self) -> Option<NonNull<sys::IServerNetworkable>> {
NonNull::new(unsafe { vcall!(self.server_entity() => IServerEntity_GetNetworkable()) })
}
#[doc(alias("GetAbsOrigin", "GetCollisionOrigin"))]
pub fn position(self) -> Option<Vector> {
let collideable = NonNull::new(unsafe {
vcall!(self.server_entity() => IServerEntity_GetCollideable())
})?;
let origin = NonNull::new(
unsafe { vcall!(collideable.as_ptr() => ICollideable_GetCollisionOrigin()) }.cast_mut(),
)?;
Some(unsafe { origin.as_ptr().read() }.into())
}
#[doc(alias("GetServerClass"))]
pub fn server_class(self) -> Option<ServerClass<'s>> {
let networkable = self.networkable()?;
let class = unsafe { vcall!(networkable.as_ptr() => IServerNetworkable_GetServerClass()) };
NonNull::new(class).map(|class| unsafe { ServerClass::from_raw(class) })
}
fn server_entity(self) -> *mut sys::IServerEntity {
unsafe { &raw mut (*self.as_ptr())._base }
}
pub(crate) fn string_key_field(self, key: &CStr) -> Option<*const sys::string_t> {
let (field, offset) = self.data_maps().find_key_field(key.to_bytes())?;
let is_string = matches!(
field.fieldType,
sys::_fieldtypes_FIELD_STRING
| sys::_fieldtypes_FIELD_MODELNAME
| sys::_fieldtypes_FIELD_SOUNDNAME
);
(is_string && offset.is_multiple_of(align_of::<sys::string_t>()))
.then(|| unsafe { self.as_ptr().byte_add(offset).cast_const().cast() })
}
pub(crate) unsafe fn teleport(
self,
slot: TeleportSlot,
origin: Option<Vector>,
angles: Option<QAngle>,
velocity: Option<Vector>,
) -> Result<(), TeleportError> {
let finite_angles = |angles: QAngle| {
angles.pitch.is_finite() && angles.yaw.is_finite() && angles.roll.is_finite()
};
if !(origin.is_none_or(|origin| origin.is_finite())
&& angles.is_none_or(finite_angles)
&& velocity.is_none_or(|velocity| velocity.is_finite()))
{
return Err(TeleportError::NonFinite);
}
if self.is_marked_for_deletion() {
return Err(TeleportError::MarkedForDeletion);
}
let origin = origin.map(sys::Vector::from);
let angles = angles.map(sys::QAngle::from);
let velocity = velocity.map(sys::Vector::from);
unsafe {
sdk_raw::entities::teleport(
self.as_ptr(),
slot,
origin.as_ref().map_or(ptr::null(), ptr::from_ref),
angles.as_ref().map_or(ptr::null(), ptr::from_ref),
velocity.as_ref().map_or(ptr::null(), ptr::from_ref),
)
};
Ok(())
}
}
#[doc(alias("CBaseHandle", "EHANDLE"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EntityHandle(u32);
impl EntityHandle {
#[doc(alias("INVALID_EHANDLE_INDEX"))]
pub const INVALID: Self = Self(INVALID_EHANDLE_INDEX);
#[doc(alias("NUM_ENT_ENTRIES"))]
pub const SLOTS: usize = NUM_ENT_ENTRIES;
pub const fn from_raw(raw: u32) -> Self {
Self(raw)
}
#[doc(alias("GetEntryIndex"))]
pub const fn index(self) -> Option<usize> {
if self.is_valid() {
Some((self.0 & ENT_ENTRY_MASK) as usize)
} else {
None
}
}
#[doc(alias("IsValid"))]
pub const fn is_valid(self) -> bool {
self.0 != Self::INVALID.0
}
#[doc(alias("GetSerialNumber"))]
pub const fn serial_number(self) -> u32 {
self.0 >> NUM_SERIAL_NUM_SHIFT_BITS
}
#[doc(alias("ToInt"))]
pub const fn to_raw(self) -> u32 {
self.0
}
}
impl Display for EntityHandle {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.index() {
Some(index) => write!(f, "{index}:{}", self.serial_number()),
None => f.write_str("invalid"),
}
}
}
#[doc(alias("hammerid", "m_iHammerID"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HammerId(NonZero<c_int>);
impl HammerId {
pub const fn new(id: c_int) -> Option<Self> {
match NonZero::new(id) {
Some(id) => Some(Self(id)),
None => None,
}
}
pub const fn get(self) -> c_int {
self.0.get()
}
}
impl Display for HammerId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("the game keeps using this entity after it is freed")]
pub struct ProtectedEntity;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum TeleportError {
#[error("the destination contains a non-finite component")]
NonFinite,
#[error("the entity is marked for deletion")]
MarkedForDeletion,
}