From 4019880cf113a2a13460ec8ea5bb30a5c8ed50f3 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 16:11:28 +0100 Subject: Items: hide full ammo pickups; robust close-target projection - Resolve m_iAmmo and m_hMyWeapons arrays from the client recv tables. - "Only if usable" now compares m_iAmmo[type] against sk_max_* (stock caps as fallback: RPG rockets 3, AR2 60, ...). Full ammo hides the pickup, including RPG rockets and fallen weapons converted to ammo. - Inventory now comes from m_hMyWeapons directly, so fallen weapons are ammo before the player equips the matching weapon. - Projection: fall back to local math when ScreenPosition claims "behind" for a point with positive view depth, and build NPC boxes from vertical samples so feet behind the near plane no longer drop the whole target. Log the first 8 near-target projection failures for diagnosis. --- CONTEXT.md | 18 +++-- src/esp.cpp | 236 +++++++++++++++++++++++++++++++++++++++++++++++++------- src/hl2h.h | 2 + src/netvars.cpp | 13 +++- 4 files changed, 233 insertions(+), 36 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index acdab2f..839d340 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -232,7 +232,11 @@ vocabulary / decisions. (>1e7). A point on the view axis (e.g. directly below the player's feet) can make `IVDebugOverlay::ScreenPosition` return NaN/huge values; casting those to int corrupted the ISurface clip state and made the whole overlay - disappear for that frame. + disappear for that frame. If the overlay reports "behind" for a point whose + view-space depth is actually positive, a local projection is used instead. +- NPC boxes are built from several vertical samples, so a target whose feet + are behind the near plane (standing over it) still gets a box from the + visible span instead of being dropped. - Projectile radius rings are drawn independently of the centre marker, so a grenade behind the camera/off-screen still shows its blast circle when any part of the ring projects. @@ -606,10 +610,14 @@ vocabulary / decisions. - A world weapon whose type is already in the local player's inventory is reclassified as ammo (`SMG ammo`, `AR2 ammo`, ...). Owner handles are unreliable on this client (`m_hOwnerEntity`/`m_hOwner` resolve to -1), so - the inventory mask is accumulated from every weapon type the player has - held (client `m_hActiveWeapon`, with the server handle as fallback) since - the current local entity appeared. NPC-held weapons are therefore ignored. - Weapons the player does not own stay under the Weapons category. + the inventory is read directly from the local `m_hMyWeapons[48]` handle + array (and remembered active types as a fallback). NPC-held weapons are + therefore ignored. Weapons the player does not own stay under the Weapons + category. +- Ammo pickups also honour "Only if usable": the reserve count from + `m_iAmmo[type]` is compared with the `sk_max_*` cvar (stock caps as fallback, + e.g. RPG rockets = 3). Full ammo hides the pickup; if either value cannot + be read the item stays visible. - Scans the client entity list every 100 ms and tags health, ammo, armor/suit, world weapon pickups, the two supply-crate models and generic items. - 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 715dbc8..1d4a6a5 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -52,27 +52,11 @@ static double esp_now() { return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; } -bool world_to_screen(const Vector &w, float &sx, float &sy) { - // Preferred: the engine's own transform. Uses the real view setup (FOV, - // aspect, roll), so boxes do not drift at the screen edges. - if (g_iface.debugoverlay) { - using Fn = int (*)(void *, const Vector *, Vector *); - Vector s; - int behind = - vfunc(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s); - if (behind) - return false; - // A point on the view axis (e.g. directly below the feet) can produce - // NaN/inf or enormous coordinates; drawing those as ints corrupts the - // surface clip state and hides the rest of the overlay. - if (!std::isfinite(s.x) || !std::isfinite(s.y) || fabsf(s.x) > 1e7f || - fabsf(s.y) > 1e7f) - return false; - sx = s.x; - sy = s.y; - return true; - } - // Fallback: local math with the default HL2 FOV. +// Local perspective projection used when the engine's debug-overlay +// projection refuses a point that is actually in front of the camera. +static bool project_manual(const Vector &w, float &sx, float &sy) { + if (!g_scr_w || !g_scr_h) + return false; float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; float sp = sinf(pitch), cp = cosf(pitch); @@ -82,7 +66,8 @@ bool world_to_screen(const Vector &w, float &sx, float &sy) { float up[3] = {sp * cy, sp * syy, cp}; float dx = w.x - g_cam_eye.x, dy = w.y - g_cam_eye.y, dz = w.z - g_cam_eye.z; float z = dx * fwd[0] + dy * fwd[1] + dz * fwd[2]; - if (z < 1.0f) return false; + if (z < 1.0f) + return false; float x = dx * right[0] + dy * right[1] + dz * right[2]; float y = dx * up[0] + dy * up[1] + dz * up[2]; float tanfov = tanf(75.0f * 0.5f * PI_F / 180.0f); @@ -92,6 +77,42 @@ bool world_to_screen(const Vector &w, float &sx, float &sy) { fabsf(sy) < 1e7f; } +bool world_to_screen(const Vector &w, float &sx, float &sy) { + // Preferred: the engine's own transform. Uses the real view setup (FOV, + // aspect, roll), so boxes do not drift at the screen edges. + if (g_iface.debugoverlay) { + using Fn = int (*)(void *, const Vector *, Vector *); + Vector s; + int behind = + vfunc(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s); + if (behind) { + // The overlay transform can report "behind" for points that are in + // front but near the view axis/extremes (e.g. something directly below + // the player). Use the local projection in that case. + float pitch = g_cam_view.x * PI_F / 180.0f; + float yaw = g_cam_view.y * PI_F / 180.0f; + float cp = cosf(pitch), sp = sinf(pitch); + float fwd[3] = {cp * cosf(yaw), cp * sinf(yaw), -sp}; + float z = (w.x - g_cam_eye.x) * fwd[0] + + (w.y - g_cam_eye.y) * fwd[1] + + (w.z - g_cam_eye.z) * fwd[2]; + if (z > 4.0f) + return project_manual(w, sx, sy); + return false; + } + // A point on the view axis can produce NaN/inf or enormous coordinates; + // drawing those as ints corrupts the surface clip state and hides the + // rest of the overlay. + if (!std::isfinite(s.x) || !std::isfinite(s.y) || fabsf(s.x) > 1e7f || + fabsf(s.y) > 1e7f) + return false; + sx = s.x; + sy = s.y; + return true; + } + return project_manual(w, sx, sy); +} + // --------------------------------------------------------------------------- // entity helpers (IClientEntityList / IClientNetworkable, header-documented) // --------------------------------------------------------------------------- @@ -431,8 +452,6 @@ static void esp_collect(int local_idx) { } Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), *(float *)(e + g_net.origin + 8)}; - Vector bottom{origin.x, origin.y, origin.z + minz}; - Vector top{origin.x, origin.y, origin.z + maxz}; bool neutral = false; bool friendly = npc && npc_is_friendly(idx, model, local_idx, neutral); // Arrows are collected before projection: a hostile behind the camera @@ -443,12 +462,47 @@ static void esp_collect(int local_idx) { a.head = Vector{origin.x, origin.y, origin.z + maxz * 0.7f}; a.neutral = neutral; } - float sx, sy, tx, ty; - if (!world_to_screen(bottom, sx, sy) || !world_to_screen(top, tx, ty)) + // Project several points along the vertical axis. Requiring both feet and + // head to project dropped the target when the feet were behind the near + // plane (common when standing over/next to something). + float min_sy = 1e9f, max_sy = -1e9f, low_x = 0.0f; + bool projected = false; + const int VSAMPLES = 6; + for (int k = 0; k <= VSAMPLES; k++) { + float z = origin.z + minz + + (maxz - minz) * (float)k / (float)VSAMPLES; + float px, py; + if (!world_to_screen(Vector{origin.x, origin.y, z}, px, py)) + continue; + if (px < -2.0f * (float)g_scr_w || px > 3.0f * (float)g_scr_w) + continue; + projected = true; + if (py < min_sy) + min_sy = py; + if (py > max_sy) { + max_sy = py; + low_x = px; + } + } + if (!projected) { + static int fail_logs = 0; + float ddx = origin.x - g_cam_eye.x; + float ddy = origin.y - g_cam_eye.y; + float ddz = origin.z - g_cam_eye.z; + if (fail_logs < 8 && ddx * ddx + ddy * ddy + ddz * ddz < 400.0f * 400.0f) { + fail_logs++; + logf("ESP: close target %d model=%s failed projection at " + "(%.1f %.1f %.1f)", + idx, model ? model : "?", origin.x, origin.y, origin.z); + } + continue; + } + float h = max_sy - min_sy; + if (h < 3.0f || max_sy < 0 || min_sy > (float)g_scr_h) + continue; // behind / offscreen + if (low_x < -(float)g_scr_w || low_x > 2.0f * (float)g_scr_w) continue; - float h = sy - ty; - if (h < 3.0f || sy < 0 || ty > (float)g_scr_h) continue; // behind / offscreen - if (sx < -(float)g_scr_w || sx > 2.0f * (float)g_scr_w) continue; + float sx = low_x, sy = max_sy; EspTarget &t = g_targets[g_ntargets++]; t.idx = idx; @@ -2088,6 +2142,7 @@ enum ItemKind { struct ItemTarget { int idx; int kind; + int ammo_type; // m_iAmmo index for IK_AMMO, -1 otherwise Vector origin; char label[128]; }; @@ -2235,6 +2290,117 @@ static int weapon_type_id(const char *model, const char *cls) { return WT_NONE; } +static int weapon_ammo_type(int wt) { + switch (wt) { + case WT_PISTOL: return 2; + case WT_357: return 4; + case WT_SMG: return 3; + case WT_AR2: return 0; + case WT_SHOTGUN: return 6; + case WT_CROSSBOW: return 5; + case WT_RPG: return 7; + case WT_GRENADE: return 11; + case WT_SNIPER: return 9; + default: return -1; + } +} + +// Ammo type index used by CBasePlayer::m_iAmmo (order of AddAmmoType in +// hl2_gamerules.cpp). +static int item_ammo_type(const char *model) { + if (!model) + return -1; + if (item_icontains(model, "boxsrounds") || + item_icontains(model, "ammocrate_pistol") || + item_icontains(model, "w_pistol")) + return 2; + if (item_icontains(model, "boxmrounds") || + item_icontains(model, "ammocrate_smg1")) + return 3; + if (item_icontains(model, "boxlrounds") || + item_icontains(model, "combine_rifle") || + item_icontains(model, "ammocrate_ar2") || + item_icontains(model, "w_ar2") || item_icontains(model, "w_irifle")) + return 0; + if (item_icontains(model, "357ammo") || + item_icontains(model, "357ammobox") || + item_icontains(model, "w_357")) + return 4; + if (item_icontains(model, "crossbowrounds") || + item_icontains(model, "w_crossbow")) + return 5; + if (item_icontains(model, "boxbuckshot") || + item_icontains(model, "ammocrate_buckshot") || + item_icontains(model, "w_shotgun")) + return 6; + if (item_icontains(model, "w_missile_closed") || + item_icontains(model, "ammocrate_rockets") || + item_icontains(model, "w_rocket_launcher")) + return 7; + if (item_icontains(model, "ar2_grenade") || + item_icontains(model, "ammocrate_smg2")) + return 8; + if (item_icontains(model, "ammocrate_grenade") || + item_icontains(model, "w_grenade") || item_icontains(model, "w_frag")) + return 11; + if (item_icontains(model, "w_smg1") || item_icontains(model, "w_smg2")) + return 3; + return -1; +} + +static int convar_int(const char *name) { + if (!g_iface.cvar || !name) + return -1; + using FindFn = void *(*)(void *, const char *); + void *v = vfunc(g_iface.cvar, 12)(g_iface.cvar, name); + if (!v) + return -1; + return (int)*(float *)((uintptr_t)v + 0x2c); // ConVar m_Value.f +} + +// Maximum reserve ammo for an m_iAmmo index. Prefers the sk_max_* cvar and +// falls back to the stock HL2 cap so the RPG's 3-rocket limit is honoured +// even when server cvars are not visible from the client. +static int ammo_max(int type) { + static int cache[16]; + static bool init = false; + if (type < 0 || type >= 16) + return -1; + if (!init) { + for (int i = 0; i < 16; i++) + cache[i] = -2; + init = true; + } + if (cache[type] != -2) + return cache[type]; + const char *name = nullptr; + int stock = -1; + switch (type) { + case 0: name = "sk_max_ar2"; stock = 60; break; + case 2: name = "sk_max_pistol"; stock = 150; break; + case 3: name = "sk_max_smg1"; stock = 225; break; + case 4: name = "sk_max_357"; stock = 12; break; + case 5: name = "sk_max_crossbow"; stock = 10; break; + case 6: name = "sk_max_buckshot"; stock = 30; break; + case 7: name = "sk_max_rpg_round"; stock = 3; break; + case 8: name = "sk_max_smg1_grenade"; stock = 3; break; + case 9: name = "sk_max_sniper_round"; stock = 30; break; + case 11: name = "sk_max_grenade"; stock = 5; break; + default: break; + } + int v = convar_int(name); + if (v <= 0) + v = stock; + cache[type] = v; + return v; +} + +static int local_ammo(void *local_ent, int type) { + if (!local_ent || g_net.ammo < 0 || type < 0 || type >= 32) + return -1; + return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4); +} + static int classify_item(const char *model, const char *cls, char *label, size_t outn) { if (!model && !cls) @@ -2535,6 +2701,12 @@ static void items_collect(int local_idx) { ItemTarget &t = g_item_targets[g_nitems++]; t.idx = idx; t.kind = kind; + t.ammo_type = -1; + if (kind == IK_AMMO) { + t.ammo_type = item_ammo_type(model); + if (t.ammo_type < 0) + t.ammo_type = weapon_ammo_type(weapon_type_id(model, cls)); + } t.origin = origin; snprintf(t.label, sizeof(t.label), "%s", label); if (dbg_lines < 8) { @@ -2605,6 +2777,12 @@ void esp_draw_items() { continue; if (t.kind == IK_ARMOR && vit_armor >= 0 && vit_armor >= 100) continue; + if (t.kind == IK_AMMO && t.ammo_type >= 0) { + int maxa = ammo_max(t.ammo_type); + int cura = local_ammo(entitylist_get(local), t.ammo_type); + if (maxa > 0 && cura >= 0 && cura >= maxa) + continue; + } } float sx, sy; if (!world_to_screen(t.origin, sx, sy)) diff --git a/src/hl2h.h b/src/hl2h.h index 0c8ffb5..4f3552e 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -100,6 +100,8 @@ struct NetVars { int punch = -1; // client m_vecPunchAngle (aim punch) int default_fov = -1; // client m_iDefaultFOV (camera fov) int dmg_radius = -1; // client C_BaseGrenade::m_DmgRadius + int my_weapons = -1; // client m_hMyWeapons[48] array base + int ammo = -1; // client m_iAmmo[] array base // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100. diff --git a/src/netvars.cpp b/src/netvars.cpp index ef564cd..2747c29 100644 --- a/src/netvars.cpp +++ b/src/netvars.cpp @@ -112,6 +112,15 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { int off = (int)(base_off + (uint32_t)poff); if (off >= 0x20 && off <= 0x2000) g_net.dmg_radius = off; + } else if (strcmp(pname2, "m_hMyWeapons") == 0 && + g_net.my_weapons < 0) { + int off = (int)(base_off + (uint32_t)poff); + if (off >= 0x20 && off <= 0x2000) + g_net.my_weapons = off; + } else if (strcmp(pname2, "m_iAmmo") == 0 && g_net.ammo < 0) { + int off = (int)(base_off + (uint32_t)poff); + if (off >= 0x20 && off <= 0x2000) + g_net.ammo = off; } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) { g_net.teamnum = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) { @@ -294,11 +303,11 @@ void resolve_netvars() { "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d " "modelindex=%d absvelocity=%d activeweapon=%d owner=%d " "weapon_owner=%d clr_render=%d punch=%d default_fov=%d " - "dmg_radius=%d", + "dmg_radius=%d my_weapons=%d ammo=%d", g_net.flags, g_net.velocity, g_net.movetype, g_net.health, g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex, g_net.absvelocity, g_net.activeweapon, g_net.owner, g_net.weapon_owner, g_net.clr_render, g_net.punch, - g_net.default_fov, g_net.dmg_radius); + g_net.default_fov, g_net.dmg_radius, g_net.my_weapons, g_net.ammo); } -- cgit v1.2.3