diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-06 16:23:29 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-06 16:23:29 +0100 |
| commit | 5a9fd14f9424d7f66b0454464be95b40b10ee5c4 (patch) | |
| tree | b77d072d1ea1b0382b79174984b3647bc2b05729 | |
| parent | ESP: extract projectile ESP into its own translation unit (diff) | |
| download | hl2h-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.cpp | 815 | ||||
| -rw-r--r-- | src/esp_internal.h | 1 | ||||
| -rw-r--r-- | src/items.cpp | 820 |
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. |
| 255 | static const char *model_name_from_index(int modelindex) { | 255 | const 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 | ||
| 2511 | enum ItemKind { | ||
| 2512 | IK_HEALTH, | ||
| 2513 | IK_AMMO, | ||
| 2514 | IK_ARMOR, | ||
| 2515 | IK_WEAPON, | ||
| 2516 | IK_CRATE, | ||
| 2517 | IK_OTHER | ||
| 2518 | }; | ||
| 2519 | |||
| 2520 | struct 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 | |||
| 2528 | static ItemTarget g_item_targets[192]; | ||
| 2529 | static int g_nitems = 0; | ||
| 2530 | static bool g_items_logged = false; | ||
| 2531 | |||
| 2532 | static 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 | |||
| 2542 | static 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. | ||
| 2547 | bool 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. | ||
| 2565 | static 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 | |||
| 2578 | static 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 | |||
| 2590 | static 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. | ||
| 2634 | enum { | ||
| 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 | |||
| 2648 | static 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 | |||
| 2671 | static 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). | ||
| 2688 | static 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 | |||
| 2734 | static 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. | ||
| 2747 | static 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 | |||
| 2781 | static 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 | |||
| 2799 | static 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 | |||
| 2967 | static 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. | ||
| 2980 | static 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 | |||
| 2988 | static 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 | |||
| 2995 | static 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. | ||
| 3192 | static 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 | |||
| 3213 | void 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(); | |||
| 18 | void *entity_networkable(int idx); | 18 | void *entity_networkable(int idx); |
| 19 | const char *entity_class_name(void *net, void **out_cc); | 19 | const char *entity_class_name(void *net, void **out_cc); |
| 20 | const char *entity_model_name(void *ent); | 20 | const char *entity_model_name(void *ent); |
| 21 | const char *model_name_from_index(int modelindex); | ||
| 21 | bool entity_origin(void *ent, Vector &out); | 22 | bool entity_origin(void *ent, Vector &out); |
| 22 | bool entity_is_dormant(void *net); | 23 | bool entity_is_dormant(void *net); |
| 23 | bool active_weapon_contains(void *local_ent, const char *needle); | 24 | bool 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 | |||
| 9 | enum ItemKind { | ||
| 10 | IK_HEALTH, | ||
| 11 | IK_AMMO, | ||
| 12 | IK_ARMOR, | ||
| 13 | IK_WEAPON, | ||
| 14 | IK_CRATE, | ||
| 15 | IK_OTHER | ||
| 16 | }; | ||
| 17 | |||
| 18 | struct 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 | |||
| 26 | static ItemTarget g_item_targets[192]; | ||
| 27 | static int g_nitems = 0; | ||
| 28 | static bool g_items_logged = false; | ||
| 29 | |||
| 30 | static 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 | |||
| 40 | static 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. | ||
| 45 | bool 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. | ||
| 63 | static 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 | |||
| 76 | static 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 | |||
| 88 | static 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. | ||
| 132 | enum { | ||
| 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 | |||
| 146 | static 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 | |||
| 169 | static 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). | ||
| 186 | static 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 | |||
| 232 | static 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. | ||
| 245 | static 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 | |||
| 279 | static 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 | |||
| 297 | static 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 | |||
| 465 | static 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. | ||
| 478 | static 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 | |||
| 486 | static 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 | |||
| 493 | static 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. | ||
| 690 | static 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 | |||
| 711 | void 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 | // --------------------------------------------------------------------------- | ||
