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}