use super::super::entities::{MOCK_EFLAGS_OFFSET, MockEntity};
use super::super::leak;
use super::super::server::export;
use crate::interfaces::ServerTools;
use crate::server::Module;
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::null_mut;
thread_local! {
static CREATED: Cell<*mut sys::CBaseEntity> = const { Cell::new(null_mut()) };
static EVENTS: RefCell<Vec<SpawnEvent>> = const { RefCell::new(Vec::new()) };
static FOUND: RefCell<Vec<(*mut sys::CBaseEntity, CString)>> = const { RefCell::new(Vec::new()) };
static REFUSED: RefCell<Option<CString>> = const { RefCell::new(None) };
static REMOVES_ON_SPAWN: Cell<bool> = const { Cell::new(false) };
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpawnEvent {
Created(CString),
KeyValue(CString, CString),
Team(c_int),
Spawned,
Removed,
}
pub unsafe extern "C" fn change_team(_: *mut sys::CBaseEntity, team: c_int) {
record(SpawnEvent::Team(team));
}
unsafe extern "C" fn create_entity_by_name(
_: *mut sys::IServerTools,
class_name: *const c_char,
) -> *mut sys::CBaseEntity {
let class_name = unsafe { CStr::from_ptr(class_name) };
record(SpawnEvent::Created(class_name.to_owned()));
CREATED.replace(null_mut())
}
unsafe extern "C" fn dispatch_spawn(_: *mut sys::IServerTools, entity: *mut sys::CBaseEntity) {
record(SpawnEvent::Spawned);
if REMOVES_ON_SPAWN.get() {
unsafe {
let flags = entity.byte_add(MOCK_EFLAGS_OFFSET).cast::<c_int>();
flags.write(flags.read() | EFL_KILLME);
}
}
}
pub fn export_spawn_tools() {
CREATED.set(null_mut());
EVENTS.take();
FOUND.take();
REFUSED.take();
REMOVES_ON_SPAWN.set(false);
let vtable = Box::leak(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_RemoveEntity).write(remove_entity);
(&raw mut (*vtable).IServerTools_SetKeyValue).write(set_key_value);
})
});
export(
Module::GameServer,
ServerTools::VERSION,
leak(sys::IServerTools { vtable_: vtable }),
);
}
unsafe extern "C" fn find_by_class_name(
_: *mut sys::IServerTools,
after: *mut sys::CBaseEntity,
class_name: *const c_char,
) -> *mut sys::CBaseEntity {
let class_name = unsafe { CStr::from_ptr(class_name) };
FOUND.with_borrow(|found| {
let start = found
.iter()
.position(|(entity, _)| *entity == after)
.map_or(0, |index| index + 1);
found[start.min(found.len())..]
.iter()
.find(|(_, name)| name.as_c_str() == class_name)
.map_or(null_mut(), |(entity, _)| *entity)
})
}
pub fn patch_slots(mock: &mut MockEntity, slots: &[(usize, *const ())]) {
let length = MockEntity::vtable_slots();
let patched_length = slots
.iter()
.map(|&(slot, _)| slot + 1)
.fold(length, usize::max);
let pointer = mock.as_ptr();
let mut vtable = vec![unexpected_call as *const (); patched_length];
unsafe {
let original = pointer.cast::<*const *const ()>().read();
for (slot, entry) in vtable.iter_mut().enumerate().take(length) {
*entry = original.add(slot).read();
}
}
for &(slot, function) in slots {
vtable[slot] = function;
}
unsafe {
pointer
.cast::<*const *const ()>()
.write(vtable.leak().as_ptr())
};
}
fn record(event: SpawnEvent) {
EVENTS.with_borrow_mut(|events| events.push(event));
}
pub fn refuse_key(key: &CStr) {
REFUSED.set(Some(key.to_owned()));
}
pub fn register_class_name(entity: *mut sys::CBaseEntity, class_name: &CStr) {
FOUND.with_borrow_mut(|found| found.push((entity, class_name.to_owned())));
}
unsafe extern "C" fn remove_entity(_: *mut sys::IServerTools, _: *mut sys::CBaseEntity) {
record(SpawnEvent::Removed);
}
pub fn remove_on_spawn(remove: bool) {
REMOVES_ON_SPAWN.set(remove);
}
pub fn set_created(entity: *mut sys::CBaseEntity) {
CREATED.set(entity);
}
unsafe extern "C" fn set_key_value(
_: *mut sys::IServerTools,
_: *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 REFUSED.with_borrow(|refused| refused.as_deref() == Some(key)) {
return false;
}
record(SpawnEvent::KeyValue(key.to_owned(), value.to_owned()));
true
}
pub fn take_events() -> Vec<SpawnEvent> {
EVENTS.take()
}