diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 13:53:45 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 13:53:45 +0100 |
| commit | 12fde50efc7e1f1d123f9fc34ffef53d4387de2d (patch) | |
| tree | 4de53807ce23df9b63598a0e3a12620ff19eb7cc /src/esp.cpp | |
| parent | No spread: detect pellet draws from the FireBullets seed call (diff) | |
| download | hl2h-12fde50efc7e1f1d123f9fc34ffef53d4387de2d.tar.gz hl2h-12fde50efc7e1f1d123f9fc34ffef53d4387de2d.zip | |
Add ESP, aim, view and movement batch
- ESP: alert state (IDLE/ALERT/COMBAT/SCRIPT), enemy weapon labels, hostile/
neutral/friendly colour pickers, off-screen direction arrows, and
trigger_changelevel volumes with next/previous map labels.
- Aim: silent aim reuses the Aimbot FOV, vischeck and bone settings; add
optional aim-punch compensation for aimbot, silent aim, triggerbot and
melee (server bullets add m_vecPunchAngle to the usercmd angles).
- View: always-on Camera / display card with camera FOV (writes the client
m_iDefaultFOV netvar), viewmodel FOV and monitor gamma (client cvars).
- Movement: Jump assist card with auto crouch-jump and edge jump (point
ground probe 0.12 s ahead).
- Spread: Engine spread override toggle gates the direct RandomFloat zeroing;
off leaves only the seed search.
- Server: entity count/base/classname helpers, trigger_changelevel map name
and world bounds via IServerUnknown/ICollideable.
- Netvars: m_vecPunchAngle and m_iDefaultFOV.
Diffstat (limited to '')
| -rw-r--r-- | src/esp.cpp | 361 |
1 files changed, 337 insertions, 24 deletions
diff --git a/src/esp.cpp b/src/esp.cpp index 51f46bc..1e67a8a 100644 --- a/src/esp.cpp +++ b/src/esp.cpp | |||
| @@ -24,8 +24,17 @@ struct EspTarget { | |||
| 24 | bool friendly; | 24 | bool friendly; |
| 25 | bool neutral; | 25 | bool neutral; |
| 26 | int damage; // NpcDamageClass | 26 | int damage; // NpcDamageClass |
| 27 | int state; // server NPC_STATE_* | ||
| 28 | char weapon[24]; | ||
| 27 | char name[48]; | 29 | char name[48]; |
| 28 | }; | 30 | }; |
| 31 | |||
| 32 | struct ArrowTarget { | ||
| 33 | Vector head; | ||
| 34 | bool neutral; | ||
| 35 | }; | ||
| 36 | static ArrowTarget g_arrows[32]; | ||
| 37 | static int g_narrows = 0; | ||
| 29 | static EspTarget g_targets[128]; | 38 | static EspTarget g_targets[128]; |
| 30 | static int g_ntargets = 0; | 39 | static int g_ntargets = 0; |
| 31 | static int g_scr_w = 0, g_scr_h = 0; | 40 | static int g_scr_w = 0, g_scr_h = 0; |
| @@ -273,6 +282,61 @@ static void short_name(const char *model, char *out, size_t outn) { | |||
| 273 | out[n] = 0; | 282 | out[n] = 0; |
| 274 | } | 283 | } |
| 275 | 284 | ||
| 285 | // Enemy weapon label. NPC weapons are server entities, so read the server | ||
| 286 | // model index first and fall back to the client class name. | ||
| 287 | static void weapon_short_name(int idx, char *out, size_t outn) { | ||
| 288 | out[0] = 0; | ||
| 289 | int wi = server_active_weapon(idx); | ||
| 290 | if (wi <= 0) | ||
| 291 | return; | ||
| 292 | const char *name = nullptr; | ||
| 293 | void *base = server_entity_base(wi); | ||
| 294 | int mi = base ? server_model_index(base) : -1; | ||
| 295 | if (mi > 0) | ||
| 296 | name = model_name_from_index(mi); | ||
| 297 | if (!name) { | ||
| 298 | void *wep = entitylist_get(wi); | ||
| 299 | if (wep) { | ||
| 300 | void *net = entity_networkable(wi); | ||
| 301 | name = entity_class_name(net, nullptr); | ||
| 302 | } | ||
| 303 | } | ||
| 304 | if (!name) | ||
| 305 | name = server_entity_classname(wi); | ||
| 306 | if (!name) | ||
| 307 | return; | ||
| 308 | static const struct { | ||
| 309 | const char *needle; | ||
| 310 | const char *label; | ||
| 311 | } map[] = { | ||
| 312 | {"smg1", "SMG1"}, {"irifle", "AR2"}, {"ar2", "AR2"}, | ||
| 313 | {"shotgun", "Shotgun"}, {"pistol", "Pistol"}, {"357", "357"}, | ||
| 314 | {"crossbow", "Crossbow"}, {"rocket", "RPG"}, {"rpg", "RPG"}, | ||
| 315 | {"grenade", "Grenade"}, {"frag", "Grenade"}, {"crowbar", "Crowbar"}, | ||
| 316 | {"physcannon", "Gravity"}, {"slam", "SLAM"}, {"bugbait", "Bugbait"}, | ||
| 317 | {"manhack", "Manhack"}, {"turret", "Turret"}, {"annabelle", "Rifle"}, | ||
| 318 | {"alyxgun", "Alyx gun"}, {"sniper", "Sniper"}, {"smg", "SMG"}, | ||
| 319 | }; | ||
| 320 | for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { | ||
| 321 | if (strcasestr(name, map[i].needle)) { | ||
| 322 | snprintf(out, outn, "%s", map[i].label); | ||
| 323 | return; | ||
| 324 | } | ||
| 325 | } | ||
| 326 | // Last resort: the short class/model name. | ||
| 327 | short_name(name, out, outn); | ||
| 328 | } | ||
| 329 | |||
| 330 | // Hostile / neutral / friendly colours come from the ESP colour pickers. | ||
| 331 | static void relation_rgb(const EspTarget &t, int &r, int &g, int &b) { | ||
| 332 | const int *c = t.friendly ? g_esp_col_friendly | ||
| 333 | : t.neutral ? g_esp_col_neutral | ||
| 334 | : g_esp_col_hostile; | ||
| 335 | r = c[0]; | ||
| 336 | g = c[1]; | ||
| 337 | b = c[2]; | ||
| 338 | } | ||
| 339 | |||
| 276 | static void esp_collect(int local_idx) { | 340 | static void esp_collect(int local_idx) { |
| 277 | // outer_draw() runs once per PaintTraverseEx call (several per frame); cache | 341 | // outer_draw() runs once per PaintTraverseEx call (several per frame); cache |
| 278 | // the scan briefly so we do not walk the entity list dozens of times a frame. | 342 | // the scan briefly so we do not walk the entity list dozens of times a frame. |
| @@ -286,6 +350,7 @@ static void esp_collect(int local_idx) { | |||
| 286 | if (dbg) last_dbg = now; | 350 | if (dbg) last_dbg = now; |
| 287 | 351 | ||
| 288 | g_ntargets = 0; | 352 | g_ntargets = 0; |
| 353 | g_narrows = 0; | ||
| 289 | using Fn = int (*)(void *); | 354 | using Fn = int (*)(void *); |
| 290 | int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); // GetHighestEntityIndex | 355 | int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); // GetHighestEntityIndex |
| 291 | if (hi < 0 || hi > 4096) hi = 4096; | 356 | if (hi < 0 || hi > 4096) hi = 4096; |
| @@ -361,6 +426,16 @@ static void esp_collect(int local_idx) { | |||
| 361 | *(float *)(e + g_net.origin + 8)}; | 426 | *(float *)(e + g_net.origin + 8)}; |
| 362 | Vector bottom{origin.x, origin.y, origin.z + minz}; | 427 | Vector bottom{origin.x, origin.y, origin.z + minz}; |
| 363 | Vector top{origin.x, origin.y, origin.z + maxz}; | 428 | Vector top{origin.x, origin.y, origin.z + maxz}; |
| 429 | bool neutral = false; | ||
| 430 | bool friendly = npc && npc_is_friendly(idx, model, local_idx, neutral); | ||
| 431 | // Arrows are collected before projection: a hostile behind the camera | ||
| 432 | // never produces on-screen box coordinates. | ||
| 433 | if (npc && g_narrows < 32 && !friendly && | ||
| 434 | (!neutral || esp_option(ESP_OPT_SHOW_NEUTRAL))) { | ||
| 435 | ArrowTarget &a = g_arrows[g_narrows++]; | ||
| 436 | a.head = Vector{origin.x, origin.y, origin.z + maxz * 0.7f}; | ||
| 437 | a.neutral = neutral; | ||
| 438 | } | ||
| 364 | float sx, sy, tx, ty; | 439 | float sx, sy, tx, ty; |
| 365 | if (!world_to_screen(bottom, sx, sy) || !world_to_screen(top, tx, ty)) | 440 | if (!world_to_screen(bottom, sx, sy) || !world_to_screen(top, tx, ty)) |
| 366 | continue; | 441 | continue; |
| @@ -374,10 +449,13 @@ static void esp_collect(int local_idx) { | |||
| 374 | t.health = hp > 0 ? (float)hp : 0.0f; | 449 | t.health = hp > 0 ? (float)hp : 0.0f; |
| 375 | t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; | 450 | t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; |
| 376 | t.show_health = hp > 0; | 451 | t.show_health = hp > 0; |
| 377 | bool neutral = false; | 452 | t.friendly = friendly; |
| 378 | t.friendly = npc_is_friendly(idx, model, local_idx, neutral); | ||
| 379 | t.neutral = neutral; | 453 | t.neutral = neutral; |
| 380 | t.damage = npc_damage_class(idx, model); | 454 | t.damage = npc_damage_class(idx, model); |
| 455 | t.state = npc ? server_npc_state(idx) : -1; | ||
| 456 | t.weapon[0] = 0; | ||
| 457 | if (npc) | ||
| 458 | weapon_short_name(idx, t.weapon, sizeof(t.weapon)); | ||
| 381 | short_name(model, t.name, sizeof(t.name)); | 459 | short_name(model, t.name, sizeof(t.name)); |
| 382 | if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); | 460 | if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); |
| 383 | } | 461 | } |
| @@ -562,12 +640,9 @@ static void draw_skeleton(const EspTarget &t) { | |||
| 562 | logd("skeleton: idx=%d bones=%d core=%d humanoid=%d", t.idx, n, core_count, | 640 | logd("skeleton: idx=%d bones=%d core=%d humanoid=%d", t.idx, n, core_count, |
| 563 | (int)bones_is_humanoid(t.idx)); | 641 | (int)bones_is_humanoid(t.idx)); |
| 564 | } | 642 | } |
| 565 | if (t.friendly) | 643 | int sr, sg, sb; |
| 566 | surf_color(60, 255, 60, 255); | 644 | relation_rgb(t, sr, sg, sb); |
| 567 | else if (t.neutral) | 645 | surf_color(sr, sg, sb, 255); |
| 568 | surf_color(220, 220, 220, 255); | ||
| 569 | else | ||
| 570 | surf_color(255, 60, 60, 255); | ||
| 571 | for (int i = 0; i < n; i++) { | 646 | for (int i = 0; i < n; i++) { |
| 572 | if (!ok[i]) | 647 | if (!ok[i]) |
| 573 | continue; | 648 | continue; |
| @@ -595,6 +670,24 @@ static QAngle angle_to(const Vector &from, const Vector &to) { | |||
| 595 | return a; | 670 | return a; |
| 596 | } | 671 | } |
| 597 | 672 | ||
| 673 | // Server bullet direction adds m_Local.m_vecPunchAngle to the usercmd view | ||
| 674 | // (GetAutoaimVector), so aim angles have to be pre-subtracted by it. | ||
| 675 | static bool punch_angles(float &px, float &py) { | ||
| 676 | px = py = 0.0f; | ||
| 677 | if (!aim_punch_enabled() || g_net.punch < 0) | ||
| 678 | return false; | ||
| 679 | int local = engine_get_local_player(); | ||
| 680 | if (local <= 0) | ||
| 681 | return false; | ||
| 682 | void *le = entitylist_get(local); | ||
| 683 | if (!le) | ||
| 684 | return false; | ||
| 685 | const float *p = (const float *)((uintptr_t)le + g_net.punch); | ||
| 686 | px = p[0]; | ||
| 687 | py = p[1]; | ||
| 688 | return std::isfinite(px) && std::isfinite(py); | ||
| 689 | } | ||
| 690 | |||
| 598 | static int g_aim_lock = -1; | 691 | static int g_aim_lock = -1; |
| 599 | static int g_silent_lock = -1; | 692 | static int g_silent_lock = -1; |
| 600 | 693 | ||
| @@ -852,6 +945,12 @@ bool aimbot_apply(CUserCmd *cmd) { | |||
| 852 | if (!bone_mask) | 945 | if (!bone_mask) |
| 853 | return false; | 946 | return false; |
| 854 | QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; | 947 | QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; |
| 948 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 949 | bool punch = punch_angles(punch_x, punch_y); | ||
| 950 | if (punch) { | ||
| 951 | view.x += punch_x; | ||
| 952 | view.y += punch_y; | ||
| 953 | } | ||
| 855 | int target = -1; | 954 | int target = -1; |
| 856 | Vector point{}; | 955 | Vector point{}; |
| 857 | float fov = 0.0f; | 956 | float fov = 0.0f; |
| @@ -866,6 +965,10 @@ bool aimbot_apply(CUserCmd *cmd) { | |||
| 866 | if (projectile_lead_point(target, point, lead)) | 965 | if (projectile_lead_point(target, point, lead)) |
| 867 | point = lead; | 966 | point = lead; |
| 868 | QAngle desired = angle_to(g_cam_eye, point); | 967 | QAngle desired = angle_to(g_cam_eye, point); |
| 968 | if (punch) { | ||
| 969 | desired.x -= punch_x; | ||
| 970 | desired.y -= punch_y; | ||
| 971 | } | ||
| 869 | static int want_logs = 0; | 972 | static int want_logs = 0; |
| 870 | if (want_logs < 10) { | 973 | if (want_logs < 10) { |
| 871 | want_logs++; | 974 | want_logs++; |
| @@ -930,20 +1033,30 @@ bool silent_aim_apply(CUserCmd *cmd) { | |||
| 930 | return false; | 1033 | return false; |
| 931 | QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; | 1034 | QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; |
| 932 | QAngle view{orig.x, orig.y, 0.0f}; | 1035 | QAngle view{orig.x, orig.y, 0.0f}; |
| 1036 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 1037 | bool punch = punch_angles(punch_x, punch_y); | ||
| 1038 | if (punch) { | ||
| 1039 | view.x += punch_x; | ||
| 1040 | view.y += punch_y; | ||
| 1041 | } | ||
| 933 | int target = -1; | 1042 | int target = -1; |
| 934 | Vector point{}; | 1043 | Vector point{}; |
| 935 | float fov = 0.0f; | 1044 | float fov = 0.0f; |
| 936 | float max_fov = (float)silentaim_fov(); | 1045 | float max_fov = (float)aimbot_fov(); |
| 937 | if (max_fov < 1.0f) max_fov = 1.0f; | 1046 | if (max_fov < 1.0f) max_fov = 1.0f; |
| 938 | if (max_fov > 180.0f) max_fov = 180.0f; | 1047 | if (max_fov > 180.0f) max_fov = 180.0f; |
| 939 | int *lock = feature_has_bind(FEAT_SILENTAIM) ? &g_silent_lock : nullptr; | 1048 | int *lock = feature_has_bind(FEAT_SILENTAIM) ? &g_silent_lock : nullptr; |
| 940 | if (!select_aim_target(view, bone_mask, true, max_fov, "silentaim", lock, | 1049 | if (!select_aim_target(view, bone_mask, aimbot_vischeck_enabled(), max_fov, |
| 941 | target, point, fov)) | 1050 | "silentaim", lock, target, point, fov)) |
| 942 | return false; | 1051 | return false; |
| 943 | Vector lead; | 1052 | Vector lead; |
| 944 | if (projectile_lead_point(target, point, lead)) | 1053 | if (projectile_lead_point(target, point, lead)) |
| 945 | point = lead; | 1054 | point = lead; |
| 946 | QAngle desired = angle_to(g_cam_eye, point); | 1055 | QAngle desired = angle_to(g_cam_eye, point); |
| 1056 | if (punch) { | ||
| 1057 | desired.x -= punch_x; | ||
| 1058 | desired.y -= punch_y; | ||
| 1059 | } | ||
| 947 | float np = desired.x; | 1060 | float np = desired.x; |
| 948 | if (np > 89.0f) np = 89.0f; | 1061 | if (np > 89.0f) np = 89.0f; |
| 949 | if (np < -89.0f) np = -89.0f; | 1062 | if (np < -89.0f) np = -89.0f; |
| @@ -1215,6 +1328,11 @@ bool melee_aimbot_apply(CUserCmd *cmd) { | |||
| 1215 | 1328 | ||
| 1216 | QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; | 1329 | QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; |
| 1217 | QAngle desired = angle_to(g_cam_eye, aim); | 1330 | QAngle desired = angle_to(g_cam_eye, aim); |
| 1331 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 1332 | if (punch_angles(punch_x, punch_y)) { | ||
| 1333 | desired.x -= punch_x; | ||
| 1334 | desired.y -= punch_y; | ||
| 1335 | } | ||
| 1218 | float np = desired.x; | 1336 | float np = desired.x; |
| 1219 | if (np > 89.0f) np = 89.0f; | 1337 | if (np > 89.0f) np = 89.0f; |
| 1220 | if (np < -89.0f) np = -89.0f; | 1338 | if (np < -89.0f) np = -89.0f; |
| @@ -1415,6 +1533,11 @@ bool triggerbot_apply(CUserCmd *cmd) { | |||
| 1415 | return false; | 1533 | return false; |
| 1416 | 1534 | ||
| 1417 | QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; | 1535 | QAngle view{cmd->viewangles[0], cmd->viewangles[1], 0.0f}; |
| 1536 | float punch_x = 0.0f, punch_y = 0.0f; | ||
| 1537 | if (punch_angles(punch_x, punch_y)) { | ||
| 1538 | view.x += punch_x; | ||
| 1539 | view.y += punch_y; | ||
| 1540 | } | ||
| 1418 | Vector fwd = angle_forward_vector(view); | 1541 | Vector fwd = angle_forward_vector(view); |
| 1419 | Vector start = g_cam_eye; | 1542 | Vector start = g_cam_eye; |
| 1420 | Vector end{start.x + fwd.x * 8192.0f, start.y + fwd.y * 8192.0f, | 1543 | Vector end{start.x + fwd.x * 8192.0f, start.y + fwd.y * 8192.0f, |
| @@ -1513,6 +1636,181 @@ void prop_tint_update() { | |||
| 1513 | } | 1636 | } |
| 1514 | } | 1637 | } |
| 1515 | 1638 | ||
| 1639 | // Small left-edge label, stacked above the box (10 px font + 1 px shadow). | ||
| 1640 | static void draw_left_tag(int left, int y, const char *text, int r, int g, | ||
| 1641 | int b) { | ||
| 1642 | int tw = (int)strlen(text) * 6; | ||
| 1643 | int tx = left - tw - 3; | ||
| 1644 | if (tx < 2) | ||
| 1645 | tx = 2; | ||
| 1646 | draw_text_small_col(tx + 1, y + 1, text, 0, 0, 0); | ||
| 1647 | draw_text_small_col(tx, y, text, r, g, b); | ||
| 1648 | } | ||
| 1649 | |||
| 1650 | static const char *alert_tag(int state, int &r, int &g, int &b) { | ||
| 1651 | switch (state) { | ||
| 1652 | case 1: r = 170; g = 180; b = 190; return "IDLE"; | ||
| 1653 | case 2: r = 255; g = 220; b = 80; return "ALERT"; | ||
| 1654 | case 3: r = 255; g = 90; b = 70; return "COMBAT"; | ||
| 1655 | case 4: r = 150; g = 200; b = 255; return "SCRIPT"; | ||
| 1656 | default: return nullptr; | ||
| 1657 | } | ||
| 1658 | } | ||
| 1659 | |||
| 1660 | // Direction markers for hostiles outside the view. Drawn at the screen edge | ||
| 1661 | // using the engine projection when the target is in front, and the view-right | ||
| 1662 | // dot product when it is behind. | ||
| 1663 | static void draw_edge_arrows() { | ||
| 1664 | if (!g_surface || !g_scr_w || !g_narrows || !esp_option(ESP_OPT_ARROWS)) | ||
| 1665 | return; | ||
| 1666 | float cx = (float)g_scr_w * 0.5f, cy = (float)g_scr_h * 0.5f; | ||
| 1667 | const float margin = 26.0f; | ||
| 1668 | int drawn = 0; | ||
| 1669 | for (int i = 0; i < g_narrows && drawn < 12; i++) { | ||
| 1670 | const ArrowTarget &a = g_arrows[i]; | ||
| 1671 | float sx, sy; | ||
| 1672 | bool proj = world_to_screen(a.head, sx, sy); | ||
| 1673 | if (proj && sx >= 4.0f && sx <= (float)g_scr_w - 4.0f && sy >= 4.0f && | ||
| 1674 | sy <= (float)g_scr_h - 4.0f) | ||
| 1675 | continue; // already visible as a box | ||
| 1676 | float dx, dy; | ||
| 1677 | if (proj) { | ||
| 1678 | dx = sx - cx; | ||
| 1679 | dy = sy - cy; | ||
| 1680 | } else { | ||
| 1681 | float yaw = g_cam_view.y * PI_F / 180.0f; | ||
| 1682 | float wx = a.head.x - g_cam_eye.x; | ||
| 1683 | float wy = a.head.y - g_cam_eye.y; | ||
| 1684 | float right = wx * sinf(yaw) + wy * (-cosf(yaw)); | ||
| 1685 | dx = right >= 0.0f ? 1.0f : -1.0f; | ||
| 1686 | dy = 0.0f; | ||
| 1687 | } | ||
| 1688 | float len = sqrtf(dx * dx + dy * dy); | ||
| 1689 | if (len < 1e-3f) { | ||
| 1690 | dx = 0.0f; | ||
| 1691 | dy = 1.0f; | ||
| 1692 | len = 1.0f; | ||
| 1693 | } | ||
| 1694 | dx /= len; | ||
| 1695 | dy /= len; | ||
| 1696 | float half_w = cx - margin, half_h = cy - margin; | ||
| 1697 | float tx = fabsf(dx) > 1e-4f ? half_w / fabsf(dx) : 1e9f; | ||
| 1698 | float ty = fabsf(dy) > 1e-4f ? half_h / fabsf(dy) : 1e9f; | ||
| 1699 | float d = tx < ty ? tx : ty; | ||
| 1700 | float ex = cx + dx * d, ey = cy + dy * d; | ||
| 1701 | float px = -dy, py = dx; | ||
| 1702 | const int *col = a.neutral ? g_esp_col_neutral : g_esp_col_hostile; | ||
| 1703 | surf_color(col[0], col[1], col[2], 235); | ||
| 1704 | surf_line((int)ex, (int)ey, (int)(ex - dx * 13.0f + px * 6.0f), | ||
| 1705 | (int)(ey - dy * 13.0f + py * 6.0f)); | ||
| 1706 | surf_line((int)ex, (int)ey, (int)(ex - dx * 13.0f - px * 6.0f), | ||
| 1707 | (int)(ey - dy * 13.0f - py * 6.0f)); | ||
| 1708 | drawn++; | ||
| 1709 | } | ||
| 1710 | } | ||
| 1711 | |||
| 1712 | // Map transition triggers are server-only brush entities. Draw their world | ||
| 1713 | // bounds and the target map; the previous map is remembered across level | ||
| 1714 | // changes from the worldspawn model name. | ||
| 1715 | struct TransitionBox { | ||
| 1716 | char next[128]; | ||
| 1717 | Vector mins, maxs; | ||
| 1718 | }; | ||
| 1719 | static TransitionBox g_trans[8]; | ||
| 1720 | static int g_ntrans = 0; | ||
| 1721 | static char g_map_last[64]; | ||
| 1722 | static char g_map_prev[64]; | ||
| 1723 | |||
| 1724 | static void map_name_from_world(char *out, size_t outn) { | ||
| 1725 | out[0] = 0; | ||
| 1726 | void *w = entitylist_get(0); | ||
| 1727 | const char *m = w ? entity_model_name(w) : nullptr; | ||
| 1728 | if (!m) | ||
| 1729 | return; | ||
| 1730 | const char *b = strrchr(m, '/'); | ||
| 1731 | b = b ? b + 1 : m; | ||
| 1732 | size_t n = strcspn(b, "."); | ||
| 1733 | if (n >= outn) | ||
| 1734 | n = outn - 1; | ||
| 1735 | memcpy(out, b, n); | ||
| 1736 | out[n] = 0; | ||
| 1737 | } | ||
| 1738 | |||
| 1739 | static void transitions_scan() { | ||
| 1740 | char cur[64]; | ||
| 1741 | map_name_from_world(cur, sizeof(cur)); | ||
| 1742 | if (strcmp(cur, g_map_last) != 0) { | ||
| 1743 | if (g_map_last[0]) | ||
| 1744 | snprintf(g_map_prev, sizeof(g_map_prev), "%s", g_map_last); | ||
| 1745 | snprintf(g_map_last, sizeof(g_map_last), "%s", cur); | ||
| 1746 | } | ||
| 1747 | int count = server_entity_count(); | ||
| 1748 | int n = 0; | ||
| 1749 | for (int i = 1; i <= count && n < 8; i++) { | ||
| 1750 | TransitionBox &b = g_trans[n]; | ||
| 1751 | b.mins = Vector{0, 0, 0}; | ||
| 1752 | b.maxs = Vector{0, 0, 0}; | ||
| 1753 | if (!server_trigger_info(i, b.next, sizeof(b.next), b.mins, b.maxs)) | ||
| 1754 | continue; | ||
| 1755 | if (b.mins.x == b.maxs.x && b.mins.y == b.maxs.y && | ||
| 1756 | b.mins.z == b.maxs.z) | ||
| 1757 | continue; | ||
| 1758 | n++; | ||
| 1759 | } | ||
| 1760 | g_ntrans = n; | ||
| 1761 | static int logged = 0; | ||
| 1762 | if (n > 0 && logged < 4) { | ||
| 1763 | logged++; | ||
| 1764 | logf("transitions: %d changelevel trigger(s), next=%s prev=%s", n, | ||
| 1765 | g_trans[0].next, g_map_prev[0] ? g_map_prev : "?"); | ||
| 1766 | } | ||
| 1767 | } | ||
| 1768 | |||
| 1769 | static void draw_transitions() { | ||
| 1770 | if (!g_surface || !g_ntrans) | ||
| 1771 | return; | ||
| 1772 | for (int i = 0; i < g_ntrans; i++) { | ||
| 1773 | const TransitionBox &b = g_trans[i]; | ||
| 1774 | float minx = 1e9f, miny = 1e9f, maxx = -1e9f, maxy = -1e9f; | ||
| 1775 | int seen = 0; | ||
| 1776 | for (int c = 0; c < 8; c++) { | ||
| 1777 | Vector p{(c & 1) ? b.maxs.x : b.mins.x, (c & 2) ? b.maxs.y : b.mins.y, | ||
| 1778 | (c & 4) ? b.maxs.z : b.mins.z}; | ||
| 1779 | float sx, sy; | ||
| 1780 | if (!world_to_screen(p, sx, sy) || !std::isfinite(sx) || | ||
| 1781 | !std::isfinite(sy)) | ||
| 1782 | continue; | ||
| 1783 | if (sx < minx) minx = sx; | ||
| 1784 | if (sx > maxx) maxx = sx; | ||
| 1785 | if (sy < miny) miny = sy; | ||
| 1786 | if (sy > maxy) maxy = sy; | ||
| 1787 | seen++; | ||
| 1788 | } | ||
| 1789 | if (!seen) | ||
| 1790 | continue; | ||
| 1791 | int x0 = (int)minx, x1 = (int)maxx, y0 = (int)miny, y1 = (int)maxy; | ||
| 1792 | if (x1 < -2000 || x0 > g_scr_w + 2000 || y1 < -2000 || | ||
| 1793 | y0 > g_scr_h + 2000) | ||
| 1794 | continue; | ||
| 1795 | surf_color(255, 170, 60, 200); | ||
| 1796 | surf_rect(0, x0, y0, x1, y1); | ||
| 1797 | char line[128]; | ||
| 1798 | snprintf(line, sizeof(line), "-> %.100s", b.next); | ||
| 1799 | int tx = x0; | ||
| 1800 | if (tx < 4) tx = 4; | ||
| 1801 | if (tx > g_scr_w - 120) tx = g_scr_w - 120; | ||
| 1802 | int ty = y0 - 16; | ||
| 1803 | if (ty < 4) ty = 4; | ||
| 1804 | draw_text_small_col(tx + 1, ty + 1, line, 0, 0, 0); | ||
| 1805 | draw_text_small_col(tx, ty, line, 255, 180, 70); | ||
| 1806 | if (g_map_prev[0]) { | ||
| 1807 | snprintf(line, sizeof(line), "prev %.48s", g_map_prev); | ||
| 1808 | draw_text_small_col(tx + 1, ty + 13, line, 0, 0, 0); | ||
| 1809 | draw_text_small_col(tx, ty + 12, line, 200, 200, 200); | ||
| 1810 | } | ||
| 1811 | } | ||
| 1812 | } | ||
| 1813 | |||
| 1516 | void esp_draw() { | 1814 | void esp_draw() { |
| 1517 | if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return; | 1815 | if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return; |
| 1518 | if (!g_scr_w) { | 1816 | if (!g_scr_w) { |
| @@ -1523,6 +1821,13 @@ void esp_draw() { | |||
| 1523 | } | 1821 | } |
| 1524 | draw_fov_circle(); | 1822 | draw_fov_circle(); |
| 1525 | esp_collect(engine_get_local_player()); | 1823 | esp_collect(engine_get_local_player()); |
| 1824 | static double last_trans = 0.0; | ||
| 1825 | double trans_now = esp_now(); | ||
| 1826 | if (trans_now - last_trans >= 0.25) { | ||
| 1827 | last_trans = trans_now; | ||
| 1828 | transitions_scan(); | ||
| 1829 | } | ||
| 1830 | draw_transitions(); | ||
| 1526 | for (int i = 0; i < g_ntargets; i++) { | 1831 | for (int i = 0; i < g_ntargets; i++) { |
| 1527 | const EspTarget &t = g_targets[i]; | 1832 | const EspTarget &t = g_targets[i]; |
| 1528 | if (t.neutral && !esp_option(ESP_OPT_SHOW_NEUTRAL)) | 1833 | if (t.neutral && !esp_option(ESP_OPT_SHOW_NEUTRAL)) |
| @@ -1532,9 +1837,9 @@ void esp_draw() { | |||
| 1532 | if (ent && bones_update(t.idx, ent)) | 1837 | if (ent && bones_update(t.idx, ent)) |
| 1533 | draw_skeleton(t); | 1838 | draw_skeleton(t); |
| 1534 | } | 1839 | } |
| 1535 | if (t.friendly) surf_color(60, 255, 60, 255); | 1840 | int cr, cg, cb; |
| 1536 | else if (t.neutral) surf_color(220, 220, 220, 255); | 1841 | relation_rgb(t, cr, cg, cb); |
| 1537 | else surf_color(255, 60, 60, 255); | 1842 | surf_color(cr, cg, cb, 255); |
| 1538 | float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width | 1843 | float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width |
| 1539 | int left = (int)(t.sx - w / 2); | 1844 | int left = (int)(t.sx - w / 2); |
| 1540 | int right = (int)(t.sx + w / 2); | 1845 | int right = (int)(t.sx + w / 2); |
| @@ -1568,7 +1873,8 @@ void esp_draw() { | |||
| 1568 | draw_text_col(num_x, top, hptext, 255, 255, 255); | 1873 | draw_text_col(num_x, top, hptext, 255, 255, 255); |
| 1569 | } | 1874 | } |
| 1570 | if (esp_option(ESP_OPT_SHOW_NAME)) | 1875 | if (esp_option(ESP_OPT_SHOW_NAME)) |
| 1571 | draw_text_col(left, top - 14, t.name, 255, 255, 255); | 1876 | draw_text_col(left, top - 14, t.name, cr, cg, cb); |
| 1877 | int tag_y = top + 2; | ||
| 1572 | if (esp_option(ESP_OPT_DAMAGE_TAGS)) { | 1878 | if (esp_option(ESP_OPT_DAMAGE_TAGS)) { |
| 1573 | const char *tag = nullptr; | 1879 | const char *tag = nullptr; |
| 1574 | int tr = 255, tg = 255, tb = 255; | 1880 | int tr = 255, tg = 255, tb = 255; |
| @@ -1583,19 +1889,26 @@ void esp_draw() { | |||
| 1583 | tr = 180; tg = 180; tb = 180; | 1889 | tr = 180; tg = 180; tb = 180; |
| 1584 | } | 1890 | } |
| 1585 | if (tag) { | 1891 | if (tag) { |
| 1586 | // Small label on the left edge of the box, black 1px shadow for | 1892 | draw_left_tag(left, tag_y, tag, tr, tg, tb); |
| 1587 | // readability against the world (the 10px ESP font). | 1893 | tag_y += 12; |
| 1588 | int tw = (int)strlen(tag) * 6; | ||
| 1589 | int tx = left - tw - 3; | ||
| 1590 | if (tx < 2) | ||
| 1591 | tx = 2; | ||
| 1592 | int ty = top + 2; | ||
| 1593 | draw_text_small_col(tx + 1, ty + 1, tag, 0, 0, 0); | ||
| 1594 | draw_text_small_col(tx, ty, tag, tr, tg, tb); | ||
| 1595 | } | 1894 | } |
| 1596 | } | 1895 | } |
| 1896 | if (esp_option(ESP_OPT_ALERT)) { | ||
| 1897 | int ar, ag, ab; | ||
| 1898 | const char *tag = alert_tag(t.state, ar, ag, ab); | ||
| 1899 | if (tag) { | ||
| 1900 | draw_left_tag(left, tag_y, tag, ar, ag, ab); | ||
| 1901 | tag_y += 12; | ||
| 1902 | } | ||
| 1903 | } | ||
| 1904 | if (esp_option(ESP_OPT_WEAPON) && t.weapon[0]) { | ||
| 1905 | draw_left_tag(left, tag_y, t.weapon, 220, 220, 220); | ||
| 1906 | tag_y += 12; | ||
| 1907 | } | ||
| 1597 | } | 1908 | } |
| 1598 | 1909 | ||
| 1910 | draw_edge_arrows(); | ||
| 1911 | |||
| 1599 | // Groundwork probe: HL2H_BONE_DEBUG=1 draws a white marker at each NPC's | 1912 | // Groundwork probe: HL2H_BONE_DEBUG=1 draws a white marker at each NPC's |
| 1600 | // "head" hitbox center (from SetupBones + studio hitboxes). | 1913 | // "head" hitbox center (from SetupBones + studio hitboxes). |
| 1601 | static bool bone_debug = getenv("HL2H_BONE_DEBUG") != nullptr; | 1914 | static bool bone_debug = getenv("HL2H_BONE_DEBUG") != nullptr; |
