use crate::datatables::{NetProp, NetPropError, NetValue, NetVar};
use crate::entities::{CollisionGroup, Entity};
use crate::inputs::{InputError, InputValue};
use crate::interfaces::ServerTools;
use crate::math::Vector;
use crate::tf2::PlayerClass;
use crate::tf2::damage::{DamageInfo, DamageType};
use crate::tf2::teams::Team;
use crate::{Game, InterfaceError, Server};
use sdk_raw::tf2::damage::DMG_VEHICLE;
use sdk_raw::tf2::ragdolls::{self as raw, RAGDOLL_MAX_ELEMENTS};
use std::ffi::{CStr, c_int};
use std::ptr::NonNull;
const ENABLE_INPUT: &CStr = c"Enable";
#[derive(Debug, thiserror::Error)]
pub enum RagdollError {
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the entity is marked for deletion")]
MarkedForDeletion,
#[error(transparent)]
ModelIndex(#[from] NetPropError),
#[error("the entity's model has no collision model a ragdoll can be made from")]
NoCollisionModel,
#[error("the entity is not an animating entity")]
NotAnimating,
#[error("the game created no ragdoll")]
NotCreated,
#[error("the new ragdoll could not be shown")]
NotShown(#[source] InputError),
#[error("the game's CreateServerRagdoll could not be found")]
Unresolved,
#[error("server ragdolls require a TF2 server")]
UnsupportedGame,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RagdollFlag {
#[doc(alias("m_bBecomeAsh"))]
BecomeAsh,
#[doc(alias("m_bBurning"))]
Burning,
#[doc(alias("m_bCloaked"))]
Cloaked,
#[doc(alias("m_bCritOnHardHit"))]
CritOnHardHit,
#[doc(alias("m_bElectrocuted"))]
Electrocuted,
#[doc(alias("m_bFeignDeath"))]
FeignDeath,
#[doc(alias("m_bGib"))]
Gib,
#[doc(alias("m_bGoldRagdoll"))]
Gold,
#[doc(alias("m_bIceRagdoll"))]
Ice,
#[doc(alias("m_bOnGround"))]
OnGround,
#[doc(alias("m_bWasDisguised"))]
WasDisguised,
}
impl RagdollFlag {
pub const ALL: [Self; 11] = [
Self::BecomeAsh,
Self::Burning,
Self::Cloaked,
Self::CritOnHardHit,
Self::Electrocuted,
Self::FeignDeath,
Self::Gib,
Self::Gold,
Self::Ice,
Self::OnGround,
Self::WasDisguised,
];
pub const fn net_prop_name(self) -> &'static CStr {
match self {
Self::BecomeAsh => c"m_bBecomeAsh",
Self::Burning => c"m_bBurning",
Self::Cloaked => c"m_bCloaked",
Self::CritOnHardHit => c"m_bCritOnHardHit",
Self::Electrocuted => c"m_bElectrocuted",
Self::FeignDeath => c"m_bFeignDeath",
Self::Gib => c"m_bGib",
Self::Gold => c"m_bGoldRagdoll",
Self::Ice => c"m_bIceRagdoll",
Self::OnGround => c"m_bOnGround",
Self::WasDisguised => c"m_bWasDisguised",
}
}
}
#[doc(alias("CreateServerRagdoll"))]
#[derive(Debug, Clone, Copy)]
pub struct ServerRagdolls<'s> {
raw: raw::ServerRagdolls,
server: Server<'s>,
}
impl<'s> ServerRagdolls<'s> {
pub fn new(server: Server<'s>) -> Result<Self, RagdollError> {
if server.game() != Game::TeamFortress2 {
return Err(RagdollError::UnsupportedGame);
}
let raw = unsafe { raw::ServerRagdolls::cached(server.game_server_factory().as_raw()) }
.map_err(|_| RagdollError::Unresolved)?;
Ok(Self { raw, server })
}
#[doc(alias("CreateServerRagdoll"))]
pub fn create_server_ragdoll(
self,
animating: Entity<'s>,
force_bone: Option<usize>,
info: &DamageInfo,
group: CollisionGroup,
) -> Result<Entity<'s>, RagdollError> {
if animating.is_marked_for_deletion() {
return Err(RagdollError::MarkedForDeletion);
}
if !animating
.server_class()
.and_then(|class| class.table())
.is_some_and(|table| table.derives_from_named(c"DT_BaseAnimating"))
{
return Err(RagdollError::NotAnimating);
}
let tools = self.server.server_tools()?;
let model_info = self.server.model_info()?;
let NetValue::Int(model) = self
.server
.server_game_dll()?
.entity_net_prop(animating, c"m_nModelIndex")?
.value(animating)?
else {
return Err(RagdollError::NoCollisionModel);
};
let solids = model_info
.vcollide_solid_count(model)
.filter(|solids| (1..=RAGDOLL_MAX_ELEMENTS).contains(solids))
.filter(|_| model_info.studio_name(model).is_some())
.ok_or(RagdollError::NoCollisionModel)?;
let force_bone = force_bone
.and_then(|bone| c_int::try_from(bone).ok())
.filter(|bone| (0..solids).contains(bone))
.unwrap_or(raw::NO_FORCE_BONE);
let mut info = info.clone();
info.set_damage_type(info.damage_type() - DamageType::from_bits_retain(DMG_VEHICLE as u32));
let ragdoll = unsafe {
self.raw.create(
NonNull::new(animating.as_ptr()).unwrap().cast(),
force_bone,
NonNull::new(info.as_ptr().cast_mut()).unwrap(),
group.to_raw(),
false,
)
}
.ok_or(RagdollError::NotCreated)?;
let ragdoll = unsafe { Entity::from_raw(ragdoll) };
show(tools, ragdoll)?;
Ok(ragdoll)
}
}
#[doc(alias("tf_ragdoll", "CTFRagdoll"))]
#[derive(Debug, Clone, Copy)]
pub struct TfRagdoll<'s> {
server: Server<'s>,
ragdoll: Entity<'s>,
}
impl<'s> TfRagdoll<'s> {
pub fn new(server: Server<'s>, ragdoll: Entity<'s>) -> Result<Self, TfRagdollError> {
if server.game() != Game::TeamFortress2
|| !ragdoll
.server_class()
.is_some_and(|class| class.name() == c"CTFRagdoll")
{
return Err(TfRagdollError::NotTfRagdoll);
}
Ok(Self { server, ragdoll })
}
#[doc(alias("m_iClass"))]
pub fn class(self) -> Result<Option<PlayerClass>, TfRagdollError> {
match self.get::<c_int>(c"m_iClass")? {
0 => Ok(None),
raw => PlayerClass::from_raw(raw)
.map(Some)
.ok_or(TfRagdollError::UnknownClass(raw)),
}
}
#[doc(alias("m_iDamageCustom"))]
pub fn damage_custom(self) -> Result<c_int, TfRagdollError> {
self.get(c"m_iDamageCustom")
}
pub const fn entity(self) -> Entity<'s> {
self.ragdoll
}
pub fn flag(self, flag: RagdollFlag) -> Result<bool, TfRagdollError> {
self.get(flag.net_prop_name())
}
#[doc(alias("m_vecForce"))]
pub fn force(self) -> Result<Vector, TfRagdollError> {
self.get(c"m_vecForce")
}
#[doc(alias("m_nForceBone"))]
pub fn force_bone(self) -> Result<c_int, TfRagdollError> {
self.get(c"m_nForceBone")
}
fn get<T: NetVar>(self, name: &CStr) -> Result<T, TfRagdollError> {
Ok(self.net_prop(name)?.get(self.ragdoll)?)
}
#[doc(alias("m_flHandScale"))]
pub fn hand_scale(self) -> Result<f32, TfRagdollError> {
self.get(c"m_flHandScale")
}
#[doc(alias("m_flHeadScale"))]
pub fn head_scale(self) -> Result<f32, TfRagdollError> {
self.get(c"m_flHeadScale")
}
fn net_prop(self, name: &CStr) -> Result<NetProp<'s>, TfRagdollError> {
Ok(self
.server
.server_game_dll()?
.entity_net_prop(self.ragdoll, name)?)
}
#[doc(alias("m_vecRagdollOrigin"))]
pub fn origin(self) -> Result<Vector, TfRagdollError> {
self.get(c"m_vecRagdollOrigin")
}
#[doc(alias("m_hPlayer"))]
pub fn player(self) -> Result<Option<Entity<'s>>, TfRagdollError> {
let handle = self.net_prop(c"m_hPlayer")?.get_handle(self.ragdoll)?;
Ok(self.server.server_tools()?.entity_by_handle(handle))
}
pub fn set_flag(self, flag: RagdollFlag, value: bool) -> Result<(), TfRagdollError> {
if self.ragdoll.is_marked_for_deletion() {
return Err(TfRagdollError::MarkedForDeletion);
}
let engine = self.server.valve_engine()?;
unsafe {
self.net_prop(flag.net_prop_name())?
.set(engine, self.ragdoll, value)
}?;
Ok(())
}
#[doc(alias("m_iTeam"))]
pub fn team(self) -> Result<Team, TfRagdollError> {
let raw = self.get::<c_int>(c"m_iTeam")?;
Team::from_raw(raw).ok_or(TfRagdollError::UnknownTeam(raw))
}
#[doc(alias("m_flTorsoScale"))]
pub fn torso_scale(self) -> Result<f32, TfRagdollError> {
self.get(c"m_flTorsoScale")
}
#[doc(alias("m_vecRagdollVelocity"))]
pub fn velocity(self) -> Result<Vector, TfRagdollError> {
self.get(c"m_vecRagdollVelocity")
}
}
#[derive(Debug, thiserror::Error)]
pub enum TfRagdollError {
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the ragdoll is marked for deletion")]
MarkedForDeletion,
#[error(transparent)]
NetProp(#[from] NetPropError),
#[error("the entity is not a tf_ragdoll")]
NotTfRagdoll,
#[error("the ragdoll's class {0} is not one of TF2's playable classes")]
UnknownClass(c_int),
#[error("the ragdoll's team {0} is not one of TF2's")]
UnknownTeam(c_int),
}
fn show(tools: ServerTools<'_>, ragdoll: Entity<'_>) -> Result<(), RagdollError> {
if let Err(error) =
tools.accept_input(ragdoll, ENABLE_INPUT, InputValue::Void, ragdoll, ragdoll)
{
let _ = tools.remove(ragdoll);
return Err(RagdollError::NotShown(error));
}
Ok(())
}