Skip to main content

tf_demo_parser/demo/message/
usermessage.rs

1use bitbuffer::{BitError, BitRead, LittleEndian};
2#[cfg(feature = "write")]
3use bitbuffer::{BitWrite, BitWriteStream, Endianness};
4use serde::{Deserialize, Serialize};
5
6use crate::demo::data::MaybeUtf8String;
7use crate::demo::message::packetentities::EntityId;
8use crate::{ReadResult, Stream};
9
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11#[derive(BitRead, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
12#[cfg_attr(feature = "write", derive(BitWrite))]
13#[repr(u8)]
14#[discriminant_bits = 8]
15pub enum UserMessageType {
16    Geiger = 0,
17    Train = 1,
18    HudText = 2,
19    SayText = 3,
20    SayText2 = 4,
21    TextMsg = 5,
22    ResetHUD = 6,
23    GameTitle = 7,
24    ItemPickup = 8,
25    ShowMenu = 9,
26    Shake = 10,
27    Fade = 11,
28    VGuiMenu = 12,
29    Rumble = 13,
30    CloseCaption = 14,
31    SendAudio = 15,
32    VoiceMask = 16,
33    RequestState = 17,
34    Damage = 18,
35    HintText = 19,
36    KeyHintText = 20,
37    HudMsg = 21,
38    AmmoDenied = 22,
39    AchievementEvent = 23,
40    UpdateRadar = 24,
41    VoiceSubtitle = 25,
42    HudNotify = 26,
43    HudNotifyCustom = 27,
44    PlayerStatsUpdate = 28,
45    PlayerIgnited = 29,
46    PlayerIgnitedInv = 30,
47    HudArenaNotify = 31,
48    UpdateAchievement = 32,
49    TrainingMsg = 33,
50    TrainingObjective = 34,
51    DamageDodged = 35,
52    PlayerJarated = 36,
53    PlayerExtinguished = 37,
54    PlayerJaratedFade = 38,
55    PlayerShieldBlocked = 39,
56    BreakModel = 40,
57    CheapBreakModel = 41,
58    BreakModelPumpkin = 42,
59    BreakModelRocketDud = 43,
60    CallVoteFailed = 44,
61    VoteStart = 45,
62    VotePass = 46,
63    VoteFailed = 47,
64    VoteSetup = 48,
65    PlayerBonusPoints = 49,
66    SpawnFlyingBird = 50,
67    PlayerGodRayEffect = 51,
68    SPHapWeapEvent = 52,
69    HapDmg = 53,
70    HapPunch = 54,
71    HapSetDrag = 55,
72    HapSet = 56,
73    HapMeleeContact = 57,
74    Unknown = 255,
75}
76
77impl PartialEq<u8> for UserMessageType {
78    fn eq(&self, other: &u8) -> bool {
79        *self as u8 == *other
80    }
81}
82
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
85#[serde(bound(deserialize = "'a: 'static"))]
86#[serde(tag = "type")]
87pub enum UserMessage<'a> {
88    SayText2(Box<SayText2Message>),
89    Text(Box<TextMessage>),
90    ResetHUD(ResetHudMessage),
91    Train(TrainMessage),
92    VoiceSubtitle(VoiceSubtitleMessage),
93    Shake(ShakeMessage),
94    VGuiMenu(VGuiMenuMessage),
95    Rumble(RumbleMessage),
96    Fade(FadeMessage),
97    HapMeleeContact(HapMeleeContactMessage),
98    Unknown(UnknownUserMessage<'a>),
99}
100
101impl UserMessage<'_> {
102    pub fn message_type(&self) -> u8 {
103        match self {
104            UserMessage::SayText2(_) => UserMessageType::SayText2 as u8,
105            UserMessage::Text(_) => UserMessageType::TextMsg as u8,
106            UserMessage::ResetHUD(_) => UserMessageType::ResetHUD as u8,
107            UserMessage::Train(_) => UserMessageType::Train as u8,
108            UserMessage::VoiceSubtitle(_) => UserMessageType::VoiceSubtitle as u8,
109            UserMessage::Shake(_) => UserMessageType::Shake as u8,
110            UserMessage::VGuiMenu(_) => UserMessageType::VGuiMenu as u8,
111            UserMessage::Rumble(_) => UserMessageType::Rumble as u8,
112            UserMessage::Fade(_) => UserMessageType::Fade as u8,
113            UserMessage::HapMeleeContact(_) => UserMessageType::HapMeleeContact as u8,
114            UserMessage::Unknown(msg) => msg.raw_type,
115        }
116    }
117}
118
119impl<'a> BitRead<'a, LittleEndian> for UserMessage<'a> {
120    fn read(stream: &mut Stream<'a>) -> ReadResult<Self> {
121        let message = match stream.read() {
122            Ok(message_type) => {
123                let length = stream.read_int(11)?;
124                let mut data = stream.read_bits(length)?;
125                match message_type {
126                    UserMessageType::SayText2 => UserMessage::SayText2(data.read()?),
127                    UserMessageType::TextMsg => UserMessage::Text(data.read()?),
128                    UserMessageType::ResetHUD => UserMessage::ResetHUD(data.read()?),
129                    UserMessageType::Train => UserMessage::Train(data.read()?),
130                    UserMessageType::VoiceSubtitle => UserMessage::VoiceSubtitle(data.read()?),
131                    UserMessageType::Shake => UserMessage::Shake(data.read()?),
132                    UserMessageType::VGuiMenu => UserMessage::VGuiMenu(data.read()?),
133                    UserMessageType::Rumble => UserMessage::Rumble(data.read()?),
134                    UserMessageType::Fade => UserMessage::Fade(data.read()?),
135                    UserMessageType::HapMeleeContact => UserMessage::HapMeleeContact(data.read()?),
136                    _ => UserMessage::Unknown(UnknownUserMessage {
137                        raw_type: message_type as u8,
138                        data,
139                    }),
140                }
141            }
142            Err(BitError::UnmatchedDiscriminant { discriminant, .. }) => {
143                let length = stream.read_int(11)?;
144                let data = stream.read_bits(length)?;
145                UserMessage::Unknown(UnknownUserMessage {
146                    raw_type: discriminant as u8,
147                    data,
148                })
149            }
150            Err(e) => return Err(e),
151        };
152
153        Ok(message)
154    }
155
156    fn skip(stream: &mut Stream) -> ReadResult<()> {
157        stream.skip_bits(8)?;
158        let length: u32 = stream.read_int(11)?;
159        stream.skip_bits(length as usize)
160    }
161}
162
163#[cfg(feature = "write")]
164impl BitWrite<LittleEndian> for UserMessage<'_> {
165    fn write(&self, stream: &mut BitWriteStream<LittleEndian>) -> ReadResult<()> {
166        self.message_type().write(stream)?;
167        stream.reserve_length(11, |stream| match self {
168            UserMessage::SayText2(body) => stream.write(body),
169            UserMessage::Text(body) => stream.write(body),
170            UserMessage::ResetHUD(body) => stream.write(body),
171            UserMessage::Train(body) => stream.write(body),
172            UserMessage::VoiceSubtitle(body) => stream.write(body),
173            UserMessage::Shake(body) => stream.write(body),
174            UserMessage::VGuiMenu(body) => stream.write(body),
175            UserMessage::Rumble(body) => stream.write(body),
176            UserMessage::Fade(body) => stream.write(body),
177            UserMessage::HapMeleeContact(body) => stream.write(body),
178            UserMessage::Unknown(body) => stream.write(&body.data),
179        })?;
180
181        Ok(())
182    }
183}
184
185#[test]
186#[cfg(feature = "write")]
187fn test_user_message_roundtrip() {
188    crate::test_roundtrip_write(UserMessage::Train(TrainMessage { data: 12 }));
189    crate::test_roundtrip_write(UserMessage::SayText2(Box::new(SayText2Message {
190        client: 3u32.into(),
191        raw: 1,
192        kind: ChatMessageKind::ChatTeamDead,
193        from: Some("Old Billy Riley".into()),
194        text: "[P-REC] Stop record.".into(),
195    })));
196}
197
198#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
199#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
200pub enum ChatMessageKind {
201    #[serde(rename = "TF_Chat_All")]
202    ChatAll,
203    #[serde(rename = "TF_Chat_Team")]
204    ChatTeam,
205    #[serde(rename = "TF_Chat_AllDead")]
206    ChatAllDead,
207    #[serde(rename = "TF_Chat_Team_Dead")]
208    ChatTeamDead,
209    #[serde(rename = "TF_Chat_AllSpec")]
210    ChatAllSpec,
211    NameChange,
212    Empty,
213}
214
215impl BitRead<'_, LittleEndian> for ChatMessageKind {
216    fn read(stream: &mut Stream) -> ReadResult<Self> {
217        let raw: String = stream.read()?;
218        Ok(match raw.as_str() {
219            "TF_Chat_Team" => ChatMessageKind::ChatTeam,
220            "TF_Chat_AllDead" => ChatMessageKind::ChatAllDead,
221            "TF_Chat_Team_Dead" => ChatMessageKind::ChatTeamDead,
222            "#TF_Name_Change" => ChatMessageKind::NameChange,
223            "TF_Chat_All" => ChatMessageKind::ChatAll,
224            "TF_Chat_AllSpec" => ChatMessageKind::ChatAllSpec,
225            _ => ChatMessageKind::ChatAll,
226        })
227    }
228}
229
230#[cfg(feature = "write")]
231impl BitWrite<LittleEndian> for ChatMessageKind {
232    fn write(&self, stream: &mut BitWriteStream<LittleEndian>) -> ReadResult<()> {
233        match self {
234            ChatMessageKind::ChatAll => "TF_Chat_All",
235            ChatMessageKind::ChatTeam => "TF_Chat_Team",
236            ChatMessageKind::ChatAllDead => "TF_Chat_AllDead",
237            ChatMessageKind::ChatTeamDead => "TF_Chat_Team_Dead",
238            ChatMessageKind::ChatAllSpec => "TF_Chat_AllSpec",
239            ChatMessageKind::NameChange => "#TF_Name_Change",
240            ChatMessageKind::Empty => "",
241        }
242        .write(stream)
243    }
244}
245
246#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
248pub struct SayText2Message {
249    pub client: EntityId,
250    pub raw: u8,
251    pub kind: ChatMessageKind,
252    pub from: Option<MaybeUtf8String>,
253    pub text: MaybeUtf8String,
254}
255
256fn to_plain_text(text: &str) -> String {
257    // 1: normal, 2: old colors, 3: team, 4: location, 5 achievement, 6 custom
258    let mut text = text.replace(|c| c <= char::from(6), "");
259    // 7: 6-char hex
260    while let Some(pos) = text.chars().enumerate().find_map(|(index, c)| {
261        if c == char::from(7) {
262            Some(index)
263        } else {
264            None
265        }
266    }) {
267        text = text
268            .chars()
269            .take(pos)
270            .chain(text.chars().skip(pos + 7))
271            .collect();
272    }
273    // 9: 8-char hex
274    while let Some(pos) = text.chars().enumerate().find_map(|(index, c)| {
275        if c == char::from(9) {
276            Some(index)
277        } else {
278            None
279        }
280    }) {
281        text = text
282            .chars()
283            .take(pos)
284            .chain(text.chars().skip(pos + 9))
285            .collect();
286    }
287    text
288}
289
290impl SayText2Message {
291    pub fn plain_text(&self) -> String {
292        to_plain_text(self.text.as_ref())
293    }
294}
295
296impl BitRead<'_, LittleEndian> for SayText2Message {
297    fn read(stream: &mut Stream) -> ReadResult<Self> {
298        let client = EntityId::from(stream.read::<u8>()? as u32);
299        let raw = stream.read()?;
300        let (kind, from, text): (ChatMessageKind, Option<MaybeUtf8String>, MaybeUtf8String) = {
301            let next_byte = stream.read::<u8>()?;
302            stream.set_pos(stream.pos() - 8)?;
303            if next_byte > 0 && next_byte <= 8 {
304                // Starts with a color code (\x01-\x08) rather than a kind
305                // string like "TF_Chat_All", so this is the simplified format
306                // where the message body is just raw colored text without
307                // kind/from fields.
308                let text: MaybeUtf8String = stream.read()?;
309                (ChatMessageKind::ChatAll, None, text)
310            } else {
311                let kind = stream.read()?;
312                let from = stream.read()?;
313                let text = stream.read()?;
314
315                // ends with 2 0 bytes?
316                if stream.bits_left() >= 16 {
317                    let _: u16 = stream.read()?;
318                }
319                (kind, Some(from), text)
320            }
321        };
322
323        Ok(SayText2Message {
324            client,
325            raw,
326            kind,
327            from,
328            text,
329        })
330    }
331}
332
333#[cfg(feature = "write")]
334impl BitWrite<LittleEndian> for SayText2Message {
335    fn write(&self, stream: &mut BitWriteStream<LittleEndian>) -> ReadResult<()> {
336        (u32::from(self.client) as u8).write(stream)?;
337        self.raw.write(stream)?;
338
339        if let Some(from) = self.from.as_ref().map(|s| s.as_ref()) {
340            self.kind.write(stream)?;
341            from.write(stream)?;
342            self.text.write(stream)?;
343            0u16.write(stream)?;
344        } else {
345            self.text.write(stream)?;
346        }
347
348        Ok(())
349    }
350}
351
352#[test]
353#[cfg(feature = "write")]
354fn test_say_text2_roundtrip() {
355    crate::test_roundtrip_write(SayText2Message {
356        client: 3u32.into(),
357        raw: 1,
358        kind: ChatMessageKind::ChatTeamDead,
359        from: Some("Old Billy Riley".into()),
360        text: "[P-REC] Stop record.".into(),
361    });
362}
363
364#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
365#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
366#[cfg_attr(feature = "write", derive(BitWrite))]
367#[discriminant_bits = 8]
368pub enum HudTextLocation {
369    PrintNotify = 1,
370    PrintConsole,
371    PrintTalk,
372    PrintCenter,
373}
374
375#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
376#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
377#[cfg_attr(feature = "write", derive(BitWrite))]
378pub struct TextMessage {
379    pub location: HudTextLocation,
380    pub text: MaybeUtf8String,
381    pub substitute: [MaybeUtf8String; 4],
382}
383
384impl TextMessage {
385    pub fn plain_text(&self) -> String {
386        to_plain_text(self.text.as_ref())
387    }
388}
389
390#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
391#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
392#[cfg_attr(feature = "write", derive(BitWrite))]
393pub struct ResetHudMessage {
394    pub data: u8,
395}
396
397#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
398#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
399#[cfg_attr(feature = "write", derive(BitWrite))]
400pub struct TrainMessage {
401    pub data: u8,
402}
403
404#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
405#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
406#[cfg_attr(feature = "write", derive(BitWrite))]
407pub struct VoiceSubtitleMessage {
408    pub client: u8,
409    pub menu: u8,
410    pub item: u8,
411}
412
413#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
414#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
415#[cfg_attr(feature = "write", derive(BitWrite))]
416pub struct ShakeMessage {
417    pub command: u8,
418    pub amplitude: f32,
419    pub frequency: f32,
420    pub duration: f32,
421}
422
423#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
424#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
425pub struct VGuiMenuMessage {
426    pub name: MaybeUtf8String,
427    pub show: u8,
428    #[size_bits = 8]
429    pub data: Vec<VGuiMenuMessageData>,
430}
431
432#[cfg(feature = "write")]
433impl<E: Endianness> BitWrite<E> for VGuiMenuMessage {
434    fn write(&self, stream: &mut BitWriteStream<E>) -> ReadResult<()> {
435        self.name.write(stream)?;
436        self.show.write(stream)?;
437        (self.data.len() as u8).write(stream)?;
438        for item in &self.data {
439            item.write(stream)?;
440        }
441        Ok(())
442    }
443}
444
445#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
446#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
447#[cfg_attr(feature = "write", derive(BitWrite))]
448pub struct VGuiMenuMessageData {
449    pub key: MaybeUtf8String,
450    pub data: MaybeUtf8String,
451}
452
453#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
454#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
455#[cfg_attr(feature = "write", derive(BitWrite))]
456pub struct RumbleMessage {
457    pub waveform_index: u8,
458    pub rumble_data: u8,
459    pub rumble_flags: u8,
460}
461
462#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
463#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
464#[cfg_attr(feature = "write", derive(BitWrite))]
465pub struct FadeMessage {
466    pub duration: u16,
467    pub hold: u16,
468    pub flags: u16,
469    pub color: [u8; 4],
470}
471
472#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
473#[derive(BitRead, Debug, Clone, PartialEq, Serialize, Deserialize)]
474#[cfg_attr(feature = "write", derive(BitWrite))]
475pub struct HapMeleeContactMessage {
476    pub data: u8,
477}
478
479#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
480#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
481#[serde(bound(deserialize = "'a: 'static"))]
482pub struct UnknownUserMessage<'a> {
483    pub raw_type: u8,
484    pub data: Stream<'a>,
485}