#[cfg(test)]
#[path = "../tests/tf2/nav.rs"]
mod tests;
use crate::entities::Entity;
use crate::math::Vector;
use crate::tf2::scoreboard::ScoringTeam;
use crate::{Game, Server};
use sdk_raw::tf2::nav::{self as raw, attribute, tf_attribute};
use sdk_raw::vcall;
use std::ffi::{CStr, c_int};
use std::marker::PhantomData;
use std::ptr::NonNull;
const COMBAT_CHARACTER_CLASS: &CStr = c"CBaseCombatCharacter";
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum NavError {
#[error("navigation areas require Team Fortress 2")]
UnsupportedGame,
#[error("the entity is not a combat character")]
NotACombatCharacter,
#[error("the navigation mesh's list of areas was not found")]
MeshNotFound,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NavDirection {
North,
East,
South,
West,
}
impl NavDirection {
pub const ALL: [Self; 4] = [Self::North, Self::East, Self::South, Self::West];
pub const fn to_raw(self) -> sys::NavDirType {
match self {
Self::North => sys::NavDirType_NORTH,
Self::East => sys::NavDirType_EAST,
Self::South => sys::NavDirType_SOUTH,
Self::West => sys::NavDirType_WEST,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NavCorner {
NorthWest,
NorthEast,
SouthEast,
SouthWest,
}
impl NavCorner {
pub const ALL: [Self; 4] = [
Self::NorthWest,
Self::NorthEast,
Self::SouthEast,
Self::SouthWest,
];
pub const fn to_raw(self) -> sys::NavCornerType {
match self {
Self::NorthWest => sys::NavCornerType_NORTH_WEST,
Self::NorthEast => sys::NavCornerType_NORTH_EAST,
Self::SouthEast => sys::NavCornerType_SOUTH_EAST,
Self::SouthWest => sys::NavCornerType_SOUTH_WEST,
}
}
}
bitflags::bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NavAttributes: u32 {
#[doc(alias("NAV_MESH_AVOID"))]
const AVOID = attribute::AVOID;
#[doc(alias("NAV_MESH_CLIFF"))]
const CLIFF = attribute::CLIFF;
#[doc(alias("NAV_MESH_CROUCH"))]
const CROUCH = attribute::CROUCH;
#[doc(alias("NAV_MESH_DONT_HIDE"))]
const DONT_HIDE = attribute::DONT_HIDE;
#[doc(alias("NAV_MESH_FUNC_COST"))]
const FUNC_COST = attribute::FUNC_COST;
#[doc(alias("NAV_MESH_HAS_ELEVATOR"))]
const HAS_ELEVATOR = attribute::HAS_ELEVATOR;
#[doc(alias("NAV_MESH_JUMP"))]
const JUMP = attribute::JUMP;
#[doc(alias("NAV_MESH_NAV_BLOCKER"))]
const NAV_BLOCKER = attribute::NAV_BLOCKER;
#[doc(alias("NAV_MESH_NO_HOSTAGES"))]
const NO_HOSTAGES = attribute::NO_HOSTAGES;
#[doc(alias("NAV_MESH_NO_JUMP"))]
const NO_JUMP = attribute::NO_JUMP;
#[doc(alias("NAV_MESH_NO_MERGE"))]
const NO_MERGE = attribute::NO_MERGE;
#[doc(alias("NAV_MESH_OBSTACLE_TOP"))]
const OBSTACLE_TOP = attribute::OBSTACLE_TOP;
#[doc(alias("NAV_MESH_PRECISE"))]
const PRECISE = attribute::PRECISE;
#[doc(alias("NAV_MESH_RUN"))]
const RUN = attribute::RUN;
#[doc(alias("NAV_MESH_STAND"))]
const STAND = attribute::STAND;
#[doc(alias("NAV_MESH_STAIRS"))]
const STAIRS = attribute::STAIRS;
#[doc(alias("NAV_MESH_STOP"))]
const STOP = attribute::STOP;
#[doc(alias("NAV_MESH_TRANSIENT"))]
const TRANSIENT = attribute::TRANSIENT;
#[doc(alias("NAV_MESH_WALK"))]
const WALK = attribute::WALK;
}
}
bitflags::bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TfNavAttributes: u32 {
#[doc(alias("TF_NAV_BLOCKED"))]
const BLOCKED = tf_attribute::BLOCKED;
#[doc(alias("TF_NAV_BLOCKED_AFTER_POINT_CAPTURE"))]
const BLOCKED_AFTER_POINT_CAPTURE = tf_attribute::BLOCKED_AFTER_POINT_CAPTURE;
#[doc(alias("TF_NAV_BLOCKED_UNTIL_POINT_CAPTURE"))]
const BLOCKED_UNTIL_POINT_CAPTURE = tf_attribute::BLOCKED_UNTIL_POINT_CAPTURE;
#[doc(alias("TF_NAV_BLUE_ONE_WAY_DOOR"))]
const BLUE_ONE_WAY_DOOR = tf_attribute::BLUE_ONE_WAY_DOOR;
#[doc(alias("TF_NAV_BLUE_SENTRY_DANGER"))]
const BLUE_SENTRY_DANGER = tf_attribute::BLUE_SENTRY_DANGER;
#[doc(alias("TF_NAV_BLUE_SETUP_GATE"))]
const BLUE_SETUP_GATE = tf_attribute::BLUE_SETUP_GATE;
#[doc(alias("TF_NAV_BOMB_CAN_DROP_HERE"))]
const BOMB_CAN_DROP_HERE = tf_attribute::BOMB_CAN_DROP_HERE;
#[doc(alias("TF_NAV_CONTROL_POINT"))]
const CONTROL_POINT = tf_attribute::CONTROL_POINT;
#[doc(alias("TF_NAV_DOOR_ALWAYS_BLOCKS"))]
const DOOR_ALWAYS_BLOCKS = tf_attribute::DOOR_ALWAYS_BLOCKS;
#[doc(alias("TF_NAV_DOOR_NEVER_BLOCKS"))]
const DOOR_NEVER_BLOCKS = tf_attribute::DOOR_NEVER_BLOCKS;
#[doc(alias("TF_NAV_ESCAPE_ROUTE"))]
const ESCAPE_ROUTE = tf_attribute::ESCAPE_ROUTE;
#[doc(alias("TF_NAV_ESCAPE_ROUTE_VISIBLE"))]
const ESCAPE_ROUTE_VISIBLE = tf_attribute::ESCAPE_ROUTE_VISIBLE;
#[doc(alias("TF_NAV_HAS_AMMO"))]
const HAS_AMMO = tf_attribute::HAS_AMMO;
#[doc(alias("TF_NAV_HAS_HEALTH"))]
const HAS_HEALTH = tf_attribute::HAS_HEALTH;
#[doc(alias("TF_NAV_NO_SPAWNING"))]
const NO_SPAWNING = tf_attribute::NO_SPAWNING;
#[doc(alias("TF_NAV_RED_ONE_WAY_DOOR"))]
const RED_ONE_WAY_DOOR = tf_attribute::RED_ONE_WAY_DOOR;
#[doc(alias("TF_NAV_RED_SENTRY_DANGER"))]
const RED_SENTRY_DANGER = tf_attribute::RED_SENTRY_DANGER;
#[doc(alias("TF_NAV_RED_SETUP_GATE"))]
const RED_SETUP_GATE = tf_attribute::RED_SETUP_GATE;
#[doc(alias("TF_NAV_RESCUE_CLOSET"))]
const RESCUE_CLOSET = tf_attribute::RESCUE_CLOSET;
#[doc(alias("TF_NAV_SENTRY_SPOT"))]
const SENTRY_SPOT = tf_attribute::SENTRY_SPOT;
#[doc(alias("TF_NAV_SNIPER_SPOT"))]
const SNIPER_SPOT = tf_attribute::SNIPER_SPOT;
#[doc(alias("TF_NAV_SPAWN_ROOM_BLUE"))]
const SPAWN_ROOM_BLUE = tf_attribute::SPAWN_ROOM_BLUE;
#[doc(alias("TF_NAV_SPAWN_ROOM_EXIT"))]
const SPAWN_ROOM_EXIT = tf_attribute::SPAWN_ROOM_EXIT;
#[doc(alias("TF_NAV_SPAWN_ROOM_RED"))]
const SPAWN_ROOM_RED = tf_attribute::SPAWN_ROOM_RED;
#[doc(alias("TF_NAV_UNBLOCKABLE"))]
const UNBLOCKABLE = tf_attribute::UNBLOCKABLE;
#[doc(alias("TF_NAV_WITH_FIFTH_POINT"))]
const WITH_FIFTH_POINT = tf_attribute::WITH_FIFTH_POINT;
#[doc(alias("TF_NAV_WITH_FOURTH_POINT"))]
const WITH_FOURTH_POINT = tf_attribute::WITH_FOURTH_POINT;
#[doc(alias("TF_NAV_WITH_SECOND_POINT"))]
const WITH_SECOND_POINT = tf_attribute::WITH_SECOND_POINT;
#[doc(alias("TF_NAV_WITH_THIRD_POINT"))]
const WITH_THIRD_POINT = tf_attribute::WITH_THIRD_POINT;
}
}
bitflags::bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HidingSpotFlags: u8 {
const EXPOSED = sys::HidingSpot_EXPOSED as u8;
const GOOD_SNIPER_SPOT = sys::HidingSpot_GOOD_SNIPER_SPOT as u8;
const IDEAL_SNIPER_SPOT = sys::HidingSpot_IDEAL_SNIPER_SPOT as u8;
const IN_COVER = sys::HidingSpot_IN_COVER as u8;
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NavConnection<'s> {
pub area: NavArea<'s>,
pub length: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HidingSpot {
pub id: u32,
pub flags: HidingSpotFlags,
pub position: Vector,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NavArea<'s> {
area: NonNull<sys::CTFNavArea>,
_scope: PhantomData<&'s ()>,
}
impl<'s> NavArea<'s> {
pub const unsafe fn from_raw(area: NonNull<sys::CTFNavArea>) -> Self {
Self {
area,
_scope: PhantomData,
}
}
#[doc(alias("GetLastKnownArea"))]
pub fn last_known(server: Server<'s>, entity: Entity<'s>) -> Result<Option<Self>, NavError> {
if server.game() != Game::TeamFortress2 {
return Err(NavError::UnsupportedGame);
}
if !entity.is_a(COMBAT_CHARACTER_CLASS) {
return Err(NavError::NotACombatCharacter);
}
let character = entity.as_ptr().cast::<sys::CBaseCombatCharacter>();
let area = unsafe {
vcall!(character as sys::CBaseCombatCharacter__bindgen_vtable => CBaseCombatCharacter_GetLastKnownArea())
};
Ok(NonNull::new(area).map(|area| unsafe { Self::from_raw(area.cast()) }))
}
pub fn attributes(self) -> NavAttributes {
NavAttributes::from_bits_retain(self.critical().m_attributeFlags as u32)
}
#[doc(alias("GetClosestPointOnArea"))]
pub fn closest_point(self, position: Vector) -> Vector {
let critical = self.critical();
let x = position
.x
.max(critical.m_nwCorner.x)
.min(critical.m_seCorner.x);
let y = position
.y
.max(critical.m_nwCorner.y)
.min(critical.m_seCorner.y);
Vector::new(x, y, self.ground_height(x, y))
}
#[doc(alias("GetCenter"))]
pub fn center(self) -> Vector {
self.critical().m_center.into()
}
#[doc(alias("GetCorner"))]
pub fn corner(self, corner: NavCorner) -> Vector {
let critical = self.critical();
let (north_west, south_east) = (critical.m_nwCorner, critical.m_seCorner);
match corner {
NavCorner::NorthWest => north_west.into(),
NavCorner::NorthEast => Vector::new(south_east.x, north_west.y, critical.m_neZ),
NavCorner::SouthEast => south_east.into(),
NavCorner::SouthWest => Vector::new(north_west.x, south_east.y, critical.m_swZ),
}
}
#[doc(alias("GetAdjacentArea", "GetAdjacentCount", "m_connect"))]
pub fn connections(
self,
direction: NavDirection,
) -> impl Iterator<Item = NavConnection<'s>> + use<'s> {
let vector = &raw const self.critical().m_connect[direction.to_raw() as usize];
let connections = unsafe { raw::ultra_conservative_elements::<sys::NavConnect>(vector) };
connections.iter().filter_map(|connection| {
let area = NonNull::new(unsafe { connection.__bindgen_anon_1.area })?;
Some(NavConnection {
area: unsafe { Self::from_raw(area.cast()) },
length: connection.length,
})
})
}
#[doc(alias("GetExtent"))]
pub fn extent(self) -> (Vector, Vector) {
let critical = self.critical();
let (north_west, south_east) = (critical.m_nwCorner, critical.m_seCorner);
let heights = [north_west.z, south_east.z, critical.m_neZ, critical.m_swZ];
let low = heights.into_iter().fold(f32::INFINITY, f32::min);
let high = heights.into_iter().fold(f32::NEG_INFINITY, f32::max);
(
Vector::new(north_west.x, north_west.y, low),
Vector::new(south_east.x, south_east.y, high),
)
}
#[doc(alias("GetZ"))]
pub fn ground_height(self, x: f32, y: f32) -> f32 {
let critical = self.critical();
if critical.m_invDxCorners == 0.0 || critical.m_invDyCorners == 0.0 {
return critical.m_neZ;
}
let (north_west, south_east) = (critical.m_nwCorner, critical.m_seCorner);
let u = ((x - north_west.x) * critical.m_invDxCorners).clamp(0.0, 1.0);
let v = ((y - north_west.y) * critical.m_invDyCorners).clamp(0.0, 1.0);
let north = north_west.z + u * (critical.m_neZ - north_west.z);
let south = critical.m_swZ + u * (south_east.z - critical.m_swZ);
north + v * (south - north)
}
#[doc(alias("m_hidingSpots"))]
pub fn hiding_spots(self) -> impl Iterator<Item = HidingSpot> + use<'s> {
let spots = unsafe {
raw::ultra_conservative_elements::<*mut sys::HidingSpot>(
&raw const self.base().m_hidingSpots,
)
};
spots.iter().filter_map(|&spot| {
let spot = unsafe { spot.as_ref() }?;
Some(HidingSpot {
id: spot.m_id,
flags: HidingSpotFlags::from_bits_retain(spot.m_flags),
position: spot.m_pos.into(),
})
})
}
#[doc(alias("GetID"))]
pub fn id(self) -> u32 {
self.base().m_id
}
#[doc(alias("GetIncursionDistance", "IsReachableByTeam"))]
pub fn incursion_distance(self, team: ScoringTeam) -> Option<f32> {
let distance = self.tf().m_distanceFromSpawnRoom[team.to_raw() as usize];
(distance >= 0.0).then_some(distance)
}
#[doc(alias("IsBlocked"))]
pub fn is_blocked(self, team: Option<ScoringTeam>) -> bool {
let area = self.area.as_ptr().cast_const();
let team = team.map_or(raw::TEAM_ANY, ScoringTeam::to_raw);
unsafe {
vcall!(area as sys::CTFNavArea__bindgen_vtable => CTFNavArea_IsBlocked(team, false))
}
}
#[doc(alias("IsOverlapping"))]
pub fn is_overlapping(self, position: Vector, tolerance: f32) -> bool {
let critical = self.critical();
position.x + tolerance >= critical.m_nwCorner.x
&& position.x - tolerance <= critical.m_seCorner.x
&& position.y + tolerance >= critical.m_nwCorner.y
&& position.y - tolerance <= critical.m_seCorner.y
}
#[doc(alias("IsPotentiallyVisibleToTeam"))]
pub fn is_potentially_visible_to(self, team: ScoringTeam) -> bool {
let area = self.area.as_ptr().cast_const();
let team: c_int = team.to_raw();
unsafe {
vcall!(area as sys::CTFNavArea__bindgen_vtable => CTFNavArea_IsPotentiallyVisibleToTeam(team))
}
}
#[doc(alias("IsUnderwater"))]
pub fn is_underwater(self) -> bool {
self.base().m_isUnderwater
}
#[doc(alias("GetPlayerCount"))]
pub fn player_count(self, team: Option<ScoringTeam>) -> u32 {
let counts = self.critical().m_playerCount;
match team {
Some(team) => counts[team.to_raw() as usize % raw::MAX_NAV_TEAMS].into(),
None => counts.iter().map(|&count| u32::from(count)).sum(),
}
}
#[doc(alias("GetAttributesTF"))]
pub fn tf_attributes(self) -> TfNavAttributes {
TfNavAttributes::from_bits_retain(self.tf().m_attributeFlags)
}
#[doc(alias("GetTravelDistanceToBombTarget"))]
pub fn travel_distance_to_bomb_target(self) -> Option<f32> {
let distance = self.tf().m_distanceToBombTarget;
(distance >= 0.0).then_some(distance)
}
pub const fn as_ptr(self) -> *mut sys::CTFNavArea {
self.area.as_ptr()
}
fn base(&self) -> &sys::CNavArea {
&self.tf()._base
}
fn critical(&self) -> &sys::CNavAreaCriticalData {
&self.base()._base
}
fn tf(&self) -> &sys::CTFNavArea {
unsafe { self.area.as_ref() }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NavMesh<'s> {
areas: NonNull<raw::NavAreaVector>,
_scope: PhantomData<&'s ()>,
}
impl<'s> NavMesh<'s> {
pub const BENEATH_LIMIT: f32 = 120.0;
#[doc(alias("TheNavAreas", "TheNavMesh"))]
pub fn find(server: Server<'s>) -> Result<Self, NavError> {
if server.game() != Game::TeamFortress2 {
return Err(NavError::UnsupportedGame);
}
let areas = unsafe { raw::cached_nav_areas(server.game_server_factory().as_raw()) }
.ok()
.flatten()
.ok_or(NavError::MeshNotFound)?;
Ok(unsafe { Self::from_raw(areas) })
}
pub const unsafe fn from_raw(areas: NonNull<raw::NavAreaVector>) -> Self {
Self {
areas,
_scope: PhantomData,
}
}
pub fn area_count(self) -> usize {
self.elements().len()
}
#[doc(alias("GetNavAreaByID"))]
pub fn area_by_id(self, id: u32) -> Option<NavArea<'s>> {
if id == 0 {
return None;
}
self.areas().find(|area| area.id() == id)
}
#[doc(alias("GetNavArea"))]
pub fn area_at(self, position: Vector, beneath: f32) -> Option<NavArea<'s>> {
let test = Vector::new(position.x, position.y, position.z + 5.0);
let mut found = None;
let mut found_height = f32::NEG_INFINITY;
for area in self.areas() {
if !area.is_overlapping(test, 0.0) {
continue;
}
let height = area.ground_height(test.x, test.y);
if height <= test.z && height >= position.z - beneath && height > found_height {
found = Some(area);
found_height = height;
}
}
found
}
pub fn areas(self) -> impl Iterator<Item = NavArea<'s>> + use<'s> {
(0..)
.map_while(move |index| self.elements().get(index).copied())
.filter_map(|area| {
NonNull::new(area).map(|area| unsafe { NavArea::from_raw(area.cast()) })
})
}
#[doc(alias("GetNearestNavArea"))]
pub fn nearest_area(
self,
position: Vector,
max_distance: f32,
team: Option<ScoringTeam>,
) -> Option<NavArea<'s>> {
let mut found = None;
let mut found_distance = max_distance * max_distance;
for area in self.areas() {
let distance = position.distance_squared(*area.closest_point(position));
if distance < found_distance && !area.is_blocked(team) {
found = Some(area);
found_distance = distance;
}
}
found
}
pub const fn as_ptr(self) -> *mut raw::NavAreaVector {
self.areas.as_ptr()
}
fn elements(&self) -> &[*mut sys::CNavArea] {
unsafe { raw::nav_area_elements(self.areas) }
}
}