use super::script_binding::{SCRIPT_DESCRIPTION_SLOT, class_description, member_binding};
use crate::Module;
use crate::datatables::PropFlags;
use crate::entities::{Entity, EntityHandle};
use crate::interfaces::ValveEngine;
use crate::test_support::datatables::{
direct_table, int8_proxy, int32_proxy, prop, table, table_prop,
};
use crate::test_support::edicts::{change_accessor, shared_change_info};
use crate::test_support::entities::MOCK_EFLAGS_OFFSET;
use crate::test_support::interfaces::server_game_dll::export_standard_proxies;
use crate::test_support::leak;
use crate::test_support::server::export;
use sdk_raw::edicts::{FL_EDICT_CHANGED, FL_FULL_EDICT_CHANGED};
use sdk_raw::entities::{NUM_NETWORKED_EHANDLE_BITS, NUM_SERIAL_NUM_SHIFT_BITS};
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 sdk_raw::tf2::script_binding::{BOOL, FLOAT, HANDLE, INT, STRING, VOID};
use std::cell::{Cell, RefCell};
use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::mem::offset_of;
use std::ptr::{NonNull, null, null_mut};
const _: () = assert!(offset_of!(FakePlayer, flags) == MOCK_EFLAGS_OFFSET);
const _: () = assert!(offset_of!(FakePlayer, instance) == INSTANCE_OFFSET);
const _: () = assert!(offset_of!(FakePlayer, script_id) == SCRIPT_ID_OFFSET);
const INSTANCE_OFFSET: usize = 72;
const SCRIPT_ID_OFFSET: usize = 80;
const SLOTS: usize = 64;
thread_local! {
static NEXT_INDEX: Cell<c_int> = const { Cell::new(1) };
}
type Handler = Box<dyn FnMut(&FakePlayer, &CStr, &[Value]) -> Option<Value>>;
#[repr(C)]
pub struct FakePlayer {
vtable: *const *const (),
map: *mut sys::datamap_t,
networkable: sys::IServerNetworkable,
description: *mut sys::ScriptClassDesc_t,
flags: Cell<c_int>,
class_name: *const c_char,
class: *mut sys::ServerClass,
edict: *mut sys::edict_t,
handle: sys::CBaseHandle,
instance: Cell<sys::HSCRIPT>,
script_id: *const u8,
slots: [Cell<u32>; SLOTS],
layout: Vec<(&'static CStr, usize)>,
calls: RefCell<Vec<(&'static CStr, Vec<Value>)>>,
handler: RefCell<Option<Handler>>,
}
impl FakePlayer {
pub fn new(methods: &[Method], vars: &[Var]) -> &'static Self {
let base = data_map(c"CBaseEntity", base_entity_fields(), null_mut());
let map = data_map(c"CTFPlayer", vec![], data_map(c"CBasePlayer", vec![], base));
Self::build(
c"player",
(c"CTFPlayer", c"DT_TFPlayer"),
map,
Some(c"CTFPlayer"),
methods,
vars,
)
}
pub fn other(
class_name: &'static CStr,
class: &'static CStr,
table: &'static CStr,
vars: &[Var],
) -> &'static Self {
let base = data_map(c"CBaseEntity", base_entity_fields(), null_mut());
Self::build(
class_name,
(class, table),
data_map(class, vec![], base),
None,
&[],
vars,
)
}
pub fn answer(&self, handler: impl FnMut(&Self, &CStr, &[Value]) -> Option<Value> + 'static) {
self.handler.replace(Some(Box::new(handler)));
}
fn build(
class_name: &'static CStr,
(class, table_name): (&'static CStr, &'static CStr),
map: *mut sys::datamap_t,
script_class: Option<&'static CStr>,
methods: &[Method],
vars: &[Var],
) -> &'static Self {
let validate = Box::leak(Box::new([member_binding(
c"ValidateScriptScope",
BOOL,
&mut [],
Some(dispatch),
)]));
validate[0].m_pFunction.val_0 = c"ValidateScriptScope".as_ptr() as isize;
let mut description = leak(class_description(c"CBaseEntity", validate, null_mut()));
if let Some(script_class) = script_class {
let bindings = methods
.iter()
.map(|method| {
let parameters = Box::leak(method.parameters.to_vec().into_boxed_slice());
let mut binding =
member_binding(method.name, method.returns, parameters, Some(dispatch));
binding.m_pFunction.val_0 = method.name.as_ptr() as isize;
binding
})
.collect::<Vec<_>>()
.leak();
description = leak(class_description(script_class, bindings, description));
}
let mut layout = Vec::new();
let mut props = Vec::new();
let mut next = 0;
for var in vars {
let offset = offset_of!(Self, slots) + next * size_of::<u32>();
let offset_int = c_int::try_from(offset).unwrap();
layout.push((var.name, next));
next += var.kind.slots();
assert!(next <= SLOTS, "too many networked variables");
props.push(match var.kind {
VarKind::Bool => prop(
var.name,
sys::SendPropType_DPT_Int,
offset_int,
PropFlags::UNSIGNED,
Some(int8_proxy),
),
VarKind::Float => prop(
var.name,
sys::SendPropType_DPT_Float,
offset_int,
PropFlags::default(),
Some(int32_proxy),
),
VarKind::Floats(len) => {
let elements = (0..len)
.map(|index| {
let name = CString::new(format!("{index:03}")).unwrap();
prop(
Box::leak(name.into_boxed_c_str()),
sys::SendPropType_DPT_Float,
c_int::try_from(index * size_of::<f32>()).unwrap(),
PropFlags::default(),
Some(int32_proxy),
)
})
.collect::<Vec<_>>()
.leak();
table_prop(
var.name,
offset_int,
leak(table(var.name, elements)),
Some(direct_table),
)
}
VarKind::Handle => {
let mut handle = prop(
var.name,
sys::SendPropType_DPT_Int,
offset_int,
PropFlags::UNSIGNED,
Some(handle_proxy),
);
handle.m_nBits = NUM_NETWORKED_EHANDLE_BITS as c_int;
handle
}
VarKind::Int | VarKind::UInt => prop(
var.name,
sys::SendPropType_DPT_Int,
offset_int,
if var.kind == VarKind::UInt {
PropFlags::UNSIGNED
} else {
PropFlags::default()
},
Some(int32_proxy),
),
});
}
let class = leak(sys::ServerClass {
m_pNetworkName: class.as_ptr(),
m_pTable: leak(table(table_name, props.leak())),
m_pNext: null_mut(),
m_ClassID: 1,
m_InstanceBaselineIndex: 0,
});
let index = NEXT_INDEX.get();
NEXT_INDEX.set(index + 1);
Box::leak(Box::new(Self {
vtable: entity_vtable(),
map,
networkable: sys::IServerNetworkable {
vtable_: networkable_vtable(),
},
description,
flags: Cell::new(0),
class_name: class_name.as_ptr(),
class,
edict: leak(mock_edict(index, false)),
instance: Cell::new(null_mut()),
script_id: null(),
handle: sys::CBaseHandle {
m_Index: index.cast_unsigned() | 1 << NUM_SERIAL_NUM_SHIFT_BITS,
},
slots: [const { Cell::new(0) }; SLOTS],
layout,
calls: RefCell::new(Vec::new()),
handler: RefCell::new(None),
}))
}
pub fn entity(&'static self) -> Entity<'static> {
unsafe { Entity::from_raw(NonNull::from(self).cast()) }
}
fn slot(&self, name: &CStr) -> usize {
self.layout
.iter()
.find(|(declared, _)| *declared == name)
.unwrap_or_else(|| panic!("no networked variable {name:?}"))
.1
}
pub fn float(&self, name: &CStr, index: usize) -> f32 {
f32::from_bits(self.slots[self.slot(name) + index].get())
}
pub fn get(&self, name: &CStr) -> u32 {
self.slots[self.slot(name)].get()
}
pub fn handle(&self) -> EntityHandle {
EntityHandle::from_raw(self.handle.m_Index)
}
pub fn instance(&self) -> sys::HSCRIPT {
self.instance.get()
}
pub fn set_float(&self, name: &CStr, index: usize, value: f32) {
self.slots[self.slot(name) + index].set(value.to_bits());
}
pub fn set_flags(&self, flags: c_int) {
self.flags.set(flags);
}
pub fn set(&self, name: &CStr, bits: u32) {
self.slots[self.slot(name)].set(bits);
}
pub fn take_calls(&self) -> Vec<(&'static CStr, Vec<Value>)> {
self.calls.take()
}
pub fn take_changed(&self) -> bool {
unsafe {
let flags = &raw mut (*self.edict)._base.m_fStateFlags;
let changed = flags.read() & (FL_EDICT_CHANGED | FL_FULL_EDICT_CHANGED) != 0;
flags.write(0);
changed
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Method {
name: &'static CStr,
returns: sys::ScriptDataType_t,
parameters: &'static [sys::ScriptDataType_t],
}
impl Method {
pub const fn new(
name: &'static CStr,
returns: sys::ScriptDataType_t,
parameters: &'static [sys::ScriptDataType_t],
) -> Self {
Self {
name,
returns,
parameters,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Bool(bool),
Float(f32),
Handle(sys::HSCRIPT),
Int(c_int),
String(CString),
Void,
}
impl Value {
unsafe fn of(variant: &sys::ScriptVariant_t) -> Self {
let value = &variant.__bindgen_anon_1;
unsafe {
match c_int::from(variant.m_type) {
BOOL => Self::Bool(value.m_bool),
FLOAT => Self::Float(value.m_float),
HANDLE => Self::Handle(value.m_hScript),
INT => Self::Int(value.m_int),
STRING => Self::String(CStr::from_ptr(value.m_pszString).to_owned()),
VOID => Self::Void,
other => panic!("unexpected argument type {other}"),
}
}
}
fn variant(&self) -> sys::ScriptVariant_t {
use sdk_raw::tf2::script_binding::{boolean, float, handle, int};
match *self {
Self::Bool(value) => boolean(value),
Self::Float(value) => float(value),
Self::Handle(value) => handle(value),
Self::Int(value) => int(value),
Self::String(_) | Self::Void => panic!("not a result fake methods return"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Var {
name: &'static CStr,
kind: VarKind,
}
impl Var {
pub const fn new(name: &'static CStr, kind: VarKind) -> Self {
Self { name, kind }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VarKind {
Bool,
Float,
Floats(usize),
Handle,
Int,
UInt,
}
impl VarKind {
const fn slots(self) -> usize {
match self {
Self::Floats(len) => len,
_ => 1,
}
}
}
fn base_entity_fields() -> Vec<sys::typedescription_t> {
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 script_id = field(
c"m_iszScriptId",
sys::_fieldtypes_FIELD_STRING,
SCRIPT_ID_OFFSET,
);
script_id.fieldSizeInBytes = size_of::<sys::string_t>() as c_int;
vec![flags, script_id]
}
unsafe extern "C" fn class_name(this: *const sys::IServerNetworkable) -> *const c_char {
unsafe { (*fake_of(this)).class_name }
}
unsafe extern "C" fn datamap(entity: *mut sys::CBaseEntity) -> *mut sys::datamap_t {
unsafe { (*entity.cast::<FakePlayer>()).map }
}
unsafe extern "C" fn description(entity: *mut sys::CBaseEntity) -> *mut sys::ScriptClassDesc_t {
unsafe { (*entity.cast::<FakePlayer>()).description }
}
unsafe extern "C" fn dispatch(
function: sys::ScriptFunctionBindingStorageType_t,
object: *mut c_void,
arguments: *mut sys::ScriptVariant_t,
count: c_int,
result: *mut sys::ScriptVariant_t,
) -> bool {
let (name, object) = unsafe {
(
CStr::from_ptr(function.val_0 as *const c_char),
&*object.cast::<FakePlayer>(),
)
};
let arguments = (0..usize::try_from(count).unwrap())
.map(|index| unsafe { Value::of(&*arguments.add(index)) })
.collect::<Vec<_>>();
object.calls.borrow_mut().push((name, arguments.clone()));
let answer = if name == c"ValidateScriptScope" {
if object.instance.get().is_null() {
object.instance.set(leak(0_u8).cast());
}
Some(Value::Bool(true))
} else {
match object.handler.take() {
Some(mut handler) => {
let answer = handler(object, name, &arguments);
object.handler.replace(Some(handler));
answer
}
None if result.is_null() => Some(Value::Void),
None => panic!("no answer for {name:?}"),
}
};
let Some(answer) = answer else {
return false;
};
if !result.is_null() {
unsafe { result.write(answer.variant()) };
}
true
}
unsafe extern "C" fn edict(this: *const sys::IServerNetworkable) -> *mut sys::edict_t {
unsafe { (*fake_of(this)).edict }
}
fn entity_vtable() -> *const *const () {
let slot = |field: usize| field / size_of::<usize>();
let networkable_slot = slot(offset_of!(
sys::IServerEntity__bindgen_vtable,
IServerEntity_GetNetworkable
));
let handle_slot = slot(offset_of!(
sys::IServerEntity__bindgen_vtable,
IServerEntity_GetRefEHandle
));
let slots = SCRIPT_DESCRIPTION_SLOT
.max(sdk_raw::entities::GET_DATA_DESC_MAP_SLOT)
.max(networkable_slot)
.max(handle_slot)
+ 1;
let mut vtable = vec![unexpected_call as *const (); slots];
vtable[sdk_raw::entities::GET_DATA_DESC_MAP_SLOT] = datamap as *const ();
vtable[networkable_slot] = networkable as *const ();
vtable[handle_slot] = ref_handle as *const ();
vtable[SCRIPT_DESCRIPTION_SLOT] = description as *const ();
vtable.leak().as_ptr()
}
fn fake_of(this: *const sys::IServerNetworkable) -> *const FakePlayer {
unsafe { this.byte_sub(offset_of!(FakePlayer, networkable)) }.cast()
}
unsafe extern "C" fn handle_proxy(
_: *const sys::SendProp,
_: *const c_void,
_: *const c_void,
_: *mut sys::DVariant,
_: c_int,
_: c_int,
) {
unexpected_call();
}
unsafe extern "C" fn networkable(entity: *mut sys::IServerUnknown) -> *mut sys::IServerNetworkable {
unsafe { (&raw const (*entity.cast::<FakePlayer>()).networkable).cast_mut() }
}
fn networkable_vtable() -> *mut sys::IServerNetworkable__bindgen_vtable {
Box::leak(unsafe {
mock_vtable::<sys::IServerNetworkable__bindgen_vtable>(
unexpected_call as *const (),
|vtable| {
(&raw mut (*vtable).IServerNetworkable_GetClassName).write(class_name);
(&raw mut (*vtable).IServerNetworkable_GetServerClass).write(server_class);
(&raw mut (*vtable).IServerNetworkable_GetEdict).write(edict);
},
)
})
}
unsafe extern "C" fn ref_handle(entity: *const sys::IServerEntity) -> *const sys::CBaseHandle {
unsafe { &raw const (*entity.cast::<FakePlayer>()).handle }
}
pub fn serve_interfaces() {
export_standard_proxies();
let engine = Box::leak(unsafe {
mock_vtable::<sys::IVEngineServer__bindgen_vtable>(unexpected_call as *const (), |vtable| {
(&raw mut (*vtable).IVEngineServer_GetChangeAccessor).write(change_accessor);
(&raw mut (*vtable).IVEngineServer_GetSharedEdictChangeInfo).write(shared_change_info);
})
});
export(
Module::Engine,
ValveEngine::VERSION,
leak(sys::IVEngineServer { vtable_: engine }),
);
}
unsafe extern "C" fn server_class(this: *mut sys::IServerNetworkable) -> *mut sys::ServerClass {
unsafe { (*fake_of(this)).class }
}