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. --- src/esp.cpp | 236 ++++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 207 insertions(+), 29 deletions(-) (limited to 'src/esp.cpp') 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)) -- cgit v1.2.3