use crate::entities::EntityHandle;
use crate::interfaces::ServerTools;
use crate::server::Game;
use crate::test_support::entities::{MOCK_EFLAGS_OFFSET, MOCK_NAME_OFFSET};
use crate::test_support::leak;
use sdk_raw::entities::EFL_KILLME;
use sdk_raw::test_support::{mock_vtable, unexpected_call};
use std::cell::{Cell, RefCell};
use std::ffi::{CStr, CString, c_char, c_int};
use std::ptr::{NonNull, null_mut};
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Search {
Class(*mut sys::CBaseEntity, CString),
Model(*mut sys::CBaseEntity, CString),
Sphere(*mut sys::CBaseEntity, [f32; 3], f32),
}
thread_local! {
static CLASSES: RefCell<Vec<CString>> = const { RefCell::new(Vec::new()) };
static CREATED: Cell<*mut sys::CBaseEntity> = const { Cell::new(null_mut()) };
static FOUND: RefCell<Vec<*mut sys::CBaseEntity>> = const { RefCell::new(Vec::new()) };
static KEYS: RefCell<Vec<(*mut sys::CBaseEntity, CString, CString)>> =
const { RefCell::new(Vec::new()) };
static LIST: Cell<*mut sys::CGlobalEntityList> = const { Cell::new(null_mut()) };
static MOVE_TYPES: RefCell<Vec<(*mut sys::CBaseEntity, c_int, Option<c_int>)>> =
const { RefCell::new(Vec::new()) };
static POOL: RefCell<Vec<CString>> = const { RefCell::new(Vec::new()) };
static REFUSED: RefCell<Option<CString>> = const { RefCell::new(None) };
static REMOVED: RefCell<Vec<*mut sys::CBaseEntity>> = const { RefCell::new(Vec::new()) };
static SEARCHES: RefCell<Vec<Search>> = const { RefCell::new(Vec::new()) };
static SPAWN_REMOVES: Cell<bool> = const { Cell::new(false) };
static SPAWNED: RefCell<Vec<*mut sys::CBaseEntity>> = const { RefCell::new(Vec::new()) };
static WORLD: Cell<*mut sys::CBaseEntity> = const { Cell::new(null_mut()) };
}
pub(crate) struct MockTools {
raw: *mut sys::IServerTools,
}
impl MockTools {
pub(crate) fn new(world: *mut sys::CBaseEntity) -> Self {
let vtable = unsafe {
mock_vtable::<sys::IServerTools__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).IServerTools_CreateEntityByName)
.write(create_entity_by_name);
(&raw mut (*vtable).IServerTools_DispatchSpawn).write(dispatch_spawn);
(&raw mut (*vtable).IServerTools_FindEntityByClassname)
.write(find_by_class_name);
(&raw mut (*vtable).IServerTools_FindEntityByModel).write(find_by_model);
(&raw mut (*vtable).IServerTools_FindEntityInSphere).write(find_in_sphere);
(&raw mut (*vtable).IServerTools_GetBaseEntityByEntIndex)
.write(entity_by_index);
(&raw mut (*vtable).IServerTools_GetEntityList).write(entity_list);
(&raw mut (*vtable).IServerTools_RemoveEntity).write(remove_entity);
(&raw mut (*vtable).IServerTools_SetKeyValue).write(set_key_value);
(&raw mut (*vtable).IServerTools_SetMoveType).write(set_move_type);
(&raw mut (*vtable).IServerTools_SetMoveType1).write(set_move_type_and_collide);
},
)
};
let list = Box::<sys::CGlobalEntityList>::new_zeroed();
CLASSES.take();
CREATED.set(null_mut());
FOUND.take();
KEYS.take();
LIST.set(Box::into_raw(unsafe { list.assume_init() }));
MOVE_TYPES.take();
REFUSED.take();
REMOVED.take();
SEARCHES.take();
SPAWN_REMOVES.set(false);
SPAWNED.take();
WORLD.set(world);
Self {
raw: leak(sys::IServerTools {
vtable_: Box::into_raw(vtable),
}),
}
}
pub(crate) fn list(&self, entity: *mut sys::CBaseEntity, handle: EntityHandle) {
let index = handle.index().unwrap();
unsafe {
let info = (&raw mut (*LIST.get())._base.m_EntPtrArray)
.cast::<sys::CEntInfo>()
.add(index);
(&raw mut (*info).m_pEntity).write(entity.cast());
(&raw mut (*info).m_SerialNumber).write(handle.serial_number().try_into().unwrap());
}
}
pub(crate) fn set_created(&self, entity: *mut sys::CBaseEntity) {
CREATED.set(entity);
}
pub(crate) fn set_found(&self, entities: Vec<*mut sys::CBaseEntity>) {
FOUND.set(entities);
}
pub(crate) fn set_refused(&self, key: &CStr) {
REFUSED.set(Some(key.to_owned()));
}
pub(crate) fn set_spawn_removes(&self, removes: bool) {
SPAWN_REMOVES.set(removes);
}
pub(crate) fn take_classes(&self) -> Vec<CString> {
CLASSES.take()
}
pub(crate) fn take_keys(&self) -> Vec<(*mut sys::CBaseEntity, CString, CString)> {
KEYS.take()
}
pub(crate) fn take_move_types(&self) -> Vec<(*mut sys::CBaseEntity, c_int, Option<c_int>)> {
MOVE_TYPES.take()
}
pub(crate) fn take_removed(&self) -> Vec<*mut sys::CBaseEntity> {
REMOVED.take()
}
pub(crate) fn take_searches(&self) -> Vec<Search> {
SEARCHES.take()
}
pub(crate) fn take_spawned(&self) -> Vec<*mut sys::CBaseEntity> {
SPAWNED.take()
}
pub(crate) fn tools(&self) -> ServerTools<'static> {
unsafe { ServerTools::from_raw(NonNull::new(self.raw).unwrap(), Game::TeamFortress2) }
}
}
unsafe extern "C" fn create_entity_by_name(
_: *mut sys::IServerTools,
class: *const c_char,
) -> *mut sys::CBaseEntity {
let class = unsafe { CStr::from_ptr(class) }.to_owned();
CLASSES.with_borrow_mut(|classes| classes.push(class));
CREATED.get()
}
unsafe extern "C" fn dispatch_spawn(_: *mut sys::IServerTools, entity: *mut sys::CBaseEntity) {
SPAWNED.with_borrow_mut(|spawned| spawned.push(entity));
if SPAWN_REMOVES.get() {
unsafe { mark_for_deletion(entity) };
}
}
unsafe extern "C" fn entity_by_index(
_: *mut sys::IServerTools,
index: c_int,
) -> *mut sys::CBaseEntity {
if index == 0 { WORLD.get() } else { null_mut() }
}
unsafe extern "C" fn entity_list(_: *mut sys::IServerTools) -> *mut sys::CGlobalEntityList {
LIST.get()
}
fn find_after(after: *mut sys::CBaseEntity) -> *mut sys::CBaseEntity {
FOUND.with_borrow(|found| {
let next = if after.is_null() {
0
} else {
found.iter().position(|&entity| entity == after).unwrap() + 1
};
found.get(next).copied().unwrap_or(null_mut())
})
}
unsafe extern "C" fn find_by_class_name(
_: *mut sys::IServerTools,
after: *mut sys::CBaseEntity,
class: *const c_char,
) -> *mut sys::CBaseEntity {
let class = unsafe { CStr::from_ptr(class) }.to_owned();
SEARCHES.with_borrow_mut(|searches| searches.push(Search::Class(after, class)));
find_after(after)
}
unsafe extern "C" fn find_by_model(
_: *mut sys::IServerTools,
after: *mut sys::CBaseEntity,
model: *const c_char,
) -> *mut sys::CBaseEntity {
let model = unsafe { CStr::from_ptr(model) }.to_owned();
SEARCHES.with_borrow_mut(|searches| searches.push(Search::Model(after, model)));
find_after(after)
}
unsafe extern "C" fn find_in_sphere(
_: *mut sys::IServerTools,
after: *mut sys::CBaseEntity,
center: *const sys::Vector,
radius: f32,
) -> *mut sys::CBaseEntity {
let sys::Vector { x, y, z } = unsafe { center.read() };
SEARCHES.with_borrow_mut(|searches| searches.push(Search::Sphere(after, [x, y, z], radius)));
find_after(after)
}
unsafe fn mark_for_deletion(entity: *mut sys::CBaseEntity) {
unsafe {
entity
.byte_add(MOCK_EFLAGS_OFFSET)
.cast::<c_int>()
.write(EFL_KILLME)
};
}
unsafe extern "C" fn remove_entity(_: *mut sys::IServerTools, entity: *mut sys::CBaseEntity) {
REMOVED.with_borrow_mut(|removed| removed.push(entity));
unsafe { mark_for_deletion(entity) };
}
unsafe extern "C" fn set_key_value(
_: *mut sys::IServerTools,
entity: *mut sys::CBaseEntity,
key: *const c_char,
value: *const c_char,
) -> bool {
let (key, value) = unsafe { (CStr::from_ptr(key), CStr::from_ptr(value)) };
if entity != WORLD.get() {
KEYS.with_borrow_mut(|keys| keys.push((entity, key.to_owned(), value.to_owned())));
}
if REFUSED.with_borrow(|refused| refused.as_deref() == Some(key)) {
return false;
}
if key == c"targetname" {
let pooled = POOL.with_borrow_mut(|pool| {
let index = pool
.iter()
.position(|pooled| pooled.to_bytes().eq_ignore_ascii_case(value.to_bytes()))
.unwrap_or_else(|| {
pool.push(value.to_owned());
pool.len() - 1
});
pool[index].as_ptr()
});
unsafe {
entity
.byte_add(MOCK_NAME_OFFSET)
.cast::<sys::string_t>()
.write(sys::string_t { pszValue: pooled })
};
}
true
}
unsafe extern "C" fn set_move_type(
_: *mut sys::IServerTools,
entity: *mut sys::CBaseEntity,
move_type: c_int,
) {
MOVE_TYPES.with_borrow_mut(|types| types.push((entity, move_type, None)));
}
unsafe extern "C" fn set_move_type_and_collide(
_: *mut sys::IServerTools,
entity: *mut sys::CBaseEntity,
move_type: c_int,
collide: c_int,
) {
MOVE_TYPES.with_borrow_mut(|types| types.push((entity, move_type, Some(collide))));
}