From 80a287910f3f77519c8243ba5f53d9c877fbddcb Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 23:32:00 +0100 Subject: Charger juice: resolve per-class offsets CWallHealth derives CBaseToggle and CNewRecharge derives CBaseAnimating, so m_iJuice is at different offsets (0x3c8 and 0x47c here). Collect every m_iJuice datamap match, derive the suit offset from m_iMaxJuice-20, and pick by the server class name. Also detect health brushes via wallhealth/recharge rather than the ESP label. Verified the offsets resolve distinctly on :99. --- src/esp.cpp | 12 ++++++++++- src/server.cpp | 66 +++++++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/esp.cpp b/src/esp.cpp index 594a8fa..dbe0051 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -2655,6 +2655,16 @@ static bool item_kind_enabled(int kind) { } } +// Health chargers are brushes (func_healthcharger/CWallHealth) and suit +// chargers are item_suitcharger/CNewRecharge. Both expose m_iJuice. +static bool item_is_charger(const char *model, const char *cls) { + return item_icontains(model, "charger") || + item_icontains(model, "suit_charger") || + item_icontains(cls, "charger") || + item_icontains(cls, "wallhealth") || + item_icontains(cls, "recharge"); +} + static void items_collect(int local_idx) { static double last_scan = 0.0; double now = esp_now(); @@ -2784,7 +2794,7 @@ static void items_collect(int local_idx) { // Health/suit chargers are dispensers that stay useful after a refill, but // an empty one cannot give anything. m_iJuice is 0 when depleted (until it // recharges); only hide the tag once the server value is readable. - if (strcasestr(label, "charger") && server_item_juice(idx) == 0) + if (item_is_charger(model, cls) && server_item_juice(idx) == 0) continue; Vector origin{*(float *)((uintptr_t)ent + g_net.origin), diff --git a/src/server.cpp b/src/server.cpp index 6479b25..f69ab30 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -58,7 +58,8 @@ static int g_srv_mapname = -1; static int g_srv_armor = -1; static int g_srv_detonate = -1; static int g_srv_grenade_radius = -1; -static int g_srv_juice = -1; +static int g_srv_juice_suit = -1; +static int g_srv_juice_health = -1; static int g_srv_ammo = -1; static int g_srv_myweapons = -1; static bool g_srv_ready = false; @@ -79,12 +80,17 @@ static uintptr_t find_string(const char *s) { // Find the typedescription_t whose fieldName points at `name` and read its // fieldOffset. type filters false positives (m_iHealth is FIELD_INTEGER). // max_off is generous on purpose: derived-class fields (m_bHackedByAlyx, -// m_hFollowTarget) live well past the base class size. -static int datamap_offset(const char *name, int want_type, int min_off = 0x40, - int max_off = 0x4000) { +// m_hFollowTarget) live well past the base class size. A name can occur in +// several classes (m_iJuice is in both CWallHealth and CNewRecharge), so the +// collector returns every distinct offset. +static int datamap_offsets(const char *name, int want_type, int min_off, + int max_off, int *out, int maxn) { + if (!out || maxn <= 0) + return 0; uintptr_t str = find_string(name); if (!str) - return -1; + return 0; + int n = 0; for (auto &r : g_server.ranges) { const uint8_t *base = (const uint8_t *)r.lo; size_t span = r.hi - r.lo; @@ -106,10 +112,22 @@ static int datamap_offset(const char *name, int want_type, int min_off = 0x40, // DEFINE_FIELD entries have none. Either way it must be in server.so. if (ext && !g_server.contains(ext)) continue; - return foff; + bool dup = false; + for (int i = 0; i < n; i++) + if (out[i] == foff) + dup = true; + if (!dup && n < maxn) + out[n++] = foff; } } - return -1; + return n; +} + +static int datamap_offset(const char *name, int want_type, int min_off = 0x40, + int max_off = 0x4000) { + int out[1]; + return datamap_offsets(name, want_type, min_off, max_off, out, 1) ? out[0] + : -1; } bool resolve_server_netvars() { @@ -163,7 +181,22 @@ bool resolve_server_netvars() { // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked). g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME); g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT); - g_srv_juice = datamap_offset("m_iJuice", FIELD_INTEGER); + int juice_offs[8]; + int njuice = datamap_offsets("m_iJuice", FIELD_INTEGER, 0x40, 0x4000, + juice_offs, 8); + int maxjuice = datamap_offset("m_iMaxJuice", FIELD_INTEGER); + // CNewRecharge (suit charger) has m_iMaxJuice five ints after m_iJuice; + // CWallHealth (health charger) has no m_iMaxJuice, so its offset is the + // other match. + g_srv_juice_suit = maxjuice > 20 ? maxjuice - 20 : -1; + g_srv_juice_health = -1; + for (int i = 0; i < njuice; i++) + if (juice_offs[i] != g_srv_juice_suit) { + g_srv_juice_health = juice_offs[i]; + break; + } + if (g_srv_juice_health < 0 && njuice) + g_srv_juice_health = juice_offs[0]; // CBaseCombatCharacter arrays: ammo counts and the weapon handle list. g_srv_ammo = datamap_offset("m_iAmmo", FIELD_INTEGER, 0x40, 0x4000); g_srv_myweapons = datamap_offset("m_hMyWeapons", FIELD_EHANDLE); @@ -173,13 +206,13 @@ bool resolve_server_netvars() { "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " "followtarget=%d spawnflags=%d hacked=%d npcstate=%d " "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d " - "ammo=%d myweapons=%d juice=%d", + "ammo=%d myweapons=%d juice_suit=%d juice_health=%d", g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage, g_srv_mapname, g_srv_armor, g_srv_detonate, g_srv_grenade_radius, - g_srv_ammo, g_srv_myweapons, g_srv_juice); + g_srv_ammo, g_srv_myweapons, g_srv_juice_suit, g_srv_juice_health); fail_logged = false; } else if (!fail_logged) { fail_logged = true; @@ -480,13 +513,22 @@ float server_grenade_radius(int idx) { // CWallHealth / CNewRecharge share the same m_iJuice layout (the fields before // it match), so one datamap offset works for both health and suit chargers. int server_item_juice(int idx) { - if (!g_srv_ready || g_srv_juice < 0) + if (!g_srv_ready) + return -1; + int off = g_srv_juice_health; + const char *cls = server_entity_classname(idx); + if (cls && (strstr(cls, "Recharge") || strstr(cls, "SuitCharger") || + strstr(cls, "suitcharger"))) + off = g_srv_juice_suit; + if (off < 0) + off = g_srv_juice_suit >= 0 ? g_srv_juice_suit : g_srv_juice_health; + if (off < 0) return -1; void *base = nullptr; server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1; - return *(int *)((uintptr_t)base + g_srv_juice); + return *(int *)((uintptr_t)base + off); } // Server-side CBaseCombatCharacter::m_iAmmo array (reserve ammo). The datamap -- cgit v1.2.3