summaryrefslogtreecommitdiffstats
path: root/src/items.cpp
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:23:29 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:23:29 +0100
commit5a9fd14f9424d7f66b0454464be95b40b10ee5c4 (patch)
treeb77d072d1ea1b0382b79174984b3647bc2b05729 /src/items.cpp
parentESP: extract projectile ESP into its own translation unit (diff)
downloadhl2h-5a9fd14f9424d7f66b0454464be95b40b10ee5c4.tar.gz
hl2h-5a9fd14f9424d7f66b0454464be95b40b10ee5c4.zip
ESP: extract item ESP into its own translation unit
Second slice of the esp.cpp split. Model-name lookup joins the shared helpers in esp_internal.h.
Diffstat (limited to '')
-rw-r--r--src/items.cpp820
1 files changed, 820 insertions, 0 deletions
diff --git a/src/items.cpp b/src/items.cpp
new file mode 100644
index 0000000..cc324d4
--- /dev/null
+++ b/src/items.cpp
@@ -0,0 +1,820 @@
1// items.cpp — item ESP (discovery, classification, drawing).
2#include "esp_internal.h"
3#include "hl2h.h"
4#include <cmath>
5#include <cstdio>
6#include <cstring>
7
8
9enum ItemKind {
10 IK_HEALTH,
11 IK_AMMO,
12 IK_ARMOR,
13 IK_WEAPON,
14 IK_CRATE,
15 IK_OTHER
16};
17
18struct ItemTarget {
19 int idx;
20 int kind;
21 int ammo_type; // m_iAmmo index for IK_AMMO, -1 otherwise
22 Vector origin;
23 char label[128];
24};
25
26static ItemTarget g_item_targets[192];
27static int g_nitems = 0;
28static bool g_items_logged = false;
29
30static bool item_icontains(const char *hay, const char *needle) {
31 if (!hay || !needle || !*needle)
32 return false;
33 size_t n = strlen(needle);
34 for (const char *p = hay; *p; p++)
35 if (strncasecmp(p, needle, n) == 0)
36 return true;
37 return false;
38}
39
40static bool item_handle_valid(uint32_t h) {
41 return h != 0 && h != 0xFFFFFFFFu;
42}
43
44// Weapons held by the player or an NPC are not world pickups.
45bool item_weapon_is_owned(void *ent) {
46 if (!ent)
47 return false;
48 if (g_net.owner >= 0) {
49 uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.owner);
50 if (item_handle_valid(h))
51 return true;
52 }
53 if (g_net.weapon_owner >= 0) {
54 uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner);
55 if (item_handle_valid(h))
56 return true;
57 }
58 return false;
59}
60
61// Entity index of the weapon's owner, or -1. Used to distinguish the local
62// player's inventory from NPC weapons.
63static int item_weapon_owner_idx(void *ent) {
64 if (!ent)
65 return -1;
66 uint32_t h = 0;
67 if (g_net.owner >= 0)
68 h = *(uint32_t *)((uintptr_t)ent + g_net.owner);
69 if (!item_handle_valid(h) && g_net.weapon_owner >= 0)
70 h = *(uint32_t *)((uintptr_t)ent + g_net.weapon_owner);
71 if (!item_handle_valid(h))
72 return -1;
73 return (int)(h & 0x7FF);
74}
75
76static int local_active_weapon_idx(int local_idx) {
77 if (local_idx <= 0 || g_net.activeweapon < 0)
78 return -1;
79 void *local = entitylist_get(local_idx);
80 if (!local)
81 return -1;
82 uint32_t h = *(uint32_t *)((uintptr_t)local + g_net.activeweapon);
83 if (!item_handle_valid(h))
84 return -1;
85 return (int)(h & 0x7FF);
86}
87
88static int classify_weapon(const char *model, const char *cls, char *label,
89 size_t outn) {
90 if (!item_icontains(model, "models/weapons/w_") &&
91 !item_icontains(cls, "weapon"))
92 return 0;
93 if (item_icontains(model, "w_crowbar"))
94 snprintf(label, outn, "Crowbar");
95 else if (item_icontains(model, "w_pistol"))
96 snprintf(label, outn, "Pistol");
97 else if (item_icontains(model, "w_357"))
98 snprintf(label, outn, "Revolver");
99 else if (item_icontains(model, "w_smg1") ||
100 item_icontains(model, "w_smg2"))
101 snprintf(label, outn, "SMG");
102 else if (item_icontains(model, "w_ar2") ||
103 item_icontains(model, "w_irifle"))
104 snprintf(label, outn, "AR2");
105 else if (item_icontains(model, "w_shotgun"))
106 snprintf(label, outn, "Shotgun");
107 else if (item_icontains(model, "w_crossbow"))
108 snprintf(label, outn, "Crossbow");
109 else if (item_icontains(model, "w_rocket_launcher"))
110 snprintf(label, outn, "RPG");
111 else if (item_icontains(model, "w_frag") ||
112 item_icontains(model, "w_grenade"))
113 snprintf(label, outn, "Grenade");
114 else if (item_icontains(model, "w_physcannon") ||
115 item_icontains(model, "w_physics"))
116 snprintf(label, outn, "Gravity gun");
117 else if (item_icontains(model, "w_stunstick") ||
118 item_icontains(model, "w_stunbaton"))
119 snprintf(label, outn, "Stunstick");
120 else if (item_icontains(model, "w_bugbait"))
121 snprintf(label, outn, "Bugbait");
122 else if (item_icontains(model, "w_sniper"))
123 snprintf(label, outn, "Sniper");
124 else if (item_icontains(model, "w_slam"))
125 snprintf(label, outn, "SLAM");
126 else
127 snprintf(label, outn, "%s", model);
128 return 1;
129}
130
131// Weapon identity for the "owned weapon -> fallen weapon is ammo" rule.
132enum {
133 WT_NONE = 0,
134 WT_PISTOL,
135 WT_357,
136 WT_SMG,
137 WT_AR2,
138 WT_SHOTGUN,
139 WT_CROSSBOW,
140 WT_RPG,
141 WT_GRENADE,
142 WT_SNIPER,
143 WT_COUNT
144};
145
146static int weapon_type_id(const char *model, const char *cls) {
147 if (item_icontains(model, "pistol") || item_icontains(cls, "pistol"))
148 return WT_PISTOL;
149 if (item_icontains(model, "357") || item_icontains(cls, "357"))
150 return WT_357;
151 if (item_icontains(model, "smg") || item_icontains(cls, "smg"))
152 return WT_SMG;
153 if (item_icontains(model, "ar2") || item_icontains(model, "irifle") ||
154 item_icontains(cls, "ar2") || item_icontains(cls, "irifle"))
155 return WT_AR2;
156 if (item_icontains(model, "shotgun") || item_icontains(cls, "shotgun"))
157 return WT_SHOTGUN;
158 if (item_icontains(model, "crossbow") || item_icontains(cls, "crossbow"))
159 return WT_CROSSBOW;
160 if (item_icontains(model, "rocket") || item_icontains(cls, "rpg"))
161 return WT_RPG;
162 if (item_icontains(model, "grenade") || item_icontains(cls, "frag"))
163 return WT_GRENADE;
164 if (item_icontains(model, "sniper") || item_icontains(cls, "sniper"))
165 return WT_SNIPER;
166 return WT_NONE;
167}
168
169static int weapon_ammo_type(int wt) {
170 switch (wt) {
171 case WT_PISTOL: return 2;
172 case WT_357: return 4;
173 case WT_SMG: return 3;
174 case WT_AR2: return 0;
175 case WT_SHOTGUN: return 6;
176 case WT_CROSSBOW: return 5;
177 case WT_RPG: return 7;
178 case WT_GRENADE: return 11;
179 case WT_SNIPER: return 9;
180 default: return -1;
181 }
182}
183
184// Ammo type index used by CBasePlayer::m_iAmmo (order of AddAmmoType in
185// hl2_gamerules.cpp).
186static int item_ammo_type(const char *model) {
187 if (!model)
188 return -1;
189 if (item_icontains(model, "boxsrounds") ||
190 item_icontains(model, "ammocrate_pistol") ||
191 item_icontains(model, "w_pistol"))
192 return 2;
193 if (item_icontains(model, "boxmrounds") ||
194 item_icontains(model, "ammocrate_smg1"))
195 return 3;
196 if (item_icontains(model, "boxlrounds") ||
197 item_icontains(model, "combine_rifle") ||
198 item_icontains(model, "ammocrate_ar2") ||
199 item_icontains(model, "w_ar2") || item_icontains(model, "w_irifle"))
200 return 0;
201 if (item_icontains(model, "357ammo") ||
202 item_icontains(model, "357ammobox") ||
203 item_icontains(model, "w_357"))
204 return 4;
205 if (item_icontains(model, "crossbowrounds") ||
206 item_icontains(model, "w_crossbow"))
207 return 5;
208 if (item_icontains(model, "boxbuckshot") ||
209 item_icontains(model, "ammocrate_buckshot") ||
210 item_icontains(model, "w_shotgun"))
211 return 6;
212 if (item_icontains(model, "w_missile_closed") ||
213 item_icontains(model, "ammocrate_rockets") ||
214 item_icontains(model, "w_rocket_launcher"))
215 return 7;
216 if (item_icontains(model, "ar2_grenade") ||
217 item_icontains(model, "ammocrate_smg2") ||
218 item_icontains(model, "smg1_grenade") ||
219 item_icontains(model, "smg_grenade"))
220 return 8;
221 if (item_icontains(model, "ammocrate_grenade") ||
222 item_icontains(model, "w_grenade") ||
223 item_icontains(model, "w_frag") ||
224 item_icontains(model, "frag_grenade") ||
225 item_icontains(model, "items/grenade"))
226 return 11;
227 if (item_icontains(model, "w_smg1") || item_icontains(model, "w_smg2"))
228 return 3;
229 return -1;
230}
231
232static int convar_int(const char *name) {
233 if (!g_iface.cvar || !name)
234 return -1;
235 using FindFn = void *(*)(void *, const char *);
236 void *v = vfunc<FindFn>(g_iface.cvar, 12)(g_iface.cvar, name);
237 if (!v)
238 return -1;
239 return (int)*(float *)((uintptr_t)v + 0x2c); // ConVar m_Value.f
240}
241
242// Maximum reserve ammo for an m_iAmmo index. Prefers the sk_max_* cvar and
243// falls back to the stock HL2 cap so the RPG's 3-rocket limit is honoured
244// even when server cvars are not visible from the client.
245static int ammo_max(int type) {
246 static int cache[16];
247 static bool init = false;
248 if (type < 0 || type >= 16)
249 return -1;
250 if (!init) {
251 for (int i = 0; i < 16; i++)
252 cache[i] = -2;
253 init = true;
254 }
255 if (cache[type] != -2)
256 return cache[type];
257 const char *name = nullptr;
258 int stock = -1;
259 switch (type) {
260 case 0: name = "sk_max_ar2"; stock = 60; break;
261 case 2: name = "sk_max_pistol"; stock = 150; break;
262 case 3: name = "sk_max_smg1"; stock = 225; break;
263 case 4: name = "sk_max_357"; stock = 12; break;
264 case 5: name = "sk_max_crossbow"; stock = 10; break;
265 case 6: name = "sk_max_buckshot"; stock = 30; break;
266 case 7: name = "sk_max_rpg_round"; stock = 3; break;
267 case 8: name = "sk_max_smg1_grenade"; stock = 3; break;
268 case 9: name = "sk_max_sniper_round"; stock = 30; break;
269 case 11: name = "sk_max_grenade"; stock = 5; break;
270 default: break;
271 }
272 int v = convar_int(name);
273 if (v <= 0)
274 v = stock;
275 cache[type] = v;
276 return v;
277}
278
279static int local_ammo(void *local_ent, int type) {
280 if (type < 0 || type >= 32)
281 return -1;
282 // Prefer the authoritative server array; the client m_iAmmo netvar offset
283 // can be off by an element, which made "full" counts flicker as other ammo
284 // was spent. server_player_ammo already skips the CNetworkArray header.
285 int local = engine_get_local_player();
286 if (local > 0 && g_iface.engineserver) {
287 resolve_server_netvars();
288 int s = server_player_ammo(local, type);
289 if (s >= 0)
290 return s;
291 }
292 if (local_ent && g_net.ammo >= 0)
293 return *(int *)((uintptr_t)local_ent + g_net.ammo + type * 4);
294 return -1;
295}
296
297static int classify_item(const char *model, const char *cls, char *label,
298 size_t outn) {
299 if (!model && !cls)
300 return -1;
301
302 // Breakable prop chunks are debris, not pickups.
303 if (item_icontains(model, "chunk"))
304 return -1;
305
306 if (item_icontains(model, "healthkit")) {
307 snprintf(label, outn, "Medkit");
308 return IK_HEALTH;
309 }
310 if (item_icontains(model, "healthvial")) {
311 snprintf(label, outn, "Health vial");
312 return IK_HEALTH;
313 }
314 if (item_icontains(model, "health_charger")) {
315 snprintf(label, outn, "Health charger");
316 return IK_HEALTH;
317 }
318 if (item_icontains(model, "battery")) {
319 snprintf(label, outn, "Battery");
320 return IK_ARMOR;
321 }
322 if (item_icontains(model, "hevsuit")) {
323 snprintf(label, outn, "HEV suit");
324 return IK_ARMOR;
325 }
326 if (item_icontains(model, "w_longjump")) {
327 snprintf(label, outn, "Long jump");
328 return IK_ARMOR;
329 }
330 if (item_icontains(model, "suit_charger")) {
331 snprintf(label, outn, "Suit charger");
332 return IK_ARMOR;
333 }
334
335 // Only the two item-crate models are supply crates. Other crates are just
336 // breakable map props and deliberately not tagged.
337 if (item_icontains(model, "item_item_crate") ||
338 item_icontains(model, "item_beacon_crate")) {
339 snprintf(label, outn, "Supply crate");
340 return IK_CRATE;
341 }
342
343 if (item_icontains(model, "ammocrate_pistol")) {
344 snprintf(label, outn, "Pistol ammo crate");
345 return IK_AMMO;
346 }
347 if (item_icontains(model, "ammocrate_smg1")) {
348 snprintf(label, outn, "SMG ammo crate");
349 return IK_AMMO;
350 }
351 if (item_icontains(model, "ammocrate_ar2")) {
352 snprintf(label, outn, "AR2 ammo crate");
353 return IK_AMMO;
354 }
355 if (item_icontains(model, "ammocrate_rockets")) {
356 snprintf(label, outn, "RPG ammo crate");
357 return IK_AMMO;
358 }
359 if (item_icontains(model, "ammocrate_buckshot")) {
360 snprintf(label, outn, "Shotgun ammo crate");
361 return IK_AMMO;
362 }
363 if (item_icontains(model, "ammocrate_grenade")) {
364 snprintf(label, outn, "Grenade crate");
365 return IK_AMMO;
366 }
367 if (item_icontains(model, "ammocrate_smg2")) {
368 snprintf(label, outn, "SMG grenade crate");
369 return IK_AMMO;
370 }
371 if (item_icontains(model, "ammocrate")) {
372 snprintf(label, outn, "Ammo crate");
373 return IK_AMMO;
374 }
375
376 if (item_icontains(model, "w_missile_closed")) {
377 snprintf(label, outn, "RPG rocket");
378 return IK_AMMO;
379 }
380 if (item_icontains(model, "boxsrounds")) {
381 snprintf(label, outn, "Pistol ammo");
382 return IK_AMMO;
383 }
384 if (item_icontains(model, "boxmrounds")) {
385 snprintf(label, outn, "SMG ammo");
386 return IK_AMMO;
387 }
388 if (item_icontains(model, "boxlrounds") ||
389 item_icontains(model, "combine_rifle_cartridge") ||
390 item_icontains(model, "combine_rifle_ammo")) {
391 snprintf(label, outn, "AR2 ammo");
392 return IK_AMMO;
393 }
394 if (item_icontains(model, "357ammo") ||
395 item_icontains(model, "357ammobox")) {
396 snprintf(label, outn, ".357 ammo");
397 return IK_AMMO;
398 }
399 if (item_icontains(model, "crossbowrounds")) {
400 snprintf(label, outn, "Crossbow bolts");
401 return IK_AMMO;
402 }
403 if (item_icontains(model, "boxbuckshot")) {
404 snprintf(label, outn, "Shotgun shells");
405 return IK_AMMO;
406 }
407 if (item_icontains(model, "boxsniperrounds")) {
408 snprintf(label, outn, "Sniper ammo");
409 return IK_AMMO;
410 }
411 if (item_icontains(model, "boxflares") ||
412 item_icontains(model, "models/items/flare")) {
413 snprintf(label, outn, "Flares");
414 return IK_AMMO;
415 }
416 if (item_icontains(model, "ar2_grenade")) {
417 snprintf(label, outn, "AR2 grenade");
418 return IK_AMMO;
419 }
420 if (item_icontains(model, "w_grenade") ||
421 item_icontains(model, "w_frag") ||
422 item_icontains(model, "frag_grenade") ||
423 item_icontains(model, "items/grenade")) {
424 snprintf(label, outn, "Frag grenade");
425 return IK_AMMO;
426 }
427 if (item_icontains(model, "models/items/")) {
428 snprintf(label, outn, "Ammo");
429 return IK_AMMO;
430 }
431
432 if (classify_weapon(model, cls, label, outn) == 1)
433 return IK_WEAPON;
434
435 if (item_icontains(cls, "healthkit") || item_icontains(cls, "healthvial") ||
436 item_icontains(cls, "wallhealth") ||
437 item_icontains(cls, "healthcharger")) {
438 snprintf(label, outn, "Health");
439 return IK_HEALTH;
440 }
441 if (item_icontains(cls, "battery") || item_icontains(cls, "itemsuit") ||
442 item_icontains(cls, "recharge")) {
443 snprintf(label, outn, "Armor");
444 return IK_ARMOR;
445 }
446 if (item_icontains(cls, "itemcrate")) {
447 snprintf(label, outn, "Supply crate");
448 return IK_CRATE;
449 }
450 if (item_icontains(cls, "item_ammo") || item_icontains(cls, "itemammo")) {
451 snprintf(label, outn, "Ammo");
452 return IK_AMMO;
453 }
454 if (item_icontains(cls, "weapon")) {
455 snprintf(label, outn, "Weapon");
456 return IK_WEAPON;
457 }
458 if (item_icontains(cls, "item") || item_icontains(cls, "pickup")) {
459 snprintf(label, outn, "Item");
460 return IK_OTHER;
461 }
462 return -1;
463}
464
465static bool item_kind_enabled(int kind) {
466 switch (kind) {
467 case IK_HEALTH: return g_item_health;
468 case IK_AMMO: return g_item_ammo;
469 case IK_ARMOR: return g_item_armor;
470 case IK_WEAPON: return g_item_weapons;
471 case IK_CRATE: return g_item_crates;
472 default: return true;
473 }
474}
475
476// Health chargers are brushes (func_healthcharger/CWallHealth) and suit
477// chargers are item_suitcharger/CNewRecharge. Both expose m_iJuice.
478static bool item_is_charger(const char *model, const char *cls) {
479 return item_icontains(model, "charger") ||
480 item_icontains(model, "suit_charger") ||
481 item_icontains(cls, "charger") ||
482 item_icontains(cls, "wallhealth") ||
483 item_icontains(cls, "recharge");
484}
485
486static bool item_is_suit_charger(const char *model, const char *cls) {
487 return item_icontains(model, "suit_charger") ||
488 item_icontains(model, "suitcharger") ||
489 item_icontains(cls, "recharge") ||
490 item_icontains(cls, "suitcharger");
491}
492
493static void items_collect(int local_idx) {
494 static double last_scan = 0.0;
495 double now = esp_now();
496 if (now - last_scan < 0.10)
497 return;
498 last_scan = now;
499 g_nitems = 0;
500 if (!g_iface.entitylist || g_net.origin < 0 || g_net.modelindex < 0)
501 return;
502 resolve_server_netvars();
503
504 using Fn = int (*)(void *);
505 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
506 if (hi < 0 || hi > 4096)
507 hi = 4096;
508 int active_idx = local_active_weapon_idx(local_idx);
509
510 // Which weapon types the local player already carries. A fallen weapon of
511 // the same type is then just an ammo pickup. Owner handles are unreliable on
512 // this client (owner/weapon_owner netvars resolve to -1), so remember every
513 // type the player has held since the current local entity appeared.
514 static uint32_t owned_mask = 0;
515 static void *last_local = nullptr;
516 void *local_ent = entitylist_get(local_idx);
517 if (local_ent != last_local) {
518 last_local = local_ent;
519 owned_mask = 0;
520 }
521 int owned_idx = active_idx;
522 if (owned_idx <= 0 && g_iface.engineserver) {
523 resolve_server_netvars();
524 owned_idx = server_active_weapon(local_idx);
525 }
526 if (owned_idx > 0) {
527 void *aw = entitylist_get(owned_idx);
528 void *anet = entity_networkable(owned_idx);
529 const char *acls = anet ? entity_class_name(anet, nullptr) : nullptr;
530 const char *amodel = aw ? entity_model_name(aw) : nullptr;
531 int at = weapon_type_id(amodel, acls);
532 if (at > 0)
533 owned_mask |= 1u << at;
534 }
535 for (int idx = 1; idx <= hi; idx++) {
536 if (idx == local_idx)
537 continue;
538 void *ent = entitylist_get(idx);
539 if (!ent || item_weapon_owner_idx(ent) != local_idx)
540 continue;
541 void *net = entity_networkable(idx);
542 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
543 const char *model = entity_model_name(ent);
544 int wt = weapon_type_id(model, cls);
545 if (wt > 0)
546 owned_mask |= 1u << wt;
547 }
548 // Server-side inventory, independent of the client recv-table arrays.
549 if (g_iface.engineserver) {
550 for (int slot = 0; slot < 48; slot++) {
551 uint32_t h = server_player_weapon(local_idx, slot);
552 if (!item_handle_valid(h))
553 continue;
554 int wi = (int)(h & 0x7FF);
555 if (wi <= 0 || wi > 4096)
556 continue;
557 void *wep = entitylist_get(wi);
558 void *net = entity_networkable(wi);
559 const char *wc = net ? entity_class_name(net, nullptr) : nullptr;
560 const char *wm = wep ? entity_model_name(wep) : nullptr;
561 if (!wm) {
562 void *sb = nullptr;
563 server_entity_health(wi, nullptr, nullptr, &sb);
564 if (sb)
565 wm = model_name_from_index(server_model_index(sb));
566 }
567 int wt = weapon_type_id(wm, wc);
568 if (wt > 0)
569 owned_mask |= 1u << wt;
570 }
571 }
572 bool owned_weapon[WT_COUNT] = {};
573 for (int t = 1; t < WT_COUNT; t++)
574 owned_weapon[t] = (owned_mask & (1u << t)) != 0;
575 static bool cats_logged = false;
576 if (!cats_logged) {
577 cats_logged = true;
578 char maxbuf[16];
579 if (g_item_max_dist == 0)
580 snprintf(maxbuf, sizeof(maxbuf), "All");
581 else
582 snprintf(maxbuf, sizeof(maxbuf), "%d", g_item_max_dist);
583 logf("items: categories health=%d ammo=%d armor=%d weapons=%d "
584 "crates=%d distance=%d max=%s",
585 (int)g_item_health, (int)g_item_ammo, (int)g_item_armor,
586 (int)g_item_weapons, (int)g_item_crates, (int)g_item_distance,
587 maxbuf);
588 }
589 static int dbg_lines = 0;
590 static bool owner_warned = false;
591 if (!owner_warned && g_net.owner < 0 && g_net.weapon_owner < 0) {
592 owner_warned = true;
593 logf("items: weapon owner netvar unavailable; held weapons may tag");
594 }
595
596 for (int idx = 1; idx <= hi && g_nitems < 192; idx++) {
597 if (idx == local_idx)
598 continue;
599 void *ent = entitylist_get(idx);
600 if (!ent)
601 continue;
602 void *net = entity_networkable(idx);
603 if (!net || entity_is_dormant(net))
604 continue;
605 const char *cls = entity_class_name(net, nullptr);
606 const char *scls = nullptr;
607 if (g_iface.engineserver &&
608 (!cls || !*cls || strcmp(cls, "CBaseEntity") == 0))
609 scls = server_entity_classname(idx);
610 const char *model = entity_model_name(ent);
611 // CPhysicsProp's client m_nModelIndex is often 0 (e.g. item_item_crate);
612 // fall back to the server entity's model index, which is set in Spawn().
613 if ((!model || !*model) && g_iface.engineserver) {
614 void *sbase = nullptr;
615 server_entity_health(idx, nullptr, nullptr, &sbase);
616 if (sbase)
617 model = model_name_from_index(server_model_index(sbase));
618 }
619 char label[128];
620 int kind = classify_item(model, cls, label, sizeof(label));
621 if (kind < 0 && scls)
622 kind = classify_item(model, scls, label, sizeof(label));
623 if (kind < 0)
624 continue;
625 // Health/suit chargers are dispensers that stay useful after a refill, but
626 // an empty suit charger cannot give anything. The health charger's
627 // m_iJuice offset is still being verified (it read 0 on a full charger),
628 // so it is left visible for now; server_item_juice logs both candidate
629 // values for diagnosis.
630 bool is_charger = item_is_charger(model, cls) ||
631 (scls && item_is_charger(model, scls));
632 bool is_suit = item_is_suit_charger(model, cls) ||
633 (scls && item_is_suit_charger(model, scls));
634 if (is_charger) {
635 int juice = server_item_juice(idx);
636 if (is_suit && juice == 0)
637 continue;
638 }
639
640 Vector origin{*(float *)((uintptr_t)ent + g_net.origin),
641 *(float *)((uintptr_t)ent + g_net.origin + 4),
642 *(float *)((uintptr_t)ent + g_net.origin + 8)};
643 float dx = origin.x - g_cam_eye.x;
644 float dy = origin.y - g_cam_eye.y;
645 float dz = origin.z - g_cam_eye.z;
646 float dist = sqrtf(dx * dx + dy * dy + dz * dz);
647
648 if (kind == IK_WEAPON) {
649 if (idx == active_idx || item_weapon_is_owned(ent))
650 continue;
651 int wt = weapon_type_id(model, cls);
652 if (wt > 0 && owned_weapon[wt]) {
653 char base_label[160];
654 snprintf(base_label, sizeof(base_label), "%s", label);
655 snprintf(label, sizeof(label), "%.100s ammo", base_label);
656 kind = IK_AMMO;
657 }
658 }
659 // Distance applies to every tag; 0 means unlimited.
660 if (g_item_max_dist > 0 && dist > (float)g_item_max_dist)
661 continue;
662
663 ItemTarget &t = g_item_targets[g_nitems++];
664 t.idx = idx;
665 t.kind = kind;
666 t.ammo_type = -1;
667 if (kind == IK_AMMO) {
668 t.ammo_type = item_ammo_type(model);
669 if (t.ammo_type < 0)
670 t.ammo_type = weapon_ammo_type(weapon_type_id(model, cls));
671 }
672 t.origin = origin;
673 snprintf(t.label, sizeof(t.label), "%s", label);
674 if (dbg_lines < 8) {
675 dbg_lines++;
676 logd("item: idx=%d kind=%d cls=%s model=%s", idx, kind,
677 cls ? cls : "?", model ? model : "?");
678 }
679 }
680
681 if (!g_items_logged) {
682 g_items_logged = true;
683 logf("items: tagging %d valuable entities", g_nitems);
684 }
685}
686
687// Authoritative local health/armor for the "only if usable" item filter.
688// The health ceiling comes from the server player's m_iMaxHealth, not a
689// hardcoded 100; when it cannot be read the filter fails open.
690static void local_vitals(int &hp, int &maxhp, int &armor) {
691 hp = maxhp = armor = -1;
692 int local = engine_get_local_player();
693 if (local <= 0)
694 return;
695 void *le = entitylist_get(local);
696 if (le && g_net.health >= 0)
697 hp = *(int *)((uintptr_t)le + g_net.health);
698 if (g_iface.engineserver) {
699 int smax = -1;
700 int shp = server_entity_health(local, &smax, nullptr, nullptr);
701 if (shp >= 0)
702 hp = shp;
703 if (smax > 0)
704 maxhp = smax;
705 armor = server_player_armor(local);
706 }
707 if (maxhp < 0 && le && g_net.maxhealth >= 0)
708 maxhp = *(int *)((uintptr_t)le + g_net.maxhealth);
709}
710
711void esp_draw_items() {
712 if (!g_surface || !g_iface.entitylist || g_net.origin < 0)
713 return;
714 if (!g_item_health && !g_item_ammo && !g_item_armor && !g_item_weapons &&
715 !g_item_crates)
716 return;
717 int local = engine_get_local_player();
718 if (local <= 0)
719 return;
720 if (!esp_update_screen())
721 return;
722 items_collect(local);
723
724 int vit_hp = -1, vit_max = -1, vit_armor = -1;
725 if (g_item_usable)
726 local_vitals(vit_hp, vit_max, vit_armor);
727 static bool ammo_logged = false;
728 if (g_item_usable && !ammo_logged) {
729 ammo_logged = true;
730 char buf[512];
731 int pos = 0;
732 void *le = entitylist_get(local);
733 for (int t = 0; t < 16 && pos < 500; t++) {
734 int a = local_ammo(le, t);
735 int m = ammo_max(t);
736 if (a >= 0 && m > 0)
737 pos += snprintf(buf + pos, sizeof(buf) - pos, " %d:%d/%d", t, a, m);
738 }
739 logf("items: ammo%s", buf);
740 }
741
742 for (int i = 0; i < g_nitems; i++) {
743 ItemTarget &t = g_item_targets[i];
744 // Follow the entity between the 10 Hz discovery scans.
745 Vector live;
746 if (entity_origin(entitylist_get(t.idx), live))
747 t.origin = live;
748 if (!item_kind_enabled(t.kind))
749 continue;
750 // Dispensers (chargers, ammo crates, supply crates) stay useful after a
751 // refill, so the fullness filter does not apply to them.
752 bool dispenser = strcasestr(t.label, "crate") ||
753 strcasestr(t.label, "charger");
754 if (g_item_usable && !dispenser) {
755 if (t.kind == IK_HEALTH && vit_hp >= 0 && vit_max > 0 &&
756 vit_hp >= vit_max)
757 continue;
758 if (t.kind == IK_ARMOR && vit_armor >= 0 && vit_armor >= 100)
759 continue;
760 if (t.kind == IK_AMMO && t.ammo_type >= 0) {
761 int maxa = ammo_max(t.ammo_type);
762 int cura = local_ammo(entitylist_get(local), t.ammo_type);
763 if (maxa > 0 && cura >= 0 && cura >= maxa)
764 continue;
765 }
766 }
767 float sx, sy;
768 if (!world_to_screen(t.origin, sx, sy))
769 continue;
770 if (sx < -40 || sx > (float)g_scr_w + 40 || sy < -20 ||
771 sy > (float)g_scr_h + 20)
772 continue;
773
774 int r = 230, g = 230, b = 230;
775 switch (t.kind) {
776 case IK_HEALTH: r = 90; g = 255; b = 130; break;
777 case IK_AMMO: r = 255; g = 210; b = 80; break;
778 case IK_ARMOR: r = 90; g = 180; b = 255; break;
779 case IK_WEAPON: r = 120; g = 230; b = 255; break;
780 case IK_CRATE: r = 220; g = 150; b = 255; break;
781 default: break;
782 }
783 int x = (int)sx, y = (int)sy;
784 int rad = (t.kind == IK_CRATE) ? 7 : 4;
785 surf_color(r, g, b, 255);
786 surf_rect(0, x - rad, y - rad, x + rad, y + rad);
787 surf_line(x - rad - 4, y, x - rad - 1, y);
788 surf_line(x + rad + 1, y, x + rad + 4, y);
789 surf_line(x, y - rad - 4, x, y - rad - 1);
790 surf_line(x, y + rad + 1, x, y + rad + 4);
791 if (t.kind == IK_CRATE)
792 surf_rect(1, x - 2, y - 2, x + 2, y + 2);
793
794 float dx = t.origin.x - g_cam_eye.x;
795 float dy = t.origin.y - g_cam_eye.y;
796 float dz = t.origin.z - g_cam_eye.z;
797 int dist = (int)sqrtf(dx * dx + dy * dy + dz * dz);
798 char text[192];
799 if (g_item_distance)
800 snprintf(text, sizeof(text), "%s %du", t.label, dist);
801 else
802 snprintf(text, sizeof(text), "%s", t.label);
803 int tw = (int)strlen(text) * 9;
804 int tx = x + 10;
805 if (tx + tw > g_scr_w - 4)
806 tx = x - 10 - tw;
807 if (tx < 4)
808 tx = 4;
809 int ty = y - 7;
810 if (ty < 0)
811 ty = 0;
812 if (ty + 16 > g_scr_h)
813 ty = g_scr_h - 16;
814 draw_text_col(tx, ty, text, r, g, b);
815 }
816}
817
818// ---------------------------------------------------------------------------
819// projectile ESP: grenades, rockets, helicopter bombs, AR2 balls
820// ---------------------------------------------------------------------------