#[cfg(test)]
#[path = "../tests/entities/health.rs"]
mod tests;
use crate::entities::Entity;
use crate::interfaces::ValveEngine;
use crate::server::{Game, InterfaceError, Server};
use sdk_raw::entities::health as raw;
use sdk_raw::players::{LIFE_ALIVE, LIFE_DEAD, LIFE_DISCARDBODY, LIFE_DYING, LIFE_RESPAWNABLE};
use std::ffi::{CStr, c_int};
use std::sync::OnceLock;
const BUILDING_HEALTH_LIMIT: f32 = 2_147_483_648.0;
const MAX_HEALED_HEALTH: c_int = 1 << 30;
pub const MAX_HEALING: f32 = 16_777_216.0;
static HEALTH: BaseEntityMember = BaseEntityMember::new(
c"m_iHealth",
sys::_fieldtypes_FIELD_INTEGER,
size_of::<c_int>(),
);
static LIFE_STATE: BaseEntityMember = BaseEntityMember::new(
c"m_lifeState",
sys::_fieldtypes_FIELD_CHARACTER,
size_of::<u8>(),
);
static MAX_HEALTH: BaseEntityMember = BaseEntityMember::new(
c"m_iMaxHealth",
sys::_fieldtypes_FIELD_INTEGER,
size_of::<c_int>(),
);
static TAKE_DAMAGE: BaseEntityMember = BaseEntityMember::new(
c"m_takedamage",
sys::_fieldtypes_FIELD_CHARACTER,
size_of::<u8>(),
);
struct BaseEntityMember {
name: &'static CStr,
field_type: sys::fieldtype_t,
size: usize,
offset: OnceLock<usize>,
}
impl BaseEntityMember {
const fn new(name: &'static CStr, field_type: sys::fieldtype_t, size: usize) -> Self {
Self {
name,
field_type,
size,
offset: OnceLock::new(),
}
}
fn offset(&self, entity: Entity<'_>) -> Result<usize, HealthError> {
if let Some(&offset) = self.offset.get() {
return Ok(offset);
}
let offset = entity
.find_base_entity_field(self.name, self.field_type, self.size)
.ok_or(HealthError::UnsupportedLayout { member: self.name })?;
Ok(*self.offset.get_or_init(|| offset))
}
fn read<T: Copy>(&self, entity: Entity<'_>) -> Result<T, HealthError> {
debug_assert_eq!(size_of::<T>(), self.size);
let offset = self.offset(entity)?;
Ok(unsafe { entity.as_ptr().byte_add(offset).cast::<T>().read() })
}
fn write<T: Copy>(
&self,
engine: ValveEngine<'_>,
entity: Entity<'_>,
value: T,
) -> Result<(), HealthError> {
debug_assert_eq!(size_of::<T>(), self.size);
let offset = self.offset(entity)?;
unsafe { entity.as_ptr().byte_add(offset).cast::<T>().write(value) };
entity.network_state_changed(engine, offset);
Ok(())
}
}
#[doc(alias("m_takedamage"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DamageMode {
#[doc(alias("DAMAGE_NO"))]
Immune,
#[doc(alias("DAMAGE_EVENTS_ONLY"))]
EventsOnly,
#[doc(alias("DAMAGE_YES"))]
Vulnerable,
#[doc(alias("DAMAGE_AIM"))]
Aimable,
}
impl DamageMode {
pub const fn from_raw(value: u8) -> Option<Self> {
match value {
raw::DAMAGE_NO => Some(Self::Immune),
raw::DAMAGE_EVENTS_ONLY => Some(Self::EventsOnly),
raw::DAMAGE_YES => Some(Self::Vulnerable),
raw::DAMAGE_AIM => Some(Self::Aimable),
_ => None,
}
}
pub const fn to_raw(self) -> u8 {
match self {
Self::Immune => raw::DAMAGE_NO,
Self::EventsOnly => raw::DAMAGE_EVENTS_ONLY,
Self::Vulnerable => raw::DAMAGE_YES,
Self::Aimable => raw::DAMAGE_AIM,
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum HealthError {
#[error(
"the game's CBaseEntity datamap does not declare `{}` as the SDK does",
.member.to_string_lossy()
)]
UnsupportedLayout {
member: &'static CStr,
},
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the slot of GetMaxHealth is only known for TF2")]
UnsupportedGame,
#[error("a maximum health must be positive, not {0}")]
NonPositiveMaxHealth(c_int),
#[error("healing must lie between 0 and 2^24, not {0}")]
InvalidAmount(f32),
#[error("an entity with {0} health is too healthy to heal")]
HealingOverflow(c_int),
#[error("a building's health must round below 2^31 as a float, which {0} does not")]
UnrepresentableHealth(c_int),
#[error("the entity's life state is {0}, which is no LIFE_* value")]
UnknownLifeState(u8),
#[error("the entity's damage mode is {0}, which is no DAMAGE_* value")]
UnknownDamageMode(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HealthInput {
#[doc(alias("SetHealth", "InputSetHealth"))]
Set(c_int),
#[doc(alias("AddHealth", "InputAddHealth"))]
Add(c_int),
#[doc(alias("RemoveHealth", "InputRemoveHealth"))]
Remove(c_int),
#[doc(alias("SetMaxHealth", "InputSetMaxHealth"))]
SetMax(c_int),
}
impl HealthInput {
pub const fn amount(self) -> c_int {
match self {
Self::Set(amount) | Self::Add(amount) | Self::Remove(amount) | Self::SetMax(amount) => {
amount
}
}
}
pub const fn name(self) -> &'static CStr {
match self {
Self::Set(_) => c"SetHealth",
Self::Add(_) => c"AddHealth",
Self::Remove(_) => c"RemoveHealth",
Self::SetMax(_) => c"SetMaxHealth",
}
}
}
#[doc(alias("m_lifeState"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LifeState {
#[doc(alias("LIFE_ALIVE"))]
Alive,
#[doc(alias("LIFE_DYING"))]
Dying,
#[doc(alias("LIFE_DEAD"))]
Dead,
#[doc(alias("LIFE_RESPAWNABLE"))]
Respawnable,
#[doc(alias("LIFE_DISCARDBODY"))]
DiscardBody,
}
impl LifeState {
pub const fn from_raw(value: u8) -> Option<Self> {
match value {
LIFE_ALIVE => Some(Self::Alive),
LIFE_DYING => Some(Self::Dying),
LIFE_DEAD => Some(Self::Dead),
LIFE_RESPAWNABLE => Some(Self::Respawnable),
LIFE_DISCARDBODY => Some(Self::DiscardBody),
_ => None,
}
}
pub const fn to_raw(self) -> u8 {
match self {
Self::Alive => LIFE_ALIVE,
Self::Dying => LIFE_DYING,
Self::Dead => LIFE_DEAD,
Self::Respawnable => LIFE_RESPAWNABLE,
Self::DiscardBody => LIFE_DISCARDBODY,
}
}
}
impl<'s> Entity<'s> {
#[doc(alias("m_takedamage"))]
pub fn damage_mode(self) -> Result<DamageMode, HealthError> {
let value = TAKE_DAMAGE.read::<u8>(self)?;
DamageMode::from_raw(value).ok_or(HealthError::UnknownDamageMode(value))
}
#[doc(alias("TakeHealth"))]
pub fn heal(self, server: Server<'_>, amount: f32) -> Result<c_int, HealthError> {
self.take_health_raw(server, amount, raw::DMG_GENERIC)
}
#[doc(alias("GetHealth", "m_iHealth"))]
pub fn health(self) -> Result<c_int, HealthError> {
HEALTH.read(self)
}
#[doc(alias("IsAlive"))]
pub fn is_alive(self) -> bool {
unsafe { raw::is_alive(self.as_ptr()) }
}
#[doc(alias("m_lifeState"))]
pub fn life_state(self) -> Result<LifeState, HealthError> {
let value = LIFE_STATE.read::<u8>(self)?;
LifeState::from_raw(value).ok_or(HealthError::UnknownLifeState(value))
}
#[doc(alias("GetMaxHealth"))]
pub fn max_health(self, server: Server<'_>) -> Result<c_int, HealthError> {
match server.game() {
Game::TeamFortress2 => Ok(unsafe { raw::get_max_health(self.as_ptr()) }),
Game::SourceSdk2013 | Game::SourceSdk2013NextBot => Err(HealthError::UnsupportedGame),
}
}
pub(super) fn network_state_changed(self, engine: ValveEngine<'_>, offset: usize) {
let Some(edict) = self.edict() else {
return;
};
match u16::try_from(offset) {
Ok(offset) => edict.state_changed(engine, offset),
Err(_) => edict.full_state_changed(engine),
}
}
#[doc(alias("m_takedamage"))]
pub fn set_damage_mode(self, server: Server<'_>, mode: DamageMode) -> Result<(), HealthError> {
TAKE_DAMAGE.write(server.valve_engine()?, self, mode.to_raw())
}
#[doc(alias("SetHealth", "m_iHealth", "health"))]
pub fn set_health(self, server: Server<'_>, health: c_int) -> Result<(), HealthError> {
let engine = server.valve_engine()?;
let Some(building) = self.tf2_building(server) else {
return HEALTH.write(engine, self, health);
};
let float = health as f32;
if float >= BUILDING_HEALTH_LIMIT {
return Err(HealthError::UnrepresentableHealth(health));
}
HEALTH.offset(self)?;
unsafe { (&raw mut (*building).m_flHealth).write(float) };
HEALTH.write(engine, self, float.ceil() as c_int)
}
#[doc(alias("m_lifeState"))]
pub fn set_life_state(self, server: Server<'_>, state: LifeState) -> Result<(), HealthError> {
LIFE_STATE.write(server.valve_engine()?, self, state.to_raw())
}
#[doc(alias("SetMaxHealth", "m_iMaxHealth", "max_health"))]
pub fn set_max_health(self, server: Server<'_>, max_health: c_int) -> Result<(), HealthError> {
if max_health <= 0 {
return Err(HealthError::NonPositiveMaxHealth(max_health));
}
if max_health as f32 >= BUILDING_HEALTH_LIMIT && self.tf2_building(server).is_some() {
return Err(HealthError::UnrepresentableHealth(max_health));
}
MAX_HEALTH.write(server.valve_engine()?, self, max_health)
}
#[doc(alias("m_iMaxHealth", "max_health"))]
pub fn stored_max_health(self) -> Result<c_int, HealthError> {
MAX_HEALTH.read(self)
}
#[cfg(feature = "tf2")]
#[cfg_attr(docsrs, doc(cfg(feature = "tf2")))]
#[doc(alias("TakeHealth"))]
pub fn take_health(
self,
server: Server<'_>,
amount: f32,
damage_type: crate::tf2::damage::DamageType,
) -> Result<c_int, HealthError> {
self.take_health_raw(server, amount, damage_type.bits().cast_signed())
}
fn take_health_raw(
self,
server: Server<'_>,
amount: f32,
damage_type: c_int,
) -> Result<c_int, HealthError> {
if !(0.0..=MAX_HEALING).contains(&amount) {
return Err(HealthError::InvalidAmount(amount));
}
let health = HEALTH.read::<c_int>(self)?;
if health > MAX_HEALED_HEALTH {
return Err(HealthError::HealingOverflow(health));
}
let gained = unsafe { raw::take_health(self.as_ptr(), amount, damage_type) };
if let Some(building) = self.tf2_building(server) {
let healed = HEALTH.read::<c_int>(self)?;
if healed != health {
unsafe { (&raw mut (*building).m_flHealth).write(healed as f32) };
}
}
Ok(gained)
}
fn tf2_building(self, server: Server<'_>) -> Option<*mut sys::CBaseObject> {
(server.game() == Game::TeamFortress2 && self.has_data_map_class(c"CBaseObject"))
.then(|| self.as_ptr().cast())
}
}