pub mod debug_overlays;
pub mod dissolve;
pub mod factory;
pub mod fields;
pub mod health;
pub mod hierarchy;
pub mod identity;
pub mod movement;
pub mod outputs;
pub mod props;
pub mod render;
pub mod search;
pub mod solid;
pub mod spawn;
pub mod think;
#[cfg(test)]
#[path = "tests/entities.rs"]
mod tests;
use crate::datatables::ServerClass;
use crate::edicts::Edict;
use crate::math::{QAngle, Vector};
use crate::{NotThreadSafe, Server};
use sdk_raw::entities::datamap::DataMaps;
use sdk_raw::entities::{
COLLISION_GROUP_BREAKABLE_GLASS, COLLISION_GROUP_DEBRIS, COLLISION_GROUP_DEBRIS_TRIGGER,
COLLISION_GROUP_DISSOLVING, COLLISION_GROUP_DOOR_BLOCKER, COLLISION_GROUP_IN_VEHICLE,
COLLISION_GROUP_INTERACTIVE, COLLISION_GROUP_INTERACTIVE_DEBRIS, COLLISION_GROUP_NONE,
COLLISION_GROUP_NPC, COLLISION_GROUP_NPC_ACTOR, COLLISION_GROUP_NPC_SCRIPTED,
COLLISION_GROUP_PASSABLE_DOOR, COLLISION_GROUP_PLAYER, COLLISION_GROUP_PLAYER_MOVEMENT,
COLLISION_GROUP_PROJECTILE, COLLISION_GROUP_PUSHAWAY, COLLISION_GROUP_VEHICLE,
COLLISION_GROUP_VEHICLE_CLIP, COLLISION_GROUP_WEAPON, 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("Collision_Group_t"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CollisionGroup {
#[doc(alias("COLLISION_GROUP_NONE"))]
None,
#[doc(alias("COLLISION_GROUP_DEBRIS"))]
Debris,
#[doc(alias("COLLISION_GROUP_DEBRIS_TRIGGER"))]
DebrisTrigger,
#[doc(alias("COLLISION_GROUP_INTERACTIVE_DEBRIS"))]
InteractiveDebris,
#[doc(alias("COLLISION_GROUP_INTERACTIVE"))]
Interactive,
#[doc(alias("COLLISION_GROUP_PLAYER"))]
Player,
#[doc(alias("COLLISION_GROUP_BREAKABLE_GLASS"))]
BreakableGlass,
#[doc(alias("COLLISION_GROUP_VEHICLE"))]
Vehicle,
#[doc(alias("COLLISION_GROUP_PLAYER_MOVEMENT"))]
PlayerMovement,
#[doc(alias("COLLISION_GROUP_NPC"))]
Npc,
#[doc(alias("COLLISION_GROUP_IN_VEHICLE"))]
InVehicle,
#[doc(alias("COLLISION_GROUP_WEAPON"))]
Weapon,
#[doc(alias("COLLISION_GROUP_VEHICLE_CLIP"))]
VehicleClip,
#[doc(alias("COLLISION_GROUP_PROJECTILE"))]
Projectile,
#[doc(alias("COLLISION_GROUP_DOOR_BLOCKER"))]
DoorBlocker,
#[doc(alias("COLLISION_GROUP_PASSABLE_DOOR"))]
PassableDoor,
#[doc(alias("COLLISION_GROUP_DISSOLVING"))]
Dissolving,
#[doc(alias("COLLISION_GROUP_PUSHAWAY"))]
Pushaway,
#[doc(alias("COLLISION_GROUP_NPC_ACTOR"))]
NpcActor,
#[doc(alias("COLLISION_GROUP_NPC_SCRIPTED"))]
NpcScripted,
}
impl CollisionGroup {
pub const fn from_raw(value: c_int) -> Option<Self> {
match value {
COLLISION_GROUP_NONE => Some(Self::None),
COLLISION_GROUP_DEBRIS => Some(Self::Debris),
COLLISION_GROUP_DEBRIS_TRIGGER => Some(Self::DebrisTrigger),
COLLISION_GROUP_INTERACTIVE_DEBRIS => Some(Self::InteractiveDebris),
COLLISION_GROUP_INTERACTIVE => Some(Self::Interactive),
COLLISION_GROUP_PLAYER => Some(Self::Player),
COLLISION_GROUP_BREAKABLE_GLASS => Some(Self::BreakableGlass),
COLLISION_GROUP_VEHICLE => Some(Self::Vehicle),
COLLISION_GROUP_PLAYER_MOVEMENT => Some(Self::PlayerMovement),
COLLISION_GROUP_NPC => Some(Self::Npc),
COLLISION_GROUP_IN_VEHICLE => Some(Self::InVehicle),
COLLISION_GROUP_WEAPON => Some(Self::Weapon),
COLLISION_GROUP_VEHICLE_CLIP => Some(Self::VehicleClip),
COLLISION_GROUP_PROJECTILE => Some(Self::Projectile),
COLLISION_GROUP_DOOR_BLOCKER => Some(Self::DoorBlocker),
COLLISION_GROUP_PASSABLE_DOOR => Some(Self::PassableDoor),
COLLISION_GROUP_DISSOLVING => Some(Self::Dissolving),
COLLISION_GROUP_PUSHAWAY => Some(Self::Pushaway),
COLLISION_GROUP_NPC_ACTOR => Some(Self::NpcActor),
COLLISION_GROUP_NPC_SCRIPTED => Some(Self::NpcScripted),
_ => None,
}
}
pub const fn to_raw(self) -> c_int {
match self {
Self::None => COLLISION_GROUP_NONE,
Self::Debris => COLLISION_GROUP_DEBRIS,
Self::DebrisTrigger => COLLISION_GROUP_DEBRIS_TRIGGER,
Self::InteractiveDebris => COLLISION_GROUP_INTERACTIVE_DEBRIS,
Self::Interactive => COLLISION_GROUP_INTERACTIVE,
Self::Player => COLLISION_GROUP_PLAYER,
Self::BreakableGlass => COLLISION_GROUP_BREAKABLE_GLASS,
Self::Vehicle => COLLISION_GROUP_VEHICLE,
Self::PlayerMovement => COLLISION_GROUP_PLAYER_MOVEMENT,
Self::Npc => COLLISION_GROUP_NPC,
Self::InVehicle => COLLISION_GROUP_IN_VEHICLE,
Self::Weapon => COLLISION_GROUP_WEAPON,
Self::VehicleClip => COLLISION_GROUP_VEHICLE_CLIP,
Self::Projectile => COLLISION_GROUP_PROJECTILE,
Self::DoorBlocker => COLLISION_GROUP_DOOR_BLOCKER,
Self::PassableDoor => COLLISION_GROUP_PASSABLE_DOOR,
Self::Dissolving => COLLISION_GROUP_DISSOLVING,
Self::Pushaway => COLLISION_GROUP_PUSHAWAY,
Self::NpcActor => COLLISION_GROUP_NPC_ACTOR,
Self::NpcScripted => COLLISION_GROUP_NPC_SCRIPTED,
}
}
}
#[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 unsafe fn from_live(_server: Server<'s>, raw: NonNull<sys::CBaseEntity>) -> Self {
unsafe { Self::from_raw(raw) }
}
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()
}
#[doc(alias("GetCollisionGroup", "m_CollisionGroup"))]
pub fn collision_group(self) -> Option<c_int> {
let collideable = NonNull::new(unsafe {
vcall!(self.server_entity() => IServerEntity_GetCollideable())
})?;
Some(unsafe { vcall!(collideable.as_ptr() => ICollideable_GetCollisionGroup()) })
}
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() })
})
}
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("GetOwnerEntity", "m_hOwnerEntity"))]
pub fn owner(self) -> Result<Option<EntityHandle>, OwnerError> {
static OWNER_OFFSET: OnceLock<usize> = OnceLock::new();
let offset = match OWNER_OFFSET.get() {
Some(&offset) => offset,
None => {
let offset = self
.find_base_entity_field(
c"m_hOwnerEntity",
sys::_fieldtypes_FIELD_EHANDLE,
size_of::<u32>(),
)
.ok_or(OwnerError::UnsupportedLayout)?;
*OWNER_OFFSET.get_or_init(|| offset)
}
};
let handle = EntityHandle(unsafe { self.as_ptr().byte_add(offset).cast::<u32>().read() });
Ok(handle.is_valid().then_some(handle))
}
#[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("WorldSpaceSurroundingBounds"))]
pub fn bounds(self) -> Option<(Vector, Vector)> {
let collideable = NonNull::new(unsafe {
vcall!(self.server_entity() => IServerEntity_GetCollideable())
})?;
let mut mins = sys::Vector {
x: 0.0,
y: 0.0,
z: 0.0,
};
let mut maxs = mins;
unsafe {
vcall!(collideable.as_ptr() => ICollideable_WorldSpaceSurroundingBounds(&raw mut mins, &raw mut maxs));
}
Some((mins.into(), maxs.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 }
}
#[doc(alias("SetOwnerEntity", "m_hOwnerEntity"))]
pub fn set_owner(self, owner: Option<Entity<'_>>) {
unsafe {
sdk_raw::entities::set_owner_entity(
self.as_ptr(),
owner.map_or(ptr::null_mut(), Entity::as_ptr),
)
};
}
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, Hash, thiserror::Error)]
pub enum OwnerError {
#[error("the game's CBaseEntity datamap does not declare m_hOwnerEntity as the SDK does")]
UnsupportedLayout,
}
#[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,
}