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