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}
45
46impl StringTable {
47    fn new(
48        name: &str,
49        user_data_fixed_size: bool,
50        user_data_size: i32,
51        user_data_size_bits: i32,
52        flags: i32,
53        using_varint_bitcounts: bool,
54    ) -> Self {
55        Self {
56            name: name.to_string(),
57            user_data_fixed_size,
58            user_data_size,
59            user_data_size_bits,
60            flags,
61            using_varint_bitcounts,
62            entries: Vec::new(),
63        }
64    }
65
66    /// Parse a string table update from a bit reader.
67    pub fn parse_update(&mut self, br: &mut BitReader, num_entries: i32) -> Result<()> {
68        let mut entry_index: i32 = -1;
69        let mut history: Vec<[u8; MAX_STRING_SIZE]> = vec![[0u8; MAX_STRING_SIZE]; HISTORY_SIZE];
70        let mut history_delta_index: usize = 0;
71        let mut string_buf = vec![0u8; 1024];
72        let mut user_data_buf = vec![0u8; MAX_USERDATA_SIZE];
73        let mut user_data_uncompressed_buf = vec![0u8; MAX_USERDATA_SIZE];
74
75        for _ in 0..num_entries as usize {
76            // Read index
77            entry_index = if br.read_bool()? {
78                entry_index + 1
79            } else {
80                br.read_uvarint32()? as i32 + 1
81            };
82
83            // Read string
84            let has_string = br.read_bool()?;
85            let string = if has_string {
86                let mut size: usize = 0;
87
88                if br.read_bool()? {
89                    // Uses history reference
90                    let mut history_delta_zero = 0;
91                    if history_delta_index > HISTORY_SIZE {
92                        history_delta_zero = history_delta_index & HISTORY_BITMASK;
93                    }
94
95                    let index = (history_delta_zero + br.read_bits(5)? as usize) & HISTORY_BITMASK;
96                    let bytes_to_copy = br.read_bits(MAX_STRING_BITS)? as usize;
97                    size += bytes_to_copy;
98
99                    string_buf[..bytes_to_copy].copy_from_slice(&history[index][..bytes_to_copy]);
100                    size += br.read_string_into(&mut string_buf[bytes_to_copy..])?;
101                } else {
102                    size += br.read_string_into(&mut string_buf)?;
103                }
104
105                // Update history
106                let mut she = [0u8; MAX_STRING_SIZE];
107                let copy_len = size.min(MAX_STRING_SIZE);
108                she[..copy_len].copy_from_slice(&string_buf[..copy_len]);
109                history[history_delta_index & HISTORY_BITMASK] = she;
110                history_delta_index += 1;
111
112                Some(String::from_utf8_lossy(&string_buf[..size]).into_owned())
113            } else {
114                None
115            };
116
117            // Read user data
118            let has_user_data = br.read_bool()?;
119            let user_data = if has_user_data {
120                if self.user_data_fixed_size {
121                    br.read_bits_to_bytes(&mut user_data_buf, self.user_data_size_bits as usize)?;
122                    Some(user_data_buf[..self.user_data_size as usize].to_vec())
123                } else {
124                    let mut is_compressed = false;
125                    if (self.flags & 0x1) != 0 {
126                        is_compressed = br.read_bool()?;
127                    }
128
129                    let size = if self.using_varint_bitcounts {
130                        br.read_ubitvar()? as usize
131                    } else {
132                        br.read_bits(MAX_USERDATA_BITS)? as usize
133                    };
134
135                    br.read_bytes(&mut user_data_buf[..size])?;
136
137                    if is_compressed {
138                        let decomp_len = snap::raw::decompress_len(&user_data_buf[..size])
139                            .map_err(|e| Error::Decompress(e.to_string()))?;
140                        user_data_uncompressed_buf.resize(decomp_len, 0);
141                        snap::raw::Decoder::new()
142                            .decompress(&user_data_buf[..size], &mut user_data_uncompressed_buf)
143                            .map_err(|e| Error::Decompress(e.to_string()))?;
144                        Some(user_data_uncompressed_buf[..decomp_len].to_vec())
145                    } else {
146                        Some(user_data_buf[..size].to_vec())
147                    }
148                }
149            } else {
150                None
151            };
152
153            // Insert or update
154            let idx = entry_index as usize;
155            if idx < self.entries.len() {
156                if let Some(ud) = user_data {
157                    self.entries[idx].user_data = Some(ud);
158                }
159                if let Some(s) = string {
160                    self.entries[idx].string = Some(s);
161                }
162            } else {
163                // Extend entries to reach idx
164                while self.entries.len() < idx {
165                    self.entries.push(StringTableEntry {
166                        string: None,
167                        user_data: None,
168                    });
169                }
170                self.entries.push(StringTableEntry { string, user_data });
171            }
172        }
173
174        Ok(())
175    }
176}
177
178/// Container for all string tables.
179#[derive(Default)]
180pub struct StringTableContainer {
181    tables: Vec<StringTable>,
182    /// Cached instance baselines: class_id -> baseline data.
183    pub instance_baselines: HashMap<i32, Vec<u8>>,
184}
185
186impl StringTableContainer {
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    /// Handle CSVCMsg_CreateStringTable. Returns `true` if the created table
192    /// is `instancebaseline` (caller should refresh baselines).
193    pub fn handle_create(&mut self, msg: CsvcMsgCreateStringTable) -> Result<bool> {
194        let name = msg.name.as_deref().unwrap_or("");
195        let is_baseline = name == INSTANCE_BASELINE_TABLE_NAME;
196        let mut table = StringTable::new(
197            name,
198            msg.user_data_fixed_size.unwrap_or(false),
199            msg.user_data_size.unwrap_or(0),
200            msg.user_data_size_bits.unwrap_or(0),
201            msg.flags.unwrap_or(0),
202            msg.using_varint_bitcounts.unwrap_or(false),
203        );
204
205        let string_data = if msg.data_compressed.unwrap_or(false) {
206            let sd = msg.string_data.as_deref().unwrap_or(&[]);
207            let decomp_len =
208                snap::raw::decompress_len(sd).map_err(|e| Error::Decompress(e.to_string()))?;
209            let mut buf = vec![0u8; decomp_len];
210            snap::raw::Decoder::new()
211                .decompress(sd, &mut buf)
212                .map_err(|e| Error::Decompress(e.to_string()))?;
213            buf
214        } else {
215            msg.string_data.unwrap_or_default()
216        };
217
218        let mut br = BitReader::new(&string_data);
219        table.parse_update(&mut br, msg.num_entries.unwrap_or(0))?;
220
221        self.tables.push(table);
222        Ok(is_baseline)
223    }
224
225    /// Handle CSVCMsg_UpdateStringTable. Returns `true` if the updated table
226    /// is `instancebaseline` (caller should refresh baselines).
227    pub fn handle_update(&mut self, msg: CsvcMsgUpdateStringTable) -> Result<bool> {
228        let table_id = msg.table_id.unwrap_or(0) as usize;
229        if table_id >= self.tables.len() {
230            return Err(Error::Parse {
231                context: format!("string table update for non-existent table {}", table_id),
232            });
233        }
234
235        let is_baseline = self.tables[table_id].name == INSTANCE_BASELINE_TABLE_NAME;
236
237        let string_data = msg.string_data.unwrap_or_default();
238        let mut br = BitReader::new(&string_data);
239        self.tables[table_id].parse_update(&mut br, msg.num_changed_entries.unwrap_or(0))?;
240
241        Ok(is_baseline)
242    }
243
244    /// Do a full update from CDemoStringTables (used in full packets).
245    pub fn do_full_update(&mut self, cmd: CDemoStringTables) {
246        for incoming in &cmd.tables {
247            let table_name = incoming.table_name.as_deref().unwrap_or("");
248            if let Some(table) = self.tables.iter_mut().find(|t| t.name == table_name) {
249                for (i, item) in incoming.items.iter().enumerate() {
250                    let entry = StringTableEntry {
251                        string: item.str.clone(),
252                        user_data: item.data.clone(),
253                    };
254                    if i < table.entries.len() {
255                        if entry.user_data.is_some() {
256                            table.entries[i].user_data = entry.user_data;
257                        }
258                    } else {
259                        while table.entries.len() < i {
260                            table.entries.push(StringTableEntry {
261                                string: None,
262                                user_data: None,
263                            });
264                        }
265                        table.entries.push(entry);
266                    }
267                }
268            }
269        }
270    }
271
272    /// Update instance baselines from the instancebaseline string table.
273    pub fn update_instance_baselines(&mut self, _class_info: &ClassInfo) {
274        if let Some(table) = self
275            .tables
276            .iter()
277            .find(|t| t.name == INSTANCE_BASELINE_TABLE_NAME)
278        {
279            for entry in &table.entries {
280                if let (Some(s), Some(data)) = (&entry.string, &entry.user_data)
281                    && let Ok(class_id) = s.parse::<i32>()
282                {
283                    // Only clone if new or changed
284                    if self.instance_baselines.get(&class_id) != Some(data) {
285                        self.instance_baselines.insert(class_id, data.clone());
286                    }
287                }
288            }
289        }
290    }
291
292    /// Look up a string table by name.
293    pub fn find_table(&self, name: &str) -> Option<&StringTable> {
294        self.tables.iter().find(|t| t.name == name)
295    }
296
297    /// Returns a slice of all string tables.
298    pub fn tables(&self) -> &[StringTable] {
299        &self.tables
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn container_new_is_empty() {
309        let c = StringTableContainer::new();
310        assert!(c.tables().is_empty());
311        assert!(c.instance_baselines.is_empty());
312    }
313
314    #[test]
315    fn find_table_missing() {
316        let c = StringTableContainer::new();
317        assert!(c.find_table("nonexistent").is_none());
318    }
319
320    #[test]
321    fn update_instance_baselines_on_empty_is_noop() {
322        let mut c = StringTableContainer::new();
323        let ci = ClassInfo::empty();
324        c.update_instance_baselines(&ci);
325        assert!(c.instance_baselines.is_empty());
326    }
327
328    #[test]
329    fn handle_update_invalid_table_id() {
330        let mut c = StringTableContainer::new();
331        let msg = CsvcMsgUpdateStringTable {
332            table_id: Some(99),
333            num_changed_entries: Some(0),
334            string_data: None,
335        };
336        let result = c.handle_update(msg);
337        assert!(result.is_err());
338    }
339
340    #[test]
341    fn handle_create_empty_uncompressed() {
342        let mut c = StringTableContainer::new();
343        let msg = CsvcMsgCreateStringTable {
344            name: Some("test".to_string()),
345            num_entries: Some(0),
346            user_data_fixed_size: Some(false),
347            user_data_size: Some(0),
348            user_data_size_bits: Some(0),
349            flags: Some(0),
350            string_data: Some(vec![]),
351            data_compressed: Some(false),
352            using_varint_bitcounts: Some(false),
353            ..Default::default()
354        };
355        c.handle_create(msg).unwrap();
356        assert_eq!(c.tables().len(), 1);
357        assert!(c.find_table("test").is_some());
358    }
359}