diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 23:32:00 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 23:32:00 +0100 |
| commit | 80a287910f3f77519c8243ba5f53d9c877fbddcb (patch) | |
| tree | 761c4b0ca3ab857cd57a886dd6e9c4048bca950d | |
| parent | Add grenade trajectory, RPG armour priority, empty chargers, mega gravity gun (diff) | |
| download | hl2h-80a287910f3f77519c8243ba5f53d9c877fbddcb.tar.gz hl2h-80a287910f3f77519c8243ba5f53d9c877fbddcb.zip | |
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.
| -rw-r--r-- | CONTEXT.md | 9 | ||||
| -rw-r--r-- | src/esp.cpp | 12 | ||||
| -rw-r--r-- | src/server.cpp | 66 |
3 files changed, 71 insertions, 16 deletions
| @@ -703,9 +703,12 @@ vocabulary / decisions. | |||
| 703 | buckshot count 30 against a max of 3, permanently hiding rockets). | 703 | buckshot count 30 against a max of 3, permanently hiding rockets). |
| 704 | - Chargers and ammo/supply crates are dispensers and are exempt from the | 704 | - Chargers and ammo/supply crates are dispensers and are exempt from the |
| 705 | fullness filter. Empty health/suit chargers are still hidden: both | 705 | fullness filter. Empty health/suit chargers are still hidden: both |
| 706 | `CWallHealth` and `CNewRecharge` expose `m_iJuice` (server datamap | 706 | `CWallHealth` (health brush) and `CNewRecharge` (suit charger) expose |
| 707 | `FIELD_INTEGER`, resolved at offset 0x47c in this build), and a value of 0 | 707 | `m_iJuice`, but at different offsets because their bases differ |
| 708 | removes the charger tag until it recharges. `-1` (unreadable) fails open. | 708 | (`CBaseToggle` vs `CBaseAnimating`). `datamap_offsets` collects every match; |
| 709 | the suit offset is `m_iMaxJuice - 20` and the health offset is the other | ||
| 710 | `m_iJuice` match (0x47c / 0x3c8 in this build). `server_item_juice` picks | ||
| 711 | by the server class name. 0 removes the tag; -1 (unreadable) fails open. | ||
| 709 | - Scans the client entity list every 100 ms and tags health, ammo, armor/suit, | 712 | - Scans the client entity list every 100 ms and tags health, ammo, armor/suit, |
| 710 | world weapon pickups, the two supply-crate models and generic items. | 713 | world weapon pickups, the two supply-crate models and generic items. |
| 711 | - Each tag is a colored marker with a name and optional distance, filtered by | 714 | - 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 594a8fa..dbe0051 100644 --- a/src/esp.cpp +++ b/src/esp.cpp | |||
| @@ -2655,6 +2655,16 @@ static bool item_kind_enabled(int kind) { | |||
| 2655 | } | 2655 | } |
| 2656 | } | 2656 | } |
| 2657 | 2657 | ||
| 2658 | // Health chargers are brushes (func_healthcharger/CWallHealth) and suit | ||
| 2659 | // chargers are item_suitcharger/CNewRecharge. Both expose m_iJuice. | ||
| 2660 | static bool item_is_charger(const char *model, const char *cls) { | ||
| 2661 | return item_icontains(model, "charger") || | ||
| 2662 | item_icontains(model, "suit_charger") || | ||
| 2663 | item_icontains(cls, "charger") || | ||
| 2664 | item_icontains(cls, "wallhealth") || | ||
| 2665 | item_icontains(cls, "recharge"); | ||
| 2666 | } | ||
| 2667 | |||
| 2658 | static void items_collect(int local_idx) { | 2668 | static void items_collect(int local_idx) { |
| 2659 | static double last_scan = 0.0; | 2669 | static double last_scan = 0.0; |
| 2660 | double now = esp_now(); | 2670 | double now = esp_now(); |
| @@ -2784,7 +2794,7 @@ static void items_collect(int local_idx) { | |||
| 2784 | // Health/suit chargers are dispensers that stay useful after a refill, but | 2794 | // Health/suit chargers are dispensers that stay useful after a refill, but |
| 2785 | // an empty one cannot give anything. m_iJuice is 0 when depleted (until it | 2795 | // an empty one cannot give anything. m_iJuice is 0 when depleted (until it |
| 2786 | // recharges); only hide the tag once the server value is readable. | 2796 | // recharges); only hide the tag once the server value is readable. |
| 2787 | if (strcasestr(label, "charger") && server_item_juice(idx) == 0) | 2797 | if (item_is_charger(model, cls) && server_item_juice(idx) == 0) |
| 2788 | continue; | 2798 | continue; |
| 2789 | 2799 | ||
| 2790 | Vector origin{*(float *)((uintptr_t)ent + g_net.origin), | 2800 | 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; | |||
| 58 | static int g_srv_armor = -1; | 58 | static int g_srv_armor = -1; |
| 59 | static int g_srv_detonate = -1; | 59 | static int g_srv_detonate = -1; |
| 60 | static int g_srv_grenade_radius = -1; | 60 | static int g_srv_grenade_radius = -1; |
| 61 | static int g_srv_juice = -1; | 61 | static int g_srv_juice_suit = -1; |
| 62 | static int g_srv_juice_health = -1; | ||
| 62 | static int g_srv_ammo = -1; | 63 | static int g_srv_ammo = -1; |
| 63 | static int g_srv_myweapons = -1; | 64 | static int g_srv_myweapons = -1; |
| 64 | static bool g_srv_ready = false; | 65 | static bool g_srv_ready = false; |
| @@ -79,12 +80,17 @@ static uintptr_t find_string(const char *s) { | |||
| 79 | // Find the typedescription_t whose fieldName points at `name` and read its | 80 | // Find the typedescription_t whose fieldName points at `name` and read its |
| 80 | // fieldOffset. type filters false positives (m_iHealth is FIELD_INTEGER). | 81 | // fieldOffset. type filters false positives (m_iHealth is FIELD_INTEGER). |
| 81 | // max_off is generous on purpose: derived-class fields (m_bHackedByAlyx, | 82 | // max_off is generous on purpose: derived-class fields (m_bHackedByAlyx, |
| 82 | // m_hFollowTarget) live well past the base class size. | 83 | // m_hFollowTarget) live well past the base class size. A name can occur in |
| 83 | static int datamap_offset(const char *name, int want_type, int min_off = 0x40, | 84 | // several classes (m_iJuice is in both CWallHealth and CNewRecharge), so the |
| 84 | int max_off = 0x4000) { | 85 | // collector returns every distinct offset. |
| 86 | static int datamap_offsets(const char *name, int want_type, int min_off, | ||
| 87 | int max_off, int *out, int maxn) { | ||
| 88 | if (!out || maxn <= 0) | ||
| 89 | return 0; | ||
| 85 | uintptr_t str = find_string(name); | 90 | uintptr_t str = find_string(name); |
| 86 | if (!str) | 91 | if (!str) |
| 87 | return -1; | 92 | return 0; |
| 93 | int n = 0; | ||
| 88 | for (auto &r : g_server.ranges) { | 94 | for (auto &r : g_server.ranges) { |
| 89 | const uint8_t *base = (const uint8_t *)r.lo; | 95 | const uint8_t *base = (const uint8_t *)r.lo; |
| 90 | size_t span = r.hi - r.lo; | 96 | 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, | |||
| 106 | // DEFINE_FIELD entries have none. Either way it must be in server.so. | 112 | // DEFINE_FIELD entries have none. Either way it must be in server.so. |
| 107 | if (ext && !g_server.contains(ext)) | 113 | if (ext && !g_server.contains(ext)) |
| 108 | continue; | 114 | continue; |
| 109 | return foff; | 115 | bool dup = false; |
| 116 | for (int i = 0; i < n; i++) | ||
| 117 | if (out[i] == foff) | ||
| 118 | dup = true; | ||
| 119 | if (!dup && n < maxn) | ||
| 120 | out[n++] = foff; | ||
| 110 | } | 121 | } |
| 111 | } | 122 | } |
| 112 | return -1; | 123 | return n; |
| 124 | } | ||
| 125 | |||
| 126 | static int datamap_offset(const char *name, int want_type, int min_off = 0x40, | ||
| 127 | int max_off = 0x4000) { | ||
| 128 | int out[1]; | ||
| 129 | return datamap_offsets(name, want_type, min_off, max_off, out, 1) ? out[0] | ||
| 130 | : -1; | ||
| 113 | } | 131 | } |
| 114 | 132 | ||
| 115 | bool resolve_server_netvars() { | 133 | bool resolve_server_netvars() { |
| @@ -163,7 +181,22 @@ bool resolve_server_netvars() { | |||
| 163 | // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked). | 181 | // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked). |
| 164 | g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME); | 182 | g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME); |
| 165 | g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT); | 183 | g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT); |
| 166 | g_srv_juice = datamap_offset("m_iJuice", FIELD_INTEGER); | 184 | int juice_offs[8]; |
| 185 | int njuice = datamap_offsets("m_iJuice", FIELD_INTEGER, 0x40, 0x4000, | ||
| 186 | juice_offs, 8); | ||
| 187 | int maxjuice = datamap_offset("m_iMaxJuice", FIELD_INTEGER); | ||
| 188 | // CNewRecharge (suit charger) has m_iMaxJuice five ints after m_iJuice; | ||
| 189 | // CWallHealth (health charger) has no m_iMaxJuice, so its offset is the | ||
| 190 | // other match. | ||
| 191 | g_srv_juice_suit = maxjuice > 20 ? maxjuice - 20 : -1; | ||
| 192 | g_srv_juice_health = -1; | ||
| 193 | for (int i = 0; i < njuice; i++) | ||
| 194 | if (juice_offs[i] != g_srv_juice_suit) { | ||
| 195 | g_srv_juice_health = juice_offs[i]; | ||
| 196 | break; | ||
| 197 | } | ||
| 198 | if (g_srv_juice_health < 0 && njuice) | ||
| 199 | g_srv_juice_health = juice_offs[0]; | ||
| 167 | // CBaseCombatCharacter arrays: ammo counts and the weapon handle list. | 200 | // CBaseCombatCharacter arrays: ammo counts and the weapon handle list. |
| 168 | g_srv_ammo = datamap_offset("m_iAmmo", FIELD_INTEGER, 0x40, 0x4000); | 201 | g_srv_ammo = datamap_offset("m_iAmmo", FIELD_INTEGER, 0x40, 0x4000); |
| 169 | g_srv_myweapons = datamap_offset("m_hMyWeapons", FIELD_EHANDLE); | 202 | g_srv_myweapons = datamap_offset("m_hMyWeapons", FIELD_EHANDLE); |
| @@ -173,13 +206,13 @@ bool resolve_server_netvars() { | |||
| 173 | "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " | 206 | "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " |
| 174 | "followtarget=%d spawnflags=%d hacked=%d npcstate=%d " | 207 | "followtarget=%d spawnflags=%d hacked=%d npcstate=%d " |
| 175 | "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d " | 208 | "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d " |
| 176 | "ammo=%d myweapons=%d juice=%d", | 209 | "ammo=%d myweapons=%d juice_suit=%d juice_health=%d", |
| 177 | g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, | 210 | g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, |
| 178 | g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, | 211 | g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, |
| 179 | g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, | 212 | g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, |
| 180 | g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage, | 213 | g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage, |
| 181 | g_srv_mapname, g_srv_armor, g_srv_detonate, g_srv_grenade_radius, | 214 | g_srv_mapname, g_srv_armor, g_srv_detonate, g_srv_grenade_radius, |
| 182 | g_srv_ammo, g_srv_myweapons, g_srv_juice); | 215 | g_srv_ammo, g_srv_myweapons, g_srv_juice_suit, g_srv_juice_health); |
| 183 | fail_logged = false; | 216 | fail_logged = false; |
| 184 | } else if (!fail_logged) { | 217 | } else if (!fail_logged) { |
| 185 | fail_logged = true; | 218 | fail_logged = true; |
| @@ -480,13 +513,22 @@ float server_grenade_radius(int idx) { | |||
| 480 | // CWallHealth / CNewRecharge share the same m_iJuice layout (the fields before | 513 | // CWallHealth / CNewRecharge share the same m_iJuice layout (the fields before |
| 481 | // it match), so one datamap offset works for both health and suit chargers. | 514 | // it match), so one datamap offset works for both health and suit chargers. |
| 482 | int server_item_juice(int idx) { | 515 | int server_item_juice(int idx) { |
| 483 | if (!g_srv_ready || g_srv_juice < 0) | 516 | if (!g_srv_ready) |
| 517 | return -1; | ||
| 518 | int off = g_srv_juice_health; | ||
| 519 | const char *cls = server_entity_classname(idx); | ||
| 520 | if (cls && (strstr(cls, "Recharge") || strstr(cls, "SuitCharger") || | ||
| 521 | strstr(cls, "suitcharger"))) | ||
| 522 | off = g_srv_juice_suit; | ||
| 523 | if (off < 0) | ||
| 524 | off = g_srv_juice_suit >= 0 ? g_srv_juice_suit : g_srv_juice_health; | ||
| 525 | if (off < 0) | ||
| 484 | return -1; | 526 | return -1; |
| 485 | void *base = nullptr; | 527 | void *base = nullptr; |
| 486 | server_entity_health(idx, nullptr, nullptr, &base); | 528 | server_entity_health(idx, nullptr, nullptr, &base); |
| 487 | if (!base) | 529 | if (!base) |
| 488 | return -1; | 530 | return -1; |
| 489 | return *(int *)((uintptr_t)base + g_srv_juice); | 531 | return *(int *)((uintptr_t)base + off); |
| 490 | } | 532 | } |
| 491 | 533 | ||
| 492 | // Server-side CBaseCombatCharacter::m_iAmmo array (reserve ammo). The datamap | 534 | // Server-side CBaseCombatCharacter::m_iAmmo array (reserve ammo). The datamap |
