use super::UserMessage;
use crate::bitbuf::BitWriter;
use crate::math::{Color32, QAngle};
use crate::net::EncodeError;
use std::ffi::CStr;
const FADE_FRACTION_BITS: u32 = 9;
fn fade_time(seconds: f32) -> u16 {
(seconds * (1 << FADE_FRACTION_BITS) as f32).clamp(0.0, u16::MAX.into()) as u16
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Fade {
pub duration: f32,
pub hold: f32,
pub flags: FadeFlags,
pub color: Color32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct FadeFlags(pub u16);
impl FadeFlags {
pub const IN: Self = Self(0x1);
pub const OUT: Self = Self(0x2);
pub const MODULATE: Self = Self(0x4);
pub const STAY_OUT: Self = Self(0x8);
pub const PURGE: Self = Self(0x10);
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
}
impl UserMessage for Fade {
fn name(&self) -> &CStr {
c"Fade"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
out.write_u16(fade_time(self.duration));
out.write_u16(fade_time(self.hold));
out.write_u16(self.flags.0);
out.write_u8(self.color.r);
out.write_u8(self.color.g);
out.write_u8(self.color.b);
out.write_u8(self.color.a);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum ShakeCommand {
Start = 0,
Stop = 1,
Amplitude = 2,
Frequency = 3,
StartRumbleOnly = 4,
StartNoRumble = 5,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Shake {
pub command: ShakeCommand,
pub amplitude: f32,
pub frequency: f32,
pub duration: f32,
}
impl UserMessage for Shake {
fn name(&self) -> &CStr {
c"Shake"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
out.write_u8(self.command as u8);
out.write_f32(self.amplitude);
out.write_f32(self.frequency);
out.write_f32(self.duration);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum HudTextEffect {
#[default]
Fade = 0,
Flicker = 1,
ScanOut = 2,
}
#[doc(alias = "HudMsg")]
#[doc(alias = "game_text")]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HudText<'a> {
pub channel: u8,
pub x: f32,
pub y: f32,
pub color: Color32,
pub effect_color: Color32,
pub effect: HudTextEffect,
pub fade_in: f32,
pub fade_out: f32,
pub hold: f32,
pub effect_time: f32,
pub text: &'a CStr,
}
impl UserMessage for HudText<'_> {
fn name(&self) -> &CStr {
c"HudMsg"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
out.write_u8(self.channel);
out.write_f32(self.x);
out.write_f32(self.y);
for color in [self.color, self.effect_color] {
out.write_u8(color.r);
out.write_u8(color.g);
out.write_u8(color.b);
out.write_u8(color.a);
}
out.write_u8(self.effect as u8);
out.write_f32(self.fade_in);
out.write_f32(self.fade_out);
out.write_f32(self.hold);
out.write_f32(self.effect_time);
out.write_cstr(self.text);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum TextDestination {
Notify = 1,
Console = 2,
Chat = 3,
Center = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TextMsg<'a> {
pub destination: TextDestination,
pub message: &'a CStr,
pub arguments: [&'a CStr; 4],
}
impl UserMessage for TextMsg<'_> {
fn name(&self) -> &CStr {
c"TextMsg"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
out.write_u8(self.destination as u8);
out.write_cstr(self.message);
for argument in self.arguments {
out.write_cstr(argument);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SayText2<'a> {
pub speaker: u8,
pub chat: bool,
pub message: &'a CStr,
pub arguments: [&'a CStr; 4],
}
impl UserMessage for SayText2<'_> {
fn name(&self) -> &CStr {
c"SayText2"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
out.write_u8(self.speaker);
out.write_u8(self.chat.into());
out.write_cstr(self.message);
for argument in self.arguments {
out.write_cstr(argument);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HintText<'a> {
pub text: &'a CStr,
}
impl UserMessage for HintText<'_> {
fn name(&self) -> &CStr {
c"HintText"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
out.write_cstr(self.text);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyHintText<'a> {
pub text: &'a CStr,
}
impl UserMessage for KeyHintText<'_> {
fn name(&self) -> &CStr {
c"KeyHintText"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
out.write_u8(1);
out.write_cstr(self.text);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HudNotification<'a> {
pub text: &'a CStr,
pub icon: &'a CStr,
pub team: u8,
}
impl UserMessage for HudNotification<'_> {
fn name(&self) -> &CStr {
c"HudNotifyCustom"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
out.write_cstr(self.text);
out.write_cstr(self.icon);
out.write_u8(self.team);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ForcePlayerViewAngles {
pub player: u8,
pub angles: QAngle,
}
impl UserMessage for ForcePlayerViewAngles {
fn name(&self) -> &CStr {
c"ForcePlayerViewAngles"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
out.write_u8(1);
out.write_u8(self.player);
out.write_bit_angles(self.angles);
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VguiMenu<'a> {
pub name: &'a CStr,
pub show: bool,
pub keys: &'a [(&'a CStr, &'a CStr)],
}
impl UserMessage for VguiMenu<'_> {
fn name(&self) -> &CStr {
c"VGUIMenu"
}
fn write(&self, out: &mut BitWriter) -> Result<(), EncodeError> {
let count = u8::try_from(self.keys.len()).map_err(|_| EncodeError::OutOfRange {
field: "key count",
value: self.keys.len() as u64,
max: u8::MAX.into(),
})?;
out.write_cstr(self.name);
out.write_u8(self.show.into());
out.write_u8(count);
for &(key, value) in self.keys {
out.write_cstr(key);
out.write_cstr(value);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn payload(message: &impl UserMessage) -> BitWriter {
let mut out = BitWriter::new();
message.write(&mut out).unwrap();
out
}
#[test]
fn fixed_size_messages_have_their_registered_sizes() {
let fade = Fade {
duration: 1.5,
hold: 100.0,
flags: FadeFlags::OUT.union(FadeFlags::STAY_OUT),
color: Color32::rgb(0, 0, 0),
};
let bits = payload(&fade);
let mut reader = bits.reader();
assert_eq!(bits.byte_len(), 10);
assert_eq!(reader.read_u16(), Ok(768));
assert_eq!(reader.read_u16(), Ok(51200));
assert_eq!(reader.read_u16(), Ok(0xA));
let shake = Shake {
command: ShakeCommand::Start,
amplitude: 10.0,
frequency: 150.0,
duration: 2.0,
};
assert_eq!(payload(&shake).byte_len(), 13);
}
#[test]
fn fade_times_clamp_to_their_range() {
assert_eq!(fade_time(-1.0), 0);
assert_eq!(fade_time(1000.0), u16::MAX);
assert_eq!(fade_time(f32::NAN), 0);
}
#[test]
fn text_messages_carry_every_argument() {
let message = TextMsg {
destination: TextDestination::Center,
message: c"Run.",
arguments: [c"", c"", c"", c""],
};
let bits = payload(&message);
let mut reader = bits.reader();
assert_eq!(reader.read_u8(), Ok(4));
assert_eq!(reader.read_cstring().as_deref(), Ok(c"Run."));
for _ in 0..4 {
assert_eq!(reader.read_cstring().as_deref(), Ok(c""));
}
assert_eq!(reader.remaining(), 0);
}
#[test]
fn view_angles_are_coordinates() {
let angles = QAngle {
pitch: 10.0,
yaw: -90.0,
roll: 0.0,
};
let bits = payload(&ForcePlayerViewAngles { player: 5, angles });
let mut reader = bits.reader();
assert_eq!(reader.read_u8(), Ok(1));
assert_eq!(reader.read_u8(), Ok(5));
assert_eq!(
reader.read_bit_vec3_coord(),
Ok(crate::math::Vector::new(10.0, -90.0, 0.0))
);
}
}