#[cfg(test)]
#[path = "../../tests/tf2/weapons.rs"]
mod tests;
pub mod identity;
use crate::datatables::NetPropError;
use crate::entities::{Entity, EntityHandle};
use crate::math::Vector;
use crate::tf2::attributes::{self, AttributeError, AttributeSet, ItemAttributes, SchemaToken};
use crate::tf2::item_schema::{ItemLevel, ItemQuality};
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("SpawnItem", "AddAttribute"))]
pub unsafe fn exchange_item_with(
self,
token: SchemaToken<'s>,
definition: ItemDefinitionIndex,
classname: Option<&CStr>,
attributes: &AttributeSet,
) -> Result<Weapon<'s>, WeaponError> {
check_live(self.player)?;
let origin = self.player.position().ok_or(WeaponError::MissingOrigin)?;
unsafe {
self.exchange_with(
classname,
|| {
generate_quality_item(
self.server,
definition,
origin,
classname,
ItemQuality::Unique,
ItemLevel::DEFAULT,
)
.map_err(WeaponError::CreationFailedNative)
},
|weapon| apply_attributes(token, weapon, attributes),
)
}
}
unsafe fn exchange_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 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 old = match weapon.slot_raw().and_then(|slot| self.get_slot(slot)) {
Ok(old) => old.filter(|old| old.entity != entity),
Err(error) => {
let _ = tools.remove(entity);
return Err(error);
}
};
if let Some(old) = old
&& let Err(error) = self.detach(old)
{
let _ = tools.remove(entity);
return Err(error);
}
if let Err(error) = self.equip(weapon).and_then(|()| finish(weapon)) {
match weapon.owner() {
Ok(Some(owner)) if owner == self.player.handle() => {
let _ = self.detach(weapon);
let _ = tools.remove(entity);
}
Ok(None) => {
let _ = tools.remove(entity);
}
Ok(Some(_)) | Err(_) => {}
}
if let Some(old) = old {
self.equip(old)?;
}
return Err(error);
}
if let Some(old) = old {
let _ = tools.remove(old.entity);
}
Ok(weapon)
}
#[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.give_item_with_quality(definition, ItemQuality::Unique, ItemLevel::DEFAULT) }
}
#[doc(alias("SpawnItem"))]
pub unsafe fn give_item_with_quality(
self,
definition: ItemDefinitionIndex,
quality: ItemQuality,
level: ItemLevel,
) -> Result<Weapon<'s>, WeaponError> {
unsafe { self.spawn_item(definition, None, quality, level, |_| 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),
ItemQuality::Unique,
ItemLevel::DEFAULT,
|_| 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,
ItemQuality::Unique,
ItemLevel::DEFAULT,
|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>,
quality: ItemQuality,
level: ItemLevel,
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_quality_item(
self.server,
definition,
origin,
classname,
quality,
level,
)
.map_err(WeaponError::CreationFailedNative)
},
finish,
)
}
}
#[doc(alias("m_hActiveWeapon", "GetActiveWeapon"))]
pub fn active(self) -> Result<Option<Weapon<'s>>, WeaponError> {
let handle = self
.server
.server_game_dll()?
.entity_net_prop(self.player, c"m_hActiveWeapon")?
.get_handle(self.player)?;
match self.server.server_tools()?.entity_by_handle(handle) {
Some(weapon) => weapon_of(self.server, weapon),
None => Ok(None),
}
}
#[doc(alias("m_hMyWeapons", "GetWeapon"))]
pub fn all(self) -> Result<Vec<Weapon<'s>>, WeaponError> {
let weapons = self
.server
.server_game_dll()?
.entity_net_prop(self.player, c"m_hMyWeapons")?;
let tools = self.server.server_tools()?;
let count = weapons
.element_count()
.ok_or_else(|| NetPropError::NotAnArray {
name: String::from("m_hMyWeapons"),
kind: weapons.prop().kind(),
})?;
let mut all = Vec::new();
for index in 0..count {
let handle = weapons.element(index)?.get_handle(self.player)?;
if let Some(weapon) = tools.entity_by_handle(handle) {
all.extend(weapon_of(self.server, weapon)?);
}
}
Ok(all)
}
#[doc(alias("Weapon_Switch"))]
pub fn switch_to(self, weapon: Weapon<'s>) -> Result<bool, 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>();
Ok(unsafe {
vcall!(player as sys::CTFPlayer__bindgen_vtable => CTFPlayer_Weapon_Switch(
weapon.entity.as_ptr().cast(),
0,
))
})
}
}
#[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_iClip1"))]
pub fn clip(self) -> Result<c_int, WeaponError> {
check_live(self.entity)?;
let offset = self.clip_offset()?;
Ok(unsafe { self.entity.as_ptr().byte_add(offset).cast::<c_int>().read() })
}
fn clip_offset(self) -> Result<usize, WeaponError> {
self.entity
.data_maps()
.find(|map| map.class_name() == Some(c"CBaseCombatWeapon"))
.and_then(|map| map.field_offset(c"m_iClip1", sys::_fieldtypes_FIELD_INTEGER))
.filter(|offset| *offset < 65_536 && offset.is_multiple_of(align_of::<c_int>()))
.ok_or(WeaponError::UnsupportedLayout)
}
#[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("IsEnergyWeapon"))]
pub fn is_energy_weapon(self) -> Result<bool, WeaponError> {
check_live(self.entity)?;
let weapon = self.entity.as_ptr().cast::<sys::CTFWeaponBase>();
Ok(unsafe {
vcall!(weapon as sys::CTFWeaponBase__bindgen_vtable => CTFWeaponBase_IsEnergyWeapon())
})
}
#[doc(alias("GetMaxClip1"))]
pub fn max_clip(self) -> Result<Option<c_int>, WeaponError> {
check_live(self.entity)?;
let weapon = self.entity.as_ptr().cast::<sys::CTFWeaponBase>();
let max = unsafe {
vcall!(weapon as sys::CTFWeaponBase__bindgen_vtable => CTFWeaponBase_GetMaxClip1())
};
Ok((max >= 0).then_some(max))
}
#[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())
})
}
#[doc(alias("m_iClip1"))]
pub fn set_clip(self, clip: c_int) -> Result<(), WeaponError> {
if self.is_energy_weapon()? {
return Err(WeaponError::NoClip);
}
let max = self.max_clip()?.ok_or(WeaponError::NoClip)?;
if !(0..=max).contains(&clip) {
return Err(WeaponError::InvalidClip { clip, max });
}
let offset = self.clip_offset()?;
let changed = u16::try_from(offset).map_err(|_| WeaponError::UnsupportedLayout)?;
let engine = self.server.valve_engine()?;
let edict = self.entity.edict().ok_or(WeaponError::NotWeapon)?;
unsafe {
self.entity
.as_ptr()
.byte_add(offset)
.cast::<c_int>()
.write(clip)
};
edict.state_changed(engine, changed);
Ok(())
}
}
#[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("cannot fill a clip of at most {max} rounds with {clip}")]
InvalidClip {
clip: c_int,
max: c_int,
},
#[error("the entity is marked for deletion")]
MarkedForDeletion,
#[error("the player's absolute position could not be read")]
MissingOrigin,
#[error(transparent)]
NetProp(#[from] NetPropError),
#[error("the weapon has no clip")]
NoClip,
#[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> {
unsafe {
generate_quality_item(
server,
definition,
origin,
classname,
ItemQuality::Unique,
ItemLevel::DEFAULT,
)
}
}
pub(crate) unsafe fn generate_quality_item(
server: Server<'_>,
definition: ItemDefinitionIndex,
origin: Vector,
classname: Option<&CStr>,
quality: ItemQuality,
level: ItemLevel,
) -> Result<NonNull<sys::CBaseEntity>, ItemGenerationError> {
let generation = unsafe { ItemGeneration::cached(server.game_server_factory().as_raw()) }?;
unsafe {
generation.spawn_with_quality(
definition.get(),
origin.into(),
classname,
c_int::from(level.get()),
quality.to_raw(),
)
}
}
fn weapon_of<'s>(
server: Server<'s>,
entity: Entity<'s>,
) -> Result<Option<Weapon<'s>>, WeaponError> {
match Weapon::new(server, entity) {
Ok(weapon) => Ok(Some(weapon)),
Err(WeaponError::NotWeapon) => Ok(None),
Err(error) => Err(error),
}
}