use crate::entities::Entity;
use crate::ffi::{NotThreadSafe, borrow_cstr, vcall};
use crate::interfaces::ValveEngine;
use std::ffi::{CStr, c_int};
use std::marker::PhantomData;
use std::ptr::NonNull;
pub const MAX_EDICTS: c_int = 1 << 11;
const FL_EDICT_CHANGED: c_int = 1 << 0;
const FL_EDICT_FREE: c_int = 1 << 1;
const FL_FULL_EDICT_CHANGED: c_int = 1 << 8;
const MAX_CHANGE_OFFSETS: u16 = 19;
const MAX_EDICT_CHANGE_INFOS: u16 = 100;
#[doc(alias = "edict_t")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Edict<'s> {
pointer: NonNull<sys::edict_t>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> Edict<'s> {
pub(crate) const unsafe fn from_raw(pointer: NonNull<sys::edict_t>) -> Self {
Self {
pointer,
_scope: PhantomData,
_not_thread_safe: PhantomData,
}
}
pub const fn as_ptr(self) -> *mut sys::edict_t {
self.pointer.as_ptr()
}
#[doc(alias = "ENTINDEX")]
#[doc(alias = "IndexOfEdict")]
pub fn index(self) -> c_int {
let index = unsafe { (&raw const (*self.as_ptr())._base.m_EdictIndex).read() };
c_int::from(index)
}
#[doc(alias = "IsFree")]
pub fn is_free(self) -> bool {
self.state_flags() & FL_EDICT_FREE != 0
}
#[doc(alias = "GetUnknown")]
pub fn entity(self) -> Option<Entity<'s>> {
if self.is_free() {
return None;
}
let unknown = NonNull::new(unsafe { (&raw const (*self.as_ptr())._base.m_pUnk).read() })?;
let entity = unsafe { vcall!(unknown.as_ptr() => IServerUnknown_GetBaseEntity()) };
NonNull::new(entity).map(|entity| unsafe { Entity::from_raw(entity) })
}
#[doc(alias = "GetClassName")]
pub fn class_name(self) -> Option<&'s CStr> {
if self.is_free() {
return None;
}
let networkable =
NonNull::new(unsafe { (&raw const (*self.as_ptr())._base.m_pNetworkable).read() })?;
unsafe { borrow_cstr(vcall!(networkable.as_ptr() => IServerNetworkable_GetClassName())) }
}
#[doc(alias = "StateChanged")]
pub fn state_changed(self, engine: ValveEngine<'_>, offset: u16) {
let flags = self.state_flags();
if flags & FL_FULL_EDICT_CHANGED != 0 {
return;
}
self.set_state_flags(flags | FL_EDICT_CHANGED);
let (Some(accessor), Some(shared)) = (
engine.change_accessor(self),
engine.shared_edict_change_info(),
) else {
return self.mark_fully_changed(None);
};
let accessor = accessor.as_ptr();
let shared = shared.as_ptr();
unsafe {
let serial_number = (&raw const (*shared).m_iSerialNumber).read();
let infos = (&raw mut (*shared).m_ChangeInfos).cast::<sys::CEdictChangeInfo>();
if (&raw const (*accessor).m_iChangeInfoSerialNumber).read() == serial_number {
let index = (&raw const (*accessor).m_iChangeInfo).read();
if index >= MAX_EDICT_CHANGE_INFOS {
return self.mark_fully_changed(Some(accessor));
}
let info = infos.add(usize::from(index));
let count = (&raw const (*info).m_nChangeOffsets).read();
let offsets = (&raw mut (*info).m_ChangeOffsets).cast::<u16>();
for slot in 0..count.min(MAX_CHANGE_OFFSETS) {
if offsets.add(usize::from(slot)).read() == offset {
return;
}
}
if count >= MAX_CHANGE_OFFSETS {
self.mark_fully_changed(Some(accessor));
} else {
offsets.add(usize::from(count)).write(offset);
(&raw mut (*info).m_nChangeOffsets).write(count + 1);
}
} else {
let count = (&raw const (*shared).m_nChangeInfos).read();
if count >= MAX_EDICT_CHANGE_INFOS {
return self.mark_fully_changed(Some(accessor));
}
(&raw mut (*accessor).m_iChangeInfo).write(count);
(&raw mut (*shared).m_nChangeInfos).write(count + 1);
(&raw mut (*accessor).m_iChangeInfoSerialNumber).write(serial_number);
let info = infos.add(usize::from(count));
(&raw mut (*info).m_ChangeOffsets)
.cast::<u16>()
.write(offset);
(&raw mut (*info).m_nChangeOffsets).write(1);
}
}
}
pub fn full_state_changed(self, engine: ValveEngine<'_>) {
self.mark_fully_changed(engine.change_accessor(self).map(NonNull::as_ptr));
}
fn mark_fully_changed(self, accessor: Option<*mut sys::IChangeInfoAccessor>) {
self.set_state_flags(self.state_flags() | FL_EDICT_CHANGED | FL_FULL_EDICT_CHANGED);
if let Some(accessor) = accessor {
unsafe { (&raw mut (*accessor).m_iChangeInfoSerialNumber).write(0) };
}
}
fn state_flags(self) -> c_int {
unsafe { (&raw const (*self.as_ptr())._base.m_fStateFlags).read() }
}
fn set_state_flags(self, flags: c_int) {
unsafe { (&raw mut (*self.as_ptr())._base.m_fStateFlags).write(flags) };
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::*;
use std::ptr::null_mut;
pub(crate) fn mock_edict(index: c_int, free: bool) -> sys::edict_t {
sys::edict_t {
_base: sys::CBaseEdict {
m_fStateFlags: if free { FL_EDICT_FREE } else { 0 },
m_NetworkSerialNumber: 0,
m_EdictIndex: index.try_into().unwrap(),
m_pNetworkable: null_mut(),
m_pUnk: null_mut(),
},
freetime: 0.0,
}
}
}
#[cfg(test)]
mod tests {
use super::test_support::mock_edict;
use super::*;
use crate::ffi::test_support::{mock_vtable, unexpected_call};
use std::cell::Cell;
use std::ptr::null_mut;
#[test]
fn reads_the_cached_index_and_free_flag() {
let mut table = [
mock_edict(0, false),
mock_edict(1, true),
mock_edict(2, false),
];
let base = table.as_mut_ptr();
let edict = |slot: usize| unsafe { Edict::from_raw(NonNull::new(base.add(slot)).unwrap()) };
assert_eq!(edict(0).index(), 0);
assert_eq!(edict(2).index(), 2);
assert!(!edict(0).is_free());
assert!(edict(1).is_free());
assert_eq!(edict(1), edict(1));
assert_ne!(edict(1), edict(2));
assert_eq!(edict(2).as_ptr(), unsafe { base.add(2) });
assert_eq!(edict(1).entity(), None);
assert_eq!(edict(0).class_name(), None);
}
thread_local! {
static ACCESSOR: Cell<*mut sys::IChangeInfoAccessor> = const { Cell::new(null_mut()) };
static SHARED: Cell<*mut sys::CSharedEdictChangeInfo> = const { Cell::new(null_mut()) };
}
unsafe extern "C" fn change_accessor(
_: *mut sys::IVEngineServer,
_: *const sys::edict_t,
) -> *mut sys::IChangeInfoAccessor {
ACCESSOR.get()
}
unsafe extern "C" fn shared_change_info(
_: *mut sys::IVEngineServer,
) -> *mut sys::CSharedEdictChangeInfo {
SHARED.get()
}
#[test]
fn state_changes_follow_the_engines_change_tracking() {
let vtable = 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);
},
)
};
let mut interface = sys::IVEngineServer {
vtable_: &raw const *vtable,
};
let engine = unsafe { ValveEngine::from_raw(NonNull::from(&mut interface)) };
let mut accessor = sys::IChangeInfoAccessor {
m_iChangeInfo: 0,
m_iChangeInfoSerialNumber: 0,
};
let mut shared = Box::new(unsafe { std::mem::zeroed::<sys::CSharedEdictChangeInfo>() });
shared.m_iSerialNumber = 7;
shared.m_nChangeInfos = 3;
ACCESSOR.set(&raw mut accessor);
SHARED.set(&raw mut *shared);
let mut slot = mock_edict(4, false);
let edict = unsafe { Edict::from_raw(NonNull::from(&mut slot)) };
edict.state_changed(engine, 40);
assert_eq!(slot._base.m_fStateFlags, FL_EDICT_CHANGED);
assert_eq!(
(accessor.m_iChangeInfo, accessor.m_iChangeInfoSerialNumber),
(3, 7)
);
assert_eq!(shared.m_nChangeInfos, 4);
assert_eq!(shared.m_ChangeInfos[3].m_nChangeOffsets, 1);
assert_eq!(shared.m_ChangeInfos[3].m_ChangeOffsets[0], 40);
edict.state_changed(engine, 44);
edict.state_changed(engine, 40);
assert_eq!(shared.m_ChangeInfos[3].m_nChangeOffsets, 2);
assert_eq!(shared.m_ChangeInfos[3].m_ChangeOffsets[1], 44);
for offset in 0..MAX_CHANGE_OFFSETS {
edict.state_changed(engine, 100 + offset);
}
assert_eq!(
slot._base.m_fStateFlags,
FL_EDICT_CHANGED | FL_FULL_EDICT_CHANGED
);
assert_eq!(accessor.m_iChangeInfoSerialNumber, 0);
let recorded = shared.m_ChangeInfos[3];
edict.state_changed(engine, 2);
assert_eq!(
shared.m_ChangeInfos[3].m_nChangeOffsets,
recorded.m_nChangeOffsets
);
ACCESSOR.set(null_mut());
SHARED.set(null_mut());
}
}