boon/entity/
string_tables.rs1use 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
10const HISTORY_SIZE: usize = 32;
14const HISTORY_BITMASK: usize = HISTORY_SIZE - 1;
15
16const MAX_STRING_BITS: usize = 5;
18const MAX_STRING_SIZE: usize = 1 << MAX_STRING_BITS;
19
20const MAX_USERDATA_BITS: usize = 17;
22const MAX_USERDATA_SIZE: usize = 1 << MAX_USERDATA_BITS;
23
24pub const INSTANCE_BASELINE_TABLE_NAME: &str = "instancebaseline";
26
27#[derive(Debug, Clone)]
29pub struct StringTableEntry {
30 pub string: Option<String>,
31 pub user_data: Option<Vec<u8>>,
32}
33
34#[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 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 pub fn dirty_indices(&self) -> &[usize] {
80 &self.dirty
81 }
82
83 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 entry_index = if br.read_bool()? {
95 entry_index + 1
96 } else {
97 br.read_uvarint32()? as i32 + 1
98 };
99
100 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 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 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 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 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 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#[derive(Default)]
216pub struct StringTableContainer {
217 tables: Vec<StringTable>,
218 pub instance_baselines: HashMap<i32, Vec<u8>>,
220}
221
222impl StringTableContainer {
223 pub fn new() -> Self {
224 Self::default()
225 }
226
227 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 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 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 pub fn clear_dirty(&mut self) {
316 for table in &mut self.tables {
317 table.dirty.clear();
318 }
319 }
320
321 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 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 pub fn find_table(&self, name: &str) -> Option<&StringTable> {
343 self.tables.iter().find(|t| t.name == name)
344 }
345
346 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}