#[cfg(test)]
#[path = "tests/datatables.rs"]
mod tests;
use crate::NotThreadSafe;
use crate::edicts::Edict;
use crate::entities::{Entity, EntityHandle};
use crate::interfaces::ValveEngine;
use crate::math::{QAngle, Vector};
use glam::Vec2;
use sdk_raw::datatables::{
SPROP_CHANGES_OFTEN, SPROP_COLLAPSIBLE, SPROP_COORD, SPROP_COORD_MP, SPROP_COORD_MP_INTEGRAL,
SPROP_COORD_MP_LOWPRECISION, SPROP_ENCODED_AGAINST_TICKCOUNT, SPROP_EXCLUDE, SPROP_INSIDEARRAY,
SPROP_IS_A_VECTOR_ELEM, SPROP_NORMAL, SPROP_NOSCALE, SPROP_PROXY_ALWAYS_YES, SPROP_ROUNDDOWN,
SPROP_ROUNDUP, SPROP_UNSIGNED, SPROP_XYZE, call_var_proxy, is_direct_table_proxy,
standard_var_proxies,
};
use sdk_raw::entities::NUM_NETWORKED_EHANDLE_BITS;
use sdk_raw::util::cstr::{borrow_cstr, copy_cstr};
use std::any::type_name;
use std::ffi::{CStr, CString, c_int, c_void};
use std::fmt::{self, Debug, Display, Formatter};
use std::marker::PhantomData;
use std::ptr::NonNull;
const MAX_TABLE_DEPTH: usize = 32;
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct PropFlags: c_int {
#[doc(alias("SPROP_CHANGES_OFTEN"))]
const CHANGES_OFTEN = SPROP_CHANGES_OFTEN;
#[doc(alias("SPROP_COLLAPSIBLE"))]
const COLLAPSIBLE = SPROP_COLLAPSIBLE;
#[doc(alias("SPROP_COORD"))]
const COORD = SPROP_COORD;
#[doc(alias("SPROP_COORD_MP"))]
const COORD_MP = SPROP_COORD_MP;
#[doc(alias("SPROP_COORD_MP_INTEGRAL"))]
const COORD_MP_INTEGRAL = SPROP_COORD_MP_INTEGRAL;
#[doc(alias("SPROP_COORD_MP_LOWPRECISION"))]
const COORD_MP_LOW_PRECISION = SPROP_COORD_MP_LOWPRECISION;
#[doc(alias("SPROP_ENCODED_AGAINST_TICKCOUNT"))]
const ENCODED_AGAINST_TICK_COUNT = SPROP_ENCODED_AGAINST_TICKCOUNT;
#[doc(alias("SPROP_EXCLUDE"))]
const EXCLUDE = SPROP_EXCLUDE;
#[doc(alias("SPROP_INSIDEARRAY"))]
const INSIDE_ARRAY = SPROP_INSIDEARRAY;
#[doc(alias("SPROP_IS_A_VECTOR_ELEM"))]
const IS_A_VECTOR_ELEM = SPROP_IS_A_VECTOR_ELEM;
#[doc(alias("SPROP_NOSCALE"))]
const NO_SCALE = SPROP_NOSCALE;
#[doc(alias("SPROP_NORMAL", "SPROP_VARINT"))]
const NORMAL = SPROP_NORMAL;
#[doc(alias("SPROP_PROXY_ALWAYS_YES"))]
const PROXY_ALWAYS_YES = SPROP_PROXY_ALWAYS_YES;
#[doc(alias("SPROP_ROUNDDOWN"))]
const ROUND_DOWN = SPROP_ROUNDDOWN;
#[doc(alias("SPROP_ROUNDUP"))]
const ROUND_UP = SPROP_ROUNDUP;
#[doc(alias("SPROP_UNSIGNED"))]
const UNSIGNED = SPROP_UNSIGNED;
#[doc(alias("SPROP_XYZE"))]
const XYZE = SPROP_XYZE;
const _ = !0;
}
}
#[doc(alias("SendPropType"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PropKind {
#[doc(alias("DPT_Int"))]
Int,
#[doc(alias("DPT_Float"))]
Float,
#[doc(alias("DPT_Vector"))]
Vector,
#[doc(alias("DPT_VectorXY"))]
VectorXY,
#[doc(alias("DPT_String"))]
String,
#[doc(alias("DPT_Array"))]
Array,
#[doc(alias("DPT_DataTable"))]
DataTable,
Unknown(i64),
}
impl PropKind {
fn from_raw(raw: sys::SendPropType) -> Self {
match raw {
sys::SendPropType_DPT_Int => Self::Int,
sys::SendPropType_DPT_Float => Self::Float,
sys::SendPropType_DPT_Vector => Self::Vector,
sys::SendPropType_DPT_VectorXY => Self::VectorXY,
sys::SendPropType_DPT_String => Self::String,
sys::SendPropType_DPT_Array => Self::Array,
sys::SendPropType_DPT_DataTable => Self::DataTable,
unknown => Self::Unknown(i64::from(unknown)),
}
}
}
impl Display for PropKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Int => f.write_str("integer"),
Self::Float => f.write_str("float"),
Self::Vector => f.write_str("vector"),
Self::VectorXY => f.write_str("XY vector"),
Self::String => f.write_str("string"),
Self::Array => f.write_str("array"),
Self::DataTable => f.write_str("data table"),
Self::Unknown(raw) => write!(f, "unknown type {raw}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SendProp<'s> {
raw: NonNull<sys::SendProp>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
#[derive(Debug, Clone)]
pub struct SendProps<'s> {
table: SendTable<'s>,
index: usize,
}
impl ExactSizeIterator for SendProps<'_> {}
impl<'s> Iterator for SendProps<'s> {
type Item = SendProp<'s>;
fn next(&mut self) -> Option<Self::Item> {
let prop = self.table.prop(self.index)?;
self.index += 1;
Some(prop)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.table.len().saturating_sub(self.index);
(remaining, Some(remaining))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SendTable<'s> {
raw: NonNull<sys::SendTable>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> SendTable<'s> {
pub(crate) const unsafe fn from_raw(raw: NonNull<sys::SendTable>) -> Self {
Self {
raw,
_scope: PhantomData,
_not_thread_safe: PhantomData,
}
}
pub const fn as_ptr(self) -> *mut sys::SendTable {
self.raw.as_ptr()
}
pub fn base(self) -> Option<SendTable<'s>> {
self.props()
.find(|prop| prop.kind() == PropKind::DataTable && prop.name() == c"baseclass")
.and_then(SendProp::data_table)
}
pub fn derives_from(self, base: SendTable<'_>) -> bool {
let mut table = Some(self);
for _ in 0..MAX_TABLE_DEPTH {
match table {
Some(current) if current.as_ptr() == base.as_ptr() => return true,
Some(current) => table = current.base(),
None => return false,
}
}
false
}
pub(crate) fn derives_from_named(self, name: &CStr) -> bool {
let mut table = Some(self);
for _ in 0..MAX_TABLE_DEPTH {
match table {
Some(current) if current.name() == name => return true,
Some(current) => table = current.base(),
None => return false,
}
}
false
}
pub fn find_prop(self, name: &CStr) -> Option<SendProp<'s>> {
fn search<'s>(table: SendTable<'s>, name: &CStr, depth: usize) -> Option<SendProp<'s>> {
if depth > MAX_TABLE_DEPTH {
return None;
}
table.props().find_map(|prop| {
if prop.flags().contains(PropFlags::INSIDE_ARRAY)
|| prop.flags().contains(PropFlags::EXCLUDE)
{
return None;
}
if prop.name() == name {
return Some(prop);
}
prop.data_table()
.filter(|_| prop.kind() == PropKind::DataTable)
.and_then(|nested| search(nested, name, depth + 1))
})
}
search(self, name, 0)
}
pub fn is_empty(self) -> bool {
self.len() == 0
}
#[doc(alias("GetNumProps"))]
pub fn len(self) -> usize {
usize::try_from(unsafe { (&raw const (*self.as_ptr()).m_nProps).read() }).unwrap_or(0)
}
#[doc(alias("GetName"))]
pub fn name(self) -> &'s CStr {
unsafe { borrow_cstr((&raw const (*self.as_ptr()).m_pNetTableName).read()) }
.unwrap_or_default()
}
#[doc(alias("GetProp"))]
pub fn prop(self, index: usize) -> Option<SendProp<'s>> {
if index >= self.len() {
return None;
}
let props = NonNull::new(unsafe { (&raw const (*self.as_ptr()).m_pProps).read() })?;
Some(unsafe { SendProp::from_raw(props.add(index)) })
}
pub fn props(self) -> SendProps<'s> {
SendProps {
table: self,
index: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ServerClass<'s> {
raw: NonNull<sys::ServerClass>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> ServerClass<'s> {
pub(crate) const unsafe fn from_raw(raw: NonNull<sys::ServerClass>) -> Self {
Self {
raw,
_scope: PhantomData,
_not_thread_safe: PhantomData,
}
}
pub const fn as_ptr(self) -> *mut sys::ServerClass {
self.raw.as_ptr()
}
#[doc(alias("m_ClassID"))]
pub fn class_id(self) -> c_int {
unsafe { (&raw const (*self.as_ptr()).m_ClassID).read() }
}
#[doc(alias("GetName"))]
pub fn name(self) -> &'s CStr {
unsafe { borrow_cstr((&raw const (*self.as_ptr()).m_pNetworkName).read()) }
.unwrap_or_default()
}
fn next(self) -> Option<Self> {
let next = NonNull::new(unsafe { (&raw const (*self.as_ptr()).m_pNext).read() })?;
Some(unsafe { Self::from_raw(next) })
}
#[doc(alias("m_pTable"))]
pub fn table(self) -> Option<SendTable<'s>> {
let table = NonNull::new(unsafe { (&raw const (*self.as_ptr()).m_pTable).read() })?;
Some(unsafe { SendTable::from_raw(table) })
}
}
#[derive(Debug, Clone)]
pub struct ServerClasses<'s> {
next: Option<ServerClass<'s>>,
}
impl<'s> ServerClasses<'s> {
pub(crate) unsafe fn new(head: *mut sys::ServerClass) -> Self {
Self {
next: NonNull::new(head).map(|head| unsafe { ServerClass::from_raw(head) }),
}
}
}
impl<'s> Iterator for ServerClasses<'s> {
type Item = ServerClass<'s>;
fn next(&mut self) -> Option<Self::Item> {
let class = self.next?;
self.next = class.next();
Some(class)
}
}
macro_rules! prop_field {
($prop:expr, $field:ident) => {
unsafe { (&raw const (*$prop.as_ptr()).$field).read() }
};
}
#[doc(alias("DVariant"))]
#[derive(Debug, Clone, PartialEq)]
pub enum NetValue {
Int(c_int),
Float(f32),
Vector(Vector),
VectorXY(Vec2),
String(CString),
}
pub trait NetVar: sealed::Sealed + Copy {
#[doc(hidden)]
const STORAGE: Storage;
#[doc(hidden)]
unsafe fn read(source: *const u8) -> Self;
#[doc(hidden)]
unsafe fn write(self, destination: *mut u8);
}
#[doc(alias("CStandardSendProxies"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StandardSendProxies<'s> {
raw: NonNull<sys::CStandardSendProxies>,
_scope: PhantomData<&'s ()>,
_not_thread_safe: NotThreadSafe,
}
impl<'s> StandardSendProxies<'s> {
pub(crate) const unsafe fn from_raw(raw: NonNull<sys::CStandardSendProxies>) -> Self {
Self {
raw,
_scope: PhantomData,
_not_thread_safe: PhantomData,
}
}
pub const fn as_ptr(self) -> *mut sys::CStandardSendProxies {
self.raw.as_ptr()
}
pub(crate) fn is_direct(self, prop: SendProp<'_>) -> bool {
unsafe { is_direct_table_proxy(self.as_ptr(), prop.as_ptr()) }
}
fn storage(self, prop: SendProp<'_>) -> Storage {
let proxies = unsafe { standard_var_proxies(self.as_ptr(), prop.as_ptr()) };
let signed = !prop.flags().contains(PropFlags::UNSIGNED);
match prop.kind() {
PropKind::Int if proxies.int8 => [Storage::U8, Storage::I8][usize::from(signed)],
PropKind::Int if proxies.int16 => [Storage::U16, Storage::I16][usize::from(signed)],
PropKind::Int if proxies.int32 => [Storage::U32, Storage::I32][usize::from(signed)],
PropKind::Float if proxies.float || proxies.int32 => Storage::F32,
PropKind::Vector if proxies.vector => Storage::Vector,
_ => Storage::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Storage {
I8,
U8,
I16,
U16,
I32,
U32,
F32,
Vector,
Unknown,
}
impl Storage {
pub const fn is_compatible(self, other: Self) -> bool {
matches!(
(self, other),
(Self::I8 | Self::U8, Self::I8 | Self::U8)
| (Self::I16 | Self::U16, Self::I16 | Self::U16)
| (Self::I32 | Self::U32, Self::I32 | Self::U32)
| (Self::F32, Self::F32)
| (Self::Vector, Self::Vector)
)
}
}
impl Display for Storage {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::I8 => "i8",
Self::U8 => "u8",
Self::I16 => "i16",
Self::U16 => "u16",
Self::I32 => "i32",
Self::U32 => "u32",
Self::F32 => "f32",
Self::Vector => "a vector",
Self::Unknown => "an unknown type",
})
}
}
impl<'s> SendProp<'s> {
pub(crate) const unsafe fn from_raw(raw: NonNull<sys::SendProp>) -> Self {
Self {
raw,
_scope: PhantomData,
_not_thread_safe: PhantomData,
}
}
#[doc(alias("GetArrayProp"))]
pub fn array_prop(self) -> Option<SendProp<'s>> {
NonNull::new(prop_field!(self, m_pArrayProp))
.map(|prop| unsafe { SendProp::from_raw(prop) })
}
pub const fn as_ptr(self) -> *mut sys::SendProp {
self.raw.as_ptr()
}
#[doc(alias("m_nBits"))]
pub fn bits(self) -> c_int {
prop_field!(self, m_nBits)
}
#[doc(alias("GetDataTable"))]
pub fn data_table(self) -> Option<SendTable<'s>> {
NonNull::new(prop_field!(self, m_pDataTable))
.map(|table| unsafe { SendTable::from_raw(table) })
}
#[doc(alias("GetNumElements"))]
pub fn element_count(self) -> c_int {
prop_field!(self, m_nElements)
}
#[doc(alias("GetElementStride"))]
pub fn element_stride(self) -> c_int {
prop_field!(self, m_ElementStride)
}
#[doc(alias("GetExcludeDTName"))]
pub fn exclude_table_name(self) -> Option<&'s CStr> {
if !self.flags().contains(PropFlags::EXCLUDE) {
return None;
}
let table = prop_field!(self, m_pExcludeDTName);
unsafe { borrow_cstr(table) }
}
#[doc(alias("GetFlags"))]
pub fn flags(self) -> PropFlags {
PropFlags::from_bits_retain(prop_field!(self, m_Flags))
}
#[doc(alias("m_fHighValue"))]
pub fn high_value(self) -> f32 {
prop_field!(self, m_fHighValue)
}
#[doc(alias("GetType"))]
pub fn kind(self) -> PropKind {
PropKind::from_raw(prop_field!(self, m_Type))
}
#[doc(alias("m_fLowValue"))]
pub fn low_value(self) -> f32 {
prop_field!(self, m_fLowValue)
}
#[doc(alias("GetName"))]
pub fn name(self) -> &'s CStr {
let name = prop_field!(self, m_pVarName);
unsafe { borrow_cstr(name) }.unwrap_or_default()
}
#[doc(alias("GetOffset"))]
pub fn offset(self) -> c_int {
prop_field!(self, m_Offset)
}
}
mod sealed {
pub trait Sealed {}
}
macro_rules! primitive_net_vars {
($($Type:ty => $Storage:ident),* $(,)?) => {$(
impl sealed::Sealed for $Type {}
impl NetVar for $Type {
const STORAGE: Storage = Storage::$Storage;
unsafe fn read(source: *const u8) -> Self {
unsafe { source.cast::<Self>().read_unaligned() }
}
unsafe fn write(self, destination: *mut u8) {
unsafe { destination.cast::<Self>().write_unaligned(self) }
}
}
)*};
}
primitive_net_vars! {
i8 => I8,
u8 => U8,
i16 => I16,
u16 => U16,
i32 => I32,
u32 => U32,
f32 => F32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NetProp<'s> {
prop: SendProp<'s>,
class_table: SendTable<'s>,
proxies: StandardSendProxies<'s>,
struct_offset: usize,
offset: usize,
element: c_int,
}
impl<'s> NetProp<'s> {
pub(crate) fn resolve(
table: SendTable<'s>,
name: &CStr,
proxies: StandardSendProxies<'s>,
) -> Result<Self, NetPropError> {
struct Found<'s> {
prop: SendProp<'s>,
struct_offset: usize,
offset: c_int,
relocated_by: Option<SendProp<'s>>,
}
fn search<'s>(
table: SendTable<'s>,
name: &CStr,
base: usize,
relocated_by: Option<SendProp<'s>>,
proxies: StandardSendProxies<'s>,
depth: usize,
) -> Option<Found<'s>> {
if depth > MAX_TABLE_DEPTH {
return None;
}
for prop in table.props() {
if prop.flags().contains(PropFlags::INSIDE_ARRAY)
|| prop.flags().contains(PropFlags::EXCLUDE)
{
continue;
}
if prop.name() == name {
return Some(Found {
prop,
struct_offset: base,
offset: prop.offset(),
relocated_by,
});
}
if prop.kind() == PropKind::DataTable
&& let Some(nested) = prop.data_table()
&& let Some(nested_base) = usize::try_from(prop.offset())
.ok()
.and_then(|offset| base.checked_add(offset))
{
let relocated_by =
relocated_by.or_else(|| (!proxies.is_direct(prop)).then_some(prop));
if let Some(found) =
search(nested, name, nested_base, relocated_by, proxies, depth + 1)
{
return Some(found);
}
}
}
None
}
let Found {
prop,
struct_offset,
offset,
relocated_by,
} = search(table, name, 0, None, proxies, 0).ok_or_else(|| NetPropError::NotFound {
table: lossy(table.name()),
name: lossy(name),
})?;
if let Some(relocated_by) = relocated_by {
return Err(NetPropError::Relocated {
name: lossy(name),
table: lossy(relocated_by.name()),
});
}
let offset = usize::try_from(offset)
.ok()
.and_then(|offset| struct_offset.checked_add(offset))
.ok_or_else(|| NetPropError::InvalidOffset {
name: lossy(name),
offset,
})?;
Ok(Self {
prop,
class_table: table,
proxies,
struct_offset,
offset,
element: 0,
})
}
fn check_entity<'e>(self, entity: Entity<'e>) -> Result<Edict<'e>, NetPropError> {
let not_networked = || NetPropError::NotNetworked {
class_name: lossy(entity.class_name()),
};
let edict = entity.edict().ok_or_else(not_networked)?;
let table = entity
.server_class()
.and_then(ServerClass::table)
.ok_or_else(not_networked)?;
if !table.derives_from(self.class_table) {
return Err(NetPropError::ClassMismatch {
name: lossy(self.prop.name()),
expected: lossy(self.class_table.name()),
class_name: lossy(entity.class_name()),
found: lossy(table.name()),
});
}
Ok(edict)
}
fn check_handle(self) -> Result<(), NetPropError> {
let is_handle = self.prop.kind() == PropKind::Int
&& self.prop.flags().contains(PropFlags::UNSIGNED)
&& u32::try_from(self.prop.bits()) == Ok(NUM_NETWORKED_EHANDLE_BITS)
&& self.storage() == Storage::Unknown;
if is_handle {
Ok(())
} else {
Err(NetPropError::NotAHandle {
name: lossy(self.prop.name()),
})
}
}
fn check_storage<T: NetVar>(self) -> Result<(), NetPropError> {
let name = || lossy(self.prop.name());
match self.prop.kind() {
kind @ (PropKind::Array | PropKind::DataTable) => {
Err(NetPropError::NotAValue { name: name(), kind })
}
_ => match self.storage() {
Storage::Unknown => Err(NetPropError::UnknownStorage { name: name() }),
storage if storage.is_compatible(T::STORAGE) => Ok(()),
storage => Err(NetPropError::TypeMismatch {
name: name(),
storage,
requested: type_name::<T>(),
}),
},
}
}
pub const fn class_table(self) -> SendTable<'s> {
self.class_table
}
pub fn element(self, index: usize) -> Result<Self, NetPropError> {
let name = || lossy(self.prop.name());
let out_of_range = |len| NetPropError::ElementOutOfRange {
name: name(),
index,
len,
};
match self.prop.kind() {
PropKind::Array => {
let len = usize::try_from(self.prop.element_count()).unwrap_or(0);
let element = self
.prop
.array_prop()
.filter(|_| index < len)
.ok_or_else(|| out_of_range(len))?;
let stride = usize::try_from(self.prop.element_stride()).ok();
let offset = usize::try_from(element.offset())
.ok()
.zip(stride)
.and_then(|(offset, stride)| {
self.struct_offset
.checked_add(offset)?
.checked_add(stride.checked_mul(index)?)
})
.ok_or_else(|| NetPropError::InvalidOffset {
name: name(),
offset: element.offset(),
})?;
Ok(Self {
prop: element,
offset,
element: c_int::try_from(index).map_err(|_| out_of_range(len))?,
..self
})
}
PropKind::DataTable => {
let table = self.prop.data_table().ok_or_else(|| out_of_range(0))?;
let element = table.prop(index).ok_or_else(|| out_of_range(table.len()))?;
if !self.proxies.is_direct(self.prop) {
return Err(NetPropError::Relocated {
name: lossy(element.name()),
table: name(),
});
}
let offset = usize::try_from(element.offset())
.ok()
.and_then(|offset| self.offset.checked_add(offset))
.ok_or_else(|| NetPropError::InvalidOffset {
name: lossy(element.name()),
offset: element.offset(),
})?;
Ok(Self {
prop: element,
struct_offset: self.offset,
offset,
element: 0,
..self
})
}
kind => Err(NetPropError::NotAnArray { name: name(), kind }),
}
}
pub fn element_count(self) -> Option<usize> {
match self.prop.kind() {
PropKind::Array => usize::try_from(self.prop.element_count()).ok(),
PropKind::DataTable => self.prop.data_table().map(SendTable::len),
_ => None,
}
}
pub fn get<T: NetVar>(self, entity: Entity<'_>) -> Result<T, NetPropError> {
self.check_entity(entity)?;
self.check_storage::<T>()?;
Ok(unsafe { T::read(entity.as_ptr().cast::<u8>().add(self.offset)) })
}
#[cfg(feature = "tf2")]
pub(crate) unsafe fn get_at<T: NetVar>(self, base: NonNull<c_void>) -> Result<T, NetPropError> {
self.check_storage::<T>()?;
Ok(unsafe { T::read(base.as_ptr().cast::<u8>().add(self.offset)) })
}
#[cfg(feature = "tf2")]
pub(crate) unsafe fn get_handle_at(
self,
base: NonNull<c_void>,
) -> Result<EntityHandle, NetPropError> {
self.check_handle()?;
Ok(EntityHandle::from_raw(unsafe {
base.as_ptr()
.cast::<u8>()
.add(self.offset)
.cast::<u32>()
.read_unaligned()
}))
}
#[doc(alias("SendPropEHandle", "CHandle", "EHANDLE"))]
pub fn get_handle(self, entity: Entity<'_>) -> Result<EntityHandle, NetPropError> {
self.check_entity(entity)?;
self.check_handle()?;
Ok(EntityHandle::from_raw(unsafe {
entity
.as_ptr()
.cast::<u8>()
.add(self.offset)
.cast::<u32>()
.read_unaligned()
}))
}
pub const fn offset(self) -> usize {
self.offset
}
pub const fn prop(self) -> SendProp<'s> {
self.prop
}
pub unsafe fn set<T: NetVar>(
self,
engine: ValveEngine<'_>,
entity: Entity<'_>,
value: T,
) -> Result<(), NetPropError> {
let edict = self.check_entity(entity)?;
self.check_storage::<T>()?;
unsafe { value.write(entity.as_ptr().cast::<u8>().add(self.offset)) };
match u16::try_from(self.offset) {
Ok(offset) => edict.state_changed(engine, offset),
Err(_) => edict.full_state_changed(engine),
}
Ok(())
}
#[cfg(feature = "tf2")]
pub(crate) unsafe fn set_at<T: NetVar>(
self,
base: NonNull<c_void>,
value: T,
) -> Result<(), NetPropError> {
self.check_storage::<T>()?;
unsafe { value.write(base.as_ptr().cast::<u8>().add(self.offset)) };
Ok(())
}
#[doc(alias("SendPropEHandle", "CHandle", "EHANDLE"))]
pub unsafe fn set_handle(
self,
engine: ValveEngine<'_>,
entity: Entity<'_>,
handle: EntityHandle,
) -> Result<(), NetPropError> {
let edict = self.check_entity(entity)?;
self.check_handle()?;
if handle
.index()
.is_some_and(|index| index >= EntityHandle::SLOTS)
{
return Err(NetPropError::HandleOutOfRange {
name: lossy(self.prop.name()),
handle,
});
}
unsafe {
entity
.as_ptr()
.cast::<u8>()
.add(self.offset)
.cast::<u32>()
.write_unaligned(handle.to_raw())
};
match u16::try_from(self.offset) {
Ok(offset) => edict.state_changed(engine, offset),
Err(_) => edict.full_state_changed(engine),
}
Ok(())
}
pub fn storage(self) -> Storage {
self.proxies.storage(self.prop)
}
#[doc(alias("SendVarProxyFn"))]
pub fn value(self, entity: Entity<'_>) -> Result<NetValue, NetPropError> {
let edict = self.check_entity(entity)?;
let kind = self.prop.kind();
if matches!(
kind,
PropKind::Array | PropKind::DataTable | PropKind::Unknown(_)
) {
return Err(NetPropError::NotAValue {
name: lossy(self.prop.name()),
kind,
});
}
let base = entity.as_ptr().cast::<u8>();
let value = unsafe {
call_var_proxy(
self.prop.as_ptr(),
base.add(self.struct_offset).cast::<c_void>(),
base.add(self.offset).cast::<c_void>(),
self.element,
edict.index(),
)
}
.ok_or_else(|| NetPropError::NoProxy {
name: lossy(self.prop.name()),
})?
.__bindgen_anon_1;
Ok(unsafe {
match kind {
PropKind::Int => NetValue::Int(value.m_Int),
PropKind::Float => NetValue::Float(value.m_Float),
PropKind::Vector => NetValue::Vector(Vector::new(
value.m_Vector[0],
value.m_Vector[1],
value.m_Vector[2],
)),
PropKind::VectorXY => {
NetValue::VectorXY(Vec2::new(value.m_Vector[0], value.m_Vector[1]))
}
PropKind::String => {
NetValue::String(copy_cstr(value.m_pString).unwrap_or_default())
}
PropKind::Array | PropKind::DataTable | PropKind::Unknown(_) => unreachable!(),
}
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum NetPropError {
#[error("server class `{class}` has no send table")]
NoTable {
class: String,
},
#[error("the game DLL provides no standard send proxies to recognize storage by")]
NoStandardProxies,
#[error("`{table}` has no networked variable named `{name}`")]
NotFound {
table: String,
name: String,
},
#[error(
"`{name}` is inside `{table}`, whose send proxy relocates its data, so its address is unknown"
)]
Relocated {
name: String,
table: String,
},
#[error("`{name}` has an invalid offset of {offset} bytes")]
InvalidOffset {
name: String,
offset: c_int,
},
#[error("`{name}` is not networked as an entity handle")]
NotAHandle {
name: String,
},
#[error("`{name}` cannot hold handle {handle}, whose slot lies past the entity list")]
HandleOutOfRange {
name: String,
handle: EntityHandle,
},
#[error("`{name}` is {kind}, which has no single value")]
NotAValue {
name: String,
kind: PropKind,
},
#[error("`{name}` is {kind}, which has no elements")]
NotAnArray {
name: String,
kind: PropKind,
},
#[error("`{name}` has {len} elements, so it has no element {index}")]
ElementOutOfRange {
name: String,
index: usize,
len: usize,
},
#[error("`{name}` is stored as {storage}, not {requested}")]
TypeMismatch {
name: String,
storage: Storage,
requested: &'static str,
},
#[error(
"`{name}` has a custom send proxy, so how it is stored is unknown; read it with `NetProp::value` instead"
)]
UnknownStorage {
name: String,
},
#[error("`{name}` has no send proxy")]
NoProxy {
name: String,
},
#[error("`{class_name}` is not networked")]
NotNetworked {
class_name: String,
},
#[error(
"`{name}` belongs to `{expected}`, which `{class_name}`'s table `{found}` does not derive from"
)]
ClassMismatch {
name: String,
expected: String,
class_name: String,
found: String,
},
}
impl NetVar for QAngle {
const STORAGE: Storage = Storage::Vector;
unsafe fn read(source: *const u8) -> Self {
unsafe { source.cast::<sys::QAngle>().read_unaligned() }.into()
}
unsafe fn write(self, destination: *mut u8) {
unsafe {
destination
.cast::<sys::QAngle>()
.write_unaligned(self.into())
}
}
}
impl sealed::Sealed for QAngle {}
impl NetVar for Vector {
const STORAGE: Storage = Storage::Vector;
unsafe fn read(source: *const u8) -> Self {
unsafe { source.cast::<sys::Vector>().read_unaligned() }.into()
}
unsafe fn write(self, destination: *mut u8) {
unsafe {
destination
.cast::<sys::Vector>()
.write_unaligned(self.into())
}
}
}
impl sealed::Sealed for Vector {}
impl NetVar for bool {
const STORAGE: Storage = Storage::U8;
unsafe fn read(source: *const u8) -> Self {
unsafe { source.read() != 0 }
}
unsafe fn write(self, destination: *mut u8) {
unsafe { destination.write(u8::from(self)) }
}
}
impl sealed::Sealed for bool {}
fn lossy(string: &CStr) -> String {
string.to_string_lossy().into_owned()
}