diff options
Diffstat (limited to '')
| -rw-r--r-- | src/esp.cpp | 815 |
1 files changed, 1 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 | |||
