Skip to main content

source_sdk_2013/tf2/
ragdolls.rs

1//! Server-side ragdolls: `prop_ragdoll`s whose physics the server simulates,
2//! made from an animating entity's current pose, as other Source games make
3//! their NPCs' corpses.
4//!
5//! [`ServerRagdolls::create_server_ragdoll`] calls the game's own
6//! `CreateServerRagdoll` (`game/server/physics_prop_ragdoll.cpp`), which TF2
7//! does not export, and which [`ServerRagdolls::new`] finds in the game server
8//! module, as [`sdk_raw::tf2::ragdolls`] describes. The ragdoll takes the
9//! entity's model, skin, body groups and pose, and its velocity, before a
10//! death's damage force pushes it.
11//!
12//! # Lifetime
13//!
14//! The ragdolls are made without the game's retirement of old ragdolls, so
15//! each stays until it is removed: by [`ServerTools::remove`], by the round's
16//! cleanup of the map's entities, or by a level change. Each holds an edict
17//! for as long, and a ragdoll that falls out of the world never comes to rest.
18//! Running out of edicts ends the server with a fatal error, so callers that
19//! keep ragdolls should budget them against
20//! [`ValveEngine::entity_count`](crate::interfaces::ValveEngine::entity_count).
21//!
22//! Clients fade a ragdoll out with distance as its `fadescale` key value says,
23//! which the game sets to 1. A key value of 0 keeps it drawn at any distance.
24//!
25//! # TF2's players
26//!
27//! A TF2 player becomes a `tf_ragdoll` instead, from which each client makes
28//! its own ragdoll or gibs, with the effects of the death such as burning or
29//! decapitation. A server ragdoll of a player has none of these, nor the
30//! player's cosmetics. The game's client ragdoll is not removed: remove the
31//! player's `tf_ragdoll` (`m_hRagdoll`) to leave only the server ragdoll, but
32//! never point `m_hRagdoll` at a server ragdoll, which TF2's clients take to
33//! be a `tf_ragdoll` unchecked.
34//!
35//! [`TfRagdoll`] reads a `tf_ragdoll`, and sets its [`RagdollFlag`]s before
36//! clients make their ragdoll from it.
37//! [`TfPlayer::ragdoll`](crate::tf2::player::TfPlayer::ragdoll) finds a
38//! player's, and
39//! [`TfPlayer::remove_ragdoll`](crate::tf2::player::TfPlayer::remove_ragdoll)
40//! removes it.
41
42use crate::datatables::{NetProp, NetPropError, NetValue, NetVar};
43use crate::entities::{CollisionGroup, Entity};
44use crate::inputs::{InputError, InputValue};
45use crate::interfaces::ServerTools;
46use crate::math::Vector;
47use crate::tf2::PlayerClass;
48use crate::tf2::damage::{DamageInfo, DamageType};
49use crate::tf2::teams::Team;
50use crate::{Game, InterfaceError, Server};
51use sdk_raw::tf2::damage::DMG_VEHICLE;
52use sdk_raw::tf2::ragdolls::{self as raw, RAGDOLL_MAX_ELEMENTS};
53use std::ffi::{CStr, c_int};
54use std::ptr::NonNull;
55
56/// The input that shows a `prop_ragdoll` (`CRagdollProp::InputTurnOn`), by
57/// clearing `EF_NODRAW`, which also has the engine send it to clients again.
58const ENABLE_INPUT: &CStr = c"Enable";
59
60/// Why a server ragdoll could not be created.
61#[derive(#[automatically_derived]
impl ::core::fmt::Debug for RagdollError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            Self::Interface(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "Interface", &__self_0),
            Self::MarkedForDeletion =>
                ::core::fmt::Formatter::write_str(f, "MarkedForDeletion"),
            Self::ModelIndex(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "ModelIndex", &__self_0),
            Self::NoCollisionModel =>
                ::core::fmt::Formatter::write_str(f, "NoCollisionModel"),
            Self::NotAnimating =>
                ::core::fmt::Formatter::write_str(f, "NotAnimating"),
            Self::NotCreated =>
                ::core::fmt::Formatter::write_str(f, "NotCreated"),
            Self::NotShown(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "NotShown", &__self_0),
            Self::Unresolved =>
                ::core::fmt::Formatter::write_str(f, "Unresolved"),
            Self::UnsupportedGame =>
                ::core::fmt::Formatter::write_str(f, "UnsupportedGame"),
        }
    }
}Debug, #[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private21::Error for RagdollError {
    fn source(&self)
        ->
            ::core::option::Option<&(dyn ::thiserror::__private21::Error +
            'static)> {
        use ::thiserror::__private21::AsDynError as _;

        #[allow(deprecated)]
        match self {
            RagdollError::Interface { 0: transparent } =>
                ::thiserror::__private21::Error::source(transparent.as_dyn_error()),
            RagdollError::MarkedForDeletion { .. } =>
                ::core::option::Option::None,
            RagdollError::ModelIndex { 0: transparent } =>
                ::thiserror::__private21::Error::source(transparent.as_dyn_error()),
            RagdollError::NoCollisionModel { .. } =>
                ::core::option::Option::None,
            RagdollError::NotAnimating { .. } => ::core::option::Option::None,
            RagdollError::NotCreated { .. } => ::core::option::Option::None,
            RagdollError::NotShown { 0: source, .. } =>
                ::core::option::Option::Some(source.as_dyn_error()),
            RagdollError::Unresolved { .. } => ::core::option::Option::None,
            RagdollError::UnsupportedGame { .. } =>
                ::core::option::Option::None,
        }
    }
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for RagdollError {
    fn fmt(&self, __formatter: &mut ::core::fmt::Formatter)
        -> ::core::fmt::Result {

        #[allow(unused_variables, deprecated, clippy ::
        used_underscore_binding)]
        match self {
            RagdollError::Interface(_0) =>
                ::core::fmt::Display::fmt(_0, __formatter),
            RagdollError::MarkedForDeletion {} =>
                __formatter.write_str("the entity is marked for deletion"),
            RagdollError::ModelIndex(_0) =>
                ::core::fmt::Display::fmt(_0, __formatter),
            RagdollError::NoCollisionModel {} =>
                __formatter.write_str("the entity's model has no collision model a ragdoll can be made from"),
            RagdollError::NotAnimating {} =>
                __formatter.write_str("the entity is not an animating entity"),
            RagdollError::NotCreated {} =>
                __formatter.write_str("the game created no ragdoll"),
            RagdollError::NotShown(_0) =>
                __formatter.write_str("the new ragdoll could not be shown"),
            RagdollError::Unresolved {} =>
                __formatter.write_str("the game's CreateServerRagdoll could not be found"),
            RagdollError::UnsupportedGame {} =>
                __formatter.write_str("server ragdolls require a TF2 server"),
        }
    }
}
RagdollError
#[allow(clippy :: redundant_field_names)]
fn from(source: InterfaceError) -> Self {
    RagdollError::Interface { 0: source }
}
RagdollError
#[allow(clippy :: redundant_field_names)]
fn from(source: NetPropError) -> Self {
    RagdollError::ModelIndex { 0: source }
}thiserror::Error)]
62pub enum RagdollError {
63	/// A required engine interface is unavailable.
64	#[error(transparent)]
65	Interface(#[from] InterfaceError),
66
67	/// The entity is marked for deletion.
68	#[error("the entity is marked for deletion")]
69	MarkedForDeletion,
70
71	/// The entity's model index could not be read.
72	#[error(transparent)]
73	ModelIndex(#[from] NetPropError),
74
75	/// The entity's model is not a studio model with a collision model of 1 to
76	/// [`RAGDOLL_MAX_ELEMENTS`] solids, from which the game would make a
77	/// ragdoll without physics.
78	#[error("the entity's model has no collision model a ragdoll can be made from")]
79	NoCollisionModel,
80
81	/// The entity is not a `CBaseAnimating`: its networked class does not
82	/// derive from `DT_BaseAnimating`.
83	#[error("the entity is not an animating entity")]
84	NotAnimating,
85
86	/// `CreateServerRagdoll` returned no ragdoll.
87	#[error("the game created no ragdoll")]
88	NotCreated,
89
90	/// The new ragdoll refused the input that shows it, so it was removed
91	/// again.
92	#[error("the new ragdoll could not be shown")]
93	NotShown(#[source] InputError),
94
95	/// The game server module's `CreateServerRagdoll` was not found, as
96	/// [`sdk_raw::tf2::ragdolls`] describes.
97	#[error("the game's CreateServerRagdoll could not be found")]
98	Unresolved,
99
100	/// The server does not run TF2.
101	#[error("server ragdolls require a TF2 server")]
102	UnsupportedGame,
103}
104
105/// A flag of a [`TfRagdoll`], which decides how clients make the ragdoll of a
106/// player's death.
107#[derive(#[automatically_derived]
impl ::core::fmt::Debug for RagdollFlag {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f,
            match self {
                RagdollFlag::BecomeAsh => "BecomeAsh",
                RagdollFlag::Burning => "Burning",
                RagdollFlag::Cloaked => "Cloaked",
                RagdollFlag::CritOnHardHit => "CritOnHardHit",
                RagdollFlag::Electrocuted => "Electrocuted",
                RagdollFlag::FeignDeath => "FeignDeath",
                RagdollFlag::Gib => "Gib",
                RagdollFlag::Gold => "Gold",
                RagdollFlag::Ice => "Ice",
                RagdollFlag::OnGround => "OnGround",
                RagdollFlag::WasDisguised => "WasDisguised",
            })
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl ::core::clone::TrivialClone for RagdollFlag { }
#[automatically_derived]
impl ::core::clone::Clone for RagdollFlag {
    #[inline]
    fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for RagdollFlag { }Copy, #[automatically_derived]
impl ::core::marker::StructuralPartialEq for RagdollFlag { }
#[automatically_derived]
impl ::core::cmp::PartialEq for RagdollFlag {
    #[inline]
    fn eq(&self, other: &Self) -> bool {
        ::core::intrinsics::discriminant_value(self) ==
            ::core::intrinsics::discriminant_value(other)
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for RagdollFlag { }Eq, #[automatically_derived]
impl ::core::hash::Hash for RagdollFlag {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
        ::core::hash::Hash::hash(&::core::intrinsics::discriminant_value(self),
            state)
    }
}Hash)]
108pub enum RagdollFlag {
109	/// Burns away to ash, as the victims of some of the Pyro's weapons do
110	/// (`m_bBecomeAsh`).
111	#[doc(alias("m_bBecomeAsh"))]
112	BecomeAsh,
113
114	/// Burns (`m_bBurning`).
115	#[doc(alias("m_bBurning"))]
116	Burning,
117
118	/// Fades out as a cloaked Spy does (`m_bCloaked`).
119	#[doc(alias("m_bCloaked"))]
120	Cloaked,
121
122	/// Bursts in a red mist as it gibs, as the victims of the Classic's
123	/// critical hits do (`m_bCritOnHardHit`).
124	#[doc(alias("m_bCritOnHardHit"))]
125	CritOnHardHit,
126
127	/// Crackles with electricity (`m_bElectrocuted`).
128	#[doc(alias("m_bElectrocuted"))]
129	Electrocuted,
130
131	/// A Spy's feigned death (`m_bFeignDeath`).
132	#[doc(alias("m_bFeignDeath"))]
133	FeignDeath,
134
135	/// Bursts into gibs instead of a ragdoll (`m_bGib`).
136	#[doc(alias("m_bGib"))]
137	Gib,
138
139	/// Turns to gold, as the victims of golden weapons do (`m_bGoldRagdoll`).
140	#[doc(alias("m_bGoldRagdoll"))]
141	Gold,
142
143	/// Freezes into an ice statue, as the Spy-cicle's victims do
144	/// (`m_bIceRagdoll`).
145	#[doc(alias("m_bIceRagdoll"))]
146	Ice,
147
148	/// The player died on the ground (`m_bOnGround`). Clients play most death
149	/// animations only for such a ragdoll.
150	#[doc(alias("m_bOnGround"))]
151	OnGround,
152
153	/// The player was disguised (`m_bWasDisguised`), so a feigned death shows
154	/// the disguise.
155	#[doc(alias("m_bWasDisguised"))]
156	WasDisguised,
157}
158
159impl RagdollFlag {
160	/// Every flag.
161	pub const ALL: [Self; 11] = [
162		Self::BecomeAsh,
163		Self::Burning,
164		Self::Cloaked,
165		Self::CritOnHardHit,
166		Self::Electrocuted,
167		Self::FeignDeath,
168		Self::Gib,
169		Self::Gold,
170		Self::Ice,
171		Self::OnGround,
172		Self::WasDisguised,
173	];
174
175	/// The name of the flag's networked variable.
176	pub const fn net_prop_name(self) -> &'static CStr {
177		match self {
178			Self::BecomeAsh => c"m_bBecomeAsh",
179			Self::Burning => c"m_bBurning",
180			Self::Cloaked => c"m_bCloaked",
181			Self::CritOnHardHit => c"m_bCritOnHardHit",
182			Self::Electrocuted => c"m_bElectrocuted",
183			Self::FeignDeath => c"m_bFeignDeath",
184			Self::Gib => c"m_bGib",
185			Self::Gold => c"m_bGoldRagdoll",
186			Self::Ice => c"m_bIceRagdoll",
187			Self::OnGround => c"m_bOnGround",
188			Self::WasDisguised => c"m_bWasDisguised",
189		}
190	}
191}
192
193/// TF2's `CreateServerRagdoll`, within the current engine callback.
194#[doc(alias("CreateServerRagdoll"))]
195#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for ServerRagdolls<'s> {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f,
            "ServerRagdolls", "raw", &self.raw, "server", &&self.server)
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for ServerRagdolls<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for ServerRagdolls<'s> {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<raw::ServerRagdolls>;
        let _: ::core::clone::AssertParamIsClone<Server<'s>>;
        *self
    }
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for ServerRagdolls<'s> { }Copy)]
196pub struct ServerRagdolls<'s> {
197	raw: raw::ServerRagdolls,
198	server: Server<'s>,
199}
200
201impl<'s> ServerRagdolls<'s> {
202	/// Finds `CreateServerRagdoll` in the game server module. Fails with
203	/// [`RagdollError::UnsupportedGame`] outside TF2, and with
204	/// [`RagdollError::Unresolved`] if it is not found.
205	///
206	/// Once a call has found it, later calls reuse it without inspecting the
207	/// module again, as
208	/// [`ServerRagdolls::cached`](sdk_raw::tf2::ragdolls::ServerRagdolls::cached)
209	/// describes. This relies on Source never unloading that module while
210	/// plugins are loaded. The first call inspects the whole module, which on
211	/// Windows means scanning its code, so make it from a callback where that
212	/// pause does not matter, such as the plugin's load or a level start.
213	pub fn new(server: Server<'s>) -> Result<Self, RagdollError> {
214		if server.game() != Game::TeamFortress2 {
215			return Err(RagdollError::UnsupportedGame);
216		}
217
218		// SAFETY: `Server::new` guarantees that the game server module, whose
219		// factory this is, stays loaded through the callback. A cached resolution
220		// for the same factory and module base was made in this same image: Source
221		// never unloads the game server module while plugins are loaded, since
222		// Metamod:Source and the engine unload plugins first, and the cache, a
223		// static of this plugin, is unloaded with it.
224		let raw = unsafe { raw::ServerRagdolls::cached(server.game_server_factory().as_raw()) }
225			.map_err(|_| RagdollError::Unresolved)?;
226
227		Ok(Self { raw, server })
228	}
229
230	/// Creates a permanent server ragdoll of `animating` in its current pose,
231	/// pushed by `info`'s damage force, and shows it.
232	///
233	/// The force is applied to the physics object of `force_bone`, a solid of
234	/// the model's collision model such as a dead player's `m_nForceBone`, and
235	/// spread over the others from that object's position. With `None`, or a
236	/// bone the collision model does not have, it is spread over every object
237	/// from `info`'s damage position, unless that is the origin, which leaves
238	/// the ragdoll unpushed. `info`'s `DMG_VEHICLE` bit is ignored, which would
239	/// take a path of the game's meant for its vehicles and NPCs.
240	///
241	/// The ragdoll is put in `group`, such as [`CollisionGroup::Debris`], with
242	/// which players, bullets, projectiles and other ragdolls pass through it,
243	/// and has `animating` as its owner. It copies `animating`'s effects, such
244	/// as a dead TF2 player's `EF_NODRAW`, which would keep the engine from
245	/// sending it to clients, so this sends it the `Enable` input, which clears
246	/// `EF_NODRAW`: the ragdoll is always shown. If it refuses the input, it is
247	/// removed again and [`RagdollError::NotShown`] returned.
248	///
249	/// Fails with [`RagdollError::MarkedForDeletion`] or
250	/// [`RagdollError::NotAnimating`] for such an `animating`, and with
251	/// [`RagdollError::NoCollisionModel`] unless its model is a studio model
252	/// whose collision model has 1 to [`RAGDOLL_MAX_ELEMENTS`] solids. The
253	/// [module documentation](self#lifetime) describes how long the ragdoll
254	/// stays.
255	#[doc(alias("CreateServerRagdoll"))]
256	pub fn create_server_ragdoll(
257		self,
258		animating: Entity<'s>,
259		force_bone: Option<usize>,
260		info: &DamageInfo,
261		group: CollisionGroup,
262	) -> Result<Entity<'s>, RagdollError> {
263		if animating.is_marked_for_deletion() {
264			return Err(RagdollError::MarkedForDeletion);
265		}
266
267		if !animating
268			.server_class()
269			.and_then(|class| class.table())
270			.is_some_and(|table| table.derives_from_named(c"DT_BaseAnimating"))
271		{
272			return Err(RagdollError::NotAnimating);
273		}
274
275		let tools = self.server.server_tools()?;
276		let model_info = self.server.model_info()?;
277
278		let NetValue::Int(model) = self
279			.server
280			.server_game_dll()?
281			.entity_net_prop(animating, c"m_nModelIndex")?
282			.value(animating)?
283		else {
284			return Err(RagdollError::NoCollisionModel);
285		};
286
287		// The game reads the bones of the studio model, and makes a physics
288		// object for each solid of its collision model.
289		let solids = model_info
290			.vcollide_solid_count(model)
291			.filter(|solids| (1..=RAGDOLL_MAX_ELEMENTS).contains(solids))
292			.filter(|_| model_info.studio_name(model).is_some())
293			.ok_or(RagdollError::NoCollisionModel)?;
294
295		let force_bone = force_bone
296			.and_then(|bone| c_int::try_from(bone).ok())
297			.filter(|bone| (0..solids).contains(bone))
298			.unwrap_or(raw::NO_FORCE_BONE);
299
300		let mut info = info.clone();
301
302		info.set_damage_type(info.damage_type() - DamageType::from_bits_retain(DMG_VEHICLE as u32));
303
304		// SAFETY: The game server module stays loaded through the callback, which
305		// runs on the main thread (conditions 1 and 3 of `Server::new`), and the
306		// game creates entities in plugin callbacks, none of which is known to
307		// run within VPhysics' simulation or its callbacks, from which the game
308		// defers its damage and removals. `animating` is live during `'s`, not
309		// marked for deletion, and a `CBaseAnimating`, whose networked class
310		// derives from `DT_BaseAnimating`, at the address of its `CBaseEntity`,
311		// as `sdk_raw::tf2` asserts for the bases of TF2's players. Its model is
312		// a studio model with 1 to `RAGDOLL_MAX_ELEMENTS` solids. The damage copy
313		// lives through the call. `CreateServerRagdoll` creates the ragdoll
314		// without spawning it (`CreateNoSpawn`), copies the model and effects,
315		// sets its owner, sets up `animating`'s bones twice, creates the physics
316		// objects (`InitRagdoll`), creates and spawns an `env_entity_dissolver`
317		// if `animating` is dissolving, and sets the collision bounds. None of
318		// this frees an entity, so condition 4 holds.
319		let ragdoll = unsafe {
320			self.raw.create(
321				NonNull::new(animating.as_ptr()).unwrap().cast(),
322				force_bone,
323				NonNull::new(info.as_ptr().cast_mut()).unwrap(),
324				group.to_raw(),
325				false,
326			)
327		}
328		.ok_or(RagdollError::NotCreated)?;
329
330		// SAFETY: The game added the new ragdoll to the entity list, and frees it
331		// only through deferred deletion, after `'s`.
332		let ragdoll = unsafe { Entity::from_raw(ragdoll) };
333
334		show(tools, ragdoll)?;
335
336		Ok(ragdoll)
337	}
338}
339
340/// What a TF2 player's death leaves for clients to make a ragdoll or gibs
341/// from (`tf_ragdoll`, `CTFRagdoll`), within one engine callback.
342///
343/// The game creates one as a player dies, which clients make their own
344/// ragdoll from when they first receive it, and which the player's
345/// [`ragdoll`](crate::tf2::player::TfPlayer::ragdoll) refers to until their
346/// next death. Clients read it once, so changing it only matters before it
347/// is first sent to them, in the frame of the death, such as from a hook of
348/// the death that runs after the game's.
349#[doc(alias("tf_ragdoll", "CTFRagdoll"))]
350#[derive(#[automatically_derived]
impl<'s> ::core::fmt::Debug for TfRagdoll<'s> {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f, "TfRagdoll",
            "server", &self.server, "ragdoll", &&self.ragdoll)
    }
}Debug, #[automatically_derived]
#[doc(hidden)]
unsafe impl<'s> ::core::clone::TrivialClone for TfRagdoll<'s> { }
#[automatically_derived]
impl<'s> ::core::clone::Clone for TfRagdoll<'s> {
    #[inline]
    fn clone(&self) -> Self {
        let _: ::core::clone::AssertParamIsClone<Server<'s>>;
        let _: ::core::clone::AssertParamIsClone<Entity<'s>>;
        *self
    }
}Clone, #[automatically_derived]
impl<'s> ::core::marker::Copy for TfRagdoll<'s> { }Copy)]
351pub struct TfRagdoll<'s> {
352	server: Server<'s>,
353	ragdoll: Entity<'s>,
354}
355
356impl<'s> TfRagdoll<'s> {
357	/// Wraps a player's ragdoll, or returns [`TfRagdollError::NotTfRagdoll`]
358	/// unless the server runs TF2 and `ragdoll`'s server class is
359	/// `CTFRagdoll`.
360	pub fn new(server: Server<'s>, ragdoll: Entity<'s>) -> Result<Self, TfRagdollError> {
361		if server.game() != Game::TeamFortress2
362			|| !ragdoll
363				.server_class()
364				.is_some_and(|class| class.name() == c"CTFRagdoll")
365		{
366			return Err(TfRagdollError::NotTfRagdoll);
367		}
368
369		Ok(Self { server, ragdoll })
370	}
371
372	/// The class the player died as (`m_iClass`), or `None` for
373	/// `TF_CLASS_UNDEFINED`.
374	///
375	/// Fails with [`TfRagdollError::UnknownClass`] for any other value that is
376	/// no playable class.
377	#[doc(alias("m_iClass"))]
378	pub fn class(self) -> Result<Option<PlayerClass>, TfRagdollError> {
379		match self.get::<c_int>(c"m_iClass")? {
380			0 => Ok(None),
381
382			raw => PlayerClass::from_raw(raw)
383				.map(Some)
384				.ok_or(TfRagdollError::UnknownClass(raw)),
385		}
386	}
387
388	/// The kind of the death's damage (`m_iDamageCustom`), a `TF_DMG_CUSTOM_*`
389	/// value such as a headshot's, from which clients choose the death
390	/// animation. The game clears it when the death animation should stop.
391	#[doc(alias("m_iDamageCustom"))]
392	pub fn damage_custom(self) -> Result<c_int, TfRagdollError> {
393		self.get(c"m_iDamageCustom")
394	}
395
396	/// The ragdoll's entity.
397	pub const fn entity(self) -> Entity<'s> {
398		self.ragdoll
399	}
400
401	/// Whether the ragdoll has `flag`.
402	pub fn flag(self, flag: RagdollFlag) -> Result<bool, TfRagdollError> {
403		self.get(flag.net_prop_name())
404	}
405
406	/// The damage force of the death (`m_vecForce`), with which clients push
407	/// the ragdoll.
408	#[doc(alias("m_vecForce"))]
409	pub fn force(self) -> Result<Vector, TfRagdollError> {
410		self.get(c"m_vecForce")
411	}
412
413	/// The bone the death's force pushes (`m_nForceBone`).
414	#[doc(alias("m_nForceBone"))]
415	pub fn force_bone(self) -> Result<c_int, TfRagdollError> {
416		self.get(c"m_nForceBone")
417	}
418
419	/// Reads one of the ragdoll's networked variables.
420	fn get<T: NetVar>(self, name: &CStr) -> Result<T, TfRagdollError> {
421		Ok(self.net_prop(name)?.get(self.ragdoll)?)
422	}
423
424	/// How much the player's hands were scaled (`m_flHandScale`), as some
425	/// conditions and taunts scale them.
426	#[doc(alias("m_flHandScale"))]
427	pub fn hand_scale(self) -> Result<f32, TfRagdollError> {
428		self.get(c"m_flHandScale")
429	}
430
431	/// How much the player's head was scaled (`m_flHeadScale`).
432	#[doc(alias("m_flHeadScale"))]
433	pub fn head_scale(self) -> Result<f32, TfRagdollError> {
434		self.get(c"m_flHeadScale")
435	}
436
437	/// Resolves one of the ragdoll's networked variables.
438	fn net_prop(self, name: &CStr) -> Result<NetProp<'s>, TfRagdollError> {
439		Ok(self
440			.server
441			.server_game_dll()?
442			.entity_net_prop(self.ragdoll, name)?)
443	}
444
445	/// Where the player died (`m_vecRagdollOrigin`).
446	#[doc(alias("m_vecRagdollOrigin"))]
447	pub fn origin(self) -> Result<Vector, TfRagdollError> {
448		self.get(c"m_vecRagdollOrigin")
449	}
450
451	/// The player who died (`m_hPlayer`), or `None` if they no longer exist.
452	#[doc(alias("m_hPlayer"))]
453	pub fn player(self) -> Result<Option<Entity<'s>>, TfRagdollError> {
454		let handle = self.net_prop(c"m_hPlayer")?.get_handle(self.ragdoll)?;
455
456		Ok(self.server.server_tools()?.entity_by_handle(handle))
457	}
458
459	/// Sets or clears one of the ragdoll's flags, and records the change so
460	/// the engine sends it to clients. Clients read the flags once, as they
461	/// make the ragdoll, so set them before the ragdoll is first sent, in the
462	/// frame of the death.
463	///
464	/// Fails with [`TfRagdollError::MarkedForDeletion`] for a ragdoll marked
465	/// for deletion.
466	pub fn set_flag(self, flag: RagdollFlag, value: bool) -> Result<(), TfRagdollError> {
467		if self.ragdoll.is_marked_for_deletion() {
468			return Err(TfRagdollError::MarkedForDeletion);
469		}
470
471		let engine = self.server.valve_engine()?;
472
473		// SAFETY: The game sets each flag either way itself, and clients
474		// combine any of them.
475		unsafe {
476			self.net_prop(flag.net_prop_name())?
477				.set(engine, self.ragdoll, value)
478		}?;
479
480		Ok(())
481	}
482
483	/// The player's team as they died (`m_iTeam`).
484	///
485	/// Fails with [`TfRagdollError::UnknownTeam`] for a value that is not one
486	/// of TF2's teams.
487	#[doc(alias("m_iTeam"))]
488	pub fn team(self) -> Result<Team, TfRagdollError> {
489		let raw = self.get::<c_int>(c"m_iTeam")?;
490
491		Team::from_raw(raw).ok_or(TfRagdollError::UnknownTeam(raw))
492	}
493
494	/// How much the player's torso was scaled (`m_flTorsoScale`).
495	#[doc(alias("m_flTorsoScale"))]
496	pub fn torso_scale(self) -> Result<f32, TfRagdollError> {
497		self.get(c"m_flTorsoScale")
498	}
499
500	/// The player's velocity as they died (`m_vecRagdollVelocity`), which
501	/// clients receive clamped to 2048 units per second in each axis.
502	#[doc(alias("m_vecRagdollVelocity"))]
503	pub fn velocity(self) -> Result<Vector, TfRagdollError> {
504		self.get(c"m_vecRagdollVelocity")
505	}
506}
507
508/// Why a [`TfRagdoll`] could not be read or changed.
509#[derive(#[automatically_derived]
impl ::core::fmt::Debug for TfRagdollError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            Self::Interface(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "Interface", &__self_0),
            Self::MarkedForDeletion =>
                ::core::fmt::Formatter::write_str(f, "MarkedForDeletion"),
            Self::NetProp(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "NetProp", &__self_0),
            Self::NotTfRagdoll =>
                ::core::fmt::Formatter::write_str(f, "NotTfRagdoll"),
            Self::UnknownClass(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "UnknownClass", &__self_0),
            Self::UnknownTeam(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "UnknownTeam", &__self_0),
        }
    }
}Debug, #[allow(unused_qualifications)]
#[automatically_derived]
impl ::thiserror::__private21::Error for TfRagdollError {
    fn source(&self)
        ->
            ::core::option::Option<&(dyn ::thiserror::__private21::Error +
            'static)> {
        use ::thiserror::__private21::AsDynError as _;

        #[allow(deprecated)]
        match self {
            TfRagdollError::Interface { 0: transparent } =>
                ::thiserror::__private21::Error::source(transparent.as_dyn_error()),
            TfRagdollError::MarkedForDeletion { .. } =>
                ::core::option::Option::None,
            TfRagdollError::NetProp { 0: transparent } =>
                ::thiserror::__private21::Error::source(transparent.as_dyn_error()),
            TfRagdollError::NotTfRagdoll { .. } =>
                ::core::option::Option::None,
            TfRagdollError::UnknownClass { .. } =>
                ::core::option::Option::None,
            TfRagdollError::UnknownTeam { .. } =>
                ::core::option::Option::None,
        }
    }
}
#[allow(unused_qualifications)]
#[automatically_derived]
impl ::core::fmt::Display for TfRagdollError {
    fn fmt(&self, __formatter: &mut ::core::fmt::Formatter)
        -> ::core::fmt::Result {
        use ::thiserror::__private21::AsDisplay as _;

        #[allow(unused_variables, deprecated, clippy ::
        used_underscore_binding)]
        match self {
            TfRagdollError::Interface(_0) =>
                ::core::fmt::Display::fmt(_0, __formatter),
            TfRagdollError::MarkedForDeletion {} =>
                __formatter.write_str("the ragdoll is marked for deletion"),
            TfRagdollError::NetProp(_0) =>
                ::core::fmt::Display::fmt(_0, __formatter),
            TfRagdollError::NotTfRagdoll {} =>
                __formatter.write_str("the entity is not a tf_ragdoll"),
            TfRagdollError::UnknownClass(_0) =>
                match (_0.as_display(),) {
                    (__display0,) =>
                        __formatter.write_fmt(format_args!("the ragdoll\'s class {0} is not one of TF2\'s playable classes",
                                __display0)),
                },
            TfRagdollError::UnknownTeam(_0) =>
                match (_0.as_display(),) {
                    (__display0,) =>
                        __formatter.write_fmt(format_args!("the ragdoll\'s team {0} is not one of TF2\'s",
                                __display0)),
                },
        }
    }
}
TfRagdollError
#[allow(clippy :: redundant_field_names)]
fn from(source: InterfaceError) -> Self {
    TfRagdollError::Interface { 0: source }
}
TfRagdollError
#[allow(clippy :: redundant_field_names)]
fn from(source: NetPropError) -> Self {
    TfRagdollError::NetProp { 0: source }
}thiserror::Error)]
510pub enum TfRagdollError {
511	/// A required engine interface is unavailable.
512	#[error(transparent)]
513	Interface(#[from] InterfaceError),
514
515	/// The ragdoll is marked for deletion.
516	#[error("the ragdoll is marked for deletion")]
517	MarkedForDeletion,
518
519	/// A networked variable could not be read or written.
520	#[error(transparent)]
521	NetProp(#[from] NetPropError),
522
523	/// The entity is not a TF2 player's ragdoll, or the server does not run
524	/// TF2.
525	#[error("the entity is not a tf_ragdoll")]
526	NotTfRagdoll,
527
528	/// The ragdoll's `m_iClass` holds neither `TF_CLASS_UNDEFINED` nor a
529	/// playable class.
530	#[error("the ragdoll's class {0} is not one of TF2's playable classes")]
531	UnknownClass(c_int),
532
533	/// The ragdoll's `m_iTeam` is not one of TF2's teams.
534	#[error("the ragdoll's team {0} is not one of TF2's")]
535	UnknownTeam(c_int),
536}
537
538/// Sends `ragdoll` the input that shows it, or removes it if it refuses.
539fn show(tools: ServerTools<'_>, ragdoll: Entity<'_>) -> Result<(), RagdollError> {
540	if let Err(error) =
541		tools.accept_input(ragdoll, ENABLE_INPUT, InputValue::Void, ragdoll, ragdoll)
542	{
543		// A ragdoll is not one of the entities `remove` protects.
544		let _ = tools.remove(ragdoll);
545
546		return Err(RagdollError::NotShown(error));
547	}
548
549	Ok(())
550}