tf_demo_parser/demo/packet/
stringtable.rs

1use bitbuffer::{BitRead, BitWrite, BitWriteStream, LittleEndian};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5use crate::demo::data::DemoTick;
6use crate::demo::message::stringtable::StringTableMeta;
7use crate::demo::parser::Encode;
8use crate::{Parse, ParseError, ParserState, ReadResult, Result, Stream};
9use std::borrow::{Borrow, Cow};
10use std::cmp::min;
11
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[derive(BitRead, BitWrite, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
14pub struct FixedUserDataSize {
15    #[size = 12]
16    pub size: u16,
17    #[size = 4]
18    pub bits: u8,
19}
20
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[derive(Debug, Serialize, Deserialize, Clone)]
23#[serde(bound(deserialize = "'a: 'static"))]
24pub struct StringTable<'a> {
25    pub name: Cow<'a, str>,
26    pub entries: Vec<(u16, StringTableEntry<'a>)>,
27    pub max_entries: u16,
28    pub fixed_user_data_size: Option<FixedUserDataSize>,
29    pub client_entries: Option<Vec<StringTableEntry<'a>>>,
30    pub compressed: bool,
31}
32
33impl PartialEq for StringTable<'_> {
34    fn eq(&self, other: &Self) -> bool {
35        // ignore `compresses` until we encode compressed
36        self.name.eq(&other.name)
37            && (self.entries.eq(&other.entries))
38            && (self.max_entries.eq(&other.max_entries))
39            && (self.fixed_user_data_size.eq(&other.fixed_user_data_size))
40            && (self.client_entries.eq(&other.client_entries))
41    }
42}
43
44impl StringTable<'_> {
45    pub fn get_table_meta(&self) -> StringTableMeta {
46        StringTableMeta {
47            fixed_userdata_size: self.fixed_user_data_size,
48            max_entries: self.max_entries,
49        }
50    }
51}
52
53impl<'a> BitRead<'a, LittleEndian> for StringTable<'a> {
54    fn read(stream: &mut Stream<'a>) -> ReadResult<Self> {
55        let name = stream.read()?;
56        let entry_count = stream.read_int(16)?;
57        let mut entries = Vec::with_capacity(min(entry_count, 128) as usize);
58
59        for index in 0..entry_count {
60            entries.push((index, stream.read()?))
61        }
62
63        let client_entries = if stream.read_bool()? {
64            let count = stream.read_int(16)?;
65            Some(stream.read_sized(count)?)
66        } else {
67            None
68        };
69
70        Ok(StringTable {
71            name,
72            entries,
73            max_entries: entry_count,
74            fixed_user_data_size: None,
75            client_entries,
76            compressed: false,
77        })
78    }
79}
80
81impl BitWrite<LittleEndian> for StringTable<'_> {
82    fn write(&self, stream: &mut BitWriteStream<LittleEndian>) -> ReadResult<()> {
83        self.name.as_ref().write(stream)?;
84        (self.entries.len() as u16).write(stream)?;
85        for (_, entry) in self.entries.iter() {
86            entry.write(stream)?;
87        }
88
89        self.client_entries.is_some().write(stream)?;
90        if let Some(client_entries) = self.client_entries.as_ref() {
91            (client_entries.len() as u16).write(stream)?;
92            client_entries.write(stream)?;
93        }
94
95        Ok(())
96    }
97}
98
99#[test]
100fn test_string_table_roundtrip() {
101    crate::test_roundtrip_write(StringTable {
102        name: "foo".into(),
103        entries: vec![],
104        max_entries: 0,
105        fixed_user_data_size: None,
106        client_entries: None,
107        compressed: false,
108    });
109    crate::test_roundtrip_write(StringTable {
110        name: "foo".into(),
111        entries: vec![(
112            0,
113            StringTableEntry {
114                text: Some("bar".into()),
115                extra_data: None,
116            },
117        )],
118        max_entries: 1,
119        fixed_user_data_size: None,
120        client_entries: None,
121        compressed: false,
122    });
123    crate::test_roundtrip_write(StringTable {
124        name: "foo".into(),
125        entries: vec![
126            (
127                0,
128                StringTableEntry {
129                    text: Some("bar".into()),
130                    extra_data: None,
131                },
132            ),
133            (
134                1,
135                StringTableEntry {
136                    text: Some("asd".into()),
137                    extra_data: None,
138                },
139            ),
140        ],
141        max_entries: 2,
142        fixed_user_data_size: None,
143        client_entries: Some(vec![StringTableEntry {
144            text: Some("client".into()),
145            extra_data: None,
146        }]),
147        compressed: false,
148    });
149}
150
151#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
152#[derive(BitRead, BitWrite, Clone, Debug, PartialEq, Serialize, Deserialize)]
153#[endianness = "LittleEndian"]
154#[serde(bound(deserialize = "'a: 'static"))]
155pub struct ExtraData<'a> {
156    pub byte_len: u16,
157    #[size = "(byte_len as usize).saturating_mul(8)"]
158    pub data: Stream<'a>,
159}
160
161impl<'a> ExtraData<'a> {
162    pub fn new(data: Stream<'a>) -> Self {
163        let byte_len = (data.bit_len() / 8) as u16;
164        ExtraData { byte_len, data }
165    }
166
167    pub fn to_owned(&self) -> ExtraData<'static> {
168        ExtraData {
169            byte_len: self.byte_len,
170            data: self.data.to_owned(),
171        }
172    }
173}
174
175#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
176#[derive(Clone, Default, PartialEq, Serialize, Deserialize)]
177#[serde(bound(deserialize = "'a: 'static"))]
178pub struct StringTableEntry<'a> {
179    pub text: Option<Cow<'a, str>>,
180    pub extra_data: Option<ExtraData<'a>>,
181}
182
183impl StringTableEntry<'_> {
184    pub fn text(&self) -> &str {
185        self.text
186            .as_ref()
187            .map(|text| text.borrow())
188            .unwrap_or_default()
189    }
190
191    pub fn to_owned(&self) -> StringTableEntry<'static> {
192        StringTableEntry {
193            text: self.text.as_deref().map(|text| Cow::Owned(text.into())),
194            extra_data: self.extra_data.as_ref().map(|data| data.to_owned()),
195        }
196    }
197}
198
199impl<'a> BitRead<'a, LittleEndian> for StringTableEntry<'a> {
200    fn read(stream: &mut Stream<'a>) -> ReadResult<Self> {
201        Ok(StringTableEntry {
202            text: Some(stream.read()?),
203            extra_data: stream.read()?,
204        })
205    }
206}
207
208impl BitWrite<LittleEndian> for StringTableEntry<'_> {
209    fn write(&self, stream: &mut BitWriteStream<LittleEndian>) -> ReadResult<()> {
210        self.text.as_deref().unwrap_or_default().write(stream)?;
211        self.extra_data.is_some().write(stream)?;
212        if let Some(extra_data) = self.extra_data.as_ref() {
213            extra_data.write(stream)?;
214        }
215        Ok(())
216    }
217}
218
219impl fmt::Debug for StringTableEntry<'_> {
220    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
221        match &self.extra_data {
222            None => write!(f, "StringTableEntry {{ text: \"{}\" }}", self.text()),
223            Some(extra_data) => write!(
224                f,
225                "StringTableEntry{{ text: \"{}\" extra_data: {} bytes }}",
226                self.text(),
227                extra_data.byte_len
228            ),
229        }
230    }
231}
232
233#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
234#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
235#[serde(bound(deserialize = "'a: 'static"))]
236pub struct StringTablePacket<'a> {
237    pub tick: DemoTick,
238    pub tables: Vec<StringTable<'a>>,
239}
240
241impl<'a> Parse<'a> for StringTablePacket<'a> {
242    fn parse(stream: &mut Stream<'a>, _state: &ParserState) -> Result<Self> {
243        let tick = stream.read()?;
244        let length: usize = stream.read_int(32)?;
245        let mut packet_data = stream.read_bits(length.saturating_mul(8))?;
246        let count: usize = packet_data.read_int(8)?;
247        let tables = packet_data.read_sized(count)?;
248
249        if packet_data.bits_left() > 7 {
250            Err(ParseError::DataRemaining(packet_data.bits_left()))
251        } else {
252            Ok(StringTablePacket { tick, tables })
253        }
254    }
255}
256
257impl Encode for StringTablePacket<'_> {
258    fn encode(
259        &self,
260        stream: &mut BitWriteStream<LittleEndian>,
261        _state: &ParserState,
262    ) -> Result<()> {
263        self.tick.write(stream)?;
264        stream.reserve_byte_length(32, |stream| {
265            (self.tables.len() as u8).write(stream)?;
266            self.tables.write(stream)?;
267
268            Ok(())
269        })
270    }
271}
272
273#[test]
274fn test_string_table_packet_roundtrip() {
275    let state = ParserState::new(24, |_| false, false);
276    crate::test_roundtrip_encode(
277        StringTablePacket {
278            tick: 1.into(),
279            tables: vec![],
280        },
281        &state,
282    );
283    crate::test_roundtrip_encode(
284        StringTablePacket {
285            tick: 1.into(),
286            tables: vec![StringTable {
287                name: "table1".into(),
288                entries: vec![],
289                max_entries: 0,
290                fixed_user_data_size: None,
291                client_entries: None,
292                compressed: false,
293            }],
294        },
295        &state,
296    );
297    crate::test_roundtrip_encode(
298        StringTablePacket {
299            tick: 1.into(),
300            tables: vec![
301                StringTable {
302                    name: "table1".into(),
303                    entries: vec![(
304                        0,
305                        StringTableEntry {
306                            text: Some("bar".into()),
307                            extra_data: None,
308                        },
309                    )],
310                    max_entries: 1,
311                    fixed_user_data_size: None,
312                    client_entries: None,
313                    compressed: false,
314                },
315                StringTable {
316                    name: "table2".into(),
317                    entries: vec![
318                        (
319                            0,
320                            StringTableEntry {
321                                text: Some("bar".into()),
322                                extra_data: None,
323                            },
324                        ),
325                        (
326                            1,
327                            StringTableEntry {
328                                text: Some("asd".into()),
329                                extra_data: None,
330                            },
331                        ),
332                    ],
333                    max_entries: 2,
334                    fixed_user_data_size: None,
335                    client_entries: Some(vec![StringTableEntry {
336                        text: Some("client".into()),
337                        extra_data: None,
338                    }]),
339                    compressed: false,
340                },
341            ],
342        },
343        &state,
344    );
345}