From 710e8cc59557bc95ed74af8726ea13012fde50c3 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 17:25:31 +0100 Subject: 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. --- CONTEXT.md | 15 ++++++++------- src/esp.cpp | 33 +++++++++++++++++++++++++++++++-- src/hl2h.h | 2 ++ src/server.cpp | 35 ++++++++++++++++++++++++++++++++--- 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. - A world weapon whose type is already in the local player's inventory is reclassified as ammo (`SMG ammo`, `AR2 ammo`, ...). Owner handles are unreliable on this client (`m_hOwnerEntity`/`m_hOwner` resolve to -1), so - the inventory is read directly from the local `m_hMyWeapons[48]` handle - array (and remembered active types as a fallback). NPC-held weapons are - therefore ignored. Weapons the player does not own stay under the Weapons - category. + the inventory is read from the authoritative server + `CBaseCombatCharacter::m_hMyWeapons[48]` array (client `m_hMyWeapons` and the + remembered active weapon are fallbacks). NPC-held weapons are ignored. + Weapons the player does not own stay under the Weapons category. - Ammo pickups also honour "Only if usable": the reserve count from - `m_iAmmo[type]` is compared with the `sk_max_*` cvar (stock caps as fallback, - e.g. RPG rockets = 3). Full ammo hides the pickup; if either value cannot - be read the item stays visible. + `m_iAmmo[type]` (client array when resolved, otherwise the server + `CBaseCombatCharacter::m_iAmmo` array) is compared with the `sk_max_*` cvar + (stock caps as fallback, e.g. RPG rockets = 3). Full ammo hides the pickup; + if either value cannot be read the item stays visible. - Scans the client entity list every 100 ms and tags health, ammo, armor/suit, world weapon pickups, the two supply-crate models and generic items. - 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) { } static int local_ammo(void *local_ent, int type) { - if (!local_ent || g_net.ammo < 0 || type < 0 || type >= 32) + if (type < 0 || type >= 32) return -1; - return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4); + if (local_ent && g_net.ammo >= 0) + return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4); + // Client array offset may not resolve; the listen-server player has the + // same array in its datamap. + int local = engine_get_local_player(); + return local > 0 ? server_player_ammo(local, type) : -1; } static int classify_item(const char *model, const char *cls, char *label, @@ -2627,6 +2632,30 @@ static void items_collect(int local_idx) { if (wt > 0) owned_mask |= 1u << wt; } + // Server-side inventory, independent of the client recv-table arrays. + if (g_iface.engineserver) { + for (int slot = 0; slot < 48; slot++) { + uint32_t h = server_player_weapon(local_idx, slot); + if (!item_handle_valid(h)) + continue; + int wi = (int)(h & 0x7FF); + if (wi <= 0 || wi > 4096) + continue; + void *wep = entitylist_get(wi); + void *net = entity_networkable(wi); + const char *wc = net ? entity_class_name(net, nullptr) : nullptr; + const char *wm = wep ? entity_model_name(wep) : nullptr; + if (!wm) { + void *sb = nullptr; + server_entity_health(wi, nullptr, nullptr, &sb); + if (sb) + wm = model_name_from_index(server_model_index(sb)); + } + int wt = weapon_type_id(wm, wc); + if (wt > 0) + owned_mask |= 1u << wt; + } + } bool owned_weapon[WT_COUNT] = {}; for (int t = 1; t < WT_COUNT; t++) 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); int server_npc_state(int idx); int server_npc_takedamage(int idx); int server_player_armor(int idx); +int server_player_ammo(int idx, int type); +uint32_t server_player_weapon(int idx, int slot); float server_grenade_detonate(int idx); float server_grenade_radius(int idx); int 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; static int g_srv_armor = -1; static int g_srv_detonate = -1; static int g_srv_grenade_radius = -1; +static int g_srv_ammo = -1; +static int g_srv_myweapons = -1; static bool g_srv_ready = false; static bool g_srv_checked = false; @@ -97,7 +99,7 @@ static int datamap_offset(const char *name, int want_type, int min_off = 0x40, continue; if (foff < min_off || foff > max_off) continue; - if (fsize < 1 || fsize > 128) + if (fsize < 1 || fsize > 512) continue; // DEFINE_KEYFIELD entries have an external name ("health"); plain // DEFINE_FIELD entries have none. Either way it must be in server.so. @@ -160,17 +162,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); + // 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); g_srv_ready = g_srv_health > 0; if (g_srv_ready) { logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " "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", + "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d " + "ammo=%d myweapons=%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_mapname, g_srv_armor, g_srv_detonate, g_srv_grenade_radius, + g_srv_ammo, g_srv_myweapons); fail_logged = false; } else if (!fail_logged) { fail_logged = true; @@ -475,6 +482,28 @@ float server_grenade_radius(int idx) { return *(float *)((uintptr_t)base + g_srv_grenade_radius); } +// Server-side CBaseCombatCharacter::m_iAmmo array (reserve ammo). +int server_player_ammo(int idx, int type) { + if (!g_srv_ready || g_srv_ammo < 0 || type < 0 || type >= 32) + return -1; + void *base = nullptr; + server_entity_health(idx, nullptr, nullptr, &base); + if (!base) + return -1; + return *(int *)((uintptr_t)base + g_srv_ammo + type * 4); +} + +// Server-side m_hMyWeapons[48] handle for one inventory slot (0 if empty). +uint32_t server_player_weapon(int idx, int slot) { + if (!g_srv_ready || g_srv_myweapons < 0 || slot < 0 || slot >= 48) + return 0; + void *base = nullptr; + server_entity_health(idx, nullptr, nullptr, &base); + if (!base) + return 0; + return *(uint32_t *)((uintptr_t)base + g_srv_myweapons + slot * 4); +} + // Entity index of the player's active weapon from the authoritative server // entity. Used as a fallback if the client m_hActiveWeapon netvar is missing. int server_active_weapon(int idx) { -- cgit v1.2.3