summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:23:29 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:23:29 +0100
commit5a9fd14f9424d7f66b0454464be95b40b10ee5c4 (patch)
treeb77d072d1ea1b0382b79174984b3647bc2b05729
parentESP: extract projectile ESP into its own translation unit (diff)
downloadhl2h-5a9fd14f9424d7f66b0454464be95b40b10ee5c4.tar.gz
hl2h-5a9fd14f9424d7f66b0454464be95b40b10ee5c4.zip
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.
-rw-r--r--src/esp.cpp815
-rw-r--r--src/esp_internal.h1
-rw-r--r--src/items.cpp820
3 files changed, 822 insertions, 814 deletions
diff --git a/src/esp.cpp b/src/esp.cpp
index fef3e7c..3f21dce 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -252,7 +252,7 @@ static void entptr_store(void *ent, int idx) {
252 252
253// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. 253// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName.
254// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. 254// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4.
255static const char *model_name_from_index(int modelindex) { 255const char *model_name_from_index(int modelindex) {
256 if (!g_iface.modelinfo || modelindex <= 0) 256 if (!g_iface.modelinfo || modelindex <= 0)
257 return nullptr; 257 return nullptr;
258 if (modelindex < MODEL_CACHE) { 258 if (modelindex < MODEL_CACHE) {
@@ -2508,816 +2508,3 @@ void esp_draw() {
2508// Valuable item tags 2508// Valuable item tags
2509// --------------------------------------------------------------------------- 2509// ---------------------------------------------------------------------------
2510 2510
2511enum ItemKind {
2512 IK_HEALTH,
2513 IK_AMMO,
2514 IK_ARMOR,
2515 IK_WEAPON,
2516 IK_CRATE,
2517 IK_OTHER
2518};
2519
2520struct ItemTarget {
2521 int idx;
2522 int kind;
2523 int ammo_type; // m_iAmmo index for IK_AMMO, -1 otherwise
2524 Vector origin;
2525 char label[128];
2526};
2527
2528static ItemTarget g_item_targets[192];
2529static int g_nitems = 0;
2530static bool g_items_logged = false;
2531
2532static bool item_icontains(const char *hay, const char *needle) {
2533 if (!hay || !needle || !*needle)
2534 return false;
2535 size_t n = strlen(needle);
2536 for (const char *p = hay; *p; p++)
2537 if (strncasecmp(p, needle, n) == 0)
2538 return true;
2539 return false;
2540}
2541
2542static bool item_handle_valid(uint32_t h) {
2543 return h != 0 && h != 0xFFFFFFFFu;
2544}
2545
2546// Weapons held by the player or an NPC are not world pickups.
2547bool item_weapon_is_owned(void *ent) {
2548 if (!ent)
2549 return false;
2550 if (g_net.owner >= 0) {
2551 uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.owner);
2552 if (item_handle_valid(h))
2553 return true;
2554 }
2555 if (g_net.weapon_owner >= 0) {
2556 uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner);
2557 if (item_handle_valid(h))
2558 return true;
2559 }
2560 return false;
2561}
2562
2563// Entity index of the weapon's owner, or -1. Used to distinguish the local
2564// player's inventory from NPC weapons.
2565static int item_weapon_owner_idx(void *ent) {
2566 if (!ent)
2567 return -1;
2568 uint32_t h = 0;
2569 if (g_net.owner >= 0)
2570 h = *(uint32_t *)((uintptr_t)ent + g_net.owner);
2571 if (!item_handle_valid(h) && g_net.weapon_owner >= 0)
2572 h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner);
2573 if (!item_handle_valid(h))
2574 return -1;
2575 return (int)(h & 0x7FF);
2576}
2577
2578static int local_active_weapon_idx(int local_idx) {
2579 if (local_idx <= 0 || g_net.activeweapon < 0)
2580 return -1;
2581 void *local = entitylist_get(local_idx);
2582 if (!local)
2583 return -1;
2584 uint32_t h = *(uint32_t *)((uintptr_t)local + g_net.activeweapon);
2585 if (!item_handle_valid(h))
2586 return -1;
2587 return (int)(h & 0x7FF);
2588}
2589
2590static int classify_weapon(const char *model, const char *cls, char *label,
2591 size_t outn) {
2592 if (!item_icontains(model, "models/weapons/w_") &&
2593 !item_icontains(cls, "weapon"))
2594 return 0;
2595 if (item_icontains(model, "w_crowbar"))
2596 snprintf(label, outn, "Crowbar");
2597 else if (item_icontains(model, "w_pistol"))
2598 snprintf(label, outn, "Pistol");
2599 else if (item_icontains(model, "w_357"))
2600 snprintf(label, outn, "Revolver");
2601 else if (item_icontains(model, "w_smg1") ||
2602 item_icontains(model, "w_smg2"))
2603 snprintf(label, outn, "SMG");
2604 else if (item_icontains(model, "w_ar2") ||
2605 item_icontains(model, "w_irifle"))
2606 snprintf(label, outn, "AR2");
2607 else if (item_icontains(model, "w_shotgun"))
2608 snprintf(label, outn, "Shotgun");
2609 else if (item_icontains(model, "w_crossbow"))
2610 snprintf(label, outn, "Crossbow");
2611 else if (item_icontains(model, "w_rocket_launcher"))
2612 snprintf(label, outn, "RPG");
2613 else if (item_icontains(model, "w_frag") ||
2614 item_icontains(model, "w_grenade"))
2615 snprintf(label, outn, "Grenade");
2616 else if (item_icontains(model, "w_physcannon") ||
2617 item_icontains(model, "w_physics"))
2618 snprintf(label, outn, "Gravity gun");
2619 else if (item_icontains(model, "w_stunstick") ||
2620 item_icontains(model, "w_stunbaton"))
2621 snprintf(label, outn, "Stunstick");
2622 else if (item_icontains(model, "w_bugbait"))
2623 snprintf(label, outn, "Bugbait");
2624 else if (item_icontains(model, "w_sniper"))
2625 snprintf(label, outn, "Sniper");
2626 else if (item_icontains(model, "w_slam"))
2627 snprintf(label, outn, "SLAM");
2628 else
2629 snprintf(label, outn, "%s", model);
2630 return 1;
2631}
2632
2633// Weapon identity for the "owned weapon -> fallen weapon is ammo" rule.
2634enum {
2635 WT_NONE = 0,
2636 WT_PISTOL,
2637 WT_357,
2638 WT_SMG,
2639 WT_AR2,
2640 WT_SHOTGUN,
2641 WT_CROSSBOW,
2642 WT_RPG,
2643 WT_GRENADE,
2644 WT_SNIPER,
2645 WT_COUNT
2646};
2647
2648static int weapon_type_id(const char *model, const char *cls) {
2649 if (item_icontains(model, "pistol") || item_icontains(cls, "pistol"))
2650 return WT_PISTOL;
2651 if (item_icontains(model, "357") || item_icontains(cls, "357"))
2652 return WT_357;
2653 if (item_icontains(model, "smg") || item_icontains(cls, "smg"))
2654 return WT_SMG;
2655 if (item_icontains(model, "ar2") || item_icontains(model, "irifle") ||
2656 item_icontains(cls, "ar2") || item_icontains(cls, "irifle"))
2657 return WT_AR2;
2658 if (item_icontains(model, "shotgun") || item_icontains(cls, "shotgun"))
2659 return WT_SHOTGUN;
2660 if (item_icontains(model, "crossbow") || item_icontains(cls, "crossbow"))
2661 return WT_CROSSBOW;
2662 if (item_icontains(model, "rocket") || item_icontains(cls, "rpg"))
2663 return WT_RPG;
2664 if (item_icontains(model, "grenade") || item_icontains(cls, "frag"))
2665 return WT_GRENADE;
2666 if (item_icontains(model, "sniper") || item_icontains(cls, "sniper"))
2667 return WT_SNIPER;
2668 return WT_NONE;
2669}
2670
2671static int weapon_ammo_type(int wt) {
2672 switch (wt) {
2673 case WT_PISTOL: return 2;
2674 case WT_357: return 4;
2675 case WT_SMG: return 3;
2676 case WT_AR2: return 0;
2677 case WT_SHOTGUN: return 6;
2678 case WT_CROSSBOW: return 5;
2679 case WT_RPG: return 7;
2680 case WT_GRENADE: return 11;
2681 case WT_SNIPER: return 9;
2682 default: return -1;
2683 }
2684}
2685
2686// Ammo type index used by CBasePlayer::m_iAmmo (order of AddAmmoType in
2687// hl2_gamerules.cpp).
2688static int item_ammo_type(const char *model) {
2689 if (!model)
2690 return -1;
2691 if (item_icontains(model, "boxsrounds") ||
2692 item_icontains(model, "ammocrate_pistol") ||
2693 item_icontains(model, "w_pistol"))
2694 return 2;
2695 if (item_icontains(model, "boxmrounds") ||
2696 item_icontains(model, "ammocrate_smg1"))
2697 return 3;
2698 if (item_icontains(model, "boxlrounds") ||
2699 item_icontains(model, "combine_rifle") ||
2700 item_icontains(model, "ammocrate_ar2") ||
2701 item_icontains(model, "w_ar2") || item_icontains(model, "w_irifle"))
2702 return 0;
2703 if (item_icontains(model, "357ammo") ||
2704 item_icontains(model, "357ammobox") ||
2705 item_icontains(model, "w_357"))
2706 return 4;
2707 if (item_icontains(model, "crossbowrounds") ||
2708 item_icontains(model, "w_crossbow"))
2709 return 5;
2710 if (item_icontains(model, "boxbuckshot") ||
2711 item_icontains(model, "ammocrate_buckshot") ||
2712 item_icontains(model, "w_shotgun"))
2713 return 6;
2714 if (item_icontains(model, "w_missile_closed") ||
2715 item_icontains(model, "ammocrate_rockets") ||
2716 item_icontains(model, "w_rocket_launcher"))
2717 return 7;
2718 if (item_icontains(model, "ar2_grenade") ||
2719 item_icontains(model, "ammocrate_smg2") ||
2720 item_icontains(model, "smg1_grenade") ||
2721 item_icontains(model, "smg_grenade"))
2722 return 8;
2723 if (item_icontains(model, "ammocrate_grenade") ||
2724 item_icontains(model, "w_grenade") ||
2725 item_icontains(model, "w_frag") ||
2726 item_icontains(model, "frag_grenade") ||
2727 item_icontains(model, "items/grenade"))
2728 return 11;
2729 if (item_icontains(model, "w_smg1") || item_icontains(model, "w_smg2"))
2730 return 3;
2731 return -1;
2732}
2733
2734static int convar_int(const char *name) {
2735 if (!g_iface.cvar || !name)
2736 return -1;
2737 using FindFn = void *(*)(void *, const char *);
2738 void *v = vfunc<FindFn>(g_iface.cvar, 12)(g_iface.cvar, name);
2739 if (!v)
2740 return -1;
2741 return (int)*(float *)((uintptr_t)v + 0x2c); // ConVar m_Value.f
2742}
2743
2744// Maximum reserve ammo for an m_iAmmo index. Prefers the sk_max_* cvar and
2745// falls back to the stock HL2 cap so the RPG's 3-rocket limit is honoured
2746// even when server cvars are not visible from the client.
2747static int ammo_max(int type) {
2748 static int cache[16];
2749 static bool init = false;
2750 if (type < 0 || type >= 16)
2751 return -1;
2752 if (!init) {
2753 for (int i = 0; i < 16; i++)
2754 cache[i] = -2;
2755 init = true;
2756 }
2757 if (cache[type] != -2)
2758 return cache[type];
2759 const char *name = nullptr;
2760 int stock = -1;
2761 switch (type) {
2762 case 0: name = "sk_max_ar2"; stock = 60; break;
2763 case 2: name = "sk_max_pistol"; stock = 150; break;
2764 case 3: name = "sk_max_smg1"; stock = 225; break;
2765 case 4: name = "sk_max_357"; stock = 12; break;
2766 case 5: name = "sk_max_crossbow"; stock = 10; break;
2767 case 6: name = "sk_max_buckshot"; stock = 30; break;
2768 case 7: name = "sk_max_rpg_round"; stock = 3; break;
2769 case 8: name = "sk_max_smg1_grenade"; stock = 3; break;
2770 case 9: name = "sk_max_sniper_round"; stock = 30; break;
2771 case 11: name = "sk_max_grenade"; stock = 5; break;
2772 default: break;
2773 }
2774 int v = convar_int(name);
2775 if (v <= 0)
2776 v = stock;
2777 cache[type] = v;
2778 return v;
2779}
2780
2781static int local_ammo(void *local_ent, int type) {
2782 if (type < 0 || type >= 32)
2783 return -1;
2784 // Prefer the authoritative server array; the client m_iAmmo netvar offset
2785 // can be off by an element, which made "full" counts flicker as other ammo
2786 // was spent. server_player_ammo already skips the CNetworkArray header.
2787 int local = engine_get_local_player();
2788 if (local > 0 && g_iface.engineserver) {
2789 resolve_server_netvars();
2790 int s = server_player_ammo(local, type);
2791 if (s >= 0)
2792 return s;
2793 }
2794 if (local_ent && g_net.ammo >= 0)
2795 return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4);
2796 return -1;
2797}
2798
2799static int classify_item(const char *model, const char *cls, char *label,
2800 size_t outn) {
2801 if (!model && !cls)
2802 return -1;
2803
2804 // Breakable prop chunks are debris, not pickups.
2805 if (item_icontains(model, "chunk"))
2806 return -1;
2807
2808 if (item_icontains(model, "healthkit")) {
2809 snprintf(label, outn, "Medkit");
2810 return IK_HEALTH;
2811 }
2812 if (item_icontains(model, "healthvial")) {
2813 snprintf(label, outn, "Health vial");
2814 return IK_HEALTH;
2815 }
2816 if (item_icontains(model, "health_charger")) {
2817 snprintf(label, outn, "Health charger");
2818 return IK_HEALTH;
2819 }
2820 if (item_icontains(model, "battery")) {
2821 snprintf(label, outn, "Battery");
2822 return IK_ARMOR;
2823 }
2824 if (item_icontains(model, "hevsuit")) {
2825 snprintf(label, outn, "HEV suit");
2826 return IK_ARMOR;
2827 }
2828 if (item_icontains(model, "w_longjump")) {
2829 snprintf(label, outn, "Long jump");
2830 return IK_ARMOR;
2831 }
2832 if (item_icontains(model, "suit_charger")) {
2833 snprintf(label, outn, "Suit charger");
2834 return IK_ARMOR;
2835 }
2836
2837 // Only the two item-crate models are supply crates. Other crates are just
2838 // breakable map props and deliberately not tagged.
2839 if (item_icontains(model, "item_item_crate") ||
2840 item_icontains(model, "item_beacon_crate")) {
2841 snprintf(label, outn, "Supply crate");
2842 return IK_CRATE;
2843 }
2844
2845 if (item_icontains(model, "ammocrate_pistol")) {
2846 snprintf(label, outn, "Pistol ammo crate");
2847 return IK_AMMO;
2848 }
2849 if (item_icontains(model, "ammocrate_smg1")) {
2850 snprintf(label, outn, "SMG ammo crate");
2851 return IK_AMMO;
2852 }
2853 if (item_icontains(model, "ammocrate_ar2")) {
2854 snprintf(label, outn, "AR2 ammo crate");
2855 return IK_AMMO;
2856 }
2857 if (item_icontains(model, "ammocrate_rockets")) {
2858 snprintf(label, outn, "RPG ammo crate");
2859 return IK_AMMO;
2860 }
2861 if (item_icontains(model, "ammocrate_buckshot")) {
2862 snprintf(label, outn, "Shotgun ammo crate");
2863 return IK_AMMO;
2864 }
2865 if (item_icontains(model, "ammocrate_grenade")) {
2866 snprintf(label, outn, "Grenade crate");
2867 return IK_AMMO;
2868 }
2869 if (item_icontains(model, "ammocrate_smg2")) {
2870 snprintf(label, outn, "SMG grenade crate");
2871 return IK_AMMO;
2872 }
2873 if (item_icontains(model, "ammocrate")) {
2874 snprintf(label, outn, "Ammo crate");
2875 return IK_AMMO;
2876 }
2877
2878 if (item_icontains(model, "w_missile_closed")) {
2879 snprintf(label, outn, "RPG rocket");
2880 return IK_AMMO;
2881 }
2882 if (item_icontains(model, "boxsrounds")) {
2883 snprintf(label, outn, "Pistol ammo");
2884 return IK_AMMO;
2885 }
2886 if (item_icontains(model, "boxmrounds")) {
2887 snprintf(label, outn, "SMG ammo");
2888 return IK_AMMO;
2889 }
2890 if (item_icontains(model, "boxlrounds") ||
2891 item_icontains(model, "combine_rifle_cartridge") ||
2892 item_icontains(model, "combine_rifle_ammo")) {
2893 snprintf(label, outn, "AR2 ammo");
2894 return IK_AMMO;
2895 }
2896 if (item_icontains(model, "357ammo") ||
2897 item_icontains(model, "357ammobox")) {
2898 snprintf(label, outn, ".357 ammo");
2899 return IK_AMMO;
2900 }
2901 if (item_icontains(model, "crossbowrounds")) {
2902 snprintf(label, outn, "Crossbow bolts");
2903 return IK_AMMO;
2904 }
2905 if (item_icontains(model, "boxbuckshot")) {
2906 snprintf(label, outn, "Shotgun shells");
2907 return IK_AMMO;
2908 }
2909 if (item_icontains(model, "boxsniperrounds")) {
2910 snprintf(label, outn, "Sniper ammo");
2911 return IK_AMMO;
2912 }
2913 if (item_icontains(model, "boxflares") ||
2914 item_icontains(model, "models/items/flare")) {
2915 snprintf(label, outn, "Flares");
2916 return IK_AMMO;
2917 }
2918 if (item_icontains(model, "ar2_grenade")) {
2919 snprintf(label, outn, "AR2 grenade");
2920 return IK_AMMO;
2921 }
2922 if (item_icontains(model, "w_grenade") ||
2923 item_icontains(model, "w_frag") ||
2924 item_icontains(model, "frag_grenade") ||
2925 item_icontains(model, "items/grenade")) {
2926 snprintf(label, outn, "Frag grenade");
2927 return IK_AMMO;
2928 }
2929 if (item_icontains(model, "models/items/")) {
2930 snprintf(label, outn, "Ammo");
2931 return IK_AMMO;
2932 }
2933
2934 if (classify_weapon(model, cls, label, outn) == 1)
2935 return IK_WEAPON;
2936
2937 if (item_icontains(cls, "healthkit") || item_icontains(cls, "healthvial") ||
2938 item_icontains(cls, "wallhealth") ||
2939 item_icontains(cls, "healthcharger")) {
2940 snprintf(label, outn, "Health");
2941 return IK_HEALTH;
2942 }
2943 if (item_icontains(cls, "battery") || item_icontains(cls, "itemsuit") ||
2944 item_icontains(cls, "recharge")) {
2945 snprintf(label, outn, "Armor");
2946 return IK_ARMOR;
2947 }
2948 if (item_icontains(cls, "itemcrate")) {
2949 snprintf(label, outn, "Supply crate");
2950 return IK_CRATE;
2951 }
2952 if (item_icontains(cls, "item_ammo") || item_icontains(cls, "itemammo")) {
2953 snprintf(label, outn, "Ammo");
2954 return IK_AMMO;
2955 }
2956 if (item_icontains(cls, "weapon")) {
2957 snprintf(label, outn, "Weapon");
2958 return IK_WEAPON;
2959 }
2960 if (item_icontains(cls, "item") || item_icontains(cls, "pickup")) {
2961 snprintf(label, outn, "Item");
2962 return IK_OTHER;
2963 }
2964 return -1;
2965}
2966
2967static bool item_kind_enabled(int kind) {
2968 switch (kind) {
2969 case IK_HEALTH: return g_item_health;
2970 case IK_AMMO: return g_item_ammo;
2971 case IK_ARMOR: return g_item_armor;
2972 case IK_WEAPON: return g_item_weapons;
2973 case IK_CRATE: return g_item_crates;
2974 default: return true;
2975 }
2976}
2977
2978// Health chargers are brushes (func_healthcharger/CWallHealth) and suit
2979// chargers are item_suitcharger/CNewRecharge. Both expose m_iJuice.
2980static bool item_is_charger(const char *model, const char *cls) {
2981 return item_icontains(model, "charger") ||
2982 item_icontains(model, "suit_charger") ||
2983 item_icontains(cls, "charger") ||
2984 item_icontains(cls, "wallhealth") ||
2985 item_icontains(cls, "recharge");
2986}
2987
2988static bool item_is_suit_charger(const char *model, const char *cls) {
2989 return item_icontains(model, "suit_charger") ||
2990 item_icontains(model, "suitcharger") ||
2991 item_icontains(cls, "recharge") ||
2992 item_icontains(cls, "suitcharger");
2993}
2994
2995static void items_collect(int local_idx) {
2996 static double last_scan = 0.0;
2997 double now = esp_now();
2998 if (now - last_scan < 0.10)
2999 return;
3000 last_scan = now;
3001 g_nitems = 0;
3002 if (!g_iface.entitylist || g_net.origin < 0 || g_net.modelindex < 0)
3003 return;
3004 resolve_server_netvars();
3005
3006 using Fn = int (*)(void *);
3007 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
3008 if (hi < 0 || hi > 4096)
3009 hi = 4096;
3010 int active_idx = local_active_weapon_idx(local_idx);
3011
3012 // Which weapon types the local player already carries. A fallen weapon of
3013 // the same type is then just an ammo pickup. Owner handles are unreliable on
3014 // this client (owner/weapon_owner netvars resolve to -1), so remember every
3015 // type the player has held since the current local entity appeared.
3016 static uint32_t owned_mask = 0;
3017 static void *last_local = nullptr;
3018 void *local_ent = entitylist_get(local_idx);
3019 if (local_ent != last_local) {
3020 last_local = local_ent;
3021 owned_mask = 0;
3022 }
3023 int owned_idx = active_idx;
3024 if (owned_idx <= 0 && g_iface.engineserver) {
3025 resolve_server_netvars();
3026 owned_idx = server_active_weapon(local_idx);
3027 }
3028 if (owned_idx > 0) {
3029 void *aw = entitylist_get(owned_idx);
3030 void *anet = entity_networkable(owned_idx);
3031 const char *acls = anet ? entity_class_name(anet, nullptr) : nullptr;
3032 const char *amodel = aw ? entity_model_name(aw) : nullptr;
3033 int at = weapon_type_id(amodel, acls);
3034 if (at > 0)
3035 owned_mask |= 1u << at;
3036 }
3037 for (int idx = 1; idx <= hi; idx++) {
3038 if (idx == local_idx)
3039 continue;
3040 void *ent = entitylist_get(idx);
3041 if (!ent || item_weapon_owner_idx(ent) != local_idx)
3042 continue;
3043 void *net = entity_networkable(idx);
3044 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
3045 const char *model = entity_model_name(ent);
3046 int wt = weapon_type_id(model, cls);
3047 if (wt > 0)
3048 owned_mask |= 1u << wt;
3049 }
3050 // Server-side inventory, independent of the client recv-table arrays.
3051 if (g_iface.engineserver) {
3052 for (int slot = 0; slot < 48; slot++) {
3053 uint32_t h = server_player_weapon(local_idx, slot);
3054 if (!item_handle_valid(h))
3055 continue;
3056 int wi = (int)(h & 0x7FF);
3057 if (wi <= 0 || wi > 4096)
3058 continue;
3059 void *wep = entitylist_get(wi);
3060 void *net = entity_networkable(wi);
3061 const char *wc = net ? entity_class_name(net, nullptr) : nullptr;
3062 const char *wm = wep ? entity_model_name(wep) : nullptr;
3063 if (!wm) {
3064 void *sb = nullptr;
3065 server_entity_health(wi, nullptr, nullptr, &sb);
3066 if (sb)
3067 wm = model_name_from_index(server_model_index(sb));
3068 }
3069 int wt = weapon_type_id(wm, wc);
3070 if (wt > 0)
3071 owned_mask |= 1u << wt;
3072 }
3073 }
3074 bool owned_weapon[WT_COUNT] = {};
3075 for (int t = 1; t < WT_COUNT; t++)
3076 owned_weapon[t] = (owned_mask & (1u << t)) != 0;
3077 static bool cats_logged = false;
3078 if (!cats_logged) {
3079 cats_logged = true;
3080 char maxbuf[16];
3081 if (g_item_max_dist == 0)
3082 snprintf(maxbuf, sizeof(maxbuf), "All");
3083 else
3084 snprintf(maxbuf, sizeof(maxbuf), "%d", g_item_max_dist);
3085 logf("items: categories health=%d ammo=%d armor=%d weapons=%d "
3086 "crates=%d distance=%d max=%s",
3087 (int)g_item_health, (int)g_item_ammo, (int)g_item_armor,
3088 (int)g_item_weapons, (int)g_item_crates, (int)g_item_distance,
3089 maxbuf);
3090 }
3091 static int dbg_lines = 0;
3092 static bool owner_warned = false;
3093 if (!owner_warned && g_net.owner < 0 && g_net.weapon_owner < 0) {
3094 owner_warned = true;
3095 logf("items: weapon owner netvar unavailable; held weapons may tag");
3096 }
3097
3098 for (int idx = 1; idx <= hi && g_nitems < 192; idx++) {
3099 if (idx == local_idx)
3100 continue;
3101 void *ent = entitylist_get(idx);
3102 if (!ent)
3103 continue;
3104 void *net = entity_networkable(idx);
3105 if (!net || entity_is_dormant(net))
3106 continue;
3107 const char *cls = entity_class_name(net, nullptr);
3108 const char *scls = nullptr;
3109 if (g_iface.engineserver &&
3110 (!cls || !*cls || strcmp(cls, "CBaseEntity") == 0))
3111 scls = server_entity_classname(idx);
3112 const char *model = entity_model_name(ent);
3113 // CPhysicsProp's client m_nModelIndex is often 0 (e.g. item_item_crate);
3114 // fall back to the server entity's model index, which is set in Spawn().
3115 if ((!model || !*model) && g_iface.engineserver) {
3116 void *sbase = nullptr;
3117 server_entity_health(idx, nullptr, nullptr, &sbase);
3118 if (sbase)
3119 model = model_name_from_index(server_model_index(sbase));
3120 }
3121 char label[128];
3122 int kind = classify_item(model, cls, label, sizeof(label));
3123 if (kind < 0 && scls)
3124 kind = classify_item(model, scls, label, sizeof(label));
3125 if (kind < 0)
3126 continue;
3127 // Health/suit chargers are dispensers that stay useful after a refill, but
3128 // an empty suit charger cannot give anything. The health charger's
3129 // m_iJuice offset is still being verified (it read 0 on a full charger),
3130 // so it is left visible for now; server_item_juice logs both candidate
3131 // values for diagnosis.
3132 bool is_charger = item_is_charger(model, cls) ||
3133 (scls && item_is_charger(model, scls));
3134 bool is_suit = item_is_suit_charger(model, cls) ||
3135 (scls && item_is_suit_charger(model, scls));
3136 if (is_charger) {
3137 int juice = server_item_juice(idx);
3138 if (is_suit && juice == 0)
3139 continue;
3140 }
3141
3142 Vector origin{*(float *)((uintptr_t)ent + g_net.origin),
3143 *(float *)((uintptr_t)ent + g_net.origin + 4),
3144 *(float *)((uintptr_t)ent + g_net.origin + 8)};
3145 float dx = origin.x - g_cam_eye.x;
3146 float dy = origin.y - g_cam_eye.y;
3147 float dz = origin.z - g_cam_eye.z;
3148 float dist = sqrtf(dx * dx + dy * dy + dz * dz);
3149
3150 if (kind == IK_WEAPON) {
3151 if (idx == active_idx || item_weapon_is_owned(ent))
3152 continue;
3153 int wt = weapon_type_id(model, cls);
3154 if (wt > 0 && owned_weapon[wt]) {
3155 char base_label[160];
3156 snprintf(base_label, sizeof(base_label), "%s", label);
3157 snprintf(label, sizeof(label), "%.100s ammo", base_label);
3158 kind = IK_AMMO;
3159 }
3160 }
3161 // Distance applies to every tag; 0 means unlimited.
3162 if (g_item_max_dist > 0 && dist > (float)g_item_max_dist)
3163 continue;
3164
3165 ItemTarget &t = g_item_targets[g_nitems++];
3166 t.idx = idx;
3167 t.kind = kind;
3168 t.ammo_type = -1;
3169 if (kind == IK_AMMO) {
3170 t.ammo_type = item_ammo_type(model);
3171 if (t.ammo_type < 0)
3172 t.ammo_type = weapon_ammo_type(weapon_type_id(model, cls));
3173 }
3174 t.origin = origin;
3175 snprintf(t.label, sizeof(t.label), "%s", label);
3176 if (dbg_lines < 8) {
3177 dbg_lines++;
3178 logd("item: idx=%d kind=%d cls=%s model=%s", idx, kind,
3179 cls ? cls : "?", model ? model : "?");
3180 }
3181 }
3182
3183 if (!g_items_logged) {
3184 g_items_logged = true;
3185 logf("items: tagging %d valuable entities", g_nitems);
3186 }
3187}
3188
3189// Authoritative local health/armor for the "only if usable" item filter.
3190// The health ceiling comes from the server player's m_iMaxHealth, not a
3191// hardcoded 100; when it cannot be read the filter fails open.
3192static void local_vitals(int &hp, int &maxhp, int &armor) {
3193 hp = maxhp = armor = -1;
3194 int local = engine_get_local_player();
3195 if (local <= 0)
3196 return;
3197 void *le = entitylist_get(local);
3198 if (le && g_net.health >= 0)
3199 hp = *(int *)((uintptr_t)le + g_net.health);
3200 if (g_iface.engineserver) {
3201 int smax = -1;
3202 int shp = server_entity_health(local, &smax, nullptr, nullptr);
3203 if (shp >= 0)
3204 hp = shp;
3205 if (smax > 0)
3206 maxhp = smax;
3207 armor = server_player_armor(local);
3208 }
3209 if (maxhp < 0 && le && g_net.maxhealth >= 0)
3210 maxhp = *(int *)((uintptr_t)le + g_net.maxhealth);
3211}
3212
3213void esp_draw_items() {
3214 if (!g_surface || !g_iface.entitylist || g_net.origin < 0)
3215 return;
3216 if (!g_item_health && !g_item_ammo && !g_item_armor && !g_item_weapons &&
3217 !g_item_crates)
3218 return;
3219 int local = engine_get_local_player();
3220 if (local <= 0)
3221 return;
3222 if (!esp_update_screen())
3223 return;
3224 items_collect(local);
3225
3226 int vit_hp = -1, vit_max = -1, vit_armor = -1;
3227 if (g_item_usable)
3228 local_vitals(vit_hp, vit_max, vit_armor);
3229 static bool ammo_logged = false;
3230 if (g_item_usable && !ammo_logged) {
3231 ammo_logged = true;
3232 char buf[512];
3233 int pos = 0;
3234 void *le = entitylist_get(local);
3235 for (int t = 0; t < 16 && pos < 500; t++) {
3236 int a = local_ammo(le, t);
3237 int m = ammo_max(t);
3238 if (a >= 0 && m > 0)
3239 pos += snprintf(buf + pos, sizeof(buf) - pos, " %d:%d/%d", t, a, m);
3240 }
3241 logf("items: ammo%s", buf);
3242 }
3243
3244 for (int i = 0; i < g_nitems; i++) {
3245 ItemTarget &t = g_item_targets[i];
3246 // Follow the entity between the 10 Hz discovery scans.
3247 Vector live;
3248 if (entity_origin(entitylist_get(t.idx), live))
3249 t.origin = live;
3250 if (!item_kind_enabled(t.kind))
3251 continue;
3252 // Dispensers (chargers, ammo crates, supply crates) stay useful after a
3253 // refill, so the fullness filter does not apply to them.
3254 bool dispenser = strcasestr(t.label, "crate") ||
3255 strcasestr(t.label, "charger");
3256 if (g_item_usable && !dispenser) {
3257 if (t.kind == IK_HEALTH && vit_hp >= 0 && vit_max > 0 &&
3258 vit_hp >= vit_max)
3259 continue;
3260 if (t.kind == IK_ARMOR && vit_armor >= 0 && vit_armor >= 100)
3261 continue;
3262 if (t.kind == IK_AMMO && t.ammo_type >= 0) {
3263 int maxa = ammo_max(t.ammo_type);
3264 int cura = local_ammo(entitylist_get(local), t.ammo_type);
3265 if (maxa > 0 && cura >= 0 && cura >= maxa)
3266 continue;
3267 }
3268 }
3269 float sx, sy;
3270 if (!world_to_screen(t.origin, sx, sy))
3271 continue;
3272 if (sx < -40 || sx > (float)g_scr_w + 40 || sy < -20 ||
3273 sy > (float)g_scr_h + 20)
3274 continue;
3275
3276 int r = 230, g = 230, b = 230;
3277 switch (t.kind) {
3278 case IK_HEALTH: r = 90; g = 255; b = 130; break;
3279 case IK_AMMO: r = 255; g = 210; b = 80; break;
3280 case IK_ARMOR: r = 90; g = 180; b = 255; break;
3281 case IK_WEAPON: r = 120; g = 230; b = 255; break;
3282 case IK_CRATE: r = 220; g = 150; b = 255; break;
3283 default: break;
3284 }
3285 int x = (int)sx, y = (int)sy;
3286 int rad = (t.kind == IK_CRATE) ? 7 : 4;
3287 surf_color(r, g, b, 255);
3288 surf_rect(0, x - rad, y - rad, x + rad, y + rad);
3289 surf_line(x - rad - 4, y, x - rad - 1, y);
3290 surf_line(x + rad + 1, y, x + rad + 4, y);
3291 surf_line(x, y - rad - 4, x, y - rad - 1);
3292 surf_line(x, y + rad + 1, x, y + rad + 4);
3293 if (t.kind == IK_CRATE)
3294 surf_rect(1, x - 2, y - 2, x + 2, y + 2);
3295
3296 float dx = t.origin.x - g_cam_eye.x;
3297 float dy = t.origin.y - g_cam_eye.y;
3298 float dz = t.origin.z - g_cam_eye.z;
3299 int dist = (int)sqrtf(dx * dx + dy * dy + dz * dz);
3300 char text[192];
3301 if (g_item_distance)
3302 snprintf(text, sizeof(text), "%s %du", t.label, dist);
3303 else
3304 snprintf(text, sizeof(text), "%s", t.label);
3305 int tw = (int)strlen(text) * 9;
3306 int tx = x + 10;
3307 if (tx + tw > g_scr_w - 4)
3308 tx = x - 10 - tw;
3309 if (tx < 4)
3310 tx = 4;
3311 int ty = y - 7;
3312 if (ty < 0)
3313 ty = 0;
3314 if (ty + 16 > g_scr_h)
3315 ty = g_scr_h - 16;
3316 draw_text_col(tx, ty, text, r, g, b);
3317 }
3318}
3319
3320// ---------------------------------------------------------------------------
3321// projectile ESP: grenades, rockets, helicopter bombs, AR2 balls
3322// ---------------------------------------------------------------------------
3323
diff --git a/src/esp_internal.h b/src/esp_internal.h
index 15178a7..6e66f42 100644
--- a/src/esp_internal.h
+++ b/src/esp_internal.h
@@ -18,6 +18,7 @@ bool esp_update_screen();
18void *entity_networkable(int idx); 18void *entity_networkable(int idx);
19const char *entity_class_name(void *net, void **out_cc); 19const char *entity_class_name(void *net, void **out_cc);
20const char *entity_model_name(void *ent); 20const char *entity_model_name(void *ent);
21const char *model_name_from_index(int modelindex);
21bool entity_origin(void *ent, Vector &out); 22bool entity_origin(void *ent, Vector &out);
22bool entity_is_dormant(void *net); 23bool entity_is_dormant(void *net);
23bool active_weapon_contains(void *local_ent, const char *needle); 24bool active_weapon_contains(void *local_ent, const char *needle);
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 @@
1// items.cpp — item ESP (discovery, classification, drawing).
2#include "esp_internal.h"
3#include "hl2h.h"
4#include <cmath>
5#include <cstdio>
6#include <cstring>
7
8
9enum ItemKind {
10 IK_HEALTH,
11 IK_AMMO,
12 IK_ARMOR,
13 IK_WEAPON,
14 IK_CRATE,
15 IK_OTHER
16};
17
18struct ItemTarget {
19 int idx;
20 int kind;
21 int ammo_type; // m_iAmmo index for IK_AMMO, -1 otherwise
22 Vector origin;
23 char label[128];
24};
25
26static ItemTarget g_item_targets[192];
27static int g_nitems = 0;
28static bool g_items_logged = false;
29
30static bool item_icontains(const char *hay, const char *needle) {
31 if (!hay || !needle || !*needle)
32 return false;
33 size_t n = strlen(needle);
34 for (const char *p = hay; *p; p++)
35 if (strncasecmp(p, needle, n) == 0)
36 return true;
37 return false;
38}
39
40static bool item_handle_valid(uint32_t h) {
41 return h != 0 && h != 0xFFFFFFFFu;
42}
43
44// Weapons held by the player or an NPC are not world pickups.
45bool item_weapon_is_owned(void *ent) {
46 if (!ent)
47 return false;
48 if (g_net.owner >= 0) {
49 uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.owner);
50 if (item_handle_valid(h))
51 return true;
52 }
53 if (g_net.weapon_owner >= 0) {
54 uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner);
55 if (item_handle_valid(h))
56 return true;
57 }
58 return false;
59}
60
61// Entity index of the weapon's owner, or -1. Used to distinguish the local
62// player's inventory from NPC weapons.
63static int item_weapon_owner_idx(void *ent) {
64 if (!ent)
65 return -1;
66 uint32_t h = 0;
67 if (g_net.owner >= 0)
68 h = *(uint32_t *)((uintptr_t)ent + g_net.owner);
69 if (!item_handle_valid(h) && g_net.weapon_owner >= 0)
70 h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner);
71 if (!item_handle_valid(h))
72 return -1;
73 return (int)(h & 0x7FF);
74}
75
76static int local_active_weapon_idx(int local_idx) {
77 if (local_idx <= 0 || g_net.activeweapon < 0)
78 return -1;
79 void *local = entitylist_get(local_idx);
80 if (!local)
81 return -1;
82 uint32_t h = *(uint32_t *)((uintptr_t)local + g_net.activeweapon);
83 if (!item_handle_valid(h))
84 return -1;
85 return (int)(h & 0x7FF);
86}
87
88static int classify_weapon(const char *model, const char *cls, char *label,
89 size_t outn) {
90 if (!item_icontains(model, "models/weapons/w_") &&
91 !item_icontains(cls, "weapon"))
92 return 0;
93 if (item_icontains(model, "w_crowbar"))
94 snprintf(label, outn, "Crowbar");
95 else if (item_icontains(model, "w_pistol"))
96 snprintf(label, outn, "Pistol");
97 else if (item_icontains(model, "w_357"))
98 snprintf(label, outn, "Revolver");
99 else if (item_icontains(model, "w_smg1") ||
100 item_icontains(model, "w_smg2"))
101 snprintf(label, outn, "SMG");
102 else if (item_icontains(model, "w_ar2") ||
103 item_icontains(model, "w_irifle"))
104 snprintf(label, outn, "AR2");
105 else if (item_icontains(model, "w_shotgun"))
106 snprintf(label, outn, "Shotgun");
107 else if (item_icontains(model, "w_crossbow"))
108 snprintf(label, outn, "Crossbow");
109 else if (item_icontains(model, "w_rocket_launcher"))
110 snprintf(label, outn, "RPG");
111 else if (item_icontains(model, "w_frag") ||
112 item_icontains(model, "w_grenade"))
113 snprintf(label, outn, "Grenade");
114 else if (item_icontains(model, "w_physcannon") ||
115 item_icontains(model, "w_physics"))
116 snprintf(label, outn, "Gravity gun");
117 else if (item_icontains(model, "w_stunstick") ||
118 item_icontains(model, "w_stunbaton"))
119 snprintf(label, outn, "Stunstick");
120 else if (item_icontains(model, "w_bugbait"))
121 snprintf(label, outn, "Bugbait");
122 else if (item_icontains(model, "w_sniper"))
123 snprintf(label, outn, "Sniper");
124 else if (item_icontains(model, "w_slam"))
125 snprintf(label, outn, "SLAM");
126 else
127 snprintf(label, outn, "%s", model);
128 return 1;
129}
130
131// Weapon identity for the "owned weapon -> fallen weapon is ammo" rule.
132enum {
133 WT_NONE = 0,
134 WT_PISTOL,
135 WT_357,
136 WT_SMG,
137 WT_AR2,
138 WT_SHOTGUN,
139 WT_CROSSBOW,
140 WT_RPG,
141 WT_GRENADE,
142 WT_SNIPER,
143 WT_COUNT
144};
145
146static int weapon_type_id(const char *model, const char *cls) {
147 if (item_icontains(model, "pistol") || item_icontains(cls, "pistol"))
148 return WT_PISTOL;
149 if (item_icontains(model, "357") || item_icontains(cls, "357"))
150 return WT_357;
151 if (item_icontains(model, "smg") || item_icontains(cls, "smg"))
152 return WT_SMG;
153 if (item_icontains(model, "ar2") || item_icontains(model, "irifle") ||
154 item_icontains(cls, "ar2") || item_icontains(cls, "irifle"))
155 return WT_AR2;
156 if (item_icontains(model, "shotgun") || item_icontains(cls, "shotgun"))
157 return WT_SHOTGUN;
158 if (item_icontains(model, "crossbow") || item_icontains(cls, "crossbow"))
159 return WT_CROSSBOW;
160 if (item_icontains(model, "rocket") || item_icontains(cls, "rpg"))
161 return WT_RPG;
162 if (item_icontains(model, "grenade") || item_icontains(cls, "frag"))
163 return WT_GRENADE;
164 if (item_icontains(model, "sniper") || item_icontains(cls, "sniper"))
165 return WT_SNIPER;
166 return WT_NONE;
167}
168
169static int weapon_ammo_type(int wt) {
170 switch (wt) {
171 case WT_PISTOL: return 2;
172 case WT_357: return 4;
173 case WT_SMG: return 3;
174 case WT_AR2: return 0;
175 case WT_SHOTGUN: return 6;
176 case WT_CROSSBOW: return 5;
177 case WT_RPG: return 7;
178 case WT_GRENADE: return 11;
179 case WT_SNIPER: return 9;
180 default: return -1;
181 }
182}
183
184// Ammo type index used by CBasePlayer::m_iAmmo (order of AddAmmoType in
185// hl2_gamerules.cpp).
186static int item_ammo_type(const char *model) {
187 if (!model)
188 return -1;
189 if (item_icontains(model, "boxsrounds") ||
190 item_icontains(model, "ammocrate_pistol") ||
191 item_icontains(model, "w_pistol"))
192 return 2;
193 if (item_icontains(model, "boxmrounds") ||
194 item_icontains(model, "ammocrate_smg1"))
195 return 3;
196 if (item_icontains(model, "boxlrounds") ||
197 item_icontains(model, "combine_rifle") ||
198 item_icontains(model, "ammocrate_ar2") ||
199 item_icontains(model, "w_ar2") || item_icontains(model, "w_irifle"))
200 return 0;
201 if (item_icontains(model, "357ammo") ||
202 item_icontains(model, "357ammobox") ||
203 item_icontains(model, "w_357"))
204 return 4;
205 if (item_icontains(model, "crossbowrounds") ||
206 item_icontains(model, "w_crossbow"))
207 return 5;
208 if (item_icontains(model, "boxbuckshot") ||
209 item_icontains(model, "ammocrate_buckshot") ||
210 item_icontains(model, "w_shotgun"))
211 return 6;
212 if (item_icontains(model, "w_missile_closed") ||
213 item_icontains(model, "ammocrate_rockets") ||
214 item_icontains(model, "w_rocket_launcher"))
215 return 7;
216 if (item_icontains(model, "ar2_grenade") ||
217 item_icontains(model, "ammocrate_smg2") ||
218 item_icontains(model, "smg1_grenade") ||
219 item_icontains(model, "smg_grenade"))
220 return 8;
221 if (item_icontains(model, "ammocrate_grenade") ||
222 item_icontains(model, "w_grenade") ||
223 item_icontains(model, "w_frag") ||
224 item_icontains(model, "frag_grenade") ||
225 item_icontains(model, "items/grenade"))
226 return 11;
227 if (item_icontains(model, "w_smg1") || item_icontains(model, "w_smg2"))
228 return 3;
229 return -1;
230}
231
232static int convar_int(const char *name) {
233 if (!g_iface.cvar || !name)
234 return -1;
235 using FindFn = void *(*)(void *, const char *);
236 void *v = vfunc<FindFn>(g_iface.cvar, 12)(g_iface.cvar, name);
237 if (!v)
238 return -1;
239 return (int)*(float *)((uintptr_t)v + 0x2c); // ConVar m_Value.f
240}
241
242// Maximum reserve ammo for an m_iAmmo index. Prefers the sk_max_* cvar and
243// falls back to the stock HL2 cap so the RPG's 3-rocket limit is honoured
244// even when server cvars are not visible from the client.
245static int ammo_max(int type) {
246 static int cache[16];
247 static bool init = false;
248 if (type < 0 || type >= 16)
249 return -1;
250 if (!init) {
251 for (int i = 0; i < 16; i++)
252 cache[i] = -2;
253 init = true;
254 }
255 if (cache[type] != -2)
256 return cache[type];
257 const char *name = nullptr;
258 int stock = -1;
259 switch (type) {
260 case 0: name = "sk_max_ar2"; stock = 60; break;
261 case 2: name = "sk_max_pistol"; stock = 150; break;
262 case 3: name = "sk_max_smg1"; stock = 225; break;
263 case 4: name = "sk_max_357"; stock = 12; break;
264 case 5: name = "sk_max_crossbow"; stock = 10; break;
265 case 6: name = "sk_max_buckshot"; stock = 30; break;
266 case 7: name = "sk_max_rpg_round"; stock = 3; break;
267 case 8: name = "sk_max_smg1_grenade"; stock = 3; break;
268 case 9: name = "sk_max_sniper_round"; stock = 30; break;
269 case 11: name = "sk_max_grenade"; stock = 5; break;
270 default: break;
271 }
272 int v = convar_int(name);
273 if (v <= 0)
274 v = stock;
275 cache[type] = v;
276 return v;
277}
278
279static int local_ammo(void *local_ent, int type) {
280 if (type < 0 || type >= 32)
281 return -1;
282 // Prefer the authoritative server array; the client m_iAmmo netvar offset
283 // can be off by an element, which made "full" counts flicker as other ammo
284 // was spent. server_player_ammo already skips the CNetworkArray header.
285 int local = engine_get_local_player();
286 if (local > 0 && g_iface.engineserver) {
287 resolve_server_netvars();
288 int s = server_player_ammo(local, type);
289 if (s >= 0)
290 return s;
291 }
292 if (local_ent && g_net.ammo >= 0)
293 return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4);
294 return -1;
295}
296
297static int classify_item(const char *model, const char *cls, char *label,
298 size_t outn) {
299 if (!model && !cls)
300 return -1;
301
302 // Breakable prop chunks are debris, not pickups.
303 if (item_icontains(model, "chunk"))
304 return -1;
305
306 if (item_icontains(model, "healthkit")) {
307 snprintf(label, outn, "Medkit");
308 return IK_HEALTH;
309 }
310 if (item_icontains(model, "healthvial")) {
311 snprintf(label, outn, "Health vial");
312 return IK_HEALTH;
313 }
314 if (item_icontains(model, "health_charger")) {
315 snprintf(label, outn, "Health charger");
316 return IK_HEALTH;
317 }
318 if (item_icontains(model, "battery")) {
319 snprintf(label, outn, "Battery");
320 return IK_ARMOR;
321 }
322 if (item_icontains(model, "hevsuit")) {
323 snprintf(label, outn, "HEV suit");
324 return IK_ARMOR;
325 }
326 if (item_icontains(model, "w_longjump")) {
327 snprintf(label, outn, "Long jump");
328 return IK_ARMOR;
329 }
330 if (item_icontains(model, "suit_charger")) {
331 snprintf(label, outn, "Suit charger");
332 return IK_ARMOR;
333 }
334
335 // Only the two item-crate models are supply crates. Other crates are just
336 // breakable map props and deliberately not tagged.
337 if (item_icontains(model, "item_item_crate") ||
338 item_icontains(model, "item_beacon_crate")) {
339 snprintf(label, outn, "Supply crate");
340 return IK_CRATE;
341 }
342
343 if (item_icontains(model, "ammocrate_pistol")) {
344 snprintf(label, outn, "Pistol ammo crate");
345 return IK_AMMO;
346 }
347 if (item_icontains(model, "ammocrate_smg1")) {
348 snprintf(label, outn, "SMG ammo crate");
349 return IK_AMMO;
350 }
351 if (item_icontains(model, "ammocrate_ar2")) {
352 snprintf(label, outn, "AR2 ammo crate");
353 return IK_AMMO;
354 }
355 if (item_icontains(model, "ammocrate_rockets")) {
356 snprintf(label, outn, "RPG ammo crate");
357 return IK_AMMO;
358 }
359 if (item_icontains(model, "ammocrate_buckshot")) {
360 snprintf(label, outn, "Shotgun ammo crate");
361 return IK_AMMO;
362 }
363 if (item_icontains(model, "ammocrate_grenade")) {
364 snprintf(label, outn, "Grenade crate");
365 return IK_AMMO;
366 }
367 if (item_icontains(model, "ammocrate_smg2")) {
368 snprintf(label, outn, "SMG grenade crate");
369 return IK_AMMO;
370 }
371 if (item_icontains(model, "ammocrate")) {
372 snprintf(label, outn, "Ammo crate");
373 return IK_AMMO;
374 }
375
376 if (item_icontains(model, "w_missile_closed")) {
377 snprintf(label, outn, "RPG rocket");
378 return IK_AMMO;
379 }
380 if (item_icontains(model, "boxsrounds")) {
381 snprintf(label, outn, "Pistol ammo");
382 return IK_AMMO;
383 }
384 if (item_icontains(model, "boxmrounds")) {
385 snprintf(label, outn, "SMG ammo");
386 return IK_AMMO;
387 }
388 if (item_icontains(model, "boxlrounds") ||
389 item_icontains(model, "combine_rifle_cartridge") ||
390 item_icontains(model, "combine_rifle_ammo")) {
391 snprintf(label, outn, "AR2 ammo");
392 return IK_AMMO;
393 }
394 if (item_icontains(model, "357ammo") ||
395 item_icontains(model, "357ammobox")) {
396 snprintf(label, outn, ".357 ammo");
397 return IK_AMMO;
398 }
399 if (item_icontains(model, "crossbowrounds")) {
400 snprintf(label, outn, "Crossbow bolts");
401 return IK_AMMO;
402 }
403 if (item_icontains(model, "boxbuckshot")) {
404 snprintf(label, outn, "Shotgun shells");
405 return IK_AMMO;
406 }
407 if (item_icontains(model, "boxsniperrounds")) {
408 snprintf(label, outn, "Sniper ammo");
409 return IK_AMMO;
410 }
411 if (item_icontains(model, "boxflares") ||
412 item_icontains(model, "models/items/flare")) {
413 snprintf(label, outn, "Flares");
414 return IK_AMMO;
415 }
416 if (item_icontains(model, "ar2_grenade")) {
417 snprintf(label, outn, "AR2 grenade");
418 return IK_AMMO;
419 }
420 if (item_icontains(model, "w_grenade") ||
421 item_icontains(model, "w_frag") ||
422 item_icontains(model, "frag_grenade") ||
423 item_icontains(model, "items/grenade")) {
424 snprintf(label, outn, "Frag grenade");
425 return IK_AMMO;
426 }
427 if (item_icontains(model, "models/items/")) {
428 snprintf(label, outn, "Ammo");
429 return IK_AMMO;
430 }
431
432 if (classify_weapon(model, cls, label, outn) == 1)
433 return IK_WEAPON;
434
435 if (item_icontains(cls, "healthkit") || item_icontains(cls, "healthvial") ||
436 item_icontains(cls, "wallhealth") ||
437 item_icontains(cls, "healthcharger")) {
438 snprintf(label, outn, "Health");
439 return IK_HEALTH;
440 }
441 if (item_icontains(cls, "battery") || item_icontains(cls, "itemsuit") ||
442 item_icontains(cls, "recharge")) {
443 snprintf(label, outn, "Armor");
444 return IK_ARMOR;
445 }
446 if (item_icontains(cls, "itemcrate")) {
447 snprintf(label, outn, "Supply crate");
448 return IK_CRATE;
449 }
450 if (item_icontains(cls, "item_ammo") || item_icontains(cls, "itemammo")) {
451 snprintf(label, outn, "Ammo");
452 return IK_AMMO;
453 }
454 if (item_icontains(cls, "weapon")) {
455 snprintf(label, outn, "Weapon");
456 return IK_WEAPON;
457 }
458 if (item_icontains(cls, "item") || item_icontains(cls, "pickup")) {
459 snprintf(label, outn, "Item");
460 return IK_OTHER;
461 }
462 return -1;
463}
464
465static bool item_kind_enabled(int kind) {
466 switch (kind) {
467 case IK_HEALTH: return g_item_health;
468 case IK_AMMO: return g_item_ammo;
469 case IK_ARMOR: return g_item_armor;
470 case IK_WEAPON: return g_item_weapons;
471 case IK_CRATE: return g_item_crates;
472 default: return true;
473 }
474}
475
476// Health chargers are brushes (func_healthcharger/CWallHealth) and suit
477// chargers are item_suitcharger/CNewRecharge. Both expose m_iJuice.
478static bool item_is_charger(const char *model, const char *cls) {
479 return item_icontains(model, "charger") ||
480 item_icontains(model, "suit_charger") ||
481 item_icontains(cls, "charger") ||
482 item_icontains(cls, "wallhealth") ||
483 item_icontains(cls, "recharge");
484}
485
486static bool item_is_suit_charger(const char *model, const char *cls) {
487 return item_icontains(model, "suit_charger") ||
488 item_icontains(model, "suitcharger") ||
489 item_icontains(cls, "recharge") ||
490 item_icontains(cls, "suitcharger");
491}
492
493static void items_collect(int local_idx) {
494 static double last_scan = 0.0;
495 double now = esp_now();
496 if (now - last_scan < 0.10)
497 return;
498 last_scan = now;
499 g_nitems = 0;
500 if (!g_iface.entitylist || g_net.origin < 0 || g_net.modelindex < 0)
501 return;
502 resolve_server_netvars();
503
504 using Fn = int (*)(void *);
505 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
506 if (hi < 0 || hi > 4096)
507 hi = 4096;
508 int active_idx = local_active_weapon_idx(local_idx);
509
510 // Which weapon types the local player already carries. A fallen weapon of
511 // the same type is then just an ammo pickup. Owner handles are unreliable on
512 // this client (owner/weapon_owner netvars resolve to -1), so remember every
513 // type the player has held since the current local entity appeared.
514 static uint32_t owned_mask = 0;
515 static void *last_local = nullptr;
516 void *local_ent = entitylist_get(local_idx);
517 if (local_ent != last_local) {
518 last_local = local_ent;
519 owned_mask = 0;
520 }
521 int owned_idx = active_idx;
522 if (owned_idx <= 0 && g_iface.engineserver) {
523 resolve_server_netvars();
524 owned_idx = server_active_weapon(local_idx);
525 }
526 if (owned_idx > 0) {
527 void *aw = entitylist_get(owned_idx);
528 void *anet = entity_networkable(owned_idx);
529 const char *acls = anet ? entity_class_name(anet, nullptr) : nullptr;
530 const char *amodel = aw ? entity_model_name(aw) : nullptr;
531 int at = weapon_type_id(amodel, acls);
532 if (at > 0)
533 owned_mask |= 1u << at;
534 }
535 for (int idx = 1; idx <= hi; idx++) {
536 if (idx == local_idx)
537 continue;
538 void *ent = entitylist_get(idx);
539 if (!ent || item_weapon_owner_idx(ent) != local_idx)
540 continue;
541 void *net = entity_networkable(idx);
542 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
543 const char *model = entity_model_name(ent);
544 int wt = weapon_type_id(model, cls);
545 if (wt > 0)
546 owned_mask |= 1u << wt;
547 }
548 // Server-side inventory, independent of the client recv-table arrays.
549 if (g_iface.engineserver) {
550 for (int slot = 0; slot < 48; slot++) {
551 uint32_t h = server_player_weapon(local_idx, slot);
552 if (!item_handle_valid(h))
553 continue;
554 int wi = (int)(h & 0x7FF);
555 if (wi <= 0 || wi > 4096)
556 continue;
557 void *wep = entitylist_get(wi);
558 void *net = entity_networkable(wi);
559 const char *wc = net ? entity_class_name(net, nullptr) : nullptr;
560 const char *wm = wep ? entity_model_name(wep) : nullptr;
561 if (!wm) {
562 void *sb = nullptr;
563 server_entity_health(wi, nullptr, nullptr, &sb);
564 if (sb)
565 wm = model_name_from_index(server_model_index(sb));
566 }
567 int wt = weapon_type_id(wm, wc);
568 if (wt > 0)
569 owned_mask |= 1u << wt;
570 }
571 }
572 bool owned_weapon[WT_COUNT] = {};
573 for (int t = 1; t < WT_COUNT; t++)
574 owned_weapon[t] = (owned_mask & (1u << t)) != 0;
575 static bool cats_logged = false;
576 if (!cats_logged) {
577 cats_logged = true;
578 char maxbuf[16];
579 if (g_item_max_dist == 0)
580 snprintf(maxbuf, sizeof(maxbuf), "All");
581 else
582 snprintf(maxbuf, sizeof(maxbuf), "%d", g_item_max_dist);
583 logf("items: categories health=%d ammo=%d armor=%d weapons=%d "
584 "crates=%d distance=%d max=%s",
585 (int)g_item_health, (int)g_item_ammo, (int)g_item_armor,
586 (int)g_item_weapons, (int)g_item_crates, (int)g_item_distance,
587 maxbuf);
588 }
589 static int dbg_lines = 0;
590 static bool owner_warned = false;
591 if (!owner_warned && g_net.owner < 0 && g_net.weapon_owner < 0) {
592 owner_warned = true;
593 logf("items: weapon owner netvar unavailable; held weapons may tag");
594 }
595
596 for (int idx = 1; idx <= hi && g_nitems < 192; idx++) {
597 if (idx == local_idx)
598 continue;
599 void *ent = entitylist_get(idx);
600 if (!ent)
601 continue;
602 void *net = entity_networkable(idx);
603 if (!net || entity_is_dormant(net))
604 continue;
605 const char *cls = entity_class_name(net, nullptr);
606 const char *scls = nullptr;
607 if (g_iface.engineserver &&
608 (!cls || !*cls || strcmp(cls, "CBaseEntity") == 0))
609 scls = server_entity_classname(idx);
610 const char *model = entity_model_name(ent);
611 // CPhysicsProp's client m_nModelIndex is often 0 (e.g. item_item_crate);
612 // fall back to the server entity's model index, which is set in Spawn().
613 if ((!model || !*model) && g_iface.engineserver) {
614 void *sbase = nullptr;
615 server_entity_health(idx, nullptr, nullptr, &sbase);
616 if (sbase)
617 model = model_name_from_index(server_model_index(sbase));
618 }
619 char label[128];
620 int kind = classify_item(model, cls, label, sizeof(label));
621 if (kind < 0 && scls)
622 kind = classify_item(model, scls, label, sizeof(label));
623 if (kind < 0)
624 continue;
625 // Health/suit chargers are dispensers that stay useful after a refill, but
626 // an empty suit charger cannot give anything. The health charger's
627 // m_iJuice offset is still being verified (it read 0 on a full charger),
628 // so it is left visible for now; server_item_juice logs both candidate
629 // values for diagnosis.
630 bool is_charger = item_is_charger(model, cls) ||
631 (scls && item_is_charger(model, scls));
632 bool is_suit = item_is_suit_charger(model, cls) ||
633 (scls && item_is_suit_charger(model, scls));
634 if (is_charger) {
635 int juice = server_item_juice(idx);
636 if (is_suit && juice == 0)
637 continue;
638 }
639
640 Vector origin{*(float *)((uintptr_t)ent + g_net.origin),
641 *(float *)((uintptr_t)ent + g_net.origin + 4),
642 *(float *)((uintptr_t)ent + g_net.origin + 8)};
643 float dx = origin.x - g_cam_eye.x;
644 float dy = origin.y - g_cam_eye.y;
645 float dz = origin.z - g_cam_eye.z;
646 float dist = sqrtf(dx * dx + dy * dy + dz * dz);
647
648 if (kind == IK_WEAPON) {
649 if (idx == active_idx || item_weapon_is_owned(ent))
650 continue;
651 int wt = weapon_type_id(model, cls);
652 if (wt > 0 && owned_weapon[wt]) {
653 char base_label[160];
654 snprintf(base_label, sizeof(base_label), "%s", label);
655 snprintf(label, sizeof(label), "%.100s ammo", base_label);
656 kind = IK_AMMO;
657 }
658 }
659 // Distance applies to every tag; 0 means unlimited.
660 if (g_item_max_dist > 0 && dist > (float)g_item_max_dist)
661 continue;
662
663 ItemTarget &t = g_item_targets[g_nitems++];
664 t.idx = idx;
665 t.kind = kind;
666 t.ammo_type = -1;
667 if (kind == IK_AMMO) {
668 t.ammo_type = item_ammo_type(model);
669 if (t.ammo_type < 0)
670 t.ammo_type = weapon_ammo_type(weapon_type_id(model, cls));
671 }
672 t.origin = origin;
673 snprintf(t.label, sizeof(t.label), "%s", label);
674 if (dbg_lines < 8) {
675 dbg_lines++;
676 logd("item: idx=%d kind=%d cls=%s model=%s", idx, kind,
677 cls ? cls : "?", model ? model : "?");
678 }
679 }
680
681 if (!g_items_logged) {
682 g_items_logged = true;
683 logf("items: tagging %d valuable entities", g_nitems);
684 }
685}
686
687// Authoritative local health/armor for the "only if usable" item filter.
688// The health ceiling comes from the server player's m_iMaxHealth, not a
689// hardcoded 100; when it cannot be read the filter fails open.
690static void local_vitals(int &hp, int &maxhp, int &armor) {
691 hp = maxhp = armor = -1;
692 int local = engine_get_local_player();
693 if (local <= 0)
694 return;
695 void *le = entitylist_get(local);
696 if (le && g_net.health >= 0)
697 hp = *(int *)((uintptr_t)le + g_net.health);
698 if (g_iface.engineserver) {
699 int smax = -1;
700 int shp = server_entity_health(local, &smax, nullptr, nullptr);
701 if (shp >= 0)
702 hp = shp;
703 if (smax > 0)
704 maxhp = smax;
705 armor = server_player_armor(local);
706 }
707 if (maxhp < 0 && le && g_net.maxhealth >= 0)
708 maxhp = *(int *)((uintptr_t)le + g_net.maxhealth);
709}
710
711void esp_draw_items() {
712 if (!g_surface || !g_iface.entitylist || g_net.origin < 0)
713 return;
714 if (!g_item_health && !g_item_ammo && !g_item_armor && !g_item_weapons &&
715 !g_item_crates)
716 return;
717 int local = engine_get_local_player();
718 if (local <= 0)
719 return;
720 if (!esp_update_screen())
721 return;
722 items_collect(local);
723
724 int vit_hp = -1, vit_max = -1, vit_armor = -1;
725 if (g_item_usable)
726 local_vitals(vit_hp, vit_max, vit_armor);
727 static bool ammo_logged = false;
728 if (g_item_usable && !ammo_logged) {
729 ammo_logged = true;
730 char buf[512];
731 int pos = 0;
732 void *le = entitylist_get(local);
733 for (int t = 0; t < 16 && pos < 500; t++) {
734 int a = local_ammo(le, t);
735 int m = ammo_max(t);
736 if (a >= 0 && m > 0)
737 pos += snprintf(buf + pos, sizeof(buf) - pos, " %d:%d/%d", t, a, m);
738 }
739 logf("items: ammo%s", buf);
740 }
741
742 for (int i = 0; i < g_nitems; i++) {
743 ItemTarget &t = g_item_targets[i];
744 // Follow the entity between the 10 Hz discovery scans.
745 Vector live;
746 if (entity_origin(entitylist_get(t.idx), live))
747 t.origin = live;
748 if (!item_kind_enabled(t.kind))
749 continue;
750 // Dispensers (chargers, ammo crates, supply crates) stay useful after a
751 // refill, so the fullness filter does not apply to them.
752 bool dispenser = strcasestr(t.label, "crate") ||
753 strcasestr(t.label, "charger");
754 if (g_item_usable && !dispenser) {
755 if (t.kind == IK_HEALTH && vit_hp >= 0 && vit_max > 0 &&
756 vit_hp >= vit_max)
757 continue;
758 if (t.kind == IK_ARMOR && vit_armor >= 0 && vit_armor >= 100)
759 continue;
760 if (t.kind == IK_AMMO && t.ammo_type >= 0) {
761 int maxa = ammo_max(t.ammo_type);
762 int cura = local_ammo(entitylist_get(local), t.ammo_type);
763 if (maxa > 0 && cura >= 0 && cura >= maxa)
764 continue;
765 }
766 }
767 float sx, sy;
768 if (!world_to_screen(t.origin, sx, sy))
769 continue;
770 if (sx < -40 || sx > (float)g_scr_w + 40 || sy < -20 ||
771 sy > (float)g_scr_h + 20)
772 continue;
773
774 int r = 230, g = 230, b = 230;
775 switch (t.kind) {
776 case IK_HEALTH: r = 90; g = 255; b = 130; break;
777 case IK_AMMO: r = 255; g = 210; b = 80; break;
778 case IK_ARMOR: r = 90; g = 180; b = 255; break;
779 case IK_WEAPON: r = 120; g = 230; b = 255; break;
780 case IK_CRATE: r = 220; g = 150; b = 255; break;
781 default: break;
782 }
783 int x = (int)sx, y = (int)sy;
784 int rad = (t.kind == IK_CRATE) ? 7 : 4;
785 surf_color(r, g, b, 255);
786 surf_rect(0, x - rad, y - rad, x + rad, y + rad);
787 surf_line(x - rad - 4, y, x - rad - 1, y);
788 surf_line(x + rad + 1, y, x + rad + 4, y);
789 surf_line(x, y - rad - 4, x, y - rad - 1);
790 surf_line(x, y + rad + 1, x, y + rad + 4);
791 if (t.kind == IK_CRATE)
792 surf_rect(1, x - 2, y - 2, x + 2, y + 2);
793
794 float dx = t.origin.x - g_cam_eye.x;
795 float dy = t.origin.y - g_cam_eye.y;
796 float dz = t.origin.z - g_cam_eye.z;
797 int dist = (int)sqrtf(dx * dx + dy * dy + dz * dz);
798 char text[192];
799 if (g_item_distance)
800 snprintf(text, sizeof(text), "%s %du", t.label, dist);
801 else
802 snprintf(text, sizeof(text), "%s", t.label);
803 int tw = (int)strlen(text) * 9;
804 int tx = x + 10;
805 if (tx + tw > g_scr_w - 4)
806 tx = x - 10 - tw;
807 if (tx < 4)
808 tx = 4;
809 int ty = y - 7;
810 if (ty < 0)
811 ty = 0;
812 if (ty + 16 > g_scr_h)
813 ty = g_scr_h - 16;
814 draw_text_col(tx, ty, text, r, g, b);
815 }
816}
817
818// ---------------------------------------------------------------------------
819// projectile ESP: grenades, rockets, helicopter bombs, AR2 balls
820// ---------------------------------------------------------------------------