#[cfg(test)]
#[path = "../tests/tf2/wearables.rs"]
mod tests;
use crate::datatables::{NetProp, NetPropError, NetValue, PropKind};
use crate::entities::{Entity, EntityHandle};
use crate::interfaces::ServerTools;
use crate::math::Vector;
use crate::tf2::weapons::{self, ItemDefinitionIndex, ItemGenerationError};
use crate::{Game, InterfaceError, Server};
use sdk_raw::edicts::MAX_EDICT_BITS;
use sdk_raw::entities::{
INVALID_NETWORKED_EHANDLE_VALUE, NUM_NETWORKED_EHANDLE_BITS,
NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS,
};
use sdk_raw::players::FIRST_GAME_TEAM;
use sdk_raw::vcall;
use std::ffi::{CStr, c_int};
use std::ptr::NonNull;
const ELEMENT_NAMES: [&CStr; MAX_NETWORKED_WEARABLES] = [
c"000", c"001", c"002", c"003", c"004", c"005", c"006", c"007",
];
#[doc(alias("MAX_WEARABLES_SENT_FROM_SERVER", "LOADOUT_MAX_WEARABLES_COUNT"))]
pub const MAX_NETWORKED_WEARABLES: usize = sdk_raw::tf2::wearables::MAX_WEARABLES_SENT_FROM_SERVER;
const MAX_PARENT_DEPTH: usize = 8;
const MAX_REMOVE_ATTEMPTS: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct NetworkedHandle(u32);
impl NetworkedHandle {
fn decode(raw: c_int) -> Option<Self> {
let raw = raw.cast_unsigned();
(raw != 0
&& raw != INVALID_NETWORKED_EHANDLE_VALUE
&& raw >> NUM_NETWORKED_EHANDLE_BITS == 0)
.then_some(Self(raw))
}
const fn index(self) -> usize {
(self.0 & ((1 << MAX_EDICT_BITS) - 1)) as usize
}
const fn matches(self, handle: EntityHandle) -> bool {
let serial = handle.serial_number() & ((1 << NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS) - 1);
matches!(handle.index(), Some(index) if index == self.index())
&& serial == self.0 >> MAX_EDICT_BITS
}
fn resolve<'s>(self, tools: ServerTools<'s>) -> Option<Entity<'s>> {
let entity = tools.entity_by_index(c_int::try_from(self.index()).ok()?)?;
self.matches(entity.handle()).then_some(entity)
}
}
#[derive(Debug, Clone, Copy)]
pub struct PlayerWearables<'s> {
server: Server<'s>,
player: Entity<'s>,
}
impl<'s> PlayerWearables<'s> {
pub fn new(server: Server<'s>, player: Entity<'s>) -> Result<Self, WearableError> {
if server.game() != Game::TeamFortress2 || !player.has_data_map_class(c"CTFPlayer") {
return Err(WearableError::NotTfPlayer);
}
Ok(Self { server, player })
}
unsafe fn attach(self, wearable: Wearable<'s>) -> Result<(), WearableError> {
unsafe { self.equip_native(wearable) };
match self.is_attached(wearable) {
Ok(true) => Ok(()),
attached => {
let discarded = unsafe { self.discard(wearable) };
attached?;
discarded?;
Err(WearableError::Rejected)
}
}
}
fn check_playing(self) -> Result<(), WearableError> {
check_live(self.player)?;
let manager = self.server.player_info_manager()?;
let info = self
.player
.edict()
.and_then(|edict| manager.player_info(edict))
.ok_or(WearableError::NotTfPlayer)?;
if info.is_dead() || info.team() < FIRST_GAME_TEAM {
return Err(WearableError::PlayerNotPlaying);
}
Ok(())
}
unsafe fn discard(self, wearable: Wearable<'s>) -> Result<(), WearableError> {
let tools = self.server.server_tools()?;
let mut result = Ok(());
for _ in 0..MAX_REMOVE_ATTEMPTS {
match self.list() {
Ok(list) if list.contains(wearable) => {
unsafe { self.remove_native(wearable) };
}
Ok(_) => break,
Err(error) => {
result = Err(error);
break;
}
}
}
let _ = tools.remove(wearable.entity);
result
}
#[doc(alias("EquipWearable"))]
pub unsafe fn equip(self, wearable: Wearable<'s>) -> Result<(), WearableError> {
self.check_playing()?;
check_live(wearable.entity)?;
let list = self.list()?;
if list.contains(wearable) {
return Ok(());
}
if wearable
.owner()?
.is_some_and(|owner| owner != self.player.handle())
{
return Err(WearableError::DifferentOwner);
}
if list.is_full() {
return Err(WearableError::Full);
}
wearable.set_validated(true)?;
unsafe { self.attach(wearable) }
}
unsafe fn equip_native(self, wearable: Wearable<'s>) {
let player = self.player.as_ptr().cast::<sys::CTFPlayer>();
unsafe {
vcall!(player as sys::CTFPlayer__bindgen_vtable => CTFPlayer_EquipWearable(
wearable.entity.as_ptr().cast(),
));
}
}
#[doc(alias("SpawnItem", "EquipWearable"))]
pub unsafe fn give(
self,
definition: ItemDefinitionIndex,
) -> Result<Wearable<'s>, WearableError> {
let wearable = unsafe {
self.give_with(|origin| {
weapons::generate_item(self.server, definition, origin, None)
.map_err(WearableError::CreationFailedNative)
})
}?;
if wearable.has_error_model()? {
let _ = unsafe { self.remove(wearable) };
return Err(WearableError::MissingModel);
}
Ok(wearable)
}
unsafe fn give_with(
self,
create: impl FnOnce(Vector) -> Result<NonNull<sys::CBaseEntity>, WearableError>,
) -> Result<Wearable<'s>, WearableError> {
self.check_playing()?;
let tools = self.server.server_tools()?;
if self.list()?.is_full() {
return Err(WearableError::Full);
}
let origin = self.player.position().ok_or(WearableError::MissingOrigin)?;
let entity = unsafe { Entity::from_raw(create(origin)?) };
let wearable = match Wearable::new(self.server, entity) {
Ok(wearable) => wearable,
Err(error) => {
let _ = tools.remove(entity);
return Err(error);
}
};
if let Err(error) = check_live(entity).and_then(|()| wearable.set_validated(true)) {
let _ = tools.remove(entity);
return Err(error);
}
unsafe { self.attach(wearable) }?;
Ok(wearable)
}
fn is_attached(self, wearable: Wearable<'s>) -> Result<bool, WearableError> {
Ok(!wearable.entity.is_marked_for_deletion()
&& wearable.owner()? == Some(self.player.handle())
&& self.is_parented(wearable)?
&& self.list()?.contains(wearable))
}
fn is_parented(self, wearable: Wearable<'s>) -> Result<bool, WearableError> {
let tools = self.server.server_tools()?;
let player = self.player.handle();
let mut entity = wearable.entity;
for _ in 0..MAX_PARENT_DEPTH {
let parent = unsafe { read_handle(entity, wearable.parent_offset) };
if parent == player {
return Ok(true);
}
match tools.entity_by_handle(parent) {
Some(found) => entity = found,
None => return Ok(false),
}
}
Ok(false)
}
#[doc(alias("m_hMyWearables", "GetWearable"))]
pub fn list(self) -> Result<WearableList<'s>, WearableError> {
check_live(self.player)?;
let tools = self.server.server_tools()?;
let vector = self
.server
.server_game_dll()?
.entity_net_prop(self.player, c"m_hMyWearables")?;
if vector.prop().kind() != PropKind::DataTable
|| vector.element_count() != Some(MAX_NETWORKED_WEARABLES + 1)
{
return Err(WearableError::UnsupportedLayout);
}
let mut elements = [vector; MAX_NETWORKED_WEARABLES];
for ((slot, name), element) in ELEMENT_NAMES.into_iter().enumerate().zip(&mut elements) {
*element = vector.element(slot + 1).map_err(layout_error)?;
if element.prop().name() != name || element.prop().kind() != PropKind::Int {
return Err(WearableError::UnsupportedLayout);
}
}
let mut list = WearableList {
entries: [None; MAX_NETWORKED_WEARABLES],
networked_len: 0,
};
for (slot, element) in elements.into_iter().enumerate() {
let NetValue::Int(raw) = element.value(self.player).map_err(layout_error)? else {
return Err(WearableError::UnsupportedLayout);
};
if raw == 0 {
break;
}
list.networked_len += 1;
let Some(entity) =
NetworkedHandle::decode(raw).and_then(|handle| handle.resolve(tools))
else {
continue;
};
list.entries[slot] = match Wearable::new(self.server, entity) {
Ok(wearable) => Some(wearable),
Err(WearableError::NotWearable) => None,
Err(error) => return Err(error),
};
}
Ok(list)
}
pub const fn player(self) -> Entity<'s> {
self.player
}
#[doc(alias("RemoveWearable"))]
pub unsafe fn remove(self, wearable: Wearable<'s>) -> Result<(), WearableError> {
check_live(self.player)?;
check_live(wearable.entity)?;
if !self.list()?.contains(wearable) {
return Err(WearableError::NotEquipped);
}
unsafe { self.discard(wearable) }
}
unsafe fn remove_native(self, wearable: Wearable<'s>) {
let player = self.player.as_ptr().cast::<sys::CTFPlayer>();
unsafe {
vcall!(player as sys::CTFPlayer__bindgen_vtable => CTFPlayer_RemoveWearable(
wearable.entity.as_ptr().cast(),
));
}
}
pub unsafe fn strip(
self,
mut filter: impl FnMut(Wearable<'s>) -> bool,
) -> Result<usize, WearableError> {
check_live(self.player)?;
let mut selected = [None; MAX_NETWORKED_WEARABLES];
for (slot, wearable) in selected.iter_mut().zip(self.list()?.iter()) {
if filter(wearable) {
*slot = Some(wearable);
}
}
let mut removed = 0;
for wearable in selected.into_iter().flatten() {
if wearable.entity.is_marked_for_deletion() || !self.list()?.contains(wearable) {
continue;
}
unsafe { self.discard(wearable) }?;
removed += 1;
}
Ok(removed)
}
}
#[doc(alias("CTFWearable"))]
#[derive(Debug, Clone, Copy)]
pub struct Wearable<'s> {
server: Server<'s>,
entity: Entity<'s>,
owner_offset: usize,
parent_offset: usize,
}
impl<'s> Wearable<'s> {
pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, WearableError> {
if server.game() != Game::TeamFortress2 || !entity.has_data_map_class(c"CTFWearable") {
return Err(WearableError::NotWearable);
}
let handle_offset = |name| {
entity
.find_base_entity_field(name, sys::_fieldtypes_FIELD_EHANDLE, size_of::<u32>())
.ok_or(WearableError::UnsupportedLayout)
};
Ok(Self {
server,
entity,
owner_offset: handle_offset(c"m_hOwnerEntity")?,
parent_offset: handle_offset(c"m_hMoveParent")?,
})
}
#[doc(alias("m_hWeaponAssociatedWith", "GetWeaponAssociatedWith"))]
pub fn associated_weapon(self) -> Result<Option<Entity<'s>>, WearableError> {
let tools = self.server.server_tools()?;
Ok(self
.associated_weapon_handle()?
.and_then(|handle| handle.resolve(tools)))
}
fn associated_weapon_handle(self) -> Result<Option<NetworkedHandle>, WearableError> {
match self
.net_prop(c"m_hWeaponAssociatedWith")?
.value(self.entity)?
{
NetValue::Int(raw) => Ok(NetworkedHandle::decode(raw)),
_ => Err(WearableError::UnsupportedLayout),
}
}
#[doc(alias("m_iItemDefinitionIndex", "GetItemDefIndex"))]
pub fn definition(self) -> Result<Option<ItemDefinitionIndex>, WearableError> {
if !self.is_initialized()? {
return Ok(None);
}
let index = self
.net_prop(c"m_iItemDefinitionIndex")?
.get::<u16>(self.entity)?;
Ok(ItemDefinitionIndex::new(index))
}
#[doc(alias("SpawnItem"))]
pub unsafe fn generate(
server: Server<'s>,
definition: ItemDefinitionIndex,
origin: Vector,
) -> Result<Self, WearableError> {
let tools = server.server_tools()?;
let entity =
unsafe { Entity::from_raw(weapons::generate_item(server, definition, origin, None)?) };
Self::new(server, entity).inspect_err(|_| {
let _ = tools.remove(entity);
})
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
pub fn has_error_model(self) -> Result<bool, WearableError> {
check_live(self.entity)?;
let NetValue::Int(index) = self.net_prop(c"m_nModelIndex")?.value(self.entity)? else {
return Err(WearableError::UnsupportedLayout);
};
Ok(self.server.model_info()?.is_error_model(index))
}
#[doc(alias("m_bDisguiseWearable", "IsDisguiseWearable"))]
pub fn is_disguise(self) -> Result<bool, WearableError> {
Ok(self
.net_prop(c"m_bDisguiseWearable")?
.get::<bool>(self.entity)?)
}
pub fn is_game_managed(self) -> Result<bool, WearableError> {
Ok(self.associated_weapon_handle()?.is_some()
|| self.is_disguise()?
|| !self.is_initialized()?)
}
fn is_initialized(self) -> Result<bool, WearableError> {
Ok(self.net_prop(c"m_bInitialized")?.get::<bool>(self.entity)?)
}
#[doc(alias("m_bValidatedAttachedEntity"))]
pub fn is_validated(self) -> Result<bool, WearableError> {
Ok(self
.net_prop(c"m_bValidatedAttachedEntity")?
.get::<bool>(self.entity)?)
}
pub fn kind(self) -> WearableKind {
WearableKind::from_class_name(self.entity.class_name())
}
fn net_prop(self, name: &CStr) -> Result<NetProp<'s>, WearableError> {
Ok(self
.server
.server_game_dll()?
.entity_net_prop(self.entity, name)?)
}
#[doc(alias("m_hOwnerEntity", "GetOwnerEntity"))]
pub fn owner(self) -> Result<Option<EntityHandle>, WearableError> {
check_live(self.entity)?;
let handle = unsafe { read_handle(self.entity, self.owner_offset) };
Ok(handle.is_valid().then_some(handle))
}
#[doc(alias("m_bValidatedAttachedEntity", "MarkAttachedEntityAsValidated"))]
pub fn set_validated(self, validated: bool) -> Result<(), WearableError> {
let engine = self.server.valve_engine()?;
let prop = self.net_prop(c"m_bValidatedAttachedEntity")?;
unsafe { prop.set(engine, self.entity, validated) }?;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum WearableError {
#[error(transparent)]
CreationFailedNative(#[from] ItemGenerationError),
#[error("the wearable is owned by another entity")]
DifferentOwner,
#[error("the player already wears as many items as the game networks")]
Full,
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the entity is marked for deletion")]
MarkedForDeletion,
#[error("the item has no model for the player's class")]
MissingModel,
#[error("the player's absolute position could not be read")]
MissingOrigin,
#[error(transparent)]
NetProp(#[from] NetPropError),
#[error("the wearable is not in this player's wearable list")]
NotEquipped,
#[error("wearable operations require a TF2 player")]
NotTfPlayer,
#[error("the entity is not a TF2 wearable")]
NotWearable,
#[error("the player is dead or not on a playing team")]
PlayerNotPlaying,
#[error("the game refused to equip the wearable")]
Rejected,
#[error("the game does not describe wearables in the expected layout")]
UnsupportedLayout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum WearableKind {
#[doc(alias("CTFWearableCampaignItem"))]
Campaign,
#[doc(alias("CTFWearableDemoShield"))]
DemoShield,
#[doc(alias("CTFWearableLevelableItem"))]
Levelable,
Other,
#[doc(alias("CTFWearable"))]
Plain,
#[doc(alias("CTFPowerupBottle"))]
PowerupBottle,
#[doc(alias("CTFWearableRazorback"))]
Razorback,
#[doc(alias("CTFWearableRobotArm"))]
RobotArm,
#[doc(alias("CTFWearableVM"))]
ViewModel,
}
impl WearableKind {
pub fn from_class_name(class_name: &CStr) -> Self {
match class_name.to_bytes() {
b"tf_powerup_bottle" => Self::PowerupBottle,
b"tf_wearable" => Self::Plain,
b"tf_wearable_campaign_item" => Self::Campaign,
b"tf_wearable_demoshield" => Self::DemoShield,
b"tf_wearable_levelable_item" => Self::Levelable,
b"tf_wearable_razorback" => Self::Razorback,
b"tf_wearable_robot_arm" => Self::RobotArm,
b"tf_wearable_vm" => Self::ViewModel,
_ => Self::Other,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct WearableList<'s> {
entries: [Option<Wearable<'s>>; MAX_NETWORKED_WEARABLES],
networked_len: usize,
}
impl<'s> WearableList<'s> {
pub fn contains(&self, wearable: Wearable<'_>) -> bool {
self.iter()
.any(|listed| listed.entity.as_ptr() == wearable.entity.as_ptr())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn is_full(&self) -> bool {
self.networked_len >= MAX_NETWORKED_WEARABLES
}
pub fn iter(&self) -> impl Iterator<Item = Wearable<'s>> + '_ {
self.entries.iter().flatten().copied()
}
pub fn len(&self) -> usize {
self.iter().count()
}
pub fn networked_len(&self) -> usize {
self.networked_len
}
}
fn check_live(entity: Entity<'_>) -> Result<(), WearableError> {
if entity.is_marked_for_deletion() {
Err(WearableError::MarkedForDeletion)
} else {
Ok(())
}
}
fn layout_error(error: NetPropError) -> WearableError {
match error {
NetPropError::ElementOutOfRange { .. }
| NetPropError::InvalidOffset { .. }
| NetPropError::NoProxy { .. }
| NetPropError::NotAValue { .. }
| NetPropError::NotAnArray { .. }
| NetPropError::Relocated { .. } => WearableError::UnsupportedLayout,
error => WearableError::NetProp(error),
}
}
unsafe fn read_handle(entity: Entity<'_>, offset: usize) -> EntityHandle {
EntityHandle::from_raw(unsafe { entity.as_ptr().byte_add(offset).cast::<u32>().read() })
}