// vischeck.cpp — line-of-sight test via EngineTraceClient003. // // IEngineTrace::TraceRay (vtable slot 4) is given a ray from the eye to the // aim point with TRACE_EVERYTHING/MASK_SHOT, ignoring the local player and the // target entity. This catches world geometry, static props and temporary // objects such as crates/NPCs. Glass is skipped only when it is non-solid or // its material name marks it breakable; solid non-break glass (bulletproof) // blocks the check. #include "hl2h.h" #include #include #include #include #include namespace { struct alignas(16) VectorAlignedCompat { float x, y, z, w; }; // Ray_t: four VectorAligned (16 bytes each) followed by two bools. struct alignas(16) RayCompat { VectorAlignedCompat start; VectorAlignedCompat delta; VectorAlignedCompat startOffset; VectorAlignedCompat extents; bool isRay; bool isSwept; }; static_assert(sizeof(VectorAlignedCompat) == 16, "VectorAligned layout"); static_assert(offsetof(RayCompat, delta) == 16, "Ray_t delta offset"); static_assert(offsetof(RayCompat, isRay) == 64, "Ray_t isRay offset"); // trace_t / CGameTrace layout (Source 2013, 32-bit): // CBaseTrace: startpos 0, endpos 12, cplane_t plane 24 (20 bytes), // fraction 44, contents 48, dispFlags 52, allsolid 54, // startsolid 55 // CGameTrace: fractionleftsolid 56, csurface_t surface 60 (8 bytes), // hitgroup 68, physicsbone 72, m_pEnt 76, hitbox 80 struct SurfaceCompat { const char *name; short surfaceProps; unsigned short flags; }; struct GameTraceCompat { Vector startpos; Vector endpos; Vector normal; float dist; uint8_t type; uint8_t signbits; uint8_t pad[2]; float fraction; int contents; uint16_t dispFlags; bool allsolid; bool startsolid; float fractionleftsolid; SurfaceCompat surface; int hitgroup; short physicsbone; void *ent; int hitbox; uint8_t rest[64]; // future-proof / avoid short writes }; static_assert(offsetof(GameTraceCompat, fraction) == 44, "trace_t fraction"); static_assert(offsetof(GameTraceCompat, startsolid) == 55, "trace_t startsolid"); static_assert(offsetof(GameTraceCompat, ent) == 76, "trace_t m_pEnt"); // ITraceFilter has no virtual destructor: vtable[0] = ShouldHitEntity, // vtable[1] = GetTraceType. struct ITraceFilterCompat { virtual bool ShouldHitEntity(void *pEntity, int contentsMask) = 0; virtual int GetTraceType() const = 0; }; struct EntityFilter : ITraceFilterCompat { void *ignore; explicit EntityFilter(void *ig) : ignore(ig) {} bool ShouldHitEntity(void *pEntity, int) override { return pEntity != ignore; } int GetTraceType() const override { return 0; } // TRACE_EVERYTHING }; struct VisibilityFilter : ITraceFilterCompat { void *self; void *target; VisibilityFilter(void *s, void *t) : self(s), target(t) {} bool ShouldHitEntity(void *pEntity, int) override { return pEntity != self && pEntity != target; } int GetTraceType() const override { return 0; } // TRACE_EVERYTHING }; constexpr unsigned CONTENTS_SOLID = 0x1u; constexpr unsigned CONTENTS_WINDOW = 0x2u; // MASK_SHOT = SOLID|MOVEABLE|MONSTER|WINDOW|DEBRIS|HITBOX constexpr unsigned MASK_SHOT = 0x1u | 0x4000u | 0x2000000u | 0x2u | 0x4000000u | 0x40000000u; // MASK_PLAYERSOLID = SOLID|MOVEABLE|PLAYERCLIP|WINDOW|MONSTER|GRATE constexpr unsigned MASK_PLAYERSOLID = 0x1u | 0x4000u | 0x10000u | 0x2u | 0x2000000u | 0x8u; static void make_ray(RayCompat &ray, const Vector &start, const Vector &end) { float dx = end.x - start.x, dy = end.y - start.y, dz = end.z - start.z; ray = RayCompat{}; ray.start = {start.x, start.y, start.z, 0.0f}; ray.delta = {dx, dy, dz, 0.0f}; ray.startOffset = {0.0f, 0.0f, 0.0f, 0.0f}; ray.extents = {0.0f, 0.0f, 0.0f, 0.0f}; ray.isRay = true; ray.isSwept = (dx * dx + dy * dy + dz * dz) > 0.0f; } // Swept AABB matching the standing player hull used by CGameMovement. // Ray_t::Init shifts m_Start to the box center and stores the negated center // offset in m_StartOffset. With mins=(-16,-16,0), maxs=(16,16,72), the // extents are (16,16,36) and m_StartOffset is (0,0,-36). static void make_standing_hull_ray(RayCompat &ray, const Vector &origin, float distance) { ray = RayCompat{}; ray.start = {origin.x, origin.y, origin.z + 36.0f, 0.0f}; ray.delta = {0.0f, 0.0f, -distance, 0.0f}; ray.startOffset = {0.0f, 0.0f, -36.0f, 0.0f}; ray.extents = {16.0f, 16.0f, 36.0f, 0.0f}; ray.isRay = false; ray.isSwept = true; } } // namespace static bool surface_contains(const SurfaceCompat &surf, const char *needle) { if (!surf.name || !needle) return false; size_t n = strlen(needle); for (const char *p = surf.name; *p; p++) if (strncasecmp(p, needle, n) == 0) return true; return false; } static bool surface_is_breakable_glass(const SurfaceCompat &surf) { return surface_contains(surf, "break"); } static bool surface_is_glass(const SurfaceCompat &surf) { return surface_contains(surf, "glass"); } bool is_visible(const Vector &eye, const Vector &point, void *self, void *target) { static bool warned = false; if (!g_iface.enginetrace) { if (!warned) { warned = true; logf("vischeck: EngineTraceClient003 unavailable; failing open"); } return true; } float dx = point.x - eye.x; float dy = point.y - eye.y; float dz = point.z - eye.z; float len2 = dx * dx + dy * dy + dz * dz; if (len2 < 0.0001f) return true; float inv_len = 1.0f / sqrtf(len2); Vector dir{dx * inv_len, dy * inv_len, dz * inv_len}; // Trace everything that bullets care about: world, static props, crates and // NPCs. Non-solid glass and glass whose material name marks it breakable is // skipped; solid non-break glass (bulletproof) blocks. Vector start = eye; for (int pass = 0; pass < 5; pass++) { RayCompat ray; make_ray(ray, start, point); GameTraceCompat tr{}; VisibilityFilter filter(self, target); using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *, GameTraceCompat *); vfunc(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, MASK_SHOT, &filter, &tr); if (tr.startsolid || tr.allsolid) return false; if (tr.fraction >= 1.0f) return true; bool glass = (tr.contents & CONTENTS_WINDOW) != 0 || surface_is_glass(tr.surface); bool solid = (tr.contents & CONTENTS_SOLID) != 0; bool breakable_glass = glass && surface_is_breakable_glass(tr.surface); if (glass && (!solid || breakable_glass)) { // Step just past the pane and look for the next obstruction. start = tr.endpos; start.x += dir.x * 0.5f; start.y += dir.y * 0.5f; start.z += dir.z * 0.5f; logd("vischeck: skipped glass surface=%s contents=0x%x solid=%d break=%d", tr.surface.name ? tr.surface.name : "?", tr.contents, (int)solid, (int)breakable_glass); continue; } if (glass) logd("vischeck: blocked by glass surface=%s contents=0x%x", tr.surface.name ? tr.surface.name : "?", tr.contents); return false; } return false; } // Trace the crosshair ray and return the first entity hit (nullptr for world // or no hit). `ignore` is the local player entity pointer. void *trace_entity(const Vector &start, const Vector &end, void *ignore) { if (!g_iface.enginetrace) return nullptr; RayCompat ray; make_ray(ray, start, end); GameTraceCompat tr{}; EntityFilter filter(ignore); using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *, GameTraceCompat *); vfunc(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, MASK_SHOT, &filter, &tr); if (tr.fraction >= 1.0f || tr.startsolid) return nullptr; return tr.ent; } // Vertical distance from the standing player hull's feet to the first // standable surface below. Matches CategorizePosition's 2-unit probe and its // plane.normal.z >= 0.7 rule, but sweeps much farther so the caller can // predict the landing. Returns -1 when there is no usable ground. float ground_distance(const Vector &origin, void *ignore, float max_dist) { if (!g_iface.enginetrace || max_dist <= 0.0f) return -1.0f; RayCompat ray; make_standing_hull_ray(ray, origin, max_dist); GameTraceCompat tr{}; EntityFilter filter(ignore); using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *, GameTraceCompat *); vfunc(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, MASK_PLAYERSOLID, &filter, &tr); if (tr.startsolid || tr.fraction >= 1.0f || tr.normal.z < 0.7f) { static int solid_logs = 0; if (tr.startsolid && solid_logs < 4) { solid_logs++; logd("ground trace: startsolid at (%.1f %.1f %.1f)", origin.x, origin.y, origin.z); } return -1.0f; } return tr.fraction * max_dist; }