#[cfg(test)]
#[path = "../tests/tf2/overlays.rs"]
mod tests;
use crate::entities::Entity;
use crate::tf2::script_binding::{self as binding, BindingError};
use crate::{Game, Server};
use std::borrow::Cow;
use std::ffi::{CStr, CString};
const _: () = assert!(MAX_MATERIAL_LEN == sdk_raw::tier0::MAX_PATH - 1);
const EXTENSION: &[u8; 4] = b".vmt";
const MATERIALS_DIRECTORY: &[u8; 10] = b"materials/";
pub const MAX_MATERIAL_LEN: usize =
size_of::<sys::CPlayerLocalData_NetworkVar_m_szScriptOverlayMaterial>() - 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum MaterialNameError {
#[error("the material name is empty")]
Empty,
#[error("the material name is {len} bytes long, but at most {MAX_MATERIAL_LEN} fit")]
TooLong {
len: usize,
},
#[error("the material name contains the byte {byte:#04x} at index {index}")]
InvalidByte {
index: usize,
byte: u8,
},
#[error("the material name starts with `materials/`, which names are relative to")]
MaterialsDirectory,
#[error("the material name ends with `.vmt`, which names omit")]
Extension,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum OverlayError {
#[error("screen overlays require Team Fortress 2")]
UnsupportedGame,
#[error("the entity is not a player")]
NotAPlayer,
#[error("the player is marked for deletion")]
MarkedForDeletion,
#[error("the game does not expose the expected native overlay method")]
UnsupportedMethod,
#[error("the native overlay method rejected the call")]
Rejected,
}
impl From<BindingError> for OverlayError {
fn from(error: BindingError) -> Self {
match error {
BindingError::Unavailable | BindingError::SignatureMismatch => Self::UnsupportedMethod,
BindingError::Rejected => Self::Rejected,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OverlayMaterial(Cow<'static, CStr>);
impl OverlayMaterial {
#[doc(alias("TF_SCREEN_OVERLAY_MATERIAL_BLEED"))]
pub const BLEED: Self = Self::stock(c"effects/bleed_overlay");
#[doc(alias("TF_SCREEN_OVERLAY_MATERIAL_BURNING"))]
pub const BURNING: Self = Self::stock(c"effects/imcookin");
#[doc(alias("TF_SCREEN_OVERLAY_MATERIAL_GAS"))]
pub const GAS: Self = Self::stock(c"effects/gas_overlay");
#[doc(alias("TF_SCREEN_OVERLAY_MATERIAL_INVULN_BLUE"))]
pub const INVULN_BLUE: Self = Self::stock(c"effects/invuln_overlay_blue");
#[doc(alias("TF_SCREEN_OVERLAY_MATERIAL_INVULN_RED"))]
pub const INVULN_RED: Self = Self::stock(c"effects/invuln_overlay_red");
#[doc(alias(
"TF_SCREEN_OVERLAY_MATERIAL_SWIMMING_CURSE",
"TF_SCREEN_OVERLAY_MATERIAL_URINE"
))]
pub const JARATE: Self = Self::stock(c"effects/jarate_overlay");
#[doc(alias("TF_SCREEN_OVERLAY_MATERIAL_PHASE"))]
pub const PHASE: Self = Self::stock(c"effects/dodge_overlay");
#[doc(alias("TF_SCREEN_OVERLAY_MATERIAL_STEALTH"))]
pub const STEALTH: Self = Self::stock(c"effects/stealth_overlay");
pub const STOCK: [Self; 8] = [
Self::BLEED,
Self::BURNING,
Self::GAS,
Self::INVULN_BLUE,
Self::INVULN_RED,
Self::JARATE,
Self::PHASE,
Self::STEALTH,
];
pub fn new(name: impl Into<Cow<'static, CStr>>) -> Result<Self, MaterialNameError> {
let name = name.into();
check_name(&name)?;
Ok(Self(name))
}
const fn stock(name: &'static CStr) -> Self {
match check_name(name) {
Ok(()) => Self(Cow::Borrowed(name)),
Err(_) => panic!("a stock overlay material name is invalid"),
}
}
pub fn as_cstr(&self) -> &CStr {
&self.0
}
pub fn vmt_path(&self) -> CString {
let name = self.as_cstr().to_bytes();
let mut path =
Vec::with_capacity(MATERIALS_DIRECTORY.len() + name.len() + EXTENSION.len() + 1);
path.extend_from_slice(MATERIALS_DIRECTORY);
path.extend_from_slice(name);
path.extend_from_slice(EXTENSION);
unsafe { CString::from_vec_unchecked(path) }
}
}
#[doc(alias("m_szScriptOverlayMaterial"))]
#[derive(Debug, Clone, Copy)]
pub struct ScreenOverlay<'s> {
player: Entity<'s>,
}
impl<'s> ScreenOverlay<'s> {
pub fn new(server: Server<'s>, player: Entity<'s>) -> Result<Self, OverlayError> {
if server.game() != Game::TeamFortress2 {
return Err(OverlayError::UnsupportedGame);
}
if !player
.data_maps()
.any(|map| map.class_name() == Some(c"CBasePlayer"))
{
return Err(OverlayError::NotAPlayer);
}
Ok(Self { player })
}
fn check_live(self) -> Result<(), OverlayError> {
if self.player.is_marked_for_deletion() {
Err(OverlayError::MarkedForDeletion)
} else {
Ok(())
}
}
#[doc(alias("SetScriptOverlayMaterial"))]
pub fn clear(self) -> Result<(), OverlayError> {
self.set_name(c"")
}
pub fn clear_if(self, expected: &OverlayMaterial) -> Result<bool, OverlayError> {
let matches = self.get()?.is_some_and(|current| {
current
.to_bytes()
.eq_ignore_ascii_case(expected.as_cstr().to_bytes())
});
if matches {
self.clear()?;
}
Ok(matches)
}
#[doc(alias("GetScriptOverlayMaterial"))]
pub fn get(self) -> Result<Option<CString>, OverlayError> {
self.check_live()?;
let name = unsafe {
binding::call_string(
self.player,
c"CBasePlayer",
c"GetScriptOverlayMaterial",
&mut [],
)
}?;
Ok(name.filter(|name| !name.is_empty()))
}
pub const fn player(self) -> Entity<'s> {
self.player
}
#[doc(alias("SetScriptOverlayMaterial"))]
pub fn set(self, material: &OverlayMaterial) -> Result<(), OverlayError> {
self.set_name(material.as_cstr())
}
fn set_name(self, name: &CStr) -> Result<(), OverlayError> {
self.check_live()?;
unsafe {
binding::call(
self.player,
c"CBasePlayer",
c"SetScriptOverlayMaterial",
&mut [binding::string(name)],
binding::VOID,
)
}?;
Ok(())
}
}
const fn check_name(name: &CStr) -> Result<(), MaterialNameError> {
let bytes = name.to_bytes();
if bytes.is_empty() {
return Err(MaterialNameError::Empty);
}
if bytes.len() > MAX_MATERIAL_LEN {
return Err(MaterialNameError::TooLong { len: bytes.len() });
}
let mut index = 0;
while index < bytes.len() {
let byte = bytes[index];
if !matches!(byte, b' '..=b'~') || matches!(byte, b'"' | b'\'' | b';' | b'\\') {
return Err(MaterialNameError::InvalidByte { index, byte });
}
index += 1;
}
if let Some(directory) = bytes.first_chunk::<10>()
&& directory.eq_ignore_ascii_case(MATERIALS_DIRECTORY)
{
return Err(MaterialNameError::MaterialsDirectory);
}
if let Some(extension) = bytes.last_chunk::<4>()
&& extension.eq_ignore_ascii_case(EXTENSION)
{
return Err(MaterialNameError::Extension);
}
Ok(())
}