summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 23:32:00 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 23:32:00 +0100
commit80a287910f3f77519c8243ba5f53d9c877fbddcb (patch)
tree761c4b0ca3ab857cd57a886dd6e9c4048bca950d
parentAdd grenade trajectory, RPG armour priority, empty chargers, mega gravity gun (diff)
downloadhl2h-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.md9
-rw-r--r--src/esp.cpp12
-rw-r--r--src/server.cpp66
3 files changed, 71 insertions, 16 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 889c3ab..d0f89cb 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -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.
2660static 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
2658static void items_collect(int local_idx) { 2668static 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;
58static int g_srv_armor = -1; 58static int g_srv_armor = -1;
59static int g_srv_detonate = -1; 59static int g_srv_detonate = -1;
60static int g_srv_grenade_radius = -1; 60static int g_srv_grenade_radius = -1;
61static int g_srv_juice = -1; 61static int g_srv_juice_suit = -1;
62static int g_srv_juice_health = -1;
62static int g_srv_ammo = -1; 63static int g_srv_ammo = -1;
63static int g_srv_myweapons = -1; 64static int g_srv_myweapons = -1;
64static bool g_srv_ready = false; 65static 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
83static 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.
86static 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
126static 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
115bool resolve_server_netvars() { 133bool 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.
482int server_item_juice(int idx) { 515int 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