use crate::datatables::{NetProp, PropFlags, PropKind, SendProp, SendTable, Storage};
use crate::entities::Entity;
use crate::interfaces::ServerGameDll;
use crate::tf2::attributes::{
AttributeError, AttributeIndex, MAX_RUNTIME_ATTRIBUTES, RuntimeAttribute,
};
use crate::tf2::weapons::ItemDefinitionIndex;
use sdk_raw::datatables::{SendPropExtraUtlVector, utl_vector_extra};
use sdk_raw::vcall;
use std::ffi::{CStr, c_int};
use std::mem::{offset_of, size_of};
use std::ptr::NonNull;
const CONTAINER: usize = offset_of!(sys::CEconEntity, m_AttributeManager);
const DEFINITION: usize = ITEM + offset_of!(sys::CEconItemView, m_iItemDefinitionIndex);
const ITEM: usize = CONTAINER + offset_of!(sys::CAttributeContainer, m_Item);
const LIST: usize = ITEM + offset_of!(sys::CEconItemView, m_AttributeList);
const MAX_LIST_LEN: c_int = 4096;
const OUTER: usize = CONTAINER + offset_of!(sys::CAttributeManager, m_hOuter);
#[derive(Debug, Clone, Copy)]
pub(super) struct ItemLayout<'s> {
entity: Entity<'s>,
}
impl<'s> ItemLayout<'s> {
pub(super) fn validate(
dll: ServerGameDll<'s>,
entity: Entity<'s>,
) -> Result<Self, AttributeError> {
let (container, item) = item_tables(dll, entity)?;
check_offset(child(container, c"m_hOuter")?, OUTER)?;
let list = child(item, c"m_AttributeList")?;
check_table(list, LIST)?;
let entry = vector_entries(child(list, c"m_Attributes")?)?;
let proxies = dll
.standard_send_proxies()
.ok_or(AttributeError::UnsupportedLayout)?;
let field = |name: &CStr, offset: usize, storage: Storage| {
let prop = NetProp::resolve(entry, name, proxies)
.map_err(|_| AttributeError::UnsupportedLayout)?;
check_offset(prop, offset)?;
check_storage(prop.storage(), storage)
};
field(
c"m_iAttributeDefinitionIndex",
offset_of!(sys::CEconItemAttribute, m_iAttributeDefinitionIndex),
Storage::U16,
)?;
field(
c"m_iRawValue32",
offset_of!(sys::CEconItemAttribute, m_flValue),
Storage::U32,
)?;
field(
c"m_nRefundableCurrency",
offset_of!(sys::CEconItemAttribute, m_nRefundableCurrency),
Storage::I32,
)?;
Ok(Self { entity })
}
pub(super) fn definition_index(self) -> u16 {
unsafe { read_definition_index(self.entity) }
}
pub(super) const fn entity(self) -> Entity<'s> {
self.entity
}
fn list(self) -> Result<*mut sys::CAttributeList, AttributeError> {
let base = self.entity.as_ptr();
let (container, list) = unsafe {
(
base.byte_add(CONTAINER).cast::<sys::CAttributeContainer>(),
base.byte_add(LIST).cast::<sys::CAttributeList>(),
)
};
let (manager, outer) = unsafe {
(
(&raw const (*list).m_pManager).read(),
(&raw const (*container)._base.m_hOuter.m_Value._base.m_Index).read(),
)
};
if manager.cast() != container || outer != self.entity.handle().to_raw() {
return Err(AttributeError::Unlinked);
}
Ok(list)
}
#[doc(alias("OnAttributeValuesChanged"))]
pub(super) fn notify(self) -> Result<(), AttributeError> {
let list = self.list()?;
unsafe {
let container = (&raw const (*list).m_pManager)
.read()
.cast::<sys::CAttributeContainer>();
type Vtable = sys::CAttributeContainer__bindgen_vtable;
vcall!(container as Vtable => CAttributeContainer_OnAttributeValuesChanged());
}
Ok(())
}
pub(super) unsafe fn restore_bits(
self,
position: usize,
bits: u32,
) -> Result<(), AttributeError> {
let (memory, len) = self.vector()?;
if position >= len {
return Err(AttributeError::UnexpectedChange);
}
unsafe {
(&raw mut (*memory.add(position)).m_flValue.m_Value)
.cast::<u32>()
.write(bits)
};
Ok(())
}
pub(super) fn snapshot(self) -> Result<Vec<RuntimeAttribute>, AttributeError> {
let (memory, len) = self.vector()?;
let mut entries = Vec::with_capacity(len);
let mut vtable = None;
for position in 0..len {
let (entry_vtable, index, bits, refundable_currency) = unsafe {
let entry = memory.add(position);
(
(&raw const (*entry).vtable_).read(),
(&raw const (*entry).m_iAttributeDefinitionIndex.m_Value).read(),
(&raw const (*entry).m_flValue.m_Value).cast::<u32>().read(),
(&raw const (*entry).m_nRefundableCurrency.m_Value).read(),
)
};
if entry_vtable.is_null() || *vtable.get_or_insert(entry_vtable) != entry_vtable {
return Err(AttributeError::UnsupportedLayout);
}
entries.push(RuntimeAttribute {
index: AttributeIndex::new(index).ok_or(AttributeError::UnsupportedLayout)?,
bits,
refundable_currency,
});
}
Ok(entries)
}
pub(super) unsafe fn truncate(self, len: usize) -> Result<(), AttributeError> {
let list = self.list()?;
let (_, current) = self.vector()?;
if len > current {
return Err(AttributeError::UnexpectedChange);
}
unsafe { (&raw mut (*list).m_Attributes.m_Size).write(len as c_int) };
Ok(())
}
fn vector(self) -> Result<(*mut sys::CEconItemAttribute, usize), AttributeError> {
let list = self.list()?;
let (memory, allocated, len) = unsafe {
(
(&raw const (*list).m_Attributes.m_Memory.m_pMemory).read(),
(&raw const (*list).m_Attributes.m_Memory.m_nAllocationCount).read(),
(&raw const (*list).m_Attributes.m_Size).read(),
)
};
let plausible = (0..=MAX_LIST_LEN).contains(&len)
&& len <= allocated
&& (len == 0 || (!memory.is_null() && memory.is_aligned()));
if !plausible {
return Err(AttributeError::UnsupportedLayout);
}
Ok((memory, len as usize))
}
}
fn check_offset(prop: NetProp<'_>, offset: usize) -> Result<(), AttributeError> {
if prop.offset() == offset {
Ok(())
} else {
Err(AttributeError::UnsupportedLayout)
}
}
fn check_storage(storage: Storage, expected: Storage) -> Result<(), AttributeError> {
if storage.is_compatible(expected) {
Ok(())
} else {
Err(AttributeError::UnsupportedLayout)
}
}
fn check_table(prop: NetProp<'_>, offset: usize) -> Result<(), AttributeError> {
if prop.prop().kind() == PropKind::DataTable {
check_offset(prop, offset)
} else {
Err(AttributeError::UnsupportedLayout)
}
}
fn child<'s>(parent: NetProp<'s>, name: &CStr) -> Result<NetProp<'s>, AttributeError> {
let index = parent
.prop()
.data_table()
.and_then(|table| {
table.props().position(|prop| {
prop.name() == name
&& !prop.flags().contains(PropFlags::EXCLUDE)
&& !prop.flags().contains(PropFlags::INSIDE_ARRAY)
})
})
.ok_or(AttributeError::UnsupportedLayout)?;
parent
.element(index)
.map_err(|_| AttributeError::UnsupportedLayout)
}
fn extra_data(prop: SendProp<'_>) -> Option<NonNull<SendPropExtraUtlVector>> {
unsafe { utl_vector_extra(prop.as_ptr()) }
}
pub(crate) fn item_definition<'s>(
dll: ServerGameDll<'s>,
entity: Entity<'s>,
) -> Result<Option<ItemDefinitionIndex>, AttributeError> {
item_tables(dll, entity)?;
Ok(ItemDefinitionIndex::new(unsafe {
read_definition_index(entity)
}))
}
fn item_tables<'s>(
dll: ServerGameDll<'s>,
entity: Entity<'s>,
) -> Result<(NetProp<'s>, NetProp<'s>), AttributeError> {
let container = dll
.entity_net_prop(entity, c"m_AttributeManager")
.map_err(|_| AttributeError::UnsupportedLayout)?;
check_table(container, CONTAINER)?;
let item = child(container, c"m_Item")?;
check_table(item, ITEM)?;
let definition = child(item, c"m_iItemDefinitionIndex")?;
check_offset(definition, DEFINITION)?;
check_storage(definition.storage(), Storage::U16)?;
Ok((container, item))
}
unsafe fn read_definition_index(entity: Entity<'_>) -> u16 {
unsafe {
let item = entity.as_ptr().byte_add(ITEM).cast::<sys::CEconItemView>();
(&raw const (*item).m_iItemDefinitionIndex.m_Value).read()
}
}
fn vector_entries(vector: NetProp<'_>) -> Result<SendTable<'_>, AttributeError> {
let unsupported = AttributeError::UnsupportedLayout;
check_table(vector, LIST)?;
vector.element(0).map_err(|_| unsupported)?;
let props = vector
.prop()
.data_table()
.filter(|props| props.len() == MAX_RUNTIME_ATTRIBUTES + 1)
.ok_or(unsupported)?;
let extra = props
.prop(0)
.filter(|prop| prop.kind() == PropKind::DataTable && prop.name() == c"lengthproxy")
.and_then(extra_data)
.ok_or(unsupported)?;
let entry = props
.prop(1)
.and_then(|prop| prop.data_table())
.ok_or(unsupported)?;
for (position, prop) in props.props().enumerate().skip(1) {
let element = prop.kind() == PropKind::DataTable
&& prop.data_table().map(|table| table.as_ptr()) == Some(entry.as_ptr())
&& extra_data(prop) == Some(extra)
&& usize::try_from(prop.element_stride()) == Ok(position - 1);
if !element {
return Err(unsupported);
}
}
let extra = unsafe { extra.read() };
let generated = usize::try_from(extra.offset)
== Ok(offset_of!(sys::CAttributeList, m_Attributes))
&& usize::try_from(extra.element_stride) == Ok(size_of::<sys::CEconItemAttribute>())
&& usize::try_from(extra.max_elements) == Ok(MAX_RUNTIME_ATTRIBUTES);
if generated {
Ok(entry)
} else {
Err(unsupported)
}
}