use crate::Server;
use crate::entities::{Entity, EntityHandle};
use std::fmt;
use std::mem::{MaybeUninit, offset_of};
use std::ops::{BitAnd, BitOr, BitOrAssign, Not};
use std::ptr::NonNull;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum CriticalHit {
#[default]
None = sys::CTakeDamageInfo_ECritType_CRIT_NONE as i32,
Mini = sys::CTakeDamageInfo_ECritType_CRIT_MINI as i32,
Full = sys::CTakeDamageInfo_ECritType_CRIT_FULL as i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CriticalPolicy {
pub full: bool,
pub mini: bool,
}
impl CriticalPolicy {
pub const ALLOW_ALL: Self = Self {
full: true,
mini: true,
};
pub const DISABLE_ALL: Self = Self {
full: false,
mini: false,
};
}
impl Default for CriticalPolicy {
fn default() -> Self {
Self::ALLOW_ALL
}
}
pub struct DamageInfo {
raw: MaybeUninit<sys::CTakeDamageInfo>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DamageType(pub u32);
impl DamageType {
pub const ALWAYS_GIB: Self = Self(1 << 13);
pub const BLAST: Self = Self(1 << 6);
pub const BUCKSHOT: Self = Self(1 << 29);
pub const BULLET: Self = Self(1 << 1);
pub const BURN: Self = Self(1 << 3);
pub const CLUB: Self = Self(1 << 7);
pub const CRITICAL: Self = Self(1 << 20);
pub const CRUSH: Self = Self(1 << 0);
pub const DIRECT: Self = Self(1 << 28);
pub const DROWN: Self = Self(1 << 14);
pub const FALL: Self = Self(1 << 5);
pub const GENERIC: Self = Self(0);
pub const NEVER_GIB: Self = Self(1 << 12);
pub const PREVENT_PHYSICS_FORCE: Self = Self(1 << 11);
pub const SHOCK: Self = Self(1 << 8);
pub const SLASH: Self = Self(1 << 2);
pub const USE_DISTANCE_MOD: Self = Self(1 << 21);
pub const USE_HIT_LOCATIONS: Self = Self(1 << 25);
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
}
impl BitAnd for DamageType {
type Output = Self;
fn bitand(self, rhs: Self) -> Self {
Self(self.0 & rhs.0)
}
}
impl BitOr for DamageType {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for DamageType {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl Not for DamageType {
type Output = Self;
fn not(self) -> Self {
Self(!self.0)
}
}
macro_rules! scalar {
($get:ident, $set:ident, $field:ident, $ty:ty) => {
scalar!($get, $set, $field, $ty, |_: $ty| true);
};
($get:ident, $set:ident, $field:ident, $ty:ty, $valid:expr) => {
pub fn $get(&self) -> $ty {
unsafe { (&raw const (*self.raw.as_ptr()).$field).read() }
}
pub fn $set(&mut self, value: $ty) {
assert!(
($valid)(value),
"invalid damage field: {}",
stringify!($field)
);
unsafe { (&raw mut (*self.raw.as_mut_ptr()).$field).write(value) };
}
};
}
#[derive(Debug)]
pub struct DamageEvent<'s> {
pub victim: Entity<'s>,
pub info: DamageInfo,
}
impl<'s> DamageEvent<'s> {
pub unsafe fn from_raw(
_server: Server<'s>,
victim: NonNull<sys::CBaseEntity>,
info: NonNull<sys::CTakeDamageInfo>,
) -> Self {
Self {
victim: unsafe { Entity::from_raw(victim) },
info: unsafe { DamageInfo::copy_from_raw(info) },
}
}
}
impl DamageInfo {
pub const MAX_DAMAGE: f32 = 1_000_000.0;
pub fn new(amount: f32, damage_type: DamageType) -> Self {
let mut result = Self {
raw: MaybeUninit::zeroed(),
};
result.set_amount(amount);
result.set_max_damage(amount);
result.set_base_damage(f32::MAX);
result.set_damage_type(damage_type);
result.set_attacker(EntityHandle::INVALID);
result.set_inflictor(EntityHandle::INVALID);
result.set_weapon(EntityHandle::INVALID);
result.set_bonus_provider(EntityHandle::INVALID);
result.set_ammo_type(-1);
result
}
pub unsafe fn copy_from_raw(raw: NonNull<sys::CTakeDamageInfo>) -> Self {
let mut result = Self {
raw: MaybeUninit::uninit(),
};
unsafe { std::ptr::copy_nonoverlapping(raw.as_ptr(), result.raw.as_mut_ptr(), 1) };
result
}
fn valid_damage(value: f32) -> bool {
value.is_finite() && (0.0..=Self::MAX_DAMAGE).contains(&value)
}
pub fn as_ptr(&self) -> *const sys::CTakeDamageInfo {
self.raw.as_ptr()
}
scalar!(amount, set_amount, m_flDamage, f32, Self::valid_damage);
scalar!(
max_damage,
set_max_damage,
m_flMaxDamage,
f32,
Self::valid_damage
);
scalar!(
base_damage,
set_base_damage,
m_flBaseDamage,
f32,
|value: f32| value == f32::MAX || Self::valid_damage(value)
);
scalar!(
damage_bonus,
set_damage_bonus,
m_flDamageBonus,
f32,
Self::valid_damage
);
scalar!(custom_damage, set_custom_damage, m_iDamageCustom, i32);
scalar!(ammo_type, set_ammo_type, m_iAmmoType, i32);
scalar!(
force_friendly_fire,
set_force_friendly_fire,
m_bForceFriendlyFire,
bool
);
pub fn apply_critical_policy(&mut self, policy: CriticalPolicy) -> bool {
let denied = match self.critical_hit() {
Some(CriticalHit::Full) => !policy.full,
Some(CriticalHit::Mini) => !policy.mini,
_ => false,
};
if denied {
self.set_amount((self.amount() - self.damage_bonus()).max(0.0));
self.set_damage_bonus(0.0);
self.set_bonus_provider(EntityHandle::INVALID);
self.set_incoming_critical(CriticalHit::None);
}
denied
}
pub fn attacker(&self) -> EntityHandle {
self.handle(offset_of!(sys::CTakeDamageInfo, m_hAttacker))
}
pub fn critical_hit(&self) -> Option<CriticalHit> {
match unsafe { (&raw const (*self.as_ptr()).m_eCritType).read() } {
0 => Some(CriticalHit::None),
1 => Some(CriticalHit::Mini),
2 => Some(CriticalHit::Full),
_ => None,
}
}
pub fn damage_type(&self) -> DamageType {
DamageType(unsafe { (&raw const (*self.as_ptr()).m_bitsDamageType).read() } as u32)
}
fn handle(&self, offset: usize) -> EntityHandle {
EntityHandle::from_raw(unsafe {
self.as_ptr().cast::<u8>().add(offset).cast::<u32>().read()
})
}
pub fn inflictor(&self) -> EntityHandle {
self.handle(offset_of!(sys::CTakeDamageInfo, m_hInflictor))
}
pub fn scale_amount(&mut self, factor: f32) {
self.set_amount(self.amount() * factor);
}
pub fn set_attacker(&mut self, handle: EntityHandle) {
self.set_handle(offset_of!(sys::CTakeDamageInfo, m_hAttacker), handle);
}
fn set_bonus_provider(&mut self, handle: EntityHandle) {
self.set_handle(
offset_of!(sys::CTakeDamageInfo, m_hDamageBonusProvider),
handle,
);
}
pub fn set_damage_type(&mut self, value: DamageType) {
unsafe { (&raw mut (*self.raw.as_mut_ptr()).m_bitsDamageType).write(value.0 as i32) };
}
fn set_handle(&mut self, offset: usize, handle: EntityHandle) {
assert!(
handle
.index()
.is_none_or(|index| index < EntityHandle::SLOTS),
"damage handle index exceeds the entity table"
);
unsafe {
self.raw
.as_mut_ptr()
.cast::<u8>()
.add(offset)
.cast::<u32>()
.write(handle.to_raw())
};
}
pub fn set_incoming_critical(&mut self, critical: CriticalHit) {
self.write_critical(critical);
let ordinary = self.damage_type() & !DamageType::CRITICAL;
self.set_damage_type(if critical == CriticalHit::Full {
ordinary | DamageType::CRITICAL
} else {
ordinary
});
}
pub fn set_inflictor(&mut self, handle: EntityHandle) {
self.set_handle(offset_of!(sys::CTakeDamageInfo, m_hInflictor), handle);
}
pub fn set_weapon(&mut self, handle: EntityHandle) {
self.set_handle(offset_of!(sys::CTakeDamageInfo, m_hWeapon), handle);
}
pub fn weapon(&self) -> EntityHandle {
self.handle(offset_of!(sys::CTakeDamageInfo, m_hWeapon))
}
fn write_critical(&mut self, critical: CriticalHit) {
unsafe { (&raw mut (*self.raw.as_mut_ptr()).m_eCritType).write(critical as _) };
}
}
impl Clone for DamageInfo {
fn clone(&self) -> Self {
unsafe { Self::copy_from_raw(NonNull::new(self.as_ptr().cast_mut()).unwrap()) }
}
}
impl fmt::Debug for DamageInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DamageInfo")
.field("amount", &self.amount())
.field("damage_type", &self.damage_type())
.field("critical_hit", &self.critical_hit())
.field("attacker", &self.attacker())
.field("weapon", &self.weapon())
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_editable_damage_scalars_are_bounded_except_the_base_damage_sentinel() {
let setters: [fn(&mut DamageInfo, f32); 4] = [
DamageInfo::set_amount,
DamageInfo::set_max_damage,
DamageInfo::set_base_damage,
DamageInfo::set_damage_bonus,
];
for setter in setters {
let mut damage = DamageInfo::new(10.0, DamageType::GENERIC);
for value in [
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
-1.0,
DamageInfo::MAX_DAMAGE + 1.0,
1.0e30,
] {
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| setter(
&mut damage,
value
)))
.is_err()
);
}
setter(&mut damage, 0.0);
setter(&mut damage, DamageInfo::MAX_DAMAGE);
}
let mut damage = DamageInfo::new(1.0, DamageType::GENERIC);
damage.set_base_damage(f32::MAX);
assert_eq!(damage.base_damage(), f32::MAX);
for setter in [
DamageInfo::set_amount,
DamageInfo::set_max_damage,
DamageInfo::set_damage_bonus,
] {
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| setter(
&mut damage,
f32::MAX
)))
.is_err()
);
}
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|| damage.scale_amount(f32::MAX)
))
.is_err()
);
assert_eq!(damage.amount(), 1.0);
}
#[test]
fn copies_change_owned_data_and_preserve_unknown_bits_and_handles() {
let original = DamageInfo::new(24.0, DamageType::BULLET | DamageType(1 << 31));
let mut copy = original.clone();
copy.scale_amount(2.0);
copy.set_weapon(EntityHandle::from_raw(17 | 3 << 16));
assert_eq!(copy.amount(), 48.0);
assert_eq!(original.amount(), 24.0);
assert_eq!(copy.damage_type().0, (1 << 31) | 2);
assert_eq!(original.weapon(), EntityHandle::INVALID);
assert_eq!(copy.weapon().serial_number(), 3);
}
#[test]
fn critical_policy_clamps_overlarge_recorded_bonus() {
let mut damage = DamageInfo::new(10.0, DamageType::CRITICAL);
damage.write_critical(CriticalHit::Full);
damage.set_damage_bonus(50.0);
assert!(damage.apply_critical_policy(CriticalPolicy::DISABLE_ALL));
assert_eq!(damage.amount(), 0.0);
}
#[test]
fn incoming_crit_changes_are_exclusive_and_do_not_scale_damage() {
let mut damage = DamageInfo::new(10.0, DamageType::BLAST);
for critical in [CriticalHit::Full, CriticalHit::Mini, CriticalHit::None] {
damage.set_incoming_critical(critical);
assert_eq!(damage.critical_hit(), Some(critical));
assert_eq!(
damage.damage_type().contains(DamageType::CRITICAL),
critical == CriticalHit::Full
);
assert!(damage.damage_type().contains(DamageType::BLAST));
assert_eq!(damage.amount(), 10.0);
}
}
#[test]
fn independent_policies_remove_bonus_only_once() {
for (critical, policy, denied) in [
(
CriticalHit::Full,
CriticalPolicy {
full: false,
mini: true,
},
true,
),
(
CriticalHit::Mini,
CriticalPolicy {
full: false,
mini: true,
},
false,
),
(
CriticalHit::Full,
CriticalPolicy {
full: true,
mini: false,
},
false,
),
(
CriticalHit::Mini,
CriticalPolicy {
full: true,
mini: false,
},
true,
),
] {
let mut damage = DamageInfo::new(135.0, DamageType::BULLET | DamageType::CRITICAL);
damage.write_critical(critical);
damage.set_damage_bonus(35.0);
assert_eq!(damage.apply_critical_policy(policy), denied);
assert_eq!(damage.amount(), if denied { 100.0 } else { 135.0 });
assert!(!damage.apply_critical_policy(policy));
if denied {
assert_eq!(damage.critical_hit(), Some(CriticalHit::None));
assert!(!damage.damage_type().contains(DamageType::CRITICAL));
}
}
}
#[test]
fn invalid_edits_leave_the_prior_record_intact() {
let mut damage = DamageInfo::new(10.0, DamageType::GENERIC);
for value in [f32::NAN, f32::INFINITY, -1.0, i32::MAX as f32] {
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| damage.set_amount(value)))
.is_err()
);
assert_eq!(damage.amount(), 10.0);
}
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|| damage.set_attacker(EntityHandle::from_raw(EntityHandle::SLOTS as u32))
))
.is_err()
);
assert_eq!(damage.attacker(), EntityHandle::INVALID);
}
}