use super::leak;
use sdk_raw::players::LIFE_ALIVE;
use sdk_raw::test_support::entities::field;
use sdk_raw::test_support::{mock_vtable, unexpected_call};
use std::alloc::{Layout, alloc_zeroed, handle_alloc_error};
use std::cell::{Cell, RefCell};
use std::ffi::{CStr, CString, c_char, c_int};
use std::mem::offset_of;
use std::ptr::{self, null_mut};
pub const MOCK_EFLAGS_OFFSET: usize = 32;
const MOCK_HAMMER_ID_OFFSET: usize = 56;
const MOCK_HANDLE_OFFSET: usize = 40;
pub const MOCK_HEALTH_OFFSET: usize = 64;
pub const MOCK_LIFE_STATE_OFFSET: usize = 72;
pub const MOCK_MAX_HEALTH_BONUS: c_int = 25;
pub const MOCK_MAX_HEALTH_OFFSET: usize = 68;
pub const MOCK_NAME_OFFSET: usize = 48;
pub const MOCK_TAKE_DAMAGE_OFFSET: usize = 73;
#[derive(Debug, Clone, PartialEq)]
pub struct ReceivedInput {
pub target: *mut sys::CBaseEntity,
pub name: CString,
pub activator: *mut sys::CBaseEntity,
pub caller: *mut sys::CBaseEntity,
pub field_type: sys::fieldtype_t,
pub payload: [u8; 12],
pub string: *const c_char,
pub handle: u32,
pub output_id: c_int,
}
thread_local! {
static ACCEPTS: Cell<bool> = const { Cell::new(true) };
static COLLIDEABLE: Cell<*mut sys::ICollideable> = const { Cell::new(null_mut()) };
static DATA_MAP: Cell<*mut sys::datamap_t> = const { Cell::new(null_mut()) };
static EDICT: Cell<*mut sys::edict_t> = const { Cell::new(null_mut()) };
static INPUTS: RefCell<Vec<ReceivedInput>> = const { RefCell::new(Vec::new()) };
static NETWORKABLE: Cell<*mut sys::IServerNetworkable> = const { Cell::new(null_mut()) };
static ORIGIN: Cell<sys::Vector> = const { Cell::new(sys::Vector { x: 1.0, y: 2.0, z: 3.0 }) };
static SERVER_CLASS: Cell<*mut sys::ServerClass> = const { Cell::new(null_mut()) };
static TAKE_HEALTH_CALLS: RefCell<Vec<(f32, c_int)>> = const { RefCell::new(Vec::new()) };
static TELEPORTS: Cell<usize> = const { Cell::new(0) };
}
pub struct MockEntity {
storage: *mut usize,
}
impl MockEntity {
pub fn new(handle: u32) -> Self {
Self::with_layout(handle, Layout::new::<[usize; 64]>())
}
pub fn with_layout(handle: u32, layout: Layout) -> Self {
let slot_count = sdk_raw::entities::TF2_TELEPORT_SLOT
.max(sdk_raw::entities::GET_DATA_DESC_MAP_SLOT)
.max(sdk_raw::entities::ACCEPT_INPUT_SLOT)
.max(sdk_raw::entities::health::TF2_GET_MAX_HEALTH_SLOT)
.max(sdk_raw::entities::health::TAKE_HEALTH_SLOT)
.max(sdk_raw::entities::health::IS_ALIVE_SLOT)
+ 1;
let mut vtable = vec![unexpected_call as *const (); slot_count];
let slot = |field: usize| field / size_of::<usize>();
vtable[slot(offset_of!(
sys::IServerEntity__bindgen_vtable,
IServerEntity_GetCollideable
))] = get_collideable as *const ();
vtable[slot(offset_of!(
sys::IServerEntity__bindgen_vtable,
IServerEntity_GetNetworkable
))] = get_networkable as *const ();
vtable[slot(offset_of!(
sys::IServerEntity__bindgen_vtable,
IServerEntity_GetRefEHandle
))] = get_handle as *const ();
vtable[sdk_raw::entities::TF2_TELEPORT_SLOT] = teleport_entity as *const ();
vtable[sdk_raw::entities::GET_DATA_DESC_MAP_SLOT] = get_datamap as *const ();
vtable[sdk_raw::entities::ACCEPT_INPUT_SLOT] = accept_input as *const ();
vtable[sdk_raw::entities::health::TF2_GET_MAX_HEALTH_SLOT] = get_max_health as *const ();
vtable[sdk_raw::entities::health::TAKE_HEALTH_SLOT] = take_health as *const ();
vtable[sdk_raw::entities::health::IS_ALIVE_SLOT] = is_alive as *const ();
let layout = Layout::from_size_align(
layout.size().max(size_of::<[usize; 64]>()),
layout.align().max(align_of::<usize>()),
)
.unwrap();
let storage = unsafe { alloc_zeroed(layout) }.cast::<usize>();
if storage.is_null() {
handle_alloc_error(layout);
}
let vtable = vtable.leak();
unsafe {
storage.cast::<*const *const ()>().write(vtable.as_ptr());
storage
.cast::<usize>()
.add(MOCK_HANDLE_OFFSET / size_of::<usize>())
.write(handle as usize);
}
let fields = Vec::from(base_entity_fields());
let map = sdk_raw::test_support::entities::data_map(c"CBaseEntity", fields, null_mut());
let collideable_vtable = unsafe {
mock_vtable::<sys::ICollideable__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).ICollideable_GetCollisionOrigin).write(get_origin);
},
)
};
let collideable = leak(sys::ICollideable {
vtable_: Box::leak(collideable_vtable),
});
let networkable_vtable = unsafe {
mock_vtable::<sys::IServerNetworkable__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).IServerNetworkable_GetClassName).write(get_class_name);
(&raw mut (*vtable).IServerNetworkable_GetServerClass).write(get_server_class);
(&raw mut (*vtable).IServerNetworkable_GetEdict).write(get_edict);
},
)
};
let networkable = leak(sys::IServerNetworkable {
vtable_: Box::leak(networkable_vtable),
});
DATA_MAP.set(map);
COLLIDEABLE.set(collideable);
NETWORKABLE.set(networkable);
ORIGIN.set(sys::Vector {
x: 1.0,
y: 2.0,
z: 3.0,
});
TELEPORTS.set(0);
TAKE_HEALTH_CALLS.take();
set_networking(null_mut(), null_mut());
Self { storage }
}
pub fn as_ptr(&mut self) -> *mut sys::CBaseEntity {
self.storage.cast()
}
pub fn byte(&mut self, offset: usize) -> u8 {
assert!(offset < size_of::<[usize; 64]>());
unsafe { self.as_ptr().byte_add(offset).cast::<u8>().read() }
}
#[cfg(test)]
pub(crate) fn entity(&mut self) -> crate::entities::Entity<'_> {
let entity = std::ptr::NonNull::new(self.as_ptr()).unwrap();
unsafe { crate::entities::Entity::from_raw(entity) }
}
pub fn int(&mut self, offset: usize) -> c_int {
assert!(offset + size_of::<c_int>() <= size_of::<[usize; 64]>());
assert!(offset.is_multiple_of(align_of::<c_int>()));
unsafe { self.as_ptr().byte_add(offset).cast::<c_int>().read() }
}
pub fn name(&mut self) -> sys::string_t {
unsafe {
self.as_ptr()
.byte_add(MOCK_NAME_OFFSET)
.cast::<sys::string_t>()
.read()
}
}
pub fn set_byte(&mut self, offset: usize, value: u8) {
assert!(offset < size_of::<[usize; 64]>());
unsafe { self.as_ptr().byte_add(offset).cast::<u8>().write(value) };
}
pub fn set_eflags(&mut self, flags: c_int) {
unsafe {
self.as_ptr()
.byte_add(MOCK_EFLAGS_OFFSET)
.cast::<c_int>()
.write(flags)
};
}
pub fn set_hammer_id(&mut self, id: c_int) {
unsafe {
self.as_ptr()
.byte_add(MOCK_HAMMER_ID_OFFSET)
.cast::<c_int>()
.write(id)
};
}
pub fn set_int(&mut self, offset: usize, value: c_int) {
assert!(offset + size_of::<c_int>() <= size_of::<[usize; 64]>());
assert!(offset.is_multiple_of(align_of::<c_int>()));
unsafe { self.as_ptr().byte_add(offset).cast::<c_int>().write(value) };
}
pub fn set_name(&mut self, name: sys::string_t) {
unsafe {
self.as_ptr()
.byte_add(MOCK_NAME_OFFSET)
.cast::<sys::string_t>()
.write(name)
};
}
}
unsafe extern "C" fn accept_input(
this: *mut sys::CBaseEntity,
name: *const c_char,
activator: *mut sys::CBaseEntity,
caller: *mut sys::CBaseEntity,
value: *mut sys::variant_t,
output_id: c_int,
) -> bool {
let received = unsafe {
ReceivedInput {
target: this,
name: CStr::from_ptr(name).to_owned(),
activator,
caller,
field_type: (&raw const (*value).fieldType).read(),
payload: value.cast::<[u8; 12]>().read(),
string: if (&raw const (*value).fieldType).read() == sys::_fieldtypes_FIELD_STRING {
(&raw const (*value).__bindgen_anon_1.iszVal.pszValue).read()
} else {
ptr::null()
},
handle: (&raw const (*value).eVal._base.m_Index).read(),
output_id,
}
};
unsafe { (&raw mut (*value).fieldType).write(sys::_fieldtypes_FIELD_VOID) };
INPUTS.with_borrow_mut(|inputs| inputs.push(received));
ACCEPTS.get()
}
pub fn base_entity_fields() -> [sys::typedescription_t; 3] {
let mut flags = field(
c"m_iEFlags",
sys::_fieldtypes_FIELD_INTEGER,
MOCK_EFLAGS_OFFSET,
);
flags.fieldSizeInBytes = size_of::<c_int>() as c_int;
let mut name = field(c"m_iName", sys::_fieldtypes_FIELD_STRING, MOCK_NAME_OFFSET);
name.fieldSizeInBytes = size_of::<sys::string_t>() as c_int;
let mut hammer_id = field(
c"m_iHammerID",
sys::_fieldtypes_FIELD_INTEGER,
MOCK_HAMMER_ID_OFFSET,
);
hammer_id.fieldSizeInBytes = size_of::<c_int>() as c_int;
[flags, name, hammer_id]
}
unsafe extern "C" fn get_class_name(_: *const sys::IServerNetworkable) -> *const c_char {
c"tf_player".as_ptr()
}
unsafe extern "C" fn get_collideable(_: *mut sys::IServerEntity) -> *mut sys::ICollideable {
COLLIDEABLE.get()
}
pub unsafe extern "C" fn get_datamap(_: *mut sys::CBaseEntity) -> *mut sys::datamap_t {
DATA_MAP.get()
}
unsafe extern "C" fn get_edict(_: *const sys::IServerNetworkable) -> *mut sys::edict_t {
EDICT.get()
}
unsafe extern "C" fn get_handle(this: *const sys::IServerEntity) -> *const sys::CBaseHandle {
unsafe { this.byte_add(MOCK_HANDLE_OFFSET).cast() }
}
unsafe extern "C" fn get_max_health(this: *const sys::CBaseEntity) -> c_int {
let stored = unsafe { this.byte_add(MOCK_MAX_HEALTH_OFFSET).cast::<c_int>().read() };
stored + MOCK_MAX_HEALTH_BONUS
}
unsafe extern "C" fn get_networkable(_: *mut sys::IServerEntity) -> *mut sys::IServerNetworkable {
NETWORKABLE.get()
}
unsafe extern "C" fn get_origin(_: *const sys::ICollideable) -> *const sys::Vector {
ORIGIN.with(Cell::as_ptr).cast_const()
}
unsafe extern "C" fn get_server_class(_: *mut sys::IServerNetworkable) -> *mut sys::ServerClass {
SERVER_CLASS.get()
}
pub fn health_fields() -> [sys::typedescription_t; 4] {
let member = |name, field_type, offset, size: usize| {
let mut member = field(name, field_type, offset);
member.fieldSizeInBytes = c_int::try_from(size).unwrap();
member
};
[
member(
c"m_iHealth",
sys::_fieldtypes_FIELD_INTEGER,
MOCK_HEALTH_OFFSET,
size_of::<c_int>(),
),
member(
c"m_iMaxHealth",
sys::_fieldtypes_FIELD_INTEGER,
MOCK_MAX_HEALTH_OFFSET,
size_of::<c_int>(),
),
member(
c"m_lifeState",
sys::_fieldtypes_FIELD_CHARACTER,
MOCK_LIFE_STATE_OFFSET,
1,
),
member(
c"m_takedamage",
sys::_fieldtypes_FIELD_CHARACTER,
MOCK_TAKE_DAMAGE_OFFSET,
1,
),
]
}
unsafe extern "C" fn is_alive(this: *mut sys::CBaseEntity) -> bool {
unsafe { this.byte_add(MOCK_LIFE_STATE_OFFSET).cast::<u8>().read() == LIFE_ALIVE }
}
pub fn set_accepts(accepts: bool) {
ACCEPTS.set(accepts);
}
pub fn set_datamap(map: *mut sys::datamap_t) {
DATA_MAP.set(map);
}
pub fn set_networking(class: *mut sys::ServerClass, edict: *mut sys::edict_t) {
SERVER_CLASS.set(class);
EDICT.set(edict);
}
unsafe extern "C" fn take_health(
this: *mut sys::CBaseEntity,
amount: f32,
damage_type: c_int,
) -> c_int {
TAKE_HEALTH_CALLS.with_borrow_mut(|calls| calls.push((amount, damage_type)));
let gained = amount as c_int;
unsafe {
let health = this.byte_add(MOCK_HEALTH_OFFSET).cast::<c_int>();
health.write(health.read() + gained);
}
gained
}
pub fn take_health_calls() -> Vec<(f32, c_int)> {
TAKE_HEALTH_CALLS.with_borrow(Clone::clone)
}
pub fn take_inputs() -> Vec<ReceivedInput> {
INPUTS.take()
}
unsafe extern "C" fn teleport_entity(
_: *mut sys::CBaseEntity,
position: *const sys::Vector,
angles: *const sys::QAngle,
velocity: *const sys::Vector,
) {
assert!(angles.is_null() && velocity.is_null());
ORIGIN.set(unsafe { *position });
TELEPORTS.set(TELEPORTS.get() + 1);
}
pub fn teleports() -> usize {
TELEPORTS.get()
}