summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 14:28:21 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 14:28:21 +0100
commit714262bcf759a238daf906de16e7b204712a104f (patch)
treed208ecfbbdb0f5f2f27aa888c806e38198ad1d52
parentRemove gamma slider; keep HDR brightness (diff)
downloadhl2h-714262bcf759a238daf906de16e7b204712a104f.tar.gz
hl2h-714262bcf759a238daf906de16e7b204712a104f.zip
Add usable item filter, projectile ESP and gravity-gun melee
- Items: "Only if usable" hides health tags at full health and armor at full suit. Health ceiling is the server player's m_iMaxHealth; armor uses the server m_ArmorValue against HL2's 100 cap. Fails open when unreadable. - Projectiles: new Visuals feature/card for thrown grenades, RPG rockets, helicopter bombs and AR2 balls. Fuse from server m_flDetonateTime vs engine time; radius from m_DmgRadius with per-type fallback; optional 24-segment radius ring and FRAG timer label. - Melee: the killbot now handles the gravity gun as well; only VULNERABLE targets, 1200-unit range, primary attack to launch the held object, and no aim-punch compensation because the launch uses EyeVectors. - Server/netvars: m_ArmorValue, m_flDetonateTime, m_DmgRadius.
-rw-r--r--CONTEXT.md42
-rw-r--r--src/esp.cpp283
-rw-r--r--src/gui.cpp15
-rw-r--r--src/hl2h.cpp11
-rw-r--r--src/hl2h.h15
-rw-r--r--src/menu.cpp39
-rw-r--r--src/netvars.cpp10
-rw-r--r--src/server.cpp49
8 files changed, 429 insertions, 35 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 5c91b5c..96a4660 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -310,10 +310,16 @@ vocabulary / decisions.
310 310
311## Melee killbot 311## Melee killbot
312 312
313- Active only while the local player's active weapon is the crowbar. The 313- Active only while the local player's active weapon is the crowbar or the
314 client `m_hActiveWeapon` handle is used first; if it is missing, the 314 gravity gun. The client `m_hActiveWeapon` handle is used first; if it is
315 authoritative server `m_hActiveWeapon` handle (0x824) is used to get the 315 missing, the authoritative server `m_hActiveWeapon` handle (0x824) is used
316 weapon entity index. 316 to get the weapon entity index.
317- With the gravity gun, only `VULNERABLE` targets (headcrab, antlion,
318 manhack) are selected, the range is 1200 units, and the attack button is
319 `IN_ATTACK`: the primary attack launches a held object along
320 `EyeVectors()`. No aim-punch compensation is applied there (EyeVectors does
321 not add punch); the crowbar path keeps it. Without a held object the primary
322 is a short physics punch.
317- Map transitions often switch the active weapon away from the crowbar. The 323- Map transitions often switch the active weapon away from the crowbar. The
318 state change is logged as `melee: active ... crowbar=0/1` (normal log), and 324 state change is logged as `melee: active ... crowbar=0/1` (normal log), and
319 a one-time toast says to equip the crowbar. If the client weapon model is 325 a one-time toast says to equip the crowbar. If the client weapon model is
@@ -328,9 +334,9 @@ vocabulary / decisions.
328 movement keys are rotated with `preserve_movement`, and the rendered view is 334 movement keys are rotated with `preserve_movement`, and the rendered view is
329 restored immediately and at `FRAME_RENDER_START` (same silent-aim path). 335 restored immediately and at `FRAME_RENDER_START` (same silent-aim path).
330 This gives a 360-degree silent melee attack. 336 This gives a 360-degree silent melee attack.
331- While the crowbar is active and the killbot feature is on, the gun aimbot, 337- While either melee weapon is active and the killbot feature is on, the gun
332 triggerbot and silent aim are skipped to avoid fighting over the usercmd 338 aimbot, triggerbot and silent aim are skipped to avoid fighting over the
333 view. 339 usercmd view.
334- Vischeck uses the shared `MASK_VISIBLE`/world-only path (static props are 340- Vischeck uses the shared `MASK_VISIBLE`/world-only path (static props are
335 ignored). 341 ignored).
336 342
@@ -583,6 +589,11 @@ vocabulary / decisions.
583## Valuable items 589## Valuable items
584 590
585- Visuals feature; Active is on by default and unbound (always-on). 591- Visuals feature; Active is on by default and unbound (always-on).
592- "Only if usable" (`[items] usable=1`) hides health pickups when the local
593 player is at full health and armor pickups at full suit. The health ceiling
594 is the server player's `m_iMaxHealth` (not a hardcoded 100); armor uses the
595 HL2 `MAX_NORMAL_BATTERY` cap of 100, read from the server
596 `m_ArmorValue`. If the values cannot be read the filter fails open.
586- Scans the client entity list every 100 ms and tags health, ammo, armor/suit, 597- Scans the client entity list every 100 ms and tags health, ammo, armor/suit,
587 world weapon pickups, the two supply-crate models and generic items. 598 world weapon pickups, the two supply-crate models and generic items.
588- Each tag is a colored marker with a name and optional distance, filtered by 599- Each tag is a colored marker with a name and optional distance, filtered by
@@ -601,6 +612,23 @@ vocabulary / decisions.
601 item crates. The item scan falls back to the server entity's model index 612 item crates. The item scan falls back to the server entity's model index
602 (`server_model_index`) to recover the model name. 613 (`server_model_index`) to recover the model name.
603 614
615## Projectile ESP
616
617- Visuals feature `g_projectiles` (`[projectiles]`, Active on, unbound).
618- Scans the client entity list every 100 ms for world-model `w_grenade`,
619 `w_missile*`, `helicopter_bomb` and `combineball`. Weapon entities are
620 excluded by class (`...weapon...`); thrown grenades are recognised by
621 `CGrenadeFrag`-style class names or by an owner handle.
622- Fuse: `CBaseGrenade::m_flDetonateTime` is not networked, so it is read from
623 the server entity datamap (`FIELD_TIME`) and compared with
624 `IVEngineClient::Time()` (slot 14). RPG rockets have no fuse.
625- Blast radius: networked client `m_DmgRadius` when available, server
626 `m_DmgRadius` (`FIELD_FLOAT`) as fallback, then 300/200/150/300 for
627 frag/RPG/AR2/bomb.
628- Draws a small marker and a `FRAG 1.2s`-style label; the optional radius ring
629 is a 24-segment horizontal circle projected at the projectile height.
630 Toggles: Fuse timer, Blast radius.
631
604## Roadmap / TODO 632## Roadmap / TODO
605 633
606- **Server-side spread/recoil control**: not planned; recoil and accuracy 634- **Server-side spread/recoil control**: not planned; recoil and accuracy
diff --git a/src/esp.cpp b/src/esp.cpp
index 7baf2c8..c956d1b 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -898,7 +898,8 @@ static bool active_weapon_contains(void *local_ent, const char *needle) {
898 void *net = entity_networkable(idx); 898 void *net = entity_networkable(idx);
899 const char *cls = net ? entity_class_name(net, nullptr) : nullptr; 899 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
900 const char *model = wep ? entity_model_name(wep) : nullptr; 900 const char *model = wep ? entity_model_name(wep) : nullptr;
901 return (cls && strstr(cls, needle)) || (model && strstr(model, needle)); 901 return (cls && strcasestr(cls, needle)) ||
902 (model && strcasestr(model, needle));
902} 903}
903 904
904// Crossbow bolts and RPG rockets are not hitscan. Lead moving targets by the 905// Crossbow bolts and RPG rockets are not hitscan. Lead moving targets by the
@@ -1175,7 +1176,35 @@ bool melee_weapon_active(void *local_ent) {
1175 return crowbar; 1176 return crowbar;
1176} 1177}
1177 1178
1178bool melee_aimbot_apply(CUserCmd *cmd) { 1179bool gravity_gun_active(void *local_ent) {
1180 if (!local_ent)
1181 return false;
1182 if (active_weapon_contains(local_ent, "physcannon"))
1183 return true;
1184 // Same server fallback as the crowbar: map transitions can leave the
1185 // client activeweapon handle stale.
1186 int local = engine_get_local_player();
1187 if (local <= 0 || !g_iface.engineserver)
1188 return false;
1189 resolve_server_netvars();
1190 int si = server_active_weapon(local);
1191 if (si <= 0)
1192 return false;
1193 void *wep = entitylist_get(si);
1194 void *net = entity_networkable(si);
1195 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
1196 const char *model = wep ? entity_model_name(wep) : nullptr;
1197 if (!model) {
1198 void *sbase = nullptr;
1199 server_entity_health(si, nullptr, nullptr, &sbase);
1200 if (sbase)
1201 model = model_name_from_index(server_model_index(sbase));
1202 }
1203 return (cls && strcasestr(cls, "physcannon")) ||
1204 (model && strcasestr(model, "physcannon"));
1205}
1206
1207bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) {
1179 if (!g_iface.entitylist || g_net.origin < 0) 1208 if (!g_iface.entitylist || g_net.origin < 0)
1180 return false; 1209 return false;
1181 int local = engine_get_local_player(); 1210 int local = engine_get_local_player();
@@ -1188,6 +1217,9 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1188 *(int *)((uintptr_t)local_ent + g_net.health) <= 0) 1217 *(int *)((uintptr_t)local_ent + g_net.health) <= 0)
1189 return false; 1218 return false;
1190 1219
1220 // The punted object can travel much farther than the crowbar.
1221 float max_range = gravity ? 1200.0f : MELEE_RANGE;
1222
1191 using Fn = int (*)(void *); 1223 using Fn = int (*)(void *);
1192 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); 1224 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
1193 if (hi < 0 || hi > 4096) 1225 if (hi < 0 || hi > 4096)
@@ -1226,6 +1258,9 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1226 continue; 1258 continue;
1227 if (!npc_can_be_damaged(idx, model)) 1259 if (!npc_can_be_damaged(idx, model))
1228 continue; 1260 continue;
1261 // The gravity gun is only the tool for physics-vulnerable units.
1262 if (gravity && npc_damage_class(idx, model) != NPC_DMG_VULNERABLE)
1263 continue;
1229 1264
1230 Vector origin{*(float *)(e + g_net.origin), 1265 Vector origin{*(float *)(e + g_net.origin),
1231 *(float *)(e + g_net.origin + 4), 1266 *(float *)(e + g_net.origin + 4),
@@ -1265,7 +1300,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1265 if (closest.z < lo.z) closest.z = lo.z; 1300 if (closest.z < lo.z) closest.z = lo.z;
1266 if (closest.z > hiV.z) closest.z = hiV.z; 1301 if (closest.z > hiV.z) closest.z = hiV.z;
1267 float d = vec_dist(g_cam_eye, closest); 1302 float d = vec_dist(g_cam_eye, closest);
1268 if (d > MELEE_RANGE) 1303 if (d > max_range)
1269 continue; 1304 continue;
1270 1305
1271 Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f, 1306 Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f,
@@ -1298,7 +1333,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1298 float bestd = 1e9f; 1333 float bestd = 1e9f;
1299 for (int i = 0; i < np; i++) { 1334 for (int i = 0; i < np; i++) {
1300 float pd = vec_dist(g_cam_eye, pts[i]); 1335 float pd = vec_dist(g_cam_eye, pts[i]);
1301 if (pd > MELEE_RANGE || pd >= bestd) 1336 if (pd > max_range || pd >= bestd)
1302 continue; 1337 continue;
1303 if (!is_visible(g_cam_eye, pts[i], local_ent, tent)) 1338 if (!is_visible(g_cam_eye, pts[i], local_ent, tent))
1304 continue; 1339 continue;
@@ -1311,7 +1346,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1311 if (!have_aim) { 1346 if (!have_aim) {
1312 aim = c.center; 1347 aim = c.center;
1313 float ad = vec_dist(g_cam_eye, aim); 1348 float ad = vec_dist(g_cam_eye, aim);
1314 if (ad > MELEE_RANGE) { 1349 if (ad > max_range) {
1315 // The body is in range but its center is not: aim just inside the 1350 // The body is in range but its center is not: aim just inside the
1316 // closest point on the AABB. 1351 // closest point on the AABB.
1317 aim = c.closest; 1352 aim = c.closest;
@@ -1319,7 +1354,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1319 aim.y += (c.center.y - c.closest.y) * 0.25f; 1354 aim.y += (c.center.y - c.closest.y) * 0.25f;
1320 aim.z += (c.center.z - c.closest.z) * 0.25f; 1355 aim.z += (c.center.z - c.closest.z) * 0.25f;
1321 ad = vec_dist(g_cam_eye, aim); 1356 ad = vec_dist(g_cam_eye, aim);
1322 if (ad > MELEE_RANGE) 1357 if (ad > max_range)
1323 aim = c.closest; 1358 aim = c.closest;
1324 } 1359 }
1325 if (!is_visible(g_cam_eye, aim, local_ent, tent)) 1360 if (!is_visible(g_cam_eye, aim, local_ent, tent))
@@ -1328,8 +1363,10 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1328 1363
1329 QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; 1364 QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z};
1330 QAngle desired = angle_to(g_cam_eye, aim); 1365 QAngle desired = angle_to(g_cam_eye, aim);
1366 // The crowbar uses GetAutoaimVector (adds punch); the gravity gun's
1367 // launch uses EyeVectors (no punch), so only compensate for melee.
1331 float punch_x = 0.0f, punch_y = 0.0f; 1368 float punch_x = 0.0f, punch_y = 0.0f;
1332 if (punch_angles(punch_x, punch_y)) { 1369 if (!gravity && punch_angles(punch_x, punch_y)) {
1333 desired.x -= punch_x; 1370 desired.x -= punch_x;
1334 desired.y -= punch_y; 1371 desired.y -= punch_y;
1335 } 1372 }
@@ -1350,13 +1387,15 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1350 using SetViewFn = void (*)(void *, QAngle *); 1387 using SetViewFn = void (*)(void *, QAngle *);
1351 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore); 1388 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore);
1352 } 1389 }
1390 // Gravity gun primary attack launches the held object (EyeVectors);
1391 // secondary would drop it instead.
1353 cmd->buttons |= IN_ATTACK; 1392 cmd->buttons |= IN_ATTACK;
1354 1393
1355 static int logs = 0; 1394 static int logs = 0;
1356 if (logs < 10) { 1395 if (logs < 10) {
1357 logs++; 1396 logs++;
1358 logd("melee: swing idx=%d dist=%.1f aim=(%.1f %.1f %.1f)", c.idx, 1397 logd("melee: %s idx=%d dist=%.1f aim=(%.1f %.1f %.1f)",
1359 c.dist, aim.x, aim.y, aim.z); 1398 gravity ? "punt" : "swing", c.idx, c.dist, aim.x, aim.y, aim.z);
1360 } 1399 }
1361 return true; 1400 return true;
1362 } 1401 }
@@ -1365,7 +1404,7 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1365 double now = esp_now(); 1404 double now = esp_now();
1366 if (now - last_none >= 2.0) { 1405 if (now - last_none >= 2.0) {
1367 last_none = now; 1406 last_none = now;
1368 logd("melee: no visible target within %.0f", MELEE_RANGE); 1407 logd("melee: no visible target within %.0f", max_range);
1369 } 1408 }
1370 return false; 1409 return false;
1371} 1410}
@@ -2331,6 +2370,30 @@ static void items_collect(int local_idx) {
2331 } 2370 }
2332} 2371}
2333 2372
2373// Authoritative local health/armor for the "only if usable" item filter.
2374// The health ceiling comes from the server player's m_iMaxHealth, not a
2375// hardcoded 100; when it cannot be read the filter fails open.
2376static void local_vitals(int &hp, int &maxhp, int &armor) {
2377 hp = maxhp = armor = -1;
2378 int local = engine_get_local_player();
2379 if (local <= 0)
2380 return;
2381 void *le = entitylist_get(local);
2382 if (le && g_net.health >= 0)
2383 hp = *(int *)((uintptr_t)le + g_net.health);
2384 if (g_iface.engineserver) {
2385 int smax = -1;
2386 int shp = server_entity_health(local, &smax, nullptr, nullptr);
2387 if (shp >= 0)
2388 hp = shp;
2389 if (smax > 0)
2390 maxhp = smax;
2391 armor = server_player_armor(local);
2392 }
2393 if (maxhp < 0 && le && g_net.maxhealth >= 0)
2394 maxhp = *(int *)((uintptr_t)le + g_net.maxhealth);
2395}
2396
2334void esp_draw_items() { 2397void esp_draw_items() {
2335 if (!g_surface || !g_iface.entitylist || g_net.origin < 0) 2398 if (!g_surface || !g_iface.entitylist || g_net.origin < 0)
2336 return; 2399 return;
@@ -2348,10 +2411,21 @@ void esp_draw_items() {
2348 } 2411 }
2349 items_collect(local); 2412 items_collect(local);
2350 2413
2414 int vit_hp = -1, vit_max = -1, vit_armor = -1;
2415 if (g_item_usable)
2416 local_vitals(vit_hp, vit_max, vit_armor);
2417
2351 for (int i = 0; i < g_nitems; i++) { 2418 for (int i = 0; i < g_nitems; i++) {
2352 const ItemTarget &t = g_item_targets[i]; 2419 const ItemTarget &t = g_item_targets[i];
2353 if (!item_kind_enabled(t.kind)) 2420 if (!item_kind_enabled(t.kind))
2354 continue; 2421 continue;
2422 if (g_item_usable) {
2423 if (t.kind == IK_HEALTH && vit_hp >= 0 && vit_max > 0 &&
2424 vit_hp >= vit_max)
2425 continue;
2426 if (t.kind == IK_ARMOR && vit_armor >= 0 && vit_armor >= 100)
2427 continue;
2428 }
2355 float sx, sy; 2429 float sx, sy;
2356 if (!world_to_screen(t.origin, sx, sy)) 2430 if (!world_to_screen(t.origin, sx, sy))
2357 continue; 2431 continue;
@@ -2402,3 +2476,192 @@ void esp_draw_items() {
2402 draw_text_col(tx, ty, text, r, g, b); 2476 draw_text_col(tx, ty, text, r, g, b);
2403 } 2477 }
2404} 2478}
2479
2480// ---------------------------------------------------------------------------
2481// projectile ESP: grenades, rockets, helicopter bombs, AR2 balls
2482// ---------------------------------------------------------------------------
2483
2484enum ProjKind { PK_FRAG, PK_RPG, PK_BOMB, PK_BALL };
2485
2486struct ProjTarget {
2487 int idx;
2488 int kind;
2489 Vector origin;
2490 float fuse; // seconds; < 0 = impact / no fuse
2491 float radius;
2492};
2493
2494static ProjTarget g_projs[32];
2495static int g_nprojs = 0;
2496
2497static float engine_curtime() {
2498 if (!g_iface.engine)
2499 return 0.0f;
2500 using TimeFn = float (*)(void *);
2501 return vfunc<TimeFn>(g_iface.engine, 14)(g_iface.engine);
2502}
2503
2504static const char *proj_name(int kind) {
2505 switch (kind) {
2506 case PK_FRAG: return "FRAG";
2507 case PK_RPG: return "RPG";
2508 case PK_BOMB: return "BOMB";
2509 case PK_BALL: return "AR2";
2510 default: return "PROJ";
2511 }
2512}
2513
2514static void projs_collect(int local_idx) {
2515 static double last_scan = 0.0;
2516 double now = esp_now();
2517 if (now - last_scan < 0.10)
2518 return;
2519 last_scan = now;
2520 g_nprojs = 0;
2521 if (!g_iface.entitylist || g_net.origin < 0)
2522 return;
2523 resolve_server_netvars();
2524 using Fn = int (*)(void *);
2525 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
2526 if (hi < 0 || hi > 4096)
2527 hi = 4096;
2528 float curtime = engine_curtime();
2529 for (int idx = 1; idx <= hi && g_nprojs < 32; idx++) {
2530 if (idx == local_idx)
2531 continue;
2532 void *ent = entitylist_get(idx);
2533 if (!ent)
2534 continue;
2535 void *net = entity_networkable(idx);
2536 if (!net)
2537 continue;
2538 const char *cls = entity_class_name(net, nullptr);
2539 const char *model = entity_model_name(ent);
2540 if (!model)
2541 continue;
2542 bool owned = item_weapon_is_owned(ent);
2543 bool frag_cls = cls && strcasestr(cls, "grenade") &&
2544 !strcasestr(cls, "weapon");
2545 bool missile_cls = cls && strcasestr(cls, "missile") &&
2546 !strcasestr(cls, "weapon");
2547 int kind;
2548 if (strcasestr(model, "w_grenade") &&
2549 (frag_cls || (!cls && owned))) {
2550 kind = PK_FRAG;
2551 } else if ((strcasestr(model, "w_missile") ||
2552 strcasestr(model, "missile_launch") ||
2553 strcasestr(model, "missile_closed")) &&
2554 (missile_cls || (!cls && owned))) {
2555 kind = PK_RPG;
2556 } else if (strcasestr(model, "helicopter_bomb")) {
2557 kind = PK_BOMB;
2558 } else if (strcasestr(model, "combineball")) {
2559 kind = PK_BALL;
2560 } else {
2561 continue;
2562 }
2563 uintptr_t e = (uintptr_t)ent;
2564 ProjTarget &p = g_projs[g_nprojs++];
2565 p.idx = idx;
2566 p.kind = kind;
2567 p.origin = Vector{*(float *)(e + g_net.origin),
2568 *(float *)(e + g_net.origin + 4),
2569 *(float *)(e + g_net.origin + 8)};
2570 // The client m_DmgRadius is networked; fall back to the server field and
2571 // then to the stock radius for this projectile type.
2572 p.radius = -1.0f;
2573 if (g_net.dmg_radius >= 0)
2574 p.radius = *(float *)(e + g_net.dmg_radius);
2575 if (!(p.radius > 1.0f && p.radius < 2048.0f))
2576 p.radius = server_grenade_radius(idx);
2577 if (!(p.radius > 1.0f && p.radius < 2048.0f))
2578 p.radius = kind == PK_RPG ? 200.0f : kind == PK_BALL ? 150.0f : 300.0f;
2579 // m_flDetonateTime is server-only (not in the send table).
2580 p.fuse = -1.0f;
2581 if (kind != PK_RPG) {
2582 float det = server_grenade_detonate(idx);
2583 if (det > 0.0f && curtime > 0.0f)
2584 p.fuse = det - curtime;
2585 if (!(p.fuse > -2.0f && p.fuse < 30.0f))
2586 p.fuse = -1.0f;
2587 }
2588 static int logs = 0;
2589 if (logs < 8) {
2590 logs++;
2591 logf("projectile: idx=%d cls=%s model=%s kind=%s fuse=%.2f "
2592 "radius=%.0f",
2593 idx, cls ? cls : "?", model, proj_name(kind), p.fuse, p.radius);
2594 }
2595 }
2596}
2597
2598void esp_draw_projectiles() {
2599 if (!g_surface || !g_iface.entitylist || g_net.origin < 0)
2600 return;
2601 if (!g_scr_w) {
2602 using Fn = void (*)(void *, int &, int &);
2603 vfunc<Fn>(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h);
2604 if (!g_scr_w)
2605 return;
2606 }
2607 projs_collect(engine_get_local_player());
2608 for (int i = 0; i < g_nprojs; i++) {
2609 const ProjTarget &p = g_projs[i];
2610 float sx, sy;
2611 if (!world_to_screen(p.origin, sx, sy))
2612 continue;
2613 if (sx < -40 || sx > (float)g_scr_w + 40 || sy < -40 ||
2614 sy > (float)g_scr_h + 40)
2615 continue;
2616 int r = 255, g = 170, b = 60;
2617 if (p.kind == PK_RPG) {
2618 r = 255; g = 120; b = 40;
2619 } else if (p.kind == PK_BOMB) {
2620 r = 255; g = 60; b = 60;
2621 } else if (p.kind == PK_BALL) {
2622 r = 120; g = 220; b = 255;
2623 } else if (p.fuse >= 0.0f && p.fuse < 0.8f) {
2624 r = 255; g = 60; b = 60;
2625 }
2626 int x = (int)sx, y = (int)sy;
2627 surf_color(r, g, b, 255);
2628 surf_rect(0, x - 4, y - 4, x + 4, y + 4);
2629 surf_rect(1, x - 1, y - 1, x + 2, y + 2);
2630 char text[64];
2631 if (g_proj_fuse && p.fuse >= 0.0f)
2632 snprintf(text, sizeof(text), "%s %.1fs", proj_name(p.kind), p.fuse);
2633 else
2634 snprintf(text, sizeof(text), "%s", proj_name(p.kind));
2635 int tx = x + 9;
2636 if (tx > g_scr_w - 80)
2637 tx = x - 9 - (int)strlen(text) * 6;
2638 if (tx < 4)
2639 tx = 4;
2640 int ty = y - 14;
2641 if (ty < 4)
2642 ty = 4;
2643 draw_text_small_col(tx + 1, ty + 1, text, 0, 0, 0);
2644 draw_text_small_col(tx, ty, text, r, g, b);
2645 if (g_proj_radius && p.radius > 1.0f) {
2646 const int SEG = 24;
2647 float lx = 0.0f, ly = 0.0f;
2648 bool have = false;
2649 surf_color(r, g, b, 150);
2650 for (int k = 0; k <= SEG; k++) {
2651 float a = (float)k * 2.0f * PI_F / (float)SEG;
2652 Vector w{p.origin.x + cosf(a) * p.radius,
2653 p.origin.y + sinf(a) * p.radius, p.origin.z};
2654 float rx, ry;
2655 if (!world_to_screen(w, rx, ry)) {
2656 have = false;
2657 continue;
2658 }
2659 if (have)
2660 surf_line((int)lx, (int)ly, (int)rx, (int)ry);
2661 lx = rx;
2662 ly = ry;
2663 have = true;
2664 }
2665 }
2666 }
2667}
diff --git a/src/gui.cpp b/src/gui.cpp
index 59c4364..2f832a2 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -364,7 +364,9 @@ static void build_cards() {
364 add_card(1, &g_triggerbot, "Triggerbot", 364 add_card(1, &g_triggerbot, "Triggerbot",
365 "Fire when the crosshair is on a hostile.", true); 365 "Fire when the crosshair is on a hostile.", true);
366 366
367 add_card(1, &g_melee, "Melee killbot", "360-degree crowbar silent aim.", 367 add_card(1, &g_melee, "Melee killbot",
368 "360-degree crowbar swing, or a gravity-gun launch at VULN "
369 "targets while holding an object.",
368 true); 370 true);
369 371
370 int esp = add_card(2, &g_esp, "ESP", "Draw hostile NPC information.", true); 372 int esp = add_card(2, &g_esp, "ESP", "Draw hostile NPC information.", true);
@@ -405,11 +407,22 @@ static void build_cards() {
405 &g_item_crates); 407 &g_item_crates);
406 add_toggle(items, "Show distance", "Append the distance in units.", 408 add_toggle(items, "Show distance", "Append the distance in units.",
407 &g_item_distance); 409 &g_item_distance);
410 add_toggle(items, "Only if usable",
411 "Hide health kits at full health and armor at full suit.",
412 &g_item_usable);
408 add_slider(items, "Max distance", 413 add_slider(items, "Max distance",
409 "0 = unlimited; otherwise items farther away are hidden.", 0, 414 "0 = unlimited; otherwise items farther away are hidden.", 0,
410 10000, &g_item_max_dist); 415 10000, &g_item_max_dist);
411 g_ctrls[g_nctrl - 1].zero_unlimited = true; 416 g_ctrls[g_nctrl - 1].zero_unlimited = true;
412 417
418 int proj = add_card(2, &g_projectiles, "Projectiles",
419 "Tag grenades, rockets, bombs and AR2 balls.", true);
420 add_toggle(proj, "Fuse timer",
421 "Show the remaining time until a grenade or ball detonates.",
422 &g_proj_fuse);
423 add_toggle(proj, "Blast radius",
424 "Draw the projectile's damage radius on the ground.", &g_proj_radius);
425
413 int view = add_card(2, nullptr, "Camera / display", 426 int view = add_card(2, nullptr, "Camera / display",
414 "Camera FOV, viewmodel FOV and HDR brightness.", true); 427 "Camera FOV, viewmodel FOV and HDR brightness.", true);
415 add_slider(view, "Camera FOV", 428 add_slider(view, "Camera FOV",
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index ca7ee85..703a582 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -909,15 +909,18 @@ static void do_move(int seq, float frametime) {
909 note_modified(cmd, flags, vel); 909 note_modified(cmd, flags, vel);
910 910
911 bool melee = feature_active(FEAT_MELEE) && melee_weapon_active(ent); 911 bool melee = feature_active(FEAT_MELEE) && melee_weapon_active(ent);
912 bool gravity = feature_active(FEAT_MELEE) && gravity_gun_active(ent);
913 bool melee_mode = melee || gravity;
912 914
913 if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT) || melee) 915 if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT) ||
916 melee_mode)
914 aimbot_reset_lock(); 917 aimbot_reset_lock();
915 if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM) || 918 if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM) ||
916 melee) 919 melee_mode)
917 silentaim_reset_lock(); 920 silentaim_reset_lock();
918 921
919 if (melee) { 922 if (melee_mode) {
920 if (melee_aimbot_apply(cmd)) 923 if (melee_aimbot_apply(cmd, gravity && !melee))
921 note_modified(cmd, flags, vel); 924 note_modified(cmd, flags, vel);
922 } else { 925 } else {
923 if (feature_active(FEAT_AIMBOT)) { 926 if (feature_active(FEAT_AIMBOT)) {
diff --git a/src/hl2h.h b/src/hl2h.h
index f1e72be..0c8ffb5 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -99,6 +99,7 @@ struct NetVars {
99 int clr_render = -1; // client m_clrRender (entity render colour) 99 int clr_render = -1; // client m_clrRender (entity render colour)
100 int punch = -1; // client m_vecPunchAngle (aim punch) 100 int punch = -1; // client m_vecPunchAngle (aim punch)
101 int default_fov = -1; // client m_iDefaultFOV (camera fov) 101 int default_fov = -1; // client m_iDefaultFOV (camera fov)
102 int dmg_radius = -1; // client C_BaseGrenade::m_DmgRadius
102 // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 103 // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72
103 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } 104 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; }
104 // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100. 105 // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100.
@@ -165,7 +166,8 @@ bool aimbot_apply(CUserCmd *cmd);
165bool triggerbot_apply(CUserCmd *cmd); 166bool triggerbot_apply(CUserCmd *cmd);
166bool silent_aim_apply(CUserCmd *cmd); 167bool silent_aim_apply(CUserCmd *cmd);
167bool melee_weapon_active(void *local_ent); 168bool melee_weapon_active(void *local_ent);
168bool melee_aimbot_apply(CUserCmd *cmd); 169bool gravity_gun_active(void *local_ent);
170bool melee_aimbot_apply(CUserCmd *cmd, bool gravity);
169bool spread_control_apply(CUserCmd *cmd, void *local_ent); 171bool spread_control_apply(CUserCmd *cmd, void *local_ent);
170int spread_best_seed(int pellets); 172int spread_best_seed(int pellets);
171extern int g_spread_pellets; 173extern int g_spread_pellets;
@@ -184,7 +186,8 @@ enum FeatureId {
184 FEAT_WORLD_TINT = 11, 186 FEAT_WORLD_TINT = 11,
185 FEAT_FOG = 12, 187 FEAT_FOG = 12,
186 FEAT_SKYBOX = 13, 188 FEAT_SKYBOX = 13,
187 FEAT_COUNT = 14 189 FEAT_PROJECTILES = 14,
190 FEAT_COUNT = 15
188}; 191};
189enum EspOption { 192enum EspOption {
190 ESP_OPT_SHOW_NEUTRAL = 0, 193 ESP_OPT_SHOW_NEUTRAL = 0,
@@ -221,7 +224,8 @@ struct FeatureState {
221 224
222extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp, 225extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp,
223 g_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright, 226 g_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright,
224 g_world_tint, g_fog, g_skybox; 227 g_world_tint, g_fog, g_skybox, g_projectiles;
228extern bool g_proj_fuse, g_proj_radius;
225extern bool g_tint_world_on, g_tint_sky_on, g_tint_prop_on; 229extern bool g_tint_world_on, g_tint_sky_on, g_tint_prop_on;
226extern int g_tint_world_rgb[3], g_tint_sky_rgb[3], g_tint_prop_rgb[3]; 230extern int g_tint_world_rgb[3], g_tint_sky_rgb[3], g_tint_prop_rgb[3];
227extern bool g_fog_skybox, g_fog_disable; 231extern bool g_fog_skybox, g_fog_disable;
@@ -236,6 +240,7 @@ extern bool g_item_armor;
236extern bool g_item_weapons; 240extern bool g_item_weapons;
237extern bool g_item_crates; 241extern bool g_item_crates;
238extern bool g_item_distance; 242extern bool g_item_distance;
243extern bool g_item_usable;
239extern int g_item_max_dist; 244extern int g_item_max_dist;
240extern bool g_aim_bones[AIM_BONE_COUNT]; 245extern bool g_aim_bones[AIM_BONE_COUNT];
241extern int g_aim_smooth; 246extern int g_aim_smooth;
@@ -289,6 +294,9 @@ int server_npc_relation(int idx, int local_idx);
289bool server_npc_has_follow_target(int idx); 294bool server_npc_has_follow_target(int idx);
290int server_npc_state(int idx); 295int server_npc_state(int idx);
291int server_npc_takedamage(int idx); 296int server_npc_takedamage(int idx);
297int server_player_armor(int idx);
298float server_grenade_detonate(int idx);
299float server_grenade_radius(int idx);
292int server_entity_count(); 300int server_entity_count();
293void *server_entity_base(int idx); 301void *server_entity_base(int idx);
294const char *server_entity_classname(int idx); 302const char *server_entity_classname(int idx);
@@ -322,6 +330,7 @@ void draw_text_col(int x, int y, const char *s, int r, int g, int b);
322void draw_text_small_col(int x, int y, const char *s, int r, int g, int b); 330void draw_text_small_col(int x, int y, const char *s, int r, int g, int b);
323void esp_draw(); 331void esp_draw();
324void esp_draw_items(); 332void esp_draw_items();
333void esp_draw_projectiles();
325void prop_tint_update(); 334void prop_tint_update();
326void studio_props_init(); 335void studio_props_init();
327bool world_to_screen(const Vector &w, float &sx, float &sy); 336bool world_to_screen(const Vector &w, float &sx, float &sy);
diff --git a/src/menu.cpp b/src/menu.cpp
index 020d2c5..521bf43 100644
--- a/src/menu.cpp
+++ b/src/menu.cpp
@@ -60,12 +60,16 @@ int g_fog_rgb[3] = {160, 170, 190};
60int g_fog_start = 200; 60int g_fog_start = 200;
61int g_fog_end = 3000; 61int g_fog_end = 3000;
62FeatureState g_skybox{false, 0}; // skybox.png image override 62FeatureState g_skybox{false, 0}; // skybox.png image override
63FeatureState g_projectiles{true, 0}; // grenade / rocket ESP
64bool g_proj_fuse = true;
65bool g_proj_radius = true;
63bool g_item_health = true; 66bool g_item_health = true;
64bool g_item_ammo = true; 67bool g_item_ammo = true;
65bool g_item_armor = true; 68bool g_item_armor = true;
66bool g_item_weapons = true; 69bool g_item_weapons = true;
67bool g_item_crates = true; 70bool g_item_crates = true;
68bool g_item_distance = true; 71bool g_item_distance = true;
72bool g_item_usable = true;
69int g_item_max_dist = 5000; 73int g_item_max_dist = 5000;
70 74
71// Aimbot bone checklist. Head defaults on (v1 behaviour). 75// Aimbot bone checklist. Head defaults on (v1 behaviour).
@@ -227,11 +231,16 @@ void config_save() {
227 g_fog_start, g_fog_end, (int)g_fog_skybox, (int)g_fog_disable); 231 g_fog_start, g_fog_end, (int)g_fog_skybox, (int)g_fog_disable);
228 fprintf(f, "[skybox]\nactive=%d\nbind=%d\n", g_skybox.active, 232 fprintf(f, "[skybox]\nactive=%d\nbind=%d\n", g_skybox.active,
229 g_skybox.bind); 233 g_skybox.bind);
234 fprintf(f, "[projectiles]\nactive=%d\nbind=%d\nfuse=%d\nradius=%d\n",
235 g_projectiles.active, g_projectiles.bind, (int)g_proj_fuse,
236 (int)g_proj_radius);
230 fprintf(f, "[items]\nactive=%d\nbind=%d\nhealth=%d\nammo=%d\n" 237 fprintf(f, "[items]\nactive=%d\nbind=%d\nhealth=%d\nammo=%d\n"
231 "armor=%d\nweapons=%d\ncrates=%d\ndistance=%d\nmax_dist=%d\n", 238 "armor=%d\nweapons=%d\ncrates=%d\ndistance=%d\nmax_dist=%d\n"
239 "usable=%d\n",
232 g_item_esp.active, g_item_esp.bind, (int)g_item_health, 240 g_item_esp.active, g_item_esp.bind, (int)g_item_health,
233 (int)g_item_ammo, (int)g_item_armor, (int)g_item_weapons, 241 (int)g_item_ammo, (int)g_item_armor, (int)g_item_weapons,
234 (int)g_item_crates, (int)g_item_distance, g_item_max_dist); 242 (int)g_item_crates, (int)g_item_distance, g_item_max_dist,
243 (int)g_item_usable);
235 fprintf(f, "[esp]\nactive=%d\nbind=%d\nneutral=%d\nhealthbar=%d\n" 244 fprintf(f, "[esp]\nactive=%d\nbind=%d\nneutral=%d\nhealthbar=%d\n"
236 "healthnum=%d\nname=%d\nskeleton=%d\nbox=%d\nspeedgraph=%d\n" 245 "healthnum=%d\nname=%d\nskeleton=%d\nbox=%d\nspeedgraph=%d\n"
237 "dmg=%d\nalert=%d\nweapon=%d\narrows=%d\n" 246 "dmg=%d\nalert=%d\nweapon=%d\narrows=%d\n"
@@ -274,7 +283,8 @@ static bool config_load() {
274 FeatureState *cur = nullptr; 283 FeatureState *cur = nullptr;
275 bool in_esp = false, in_bhop = false, in_aim = false, 284 bool in_esp = false, in_bhop = false, in_aim = false,
276 in_jumpbug = false, in_spread = false, in_items = false, 285 in_jumpbug = false, in_spread = false, in_items = false,
277 in_tint = false, in_fog = false, in_view = false, in_movement = false; 286 in_tint = false, in_fog = false, in_view = false, in_movement = false,
287 in_proj = false;
278 while (fgets(line, sizeof(line), f)) { 288 while (fgets(line, sizeof(line), f)) {
279 line[strcspn(line, "\r\n")] = 0; 289 line[strcspn(line, "\r\n")] = 0;
280 if (line[0] == '[') { 290 if (line[0] == '[') {
@@ -300,6 +310,8 @@ static bool config_load() {
300 cur = &g_fog; 310 cur = &g_fog;
301 else if (strstr(line, "[skybox]")) 311 else if (strstr(line, "[skybox]"))
302 cur = &g_skybox; 312 cur = &g_skybox;
313 else if (strstr(line, "[projectiles]"))
314 cur = &g_projectiles;
303 else if (strstr(line, "[aimbot]")) 315 else if (strstr(line, "[aimbot]"))
304 cur = &g_aimbot; 316 cur = &g_aimbot;
305 else if (strstr(line, "[triggerbot]")) 317 else if (strstr(line, "[triggerbot]"))
@@ -322,6 +334,7 @@ static bool config_load() {
322 in_fog = (cur == &g_fog); 334 in_fog = (cur == &g_fog);
323 in_view = strstr(line, "[view]") != nullptr; 335 in_view = strstr(line, "[view]") != nullptr;
324 in_movement = strstr(line, "[movement]") != nullptr; 336 in_movement = strstr(line, "[movement]") != nullptr;
337 in_proj = (cur == &g_projectiles);
325 } else if (cur || in_view || in_movement) { 338 } else if (cur || in_view || in_movement) {
326 char *eq = strchr(line, '='); 339 char *eq = strchr(line, '=');
327 if (!eq) 340 if (!eq)
@@ -353,6 +366,8 @@ static bool config_load() {
353 g_item_crates = atoi(val) != 0; 366 g_item_crates = atoi(val) != 0;
354 else if (strcmp(key, "distance") == 0) 367 else if (strcmp(key, "distance") == 0)
355 g_item_distance = atoi(val) != 0; 368 g_item_distance = atoi(val) != 0;
369 else if (strcmp(key, "usable") == 0)
370 g_item_usable = atoi(val) != 0;
356 else if (strcmp(key, "max_dist") == 0) { 371 else if (strcmp(key, "max_dist") == 0) {
357 int v = atoi(val); 372 int v = atoi(val);
358 if (v < 0) v = 0; // 0 = unlimited 373 if (v < 0) v = 0; // 0 = unlimited
@@ -360,6 +375,12 @@ static bool config_load() {
360 g_item_max_dist = v; 375 g_item_max_dist = v;
361 } 376 }
362 } 377 }
378 if (in_proj) {
379 if (strcmp(key, "fuse") == 0)
380 g_proj_fuse = atoi(val) != 0;
381 else if (strcmp(key, "radius") == 0)
382 g_proj_radius = atoi(val) != 0;
383 }
363 if (in_aim) { 384 if (in_aim) {
364 if (strcmp(key, "smooth") == 0) { 385 if (strcmp(key, "smooth") == 0) {
365 int v = atoi(val); 386 int v = atoi(val);
@@ -581,12 +602,16 @@ void config_reset_defaults() {
581 g_fog_start = 200; 602 g_fog_start = 200;
582 g_fog_end = 3000; 603 g_fog_end = 3000;
583 g_skybox = {false, 0}; 604 g_skybox = {false, 0};
605 g_projectiles = {true, 0};
606 g_proj_fuse = true;
607 g_proj_radius = true;
584 g_item_health = true; 608 g_item_health = true;
585 g_item_ammo = true; 609 g_item_ammo = true;
586 g_item_armor = true; 610 g_item_armor = true;
587 g_item_weapons = true; 611 g_item_weapons = true;
588 g_item_crates = true; 612 g_item_crates = true;
589 g_item_distance = true; 613 g_item_distance = true;
614 g_item_usable = true;
590 g_item_max_dist = 5000; 615 g_item_max_dist = 5000;
591 g_aimbot = {true, 604}; 616 g_aimbot = {true, 604};
592 g_triggerbot = {true, 603}; 617 g_triggerbot = {true, 603};
@@ -713,7 +738,7 @@ static FeatureState *feature_state(int feature) {
713 static FeatureState *feats[FEAT_COUNT] = { 738 static FeatureState *feats[FEAT_COUNT] = {
714 &g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot, 739 &g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot,
715 &g_silentaim, &g_jumpbug, &g_melee, &g_spread, &g_item_esp, 740 &g_silentaim, &g_jumpbug, &g_melee, &g_spread, &g_item_esp,
716 &g_fullbright, &g_world_tint, &g_fog, &g_skybox}; 741 &g_fullbright, &g_world_tint, &g_fog, &g_skybox, &g_projectiles};
717 if (feature < 0 || feature >= FEAT_COUNT) 742 if (feature < 0 || feature >= FEAT_COUNT)
718 return nullptr; 743 return nullptr;
719 return feats[feature]; 744 return feats[feature];
@@ -940,9 +965,11 @@ void outer_draw() {
940 static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr; 965 static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr;
941 bool esp_on = feature_active(FEAT_ESP); 966 bool esp_on = feature_active(FEAT_ESP);
942 bool items_on = feature_active(FEAT_ITEM_ESP); 967 bool items_on = feature_active(FEAT_ITEM_ESP);
968 bool projs_on = feature_active(FEAT_PROJECTILES);
943 bool toasts = gui_toasts_active(); 969 bool toasts = gui_toasts_active();
944 if (!g_surface || 970 if (!g_surface ||
945 (!gui_is_open() && !draw_test && !esp_on && !items_on && !toasts)) 971 (!gui_is_open() && !draw_test && !esp_on && !items_on && !projs_on &&
972 !toasts))
946 return; 973 return;
947 974
948 // Level/menu -> game transitions can invalidate the VGUI font handle. 975 // Level/menu -> game transitions can invalidate the VGUI font handle.
@@ -1007,6 +1034,8 @@ void outer_draw() {
1007 esp_draw(); 1034 esp_draw();
1008 if (items_on && overlay_font) 1035 if (items_on && overlay_font)
1009 esp_draw_items(); 1036 esp_draw_items();
1037 if (projs_on && overlay_font)
1038 esp_draw_projectiles();
1010 if (draw_test && overlay_font) { 1039 if (draw_test && overlay_font) {
1011 surf_draw_set_color(255, 0, 0, 255); 1040 surf_draw_set_color(255, 0, 0, 255);
1012 surf_draw_filled_rect(20, 20, 180, 60); 1041 surf_draw_filled_rect(20, 20, 180, 60);
diff --git a/src/netvars.cpp b/src/netvars.cpp
index 33fd6a3..ef564cd 100644
--- a/src/netvars.cpp
+++ b/src/netvars.cpp
@@ -107,6 +107,11 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
107 int off = (int)(base_off + (uint32_t)poff); 107 int off = (int)(base_off + (uint32_t)poff);
108 if (off >= 0x20 && off <= 0x2000) 108 if (off >= 0x20 && off <= 0x2000)
109 g_net.default_fov = off; 109 g_net.default_fov = off;
110 } else if (strcmp(pname2, "m_DmgRadius") == 0 &&
111 g_net.dmg_radius < 0) {
112 int off = (int)(base_off + (uint32_t)poff);
113 if (off >= 0x20 && off <= 0x2000)
114 g_net.dmg_radius = off;
110 } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) { 115 } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) {
111 g_net.teamnum = (int)(base_off + (uint32_t)poff); 116 g_net.teamnum = (int)(base_off + (uint32_t)poff);
112 } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) { 117 } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) {
@@ -288,11 +293,12 @@ void resolve_netvars() {
288 logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " 293 logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d "
289 "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d " 294 "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d "
290 "modelindex=%d absvelocity=%d activeweapon=%d owner=%d " 295 "modelindex=%d absvelocity=%d activeweapon=%d owner=%d "
291 "weapon_owner=%d clr_render=%d punch=%d default_fov=%d", 296 "weapon_owner=%d clr_render=%d punch=%d default_fov=%d "
297 "dmg_radius=%d",
292 g_net.flags, g_net.velocity, g_net.movetype, g_net.health, 298 g_net.flags, g_net.velocity, g_net.movetype, g_net.health,
293 g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, 299 g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset,
294 g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex, 300 g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex,
295 g_net.absvelocity, g_net.activeweapon, g_net.owner, 301 g_net.absvelocity, g_net.activeweapon, g_net.owner,
296 g_net.weapon_owner, g_net.clr_render, g_net.punch, 302 g_net.weapon_owner, g_net.clr_render, g_net.punch,
297 g_net.default_fov); 303 g_net.default_fov, g_net.dmg_radius);
298} 304}
diff --git a/src/server.cpp b/src/server.cpp
index 39bb162..40aa850 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -31,7 +31,9 @@ enum {
31 FIELD_CHARACTER = 8, 31 FIELD_CHARACTER = 8,
32 FIELD_EMBEDDED = 10, 32 FIELD_EMBEDDED = 10,
33 FIELD_CUSTOM = 11, 33 FIELD_CUSTOM = 11,
34 FIELD_EHANDLE = 13 34 FIELD_EHANDLE = 13,
35 FIELD_TIME = 16,
36 FIELD_FLOAT = 1
35}; 37};
36 38
37// SF_FLOOR_TURRET_CITIZEN (npc_turret_floor.h). Citizen-built turrets 39// SF_FLOOR_TURRET_CITIZEN (npc_turret_floor.h). Citizen-built turrets
@@ -53,6 +55,9 @@ static int g_srv_hacked = -1;
53static int g_srv_npcstate = -1; 55static int g_srv_npcstate = -1;
54static int g_srv_takedamage = -1; 56static int g_srv_takedamage = -1;
55static int g_srv_mapname = -1; 57static int g_srv_mapname = -1;
58static int g_srv_armor = -1;
59static int g_srv_detonate = -1;
60static int g_srv_grenade_radius = -1;
56static bool g_srv_ready = false; 61static bool g_srv_ready = false;
57static bool g_srv_checked = false; 62static bool g_srv_checked = false;
58 63
@@ -150,17 +155,22 @@ bool resolve_server_netvars() {
150 g_srv_takedamage = datamap_offset("m_takedamage", FIELD_INTEGER); 155 g_srv_takedamage = datamap_offset("m_takedamage", FIELD_INTEGER);
151 // CChangeLevel::m_szMapName is a char[cchMapNameMost] on trigger_changelevel. 156 // CChangeLevel::m_szMapName is a char[cchMapNameMost] on trigger_changelevel.
152 g_srv_mapname = datamap_offset("m_szMapName", FIELD_CHARACTER, 0x40, 0x4000); 157 g_srv_mapname = datamap_offset("m_szMapName", FIELD_CHARACTER, 0x40, 0x4000);
158 // CBasePlayer::m_ArmorValue (int).
159 g_srv_armor = datamap_offset("m_ArmorValue", FIELD_INTEGER);
160 // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked).
161 g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME);
162 g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT);
153 g_srv_ready = g_srv_health > 0; 163 g_srv_ready = g_srv_health > 0;
154 if (g_srv_ready) { 164 if (g_srv_ready) {
155 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " 165 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d "
156 "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " 166 "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d "
157 "followtarget=%d spawnflags=%d hacked=%d npcstate=%d " 167 "followtarget=%d spawnflags=%d hacked=%d npcstate=%d "
158 "takedamage=%d mapname=%d", 168 "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d",
159 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, 169 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate,
160 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, 170 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity,
161 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, 171 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget,
162 g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage, 172 g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage,
163 g_srv_mapname); 173 g_srv_mapname, g_srv_armor, g_srv_detonate, g_srv_grenade_radius);
164 fail_logged = false; 174 fail_logged = false;
165 } else if (!fail_logged) { 175 } else if (!fail_logged) {
166 fail_logged = true; 176 fail_logged = true;
@@ -425,6 +435,39 @@ bool server_set_velocity_z(int idx, float vz) {
425 return true; 435 return true;
426} 436}
427 437
438// Local player's HEV suit armor from the authoritative server entity.
439int server_player_armor(int idx) {
440 if (!g_srv_ready || g_srv_armor < 0)
441 return -1;
442 void *base = nullptr;
443 server_entity_health(idx, nullptr, nullptr, &base);
444 if (!base)
445 return -1;
446 return *(int *)((uintptr_t)base + g_srv_armor);
447}
448
449// CBaseGrenade fuse time and blast radius from the server entity. Both are
450// non-networked (m_flDetonateTime is commented out of the send table).
451float server_grenade_detonate(int idx) {
452 if (!g_srv_ready || g_srv_detonate < 0)
453 return -1.0f;
454 void *base = nullptr;
455 server_entity_health(idx, nullptr, nullptr, &base);
456 if (!base)
457 return -1.0f;
458 return *(float *)((uintptr_t)base + g_srv_detonate);
459}
460
461float server_grenade_radius(int idx) {
462 if (!g_srv_ready || g_srv_grenade_radius < 0)
463 return -1.0f;
464 void *base = nullptr;
465 server_entity_health(idx, nullptr, nullptr, &base);
466 if (!base)
467 return -1.0f;
468 return *(float *)((uintptr_t)base + g_srv_grenade_radius);
469}
470
428// Entity index of the player's active weapon from the authoritative server 471// Entity index of the player's active weapon from the authoritative server
429// entity. Used as a fallback if the client m_hActiveWeapon netvar is missing. 472// entity. Used as a fallback if the client m_hActiveWeapon netvar is missing.
430int server_active_weapon(int idx) { 473int server_active_weapon(int idx) {