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 let mut text = text.replace(|c| c <= char::from(6), "");
253 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 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 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}