#[cfg(test)]
#[path = "../../tests/tf2/weapons.rs"]
mod tests;
use crate::entities::{Entity, EntityHandle};
use crate::math::Vector;
use crate::tf2::attributes::{self, AttributeError, AttributeSet, ItemAttributes, SchemaToken};
use crate::{Game, InterfaceError, Server};
use sdk_raw::tf2::item_generation::ItemGeneration;
use sdk_raw::tf2::weapons::INVALID_ITEM_DEF_INDEX;
use sdk_raw::vcall;
use std::ffi::{CStr, c_int};
use std::ptr::NonNull;
pub use sdk_raw::tf2::item_generation::ItemGenerationError;
pub trait IntoWeaponSlot {
fn into_weapon_slot(self) -> c_int;
}
impl IntoWeaponSlot for c_int {
fn into_weapon_slot(self) -> c_int {
self
}
}
#[doc(alias("item_definition_index_t"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ItemDefinitionIndex(u16);
impl ItemDefinitionIndex {
pub const fn new(index: u16) -> Option<Self> {
if index == INVALID_ITEM_DEF_INDEX {
None
} else {
Some(Self(index))
}
}
pub const fn get(self) -> u16 {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct PlayerWeapons<'s> {
server: Server<'s>,
player: Entity<'s>,
}
impl<'s> PlayerWeapons<'s> {
pub fn new(server: Server<'s>, player: Entity<'s>) -> Result<Self, WeaponError> {
if server.game() != Game::TeamFortress2 || !player.has_data_map_class(c"CTFPlayer") {
return Err(WeaponError::NotTfPlayer);
}
Ok(Self { server, player })
}
#[doc(alias("RemovePlayerItem"))]
pub fn detach(self, weapon: Weapon<'s>) -> Result<(), WeaponError> {
check_live(self.player)?;
if weapon.owner()? != Some(self.player.handle()) {
return Err(WeaponError::DifferentOwner);
}
let player = self.player.as_ptr().cast::<sys::CTFPlayer>();
let removed = unsafe {
vcall!(player as sys::CTFPlayer__bindgen_vtable => CTFPlayer_RemovePlayerItem(
weapon.entity.as_ptr().cast(),
))
};
if !removed {
return Err(WeaponError::Rejected);
}
Ok(())
}
#[doc(alias("Weapon_Equip"))]
pub fn equip(self, weapon: Weapon<'s>) -> Result<(), WeaponError> {
check_live(self.player)?;
check_live(weapon.entity)?;
let owner = weapon.owner()?;
if owner.is_some_and(|owner| owner != self.player.handle()) {
return Err(WeaponError::DifferentOwner);
}
let slot = weapon.slot_raw()?;
if let Some(existing) = self.get_slot(slot)? {
return if existing.entity != weapon.entity {
Err(WeaponError::SlotOccupied)
} else if owner == Some(self.player.handle()) {
Ok(())
} else {
Err(WeaponError::Rejected)
};
}
let player = self.player.as_ptr().cast::<sys::CTFPlayer>();
unsafe {
vcall!(player as sys::CTFPlayer__bindgen_vtable => CTFPlayer_Weapon_Equip(
weapon.entity.as_ptr().cast(),
));
}
if weapon.owner()? != Some(self.player.handle())
|| !self
.get_slot(slot)?
.is_some_and(|found| found.entity == weapon.entity)
{
return Err(WeaponError::Rejected);
}
Ok(())
}
#[doc(alias("Weapon_GetSlot"))]
pub fn get_slot(self, slot: impl IntoWeaponSlot) -> Result<Option<Weapon<'s>>, WeaponError> {
check_live(self.player)?;
let player = self.player.as_ptr().cast::<sys::CTFPlayer>();
let raw = unsafe {
vcall!(player as sys::CTFPlayer__bindgen_vtable => CTFPlayer_Weapon_GetSlot(
slot.into_weapon_slot(),
))
};
NonNull::new(raw)
.map(|raw| {
Weapon::new(self.server, unsafe { Entity::from_raw(raw.cast()) })
})
.transpose()
}
#[doc(alias("GiveNamedItem"))]
pub unsafe fn give(self, classname: &CStr, subtype: i32) -> Result<Weapon<'s>, WeaponError> {
check_live(self.player)?;
let player = self.player.as_ptr().cast::<sys::CTFPlayer>();
unsafe {
self.give_with(
None,
|| {
NonNull::new(
vcall!(player as sys::CTFPlayer__bindgen_vtable => CTFPlayer_GiveNamedItem1(
classname.as_ptr(),
subtype,
std::ptr::null(),
true,
)),
)
.ok_or(WeaponError::CreationFailed)
},
|_| Ok(()),
)
}
}
#[doc(alias("SpawnItem"))]
pub unsafe fn give_item(
self,
definition: ItemDefinitionIndex,
) -> Result<Weapon<'s>, WeaponError> {
unsafe { self.spawn_item(definition, None, |_| Ok(())) }
}
#[doc(alias("SpawnItem"))]
pub unsafe fn give_item_as(
self,
definition: ItemDefinitionIndex,
classname: &CStr,
) -> Result<Weapon<'s>, WeaponError> {
unsafe { self.spawn_item(definition, Some(classname), |_| Ok(())) }
}
#[doc(alias("SpawnItem", "AddAttribute"))]
pub unsafe fn give_item_with(
self,
token: SchemaToken<'s>,
definition: ItemDefinitionIndex,
attributes: &AttributeSet,
) -> Result<Weapon<'s>, WeaponError> {
unsafe {
self.spawn_item(definition, None, |weapon| {
apply_attributes(token, weapon, attributes)
})
}
}
unsafe fn give_with(
self,
expected_classname: Option<&CStr>,
create: impl FnOnce() -> Result<NonNull<sys::CBaseEntity>, WeaponError>,
finish: impl FnOnce(Weapon<'s>) -> Result<(), WeaponError>,
) -> Result<Weapon<'s>, WeaponError> {
check_live(self.player)?;
let tools = self.server.server_tools()?;
let mut occupied = [false; 256];
for slot in 0..=u8::MAX {
occupied[usize::from(slot)] = self.get_slot(c_int::from(slot))?.is_some();
}
let raw = create()?;
let entity = unsafe { Entity::from_raw(raw) };
check_live(entity)?;
if expected_classname.is_some_and(|expected| entity.class_name() != expected) {
let _ = tools.remove(entity);
return Err(WeaponError::CreationFailed);
}
let weapon = match Weapon::new(self.server, entity) {
Ok(weapon) => weapon,
Err(error) => {
let _ = tools.remove(entity);
return Err(error);
}
};
let equipped = weapon
.slot_raw()
.and_then(|slot| {
let slot = u8::try_from(slot).map_err(|_| WeaponError::WrongSlot)?;
if occupied[usize::from(slot)] {
Err(WeaponError::SlotOccupied)
} else {
self.equip(weapon)
}
})
.and_then(|()| finish(weapon));
if let Err(error) = equipped {
let owner = weapon.owner()?;
if owner == Some(self.player.handle()) {
let _ = self.detach(weapon);
let _ = tools.remove(entity);
} else if owner.is_none() {
let _ = tools.remove(entity);
}
return Err(error);
}
Ok(weapon)
}
pub const fn player(self) -> Entity<'s> {
self.player
}
pub unsafe fn replace(
self,
slot: impl IntoWeaponSlot,
classname: &CStr,
subtype: i32,
) -> Result<Weapon<'s>, WeaponError> {
self.replace_with(slot, || unsafe { self.give(classname, subtype) })
}
pub unsafe fn replace_item(
self,
slot: impl IntoWeaponSlot,
definition: ItemDefinitionIndex,
) -> Result<Weapon<'s>, WeaponError> {
self.replace_with(slot, || unsafe { self.give_item(definition) })
}
pub unsafe fn replace_item_as(
self,
slot: impl IntoWeaponSlot,
definition: ItemDefinitionIndex,
classname: &CStr,
) -> Result<Weapon<'s>, WeaponError> {
self.replace_with(slot, || unsafe { self.give_item_as(definition, classname) })
}
fn replace_with(
self,
slot: impl IntoWeaponSlot,
create: impl FnOnce() -> Result<Weapon<'s>, WeaponError>,
) -> Result<Weapon<'s>, WeaponError> {
let slot = slot.into_weapon_slot();
let tools = self.server.server_tools()?;
let old = self.get_slot(slot)?;
if let Some(old) = old {
self.detach(old)?;
}
let replacement = create().and_then(|weapon| {
if weapon.slot_raw()? == slot {
return Ok(weapon);
}
let detached = self.detach(weapon);
let _ = tools.remove(weapon.entity);
detached?;
Err(WeaponError::WrongSlot)
});
match replacement {
Ok(weapon) => {
if let Some(old) = old {
let _ = tools.remove(old.entity);
}
Ok(weapon)
}
Err(error) => {
if let Some(old) = old {
self.equip(old)?;
}
Err(error)
}
}
}
unsafe fn spawn_item(
self,
definition: ItemDefinitionIndex,
classname: Option<&CStr>,
finish: impl FnOnce(Weapon<'s>) -> Result<(), WeaponError>,
) -> Result<Weapon<'s>, WeaponError> {
check_live(self.player)?;
let origin = self.player.position().ok_or(WeaponError::MissingOrigin)?;
unsafe {
self.give_with(
classname,
|| {
generate_item(self.server, definition, origin, classname)
.map_err(WeaponError::CreationFailedNative)
},
finish,
)
}
}
}
#[doc(alias("CTFWeaponBase"))]
#[derive(Debug, Clone, Copy)]
pub struct Weapon<'s> {
server: Server<'s>,
entity: Entity<'s>,
owner_offset: usize,
}
impl<'s> Weapon<'s> {
pub fn new(server: Server<'s>, entity: Entity<'s>) -> Result<Self, WeaponError> {
if server.game() != Game::TeamFortress2 || !entity.has_data_map_class(c"CTFWeaponBase") {
return Err(WeaponError::NotWeapon);
}
let owner_offset = entity
.data_maps()
.find(|map| map.class_name() == Some(c"CBaseCombatWeapon"))
.and_then(|map| map.field_offset(c"m_hOwner", sys::_fieldtypes_FIELD_EHANDLE))
.filter(|offset| *offset < 65_536 && offset.is_multiple_of(align_of::<u32>()))
.ok_or(WeaponError::UnsupportedLayout)?;
Ok(Self {
server,
entity,
owner_offset,
})
}
pub fn attributes(self) -> Result<ItemAttributes<'s>, AttributeError> {
ItemAttributes::new(self.server, self.entity)
}
#[doc(alias("m_iItemDefinitionIndex", "GetItemDefIndex"))]
pub fn definition(self) -> Result<Option<ItemDefinitionIndex>, WeaponError> {
check_live(self.entity)?;
let dll = self.server.server_game_dll()?;
attributes::item_definition(dll, self.entity).map_err(|_| WeaponError::UnsupportedLayout)
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
#[doc(alias("m_hOwner"))]
pub fn owner(self) -> Result<Option<EntityHandle>, WeaponError> {
check_live(self.entity)?;
let raw = unsafe {
self.entity
.as_ptr()
.byte_add(self.owner_offset)
.cast::<u32>()
.read()
};
let handle = EntityHandle::from_raw(raw);
Ok(handle.is_valid().then_some(handle))
}
#[doc(alias("GetSlot"))]
pub fn slot(self) -> Result<Option<WeaponSlot>, WeaponError> {
self.slot_raw().map(WeaponSlot::from_raw)
}
#[doc(alias("GetSlot"))]
pub fn slot_raw(self) -> Result<c_int, WeaponError> {
check_live(self.entity)?;
let weapon = self.entity.as_ptr().cast::<sys::CTFWeaponBase>();
Ok(unsafe {
vcall!(weapon as sys::CTFWeaponBase__bindgen_vtable => CTFWeaponBase_GetSlot())
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum WeaponError {
#[error(transparent)]
Attribute(#[from] AttributeError),
#[error("the game could not create the weapon (including an existing weapon of the same type)")]
CreationFailed,
#[error(transparent)]
CreationFailedNative(#[from] ItemGenerationError),
#[error("the weapon is not owned by this player")]
DifferentOwner,
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the entity is marked for deletion")]
MarkedForDeletion,
#[error("the player's absolute position could not be read")]
MissingOrigin,
#[error("weapon operations require a TF2 player")]
NotTfPlayer,
#[error("the entity is not a TF2 combat weapon")]
NotWeapon,
#[error("the game refused to detach or equip the weapon")]
Rejected,
#[error("the weapon slot is already occupied; use replace to exchange its weapon")]
SlotOccupied,
#[error("the weapon's datamap or networked variables do not match the sdk's layout")]
UnsupportedLayout,
#[error("the weapon's native slot is not the requested slot, or is outside 0..=255")]
WrongSlot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum WeaponSlot {
Primary = 0,
Secondary = 1,
Melee = 2,
Pda = 3,
Pda2 = 4,
Building = 5,
}
impl WeaponSlot {
pub const ALL: [Self; 6] = [
Self::Primary,
Self::Secondary,
Self::Melee,
Self::Pda,
Self::Pda2,
Self::Building,
];
pub const fn from_raw(raw: c_int) -> Option<Self> {
match raw {
0 => Some(Self::Primary),
1 => Some(Self::Secondary),
2 => Some(Self::Melee),
3 => Some(Self::Pda),
4 => Some(Self::Pda2),
5 => Some(Self::Building),
_ => None,
}
}
pub const fn to_raw(self) -> c_int {
self as c_int
}
}
impl IntoWeaponSlot for WeaponSlot {
fn into_weapon_slot(self) -> c_int {
self.to_raw()
}
}
fn apply_attributes<'s>(
token: SchemaToken<'s>,
weapon: Weapon<'s>,
attributes: &AttributeSet,
) -> Result<(), WeaponError> {
if attributes.is_empty() {
return Ok(());
}
let item = weapon.attributes()?;
attributes.apply(token, item)?;
item.reapply_provision(token)?;
Ok(())
}
fn check_live(entity: Entity<'_>) -> Result<(), WeaponError> {
if entity.is_marked_for_deletion() {
Err(WeaponError::MarkedForDeletion)
} else {
Ok(())
}
}
pub(crate) unsafe fn generate_item(
server: Server<'_>,
definition: ItemDefinitionIndex,
origin: Vector,
classname: Option<&CStr>,
) -> Result<NonNull<sys::CBaseEntity>, ItemGenerationError> {
let generation = unsafe { ItemGeneration::cached(server.game_server_factory().as_raw()) }?;
unsafe { generation.spawn(definition.get(), origin.into(), classname) }
}