From 2e9373d530bca779b4070a79972f246ef41e4038 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 3 Nov 2026 22:03:01 +0100 Subject: 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. --- CONTEXT.md | 11 +++++- src/esp.cpp | 6 +-- src/gui.cpp | 106 +++++++++++++++++++++++++++++++++++++++------------ src/hl2h.h | 3 +- src/vischeck.cpp | 113 +++++++++++++++++++++++++++++++++++++++++++------------ 5 files changed, 184 insertions(+), 55 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 5f27572..922ea98 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -288,10 +288,17 @@ vocabulary / decisions. mask. This caused active-weapon lookups to produce bogus indices after a map change once the weapon serial was nonzero (melee killbot looked dead). - `ISurface` binary slot = SDK header slot + 4; see `src/ISURFACE_STATUS.md`. +- GUI font handles are validated with `GetTextSize` and recreated when the + local player entity or UI scale changes; if the scaled size fails, 16 px is + used as a fallback. VGUI can silently invalidate font handles across level + transitions. - Silent aim and ABH temporarily replace the usercmd view; silent aim preserves movement, ABH intentionally zeroes it for the jump tick. -- Aimbot vischeck (`MASK_VISIBLE`/world-only) differs from triggerbot - (`MASK_SHOT`/all entities): glass and static props can behave differently. +- Aimbot/silent/melee vischeck now traces everything bullets care about + (`MASK_SHOT`) while ignoring the local player and target. Static props, + crates and other NPCs block. Non-solid glass and breakable-glass materials + are skipped; solid non-break glass (bulletproof) blocks. Triggerbot still + uses the same `MASK_SHOT` trace and will not fire through glass. ## Valuable items 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, continue; if (f >= best) continue; - if (vischeck && !is_visible(g_cam_eye, pts[i])) + if (vischeck && !is_visible(g_cam_eye, pts[i], local_ent, ent)) continue; best = f; best_pt = pts[i]; @@ -1061,7 +1061,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) { float pd = vec_dist(g_cam_eye, pts[i]); if (pd > MELEE_RANGE || pd >= bestd) continue; - if (!is_visible(g_cam_eye, pts[i])) + if (!is_visible(g_cam_eye, pts[i], local_ent, tent)) continue; bestd = pd; aim = pts[i]; @@ -1083,7 +1083,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) { if (ad > MELEE_RANGE) aim = c.closest; } - if (!is_visible(g_cam_eye, aim)) + if (!is_visible(g_cam_eye, aim, local_ent, tent)) continue; } 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; static int g_body_h = 15; static int g_title_h = 20; static float g_font_scale = 0.0f; +static void *g_font_local = (void *)-1; +static bool g_font_fail_logged = false; static int g_scr_w = 0, g_scr_h = 0; static void s_color(int r, int g, int b, int a) { @@ -485,41 +487,97 @@ bool gui_toasts_active() { return active; } -static void ensure_fonts() { - if (g_font_body && fabsf(g_font_scale - g_scale) < 0.001f) - return; - g_font_scale = g_scale; - g_body_h = (int)(15.0f * g_scale + 0.5f); - g_title_h = (int)(20.0f * g_scale + 0.5f); - if (g_body_h < 11) g_body_h = 11; - if (g_title_h < 15) g_title_h = 15; +static bool font_valid(unsigned long f) { + if (!f || !g_surface) + return false; + int w = 0, h = 0; + s_text_size(f, L"M", w, h); + return w > 0 && h > 0; +} + +static void gui_reset_fonts() { + g_font_body = 0; + g_font_title = 0; + g_font_scale = 0.0f; +} + +static unsigned long create_gui_font(int tall) { static const char *families[] = {"Liberation Sans", "DejaVu Sans", "Noto Sans", "Tahoma", "Verdana", "Arial", nullptr}; + for (int i = 0; families[i]; i++) { + unsigned long f = s_create_font(); + if (!f) + break; + if (s_set_font(f, families[i], tall, 400, 0x400, 0x20, 0xFF) && + font_valid(f)) + return f; + } + return 0; +} + +static void ensure_fonts() { + void *le = nullptr; + if (g_iface.engine && g_iface.entitylist) { + int local = engine_get_local_player(); + if (local > 0) + le = entitylist_get(local); + } + if (le != g_font_local) { + g_font_local = le; + gui_reset_fonts(); + } + + bool scale_changed = fabsf(g_font_scale - g_scale) >= 0.001f; + if (g_font_body && g_font_title && !scale_changed && + font_valid(g_font_body) && font_valid(g_font_title)) + return; + // Level transitions can silently invalidate VGUI font handles. Recreate + // them instead of drawing invisible text. + if ((g_font_body && !font_valid(g_font_body)) || + (g_font_title && !font_valid(g_font_title))) + gui_reset_fonts(); + if (scale_changed) + gui_reset_fonts(); + + g_font_scale = g_scale; + int req_body = (int)(15.0f * g_scale + 0.5f); + int req_title = (int)(20.0f * g_scale + 0.5f); + if (req_body < 11) req_body = 11; + if (req_title < 15) req_title = 15; + g_body_h = req_body; + g_title_h = req_title; + + if (!g_font_body) + g_font_body = create_gui_font(g_body_h); + if (!g_font_title) + g_font_title = create_gui_font(g_title_h); + + // Small scaled sizes can fail in the font manager; 16 px is the proven ESP + // size, so use it as a readable fallback. if (!g_font_body) { - for (int i = 0; families[i] && !g_font_body; i++) { - unsigned long f = s_create_font(); - if (!f) - break; - if (s_set_font(f, families[i], g_body_h, 400, 0x400, 0x20, 0xFF)) - g_font_body = f; - } + g_body_h = 16; + g_font_body = create_gui_font(g_body_h); } if (!g_font_title) { - for (int i = 0; families[i] && !g_font_title; i++) { - unsigned long f = s_create_font(); - if (!f) - break; - if (s_set_font(f, families[i], g_title_h, 400, 0x400, 0x20, 0xFF)) - g_font_title = f; - } + g_title_h = 16; + g_font_title = create_gui_font(g_title_h); } if (!g_font_title) g_font_title = g_font_body; + if (!g_font_body) { - logf("gui: no usable body font"); - toastf(TOAST_ERROR, "No usable GUI font"); + if (!g_font_fail_logged) { + g_font_fail_logged = true; + logf("gui: no usable body font"); + toastf(TOAST_ERROR, "No usable GUI font"); + } + return; } + g_font_fail_logged = false; + if (g_body_h != req_body || g_title_h != req_title) + logd("gui: font fallback body=%d title=%d (requested %d/%d)", g_body_h, + g_title_h, req_body, req_title); } static int text_w(const char *s, int tall) { diff --git a/src/hl2h.h b/src/hl2h.h index 9c0440f..45a297e 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -272,7 +272,8 @@ void esp_draw(); void esp_draw_items(); bool world_to_screen(const Vector &w, float &sx, float &sy); void set_silent_restore(const QAngle &angles); -bool is_visible(const Vector &eye, const Vector &point); +bool is_visible(const Vector &eye, const Vector &point, + void *self = nullptr, void *target = nullptr); void *trace_entity(const Vector &start, const Vector &end, void *ignore); float ground_distance(const Vector &origin, void *ignore, float max_dist); 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 @@ -// vischeck.cpp — world-only line-of-sight test via EngineTraceClient003. +// vischeck.cpp — line-of-sight test via EngineTraceClient003. // // IEngineTrace::TraceRay (vtable slot 4) is given a ray from the eye to the -// aim point and a TRACE_WORLD_ONLY filter. MASK_VISIBLE ignores glass/windows -// (you can see through them) while still hitting opaque world geometry. -// Per the SDK, TRACE_WORLD_ONLY does not test static props; that is the -// deliberate trade-off for this build. +// 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 { @@ -75,11 +79,6 @@ struct ITraceFilterCompat { virtual int GetTraceType() const = 0; }; -struct WorldOnlyFilter : ITraceFilterCompat { - bool ShouldHitEntity(void *, int) override { return false; } - int GetTraceType() const override { return 1; } // TRACE_WORLD_ONLY -}; - struct EntityFilter : ITraceFilterCompat { void *ignore; explicit EntityFilter(void *ig) : ignore(ig) {} @@ -89,15 +88,24 @@ struct EntityFilter : ITraceFilterCompat { int GetTraceType() const override { return 0; } // TRACE_EVERYTHING }; -// MASK_VISIBLE = CONTENTS_SOLID | CONTENTS_MOVEABLE | CONTENTS_OPAQUE | -// CONTENTS_IGNORE_NODRAW_OPAQUE -constexpr unsigned MASK_VISIBLE = 0x1u | 0x4000u | 0x80u | 0x2000u; +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 | 0x4u | +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 | 0x4u | 0x2000000u | 0x8u; + 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; @@ -127,7 +135,26 @@ static void make_standing_hull_ray(RayCompat &ray, const Vector &origin, } // namespace -bool is_visible(const Vector &eye, const Vector &point) { +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") || surface_contains(surf, "window"); +} + +bool is_visible(const Vector &eye, const Vector &point, void *self, + void *target) { static bool warned = false; if (!g_iface.enginetrace) { if (!warned) { @@ -137,15 +164,51 @@ bool is_visible(const Vector &eye, const Vector &point) { return true; } - RayCompat ray; - make_ray(ray, eye, point); - GameTraceCompat tr{}; - WorldOnlyFilter filter; - using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *, - GameTraceCompat *); - vfunc(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, - MASK_VISIBLE, &filter, &tr); - return tr.fraction > 0.97f && !tr.startsolid; + 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; + } + return false; + } + return false; } // Trace the crosshair ray and return the first entity hit (nullptr for world -- cgit v1.2.3