mod native;
use crate::entities::{Entity, EntityHandle, data_field_offset, data_map_class};
use crate::{Game, InterfaceError, Server};
use std::ffi::CStr;
use std::ptr::NonNull;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ItemDefinitionIndex(u16);
impl ItemDefinitionIndex {
pub const BRASS_BEAST: Self = Self(312);
pub const IRON_BOMBER: Self = Self(1151);
pub const fn new(index: u16) -> Option<Self> {
if index == u16::MAX {
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 || !has_class(player, c"CTFPlayer") {
return Err(WeaponError::NotTfPlayer);
}
Ok(Self { server, player })
}
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 {
let vtable = player
.cast::<*const sys::CTFPlayer__bindgen_vtable>()
.read();
((*vtable).CTFPlayer_RemovePlayerItem)(player, weapon.entity.as_ptr().cast())
};
if !removed {
return Err(WeaponError::Rejected);
}
Ok(())
}
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()?;
if let Some(existing) = self.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 {
let vtable = player
.cast::<*const sys::CTFPlayer__bindgen_vtable>()
.read();
((*vtable).CTFPlayer_Weapon_Equip)(player, weapon.entity.as_ptr().cast());
}
if weapon.owner()? != Some(self.player.handle())
|| !self
.slot(slot)?
.is_some_and(|found| found.entity == weapon.entity)
{
return Err(WeaponError::Rejected);
}
Ok(())
}
pub unsafe fn give(self, classname: &CStr, subtype: i32) -> Result<Weapon<'s>, WeaponError> {
check_live(self.player)?;
validate_request(classname, subtype)?;
let player = self.player.as_ptr().cast::<sys::CTFPlayer>();
let give = unsafe {
let vtable = player
.cast::<*const sys::CTFPlayer__bindgen_vtable>()
.read();
(*vtable).CTFPlayer_GiveNamedItem1
};
unsafe {
self.give_with(None, || {
NonNull::new(give(
player,
classname.as_ptr(),
subtype,
std::ptr::null(),
true,
))
.ok_or(WeaponError::CreationFailed)
})
}
}
pub unsafe fn give_item(
self,
definition: ItemDefinitionIndex,
) -> Result<Weapon<'s>, WeaponError> {
unsafe { self.give_item_inner(definition, None) }
}
pub unsafe fn give_item_as(
self,
definition: ItemDefinitionIndex,
classname: &CStr,
) -> Result<Weapon<'s>, WeaponError> {
validate_request(classname, 0)?;
unsafe { self.give_item_inner(definition, Some(classname)) }
}
unsafe fn give_item_inner(
self,
definition: ItemDefinitionIndex,
classname: Option<&CStr>,
) -> Result<Weapon<'s>, WeaponError> {
check_live(self.player)?;
let origin = self.player.position().ok_or(WeaponError::MissingOrigin)?;
unsafe {
self.give_with(classname, || {
native::spawn(self.server, definition.get(), origin.into(), classname)
})
}
}
unsafe fn give_with(
self,
expected_classname: Option<&CStr>,
create: impl FnOnce() -> Result<NonNull<sys::CBaseEntity>, WeaponError>,
) -> Result<Weapon<'s>, WeaponError> {
check_live(self.player)?;
let tools = self.server.server_tools()?;
let mut occupied = [false; 256];
for (index, occupied) in occupied.iter_mut().enumerate() {
*occupied = self.slot(WeaponSlot(index as u8))?.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().and_then(|slot| {
if occupied[usize::from(slot.0)] {
Err(WeaponError::SlotOccupied)
} else {
self.equip(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: WeaponSlot,
classname: &CStr,
subtype: i32,
) -> Result<Weapon<'s>, WeaponError> {
validate_request(classname, subtype)?;
self.replace_with(slot, || unsafe { self.give(classname, subtype) })
}
pub unsafe fn replace_item(
self,
slot: WeaponSlot,
definition: ItemDefinitionIndex,
) -> Result<Weapon<'s>, WeaponError> {
self.replace_with(slot, || unsafe { self.give_item(definition) })
}
pub unsafe fn replace_item_as(
self,
slot: WeaponSlot,
definition: ItemDefinitionIndex,
classname: &CStr,
) -> Result<Weapon<'s>, WeaponError> {
validate_request(classname, 0)?;
self.replace_with(slot, || unsafe { self.give_item_as(definition, classname) })
}
fn replace_with(
self,
slot: WeaponSlot,
create: impl FnOnce() -> Result<Weapon<'s>, WeaponError>,
) -> Result<Weapon<'s>, WeaponError> {
let tools = self.server.server_tools()?;
let old = self.slot(slot)?;
if let Some(old) = old {
self.detach(old)?;
}
let replacement = create().and_then(|weapon| {
if weapon.slot()? == 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)
}
}
}
pub fn slot(self, slot: WeaponSlot) -> Result<Option<Weapon<'s>>, WeaponError> {
check_live(self.player)?;
let player = self.player.as_ptr().cast::<sys::CTFPlayer>();
let raw = unsafe {
let vtable = player
.cast::<*const sys::CTFPlayer__bindgen_vtable>()
.read();
((*vtable).CTFPlayer_Weapon_GetSlot)(player, i32::from(slot.0))
};
NonNull::new(raw)
.map(|raw| {
Weapon::new(self.server, unsafe { Entity::from_raw(raw.cast()) })
})
.transpose()
}
}
#[derive(Debug, Clone, Copy)]
pub struct Weapon<'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 || !has_class(entity, c"CTFWeaponBase") {
return Err(WeaponError::NotWeapon);
}
let owner_offset = entity
.data_maps()
.find(|map| data_map_class(map) == Some(c"CBaseCombatWeapon"))
.and_then(|map| data_field_offset(map, 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 {
entity,
owner_offset,
})
}
pub const fn entity(self) -> Entity<'s> {
self.entity
}
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))
}
pub fn slot(self) -> Result<WeaponSlot, WeaponError> {
check_live(self.entity)?;
let weapon = self.entity.as_ptr().cast::<sys::CTFWeaponBase>();
let index = unsafe {
let vtable = weapon
.cast::<*const sys::CTFWeaponBase__bindgen_vtable>()
.read();
((*vtable).CTFWeaponBase_GetSlot)(weapon)
};
u8::try_from(index)
.map(WeaponSlot)
.map_err(|_| WeaponError::WrongSlot)
}
}
#[derive(Debug, thiserror::Error)]
pub enum WeaponError {
#[error("weapon operations require a TF2 player")]
NotTfPlayer,
#[error("the entity is not a TF2 combat weapon")]
NotWeapon,
#[error("the entity is marked for deletion")]
MarkedForDeletion,
#[error("a weapon classname must start with tf_weapon_ and a subtype must be nonnegative")]
InvalidRequest,
#[error("the game could not create the weapon (including an existing weapon of the same type)")]
CreationFailed,
#[error("native TF2 item generation could not be resolved for this server build")]
NativeUnavailable,
#[error("the player's absolute position could not be read")]
MissingOrigin,
#[error("the weapon belongs to another player")]
DifferentOwner,
#[error("the weapon slot is already occupied; use replace to exchange its weapon")]
SlotOccupied,
#[error("the new weapon's native slot differs from the requested replacement slot")]
WrongSlot,
#[error("the game refused to detach or equip the weapon")]
Rejected,
#[error("the weapon's datamap does not describe its combat owner handle")]
UnsupportedLayout,
#[error(transparent)]
Interface(#[from] InterfaceError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WeaponSlot(pub u8);
impl WeaponSlot {
pub const BUILDING: Self = Self(5);
pub const MELEE: Self = Self(2);
pub const PDA: Self = Self(3);
pub const PDA2: Self = Self(4);
pub const PRIMARY: Self = Self(0);
pub const SECONDARY: Self = Self(1);
}
fn check_live(entity: Entity<'_>) -> Result<(), WeaponError> {
if entity.is_marked_for_deletion() {
Err(WeaponError::MarkedForDeletion)
} else {
Ok(())
}
}
fn has_class(entity: Entity<'_>, class: &CStr) -> bool {
entity
.data_maps()
.any(|map| data_map_class(map) == Some(class))
}
fn validate_request(classname: &CStr, subtype: i32) -> Result<(), WeaponError> {
let name = classname.to_bytes();
if !name.starts_with(b"tf_weapon_")
|| name.len() <= 10
|| !name
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
|| subtype < 0
{
return Err(WeaponError::InvalidRequest);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::InterfaceFactory;
use crate::entities::test_support::{base_entity_fields, data_map, field};
use crate::ffi::test_support::{mock_vtable, unexpected_call};
use std::cell::Cell;
use std::ffi::{c_char, c_void};
use std::mem::{offset_of, size_of};
use std::ptr::null_mut;
const EQUIP: usize =
offset_of!(sys::CTFPlayer__bindgen_vtable, CTFPlayer_Weapon_Equip) / size_of::<usize>();
const GET_SLOT: usize =
offset_of!(sys::CTFPlayer__bindgen_vtable, CTFPlayer_Weapon_GetSlot) / size_of::<usize>();
const GIVE: usize =
offset_of!(sys::CTFPlayer__bindgen_vtable, CTFPlayer_GiveNamedItem1) / size_of::<usize>();
const REMOVE: usize =
offset_of!(sys::CTFPlayer__bindgen_vtable, CTFPlayer_RemovePlayerItem) / size_of::<usize>();
const WEAPON_SLOT: usize =
offset_of!(sys::CTFWeaponBase__bindgen_vtable, CTFWeaponBase_GetSlot) / size_of::<usize>();
#[repr(C)]
struct FakeEntity {
vtable: *const *const (),
map: *mut sys::datamap_t,
weapon: *mut sys::CBaseEntity,
slot: i32,
padding: i32,
flags: i32,
owner: u32,
handle: u32,
owner_entity: u32,
second_weapon: *mut sys::CBaseEntity,
equip_calls: usize,
}
thread_local! {
static TOOLS: Cell<*mut c_void> = const { Cell::new(null_mut()) };
static GIVE_RESULT: Cell<*mut sys::CBaseEntity> = const { Cell::new(null_mut()) };
static INVENTORY_FULL: Cell<bool> = const { Cell::new(false) };
static REJECT_DETACH: Cell<bool> = const { Cell::new(false) };
static NETWORKABLE: Cell<*mut sys::IServerNetworkable> = const { Cell::new(null_mut()) };
}
unsafe extern "C" fn classname(_: *const sys::IServerNetworkable) -> *const c_char {
c"tf_weapon_bottle".as_ptr()
}
unsafe extern "C" fn datamap(entity: *mut sys::CBaseEntity) -> *mut sys::datamap_t {
unsafe { (*entity.cast::<FakeEntity>()).map }
}
unsafe extern "C" fn detach(
player: *mut sys::CTFPlayer,
weapon: *mut sys::CBaseCombatWeapon,
) -> bool {
if REJECT_DETACH.get() {
return false;
}
unsafe {
let player = player.cast::<FakeEntity>();
let weapon = weapon.cast::<sys::CBaseEntity>();
if (*player).weapon == weapon {
(*player).weapon = null_mut();
} else if (*player).second_weapon == weapon {
(*player).second_weapon = null_mut();
} else {
return false;
}
(*weapon.cast::<FakeEntity>()).owner = EntityHandle::INVALID.to_raw();
true
}
}
unsafe extern "C" fn equip(player: *mut sys::CTFPlayer, weapon: *mut sys::CBaseCombatWeapon) {
unsafe {
let player = player.cast::<FakeEntity>();
let weapon = weapon.cast::<sys::CBaseEntity>();
(*player).equip_calls += 1;
if !INVENTORY_FULL.get() {
if (*player).weapon.is_null() {
(*player).weapon = weapon;
} else {
(*player).second_weapon = weapon;
}
}
(*weapon.cast::<FakeEntity>()).owner = (*player).handle;
(*weapon.cast::<FakeEntity>()).owner_entity = (*player).handle;
}
}
unsafe extern "C" fn factory(name: *const c_char, _: *mut i32) -> *mut c_void {
if unsafe { CStr::from_ptr(name) } == c"VSERVERTOOLS003" {
TOOLS.get()
} else {
null_mut()
}
}
#[test]
fn failed_replacement_restores_inventory_and_rejected_pickup_does_not_leak_weapon() {
let base = data_map(c"CBaseEntity", Vec::from(base_entity_fields()), null_mut());
let player_map = data_map(c"CTFPlayer", vec![], base);
let weapon_map = weapon_map(base);
let mut player_table = vec![std::ptr::null(); GIVE + 1];
player_table[sys::CBASEENTITY_DATAMAP_VTABLE_SLOT] = datamap as *const ();
player_table[GET_SLOT] = inventory_slot as *const ();
player_table[GIVE] = give as *const ();
player_table[EQUIP] = equip as *const ();
player_table[REMOVE] = detach as *const ();
let handle_slot = offset_of!(
sys::IServerUnknown__bindgen_vtable,
IServerUnknown_GetRefEHandle
) / size_of::<usize>();
player_table[handle_slot] = handle as *const ();
let mut weapon_table = vec![std::ptr::null(); WEAPON_SLOT + 1];
weapon_table[sys::CBASEENTITY_DATAMAP_VTABLE_SLOT] = datamap as *const ();
weapon_table[WEAPON_SLOT] = weapon_slot as *const ();
weapon_table[handle_slot] = handle as *const ();
let networkable_slot = offset_of!(
sys::IServerUnknown__bindgen_vtable,
IServerUnknown_GetNetworkable
) / size_of::<usize>();
weapon_table[networkable_slot] = networkable as *const ();
let networkable_vtable = unsafe {
mock_vtable::<sys::IServerNetworkable__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).IServerNetworkable_GetClassName).write(classname);
},
)
};
let mut networkable = sys::IServerNetworkable {
vtable_: &*networkable_vtable,
};
NETWORKABLE.set(&raw mut networkable);
let mut old = FakeEntity {
vtable: weapon_table.as_ptr(),
map: weapon_map,
weapon: null_mut(),
slot: 2,
padding: 0,
flags: 0,
owner: 1,
handle: 2,
owner_entity: 1,
second_weapon: null_mut(),
equip_calls: 0,
};
let old_ptr = (&raw mut old).cast();
let mut fresh = FakeEntity {
vtable: weapon_table.as_ptr(),
map: weapon_map,
weapon: null_mut(),
slot: 2,
padding: 0,
flags: 0,
owner: EntityHandle::INVALID.to_raw(),
handle: 3,
owner_entity: 0,
second_weapon: null_mut(),
equip_calls: 0,
};
let mut player = FakeEntity {
vtable: player_table.as_ptr(),
map: player_map,
weapon: old_ptr,
slot: 2,
padding: 0,
flags: 0,
owner: 0,
handle: 1,
owner_entity: 0,
second_weapon: null_mut(),
equip_calls: 0,
};
let tools_vtable = unsafe {
mock_vtable::<sys::IServerTools__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).IServerTools_RemoveEntity).write(remove);
},
)
};
let mut tools = sys::IServerTools {
vtable_: &*tools_vtable,
};
TOOLS.set((&raw mut tools).cast());
GIVE_RESULT.set(null_mut());
let scope = ();
let factory = InterfaceFactory::new(factory);
let server = unsafe { Server::new(factory, factory, Game::TeamFortress2, &scope) };
let entity = unsafe { Entity::from_raw(NonNull::from(&mut player).cast()) };
let inventory = PlayerWeapons::new(server, entity).unwrap();
let weapon = inventory.slot(WeaponSlot::MELEE).unwrap().unwrap();
inventory.detach(weapon).unwrap();
assert_eq!(weapon.owner().unwrap(), None);
assert_eq!(
old.owner_entity, 1,
"native detach leaves the unrelated base owner intact"
);
assert!(inventory.slot(WeaponSlot::MELEE).unwrap().is_none());
inventory.equip(weapon).unwrap();
assert_eq!(
inventory
.slot(WeaponSlot::MELEE)
.unwrap()
.unwrap()
.entity
.as_ptr(),
old_ptr
);
assert_eq!(player.equip_calls, 1);
assert!(matches!(
unsafe { inventory.replace(WeaponSlot::MELEE, c"tf_weapon_missing", 0) },
Err(WeaponError::CreationFailed)
));
assert_eq!(
inventory
.slot(WeaponSlot::MELEE)
.unwrap()
.unwrap()
.entity
.as_ptr(),
old_ptr
);
assert_eq!(weapon.owner().unwrap(), Some(entity.handle()));
assert_eq!(player.equip_calls, 2);
assert_eq!(old.flags, 0);
unsafe { (&raw mut player.weapon).write(null_mut()) };
inventory.equip(weapon).unwrap();
assert_eq!(
inventory
.slot(WeaponSlot::MELEE)
.unwrap()
.unwrap()
.entity
.as_ptr(),
old_ptr
);
assert_eq!(player.equip_calls, 3);
GIVE_RESULT.set((&raw mut fresh).cast());
assert!(matches!(
unsafe { inventory.give(c"tf_weapon_bottle", 0) },
Err(WeaponError::SlotOccupied)
));
assert_eq!(
inventory
.slot(WeaponSlot::MELEE)
.unwrap()
.unwrap()
.entity
.as_ptr(),
old_ptr
);
assert!(player.second_weapon.is_null());
assert_eq!(fresh.owner, EntityHandle::INVALID.to_raw());
assert_eq!(fresh.flags, 1, "the rejected new entity must be removed");
assert_eq!(old.flags, 0);
unsafe {
(&raw mut player.weapon).write(null_mut());
(&raw mut fresh.flags).write(0);
}
INVENTORY_FULL.set(true);
assert!(matches!(
unsafe { inventory.give(c"tf_weapon_bottle", 0) },
Err(WeaponError::Rejected)
));
assert!(inventory.slot(WeaponSlot::MELEE).unwrap().is_none());
assert_eq!(
fresh.flags, 1,
"failed native detach must not leak the new entity"
);
INVENTORY_FULL.set(false);
unsafe {
(&raw mut player.weapon).write(old_ptr);
(&raw mut fresh.flags).write(0);
(&raw mut fresh.owner).write(EntityHandle::INVALID.to_raw());
}
let fresh_ptr = NonNull::from(&mut fresh).cast();
assert!(matches!(
unsafe { inventory.give_with(None, || Ok(fresh_ptr)) },
Err(WeaponError::SlotOccupied)
));
assert_eq!(player.weapon, old_ptr);
assert_eq!(fresh.flags, 1);
assert_eq!(old.flags, 0);
assert!(matches!(
inventory.replace_with(WeaponSlot::MELEE, || Err(WeaponError::CreationFailed)),
Err(WeaponError::CreationFailed)
));
assert_eq!(
player.weapon, old_ptr,
"missing item definition restores the old weapon"
);
unsafe {
(&raw mut fresh.flags).write(0);
(&raw mut fresh.slot).write(0);
}
assert!(matches!(
inventory.replace_with(WeaponSlot::MELEE, || unsafe {
inventory.give_with(None, || Ok(fresh_ptr))
}),
Err(WeaponError::WrongSlot)
));
assert_eq!(
player.weapon, old_ptr,
"wrong item slot restores the old weapon"
);
assert_eq!(fresh.flags, 1, "wrong-slot item is removed");
assert_eq!(fresh.owner, EntityHandle::INVALID.to_raw());
unsafe {
(&raw mut fresh.flags).write(0);
}
let rejected_detach = inventory.replace_with(WeaponSlot::MELEE, || {
let replacement = unsafe { inventory.give_with(None, || Ok(fresh_ptr)) }?;
REJECT_DETACH.set(true);
Ok(replacement)
});
REJECT_DETACH.set(false);
assert!(matches!(rejected_detach, Err(WeaponError::Rejected)));
assert_eq!(
fresh.flags, 1,
"wrong-slot item is removed even when detach rejects"
);
assert_eq!(
inventory
.slot(WeaponSlot::MELEE)
.unwrap()
.unwrap()
.entity()
.as_ptr(),
old_ptr
);
unsafe {
(&raw mut player.weapon).write(old_ptr);
(&raw mut player.second_weapon).write(null_mut());
(&raw mut fresh.flags).write(0);
(&raw mut fresh.owner).write(EntityHandle::INVALID.to_raw());
(&raw mut fresh.map).write(data_map(c"CEconWearable", vec![], base));
}
assert!(matches!(
inventory.replace_with(WeaponSlot::MELEE, || unsafe {
inventory.give_with(None, || Ok(fresh_ptr))
}),
Err(WeaponError::NotWeapon)
));
assert_eq!(
player.weapon, old_ptr,
"cosmetic item restores the old weapon"
);
assert_eq!(fresh.flags, 1, "nonweapon item is removed");
assert_eq!(old.flags, 0);
unsafe {
(&raw mut fresh.flags).write(0);
(&raw mut fresh.map).write(weapon_map);
(&raw mut fresh.slot).write(2);
}
assert!(matches!(
inventory.replace_with(WeaponSlot::MELEE, || unsafe {
inventory.give_with(Some(c"tf_weapon_sdk_missing"), || Ok(fresh_ptr))
}),
Err(WeaponError::CreationFailed)
));
assert_eq!(fresh.flags, 1, "native classname fallback must be removed");
assert_eq!(
player.weapon, old_ptr,
"override mismatch restores the old weapon"
);
unsafe {
(&raw mut fresh.flags).write(0);
}
let replacement = inventory
.replace_with(WeaponSlot::MELEE, || unsafe {
inventory.give_with(Some(c"tf_weapon_bottle"), || Ok(fresh_ptr))
})
.unwrap();
assert_eq!(replacement.entity().as_ptr(), fresh_ptr.as_ptr());
assert_eq!(player.weapon, fresh_ptr.as_ptr());
assert_eq!(fresh.owner, 1);
assert_eq!(fresh.flags, 0);
assert_eq!(
old.flags, 1,
"successful economy replacement removes the old weapon"
);
TOOLS.set(null_mut());
GIVE_RESULT.set(null_mut());
NETWORKABLE.set(null_mut());
}
unsafe extern "C" fn give(
player: *mut sys::CTFPlayer,
_: *const c_char,
_: i32,
item: *const sys::CEconItemView,
force: bool,
) -> *mut sys::CBaseEntity {
assert!(
item.is_null(),
"stock generation requires a null CEconItemView"
);
assert!(force, "the requested classname must not be translated");
let weapon = GIVE_RESULT.get();
if !weapon.is_null() {
unsafe { equip(player, weapon.cast()) };
}
weapon
}
unsafe extern "C" fn handle(entity: *const sys::IServerUnknown) -> *const sys::CBaseHandle {
unsafe { (&raw const (*entity.cast::<FakeEntity>()).handle).cast() }
}
unsafe extern "C" fn inventory_slot(
entity: *const sys::CTFPlayer,
slot: i32,
) -> *mut sys::CBaseCombatWeapon {
unsafe {
let entity = entity.cast::<FakeEntity>();
for weapon in [(*entity).second_weapon, (*entity).weapon] {
if !weapon.is_null() && (*weapon.cast::<FakeEntity>()).slot == slot {
return weapon.cast();
}
}
null_mut()
}
}
#[test]
fn native_inventory_slots_use_validated_classes_and_refuse_deleted_entities() {
assert_eq!(
std::mem::offset_of!(FakeEntity, flags),
crate::entities::test_support::MOCK_EFLAGS_OFFSET
);
let base = data_map(c"CBaseEntity", Vec::from(base_entity_fields()), null_mut());
let player_map = data_map(c"CTFPlayer", vec![], base);
let weapon_map = weapon_map(base);
let mut player_table = vec![std::ptr::null(); GIVE + 1];
player_table[sys::CBASEENTITY_DATAMAP_VTABLE_SLOT] = datamap as *const ();
player_table[GET_SLOT] = inventory_slot as *const ();
let mut weapon_table = vec![std::ptr::null(); WEAPON_SLOT + 1];
weapon_table[sys::CBASEENTITY_DATAMAP_VTABLE_SLOT] = datamap as *const ();
weapon_table[WEAPON_SLOT] = weapon_slot as *const ();
let mut weapon = FakeEntity {
vtable: weapon_table.as_ptr(),
map: weapon_map,
weapon: null_mut(),
slot: 2,
padding: 0,
flags: 0,
owner: 1,
handle: 2,
owner_entity: 1,
second_weapon: null_mut(),
equip_calls: 0,
};
let raw_weapon = (&raw mut weapon).cast();
let mut player = FakeEntity {
vtable: player_table.as_ptr(),
map: player_map,
weapon: raw_weapon,
slot: 2,
padding: 0,
flags: 0,
owner: 0,
handle: 1,
owner_entity: 0,
second_weapon: null_mut(),
equip_calls: 0,
};
let scope = ();
let factory = InterfaceFactory::new(factory);
let server = unsafe { Server::new(factory, factory, Game::TeamFortress2, &scope) };
let entity = unsafe { Entity::from_raw(NonNull::from(&mut player).cast()) };
let inventory = PlayerWeapons::new(server, entity).unwrap();
let found = inventory.slot(WeaponSlot::MELEE).unwrap().unwrap();
assert_eq!(found.entity.as_ptr(), raw_weapon);
assert_eq!(found.slot().unwrap(), WeaponSlot::MELEE);
assert!(inventory.slot(WeaponSlot::PRIMARY).unwrap().is_none());
assert!(matches!(
PlayerWeapons::new(server, found.entity()),
Err(WeaponError::NotTfPlayer)
));
assert!(matches!(
Weapon::new(server, entity),
Err(WeaponError::NotWeapon)
));
unsafe { (&raw mut player.flags).write(1) };
assert!(matches!(
inventory.slot(WeaponSlot::MELEE),
Err(WeaponError::MarkedForDeletion)
));
}
unsafe extern "C" fn networkable(_: *mut sys::IServerUnknown) -> *mut sys::IServerNetworkable {
NETWORKABLE.get()
}
unsafe extern "C" fn remove(_: *mut sys::IServerTools, entity: *mut sys::CBaseEntity) {
unsafe {
(*entity.cast::<FakeEntity>()).flags |= 1;
}
}
#[test]
fn weapon_creation_rejects_nonweapon_entities_and_negative_subtypes() {
assert_eq!(ItemDefinitionIndex::new(0).unwrap().get(), 0);
assert_eq!(ItemDefinitionIndex::new(65534).unwrap().get(), 65534);
assert!(ItemDefinitionIndex::new(65535).is_none());
assert_eq!(ItemDefinitionIndex::IRON_BOMBER.get(), 1151);
assert_eq!(ItemDefinitionIndex::BRASS_BEAST.get(), 312);
for classname in [
c"point_servercommand",
c"tf_weapon_",
c"tf_weapon_rocketlauncher;quit",
c"",
] {
assert!(matches!(
validate_request(classname, 0),
Err(WeaponError::InvalidRequest)
));
}
assert!(matches!(
validate_request(c"tf_weapon_rocketlauncher", -1),
Err(WeaponError::InvalidRequest)
));
assert!(validate_request(c"tf_weapon_rocketlauncher", 0).is_ok());
}
fn weapon_map(base: *mut sys::datamap_t) -> *mut sys::datamap_t {
let mut owner = field();
owner.fieldName = c"m_hOwner".as_ptr();
owner.fieldType = sys::_fieldtypes_FIELD_EHANDLE;
owner.fieldOffset[0] = offset_of!(FakeEntity, owner) as i32;
owner.fieldSize = 1;
owner.fieldSizeInBytes = 4;
let combat = data_map(c"CBaseCombatWeapon", vec![owner], base);
data_map(c"CTFWeaponBase", vec![], combat)
}
unsafe extern "C" fn weapon_slot(entity: *const sys::CTFWeaponBase) -> i32 {
unsafe { (*entity.cast::<FakeEntity>()).slot }
}
}