diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-03 22:03:01 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-03 22:03:01 +0100 |
| commit | 2e9373d530bca779b4070a79972f246ef41e4038 (patch) | |
| tree | cc263d92cdcf7c9befd29f36da3cf04ed0b3a7f4 | |
| parent | Handles: use the 11-bit networked EHANDLE entity index (diff) | |
| download | hl2h-2e9373d530bca779b4070a79972f246ef41e4038.tar.gz hl2h-2e9373d530bca779b4070a79972f246ef41e4038.zip | |
GUI: recover invalid fonts; vischeck: props and bulletproof glass
- Validate the GUI font handles with GetTextSize and recreate them when the
local player entity or UI scale changes. Small scaled sizes fall back to the
proven 16 px size, and the failure log/toast is emitted once instead of every
frame. This fixes sessions where the menu rendered with no text after a
level transition.
- Rework visibility: trace with MASK_SHOT/TRACE_EVERYTHING while ignoring the
local player and target, so static props, crates and other NPCs block.
Non-solid glass and breakable-glass materials are skipped; solid non-break
glass (bulletproof) blocks. Fix CONTENTS_WINDOW 0x2 (was 0x4) in the masks.
- Pass self/target to is_visible from aimbot/silent/melee callers.
| -rw-r--r-- | CONTEXT.md | 11 | ||||
| -rw-r--r-- | src/esp.cpp | 6 | ||||
| -rw-r--r-- | src/gui.cpp | 106 | ||||
| -rw-r--r-- | src/hl2h.h | 3 | ||||
| -rw-r--r-- | src/vischeck.cpp | 113 |
5 files changed, 184 insertions, 55 deletions
| @@ -288,10 +288,17 @@ vocabulary / decisions. | |||
| 288 | mask. This caused active-weapon lookups to produce bogus indices after a | 288 | mask. This caused active-weapon lookups to produce bogus indices after a |
| 289 | map change once the weapon serial was nonzero (melee killbot looked dead). | 289 | map change once the weapon serial was nonzero (melee killbot looked dead). |
| 290 | - `ISurface` binary slot = SDK header slot + 4; see `src/ISURFACE_STATUS.md`. | 290 | - `ISurface` binary slot = SDK header slot + 4; see `src/ISURFACE_STATUS.md`. |
| 291 | - GUI font handles are validated with `GetTextSize` and recreated when the | ||
| 292 | local player entity or UI scale changes; if the scaled size fails, 16 px is | ||
| 293 | used as a fallback. VGUI can silently invalidate font handles across level | ||
| 294 | transitions. | ||
| 291 | - Silent aim and ABH temporarily replace the usercmd view; silent aim preserves | 295 | - Silent aim and ABH temporarily replace the usercmd view; silent aim preserves |
| 292 | movement, ABH intentionally zeroes it for the jump tick. | 296 | movement, ABH intentionally zeroes it for the jump tick. |
| 293 | - Aimbot vischeck (`MASK_VISIBLE`/world-only) differs from triggerbot | 297 | - Aimbot/silent/melee vischeck now traces everything bullets care about |
| 294 | (`MASK_SHOT`/all entities): glass and static props can behave differently. | 298 | (`MASK_SHOT`) while ignoring the local player and target. Static props, |
| 299 | crates and other NPCs block. Non-solid glass and breakable-glass materials | ||
| 300 | are skipped; solid non-break glass (bulletproof) blocks. Triggerbot still | ||
| 301 | uses the same `MASK_SHOT` trace and will not fire through glass. | ||
| 295 | 302 | ||
| 296 | ## Valuable items | 303 | ## Valuable items |
| 297 | 304 | ||
diff --git a/src/esp.cpp b/src/esp.cpp index 3db25e6..5683704 100644 --- a/src/esp.cpp +++ b/src/esp.cpp | |||
| @@ -541,7 +541,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, | |||
| 541 | continue; | 541 | continue; |
| 542 | if (f >= best) | 542 | if (f >= best) |
| 543 | continue; | 543 | continue; |
| 544 | if (vischeck && !is_visible(g_cam_eye, pts[i])) | 544 | if (vischeck && !is_visible(g_cam_eye, pts[i], local_ent, ent)) |
| 545 | continue; | 545 | continue; |
| 546 | best = f; | 546 | best = f; |
| 547 | best_pt = pts[i]; | 547 | best_pt = pts[i]; |
| @@ -1061,7 +1061,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) { | |||
| 1061 | float pd = vec_dist(g_cam_eye, pts[i]); | 1061 | float pd = vec_dist(g_cam_eye, pts[i]); |
| 1062 | if (pd > MELEE_RANGE || pd >= bestd) | 1062 | if (pd > MELEE_RANGE || pd >= bestd) |
| 1063 | continue; | 1063 | continue; |
| 1064 | if (!is_visible(g_cam_eye, pts[i])) | 1064 | if (!is_visible(g_cam_eye, pts[i], local_ent, tent)) |
| 1065 | continue; | 1065 | continue; |
| 1066 | bestd = pd; | 1066 | bestd = pd; |
| 1067 | aim = pts[i]; | 1067 | aim = pts[i]; |
| @@ -1083,7 +1083,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) { | |||
| 1083 | if (ad > MELEE_RANGE) | 1083 | if (ad > MELEE_RANGE) |
| 1084 | aim = c.closest; | 1084 | aim = c.closest; |
| 1085 | } | 1085 | } |
| 1086 | if (!is_visible(g_cam_eye, aim)) | 1086 | if (!is_visible(g_cam_eye, aim, local_ent, tent)) |
| 1087 | continue; | 1087 | continue; |
| 1088 | } | 1088 | } |
| 1089 | 1089 | ||
diff --git a/src/gui.cpp b/src/gui.cpp index b512ef3..417860f 100644 --- a/src/gui.cpp +++ b/src/gui.cpp | |||
| @@ -39,6 +39,8 @@ static unsigned long g_font_title = 0; | |||
| 39 | static int g_body_h = 15; | 39 | static int g_body_h = 15; |
| 40 | static int g_title_h = 20; | 40 | static int g_title_h = 20; |
| 41 | static float g_font_scale = 0.0f; | 41 | static float g_font_scale = 0.0f; |
| 42 | static void *g_font_local = (void *)-1; | ||
| 43 | static bool g_font_fail_logged = false; | ||
| 42 | static int g_scr_w = 0, g_scr_h = 0; | 44 | static int g_scr_w = 0, g_scr_h = 0; |
| 43 | 45 | ||
| 44 | static void s_color(int r, int g, int b, int a) { | 46 | static void s_color(int r, int g, int b, int a) { |
| @@ -485,41 +487,97 @@ bool gui_toasts_active() { | |||
| 485 | return active; | 487 | return active; |
| 486 | } | 488 | } |
| 487 | 489 | ||
| 488 | static void ensure_fonts() { | 490 | static bool font_valid(unsigned long f) { |
| 489 | if (g_font_body && fabsf(g_font_scale - g_scale) < 0.001f) | 491 | if (!f || !g_surface) |
| 490 | return; | 492 | return false; |
| 491 | g_font_scale = g_scale; | 493 | int w = 0, h = 0; |
| 492 | g_body_h = (int)(15.0f * g_scale + 0.5f); | 494 | s_text_size(f, L"M", w, h); |
| 493 | g_title_h = (int)(20.0f * g_scale + 0.5f); | 495 | return w > 0 && h > 0; |
| 494 | if (g_body_h < 11) g_body_h = 11; | 496 | } |
| 495 | if (g_title_h < 15) g_title_h = 15; | 497 | |
| 498 | static void gui_reset_fonts() { | ||
| 499 | g_font_body = 0; | ||
| 500 | g_font_title = 0; | ||
| 501 | g_font_scale = 0.0f; | ||
| 502 | } | ||
| 503 | |||
| 504 | static unsigned long create_gui_font(int tall) { | ||
| 496 | static const char *families[] = {"Liberation Sans", "DejaVu Sans", | 505 | static const char *families[] = {"Liberation Sans", "DejaVu Sans", |
| 497 | "Noto Sans", "Tahoma", "Verdana", "Arial", | 506 | "Noto Sans", "Tahoma", "Verdana", "Arial", |
| 498 | nullptr}; | 507 | nullptr}; |
| 508 | for (int i = 0; families[i]; i++) { | ||
| 509 | unsigned long f = s_create_font(); | ||
| 510 | if (!f) | ||
| 511 | break; | ||
| 512 | if (s_set_font(f, families[i], tall, 400, 0x400, 0x20, 0xFF) && | ||
| 513 | font_valid(f)) | ||
| 514 | return f; | ||
| 515 | } | ||
| 516 | return 0; | ||
| 517 | } | ||
| 518 | |||
| 519 | static void ensure_fonts() { | ||
| 520 | void *le = nullptr; | ||
| 521 | if (g_iface.engine && g_iface.entitylist) { | ||
| 522 | int local = engine_get_local_player(); | ||
| 523 | if (local > 0) | ||
| 524 | le = entitylist_get(local); | ||
| 525 | } | ||
| 526 | if (le != g_font_local) { | ||
| 527 | g_font_local = le; | ||
| 528 | gui_reset_fonts(); | ||
| 529 | } | ||
| 530 | |||
| 531 | bool scale_changed = fabsf(g_font_scale - g_scale) >= 0.001f; | ||
| 532 | if (g_font_body && g_font_title && !scale_changed && | ||
| 533 | font_valid(g_font_body) && font_valid(g_font_title)) | ||
| 534 | return; | ||
| 535 | // Level transitions can silently invalidate VGUI font handles. Recreate | ||
| 536 | // them instead of drawing invisible text. | ||
| 537 | if ((g_font_body && !font_valid(g_font_body)) || | ||
| 538 | (g_font_title && !font_valid(g_font_title))) | ||
| 539 | gui_reset_fonts(); | ||
| 540 | if (scale_changed) | ||
| 541 | gui_reset_fonts(); | ||
| 542 | |||
| 543 | g_font_scale = g_scale; | ||
| 544 | int req_body = (int)(15.0f * g_scale + 0.5f); | ||
| 545 | int req_title = (int)(20.0f * g_scale + 0.5f); | ||
| 546 | if (req_body < 11) req_body = 11; | ||
| 547 | if (req_title < 15) req_title = 15; | ||
| 548 | g_body_h = req_body; | ||
| 549 | g_title_h = req_title; | ||
| 550 | |||
| 551 | if (!g_font_body) | ||
| 552 | g_font_body = create_gui_font(g_body_h); | ||
| 553 | if (!g_font_title) | ||
| 554 | g_font_title = create_gui_font(g_title_h); | ||
| 555 | |||
| 556 | // Small scaled sizes can fail in the font manager; 16 px is the proven ESP | ||
| 557 | // size, so use it as a readable fallback. | ||
| 499 | if (!g_font_body) { | 558 | if (!g_font_body) { |
| 500 | for (int i = 0; families[i] && !g_font_body; i++) { | 559 | g_body_h = 16; |
| 501 | unsigned long f = s_create_font(); | 560 | g_font_body = create_gui_font(g_body_h); |
| 502 | if (!f) | ||
| 503 | break; | ||
| 504 | if (s_set_font(f, families[i], g_body_h, 400, 0x400, 0x20, 0xFF)) | ||
| 505 | g_font_body = f; | ||
| 506 | } | ||
| 507 | } | 561 | } |
| 508 | if (!g_font_title) { | 562 | if (!g_font_title) { |
| 509 | for (int i = 0; families[i] && !g_font_title; i++) { | 563 | g_title_h = 16; |
| 510 | unsigned long f = s_create_font(); | 564 | g_font_title = create_gui_font(g_title_h); |
| 511 | if (!f) | ||
| 512 | break; | ||
| 513 | if (s_set_font(f, families[i], g_title_h, 400, 0x400, 0x20, 0xFF)) | ||
| 514 | g_font_title = f; | ||
| 515 | } | ||
| 516 | } | 565 | } |
| 517 | if (!g_font_title) | 566 | if (!g_font_title) |
| 518 | g_font_title = g_font_body; | 567 | g_font_title = g_font_body; |
| 568 | |||
| 519 | if (!g_font_body) { | 569 | if (!g_font_body) { |
| 520 | logf("gui: no usable body font"); | 570 | if (!g_font_fail_logged) { |
| 521 | toastf(TOAST_ERROR, "No usable GUI font"); | 571 | g_font_fail_logged = true; |
| 572 | logf("gui: no usable body font"); | ||
| 573 | toastf(TOAST_ERROR, "No usable GUI font"); | ||
| 574 | } | ||
| 575 | return; | ||
| 522 | } | 576 | } |
| 577 | g_font_fail_logged = false; | ||
| 578 | if (g_body_h != req_body || g_title_h != req_title) | ||
| 579 | logd("gui: font fallback body=%d title=%d (requested %d/%d)", g_body_h, | ||
| 580 | g_title_h, req_body, req_title); | ||
| 523 | } | 581 | } |
| 524 | 582 | ||
| 525 | static int text_w(const char *s, int tall) { | 583 | static int text_w(const char *s, int tall) { |
| @@ -272,7 +272,8 @@ void esp_draw(); | |||
| 272 | void esp_draw_items(); | 272 | void esp_draw_items(); |
| 273 | bool world_to_screen(const Vector &w, float &sx, float &sy); | 273 | bool world_to_screen(const Vector &w, float &sx, float &sy); |
| 274 | void set_silent_restore(const QAngle &angles); | 274 | void set_silent_restore(const QAngle &angles); |
| 275 | bool is_visible(const Vector &eye, const Vector &point); | 275 | bool is_visible(const Vector &eye, const Vector &point, |
| 276 | void *self = nullptr, void *target = nullptr); | ||
| 276 | void *trace_entity(const Vector &start, const Vector &end, void *ignore); | 277 | void *trace_entity(const Vector &start, const Vector &end, void *ignore); |
| 277 | float ground_distance(const Vector &origin, void *ignore, float max_dist); | 278 | float ground_distance(const Vector &origin, void *ignore, float max_dist); |
| 278 | 279 | ||
diff --git a/src/vischeck.cpp b/src/vischeck.cpp index dcb5c3c..a568f31 100644 --- a/src/vischeck.cpp +++ b/src/vischeck.cpp | |||
| @@ -1,14 +1,18 @@ | |||
| 1 | // vischeck.cpp — world-only line-of-sight test via EngineTraceClient003. | 1 | // vischeck.cpp — line-of-sight test via EngineTraceClient003. |
| 2 | // | 2 | // |
| 3 | // IEngineTrace::TraceRay (vtable slot 4) is given a ray from the eye to the | 3 | // IEngineTrace::TraceRay (vtable slot 4) is given a ray from the eye to the |
| 4 | // aim point and a TRACE_WORLD_ONLY filter. MASK_VISIBLE ignores glass/windows | 4 | // aim point with TRACE_EVERYTHING/MASK_SHOT, ignoring the local player and the |
| 5 | // (you can see through them) while still hitting opaque world geometry. | 5 | // target entity. This catches world geometry, static props and temporary |
| 6 | // Per the SDK, TRACE_WORLD_ONLY does not test static props; that is the | 6 | // objects such as crates/NPCs. Glass is skipped only when it is non-solid or |
| 7 | // deliberate trade-off for this build. | 7 | // its material name marks it breakable; solid non-break glass (bulletproof) |
| 8 | // blocks the check. | ||
| 8 | 9 | ||
| 9 | #include "hl2h.h" | 10 | #include "hl2h.h" |
| 11 | #include <cmath> | ||
| 10 | #include <cstddef> | 12 | #include <cstddef> |
| 11 | #include <cstdint> | 13 | #include <cstdint> |
| 14 | #include <cstring> | ||
| 15 | #include <strings.h> | ||
| 12 | 16 | ||
| 13 | namespace { | 17 | namespace { |
| 14 | 18 | ||
| @@ -75,11 +79,6 @@ struct ITraceFilterCompat { | |||
| 75 | virtual int GetTraceType() const = 0; | 79 | virtual int GetTraceType() const = 0; |
| 76 | }; | 80 | }; |
| 77 | 81 | ||
| 78 | struct WorldOnlyFilter : ITraceFilterCompat { | ||
| 79 | bool ShouldHitEntity(void *, int) override { return false; } | ||
| 80 | int GetTraceType() const override { return 1; } // TRACE_WORLD_ONLY | ||
| 81 | }; | ||
| 82 | |||
| 83 | struct EntityFilter : ITraceFilterCompat { | 82 | struct EntityFilter : ITraceFilterCompat { |
| 84 | void *ignore; | 83 | void *ignore; |
| 85 | explicit EntityFilter(void *ig) : ignore(ig) {} | 84 | explicit EntityFilter(void *ig) : ignore(ig) {} |
| @@ -89,15 +88,24 @@ struct EntityFilter : ITraceFilterCompat { | |||
| 89 | int GetTraceType() const override { return 0; } // TRACE_EVERYTHING | 88 | int GetTraceType() const override { return 0; } // TRACE_EVERYTHING |
| 90 | }; | 89 | }; |
| 91 | 90 | ||
| 92 | // MASK_VISIBLE = CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_OPAQUE | | 91 | struct VisibilityFilter : ITraceFilterCompat { |
| 93 | // CONTENTS_IGNORE_NODRAW_OPAQUE | 92 | void *self; |
| 94 | constexpr unsigned MASK_VISIBLE = 0x1u | 0x4000u | 0x80u | 0x2000u; | 93 | void *target; |
| 94 | VisibilityFilter(void *s, void *t) : self(s), target(t) {} | ||
| 95 | bool ShouldHitEntity(void *pEntity, int) override { | ||
| 96 | return pEntity != self && pEntity != target; | ||
| 97 | } | ||
| 98 | int GetTraceType() const override { return 0; } // TRACE_EVERYTHING | ||
| 99 | }; | ||
| 100 | |||
| 101 | constexpr unsigned CONTENTS_SOLID = 0x1u; | ||
| 102 | constexpr unsigned CONTENTS_WINDOW = 0x2u; | ||
| 95 | // MASK_SHOT = SOLID|MOVEABLE|MONSTER|WINDOW|DEBRIS|HITBOX | 103 | // MASK_SHOT = SOLID|MOVEABLE|MONSTER|WINDOW|DEBRIS|HITBOX |
| 96 | constexpr unsigned MASK_SHOT = 0x1u | 0x4000u | 0x2000000u | 0x4u | | 104 | constexpr unsigned MASK_SHOT = 0x1u | 0x4000u | 0x2000000u | 0x2u | |
| 97 | 0x4000000u | 0x40000000u; | 105 | 0x4000000u | 0x40000000u; |
| 98 | // MASK_PLAYERSOLID = SOLID|MOVEABLE|PLAYERCLIP|WINDOW|MONSTER|GRATE | 106 | // MASK_PLAYERSOLID = SOLID|MOVEABLE|PLAYERCLIP|WINDOW|MONSTER|GRATE |
| 99 | constexpr unsigned MASK_PLAYERSOLID = | 107 | constexpr unsigned MASK_PLAYERSOLID = |
| 100 | 0x1u | 0x4000u | 0x10000u | 0x4u | 0x2000000u | 0x8u; | 108 | 0x1u | 0x4000u | 0x10000u | 0x2u | 0x2000000u | 0x8u; |
| 101 | 109 | ||
| 102 | static void make_ray(RayCompat &ray, const Vector &start, const Vector &end) { | 110 | static void make_ray(RayCompat &ray, const Vector &start, const Vector &end) { |
| 103 | float dx = end.x - start.x, dy = end.y - start.y, dz = end.z - start.z; | 111 | float dx = end.x - start.x, dy = end.y - start.y, dz = end.z - start.z; |
| @@ -127,7 +135,26 @@ static void make_standing_hull_ray(RayCompat &ray, const Vector &origin, | |||
| 127 | 135 | ||
| 128 | } // namespace | 136 | } // namespace |
| 129 | 137 | ||
| 130 | bool is_visible(const Vector &eye, const Vector &point) { | 138 | static bool surface_contains(const SurfaceCompat &surf, const char *needle) { |
| 139 | if (!surf.name || !needle) | ||
| 140 | return false; | ||
| 141 | size_t n = strlen(needle); | ||
| 142 | for (const char *p = surf.name; *p; p++) | ||
| 143 | if (strncasecmp(p, needle, n) == 0) | ||
| 144 | return true; | ||
| 145 | return false; | ||
| 146 | } | ||
| 147 | |||
| 148 | static bool surface_is_breakable_glass(const SurfaceCompat &surf) { | ||
| 149 | return surface_contains(surf, "break"); | ||
| 150 | } | ||
| 151 | |||
| 152 | static bool surface_is_glass(const SurfaceCompat &surf) { | ||
| 153 | return surface_contains(surf, "glass") || surface_contains(surf, "window"); | ||
| 154 | } | ||
| 155 | |||
| 156 | bool is_visible(const Vector &eye, const Vector &point, void *self, | ||
| 157 | void *target) { | ||
| 131 | static bool warned = false; | 158 | static bool warned = false; |
| 132 | if (!g_iface.enginetrace) { | 159 | if (!g_iface.enginetrace) { |
| 133 | if (!warned) { | 160 | if (!warned) { |
| @@ -137,15 +164,51 @@ bool is_visible(const Vector &eye, const Vector &point) { | |||
| 137 | return true; | 164 | return true; |
| 138 | } | 165 | } |
| 139 | 166 | ||
| 140 | RayCompat ray; | 167 | float dx = point.x - eye.x; |
| 141 | make_ray(ray, eye, point); | 168 | float dy = point.y - eye.y; |
| 142 | GameTraceCompat tr{}; | 169 | float dz = point.z - eye.z; |
| 143 | WorldOnlyFilter filter; | 170 | float len2 = dx * dx + dy * dy + dz * dz; |
| 144 | using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *, | 171 | if (len2 < 0.0001f) |
| 145 | GameTraceCompat *); | 172 | return true; |
| 146 | vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, | 173 | float inv_len = 1.0f / sqrtf(len2); |
| 147 | MASK_VISIBLE, &filter, &tr); | 174 | Vector dir{dx * inv_len, dy * inv_len, dz * inv_len}; |
| 148 | return tr.fraction > 0.97f && !tr.startsolid; | 175 | |
| 176 | // Trace everything that bullets care about: world, static props, crates and | ||
| 177 | // NPCs. Non-solid glass and glass whose material name marks it breakable is | ||
| 178 | // skipped; solid non-break glass (bulletproof) blocks. | ||
| 179 | Vector start = eye; | ||
| 180 | for (int pass = 0; pass < 5; pass++) { | ||
| 181 | RayCompat ray; | ||
| 182 | make_ray(ray, start, point); | ||
| 183 | GameTraceCompat tr{}; | ||
| 184 | VisibilityFilter filter(self, target); | ||
| 185 | using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *, | ||
| 186 | GameTraceCompat *); | ||
| 187 | vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, | ||
| 188 | MASK_SHOT, &filter, &tr); | ||
| 189 | if (tr.startsolid || tr.allsolid) | ||
| 190 | return false; | ||
| 191 | if (tr.fraction >= 1.0f) | ||
| 192 | return true; | ||
| 193 | |||
| 194 | bool glass = (tr.contents & CONTENTS_WINDOW) != 0 || | ||
| 195 | surface_is_glass(tr.surface); | ||
| 196 | bool solid = (tr.contents & CONTENTS_SOLID) != 0; | ||
| 197 | bool breakable_glass = glass && surface_is_breakable_glass(tr.surface); | ||
| 198 | if (glass && (!solid || breakable_glass)) { | ||
| 199 | // Step just past the pane and look for the next obstruction. | ||
| 200 | start = tr.endpos; | ||
| 201 | start.x += dir.x * 0.5f; | ||
| 202 | start.y += dir.y * 0.5f; | ||
| 203 | start.z += dir.z * 0.5f; | ||
| 204 | logd("vischeck: skipped glass surface=%s contents=0x%x solid=%d break=%d", | ||
| 205 | tr.surface.name ? tr.surface.name : "?", tr.contents, (int)solid, | ||
| 206 | (int)breakable_glass); | ||
| 207 | continue; | ||
| 208 | } | ||
| 209 | return false; | ||
| 210 | } | ||
| 211 | return false; | ||
| 149 | } | 212 | } |
| 150 | 213 | ||
| 151 | // Trace the crosshair ray and return the first entity hit (nullptr for world | 214 | // Trace the crosshair ray and return the first entity hit (nullptr for world |
