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.
4142use 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::{selfas raw, RAGDOLL_MAX_ELEMENTS};
53use std::ffi::{CStr, c_int};
54use std::ptr::NonNull;
5556/// 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";
5960/// 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)]
65Interface(#[from] InterfaceError),
6667/// The entity is marked for deletion.
68#[error("the entity is marked for deletion")]
69MarkedForDeletion,
7071/// The entity's model index could not be read.
72#[error(transparent)]
73ModelIndex(#[from] NetPropError),
7475/// 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")]
79NoCollisionModel,
8081/// 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")]
84NotAnimating,
8586/// `CreateServerRagdoll` returned no ragdoll.
87#[error("the game created no ragdoll")]
88NotCreated,
8990/// The new ragdoll refused the input that shows it, so it was removed
91 /// again.
92#[error("the new ragdoll could not be shown")]
93NotShown(#[source] InputError),
9495/// 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")]
98Unresolved,
99100/// The server does not run TF2.
101#[error("server ragdolls require a TF2 server")]
102UnsupportedGame,
103}
104105/// 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"))]
112BecomeAsh,
113114/// Burns (`m_bBurning`).
115#[doc(alias("m_bBurning"))]
116Burning,
117118/// Fades out as a cloaked Spy does (`m_bCloaked`).
119#[doc(alias("m_bCloaked"))]
120Cloaked,
121122/// 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"))]
125CritOnHardHit,
126127/// Crackles with electricity (`m_bElectrocuted`).
128#[doc(alias("m_bElectrocuted"))]
129Electrocuted,
130131/// A Spy's feigned death (`m_bFeignDeath`).
132#[doc(alias("m_bFeignDeath"))]
133FeignDeath,
134135/// Bursts into gibs instead of a ragdoll (`m_bGib`).
136#[doc(alias("m_bGib"))]
137Gib,
138139/// Turns to gold, as the victims of golden weapons do (`m_bGoldRagdoll`).
140#[doc(alias("m_bGoldRagdoll"))]
141Gold,
142143/// Freezes into an ice statue, as the Spy-cicle's victims do
144 /// (`m_bIceRagdoll`).
145#[doc(alias("m_bIceRagdoll"))]
146Ice,
147148/// The player died on the ground (`m_bOnGround`). Clients play most death
149 /// animations only for such a ragdoll.
150#[doc(alias("m_bOnGround"))]
151OnGround,
152153/// The player was disguised (`m_bWasDisguised`), so a feigned death shows
154 /// the disguise.
155#[doc(alias("m_bWasDisguised"))]
156WasDisguised,
157}
158159impl RagdollFlag {
160/// Every flag.
161pub const ALL: [Self; 11] = [
162Self::BecomeAsh,
163Self::Burning,
164Self::Cloaked,
165Self::CritOnHardHit,
166Self::Electrocuted,
167Self::FeignDeath,
168Self::Gib,
169Self::Gold,
170Self::Ice,
171Self::OnGround,
172Self::WasDisguised,
173 ];
174175/// The name of the flag's networked variable.
176pub const fn net_prop_name(self) -> &'static CStr {
177match self {
178Self::BecomeAsh => c"m_bBecomeAsh",
179Self::Burning => c"m_bBurning",
180Self::Cloaked => c"m_bCloaked",
181Self::CritOnHardHit => c"m_bCritOnHardHit",
182Self::Electrocuted => c"m_bElectrocuted",
183Self::FeignDeath => c"m_bFeignDeath",
184Self::Gib => c"m_bGib",
185Self::Gold => c"m_bGoldRagdoll",
186Self::Ice => c"m_bIceRagdoll",
187Self::OnGround => c"m_bOnGround",
188Self::WasDisguised => c"m_bWasDisguised",
189 }
190 }
191}
192193/// 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}
200201impl<'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.
213pub fn new(server: Server<'s>) -> Result<Self, RagdollError> {
214if server.game() != Game::TeamFortress2 {
215return Err(RagdollError::UnsupportedGame);
216 }
217218// 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.
224let raw = unsafe { raw::ServerRagdolls::cached(server.game_server_factory().as_raw()) }
225 .map_err(|_| RagdollError::Unresolved)?;
226227Ok(Self { raw, server })
228 }
229230/// 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"))]
256pub fn create_server_ragdoll(
257self,
258 animating: Entity<'s>,
259 force_bone: Option<usize>,
260 info: &DamageInfo,
261 group: CollisionGroup,
262 ) -> Result<Entity<'s>, RagdollError> {
263if animating.is_marked_for_deletion() {
264return Err(RagdollError::MarkedForDeletion);
265 }
266267if !animating268 .server_class()
269 .and_then(|class| class.table())
270 .is_some_and(|table| table.derives_from_named(c"DT_BaseAnimating"))
271 {
272return Err(RagdollError::NotAnimating);
273 }
274275let tools = self.server.server_tools()?;
276let model_info = self.server.model_info()?;
277278let NetValue::Int(model) = self
279.server
280 .server_game_dll()?
281.entity_net_prop(animating, c"m_nModelIndex")?
282.value(animating)?
283else {
284return Err(RagdollError::NoCollisionModel);
285 };
286287// The game reads the bones of the studio model, and makes a physics
288 // object for each solid of its collision model.
289let 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)?;
294295let force_bone = force_bone296 .and_then(|bone| c_int::try_from(bone).ok())
297 .filter(|bone| (0..solids).contains(bone))
298 .unwrap_or(raw::NO_FORCE_BONE);
299300let mut info = info.clone();
301302info.set_damage_type(info.damage_type() - DamageType::from_bits_retain(DMG_VEHICLEas u32));
303304// 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.
319let ragdoll = unsafe {
320self.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(),
325false,
326 )
327 }
328 .ok_or(RagdollError::NotCreated)?;
329330// SAFETY: The game added the new ragdoll to the entity list, and frees it
331 // only through deferred deletion, after `'s`.
332let ragdoll = unsafe { Entity::from_raw(ragdoll) };
333334 show(tools, ragdoll)?;
335336Ok(ragdoll)
337 }
338}
339340/// 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}
355356impl<'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`.
360pub fn new(server: Server<'s>, ragdoll: Entity<'s>) -> Result<Self, TfRagdollError> {
361if server.game() != Game::TeamFortress2362 || !ragdoll363 .server_class()
364 .is_some_and(|class| class.name() == c"CTFRagdoll")
365 {
366return Err(TfRagdollError::NotTfRagdoll);
367 }
368369Ok(Self { server, ragdoll })
370 }
371372/// 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"))]
378pub fn class(self) -> Result<Option<PlayerClass>, TfRagdollError> {
379match self.get::<c_int>(c"m_iClass")? {
3800 => Ok(None),
381382 raw => PlayerClass::from_raw(raw)
383 .map(Some)
384 .ok_or(TfRagdollError::UnknownClass(raw)),
385 }
386 }
387388/// 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"))]
392pub fn damage_custom(self) -> Result<c_int, TfRagdollError> {
393self.get(c"m_iDamageCustom")
394 }
395396/// The ragdoll's entity.
397pub const fn entity(self) -> Entity<'s> {
398self.ragdoll
399 }
400401/// Whether the ragdoll has `flag`.
402pub fn flag(self, flag: RagdollFlag) -> Result<bool, TfRagdollError> {
403self.get(flag.net_prop_name())
404 }
405406/// The damage force of the death (`m_vecForce`), with which clients push
407 /// the ragdoll.
408#[doc(alias("m_vecForce"))]
409pub fn force(self) -> Result<Vector, TfRagdollError> {
410self.get(c"m_vecForce")
411 }
412413/// The bone the death's force pushes (`m_nForceBone`).
414#[doc(alias("m_nForceBone"))]
415pub fn force_bone(self) -> Result<c_int, TfRagdollError> {
416self.get(c"m_nForceBone")
417 }
418419/// Reads one of the ragdoll's networked variables.
420fn get<T: NetVar>(self, name: &CStr) -> Result<T, TfRagdollError> {
421Ok(self.net_prop(name)?.get(self.ragdoll)?)
422 }
423424/// How much the player's hands were scaled (`m_flHandScale`), as some
425 /// conditions and taunts scale them.
426#[doc(alias("m_flHandScale"))]
427pub fn hand_scale(self) -> Result<f32, TfRagdollError> {
428self.get(c"m_flHandScale")
429 }
430431/// How much the player's head was scaled (`m_flHeadScale`).
432#[doc(alias("m_flHeadScale"))]
433pub fn head_scale(self) -> Result<f32, TfRagdollError> {
434self.get(c"m_flHeadScale")
435 }
436437/// Resolves one of the ragdoll's networked variables.
438fn net_prop(self, name: &CStr) -> Result<NetProp<'s>, TfRagdollError> {
439Ok(self
440.server
441 .server_game_dll()?
442.entity_net_prop(self.ragdoll, name)?)
443 }
444445/// Where the player died (`m_vecRagdollOrigin`).
446#[doc(alias("m_vecRagdollOrigin"))]
447pub fn origin(self) -> Result<Vector, TfRagdollError> {
448self.get(c"m_vecRagdollOrigin")
449 }
450451/// The player who died (`m_hPlayer`), or `None` if they no longer exist.
452#[doc(alias("m_hPlayer"))]
453pub fn player(self) -> Result<Option<Entity<'s>>, TfRagdollError> {
454let handle = self.net_prop(c"m_hPlayer")?.get_handle(self.ragdoll)?;
455456Ok(self.server.server_tools()?.entity_by_handle(handle))
457 }
458459/// 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.
466pub fn set_flag(self, flag: RagdollFlag, value: bool) -> Result<(), TfRagdollError> {
467if self.ragdoll.is_marked_for_deletion() {
468return Err(TfRagdollError::MarkedForDeletion);
469 }
470471let engine = self.server.valve_engine()?;
472473// SAFETY: The game sets each flag either way itself, and clients
474 // combine any of them.
475unsafe {
476self.net_prop(flag.net_prop_name())?
477.set(engine, self.ragdoll, value)
478 }?;
479480Ok(())
481 }
482483/// 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"))]
488pub fn team(self) -> Result<Team, TfRagdollError> {
489let raw = self.get::<c_int>(c"m_iTeam")?;
490491Team::from_raw(raw).ok_or(TfRagdollError::UnknownTeam(raw))
492 }
493494/// How much the player's torso was scaled (`m_flTorsoScale`).
495#[doc(alias("m_flTorsoScale"))]
496pub fn torso_scale(self) -> Result<f32, TfRagdollError> {
497self.get(c"m_flTorsoScale")
498 }
499500/// 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"))]
503pub fn velocity(self) -> Result<Vector, TfRagdollError> {
504self.get(c"m_vecRagdollVelocity")
505 }
506}
507508/// 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)]
513Interface(#[from] InterfaceError),
514515/// The ragdoll is marked for deletion.
516#[error("the ragdoll is marked for deletion")]
517MarkedForDeletion,
518519/// A networked variable could not be read or written.
520#[error(transparent)]
521NetProp(#[from] NetPropError),
522523/// 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")]
526NotTfRagdoll,
527528/// 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")]
531UnknownClass(c_int),
532533/// 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")]
535UnknownTeam(c_int),
536}
537538/// Sends `ragdoll` the input that shows it, or removes it if it refuses.
539fn show(tools: ServerTools<'_>, ragdoll: Entity<'_>) -> Result<(), RagdollError> {
540if let Err(error) =
541tools.accept_input(ragdoll, ENABLE_INPUT, InputValue::Void, ragdoll, ragdoll)
542 {
543// A ragdoll is not one of the entities `remove` protects.
544let _ = tools.remove(ragdoll);
545546return Err(RagdollError::NotShown(error));
547 }
548549Ok(())
550}