use std::collections::HashSet;
use rustc_hash::FxHashMap;
use crate::error::{Error, Result};
use crate::io::BitReader;
use super::class_info::ClassInfo;
use super::field_decoder::FieldDecodeContext;
use super::field_path::{self, FieldPath};
use super::field_value::FieldValue;
use super::serializers::{Serializer, SerializerContainer, SerializerField};
use super::string_tables::StringTableContainer;
use boon_proto::proto::CsvcMsgPacketEntities;
const MAX_EDICT_BITS: u32 = 14;
const NUM_ENT_ENTRY_BITS: u32 = MAX_EDICT_BITS + 1;
const NUM_SERIAL_NUM_BITS: u32 = 32 - NUM_ENT_ENTRY_BITS;
const MAX_ENTITY_INDEX: i64 = (1i64 << MAX_EDICT_BITS) - 1;
pub const ENTITY_HANDLE_INDEX_MASK: u32 = 0x3FFF;
pub const INVALID_ENTITY_HANDLE: u32 = 0x00FF_FFFF;
pub fn protobuf_handle_index(handle: Option<u32>) -> Option<i32> {
handle
.filter(|&h| h != INVALID_ENTITY_HANDLE)
.map(|h| (h & ENTITY_HANDLE_INDEX_MASK) as i32)
}
const DELTA_UPDATE: u8 = 0b00;
const DELTA_CREATE: u8 = 0b10;
const DELTA_LEAVE: u8 = 0b01;
const DELTA_DELETE: u8 = 0b11;
fn resolve_field_path<'a>(
serializer: &'a Serializer,
fp: &FieldPath,
) -> Result<&'a SerializerField> {
let oob = || Error::Parse {
context: format!(
"field path out of range for serializer '{}' ({} fields)",
serializer.name,
serializer.fields.len()
),
};
let mut field = serializer.fields.get(fp.get(0)).ok_or_else(oob)?;
for i in 1..=fp.last {
let idx = fp.get(i);
if field.is_dynamic_array() {
match field.field_serializer.as_ref() {
Some(fs) => field = fs.fields.first().ok_or_else(oob)?,
None => break,
}
} else if let Some(fs) = field.field_serializer.as_ref() {
field = fs.fields.get(idx).ok_or_else(oob)?;
} else {
break;
}
}
Ok(field)
}
#[derive(Debug, Clone)]
pub struct Entity {
pub index: i32,
pub serial: u32,
pub class_id: i32,
pub class_name: String,
pub fields: FxHashMap<u64, FieldValue>,
}
impl Entity {
fn new(index: i32, class_id: i32, class_name: String) -> Self {
Self {
index,
serial: 0,
class_id,
class_name,
fields: FxHashMap::default(),
}
}
#[allow(clippy::needless_range_loop)]
fn apply_update(
&mut self,
br: &mut BitReader,
serializer: &Serializer,
ctx: &mut FieldDecodeContext,
fp_buf: &mut Vec<FieldPath>,
) -> Result<()> {
field_path::read_field_paths(br, fp_buf)?;
for fp_idx in 0..fp_buf.len() {
let field = resolve_field_path(serializer, &fp_buf[fp_idx])?;
let key = fp_buf[fp_idx].pack();
let value = field
.metadata
.decoder
.decode(ctx, br)
.map_err(|e| Error::Parse {
context: format!(
"field #{} key={:#x} (type: {}, decoder: {:?}, pos: {}, remaining: {}): {}",
fp_idx,
key,
field.var_type,
field.metadata.decoder,
br.position(),
br.bits_remaining(),
e
),
})?;
self.fields.insert(key, value);
}
Ok(())
}
#[allow(clippy::needless_range_loop)]
fn skip_update(
br: &mut BitReader,
serializer: &Serializer,
ctx: &mut FieldDecodeContext,
fp_buf: &mut Vec<FieldPath>,
) -> Result<()> {
field_path::read_field_paths(br, fp_buf)?;
for fp_idx in 0..fp_buf.len() {
let field = resolve_field_path(serializer, &fp_buf[fp_idx])?;
field.metadata.decoder.skip(ctx, br)?;
}
Ok(())
}
pub fn get_by_name(&self, path: &str, serializer: &Serializer) -> Option<&FieldValue> {
let key = serializer.resolve_field_key(path)?;
self.fields.get(&key)
}
pub fn get_i64(&self, key: Option<u64>) -> i64 {
key.and_then(|k| self.fields.get(&k))
.and_then(|v| match v {
FieldValue::U32(n) => Some(*n as i64),
FieldValue::U64(n) => Some(*n as i64),
FieldValue::I32(n) => Some(*n as i64),
FieldValue::I64(n) => Some(*n),
_ => None,
})
.unwrap_or(0)
}
pub fn get_u32(&self, key: Option<u64>) -> u32 {
key.and_then(|k| self.fields.get(&k))
.and_then(|v| match v {
FieldValue::U32(n) => Some(*n),
FieldValue::U64(n) => Some(*n as u32),
FieldValue::I32(n) => Some(*n as u32),
FieldValue::I64(n) => Some(*n as u32),
_ => None,
})
.unwrap_or(0)
}
pub fn get_f32(&self, key: Option<u64>) -> f32 {
key.and_then(|k| self.fields.get(&k))
.and_then(|v| match v {
FieldValue::F32(f) => Some(*f),
_ => None,
})
.unwrap_or(0.0)
}
pub fn get_bool(&self, key: Option<u64>) -> bool {
key.and_then(|k| self.fields.get(&k))
.and_then(|v| match v {
FieldValue::Bool(b) => Some(*b),
_ => None,
})
.unwrap_or(false)
}
pub fn get_qangle(&self, key: Option<u64>) -> [f32; 3] {
key.and_then(|k| self.fields.get(&k))
.and_then(|v| match v {
FieldValue::QAngle(a) => Some(*a),
_ => None,
})
.unwrap_or([0.0; 3])
}
pub fn world_position(
&self,
cell_keys: [Option<u64>; 3],
offset_keys: [Option<u64>; 3],
) -> [f32; 3] {
let cell = [
self.get_i64(cell_keys[0]) as i32,
self.get_i64(cell_keys[1]) as i32,
self.get_i64(cell_keys[2]) as i32,
];
let offset = [
self.get_f32(offset_keys[0]),
self.get_f32(offset_keys[1]),
self.get_f32(offset_keys[2]),
];
[
crate::position::cell_to_world(cell[0], offset[0]),
crate::position::cell_to_world(cell[1], offset[1]),
crate::position::cell_to_world(cell[2], offset[2]),
]
}
pub fn get_handle(&self, key: Option<u64>) -> Option<u32> {
key.and_then(|k| self.fields.get(&k)).and_then(|v| match v {
FieldValue::U32(n) => Some(*n),
FieldValue::U64(n) => Some(*n as u32),
FieldValue::I32(n) => Some(*n as u32),
FieldValue::I64(n) => Some(*n as u32),
_ => None,
})
}
}
#[derive(Default)]
pub struct EntityContainer {
pub entities: Vec<Option<Entity>>,
skipped_entity_classes: Vec<i32>,
updated: Vec<i32>,
}
impl EntityContainer {
pub fn new() -> Self {
Self::default()
}
#[inline]
fn entity_mut(&mut self, index: i32) -> Option<&mut Entity> {
usize::try_from(index)
.ok()
.and_then(|i| self.entities.get_mut(i))
.and_then(Option::as_mut)
}
#[inline]
fn put_entity(&mut self, index: i32, entity: Entity) {
let Ok(i) = usize::try_from(index) else {
return;
};
if i >= self.entities.len() {
self.entities.resize_with(i + 1, || None);
}
self.entities[i] = Some(entity);
}
#[inline]
fn take_entity(&mut self, index: i32) {
if let Ok(i) = usize::try_from(index)
&& let Some(slot) = self.entities.get_mut(i)
{
*slot = None;
}
}
#[inline]
fn skipped_class(&self, index: i32) -> Option<i32> {
usize::try_from(index)
.ok()
.and_then(|i| self.skipped_entity_classes.get(i))
.copied()
.filter(|&c| c >= 0)
}
#[inline]
fn set_skipped(&mut self, index: i32, class_id: Option<i32>) {
let Ok(i) = usize::try_from(index) else {
return;
};
let value = class_id.unwrap_or(-1);
if i >= self.skipped_entity_classes.len() {
if value < 0 {
return; }
self.skipped_entity_classes.resize(i + 1, -1);
}
self.skipped_entity_classes[i] = value;
}
pub fn handle_packet_entities(
&mut self,
msg: CsvcMsgPacketEntities,
class_info: &ClassInfo,
serializers: &SerializerContainer,
string_tables: &StringTableContainer,
field_decode_ctx: &mut FieldDecodeContext,
fp_buf: &mut Vec<FieldPath>,
) -> Result<()> {
let has_pvs_vis_bits = msg.has_pvs_vis_bits_deprecated.unwrap_or(0);
let entity_data = msg.entity_data.unwrap_or_default();
let mut br = BitReader::new(&entity_data);
let mut entity_index: i32 = -1;
for _ in 0..msg.updated_entries.unwrap_or(0) {
let next = entity_index as i64 + br.read_ubitvar()? as i64 + 1;
if !(0..=MAX_ENTITY_INDEX).contains(&next) {
return Err(Error::Parse {
context: format!("entity index {next} out of range"),
});
}
entity_index = next as i32;
let dh = br.read_bits(2)? as u8;
match dh {
DELTA_CREATE => {
self.handle_create(
entity_index,
&mut br,
class_info,
serializers,
string_tables,
field_decode_ctx,
fp_buf,
)
.map_err(|e| Error::Parse {
context: format!("entity create #{}: {}", entity_index, e),
})?;
self.updated.push(entity_index);
}
DELTA_UPDATE => {
if has_pvs_vis_bits > 0 && (br.read_bits(2)? & 0x01) != 0 {
continue;
}
self.handle_update(
entity_index,
&mut br,
class_info,
serializers,
field_decode_ctx,
fp_buf,
)
.map_err(|e| Error::Parse {
context: format!(
"entity update #{} (class: {:?}): {}",
entity_index,
self.get(entity_index).map(|e| &e.class_name),
e
),
})?;
self.updated.push(entity_index);
}
DELTA_DELETE | DELTA_LEAVE => {
self.take_entity(entity_index);
}
_ => {}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn handle_packet_entities_filtered(
&mut self,
msg: CsvcMsgPacketEntities,
class_info: &ClassInfo,
serializers: &SerializerContainer,
string_tables: &StringTableContainer,
field_decode_ctx: &mut FieldDecodeContext,
class_filter: &HashSet<&str>,
fp_buf: &mut Vec<FieldPath>,
) -> Result<()> {
let has_pvs_vis_bits = msg.has_pvs_vis_bits_deprecated.unwrap_or(0);
let entity_data = msg.entity_data.unwrap_or_default();
let mut br = BitReader::new(&entity_data);
let mut entity_index: i32 = -1;
for _ in 0..msg.updated_entries.unwrap_or(0) {
let next = entity_index as i64 + br.read_ubitvar()? as i64 + 1;
if !(0..=MAX_ENTITY_INDEX).contains(&next) {
return Err(Error::Parse {
context: format!("entity index {next} out of range"),
});
}
entity_index = next as i32;
let dh = br.read_bits(2)? as u8;
match dh {
DELTA_CREATE => {
if self.handle_create_filtered(
entity_index,
&mut br,
class_info,
serializers,
string_tables,
field_decode_ctx,
class_filter,
fp_buf,
)? {
self.updated.push(entity_index);
}
}
DELTA_UPDATE => {
if has_pvs_vis_bits > 0 && (br.read_bits(2)? & 0x01) != 0 {
continue;
}
if self.handle_update_filtered(
entity_index,
&mut br,
class_info,
serializers,
field_decode_ctx,
class_filter,
fp_buf,
)? {
self.updated.push(entity_index);
}
}
DELTA_DELETE | DELTA_LEAVE => {
self.take_entity(entity_index);
self.set_skipped(entity_index, None);
}
_ => {}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn handle_create(
&mut self,
index: i32,
br: &mut BitReader,
class_info: &ClassInfo,
serializers: &SerializerContainer,
string_tables: &StringTableContainer,
field_decode_ctx: &mut FieldDecodeContext,
fp_buf: &mut Vec<FieldPath>,
) -> Result<()> {
let class_id = br.read_bits(class_info.bits)? as i32;
let _serial = br.read_bits(NUM_SERIAL_NUM_BITS as usize)?;
let _unknown = br.read_uvarint32()?;
let class_entry = class_info.by_id(class_id).ok_or_else(|| Error::Parse {
context: format!("unknown class_id {}", class_id),
})?;
let serializer =
serializers
.get(&class_entry.network_name)
.ok_or_else(|| Error::Parse {
context: format!("no serializer for {}", class_entry.network_name),
})?;
let mut entity = Entity::new(index, class_id, class_entry.network_name.clone());
entity.fields.reserve(serializer.fields.len());
if let Some(baseline_data) = string_tables.instance_baselines.get(&class_id) {
let mut baseline_br = BitReader::new(baseline_data);
entity
.apply_update(&mut baseline_br, serializer, field_decode_ctx, fp_buf)
.map_err(|err| Error::Parse {
context: format!(
"baseline for {} (class_id {}): {}",
class_entry.network_name, class_id, err
),
})?;
}
entity
.apply_update(br, serializer, field_decode_ctx, fp_buf)
.map_err(|err| Error::Parse {
context: format!(
"create delta for {} (class_id {}): {}",
class_entry.network_name, class_id, err
),
})?;
self.put_entity(index, entity);
Ok(())
}
fn handle_update(
&mut self,
index: i32,
br: &mut BitReader,
_class_info: &ClassInfo,
serializers: &SerializerContainer,
field_decode_ctx: &mut FieldDecodeContext,
fp_buf: &mut Vec<FieldPath>,
) -> Result<()> {
let entity = match self.entity_mut(index) {
Some(e) => e,
None => {
return Err(Error::Parse {
context: format!("tried to update non-existent entity #{}", index),
});
}
};
let serializer = serializers
.get(&entity.class_name)
.ok_or_else(|| Error::Parse {
context: format!("no serializer for {}", entity.class_name),
})?;
entity.apply_update(br, serializer, field_decode_ctx, fp_buf)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn handle_create_filtered(
&mut self,
index: i32,
br: &mut BitReader,
class_info: &ClassInfo,
serializers: &SerializerContainer,
string_tables: &StringTableContainer,
field_decode_ctx: &mut FieldDecodeContext,
class_filter: &HashSet<&str>,
fp_buf: &mut Vec<FieldPath>,
) -> Result<bool> {
let class_id = br.read_bits(class_info.bits)? as i32;
let _serial = br.read_bits(NUM_SERIAL_NUM_BITS as usize)?;
let _unknown = br.read_uvarint32()?;
let class_entry = class_info.by_id(class_id).ok_or_else(|| Error::Parse {
context: format!("unknown class_id {}", class_id),
})?;
let serializer =
serializers
.get(&class_entry.network_name)
.ok_or_else(|| Error::Parse {
context: format!("no serializer for {}", class_entry.network_name),
})?;
if !class_filter.contains(class_entry.network_name.as_str()) {
self.set_skipped(index, Some(class_id));
Entity::skip_update(br, serializer, field_decode_ctx, fp_buf)?;
return Ok(false);
}
let mut entity = Entity::new(index, class_id, class_entry.network_name.clone());
entity.fields.reserve(serializer.fields.len());
if let Some(baseline_data) = string_tables.instance_baselines.get(&class_id) {
let mut baseline_br = BitReader::new(baseline_data);
entity.apply_update(&mut baseline_br, serializer, field_decode_ctx, fp_buf)?;
}
entity.apply_update(br, serializer, field_decode_ctx, fp_buf)?;
self.put_entity(index, entity);
Ok(true)
}
#[allow(clippy::too_many_arguments)]
fn handle_update_filtered(
&mut self,
index: i32,
br: &mut BitReader,
class_info: &ClassInfo,
serializers: &SerializerContainer,
field_decode_ctx: &mut FieldDecodeContext,
_class_filter: &HashSet<&str>,
fp_buf: &mut Vec<FieldPath>,
) -> Result<bool> {
if let Some(entity) = self.entity_mut(index) {
let serializer = serializers
.get(&entity.class_name)
.ok_or_else(|| Error::Parse {
context: format!("no serializer for {}", entity.class_name),
})?;
entity.apply_update(br, serializer, field_decode_ctx, fp_buf)?;
return Ok(true);
}
if let Some(class_id) = self.skipped_class(index) {
let class_entry = class_info.by_id(class_id).ok_or_else(|| Error::Parse {
context: format!("unknown class_id {}", class_id),
})?;
let serializer =
serializers
.get(&class_entry.network_name)
.ok_or_else(|| Error::Parse {
context: format!("no serializer for {}", class_entry.network_name),
})?;
Entity::skip_update(br, serializer, field_decode_ctx, fp_buf)?;
}
Ok(false)
}
pub fn get(&self, index: i32) -> Option<&Entity> {
usize::try_from(index)
.ok()
.and_then(|i| self.entities.get(i))
.and_then(Option::as_ref)
}
pub fn get_by_handle(&self, handle: u32) -> Option<&Entity> {
self.get((handle & ENTITY_HANDLE_INDEX_MASK) as i32)
}
pub fn iter(&self) -> impl Iterator<Item = (i32, &Entity)> {
self.entities
.iter()
.enumerate()
.filter_map(|(i, slot)| slot.as_ref().map(|e| (i as i32, e)))
}
pub fn updated_indices(&self) -> &[i32] {
&self.updated
}
pub fn clear_updated(&mut self) {
self.updated.clear();
}
pub fn len(&self) -> usize {
self.entities.iter().filter(|slot| slot.is_some()).count()
}
pub fn is_empty(&self) -> bool {
self.entities.iter().all(Option::is_none)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn container_new_is_empty() {
let c = EntityContainer::new();
assert!(c.is_empty());
assert_eq!(c.len(), 0);
assert!(c.get(0).is_none());
}
#[test]
fn entity_fields_insert_and_get() {
let mut e = Entity::new(1, 10, "TestClass".to_string());
e.fields.insert(42, FieldValue::I32(100));
assert!(matches!(e.fields.get(&42), Some(FieldValue::I32(100))));
}
#[test]
fn container_insert_and_iter() {
let mut c = EntityContainer::new();
let e = Entity::new(5, 10, "Hero".to_string());
c.put_entity(5, e);
assert_eq!(c.len(), 1);
assert!(!c.is_empty());
assert!(c.get(5).is_some());
assert_eq!(c.get(5).unwrap().class_name, "Hero");
}
#[test]
fn container_iter_yields_entries() {
let mut c = EntityContainer::new();
c.put_entity(1, Entity::new(1, 1, "A".to_string()));
c.put_entity(2, Entity::new(2, 2, "B".to_string()));
let keys: Vec<i32> = c.iter().map(|(k, _)| k).collect();
assert_eq!(keys.len(), 2);
}
#[test]
fn entity_basic_fields() {
let e = Entity::new(7, 42, "NPC".to_string());
assert_eq!(e.index, 7);
assert_eq!(e.class_id, 42);
assert_eq!(e.class_name, "NPC");
assert_eq!(e.serial, 0);
assert!(e.fields.is_empty());
}
}