#[cfg(test)]
#[path = "../tests/tf2/observer.rs"]
mod tests;
use crate::datatables::{NetProp, NetPropError};
use crate::entities::Entity;
use crate::{Game, InterfaceError, Server};
use sdk_raw::tf2::observer as raw;
use sdk_raw::vcall;
use std::ffi::{CStr, c_int};
pub(crate) const VIEW_OFFSET: [&CStr; 3] = [
c"m_vecViewOffset[0]",
c"m_vecViewOffset[1]",
c"m_vecViewOffset[2]",
];
#[derive(Debug, thiserror::Error)]
pub enum ObserverError {
#[error(transparent)]
Interface(#[from] InterfaceError),
#[error("the player is marked for deletion")]
MarkedForDeletion,
#[error(transparent)]
NetProp(#[from] NetPropError),
#[error("the player is not spectating in a mode that can roam ({0:?})")]
NotObserving(ObserverMode),
#[error("observer operations require a TF2 player")]
NotTfPlayer,
#[error("the player's observer mode {0} is not one of TF2's")]
UnknownMode(c_int),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ObserverMode {
#[doc(alias("OBS_MODE_CHASE"))]
Chase,
#[doc(alias("OBS_MODE_DEATHCAM"))]
DeathCam,
#[doc(alias("OBS_MODE_FIXED"))]
Fixed,
#[doc(alias("OBS_MODE_FREEZECAM"))]
FreezeCam,
#[doc(alias("OBS_MODE_IN_EYE"))]
InEye,
#[doc(alias("OBS_MODE_NONE"))]
None,
#[doc(alias("OBS_MODE_POI"))]
PointOfInterest,
#[doc(alias("OBS_MODE_ROAMING"))]
Roaming,
}
impl ObserverMode {
pub const fn from_raw(raw: c_int) -> Option<Self> {
Some(match raw {
raw::OBS_MODE_NONE => Self::None,
raw::OBS_MODE_DEATHCAM => Self::DeathCam,
raw::OBS_MODE_FREEZECAM => Self::FreezeCam,
raw::OBS_MODE_FIXED => Self::Fixed,
raw::OBS_MODE_IN_EYE => Self::InEye,
raw::OBS_MODE_CHASE => Self::Chase,
raw::OBS_MODE_POI => Self::PointOfInterest,
raw::OBS_MODE_ROAMING => Self::Roaming,
_ => return None,
})
}
pub const fn to_raw(self) -> c_int {
match self {
Self::None => raw::OBS_MODE_NONE,
Self::DeathCam => raw::OBS_MODE_DEATHCAM,
Self::FreezeCam => raw::OBS_MODE_FREEZECAM,
Self::Fixed => raw::OBS_MODE_FIXED,
Self::InEye => raw::OBS_MODE_IN_EYE,
Self::Chase => raw::OBS_MODE_CHASE,
Self::PointOfInterest => raw::OBS_MODE_POI,
Self::Roaming => raw::OBS_MODE_ROAMING,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PlayerObserver<'s> {
server: Server<'s>,
player: Entity<'s>,
}
impl<'s> PlayerObserver<'s> {
pub fn new(server: Server<'s>, player: Entity<'s>) -> Result<Self, ObserverError> {
if server.game() != Game::TeamFortress2 || !player.has_data_map_class(c"CTFPlayer") {
return Err(ObserverError::NotTfPlayer);
}
Ok(Self { server, player })
}
#[doc(alias("m_iObserverMode", "GetObserverMode"))]
pub fn mode(self) -> Result<ObserverMode, ObserverError> {
let raw = self
.net_prop(c"m_iObserverMode")?
.get::<c_int>(self.player)?;
ObserverMode::from_raw(raw).ok_or(ObserverError::UnknownMode(raw))
}
fn net_prop(self, name: &CStr) -> Result<NetProp<'s>, ObserverError> {
Ok(self
.server
.server_game_dll()?
.entity_net_prop(self.player, name)?)
}
pub const fn player(self) -> Entity<'s> {
self.player
}
pub fn roam(self) -> Result<(), ObserverError> {
if self.player.is_marked_for_deletion() {
return Err(ObserverError::MarkedForDeletion);
}
match self.mode()? {
ObserverMode::Roaming => return Ok(()),
ObserverMode::Fixed
| ObserverMode::InEye
| ObserverMode::Chase
| ObserverMode::PointOfInterest => {}
mode => return Err(ObserverError::NotObserving(mode)),
}
let engine = self.server.valve_engine()?;
let mode = self.net_prop(c"m_iObserverMode")?;
let [x, y, z] = VIEW_OFFSET;
let view_offset = [self.net_prop(x)?, self.net_prop(y)?, self.net_prop(z)?];
let target = self.target()?;
unsafe { mode.set(engine, self.player, raw::OBS_MODE_ROAMING) }?;
for component in view_offset {
unsafe { component.set(engine, self.player, 0.0f32) }?;
}
if let Some(target) = target {
let player = self.player.as_ptr().cast::<sys::CTFPlayer>();
unsafe {
vcall!(player as sys::CTFPlayer__bindgen_vtable => CTFPlayer_SetObserverTarget(
target.as_ptr(),
));
}
}
Ok(())
}
#[doc(alias("m_hObserverTarget", "GetObserverTarget"))]
pub fn target(self) -> Result<Option<Entity<'s>>, ObserverError> {
let handle = self
.net_prop(c"m_hObserverTarget")?
.get_handle(self.player)?;
Ok(self.server.server_tools()?.entity_by_handle(handle))
}
}