use crate::datatables::{NetPropError, NetValue};
use crate::entities::{CollisionGroup, Entity};
use crate::inputs::{InputError, InputValue};
use crate::interfaces::ServerTools;
use crate::tf2::damage::{DamageInfo, DamageType};
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,
}
#[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)
}
}
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(())
}