diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 16:01:36 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 16:01:36 +0100 |
| commit | 606a5ca110574974861d31d35a10a53419c0e8c1 (patch) | |
| tree | 20d1bde49f1ee4930bffc97e72ba6075199fe6b4 | |
| parent | Items: fallen owned weapons count as ammo; fuse diagnostics (diff) | |
| download | hl2h-606a5ca110574974861d31d35a10a53419c0e8c1.tar.gz hl2h-606a5ca110574974861d31d35a10a53419c0e8c1.zip | |
Fix overlay-vanishing projections; ammo rule via held weapons
- world_to_screen now rejects NaN/inf and absurd projections. A point on the
view axis (directly below the feet) could produce NaN and corrupt the
ISurface clip state, hiding the whole ESP for that frame.
- Draw projectile radius rings even when the projectile centre is off-screen
or behind the camera.
- Owner netvars are -1 on this client, so the "owned weapon -> ammo" rule now
accumulates the weapon types the player has actually held (active weapon,
server fallback), reset when the local entity changes.
| -rw-r--r-- | CONTEXT.md | 18 | ||||
| -rw-r--r-- | src/esp.cpp | 92 |
2 files changed, 77 insertions, 33 deletions
| @@ -228,6 +228,14 @@ vocabulary / decisions. | |||
| 228 | a chevron clamped to a 26 px margin; behind-the-camera targets use the | 228 | a chevron clamped to a 26 px margin; behind-the-camera targets use the |
| 229 | dot product with the view right vector. Toggle `ESP -> Off-screen arrows` | 229 | dot product with the view right vector. Toggle `ESP -> Off-screen arrows` |
| 230 | (`arrows=1`). | 230 | (`arrows=1`). |
| 231 | - `world_to_screen` rejects non-finite projections and absurd magnitudes | ||
| 232 | (>1e7). A point on the view axis (e.g. directly below the player's feet) | ||
| 233 | can make `IVDebugOverlay::ScreenPosition` return NaN/huge values; casting | ||
| 234 | those to int corrupted the ISurface clip state and made the whole overlay | ||
| 235 | disappear for that frame. | ||
| 236 | - Projectile radius rings are drawn independently of the centre marker, so a | ||
| 237 | grenade behind the camera/off-screen still shows its blast circle when any | ||
| 238 | part of the ring projects. | ||
| 231 | - Map transitions: see below. | 239 | - Map transitions: see below. |
| 232 | 240 | ||
| 233 | ## Camera / display | 241 | ## Camera / display |
| @@ -595,10 +603,12 @@ vocabulary / decisions. | |||
| 595 | HL2 `MAX_NORMAL_BATTERY` cap of 100, read from the server | 603 | HL2 `MAX_NORMAL_BATTERY` cap of 100, read from the server |
| 596 | `m_ArmorValue`. If the values cannot be read the filter fails open. | 604 | `m_ArmorValue`. If the values cannot be read the filter fails open. |
| 597 | - A world weapon whose type is already in the local player's inventory is | 605 | - A world weapon whose type is already in the local player's inventory is |
| 598 | reclassified as ammo (`SMG ammo`, `AR2 ammo`, ...). Inventory types come | 606 | reclassified as ammo (`SMG ammo`, `AR2 ammo`, ...). Owner handles are |
| 599 | from client weapon entities whose owner handle resolves to the local player | 607 | unreliable on this client (`m_hOwnerEntity`/`m_hOwner` resolve to -1), so |
| 600 | index; NPC-held weapons are ignored. Weapons the player does not own stay | 608 | the inventory mask is accumulated from every weapon type the player has |
| 601 | under the Weapons category. | 609 | held (client `m_hActiveWeapon`, with the server handle as fallback) since |
| 610 | the current local entity appeared. NPC-held weapons are therefore ignored. | ||
| 611 | Weapons the player does not own stay under the Weapons category. | ||
| 602 | - Scans the client entity list every 100 ms and tags health, ammo, armor/suit, | 612 | - Scans the client entity list every 100 ms and tags health, ammo, armor/suit, |
| 603 | world weapon pickups, the two supply-crate models and generic items. | 613 | world weapon pickups, the two supply-crate models and generic items. |
| 604 | - Each tag is a colored marker with a name and optional distance, filtered by | 614 | - Each tag is a colored marker with a name and optional distance, filtered by |
diff --git a/src/esp.cpp b/src/esp.cpp index e273bf6..447bee7 100644 --- a/src/esp.cpp +++ b/src/esp.cpp | |||
| @@ -62,6 +62,12 @@ bool world_to_screen(const Vector &w, float &sx, float &sy) { | |||
| 62 | vfunc<Fn>(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s); | 62 | vfunc<Fn>(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s); |
| 63 | if (behind) | 63 | if (behind) |
| 64 | return false; | 64 | return false; |
| 65 | // A point on the view axis (e.g. directly below the feet) can produce | ||
| 66 | // NaN/inf or enormous coordinates; drawing those as ints corrupts the | ||
| 67 | // surface clip state and hides the rest of the overlay. | ||
| 68 | if (!std::isfinite(s.x) || !std::isfinite(s.y) || fabsf(s.x) > 1e7f || | ||
| 69 | fabsf(s.y) > 1e7f) | ||
| 70 | return false; | ||
| 65 | sx = s.x; | 71 | sx = s.x; |
| 66 | sy = s.y; | 72 | sy = s.y; |
| 67 | return true; | 73 | return true; |
| @@ -82,7 +88,8 @@ bool world_to_screen(const Vector &w, float &sx, float &sy) { | |||
| 82 | float tanfov = tanf(75.0f * 0.5f * PI_F / 180.0f); | 88 | float tanfov = tanf(75.0f * 0.5f * PI_F / 180.0f); |
| 83 | sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov); | 89 | sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov); |
| 84 | sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov); | 90 | sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov); |
| 85 | return true; | 91 | return std::isfinite(sx) && std::isfinite(sy) && fabsf(sx) < 1e7f && |
| 92 | fabsf(sy) < 1e7f; | ||
| 86 | } | 93 | } |
| 87 | 94 | ||
| 88 | // --------------------------------------------------------------------------- | 95 | // --------------------------------------------------------------------------- |
| @@ -2346,8 +2353,30 @@ static void items_collect(int local_idx) { | |||
| 2346 | int active_idx = local_active_weapon_idx(local_idx); | 2353 | int active_idx = local_active_weapon_idx(local_idx); |
| 2347 | 2354 | ||
| 2348 | // Which weapon types the local player already carries. A fallen weapon of | 2355 | // Which weapon types the local player already carries. A fallen weapon of |
| 2349 | // the same type is then just an ammo pickup. | 2356 | // the same type is then just an ammo pickup. Owner handles are unreliable on |
| 2350 | bool owned_weapon[WT_COUNT] = {}; | 2357 | // this client (owner/weapon_owner netvars resolve to -1), so remember every |
| 2358 | // type the player has held since the current local entity appeared. | ||
| 2359 | static uint32_t owned_mask = 0; | ||
| 2360 | static void *last_local = nullptr; | ||
| 2361 | void *local_ent = entitylist_get(local_idx); | ||
| 2362 | if (local_ent != last_local) { | ||
| 2363 | last_local = local_ent; | ||
| 2364 | owned_mask = 0; | ||
| 2365 | } | ||
| 2366 | int owned_idx = active_idx; | ||
| 2367 | if (owned_idx <= 0 && g_iface.engineserver) { | ||
| 2368 | resolve_server_netvars(); | ||
| 2369 | owned_idx = server_active_weapon(local_idx); | ||
| 2370 | } | ||
| 2371 | if (owned_idx > 0) { | ||
| 2372 | void *aw = entitylist_get(owned_idx); | ||
| 2373 | void *anet = entity_networkable(owned_idx); | ||
| 2374 | const char *acls = anet ? entity_class_name(anet, nullptr) : nullptr; | ||
| 2375 | const char *amodel = aw ? entity_model_name(aw) : nullptr; | ||
| 2376 | int at = weapon_type_id(amodel, acls); | ||
| 2377 | if (at > 0) | ||
| 2378 | owned_mask |= 1u << at; | ||
| 2379 | } | ||
| 2351 | for (int idx = 1; idx <= hi; idx++) { | 2380 | for (int idx = 1; idx <= hi; idx++) { |
| 2352 | if (idx == local_idx) | 2381 | if (idx == local_idx) |
| 2353 | continue; | 2382 | continue; |
| @@ -2359,8 +2388,11 @@ static void items_collect(int local_idx) { | |||
| 2359 | const char *model = entity_model_name(ent); | 2388 | const char *model = entity_model_name(ent); |
| 2360 | int wt = weapon_type_id(model, cls); | 2389 | int wt = weapon_type_id(model, cls); |
| 2361 | if (wt > 0) | 2390 | if (wt > 0) |
| 2362 | owned_weapon[wt] = true; | 2391 | owned_mask |= 1u << wt; |
| 2363 | } | 2392 | } |
| 2393 | bool owned_weapon[WT_COUNT] = {}; | ||
| 2394 | for (int t = 1; t < WT_COUNT; t++) | ||
| 2395 | owned_weapon[t] = (owned_mask & (1u << t)) != 0; | ||
| 2364 | static bool cats_logged = false; | 2396 | static bool cats_logged = false; |
| 2365 | if (!cats_logged) { | 2397 | if (!cats_logged) { |
| 2366 | cats_logged = true; | 2398 | cats_logged = true; |
| @@ -2706,12 +2738,6 @@ void esp_draw_projectiles() { | |||
| 2706 | projs_collect(engine_get_local_player()); | 2738 | projs_collect(engine_get_local_player()); |
| 2707 | for (int i = 0; i < g_nprojs; i++) { | 2739 | for (int i = 0; i < g_nprojs; i++) { |
| 2708 | const ProjTarget &p = g_projs[i]; | 2740 | const ProjTarget &p = g_projs[i]; |
| 2709 | float sx, sy; | ||
| 2710 | if (!world_to_screen(p.origin, sx, sy)) | ||
| 2711 | continue; | ||
| 2712 | if (sx < -40 || sx > (float)g_scr_w + 40 || sy < -40 || | ||
| 2713 | sy > (float)g_scr_h + 40) | ||
| 2714 | continue; | ||
| 2715 | int r = 255, g = 170, b = 60; | 2741 | int r = 255, g = 170, b = 60; |
| 2716 | if (p.kind == PK_RPG) { | 2742 | if (p.kind == PK_RPG) { |
| 2717 | r = 255; g = 120; b = 40; | 2743 | r = 255; g = 120; b = 40; |
| @@ -2722,25 +2748,33 @@ void esp_draw_projectiles() { | |||
| 2722 | } else if (p.fuse >= 0.0f && p.fuse < 0.8f) { | 2748 | } else if (p.fuse >= 0.0f && p.fuse < 0.8f) { |
| 2723 | r = 255; g = 60; b = 60; | 2749 | r = 255; g = 60; b = 60; |
| 2724 | } | 2750 | } |
| 2725 | int x = (int)sx, y = (int)sy; | 2751 | float sx, sy; |
| 2726 | surf_color(r, g, b, 255); | 2752 | bool center_ok = world_to_screen(p.origin, sx, sy) && |
| 2727 | surf_rect(0, x - 4, y - 4, x + 4, y + 4); | 2753 | sx >= -40.0f && sx <= (float)g_scr_w + 40.0f && |
| 2728 | surf_rect(1, x - 1, y - 1, x + 2, y + 2); | 2754 | sy >= -40.0f && sy <= (float)g_scr_h + 40.0f; |
| 2729 | char text[64]; | 2755 | if (center_ok) { |
| 2730 | if (g_proj_fuse && p.fuse >= 0.0f) | 2756 | int x = (int)sx, y = (int)sy; |
| 2731 | snprintf(text, sizeof(text), "%s %.1fs", proj_name(p.kind), p.fuse); | 2757 | surf_color(r, g, b, 255); |
| 2732 | else | 2758 | surf_rect(0, x - 4, y - 4, x + 4, y + 4); |
| 2733 | snprintf(text, sizeof(text), "%s", proj_name(p.kind)); | 2759 | surf_rect(1, x - 1, y - 1, x + 2, y + 2); |
| 2734 | int tx = x + 9; | 2760 | char text[64]; |
| 2735 | if (tx > g_scr_w - 80) | 2761 | if (g_proj_fuse && p.fuse >= 0.0f) |
| 2736 | tx = x - 9 - (int)strlen(text) * 6; | 2762 | snprintf(text, sizeof(text), "%s %.1fs", proj_name(p.kind), p.fuse); |
| 2737 | if (tx < 4) | 2763 | else |
| 2738 | tx = 4; | 2764 | snprintf(text, sizeof(text), "%s", proj_name(p.kind)); |
| 2739 | int ty = y - 14; | 2765 | int tx = x + 9; |
| 2740 | if (ty < 4) | 2766 | if (tx > g_scr_w - 80) |
| 2741 | ty = 4; | 2767 | tx = x - 9 - (int)strlen(text) * 6; |
| 2742 | draw_text_small_col(tx + 1, ty + 1, text, 0, 0, 0); | 2768 | if (tx < 4) |
| 2743 | draw_text_small_col(tx, ty, text, r, g, b); | 2769 | tx = 4; |
| 2770 | int ty = y - 14; | ||
| 2771 | if (ty < 4) | ||
| 2772 | ty = 4; | ||
| 2773 | draw_text_small_col(tx + 1, ty + 1, text, 0, 0, 0); | ||
| 2774 | draw_text_small_col(tx, ty, text, r, g, b); | ||
| 2775 | } | ||
| 2776 | // The ring is useful even when the grenade itself is behind the camera or | ||
| 2777 | // off-screen, so it is not gated on center_ok. | ||
| 2744 | if (g_proj_radius && p.radius > 1.0f) { | 2778 | if (g_proj_radius && p.radius > 1.0f) { |
| 2745 | const int SEG = 24; | 2779 | const int SEG = 24; |
| 2746 | float lx = 0.0f, ly = 0.0f; | 2780 | float lx = 0.0f, ly = 0.0f; |
