summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 17:25:31 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 17:25:31 +0100
commit710e8cc59557bc95ed74af8726ea13012fde50c3 (patch)
treef1fd242c12d2ce5a939fbf1d3828e3546bc8f1cf
parentNetvars: catch array element props and log the walk (diff)
downloadhl2h-710e8cc59557bc95ed74af8726ea13012fde50c3.tar.gz
hl2h-710e8cc59557bc95ed74af8726ea13012fde50c3.zip
Items: read ammo and inventory from the server arrays
The client m_iAmmo/m_hMyWeapons recv props still do not resolve, but the listen server has the same CBaseCombatCharacter arrays in its datamap. Resolve m_iAmmo and m_hMyWeapons there: - reserve ammo comes from server_player_ammo() when the client array is missing, so full-ammo filtering works (RPG rockets at 3); - the owned-weapon mask is filled from the server m_hMyWeapons[48] handles, so fallen weapons count as ammo before the player equips them. Widen the datamap field-size filter to 512 for the 192-byte handle array.
-rw-r--r--CONTEXT.md15
-rw-r--r--src/esp.cpp33
-rw-r--r--src/hl2h.h2
-rw-r--r--src/server.cpp35
4 files changed, 73 insertions, 12 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 839d340..d1dbf26 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -610,14 +610,15 @@ vocabulary / decisions.
610- A world weapon whose type is already in the local player's inventory is 610- A world weapon whose type is already in the local player's inventory is
611 reclassified as ammo (`SMG ammo`, `AR2 ammo`, ...). Owner handles are 611 reclassified as ammo (`SMG ammo`, `AR2 ammo`, ...). Owner handles are
612 unreliable on this client (`m_hOwnerEntity`/`m_hOwner` resolve to -1), so 612 unreliable on this client (`m_hOwnerEntity`/`m_hOwner` resolve to -1), so
613 the inventory is read directly from the local `m_hMyWeapons[48]` handle 613 the inventory is read from the authoritative server
614 array (and remembered active types as a fallback). NPC-held weapons are 614 `CBaseCombatCharacter::m_hMyWeapons[48]` array (client `m_hMyWeapons` and the
615 therefore ignored. Weapons the player does not own stay under the Weapons 615 remembered active weapon are fallbacks). NPC-held weapons are ignored.
616 category. 616 Weapons the player does not own stay under the Weapons category.
617- Ammo pickups also honour "Only if usable": the reserve count from 617- Ammo pickups also honour "Only if usable": the reserve count from
618 `m_iAmmo[type]` is compared with the `sk_max_*` cvar (stock caps as fallback, 618 `m_iAmmo[type]` (client array when resolved, otherwise the server
619 e.g. RPG rockets = 3). Full ammo hides the pickup; if either value cannot 619 `CBaseCombatCharacter::m_iAmmo` array) is compared with the `sk_max_*` cvar
620 be read the item stays visible. 620 (stock caps as fallback, e.g. RPG rockets = 3). Full ammo hides the pickup;
621 if either value cannot be read the item stays visible.
621- Scans the client entity list every 100 ms and tags health, ammo, armor/suit, 622- Scans the client entity list every 100 ms and tags health, ammo, armor/suit,
622 world weapon pickups, the two supply-crate models and generic items. 623 world weapon pickups, the two supply-crate models and generic items.
623- Each tag is a colored marker with a name and optional distance, filtered by 624- 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 1d4a6a5..b4d3eee 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -2396,9 +2396,14 @@ static int ammo_max(int type) {
2396} 2396}
2397 2397
2398static int local_ammo(void *local_ent, int type) { 2398static int local_ammo(void *local_ent, int type) {
2399 if (!local_ent || g_net.ammo < 0 || type < 0 || type >= 32) 2399 if (type < 0 || type >= 32)
2400 return -1; 2400 return -1;
2401 return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4); 2401 if (local_ent && g_net.ammo >= 0)
2402 return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4);
2403 // Client array offset may not resolve; the listen-server player has the
2404 // same array in its datamap.
2405 int local = engine_get_local_player();
2406 return local > 0 ? server_player_ammo(local, type) : -1;
2402} 2407}
2403 2408
2404static int classify_item(const char *model, const char *cls, char *label, 2409static int classify_item(const char *model, const char *cls, char *label,
@@ -2627,6 +2632,30 @@ static void items_collect(int local_idx) {
2627 if (wt > 0) 2632 if (wt > 0)
2628 owned_mask |= 1u << wt; 2633 owned_mask |= 1u << wt;
2629 } 2634 }
2635 // Server-side inventory, independent of the client recv-table arrays.
2636 if (g_iface.engineserver) {
2637 for (int slot = 0; slot < 48; slot++) {
2638 uint32_t h = server_player_weapon(local_idx, slot);
2639 if (!item_handle_valid(h))
2640 continue;
2641 int wi = (int)(h & 0x7FF);
2642 if (wi <= 0 || wi > 4096)
2643 continue;
2644 void *wep = entitylist_get(wi);
2645 void *net = entity_networkable(wi);
2646 const char *wc = net ? entity_class_name(net, nullptr) : nullptr;
2647 const char *wm = wep ? entity_model_name(wep) : nullptr;
2648 if (!wm) {
2649 void *sb = nullptr;
2650 server_entity_health(wi, nullptr, nullptr, &sb);
2651 if (sb)
2652 wm = model_name_from_index(server_model_index(sb));
2653 }
2654 int wt = weapon_type_id(wm, wc);
2655 if (wt > 0)
2656 owned_mask |= 1u << wt;
2657 }
2658 }
2630 bool owned_weapon[WT_COUNT] = {}; 2659 bool owned_weapon[WT_COUNT] = {};
2631 for (int t = 1; t < WT_COUNT; t++) 2660 for (int t = 1; t < WT_COUNT; t++)
2632 owned_weapon[t] = (owned_mask & (1u << t)) != 0; 2661 owned_weapon[t] = (owned_mask & (1u << t)) != 0;
diff --git a/src/hl2h.h b/src/hl2h.h
index 4f3552e..d8927c7 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -297,6 +297,8 @@ bool server_npc_has_follow_target(int idx);
297int server_npc_state(int idx); 297int server_npc_state(int idx);
298int server_npc_takedamage(int idx); 298int server_npc_takedamage(int idx);
299int server_player_armor(int idx); 299int server_player_armor(int idx);
300int server_player_ammo(int idx, int type);
301uint32_t server_player_weapon(int idx, int slot);
300float server_grenade_detonate(int idx); 302float server_grenade_detonate(int idx);
301float server_grenade_radius(int idx); 303float server_grenade_radius(int idx);
302int server_entity_count(); 304int server_entity_count();
diff --git a/src/server.cpp b/src/server.cpp
index 1313f00..af0a8ff 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -58,6 +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_ammo = -1;
62static int g_srv_myweapons = -1;
61static bool g_srv_ready = false; 63static bool g_srv_ready = false;
62static bool g_srv_checked = false; 64static bool g_srv_checked = false;
63 65
@@ -97,7 +99,7 @@ static int datamap_offset(const char *name, int want_type, int min_off = 0x40,
97 continue; 99 continue;
98 if (foff < min_off || foff > max_off) 100 if (foff < min_off || foff > max_off)
99 continue; 101 continue;
100 if (fsize < 1 || fsize > 128) 102 if (fsize < 1 || fsize > 512)
101 continue; 103 continue;
102 // DEFINE_KEYFIELD entries have an external name ("health"); plain 104 // DEFINE_KEYFIELD entries have an external name ("health"); plain
103 // DEFINE_FIELD entries have none. Either way it must be in server.so. 105 // DEFINE_FIELD entries have none. Either way it must be in server.so.
@@ -160,17 +162,22 @@ bool resolve_server_netvars() {
160 // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked). 162 // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked).
161 g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME); 163 g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME);
162 g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT); 164 g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT);
165 // CBaseCombatCharacter arrays: ammo counts and the weapon handle list.
166 g_srv_ammo = datamap_offset("m_iAmmo", FIELD_INTEGER, 0x40, 0x4000);
167 g_srv_myweapons = datamap_offset("m_hMyWeapons", FIELD_EHANDLE);
163 g_srv_ready = g_srv_health > 0; 168 g_srv_ready = g_srv_health > 0;
164 if (g_srv_ready) { 169 if (g_srv_ready) {
165 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " 170 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d "
166 "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " 171 "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d "
167 "followtarget=%d spawnflags=%d hacked=%d npcstate=%d " 172 "followtarget=%d spawnflags=%d hacked=%d npcstate=%d "
168 "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d", 173 "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d "
174 "ammo=%d myweapons=%d",
169 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, 175 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate,
170 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, 176 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity,
171 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, 177 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget,
172 g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage, 178 g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage,
173 g_srv_mapname, g_srv_armor, g_srv_detonate, g_srv_grenade_radius); 179 g_srv_mapname, g_srv_armor, g_srv_detonate, g_srv_grenade_radius,
180 g_srv_ammo, g_srv_myweapons);
174 fail_logged = false; 181 fail_logged = false;
175 } else if (!fail_logged) { 182 } else if (!fail_logged) {
176 fail_logged = true; 183 fail_logged = true;
@@ -475,6 +482,28 @@ float server_grenade_radius(int idx) {
475 return *(float *)((uintptr_t)base + g_srv_grenade_radius); 482 return *(float *)((uintptr_t)base + g_srv_grenade_radius);
476} 483}
477 484
485// Server-side CBaseCombatCharacter::m_iAmmo array (reserve ammo).
486int server_player_ammo(int idx, int type) {
487 if (!g_srv_ready || g_srv_ammo < 0 || type < 0 || type >= 32)
488 return -1;
489 void *base = nullptr;
490 server_entity_health(idx, nullptr, nullptr, &base);
491 if (!base)
492 return -1;
493 return *(int *)((uintptr_t)base + g_srv_ammo + type * 4);
494}
495
496// Server-side m_hMyWeapons[48] handle for one inventory slot (0 if empty).
497uint32_t server_player_weapon(int idx, int slot) {
498 if (!g_srv_ready || g_srv_myweapons < 0 || slot < 0 || slot >= 48)
499 return 0;
500 void *base = nullptr;
501 server_entity_health(idx, nullptr, nullptr, &base);
502 if (!base)
503 return 0;
504 return *(uint32_t *)((uintptr_t)base + g_srv_myweapons + slot * 4);
505}
506
478// Entity index of the player's active weapon from the authoritative server 507// Entity index of the player's active weapon from the authoritative server
479// entity. Used as a fallback if the client m_hActiveWeapon netvar is missing. 508// entity. Used as a fallback if the client m_hActiveWeapon netvar is missing.
480int server_active_weapon(int idx) { 509int server_active_weapon(int idx) {