use super::super::datatables::{
custom_proxy, direct_table, int8_proxy, int32_proxy, prop, table, table_prop, vector_proxy,
};
use super::super::entities::{
MOCK_NAME_OFFSET, MockEntity, base_entity_fields, set_datamap, set_networking, take_inputs,
};
use super::super::interfaces::player_info_manager::{global_vars, serve_global_vars};
use super::super::interfaces::server_game_dll::export_standard_proxies;
use super::super::leak;
use super::super::server::export;
use crate::datatables::PropFlags;
use crate::interfaces::{PlayerInfoManager, ServerTools};
use crate::server::Module;
use sdk_raw::entities::NUM_NETWORKED_EHANDLE_BITS;
use sdk_raw::entities::datamap::{FTYPEDESC_INPUT, FTYPEDESC_KEY};
use sdk_raw::test_support::edicts::mock_edict;
use sdk_raw::test_support::entities::{data_map, field};
use sdk_raw::test_support::{mock_vtable, unexpected_call};
use std::alloc::Layout;
use std::cell::{Cell, RefCell};
use std::ffi::{CStr, CString, c_char, c_int};
use std::ptr::null_mut;
pub const FIELDS_OFFSET: usize = 128;
pub const STORAGE_SIZE: usize = size_of::<[usize; 512]>();
thread_local! {
static WORLD: Cell<*mut sys::CBaseEntity> = const { Cell::new(null_mut()) };
static NAMED: RefCell<Vec<(*mut sys::CBaseEntity, CString)>> = const { RefCell::new(Vec::new()) };
static ENTITY_LIST: Cell<*mut sys::CGlobalEntityList> = const { Cell::new(null_mut()) };
static CLASS_NAMES: RefCell<Vec<(*mut sys::CBaseEntity, CString)>> = const { RefCell::new(Vec::new()) };
static NETWORKED: Cell<*mut sys::CBaseEntity> = const { Cell::new(null_mut()) };
static POOL: RefCell<Vec<CString>> = const { RefCell::new(Vec::new()) };
static KEY_VALUES: RefCell<Vec<(*mut sys::CBaseEntity, CString, CString)>> = const { RefCell::new(Vec::new()) };
}
pub struct FakeClass {
pub maps: Vec<(&'static CStr, Vec<sys::typedescription_t>)>,
pub base_fields: Vec<sys::typedescription_t>,
pub table: Option<(&'static CStr, Vec<sys::SendProp>)>,
}
pub struct FakeObjective {
pub mock: MockEntity,
pub world: MockEntity,
pub globals: *mut sys::CGlobalVars,
pub edict: *mut sys::edict_t,
address: *mut sys::CBaseEntity,
}
impl FakeObjective {
pub fn new(class: FakeClass) -> Self {
let mut world = MockEntity::new(0);
let mut mock = MockEntity::with_layout(5 | 1 << 16, Layout::new::<[usize; 512]>());
let address = mock.as_ptr();
let mut base_fields = Vec::from(base_entity_fields());
base_fields.extend(class.base_fields);
let chain = class.maps.into_iter().rev().fold(
data_map(c"CBaseEntity", base_fields, null_mut()),
|base, (name, fields)| data_map(name, fields, base),
);
set_datamap(chain);
let edict = match class.table {
Some((name, props)) => {
let props = props.leak();
for index in 1..props.len() {
if props[index].m_Type == sys::SendPropType_DPT_Array
&& props[index].m_pArrayProp.is_null()
{
let element = &raw mut props[index - 1];
props[index].m_pArrayProp = element;
}
}
let class = leak(sys::ServerClass {
m_pNetworkName: name.as_ptr(),
m_pTable: leak(table(name, props)),
m_pNext: null_mut(),
m_ClassID: 1,
m_InstanceBaselineIndex: 0,
});
let edict = leak(mock_edict(5, false));
set_networking(class, edict);
NETWORKED.set(address);
edict
}
None => {
NETWORKED.set(null_mut());
null_mut()
}
};
WORLD.set(world.as_ptr());
NAMED.take();
CLASS_NAMES.take();
KEY_VALUES.take();
export_tools();
export_standard_proxies();
let globals = serve_global_vars(32);
let manager = Box::leak(unsafe {
mock_vtable::<sys::IPlayerInfoManager__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).IPlayerInfoManager_GetGlobalVars).write(global_vars);
},
)
});
export(
Module::GameServer,
PlayerInfoManager::VERSION,
leak(sys::IPlayerInfoManager { vtable_: manager }),
);
world.set_name(sys::string_t {
pszValue: c"worldspawn".as_ptr(),
});
Self {
mock,
world,
globals,
edict,
address,
}
}
pub fn as_ptr(&self) -> *mut sys::CBaseEntity {
self.address
}
pub fn bool(&mut self, offset: usize) -> bool {
self.read::<u8>(offset) != 0
}
#[cfg(test)]
pub(crate) fn entity(&self) -> crate::entities::Entity<'static> {
let entity = std::ptr::NonNull::new(self.address).unwrap();
unsafe { crate::entities::Entity::from_raw(entity) }
}
pub fn float(&mut self, offset: usize) -> f32 {
self.read(offset)
}
pub fn int(&mut self, offset: usize) -> c_int {
self.read(offset)
}
fn read<T: Copy>(&mut self, offset: usize) -> T {
assert!(offset + size_of::<T>() <= STORAGE_SIZE);
assert!(offset.is_multiple_of(align_of::<T>()));
unsafe { self.mock.as_ptr().byte_add(offset).cast::<T>().read() }
}
pub fn set_bool(&mut self, offset: usize, value: bool) {
self.write(offset, u8::from(value));
}
pub fn set_float(&mut self, offset: usize, value: f32) {
self.write(offset, value);
}
pub fn set_int(&mut self, offset: usize, value: c_int) {
self.write(offset, value);
}
pub fn set_string(&mut self, offset: usize, value: &'static CStr) {
self.write(
offset,
sys::string_t {
pszValue: value.as_ptr(),
},
);
}
pub fn set_time(&mut self, time: f32) {
unsafe { (&raw mut (*self.globals)._base.curtime).write(time) };
}
#[cfg(test)]
pub(crate) fn world_entity(&self) -> crate::entities::Entity<'static> {
let world = std::ptr::NonNull::new(WORLD.get()).unwrap();
unsafe { crate::entities::Entity::from_raw(world) }
}
fn write<T: Copy>(&mut self, offset: usize, value: T) {
assert!(offset + size_of::<T>() <= STORAGE_SIZE);
assert!(offset.is_multiple_of(align_of::<T>()));
unsafe { self.mock.as_ptr().byte_add(offset).cast::<T>().write(value) };
}
}
pub fn array_field(
name: &'static CStr,
field_type: sys::fieldtype_t,
offset: usize,
count: usize,
size: usize,
) -> sys::typedescription_t {
let mut field = data_field(name, field_type, offset, count * size);
field.fieldSize = u16::try_from(count).unwrap();
field
}
pub fn array_props(
name: &'static CStr,
offset: usize,
count: usize,
stride: usize,
element: fn(&'static CStr, usize) -> sys::SendProp,
) -> [sys::SendProp; 2] {
let mut element = element(name, offset);
let mut array = prop(
name,
sys::SendPropType_DPT_Array,
c_int::try_from(offset).unwrap(),
PropFlags::default(),
None,
);
element.m_Flags |= PropFlags::INSIDE_ARRAY.bits();
array.m_nElements = c_int::try_from(count).unwrap();
array.m_ElementStride = c_int::try_from(stride).unwrap();
[element, array]
}
pub fn array3_prop(
name: &'static CStr,
offset: usize,
count: usize,
stride: usize,
element: fn(&'static CStr, usize) -> sys::SendProp,
) -> sys::SendProp {
let props: Vec<sys::SendProp> = (0..count)
.map(|index| {
let name = CString::new(format!("{index:03}")).unwrap();
element(Box::leak(name.into_boxed_c_str()), index * stride)
})
.collect();
table_prop(
name,
c_int::try_from(offset).unwrap(),
leak(table(name, props.leak())),
Some(direct_table),
)
}
pub fn bool_prop(name: &'static CStr, offset: usize) -> sys::SendProp {
prop(
name,
sys::SendPropType_DPT_Int,
c_int::try_from(offset).unwrap(),
PropFlags::UNSIGNED,
Some(int8_proxy),
)
}
pub fn data_field(
name: &'static CStr,
field_type: sys::fieldtype_t,
offset: usize,
size: usize,
) -> sys::typedescription_t {
let mut field = field(name, field_type, offset);
field.fieldSizeInBytes = c_int::try_from(size).unwrap();
field.fieldSize = 1;
field
}
unsafe extern "C" fn entity_by_index(
_: *mut sys::IServerTools,
index: c_int,
) -> *mut sys::CBaseEntity {
match index {
0 => WORLD.get(),
5 => NETWORKED.get(),
_ => null_mut(),
}
}
unsafe extern "C" fn entity_list(_: *mut sys::IServerTools) -> *mut sys::CGlobalEntityList {
ENTITY_LIST.get()
}
pub fn expected(inputs: &[(&CStr, c_int)]) -> Vec<(CString, c_int)> {
inputs
.iter()
.map(|(name, value)| ((*name).to_owned(), *value))
.collect()
}
fn export_tools() {
ENTITY_LIST.set(Box::leak(Box::<sys::CGlobalEntityList>::new_zeroed()).as_mut_ptr());
let vtable = Box::leak(unsafe {
mock_vtable::<sys::IServerTools__bindgen_vtable>(unexpected_call as *const (), |vtable| {
(&raw mut (*vtable).IServerTools_GetBaseEntityByEntIndex).write(entity_by_index);
(&raw mut (*vtable).IServerTools_SetKeyValue).write(set_key_value);
(&raw mut (*vtable).IServerTools_FindEntityByName).write(find_by_name);
(&raw mut (*vtable).IServerTools_FindEntityByClassname).write(find_by_class_name);
(&raw mut (*vtable).IServerTools_GetEntityList).write(entity_list);
})
});
export(
Module::GameServer,
ServerTools::VERSION,
leak(sys::IServerTools { vtable_: vtable }),
);
}
fn find_after(
named: &[(*mut sys::CBaseEntity, CString)],
after: *mut sys::CBaseEntity,
name: &CStr,
) -> *mut sys::CBaseEntity {
let start = named
.iter()
.position(|(entity, _)| *entity == after)
.map_or(0, |index| index + 1);
named[start.min(named.len())..]
.iter()
.find(|(_, entity_name)| entity_name.to_bytes().eq_ignore_ascii_case(name.to_bytes()))
.map_or(null_mut(), |(entity, _)| *entity)
}
unsafe extern "C" fn find_by_class_name(
_: *mut sys::IServerTools,
after: *mut sys::CBaseEntity,
name: *const c_char,
) -> *mut sys::CBaseEntity {
let name = unsafe { CStr::from_ptr(name) };
CLASS_NAMES.with_borrow(|named| find_after(named, after, name))
}
unsafe extern "C" fn find_by_name(
_: *mut sys::IServerTools,
after: *mut sys::CBaseEntity,
name: *const c_char,
_: *mut sys::CBaseEntity,
_: *mut sys::CBaseEntity,
_: *mut sys::CBaseEntity,
_: *mut sys::IEntityFindFilter,
) -> *mut sys::CBaseEntity {
let name = unsafe { CStr::from_ptr(name) };
NAMED.with_borrow(|named| find_after(named, after, name))
}
pub fn float_prop(name: &'static CStr, offset: usize) -> sys::SendProp {
prop(
name,
sys::SendPropType_DPT_Float,
c_int::try_from(offset).unwrap(),
PropFlags::default(),
Some(int32_proxy),
)
}
pub fn handle_prop(name: &'static CStr, offset: usize) -> sys::SendProp {
let mut handle = prop(
name,
sys::SendPropType_DPT_Int,
c_int::try_from(offset).unwrap(),
PropFlags::UNSIGNED,
Some(custom_proxy),
);
handle.m_nBits = c_int::try_from(NUM_NETWORKED_EHANDLE_BITS).unwrap();
handle
}
pub fn input(name: &'static CStr, field_type: sys::fieldtype_t) -> sys::typedescription_t {
let mut input: sys::typedescription_t = unsafe { std::mem::zeroed() };
input.fieldType = field_type;
input.externalName = name.as_ptr();
input.flags = FTYPEDESC_INPUT;
input
}
pub fn int_prop(name: &'static CStr, offset: usize) -> sys::SendProp {
prop(
name,
sys::SendPropType_DPT_Int,
c_int::try_from(offset).unwrap(),
PropFlags::default(),
Some(int32_proxy),
)
}
pub fn key_field(
name: &'static CStr,
key: &'static CStr,
field_type: sys::fieldtype_t,
offset: usize,
size: usize,
) -> sys::typedescription_t {
let mut field = data_field(name, field_type, offset, size);
field.externalName = key.as_ptr();
field.flags = FTYPEDESC_KEY;
field
}
pub fn list_entity(entity: *mut sys::CBaseEntity, index: usize, serial: c_int) {
unsafe {
let slots = &raw mut (*ENTITY_LIST.get())._base.m_EntPtrArray;
let info = (&raw mut (*slots)[index]).cast::<sys::CEntInfo>();
(&raw mut (*info).m_pEntity).write(entity.cast());
(&raw mut (*info).m_SerialNumber).write(serial);
}
}
pub fn received() -> Vec<(CString, c_int)> {
take_inputs()
.iter()
.map(|input| {
let value = if input.field_type == sys::_fieldtypes_FIELD_INTEGER {
c_int::from_ne_bytes(input.payload[..4].try_into().unwrap())
} else {
0
};
(input.name.clone(), value)
})
.collect()
}
pub fn register_class_name(entity: *mut sys::CBaseEntity, class_name: &CStr) {
CLASS_NAMES.with_borrow_mut(|named| named.push((entity, class_name.to_owned())));
}
pub fn register_name(entity: *mut sys::CBaseEntity, name: &CStr) {
NAMED.with_borrow_mut(|named| named.push((entity, name.to_owned())));
}
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 key != c"targetname" {
KEY_VALUES.with_borrow_mut(|set| set.push((entity, key.to_owned(), value.to_owned())));
return true;
}
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
}
pub fn take_key_values() -> Vec<(*mut sys::CBaseEntity, CString, CString)> {
KEY_VALUES.take()
}
pub fn vector_prop(name: &'static CStr, offset: usize) -> sys::SendProp {
prop(
name,
sys::SendPropType_DPT_Vector,
c_int::try_from(offset).unwrap(),
PropFlags::default(),
Some(vector_proxy),
)
}