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}