// 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 // ---------------------------------------------------------------------------