Skip to main content

source2_demo/string_table/
mod.rs

1//! String table system for managing game data.
2//!
3//! String tables are a key-value storage mechanism used by Source 2 games to
4//! store various game data like hero names, item names, modifiers, and more.
5//!
6//! # Overview
7//!
8//! Each string table has:
9//! - A name (e.g., "ActiveModifiers", "EntityNames")
10//! - Rows containing key-value pairs
11//! - Optional user data associated with each entry
12//!
13//! # Examples
14//!
15//! ## Accessing string tables
16//!
17//! ```no_run
18//! use source2_demo::prelude::*;
19//!
20//! #[derive(Default)]
21//! struct TableReader;
22//!
23//! impl Observer for TableReader {
24//!     fn interests(&self) -> Interests {
25//!         Interests::ENABLE_STRINGTAB | Interests::TRACK_STRINGTAB
26//!     }
27//!
28//!     fn on_string_table(&mut self, ctx: &Context, st: &StringTable, modified: &[i32]) -> ObserverResult {
29//!         println!("Table '{}' updated: {} rows modified", st.name(), modified.len());
30//!
31//!         // Iterate all rows
32//!         for row in st.iter() {
33//!             println!("Key: {}", row.key());
34//!         }
35//!
36//!         Ok(())
37//!     }
38//! }
39//! ```
40//!
41//! ## Finding specific string tables
42//!
43//! ```no_run
44//! use source2_demo::prelude::*;
45//!
46//! # fn example(ctx: &Context) -> anyhow::Result<()> {
47//! // Get string table by name
48//! let modifiers = ctx.string_tables().get_by_name("ActiveModifiers")?;
49//! println!("Active modifiers: {}", modifiers.iter().count());
50//!
51//! // Get by index
52//! let table = ctx.string_tables().get_by_index(0)?;
53//! # Ok(())
54//! # }
55//! ```
56
57mod container;
58mod row;
59
60pub use container::*;
61pub use row::*;
62
63use crate::entity::BaselineContainer;
64use crate::error::StringTableError;
65use crate::reader::{BitsReader, SliceReader};
66use std::cell::RefCell;
67use std::rc::Rc;
68
69/// A string table containing key-value pairs.
70///
71/// String tables store game data in a table format where each row has a key
72/// (string) and optional value (binary data). They're used for various purposes
73/// like tracking active modifiers, entity names, particle systems, etc.
74///
75/// # Usage Patterns
76///
77/// ## Accessing player data
78///
79/// ```no_run
80/// use source2_demo::prelude::*;
81/// use source2_demo::proto::CMsgPlayerInfo;
82///
83/// # fn example(ctx: &Context) -> anyhow::Result<()> {
84/// let userinfo = ctx.string_tables().get_by_name("userinfo")?;
85/// let row = userinfo.get_row_by_index(0)?;
86///
87/// if let Some(data) = row.value() {
88///     let player_info = CMsgPlayerInfo::decode(data)?;
89///     println!("Player: {}", player_info.name());
90/// }
91/// # Ok(())
92/// # }
93/// ```
94///
95/// ## Listing all entries
96///
97/// ```no_run
98/// use source2_demo::prelude::*;
99///
100/// # fn example(table: &StringTable) {
101/// for row in table.iter() {
102///     println!("Key: {}", row.key());
103///     if let Some(value) = row.value() {
104///         println!("  Value size: {} bytes", value.len());
105///     }
106/// }
107/// # }
108/// ```
109#[derive(Clone, Default)]
110pub struct StringTable {
111    pub(crate) index: i32,
112    pub(crate) name: String,
113    pub(crate) items: Vec<StringTableRow>,
114    pub(crate) user_data_fixed_size: bool,
115    pub(crate) user_data_size: i32,
116    pub(crate) flags: u32,
117    pub(crate) var_int_bit_counts: bool,
118    pub(crate) keys: RefCell<Vec<String>>,
119}
120
121impl StringTable {
122    /// Returns the table's numeric index.
123    pub fn index(&self) -> i32 {
124        self.index
125    }
126
127    /// Returns the table's name.
128    pub fn name(&self) -> &str {
129        &self.name
130    }
131
132    /// Returns an iterator over all rows in the string table.
133    ///
134    /// This allows you to inspect all key-value pairs stored in the table.
135    ///
136    /// # Examples
137    ///
138    /// ```no_run
139    /// use source2_demo::prelude::*;
140    ///
141    /// # fn example(ctx: &Context) -> anyhow::Result<()> {
142    /// let table = ctx.string_tables().get_by_name("ActiveModifiers")?;
143    ///
144    /// for row in table.iter() {
145    ///     println!("Key: {}", row.key());
146    ///     if let Some(value) = row.value() {
147    ///         println!("  Value size: {} bytes", value.len());
148    ///     }
149    /// }
150    /// # Ok(())
151    /// # }
152    /// ```
153    pub fn iter(&self) -> impl Iterator<Item = &StringTableRow> {
154        self.items.iter()
155    }
156
157    /// Gets a specific row by its index in the string table.
158    ///
159    /// Each string table is essentially a list of key-value pairs.
160    /// This retrieves the row at the specified position.
161    ///
162    /// # Arguments
163    ///
164    /// * `idx` - The row index (0-based)
165    ///
166    /// # Errors
167    ///
168    /// Returns [`StringTableError::RowNotFoundByIndex`] if the index is out of bounds.
169    ///
170    /// # Examples
171    ///
172    /// ```no_run
173    /// use source2_demo::prelude::*;
174    ///
175    /// # fn example(ctx: &Context) -> anyhow::Result<()> {
176    /// let userinfo = ctx.string_tables().get_by_name("userinfo")?;
177    ///
178    /// // Get player info at slot 0
179    /// let row = userinfo.get_row_by_index(0)?;
180    /// println!("Slot 0 key: {}", row.key());
181    /// # Ok(())
182    /// # }
183    /// ```
184    pub fn get_row_by_index(&self, idx: usize) -> Result<&StringTableRow, StringTableError> {
185        self.items
186            .get(idx)
187            .ok_or(StringTableError::RowNotFoundByIndex(
188                idx as i32,
189                self.name.clone(),
190            ))
191    }
192
193    pub(crate) fn parse(
194        &mut self,
195        baselines: &mut BaselineContainer,
196        buf: &[u8],
197        num_updates: i32,
198    ) -> Result<Vec<i32>, StringTableError> {
199        let items = &mut self.items;
200        let mut reader = SliceReader::new(buf);
201        let mut index = -1;
202        let mut delta_pos = 0;
203        let mut keys = self.keys.borrow_mut();
204
205        let mut modified = vec![];
206
207        if self.name == "decalprecache" {
208            return Ok(modified);
209        }
210
211        for _ in 0..num_updates {
212            reader.refill();
213
214            index += 1;
215            if !reader.read_bool() {
216                index += reader.read_var_u32() as i32 + 1;
217            }
218
219            let key = reader.read_bool().then(|| {
220                let delta_zero = if delta_pos > 32 { delta_pos & 31 } else { 0 };
221                let key = if reader.read_bool() {
222                    let pos = (delta_zero + reader.read_bits_unchecked(5) as usize) & 31;
223                    let size = reader.read_bits_unchecked(5) as usize;
224
225                    if delta_pos < pos || keys[pos].len() < size {
226                        reader.read_cstring()
227                    } else {
228                        let x = String::new();
229                        x + &keys[pos][..size] + &reader.read_cstring()
230                    }
231                } else {
232                    reader.read_cstring()
233                };
234                keys[delta_pos & 31].clone_from(&key);
235                delta_pos += 1;
236                key
237            });
238
239            let value = reader.read_bool().then(|| {
240                let mut is_compressed = false;
241                let bit_size = if self.user_data_fixed_size {
242                    self.user_data_size as u32
243                } else {
244                    if (self.flags & 0x1) != 0 {
245                        is_compressed = reader.read_bool();
246                    }
247                    if self.var_int_bit_counts {
248                        reader.read_ubit_var() * 8
249                    } else {
250                        reader.read_bits_unchecked(17) * 8
251                    }
252                };
253
254                let value = Rc::new(if is_compressed {
255                    let mut decoder = snap::raw::Decoder::new();
256                    decoder
257                        .decompress_vec(&reader.read_bits_as_bytes(bit_size))
258                        .unwrap()
259                } else {
260                    reader.read_bits_as_bytes(bit_size)
261                });
262
263                if self.name == "instancebaseline" {
264                    baselines.add_baseline(key.as_ref().unwrap().parse().unwrap_or(-1), value.clone());
265                }
266
267                value
268            });
269
270            if let Some(x) = items.get_mut(index as usize) {
271                if let Some(k) = key {
272                    x.key = k;
273                }
274                x.value = value;
275            } else {
276                items.push(StringTableRow::new(index, key.unwrap_or_default(), value));
277            }
278
279            modified.push(index);
280        }
281
282        Ok(modified)
283    }
284}