Skip to main content

boon/entity/
string_tables.rs

1use std::collections::HashMap;
2
3use crate::error::{Error, Result};
4use crate::io::BitReader;
5
6use super::class_info::ClassInfo;
7
8use boon_proto::proto::{CDemoStringTables, CsvcMsgCreateStringTable, CsvcMsgUpdateStringTable};
9
10// String table delta encoding uses a circular buffer of recently-seen
11// strings. New strings can reference a history entry by index and copy a
12// prefix, then append the remainder. These constants control the buffer.
13const HISTORY_SIZE: usize = 32;
14const HISTORY_BITMASK: usize = HISTORY_SIZE - 1;
15
16/// Maximum length (in characters) of a string table key.
17const MAX_STRING_BITS: usize = 5;
18const MAX_STRING_SIZE: usize = 1 << MAX_STRING_BITS;
19
20/// Maximum size (in bytes) of per-entry user data.
21const MAX_USERDATA_BITS: usize = 17;
22const MAX_USERDATA_SIZE: usize = 1 << MAX_USERDATA_BITS;
23
24/// The `instancebaseline` table stores default field values for each entity class.
25pub const INSTANCE_BASELINE_TABLE_NAME: &str = "instancebaseline";
26
27/// A single entry in a string table.
28#[derive(Debug, Clone)]
29pub struct StringTableEntry {
30    pub string: Option<String>,
31    pub user_data: Option<Vec<u8>>,
32}
33
34/// A string table.
35#[derive(Debug)]
36pub struct StringTable {
37    pub name: String,
38    user_data_fixed_size: bool,
39    user_data_size: i32,
40    user_data_size_bits: i32,
41    flags: i32,
42    using_varint_bitcounts: bool,
43    pub entries: Vec<StringTableEntry>,
44    /// Entry indices written since the last [`StringTableContainer::clear_dirty`]
45    /// (i.e. since the previous per-tick callback). Lets consumers decode only
46    /// what changed this tick instead of rescanning the whole table — see
47    /// [`StringTable::dirty_indices`]. May contain duplicates if an index is
48    /// touched more than once between callbacks.
49    dirty: Vec<usize>,
50}
51
52impl StringTable {
53    fn new(
54        name: &str,
55        user_data_fixed_size: bool,
56        user_data_size: i32,
57        user_data_size_bits: i32,
58        flags: i32,
59        using_varint_bitcounts: bool,
60    ) -> Self {
61        Self {
62            name: name.to_string(),
63            user_data_fixed_size,
64            user_data_size,
65            user_data_size_bits,
66            flags,
67            using_varint_bitcounts,
68            entries: Vec::new(),
69            dirty: Vec::new(),
70        }
71    }
72
73    /// Entry indices written since the last per-tick callback.
74    ///
75    /// A consumer that maintains its own per-index state can react to only
76    /// these entries rather than iterating (and decoding) every entry every
77    /// tick. The list is cleared by the parser after each callback via
78    /// [`StringTableContainer::clear_dirty`]. Indices may repeat.
79    pub fn dirty_indices(&self) -> &[usize] {
80        &self.dirty
81    }
82
83    /// Parse a string table update from a bit reader.
84    pub fn parse_update(&mut self, br: &mut BitReader, num_entries: i32) -> Result<()> {
85        let mut entry_index: i32 = -1;
86        let mut history: Vec<[u8; MAX_STRING_SIZE]> = vec![[0u8; MAX_STRING_SIZE]; HISTORY_SIZE];
87        let mut history_delta_index: usize = 0;
88        let mut string_buf = vec![0u8; 1024];
89        let mut user_data_buf = vec![0u8; MAX_USERDATA_SIZE];
90        let mut user_data_uncompressed_buf = vec![0u8; MAX_USERDATA_SIZE];
91
92        for _ in 0..num_entries as usize {
93            // Read index
94            entry_index = if br.read_bool()? {
95                entry_index + 1
96            } else {
97                br.read_uvarint32()? as i32 + 1
98            };
99
100            // Read string
101            let has_string = br.read_bool()?;
102            let string = if has_string {
103                let mut size: usize = 0;
104
105                if br.read_bool()? {
106                    // Uses history reference
107                    let mut history_delta_zero = 0;
108                    if history_delta_index > HISTORY_SIZE {
109                        history_delta_zero = history_delta_index & HISTORY_BITMASK;
110                    }
111
112                    let index = (history_delta_zero + br.read_bits(5)? as usize) & HISTORY_BITMASK;
113                    let bytes_to_copy = br.read_bits(MAX_STRING_BITS)? as usize;
114                    size += bytes_to_copy;
115
116                    string_buf[..bytes_to_copy].copy_from_slice(&history[index][..bytes_to_copy]);
117                    size += br.read_string_into(&mut string_buf[bytes_to_copy..])?;
118                } else {
119                    size += br.read_string_into(&mut string_buf)?;
120                }
121
122                // Update history
123                let mut she = [0u8; MAX_STRING_SIZE];
124                let copy_len = size.min(MAX_STRING_SIZE);
125                she[..copy_len].copy_from_slice(&string_buf[..copy_len]);
126                history[history_delta_index & HISTORY_BITMASK] = she;
127                history_delta_index += 1;
128
129                Some(String::from_utf8_lossy(&string_buf[..size]).into_owned())
130            } else {
131                None
132            };
133
134            // Read user data
135            let has_user_data = br.read_bool()?;
136            let user_data = if has_user_data {
137                if self.user_data_fixed_size {
138                    let size = self.user_data_size as usize;
139                    let size_bits = self.user_data_size_bits as usize;
140                    if size > user_data_buf.len() || size_bits.div_ceil(8) > user_data_buf.len() {
141                        return Err(Error::Parse {
142                            context: format!(
143                                "string-table fixed user_data size ({size} bytes / {size_bits} bits) exceeds {} bytes",
144                                user_data_buf.len()
145                            ),
146                        });
147                    }
148                    br.read_bits_to_bytes(&mut user_data_buf, size_bits)?;
149                    Some(user_data_buf[..size].to_vec())
150                } else {
151                    let mut is_compressed = false;
152                    if (self.flags & 0x1) != 0 {
153                        is_compressed = br.read_bool()?;
154                    }
155
156                    let size = if self.using_varint_bitcounts {
157                        br.read_ubitvar()? as usize
158                    } else {
159                        br.read_bits(MAX_USERDATA_BITS)? as usize
160                    };
161                    if size > user_data_buf.len() {
162                        return Err(Error::Parse {
163                            context: format!(
164                                "string-table user_data size {size} exceeds {} bytes",
165                                user_data_buf.len()
166                            ),
167                        });
168                    }
169
170                    br.read_bytes(&mut user_data_buf[..size])?;
171
172                    if is_compressed {
173                        let decomp_len = snap::raw::decompress_len(&user_data_buf[..size])
174                            .map_err(|e| Error::Decompress(e.to_string()))?;
175                        user_data_uncompressed_buf.resize(decomp_len, 0);
176                        snap::raw::Decoder::new()
177                            .decompress(&user_data_buf[..size], &mut user_data_uncompressed_buf)
178                            .map_err(|e| Error::Decompress(e.to_string()))?;
179                        Some(user_data_uncompressed_buf[..decomp_len].to_vec())
180                    } else {
181                        Some(user_data_buf[..size].to_vec())
182                    }
183                }
184            } else {
185                None
186            };
187
188            // Insert or update
189            let idx = entry_index as usize;
190            if idx < self.entries.len() {
191                if let Some(ud) = user_data {
192                    self.entries[idx].user_data = Some(ud);
193                }
194                if let Some(s) = string {
195                    self.entries[idx].string = Some(s);
196                }
197            } else {
198                // Extend entries to reach idx
199                while self.entries.len() < idx {
200                    self.entries.push(StringTableEntry {
201                        string: None,
202                        user_data: None,
203                    });
204                }
205                self.entries.push(StringTableEntry { string, user_data });
206            }
207            self.dirty.push(idx);
208        }
209
210        Ok(())
211    }
212}
213
214/// Container for all string tables.
215#[derive(Default)]
216pub struct StringTableContainer {
217    tables: Vec<StringTable>,
218    /// Cached instance baselines: class_id -> baseline data.
219    pub instance_baselines: HashMap<i32, Vec<u8>>,
220}
221
222impl StringTableContainer {
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    /// Handle CSVCMsg_CreateStringTable. Returns `true` if the created table
228    /// is `instancebaseline` (caller should refresh baselines).
229    pub fn handle_create(&mut self, msg: CsvcMsgCreateStringTable) -> Result<bool> {
230        let name = msg.name.as_deref().unwrap_or("");
231        let is_baseline = name == INSTANCE_BASELINE_TABLE_NAME;
232        let mut table = StringTable::new(
233            name,
234            msg.user_data_fixed_size.unwrap_or(false),
235            msg.user_data_size.unwrap_or(0),
236            msg.user_data_size_bits.unwrap_or(0),
237            msg.flags.unwrap_or(0),
238            msg.using_varint_bitcounts.unwrap_or(false),
239        );
240
241        let string_data = if msg.data_compressed.unwrap_or(false) {
242            let sd = msg.string_data.as_deref().unwrap_or(&[]);
243            let decomp_len =
244                snap::raw::decompress_len(sd).map_err(|e| Error::Decompress(e.to_string()))?;
245            let mut buf = vec![0u8; decomp_len];
246            snap::raw::Decoder::new()
247                .decompress(sd, &mut buf)
248                .map_err(|e| Error::Decompress(e.to_string()))?;
249            buf
250        } else {
251            msg.string_data.unwrap_or_default()
252        };
253
254        let mut br = BitReader::new(&string_data);
255        table.parse_update(&mut br, msg.num_entries.unwrap_or(0))?;
256
257        self.tables.push(table);
258        Ok(is_baseline)
259    }
260
261    /// Handle CSVCMsg_UpdateStringTable. Returns `true` if the updated table
262    /// is `instancebaseline` (caller should refresh baselines).
263    pub fn handle_update(&mut self, msg: CsvcMsgUpdateStringTable) -> Result<bool> {
264        let table_id = msg.table_id.unwrap_or(0) as usize;
265        if table_id >= self.tables.len() {
266            return Err(Error::Parse {
267                context: format!("string table update for non-existent table {}", table_id),
268            });
269        }
270
271        let is_baseline = self.tables[table_id].name == INSTANCE_BASELINE_TABLE_NAME;
272
273        let string_data = msg.string_data.unwrap_or_default();
274        let mut br = BitReader::new(&string_data);
275        self.tables[table_id].parse_update(&mut br, msg.num_changed_entries.unwrap_or(0))?;
276
277        Ok(is_baseline)
278    }
279
280    /// Do a full update from CDemoStringTables (used in full packets).
281    pub fn do_full_update(&mut self, cmd: CDemoStringTables) {
282        for incoming in &cmd.tables {
283            let table_name = incoming.table_name.as_deref().unwrap_or("");
284            if let Some(table) = self.tables.iter_mut().find(|t| t.name == table_name) {
285                for (i, item) in incoming.items.iter().enumerate() {
286                    let entry = StringTableEntry {
287                        string: item.str.clone(),
288                        user_data: item.data.clone(),
289                    };
290                    if i < table.entries.len() {
291                        if entry.user_data.is_some() {
292                            table.entries[i].user_data = entry.user_data;
293                            table.dirty.push(i);
294                        }
295                    } else {
296                        while table.entries.len() < i {
297                            table.entries.push(StringTableEntry {
298                                string: None,
299                                user_data: None,
300                            });
301                        }
302                        table.entries.push(entry);
303                        table.dirty.push(i);
304                    }
305                }
306            }
307        }
308    }
309
310    /// Clear every table's dirty-index list.
311    ///
312    /// The parser calls this after each per-tick callback so that
313    /// [`StringTable::dirty_indices`] reflects only the entries written since
314    /// the previous callback.
315    pub fn clear_dirty(&mut self) {
316        for table in &mut self.tables {
317            table.dirty.clear();
318        }
319    }
320
321    /// Update instance baselines from the instancebaseline string table.
322    pub fn update_instance_baselines(&mut self, _class_info: &ClassInfo) {
323        if let Some(table) = self
324            .tables
325            .iter()
326            .find(|t| t.name == INSTANCE_BASELINE_TABLE_NAME)
327        {
328            for entry in &table.entries {
329                if let (Some(s), Some(data)) = (&entry.string, &entry.user_data)
330                    && let Ok(class_id) = s.parse::<i32>()
331                {
332                    // Only clone if new or changed
333                    if self.instance_baselines.get(&class_id) != Some(data) {
334                        self.instance_baselines.insert(class_id, data.clone());
335                    }
336                }
337            }
338        }
339    }
340
341    /// Look up a string table by name.
342    pub fn find_table(&self, name: &str) -> Option<&StringTable> {
343        self.tables.iter().find(|t| t.name == name)
344    }
345
346    /// Returns a slice of all string tables.
347    pub fn tables(&self) -> &[StringTable] {
348        &self.tables
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    #[test]
357    fn container_new_is_empty() {
358        let c = StringTableContainer::new();
359        assert!(c.tables().is_empty());
360        assert!(c.instance_baselines.is_empty());
361    }
362
363    #[test]
364    fn find_table_missing() {
365        let c = StringTableContainer::new();
366        assert!(c.find_table("nonexistent").is_none());
367    }
368
369    #[test]
370    fn update_instance_baselines_on_empty_is_noop() {
371        let mut c = StringTableContainer::new();
372        let ci = ClassInfo::empty();
373        c.update_instance_baselines(&ci);
374        assert!(c.instance_baselines.is_empty());
375    }
376
377    #[test]
378    fn handle_update_invalid_table_id() {
379        let mut c = StringTableContainer::new();
380        let msg = CsvcMsgUpdateStringTable {
381            table_id: Some(99),
382            num_changed_entries: Some(0),
383            string_data: None,
384        };
385        let result = c.handle_update(msg);
386        assert!(result.is_err());
387    }
388
389    #[test]
390    fn handle_create_empty_uncompressed() {
391        let mut c = StringTableContainer::new();
392        let msg = CsvcMsgCreateStringTable {
393            name: Some("test".to_string()),
394            num_entries: Some(0),
395            user_data_fixed_size: Some(false),
396            user_data_size: Some(0),
397            user_data_size_bits: Some(0),
398            flags: Some(0),
399            string_data: Some(vec![]),
400            data_compressed: Some(false),
401            using_varint_bitcounts: Some(false),
402            ..Default::default()
403        };
404        c.handle_create(msg).unwrap();
405        assert_eq!(c.tables().len(), 1);
406        assert!(c.find_table("test").is_some());
407    }
408}