tf_demo_parser/demo/message/
usermessage.rs

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