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/server.cpp | 66 +++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 12 deletions(-) (limited to 'src/server.cpp') 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