From 5a9fd14f9424d7f66b0454464be95b40b10ee5c4 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 6 Nov 2026 16:23:29 +0100 Subject: ESP: extract item ESP into its own translation unit Second slice of the esp.cpp split. Model-name lookup joins the shared helpers in esp_internal.h. --- src/items.cpp | 820 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 820 insertions(+) create mode 100644 src/items.cpp (limited to 'src/items.cpp') diff --git a/src/items.cpp b/src/items.cpp new file mode 100644 index 0000000..cc324d4 --- /dev/null +++ b/src/items.cpp @@ -0,0 +1,820 @@ +// items.cpp — item ESP (discovery, classification, drawing). +#include "esp_internal.h" +#include "hl2h.h" +#include +#include +#include + + +enum ItemKind { + IK_HEALTH, + IK_AMMO, + IK_ARMOR, + IK_WEAPON, + IK_CRATE, + IK_OTHER +}; + +struct ItemTarget { + int idx; + int kind; + int ammo_type; // m_iAmmo index for IK_AMMO, -1 otherwise + Vector origin; + char label[128]; +}; + +static ItemTarget g_item_targets[192]; +static int g_nitems = 0; +static bool g_items_logged = false; + +static bool item_icontains(const char *hay, const char *needle) { + if (!hay || !needle || !*needle) + return false; + size_t n = strlen(needle); + for (const char *p = hay; *p; p++) + if (strncasecmp(p, needle, n) == 0) + return true; + return false; +} + +static bool item_handle_valid(uint32_t h) { + return h != 0 && h != 0xFFFFFFFFu; +} + +// Weapons held by the player or an NPC are not world pickups. +bool item_weapon_is_owned(void *ent) { + if (!ent) + return false; + if (g_net.owner >= 0) { + uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.owner); + if (item_handle_valid(h)) + return true; + } + if (g_net.weapon_owner >= 0) { + uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner); + if (item_handle_valid(h)) + return true; + } + return false; +} + +// Entity index of the weapon's owner, or -1. Used to distinguish the local +// player's inventory from NPC weapons. +static int item_weapon_owner_idx(void *ent) { + if (!ent) + return -1; + uint32_t h = 0; + if (g_net.owner >= 0) + h = *(uint32_t *)((uintptr_t)ent + g_net.owner); + if (!item_handle_valid(h) && g_net.weapon_owner >= 0) + h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner); + if (!item_handle_valid(h)) + return -1; + return (int)(h & 0x7FF); +} + +static int local_active_weapon_idx(int local_idx) { + if (local_idx <= 0 || g_net.activeweapon < 0) + return -1; + void *local = entitylist_get(local_idx); + if (!local) + return -1; + uint32_t h = *(uint32_t *)((uintptr_t)local + g_net.activeweapon); + if (!item_handle_valid(h)) + return -1; + return (int)(h & 0x7FF); +} + +static int classify_weapon(const char *model, const char *cls, char *label, + size_t outn) { + if (!item_icontains(model, "models/weapons/w_") && + !item_icontains(cls, "weapon")) + return 0; + if (item_icontains(model, "w_crowbar")) + snprintf(label, outn, "Crowbar"); + else if (item_icontains(model, "w_pistol")) + snprintf(label, outn, "Pistol"); + else if (item_icontains(model, "w_357")) + snprintf(label, outn, "Revolver"); + else if (item_icontains(model, "w_smg1") || + item_icontains(model, "w_smg2")) + snprintf(label, outn, "SMG"); + else if (item_icontains(model, "w_ar2") || + item_icontains(model, "w_irifle")) + snprintf(label, outn, "AR2"); + else if (item_icontains(model, "w_shotgun")) + snprintf(label, outn, "Shotgun"); + else if (item_icontains(model, "w_crossbow")) + snprintf(label, outn, "Crossbow"); + else if (item_icontains(model, "w_rocket_launcher")) + snprintf(label, outn, "RPG"); + else if (item_icontains(model, "w_frag") || + item_icontains(model, "w_grenade")) + snprintf(label, outn, "Grenade"); + else if (item_icontains(model, "w_physcannon") || + item_icontains(model, "w_physics")) + snprintf(label, outn, "Gravity gun"); + else if (item_icontains(model, "w_stunstick") || + item_icontains(model, "w_stunbaton")) + snprintf(label, outn, "Stunstick"); + else if (item_icontains(model, "w_bugbait")) + snprintf(label, outn, "Bugbait"); + else if (item_icontains(model, "w_sniper")) + snprintf(label, outn, "Sniper"); + else if (item_icontains(model, "w_slam")) + snprintf(label, outn, "SLAM"); + else + snprintf(label, outn, "%s", model); + return 1; +} + +// Weapon identity for the "owned weapon -> fallen weapon is ammo" rule. +enum { + WT_NONE = 0, + WT_PISTOL, + WT_357, + WT_SMG, + WT_AR2, + WT_SHOTGUN, + WT_CROSSBOW, + WT_RPG, + WT_GRENADE, + WT_SNIPER, + WT_COUNT +}; + +static int weapon_type_id(const char *model, const char *cls) { + if (item_icontains(model, "pistol") || item_icontains(cls, "pistol")) + return WT_PISTOL; + if (item_icontains(model, "357") || item_icontains(cls, "357")) + return WT_357; + if (item_icontains(model, "smg") || item_icontains(cls, "smg")) + return WT_SMG; + if (item_icontains(model, "ar2") || item_icontains(model, "irifle") || + item_icontains(cls, "ar2") || item_icontains(cls, "irifle")) + return WT_AR2; + if (item_icontains(model, "shotgun") || item_icontains(cls, "shotgun")) + return WT_SHOTGUN; + if (item_icontains(model, "crossbow") || item_icontains(cls, "crossbow")) + return WT_CROSSBOW; + if (item_icontains(model, "rocket") || item_icontains(cls, "rpg")) + return WT_RPG; + if (item_icontains(model, "grenade") || item_icontains(cls, "frag")) + return WT_GRENADE; + if (item_icontains(model, "sniper") || item_icontains(cls, "sniper")) + return WT_SNIPER; + return WT_NONE; +} + +static int weapon_ammo_type(int wt) { + switch (wt) { + case WT_PISTOL: return 2; + case WT_357: return 4; + case WT_SMG: return 3; + case WT_AR2: return 0; + case WT_SHOTGUN: return 6; + case WT_CROSSBOW: return 5; + case WT_RPG: return 7; + case WT_GRENADE: return 11; + case WT_SNIPER: return 9; + default: return -1; + } +} + +// Ammo type index used by CBasePlayer::m_iAmmo (order of AddAmmoType in +// hl2_gamerules.cpp). +static int item_ammo_type(const char *model) { + if (!model) + return -1; + if (item_icontains(model, "boxsrounds") || + item_icontains(model, "ammocrate_pistol") || + item_icontains(model, "w_pistol")) + return 2; + if (item_icontains(model, "boxmrounds") || + item_icontains(model, "ammocrate_smg1")) + return 3; + if (item_icontains(model, "boxlrounds") || + item_icontains(model, "combine_rifle") || + item_icontains(model, "ammocrate_ar2") || + item_icontains(model, "w_ar2") || item_icontains(model, "w_irifle")) + return 0; + if (item_icontains(model, "357ammo") || + item_icontains(model, "357ammobox") || + item_icontains(model, "w_357")) + return 4; + if (item_icontains(model, "crossbowrounds") || + item_icontains(model, "w_crossbow")) + return 5; + if (item_icontains(model, "boxbuckshot") || + item_icontains(model, "ammocrate_buckshot") || + item_icontains(model, "w_shotgun")) + return 6; + if (item_icontains(model, "w_missile_closed") || + item_icontains(model, "ammocrate_rockets") || + item_icontains(model, "w_rocket_launcher")) + return 7; + if (item_icontains(model, "ar2_grenade") || + item_icontains(model, "ammocrate_smg2") || + item_icontains(model, "smg1_grenade") || + item_icontains(model, "smg_grenade")) + return 8; + if (item_icontains(model, "ammocrate_grenade") || + item_icontains(model, "w_grenade") || + item_icontains(model, "w_frag") || + item_icontains(model, "frag_grenade") || + item_icontains(model, "items/grenade")) + return 11; + if (item_icontains(model, "w_smg1") || item_icontains(model, "w_smg2")) + return 3; + return -1; +} + +static int convar_int(const char *name) { + if (!g_iface.cvar || !name) + return -1; + using FindFn = void *(*)(void *, const char *); + void *v = vfunc(g_iface.cvar, 12)(g_iface.cvar, name); + if (!v) + return -1; + return (int)*(float *)((uintptr_t)v + 0x2c); // ConVar m_Value.f +} + +// Maximum reserve ammo for an m_iAmmo index. Prefers the sk_max_* cvar and +// falls back to the stock HL2 cap so the RPG's 3-rocket limit is honoured +// even when server cvars are not visible from the client. +static int ammo_max(int type) { + static int cache[16]; + static bool init = false; + if (type < 0 || type >= 16) + return -1; + if (!init) { + for (int i = 0; i < 16; i++) + cache[i] = -2; + init = true; + } + if (cache[type] != -2) + return cache[type]; + const char *name = nullptr; + int stock = -1; + switch (type) { + case 0: name = "sk_max_ar2"; stock = 60; break; + case 2: name = "sk_max_pistol"; stock = 150; break; + case 3: name = "sk_max_smg1"; stock = 225; break; + case 4: name = "sk_max_357"; stock = 12; break; + case 5: name = "sk_max_crossbow"; stock = 10; break; + case 6: name = "sk_max_buckshot"; stock = 30; break; + case 7: name = "sk_max_rpg_round"; stock = 3; break; + case 8: name = "sk_max_smg1_grenade"; stock = 3; break; + case 9: name = "sk_max_sniper_round"; stock = 30; break; + case 11: name = "sk_max_grenade"; stock = 5; break; + default: break; + } + int v = convar_int(name); + if (v <= 0) + v = stock; + cache[type] = v; + return v; +} + +static int local_ammo(void *local_ent, int type) { + if (type < 0 || type >= 32) + return -1; + // Prefer the authoritative server array; the client m_iAmmo netvar offset + // can be off by an element, which made "full" counts flicker as other ammo + // was spent. server_player_ammo already skips the CNetworkArray header. + int local = engine_get_local_player(); + if (local > 0 && g_iface.engineserver) { + resolve_server_netvars(); + int s = server_player_ammo(local, type); + if (s >= 0) + return s; + } + if (local_ent && g_net.ammo >= 0) + return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4); + return -1; +} + +static int classify_item(const char *model, const char *cls, char *label, + size_t outn) { + if (!model && !cls) + return -1; + + // Breakable prop chunks are debris, not pickups. + if (item_icontains(model, "chunk")) + return -1; + + if (item_icontains(model, "healthkit")) { + snprintf(label, outn, "Medkit"); + return IK_HEALTH; + } + if (item_icontains(model, "healthvial")) { + snprintf(label, outn, "Health vial"); + return IK_HEALTH; + } + if (item_icontains(model, "health_charger")) { + snprintf(label, outn, "Health charger"); + return IK_HEALTH; + } + if (item_icontains(model, "battery")) { + snprintf(label, outn, "Battery"); + return IK_ARMOR; + } + if (item_icontains(model, "hevsuit")) { + snprintf(label, outn, "HEV suit"); + return IK_ARMOR; + } + if (item_icontains(model, "w_longjump")) { + snprintf(label, outn, "Long jump"); + return IK_ARMOR; + } + if (item_icontains(model, "suit_charger")) { + snprintf(label, outn, "Suit charger"); + return IK_ARMOR; + } + + // Only the two item-crate models are supply crates. Other crates are just + // breakable map props and deliberately not tagged. + if (item_icontains(model, "item_item_crate") || + item_icontains(model, "item_beacon_crate")) { + snprintf(label, outn, "Supply crate"); + return IK_CRATE; + } + + if (item_icontains(model, "ammocrate_pistol")) { + snprintf(label, outn, "Pistol ammo crate"); + return IK_AMMO; + } + if (item_icontains(model, "ammocrate_smg1")) { + snprintf(label, outn, "SMG ammo crate"); + return IK_AMMO; + } + if (item_icontains(model, "ammocrate_ar2")) { + snprintf(label, outn, "AR2 ammo crate"); + return IK_AMMO; + } + if (item_icontains(model, "ammocrate_rockets")) { + snprintf(label, outn, "RPG ammo crate"); + return IK_AMMO; + } + if (item_icontains(model, "ammocrate_buckshot")) { + snprintf(label, outn, "Shotgun ammo crate"); + return IK_AMMO; + } + if (item_icontains(model, "ammocrate_grenade")) { + snprintf(label, outn, "Grenade crate"); + return IK_AMMO; + } + if (item_icontains(model, "ammocrate_smg2")) { + snprintf(label, outn, "SMG grenade crate"); + return IK_AMMO; + } + if (item_icontains(model, "ammocrate")) { + snprintf(label, outn, "Ammo crate"); + return IK_AMMO; + } + + if (item_icontains(model, "w_missile_closed")) { + snprintf(label, outn, "RPG rocket"); + return IK_AMMO; + } + if (item_icontains(model, "boxsrounds")) { + snprintf(label, outn, "Pistol ammo"); + return IK_AMMO; + } + if (item_icontains(model, "boxmrounds")) { + snprintf(label, outn, "SMG ammo"); + return IK_AMMO; + } + if (item_icontains(model, "boxlrounds") || + item_icontains(model, "combine_rifle_cartridge") || + item_icontains(model, "combine_rifle_ammo")) { + snprintf(label, outn, "AR2 ammo"); + return IK_AMMO; + } + if (item_icontains(model, "357ammo") || + item_icontains(model, "357ammobox")) { + snprintf(label, outn, ".357 ammo"); + return IK_AMMO; + } + if (item_icontains(model, "crossbowrounds")) { + snprintf(label, outn, "Crossbow bolts"); + return IK_AMMO; + } + if (item_icontains(model, "boxbuckshot")) { + snprintf(label, outn, "Shotgun shells"); + return IK_AMMO; + } + if (item_icontains(model, "boxsniperrounds")) { + snprintf(label, outn, "Sniper ammo"); + return IK_AMMO; + } + if (item_icontains(model, "boxflares") || + item_icontains(model, "models/items/flare")) { + snprintf(label, outn, "Flares"); + return IK_AMMO; + } + if (item_icontains(model, "ar2_grenade")) { + snprintf(label, outn, "AR2 grenade"); + return IK_AMMO; + } + if (item_icontains(model, "w_grenade") || + item_icontains(model, "w_frag") || + item_icontains(model, "frag_grenade") || + item_icontains(model, "items/grenade")) { + snprintf(label, outn, "Frag grenade"); + return IK_AMMO; + } + if (item_icontains(model, "models/items/")) { + snprintf(label, outn, "Ammo"); + return IK_AMMO; + } + + if (classify_weapon(model, cls, label, outn) == 1) + return IK_WEAPON; + + if (item_icontains(cls, "healthkit") || item_icontains(cls, "healthvial") || + item_icontains(cls, "wallhealth") || + item_icontains(cls, "healthcharger")) { + snprintf(label, outn, "Health"); + return IK_HEALTH; + } + if (item_icontains(cls, "battery") || item_icontains(cls, "itemsuit") || + item_icontains(cls, "recharge")) { + snprintf(label, outn, "Armor"); + return IK_ARMOR; + } + if (item_icontains(cls, "itemcrate")) { + snprintf(label, outn, "Supply crate"); + return IK_CRATE; + } + if (item_icontains(cls, "item_ammo") || item_icontains(cls, "itemammo")) { + snprintf(label, outn, "Ammo"); + return IK_AMMO; + } + if (item_icontains(cls, "weapon")) { + snprintf(label, outn, "Weapon"); + return IK_WEAPON; + } + if (item_icontains(cls, "item") || item_icontains(cls, "pickup")) { + snprintf(label, outn, "Item"); + return IK_OTHER; + } + return -1; +} + +static bool item_kind_enabled(int kind) { + switch (kind) { + case IK_HEALTH: return g_item_health; + case IK_AMMO: return g_item_ammo; + case IK_ARMOR: return g_item_armor; + case IK_WEAPON: return g_item_weapons; + case IK_CRATE: return g_item_crates; + default: return true; + } +} + +// Health chargers are brushes (func_healthcharger/CWallHealth) and suit +// chargers are item_suitcharger/CNewRecharge. Both expose m_iJuice. +static bool item_is_charger(const char *model, const char *cls) { + return item_icontains(model, "charger") || + item_icontains(model, "suit_charger") || + item_icontains(cls, "charger") || + item_icontains(cls, "wallhealth") || + item_icontains(cls, "recharge"); +} + +static bool item_is_suit_charger(const char *model, const char *cls) { + return item_icontains(model, "suit_charger") || + item_icontains(model, "suitcharger") || + item_icontains(cls, "recharge") || + item_icontains(cls, "suitcharger"); +} + +static void items_collect(int local_idx) { + static double last_scan = 0.0; + double now = esp_now(); + if (now - last_scan < 0.10) + return; + last_scan = now; + g_nitems = 0; + if (!g_iface.entitylist || g_net.origin < 0 || g_net.modelindex < 0) + return; + resolve_server_netvars(); + + using Fn = int (*)(void *); + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); + if (hi < 0 || hi > 4096) + hi = 4096; + int active_idx = local_active_weapon_idx(local_idx); + + // Which weapon types the local player already carries. A fallen weapon of + // the same type is then just an ammo pickup. Owner handles are unreliable on + // this client (owner/weapon_owner netvars resolve to -1), so remember every + // type the player has held since the current local entity appeared. + static uint32_t owned_mask = 0; + static void *last_local = nullptr; + void *local_ent = entitylist_get(local_idx); + if (local_ent != last_local) { + last_local = local_ent; + owned_mask = 0; + } + int owned_idx = active_idx; + if (owned_idx <= 0 && g_iface.engineserver) { + resolve_server_netvars(); + owned_idx = server_active_weapon(local_idx); + } + if (owned_idx > 0) { + void *aw = entitylist_get(owned_idx); + void *anet = entity_networkable(owned_idx); + const char *acls = anet ? entity_class_name(anet, nullptr) : nullptr; + const char *amodel = aw ? entity_model_name(aw) : nullptr; + int at = weapon_type_id(amodel, acls); + if (at > 0) + owned_mask |= 1u << at; + } + for (int idx = 1; idx <= hi; idx++) { + if (idx == local_idx) + continue; + void *ent = entitylist_get(idx); + if (!ent || item_weapon_owner_idx(ent) != local_idx) + continue; + void *net = entity_networkable(idx); + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + const char *model = entity_model_name(ent); + int wt = weapon_type_id(model, cls); + 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; + static bool cats_logged = false; + if (!cats_logged) { + cats_logged = true; + char maxbuf[16]; + if (g_item_max_dist == 0) + snprintf(maxbuf, sizeof(maxbuf), "All"); + else + snprintf(maxbuf, sizeof(maxbuf), "%d", g_item_max_dist); + logf("items: categories health=%d ammo=%d armor=%d weapons=%d " + "crates=%d distance=%d max=%s", + (int)g_item_health, (int)g_item_ammo, (int)g_item_armor, + (int)g_item_weapons, (int)g_item_crates, (int)g_item_distance, + maxbuf); + } + static int dbg_lines = 0; + static bool owner_warned = false; + if (!owner_warned && g_net.owner < 0 && g_net.weapon_owner < 0) { + owner_warned = true; + logf("items: weapon owner netvar unavailable; held weapons may tag"); + } + + for (int idx = 1; idx <= hi && g_nitems < 192; idx++) { + if (idx == local_idx) + continue; + void *ent = entitylist_get(idx); + if (!ent) + continue; + void *net = entity_networkable(idx); + if (!net || entity_is_dormant(net)) + continue; + const char *cls = entity_class_name(net, nullptr); + const char *scls = nullptr; + if (g_iface.engineserver && + (!cls || !*cls || strcmp(cls, "CBaseEntity") == 0)) + scls = server_entity_classname(idx); + const char *model = entity_model_name(ent); + // CPhysicsProp's client m_nModelIndex is often 0 (e.g. item_item_crate); + // fall back to the server entity's model index, which is set in Spawn(). + if ((!model || !*model) && g_iface.engineserver) { + void *sbase = nullptr; + server_entity_health(idx, nullptr, nullptr, &sbase); + if (sbase) + model = model_name_from_index(server_model_index(sbase)); + } + char label[128]; + int kind = classify_item(model, cls, label, sizeof(label)); + if (kind < 0 && scls) + kind = classify_item(model, scls, label, sizeof(label)); + if (kind < 0) + continue; + // Health/suit chargers are dispensers that stay useful after a refill, but + // an empty suit charger cannot give anything. The health charger's + // m_iJuice offset is still being verified (it read 0 on a full charger), + // so it is left visible for now; server_item_juice logs both candidate + // values for diagnosis. + bool is_charger = item_is_charger(model, cls) || + (scls && item_is_charger(model, scls)); + bool is_suit = item_is_suit_charger(model, cls) || + (scls && item_is_suit_charger(model, scls)); + if (is_charger) { + int juice = server_item_juice(idx); + if (is_suit && juice == 0) + continue; + } + + Vector origin{*(float *)((uintptr_t)ent + g_net.origin), + *(float *)((uintptr_t)ent + g_net.origin + 4), + *(float *)((uintptr_t)ent + g_net.origin + 8)}; + float dx = origin.x - g_cam_eye.x; + float dy = origin.y - g_cam_eye.y; + float dz = origin.z - g_cam_eye.z; + float dist = sqrtf(dx * dx + dy * dy + dz * dz); + + if (kind == IK_WEAPON) { + if (idx == active_idx || item_weapon_is_owned(ent)) + continue; + int wt = weapon_type_id(model, cls); + if (wt > 0 && owned_weapon[wt]) { + char base_label[160]; + snprintf(base_label, sizeof(base_label), "%s", label); + snprintf(label, sizeof(label), "%.100s ammo", base_label); + kind = IK_AMMO; + } + } + // Distance applies to every tag; 0 means unlimited. + if (g_item_max_dist > 0 && dist > (float)g_item_max_dist) + continue; + + ItemTarget &t = g_item_targets[g_nitems++]; + t.idx = idx; + t.kind = kind; + t.ammo_type = -1; + if (kind == IK_AMMO) { + t.ammo_type = item_ammo_type(model); + if (t.ammo_type < 0) + t.ammo_type = weapon_ammo_type(weapon_type_id(model, cls)); + } + t.origin = origin; + snprintf(t.label, sizeof(t.label), "%s", label); + if (dbg_lines < 8) { + dbg_lines++; + logd("item: idx=%d kind=%d cls=%s model=%s", idx, kind, + cls ? cls : "?", model ? model : "?"); + } + } + + if (!g_items_logged) { + g_items_logged = true; + logf("items: tagging %d valuable entities", g_nitems); + } +} + +// Authoritative local health/armor for the "only if usable" item filter. +// The health ceiling comes from the server player's m_iMaxHealth, not a +// hardcoded 100; when it cannot be read the filter fails open. +static void local_vitals(int &hp, int &maxhp, int &armor) { + hp = maxhp = armor = -1; + int local = engine_get_local_player(); + if (local <= 0) + return; + void *le = entitylist_get(local); + if (le && g_net.health >= 0) + hp = *(int *)((uintptr_t)le + g_net.health); + if (g_iface.engineserver) { + int smax = -1; + int shp = server_entity_health(local, &smax, nullptr, nullptr); + if (shp >= 0) + hp = shp; + if (smax > 0) + maxhp = smax; + armor = server_player_armor(local); + } + if (maxhp < 0 && le && g_net.maxhealth >= 0) + maxhp = *(int *)((uintptr_t)le + g_net.maxhealth); +} + +void esp_draw_items() { + if (!g_surface || !g_iface.entitylist || g_net.origin < 0) + return; + if (!g_item_health && !g_item_ammo && !g_item_armor && !g_item_weapons && + !g_item_crates) + return; + int local = engine_get_local_player(); + if (local <= 0) + return; + if (!esp_update_screen()) + return; + items_collect(local); + + int vit_hp = -1, vit_max = -1, vit_armor = -1; + if (g_item_usable) + local_vitals(vit_hp, vit_max, vit_armor); + static bool ammo_logged = false; + if (g_item_usable && !ammo_logged) { + ammo_logged = true; + char buf[512]; + int pos = 0; + void *le = entitylist_get(local); + for (int t = 0; t < 16 && pos < 500; t++) { + int a = local_ammo(le, t); + int m = ammo_max(t); + if (a >= 0 && m > 0) + pos += snprintf(buf + pos, sizeof(buf) - pos, " %d:%d/%d", t, a, m); + } + logf("items: ammo%s", buf); + } + + for (int i = 0; i < g_nitems; i++) { + ItemTarget &t = g_item_targets[i]; + // Follow the entity between the 10 Hz discovery scans. + Vector live; + if (entity_origin(entitylist_get(t.idx), live)) + t.origin = live; + if (!item_kind_enabled(t.kind)) + continue; + // Dispensers (chargers, ammo crates, supply crates) stay useful after a + // refill, so the fullness filter does not apply to them. + bool dispenser = strcasestr(t.label, "crate") || + strcasestr(t.label, "charger"); + if (g_item_usable && !dispenser) { + if (t.kind == IK_HEALTH && vit_hp >= 0 && vit_max > 0 && + vit_hp >= vit_max) + continue; + if (t.kind == IK_ARMOR && vit_armor >= 0 && vit_armor >= 100) + continue; + if (t.kind == IK_AMMO && t.ammo_type >= 0) { + int maxa = ammo_max(t.ammo_type); + int cura = local_ammo(entitylist_get(local), t.ammo_type); + if (maxa > 0 && cura >= 0 && cura >= maxa) + continue; + } + } + float sx, sy; + if (!world_to_screen(t.origin, sx, sy)) + continue; + if (sx < -40 || sx > (float)g_scr_w + 40 || sy < -20 || + sy > (float)g_scr_h + 20) + continue; + + int r = 230, g = 230, b = 230; + switch (t.kind) { + case IK_HEALTH: r = 90; g = 255; b = 130; break; + case IK_AMMO: r = 255; g = 210; b = 80; break; + case IK_ARMOR: r = 90; g = 180; b = 255; break; + case IK_WEAPON: r = 120; g = 230; b = 255; break; + case IK_CRATE: r = 220; g = 150; b = 255; break; + default: break; + } + int x = (int)sx, y = (int)sy; + int rad = (t.kind == IK_CRATE) ? 7 : 4; + surf_color(r, g, b, 255); + surf_rect(0, x - rad, y - rad, x + rad, y + rad); + surf_line(x - rad - 4, y, x - rad - 1, y); + surf_line(x + rad + 1, y, x + rad + 4, y); + surf_line(x, y - rad - 4, x, y - rad - 1); + surf_line(x, y + rad + 1, x, y + rad + 4); + if (t.kind == IK_CRATE) + surf_rect(1, x - 2, y - 2, x + 2, y + 2); + + float dx = t.origin.x - g_cam_eye.x; + float dy = t.origin.y - g_cam_eye.y; + float dz = t.origin.z - g_cam_eye.z; + int dist = (int)sqrtf(dx * dx + dy * dy + dz * dz); + char text[192]; + if (g_item_distance) + snprintf(text, sizeof(text), "%s %du", t.label, dist); + else + snprintf(text, sizeof(text), "%s", t.label); + int tw = (int)strlen(text) * 9; + int tx = x + 10; + if (tx + tw > g_scr_w - 4) + tx = x - 10 - tw; + if (tx < 4) + tx = 4; + int ty = y - 7; + if (ty < 0) + ty = 0; + if (ty + 16 > g_scr_h) + ty = g_scr_h - 16; + draw_text_col(tx, ty, text, r, g, b); + } +} + +// --------------------------------------------------------------------------- +// projectile ESP: grenades, rockets, helicopter bombs, AR2 balls +// --------------------------------------------------------------------------- -- cgit v1.2.3