#[cfg(test)]
#[path = "../../tests/interfaces/engine_trace.rs"]
mod tests;
use crate::entities::Entity;
use crate::math::Vector;
use sdk_raw::vcall;
use std::ffi::{CStr, c_int, c_uint, c_ushort};
use std::mem::MaybeUninit;
use std::ptr::{self, NonNull};
use std::slice;
pub use sdk_raw::interfaces::engine_trace::{
ALL_VISIBLE_CONTENTS, CONTENTS_AREAPORTAL, CONTENTS_AUX, CONTENTS_BLOCKLOS, CONTENTS_CURRENT_0,
CONTENTS_CURRENT_90, CONTENTS_CURRENT_180, CONTENTS_CURRENT_270, CONTENTS_CURRENT_DOWN,
CONTENTS_CURRENT_UP, CONTENTS_DEBRIS, CONTENTS_DETAIL, CONTENTS_EMPTY, CONTENTS_GRATE,
CONTENTS_HITBOX, CONTENTS_IGNORE_NODRAW_OPAQUE, CONTENTS_LADDER, CONTENTS_MONSTER,
CONTENTS_MONSTERCLIP, CONTENTS_MOVEABLE, CONTENTS_OPAQUE, CONTENTS_ORIGIN, CONTENTS_PLAYERCLIP,
CONTENTS_SLIME, CONTENTS_SOLID, CONTENTS_TEAM1, CONTENTS_TEAM2, CONTENTS_TESTFOGVOLUME,
CONTENTS_TRANSLUCENT, CONTENTS_UNUSED, CONTENTS_UNUSED6, CONTENTS_WATER, CONTENTS_WINDOW,
LAST_VISIBLE_CONTENTS, MASK_ALL, MASK_BLOCKLOS, MASK_BLOCKLOS_AND_NPCS, MASK_CURRENT,
MASK_DEADSOLID, MASK_NPCSOLID, MASK_NPCSOLID_BRUSHONLY, MASK_NPCWORLDSTATIC, MASK_OPAQUE,
MASK_OPAQUE_AND_NPCS, MASK_PLAYERSOLID, MASK_PLAYERSOLID_BRUSHONLY, MASK_SHOT, MASK_SHOT_HULL,
MASK_SHOT_PORTAL, MASK_SOLID, MASK_SOLID_BRUSHONLY, MASK_SPLITAREAPORTAL, MASK_VISIBLE,
MASK_VISIBLE_AND_NPCS, MASK_WATER,
};
const _: () = {
assert!(
std::mem::offset_of!(
sys::ITraceFilter__bindgen_vtable,
ITraceFilter_ShouldHitEntity
) == 0
);
assert!(
size_of::<sys::ITraceFilter__bindgen_vtable>() == 2 * size_of::<unsafe extern "C" fn()>()
);
assert!(std::mem::offset_of!(FilterInterface<'_, '_>, interface) == 0);
};
static FILTER_VTABLE: sys::ITraceFilter__bindgen_vtable = sys::ITraceFilter__bindgen_vtable {
ITraceFilter_ShouldHitEntity: should_hit_entity,
ITraceFilter_GetTraceType: trace_type,
};
#[repr(C)]
struct FilterInterface<'a, 'e> {
interface: sys::ITraceFilter,
filter: TraceFilter<'a, 'e>,
}
#[doc(alias("Ray_t"))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Ray {
pub start: Vector,
pub end: Vector,
pub mins: Vector,
pub maxs: Vector,
}
impl Ray {
pub const fn hull(start: Vector, end: Vector, mins: Vector, maxs: Vector) -> Self {
Self {
start,
end,
mins,
maxs,
}
}
pub const fn line(start: Vector, end: Vector) -> Self {
let origin = Vector::new(0.0, 0.0, 0.0);
Self::hull(start, end, origin, origin)
}
fn to_raw(self) -> sys::Ray_t {
let aligned = |vector: Vector| sys::VectorAligned {
_base: sys::Vector::from(vector),
};
let delta = *self.end - *self.start;
let extents = (*self.maxs - *self.mins) * 0.5;
let center = (*self.mins + *self.maxs) * 0.5;
sys::Ray_t {
m_Start: aligned(Vector(*self.start + center)),
m_Delta: aligned(Vector(delta)),
m_StartOffset: aligned(Vector(-center)),
m_Extents: aligned(Vector(extents)),
m_IsRay: extents.length_squared() < 1e-6,
m_IsSwept: delta.length_squared() != 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Trace<'s> {
pub fraction: f32,
pub end: Vector,
pub start_solid: bool,
pub all_solid: bool,
#[doc(alias("fractionleftsolid"))]
pub fraction_left_solid: f32,
pub normal: Vector,
pub contents: c_int,
#[doc(alias("csurface_t", "surface"))]
pub surface_name: Option<&'s CStr>,
pub surface_flags: c_ushort,
#[doc(alias("dispFlags", "DISPSURF_FLAG"))]
pub displacement_flags: c_ushort,
pub hitbox: c_int,
pub entity: Option<Entity<'s>>,
}
impl Trace<'_> {
const DISPLACEMENT_SURFACE: c_ushort = 1 << 0;
pub fn hit(&self) -> bool {
self.fraction < 1.0
}
pub fn hit_displacement(&self) -> bool {
self.displacement_flags & Self::DISPLACEMENT_SURFACE != 0
}
pub fn static_prop(&self) -> Option<u32> {
let world = self.entity.and_then(Entity::index) == Some(0);
u32::try_from(self.hitbox)
.ok()
.filter(|&hitbox| world && hitbox > 0)
.map(|hitbox| hitbox - 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TraceFilter<'a, 'e> {
#[doc(alias("CTraceFilterHitAll"))]
Everything,
#[doc(alias("CTraceFilterWorldOnly", "TRACE_WORLD_ONLY"))]
WorldOnly,
#[doc(alias("CTraceFilterWorldAndPropsOnly"))]
WorldAndProps,
#[doc(alias("CTraceFilterEntitiesOnly", "TRACE_ENTITIES_ONLY"))]
EntitiesOnly,
#[doc(alias(
"CTraceFilterSimple",
"CTraceFilterSkipTwoEntities",
"CTraceFilterSimpleList"
))]
Skip(&'a [Entity<'e>]),
Only(&'a [Entity<'e>]),
#[doc(alias("CTraceFilterIgnoreTeammates"))]
SkipTeam(c_int),
}
impl TraceFilter<'_, '_> {
fn hits(&self, entity: Entity<'_>) -> bool {
let listed = |entities: &[Entity<'_>]| {
entities
.iter()
.any(|listed| ptr::eq(listed.as_ptr(), entity.as_ptr()))
};
match *self {
Self::Everything | Self::EntitiesOnly => true,
Self::WorldOnly | Self::WorldAndProps => false,
Self::Skip(entities) => !listed(entities),
Self::Only(entities) => listed(entities),
Self::SkipTeam(team) => entity.team().ok() != Some(team),
}
}
fn trace_type(&self) -> sys::TraceType_t {
match self {
Self::WorldOnly => sys::TraceType_t_TRACE_WORLD_ONLY,
Self::EntitiesOnly => sys::TraceType_t_TRACE_ENTITIES_ONLY,
_ => sys::TraceType_t_TRACE_EVERYTHING,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineTrace<'s> {
pub fraction: f32,
pub end: Vector,
pub start_solid: bool,
pub all_solid: bool,
pub entity: Option<Entity<'s>>,
}
impl LineTrace<'_> {
pub fn hit(&self) -> bool {
self.fraction < 1.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WorldTrace {
pub fraction: f32,
pub end: Vector,
pub start_solid: bool,
pub all_solid: bool,
}
impl WorldTrace {
pub fn hit(&self) -> bool {
self.fraction < 1.0
}
}
unsafe extern "C" fn should_hit_entity(
filter: *mut sys::ITraceFilter,
entity: *mut sys::IHandleEntity,
_contents_mask: c_int,
) -> bool {
unsafe {
let filter = &(*filter.cast::<FilterInterface<'_, '_>>()).filter;
NonNull::new(entity.cast::<sys::CBaseEntity>())
.is_some_and(|entity| filter.hits(Entity::from_raw(entity)))
}
}
unsafe extern "C" fn trace_type(filter: *const sys::ITraceFilter) -> sys::TraceType_t {
unsafe {
(*filter.cast::<FilterInterface<'_, '_>>())
.filter
.trace_type()
}
}
interface! {
#[doc(alias("IEngineTrace"))]
pub struct EngineTrace(sys::IEngineTrace) = Engine sdk_raw::interfaces::engine_trace::VERSION;
}
impl<'s> EngineTrace<'s> {
#[doc(alias("ClipRayToEntity", "PointIsWithin"))]
pub fn box_overlaps_entity(
self,
entity: Entity<'_>,
position: Vector,
mins: Vector,
maxs: Vector,
mask: c_uint,
) -> bool {
let ray = Ray::hull(position, position, mins, maxs).to_raw();
let handle = entity.as_ptr().cast::<sys::IHandleEntity>();
let mut trace = MaybeUninit::<sys::trace_t>::zeroed();
unsafe {
vcall!(self.as_ptr() => IEngineTrace_ClipRayToEntity(&ray, mask, handle, trace.as_mut_ptr()))
};
unsafe { trace.assume_init() }._base.startsolid
}
#[doc(alias("GetPointContents"))]
pub fn point_contents(self, position: Vector) -> c_int {
let position = sys::Vector::from(position);
unsafe {
vcall!(self.as_ptr() => IEngineTrace_GetPointContents(&position, ptr::null_mut()))
}
}
#[doc(alias("TraceRay", "UTIL_TraceLine", "UTIL_TraceHull", "UTIL_TraceRay"))]
pub fn trace(self, ray: Ray, mask: c_uint, filter: TraceFilter<'_, '_>) -> Trace<'s> {
let mut filter = FilterInterface {
interface: sys::ITraceFilter {
vtable_: &raw const FILTER_VTABLE,
},
filter,
};
let ray = ray.to_raw();
let mut trace = MaybeUninit::<sys::trace_t>::zeroed();
unsafe {
vcall!(self.as_ptr() => IEngineTrace_TraceRay(&ray, mask, (&raw mut filter).cast(), trace.as_mut_ptr()))
};
let trace = unsafe { trace.assume_init() };
let hit = trace._base.fraction < 1.0 || trace._base.startsolid || trace._base.allsolid;
Trace {
fraction: trace._base.fraction,
end: trace._base.endpos.into(),
start_solid: trace._base.startsolid,
all_solid: trace._base.allsolid,
fraction_left_solid: trace.fractionleftsolid,
normal: trace._base.plane.normal.into(),
contents: trace._base.contents,
surface_name: NonNull::new(trace.surface.name.cast_mut())
.map(|name| unsafe { CStr::from_ptr(name.as_ptr()) }),
surface_flags: trace.surface.flags,
displacement_flags: trace._base.dispFlags,
hitbox: trace.hitbox,
entity: NonNull::new(trace.m_pEnt)
.filter(|_| hit)
.map(|entity| unsafe { Entity::from_raw(entity) }),
}
}
#[doc(alias("TraceRay", "UTIL_TraceLine", "CTraceFilterSimple"))]
pub fn trace_line(
self,
start: Vector,
end: Vector,
mask: c_uint,
skip: Option<Entity<'_>>,
) -> LineTrace<'s> {
let filter = skip.as_ref().map_or(TraceFilter::Everything, |skip| {
TraceFilter::Skip(slice::from_ref(skip))
});
let trace = self.trace(Ray::line(start, end), mask, filter);
LineTrace {
fraction: trace.fraction,
end: trace.end,
start_solid: trace.start_solid,
all_solid: trace.all_solid,
entity: trace.entity,
}
}
#[doc(alias("TraceRay", "UTIL_TraceLine", "CTraceFilterWorldOnly"))]
pub fn trace_world_line(self, start: Vector, end: Vector, mask: c_uint) -> WorldTrace {
let trace = self.trace(Ray::line(start, end), mask, TraceFilter::WorldOnly);
WorldTrace {
fraction: trace.fraction,
end: trace.end,
start_solid: trace.start_solid,
all_solid: trace.all_solid,
}
}
}