// esp.cpp — ESP drawing + shared world_to_screen / entity gather (aimbot reuses these) #include "esp_internal.h" #include "hl2h.h" #include #include #include #include #include #include struct EspTarget { int idx; float sx, sy; // screen pos of feet float h; // box height px float health, maxhealth; bool show_health; bool friendly; bool neutral; int damage; // NpcDamageClass int state; // server NPC_STATE_* char weapon[24]; char name[48]; }; struct ArrowTarget { Vector head; bool neutral; }; static ArrowTarget g_arrows[32]; static int g_narrows = 0; static EspTarget g_targets[128]; static int g_ntargets = 0; int g_scr_w = 0, g_scr_h = 0; static bool g_esp_logged = false; // Speed graph: 100 samples at 0.1 s = the last 10 seconds. static constexpr int SPEED_SAMPLES = 100; static float g_speed_samples[SPEED_SAMPLES] = {}; static int g_speed_head = 0; static double g_speed_last_sample = 0.0; double esp_now() { timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; } // Refresh the cached screen size every frame. It used to be read once, so a // resolution or window change left every projection, screen-edge clamp and // centering calculation stale for the rest of the session. bool esp_update_screen() { if (!g_surface) return false; int w = 0, h = 0; overlay_screen_size(w, h); if (w > 0) g_scr_w = w; if (h > 0) g_scr_h = h; return g_scr_w > 0 && g_scr_h > 0; } // Local perspective projection used when the engine's debug-overlay // projection refuses a point that is actually in front of the camera. static bool project_manual(const Vector &w, float &sx, float &sy) { if (!g_scr_w || !g_scr_h) return false; float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; float roll = g_cam_view.z * PI_F / 180.0f; float sp = sinf(pitch), cp = cosf(pitch); float syy = sinf(yaw), cy = cosf(yaw); float sr = sinf(roll), cr = cosf(roll); float fwd[3] = {cp * cy, cp * syy, -sp}; // Source's AngleVectors with roll; the old right/up vectors assumed roll 0, // so view punch made the fallback projection rotate against the world. float right[3] = {-sr * sp * cy + cr * syy, -sr * sp * syy - cr * cy, -sr * cp}; float up[3] = {cr * sp * cy + sr * syy, cr * sp * syy - sr * cy, cr * cp}; float dx = w.x - g_cam_eye.x, dy = w.y - g_cam_eye.y, dz = w.z - g_cam_eye.z; float z = dx * fwd[0] + dy * fwd[1] + dz * fwd[2]; if (z < 1.0f) return false; float x = dx * right[0] + dy * right[1] + dz * right[2]; float y = dx * up[0] + dy * up[1] + dz * up[2]; float fov = g_fov_camera > 10 ? (float)g_fov_camera : 75.0f; // Source defines the FOV for a 4:3 reference and widens it on widescreen; // using a raw tan(fov/2) made the fallback ~33% too narrow at 16:9. float aspect = (float)g_scr_w / (float)g_scr_h; float tanfov = tanf(fov * 0.5f * PI_F / 180.0f) * (aspect / (4.0f / 3.0f)); sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov); sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov); return std::isfinite(sx) && std::isfinite(sy) && fabsf(sx) < 1e7f && fabsf(sy) < 1e7f; } bool world_to_screen(const Vector &w, float &sx, float &sy) { // Preferred: the engine's own transform. Uses the real view setup (FOV, // aspect, roll), so boxes do not drift at the screen edges. if (g_iface.debugoverlay) { using Fn = int (*)(void *, const Vector *, Vector *); Vector s; int behind = vfunc(g_iface.debugoverlay, 9)(g_iface.debugoverlay, &w, &s); if (behind) { // The overlay transform can report "behind" for points that are in // front but near the view axis/extremes (e.g. something directly below // the player). Use the local projection in that case. float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; float cp = cosf(pitch), sp = sinf(pitch); float fwd[3] = {cp * cosf(yaw), cp * sinf(yaw), -sp}; float z = (w.x - g_cam_eye.x) * fwd[0] + (w.y - g_cam_eye.y) * fwd[1] + (w.z - g_cam_eye.z) * fwd[2]; if (z > 4.0f) return project_manual(w, sx, sy); return false; } // A point on the view axis can produce NaN/inf or enormous coordinates; // drawing those as ints corrupts the surface clip state and hides the // rest of the overlay. if (!std::isfinite(s.x) || !std::isfinite(s.y) || fabsf(s.x) > 1e7f || fabsf(s.y) > 1e7f) return false; sx = s.x; sy = s.y; return true; } return project_manual(w, sx, sy); } // --------------------------------------------------------------------------- // entity helpers (IClientEntityList / IClientNetworkable, header-documented) // --------------------------------------------------------------------------- using GetNetFn = void *(*)(void *, int); using GetCCFn = void *(*)(void *); using GetModelFn = const void *(*)(void *, int); using GetModelNameFn = const char *(*)(void *, const void *); // entitylist slot 0 = GetClientNetworkable(entnum). Safer than assuming the // IClientNetworkable subobject offset inside the entity (see projectz). static void relation_rgb(const EspTarget &t, int &r, int &g, int &b) { const int *c = t.friendly ? g_esp_col_friendly : t.neutral ? g_esp_col_neutral : g_esp_col_hostile; r = c[0]; g = c[1]; b = c[2]; } static void esp_collect(int local_idx) { // outer_draw() runs once per PaintTraverseEx call (several per frame); cache // the scan briefly so we do not walk the entity list dozens of times a frame. static double last_scan = 0.0; static double last_dbg = 0.0; double now = esp_now(); if (now - last_scan < 0.01) return; last_scan = now; bool dbg = (now - last_dbg) >= 2.0; if (dbg) last_dbg = now; g_ntargets = 0; g_narrows = 0; using Fn = int (*)(void *); int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); // GetHighestEntityIndex if (hi < 0 || hi > 4096) hi = 4096; int npc_alive = 0, dbg_lines = 0; bool srv = resolve_server_netvars(); for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) { if (idx == local_idx) continue; void *ent = entitylist_get(idx); if (!ent) continue; entptr_store(ent, idx); void *net = entity_networkable(idx); if (!net || entity_is_dormant(net)) continue; const char *cls = entity_class_name(net, nullptr); if (!cls) continue; uintptr_t e = (uintptr_t)ent; int cli_life = g_net.lifestate >= 0 ? (int)*(unsigned char *)(e + g_net.lifestate) : -1; int cli_hp = g_net.health >= 0 ? *(int *)(e + g_net.health) : 0; bool npc = is_npc_class(cls); const char *model = entity_model_name(ent); // NPC health is not networked; read it from the listen-server entity. int hp = cli_hp, maxhp = -1, life = cli_life; if (npc && srv) { int slife = -1, smax = -1; void *sbase = nullptr; int shp = server_entity_health(idx, &smax, &slife, &sbase); if (!g_srv_checked && sbase) { int cli_mi = g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1; int srv_mi = server_model_index(sbase); // Wait for both indices: the client index is often 0 for a tick right // after a map load, which used to latch "untrusted" permanently. if (cli_mi > 0 && srv_mi > 0) { g_srv_checked = true; g_srv_trusted = (cli_mi == srv_mi); logd("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d " "hp=%d maxhp=%d life=%d trusted=%d", idx, sbase, cli_mi, srv_mi, shp, smax, slife, (int)g_srv_trusted); if (!g_srv_trusted) logf("ESP: server entity mismatch, ignoring server values"); } } if (g_srv_trusted) { if (shp >= 0) hp = shp; if (smax > 0) maxhp = smax; if (slife >= 0) life = slife; } } if (dbg && npc && dbg_lines < 40) { dbg_lines++; logd("ESP npc %d cls=%s life=%d(cli %d) hp=%d(cli %d) maxhp=%d model=%s", idx, cls, life, cli_life, hp, cli_hp, maxhp, model ? model : "?"); } if (npc) { if (life != LIFE_ALIVE) continue; npc_alive++; } else { // players (HL2DM) are the only entities with a live networked m_iHealth if (!strstr(cls, "Player") || !(hp > 0)) continue; } float minz = g_net.mins >= 0 ? *(float *)(e + g_net.mins + 8) : 0.0f; float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; if (!(maxz > minz + 4.0f && maxz - minz < 512.0f)) { minz = 0.0f; maxz = 72.0f; } Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), *(float *)(e + g_net.origin + 8)}; bool neutral = false; bool friendly = npc && npc_is_friendly(idx, model, local_idx, neutral); // Arrows are collected before projection: a hostile behind the camera // never produces on-screen box coordinates. if (npc && g_narrows < 32 && !friendly && (!neutral || esp_option(ESP_OPT_SHOW_NEUTRAL))) { ArrowTarget &a = g_arrows[g_narrows++]; a.head = Vector{origin.x, origin.y, origin.z + maxz * 0.7f}; a.neutral = neutral; } // Project several points along the vertical axis. Requiring both feet and // head to project dropped the target when the feet were behind the near // plane (common when standing over/next to something). float min_sy = 1e9f, max_sy = -1e9f, low_x = 0.0f; bool projected = false; const int VSAMPLES = 6; for (int k = 0; k <= VSAMPLES; k++) { float z = origin.z + minz + (maxz - minz) * (float)k / (float)VSAMPLES; float px, py; if (!world_to_screen(Vector{origin.x, origin.y, z}, px, py)) continue; if (px < -2.0f * (float)g_scr_w || px > 3.0f * (float)g_scr_w) continue; projected = true; if (py < min_sy) min_sy = py; if (py > max_sy) { max_sy = py; low_x = px; } } if (!projected) { static int fail_logs = 0; float ddx = origin.x - g_cam_eye.x; float ddy = origin.y - g_cam_eye.y; float ddz = origin.z - g_cam_eye.z; if (fail_logs < 8 && ddx * ddx + ddy * ddy + ddz * ddz < 400.0f * 400.0f) { fail_logs++; logf("ESP: close target %d model=%s failed projection at " "(%.1f %.1f %.1f)", idx, model ? model : "?", origin.x, origin.y, origin.z); } continue; } float h = max_sy - min_sy; if (h < 3.0f || max_sy < 0 || min_sy > (float)g_scr_h) continue; // behind / offscreen if (low_x < -(float)g_scr_w || low_x > 2.0f * (float)g_scr_w) continue; float sx = low_x, sy = max_sy; EspTarget &t = g_targets[g_ntargets++]; t.idx = idx; t.sx = sx; t.sy = sy; t.h = h; t.health = hp > 0 ? (float)hp : 0.0f; t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; t.show_health = hp > 0; t.friendly = friendly; t.neutral = neutral; t.damage = npc_damage_class(idx, model); t.state = npc ? server_npc_state(idx) : -1; t.weapon[0] = 0; if (npc) weapon_short_name(idx, t.weapon, sizeof(t.weapon)); short_name(model, t.name, sizeof(t.name)); if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); } if (dbg) logd("ESP scan: hi=%d npc_alive=%d targets=%d", hi, npc_alive, g_ntargets); if (g_ntargets && !g_esp_logged) { g_esp_logged = true; logd("ESP: first targets, %d", g_ntargets); } } void surf_rect(int filled, int x0, int y0, int x1, int y1) { if (filled) overlay_fill(x0, y0, x1, y1); else overlay_outline_rect(x0, y0, x1, y1); } void surf_color(int r, int g, int b, int a) { overlay_color(r, g, b, a); } void surf_line(int x0, int y0, int x1, int y1) { overlay_line(x0, y0, x1, y1); } // Screen projection of the aimbot FOV cone boundary, using the engine's own // world->screen transform so it follows the actual aspect/fov. QAngle engine_view_angles() { QAngle v{g_cam_view.x, g_cam_view.y, g_cam_view.z}; if (g_iface.engine) { using Fn = void (*)(void *, QAngle *); vfunc(g_iface.engine, 19)(g_iface.engine, &v); } return v; } void esp_draw_fov_circle() { if (!g_surface || !aimbot_fov_circle()) return; // No local player means no game world (main menu); the cached camera is stale // and the projected circle would land at a meaningless spot. if (engine_get_local_player() <= 0) return; if (!esp_update_screen()) return; float fov = (float)aimbot_fov(); if (fov < 1.0f || fov >= 89.0f) // beyond this the "circle" is behind the eye return; float alpha = fov * PI_F / 180.0f; QAngle view = engine_view_angles(); float yaw = view.y * PI_F / 180.0f; float pitch = view.x * PI_F / 180.0f; Vector fwd{cosf(pitch) * cosf(yaw), cosf(pitch) * sinf(yaw), -sinf(pitch)}; Vector right{sinf(yaw), -cosf(yaw), 0.0f}; Vector up{right.y * fwd.z - right.z * fwd.y, right.z * fwd.x - right.x * fwd.z, right.x * fwd.y - right.y * fwd.x}; constexpr float D = 1024.0f; float ca = cosf(alpha), sa = sinf(alpha); Vector eh{g_cam_eye.x + (fwd.x * ca + right.x * sa) * D, g_cam_eye.y + (fwd.y * ca + right.y * sa) * D, g_cam_eye.z + (fwd.z * ca + right.z * sa) * D}; Vector ev{g_cam_eye.x + (fwd.x * ca + up.x * sa) * D, g_cam_eye.y + (fwd.y * ca + up.y * sa) * D, g_cam_eye.z + (fwd.z * ca + up.z * sa) * D}; float hsx, hsy, vsx, vsy; if (!world_to_screen(eh, hsx, hsy) || !world_to_screen(ev, vsx, vsy)) return; float cx = (float)g_scr_w * 0.5f, cy = (float)g_scr_h * 0.5f; float rx = fabsf(hsx - cx), ry = fabsf(vsy - cy); if (!(rx > 1.0f && ry > 1.0f) || rx > 1e5f || ry > 1e5f) return; constexpr int N = 64; int px = 0, py = 0; surf_color(255, 255, 255, 160); for (int i = 0; i <= N; i++) { float phi = (2.0f * PI_F * i) / N; int x = (int)(cx + rx * cosf(phi)); int y = (int)(cy + ry * sinf(phi)); if (i > 0) surf_line(px, py, x, y); px = x; py = y; } } // Speed graph: 100 samples at 0.1 s = the last 10 seconds; min is 0, max is // the window maximum. Small and centered in the lower half of the screen. static void speed_graph_sample() { // Only while actually playing: IVEngineClient013 slot 26 IsInGame, // slot 84 IsPaused. if (g_iface.engine) { using BoolFn = bool (*)(void *); if (!vfunc(g_iface.engine, 26)(g_iface.engine)) return; if (vfunc(g_iface.engine, 84)(g_iface.engine)) return; } double now = esp_now(); if (now - g_speed_last_sample < 0.1) return; g_speed_last_sample = now; float speed = 0.0f; int local = engine_get_local_player(); if (local > 0 && g_net.velocity >= 0) { void *ent = entitylist_get(local); if (ent) { const float *v = (const float *)((uintptr_t)ent + g_net.velocity); speed = sqrtf(v[0] * v[0] + v[1] * v[1]); } } g_speed_samples[g_speed_head] = speed; g_speed_head = (g_speed_head + 1) % SPEED_SAMPLES; } static void draw_speed_graph() { if (!esp_option(ESP_OPT_SHOW_SPEEDGRAPH)) return; speed_graph_sample(); float maxv = 0.0f; for (int i = 0; i < SPEED_SAMPLES; i++) if (g_speed_samples[i] > maxv) maxv = g_speed_samples[i]; float scale = maxv > 1.0f ? maxv : 1.0f; const int w = 240, h = 64; int x = (g_scr_w - w) / 2; int y = (g_scr_h * 9) / 10 - h / 2; if (x < 0) x = 0; if (y < 0) y = 0; surf_color(0, 0, 0, 120); surf_rect(1, x, y, x + w, y + h); surf_color(80, 80, 80, 180); surf_rect(0, x, y, x + w, y + h); surf_color(0, 255, 200, 220); int px = 0, py = 0; for (int i = 0; i < SPEED_SAMPLES; i++) { int idx = (g_speed_head + i) % SPEED_SAMPLES; // oldest -> newest float v = g_speed_samples[idx]; int sx = x + 1 + (i * (w - 2)) / (SPEED_SAMPLES - 1); int sy = y + h - 2 - (int)((v / scale) * (float)(h - 4)); if (sy < y + 1) sy = y + 1; if (i > 0) surf_line(px, py, sx, sy); px = sx; py = sy; } char label[32]; snprintf(label, sizeof(label), "max %d", (int)maxv); draw_text_col(x + 3, y + 2, label, 200, 200, 200); int cur_idx = (g_speed_head + SPEED_SAMPLES - 1) % SPEED_SAMPLES; char cur[16]; snprintf(cur, sizeof(cur), "%d", (int)g_speed_samples[cur_idx]); int cur_w = (int)strlen(cur) * 8; draw_text_col(x + w - 4 - cur_w, y + 2, cur, 255, 255, 255); } // Draw the parent->child bone links. Bone snapshots are world-space matrices // from SetupBones; project each bone once and reuse for both endpoints. static void draw_skeleton(const EspTarget &t) { int n = bones_count(t.idx); if (n <= 0 || n > 256) return; Vector wp[256]; float xs[256], ys[256]; bool ok[256]; int core_count = 0; for (int i = 0; i < n; i++) { ok[i] = false; if (!bone_is_skeleton_core(t.idx, i)) continue; core_count++; ok[i] = bone_pos(t.idx, i, wp[i]) && world_to_screen(wp[i], xs[i], ys[i]) && std::isfinite(xs[i]) && std::isfinite(ys[i]); } static int logged = 0; if (core_count > 0 && logged < 4) { logged++; logd("skeleton: idx=%d bones=%d core=%d humanoid=%d", t.idx, n, core_count, (int)bones_is_humanoid(t.idx)); } int sr, sg, sb; relation_rgb(t, sr, sg, sb); surf_color(sr, sg, sb, 255); for (int i = 0; i < n; i++) { if (!ok[i]) continue; int p = bone_nearest_core_parent(t.idx, i); if (p >= 0 && p < n && ok[p]) surf_line((int)xs[p], (int)ys[p], (int)xs[i], (int)ys[i]); } } // --------------------------------------------------------------------------- // aimbot v1: nearest hostile NPC head to the crosshair. // Bounded smoothing: never snap, never overshoot. Each tick moves at most // MAX_STEP degrees toward the target and stops inside DEADZONE. // --------------------------------------------------------------------------- QAngle angle_to(const Vector &from, const Vector &to) { float dx = to.x - from.x, dy = to.y - from.y, dz = to.z - from.z; float horiz = sqrtf(dx * dx + dy * dy); if (horiz < 0.001f) horiz = 0.001f; QAngle a{}; a.x = -atan2f(dz, horiz) * 180.0f / PI_F; // Source: positive pitch = down a.y = atan2f(dy, dx) * 180.0f / PI_F; a.z = 0.0f; return a; } // Server bullet direction adds m_Local.m_vecPunchAngle to the usercmd view // (GetAutoaimVector), so aim angles have to be pre-subtracted by it. bool punch_angles(float &px, float &py) { px = py = 0.0f; if (!aim_punch_enabled() || g_net.punch < 0) return false; int local = engine_get_local_player(); if (local <= 0) return false; void *le = entitylist_get(local); if (!le) return false; const float *p = (const float *)((uintptr_t)le + g_net.punch); px = p[0]; py = p[1]; return std::isfinite(px) && std::isfinite(py); } // Dynamic props are tinted through their client render colour. StudioRender // feeds m_clrRender into the per-model colour modulation each draw, which is // why tinting the prop material alone had no effect. (The IStudioRender hook // experiment corrupted materials and crashed; see studio.cpp.) void prop_tint_update() { static bool applied = false; static bool logged_missing = false; static bool logged_touched = false; bool on = feature_active(FEAT_WORLD_TINT) && g_tint_prop_on; if (on) studio_props_init(); if (!on && !applied) return; if (!g_iface.entitylist || g_net.clr_render < 0) { if (on && !logged_missing) { logged_missing = true; logf("prop tint: m_clrRender unavailable"); } return; } static double last = 0.0; double now = esp_now(); if (on && now - last < 0.01) return; last = now; using Fn = int (*)(void *); int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); if (hi < 0 || hi > 4096) hi = 4096; uint32_t col = on ? ((uint32_t)g_tint_prop_rgb[0] | ((uint32_t)g_tint_prop_rgb[1] << 8) | ((uint32_t)g_tint_prop_rgb[2] << 16) | (255u << 24)) : 0xFFFFFFFFu; int touched = 0; for (int idx = 1; idx <= hi; idx++) { void *ent = entitylist_get(idx); if (!ent) continue; const char *model = entity_model_name(ent); if (!model) continue; bool prop = strncmp(model, "models/props", 12) == 0 || strncmp(model, "props_", 6) == 0; if (!prop) continue; *(uint32_t *)((uintptr_t)ent + (uintptr_t)g_net.clr_render) = col; touched++; } applied = on; if (on && touched > 0 && !logged_touched) { logged_touched = true; logf("prop tint: %d prop entities", touched); } } // Small left-edge label, stacked above the box (10 px font + 1 px shadow). static void draw_left_tag(int left, int y, const char *text, int r, int g, int b) { int tw = (int)strlen(text) * 6; int tx = left - tw - 3; if (tx < 2) tx = 2; draw_text_small_col(tx + 1, y + 1, text, 0, 0, 0); draw_text_small_col(tx, y, text, r, g, b); } static const char *alert_tag(int state, int &r, int &g, int &b) { switch (state) { case 1: r = 170; g = 180; b = 190; return "IDLE"; case 2: r = 255; g = 220; b = 80; return "ALERT"; case 3: r = 255; g = 90; b = 70; return "COMBAT"; case 4: r = 150; g = 200; b = 255; return "SCRIPT"; default: return nullptr; } } // Direction markers for hostiles outside the view. Drawn at the screen edge // using the engine projection when the target is in front, and the view-right // dot product when it is behind. static void draw_edge_arrows() { if (!g_surface || !g_scr_w || !g_narrows || !esp_option(ESP_OPT_ARROWS)) return; float cx = (float)g_scr_w * 0.5f, cy = (float)g_scr_h * 0.5f; const float margin = 26.0f; int drawn = 0; for (int i = 0; i < g_narrows && drawn < 12; i++) { const ArrowTarget &a = g_arrows[i]; float sx, sy; bool proj = world_to_screen(a.head, sx, sy); if (proj && sx >= 4.0f && sx <= (float)g_scr_w - 4.0f && sy >= 4.0f && sy <= (float)g_scr_h - 4.0f) continue; // already visible as a box float dx, dy; if (proj) { dx = sx - cx; dy = sy - cy; } else { float yaw = g_cam_view.y * PI_F / 180.0f; float wx = a.head.x - g_cam_eye.x; float wy = a.head.y - g_cam_eye.y; float right = wx * sinf(yaw) + wy * (-cosf(yaw)); dx = right >= 0.0f ? 1.0f : -1.0f; dy = 0.0f; } float len = sqrtf(dx * dx + dy * dy); if (len < 1e-3f) { dx = 0.0f; dy = 1.0f; len = 1.0f; } dx /= len; dy /= len; float half_w = cx - margin, half_h = cy - margin; float tx = fabsf(dx) > 1e-4f ? half_w / fabsf(dx) : 1e9f; float ty = fabsf(dy) > 1e-4f ? half_h / fabsf(dy) : 1e9f; float d = tx < ty ? tx : ty; float ex = cx + dx * d, ey = cy + dy * d; float px = -dy, py = dx; const int *col = a.neutral ? g_esp_col_neutral : g_esp_col_hostile; surf_color(col[0], col[1], col[2], 235); surf_line((int)ex, (int)ey, (int)(ex - dx * 13.0f + px * 6.0f), (int)(ey - dy * 13.0f + py * 6.0f)); surf_line((int)ex, (int)ey, (int)(ex - dx * 13.0f - px * 6.0f), (int)(ey - dy * 13.0f - py * 6.0f)); drawn++; } } // Map transition triggers are server-only brush entities. Draw their world // bounds and the target map; the previous map is remembered across level // changes from the worldspawn model name. struct TransitionBox { char next[128]; Vector mins, maxs; }; static TransitionBox g_trans[8]; static int g_ntrans = 0; static char g_map_last[64]; static char g_map_prev[64]; static void map_name_from_world(char *out, size_t outn) { out[0] = 0; void *w = entitylist_get(0); const char *m = w ? entity_model_name(w) : nullptr; if (!m) return; const char *b = strrchr(m, '/'); b = b ? b + 1 : m; size_t n = strcspn(b, "."); if (n >= outn) n = outn - 1; memcpy(out, b, n); out[n] = 0; } static void transitions_scan() { char cur[64]; map_name_from_world(cur, sizeof(cur)); if (strcmp(cur, g_map_last) != 0) { if (g_map_last[0]) snprintf(g_map_prev, sizeof(g_map_prev), "%s", g_map_last); snprintf(g_map_last, sizeof(g_map_last), "%s", cur); } int count = server_entity_count(); int n = 0; for (int i = 1; i <= count && n < 8; i++) { TransitionBox &b = g_trans[n]; b.mins = Vector{0, 0, 0}; b.maxs = Vector{0, 0, 0}; if (!server_trigger_info(i, b.next, sizeof(b.next), b.mins, b.maxs)) continue; if (b.mins.x == b.maxs.x && b.mins.y == b.maxs.y && b.mins.z == b.maxs.z) continue; n++; } g_ntrans = n; static int logged = 0; if (n > 0 && logged < 4) { logged++; logf("transitions: %d changelevel trigger(s), next=%s prev=%s", n, g_trans[0].next, g_map_prev[0] ? g_map_prev : "?"); } } // Clip a 2D segment to the screen (with a small margin). Liang-Barsky. static bool clip_line2d(float &x0, float &y0, float &x1, float &y1, float xmin, float ymin, float xmax, float ymax) { float dx = x1 - x0, dy = y1 - y0; float p[4] = {-dx, dx, -dy, dy}; float q[4] = {x0 - xmin, xmax - x0, y0 - ymin, ymax - y0}; float u1 = 0.0f, u2 = 1.0f; for (int i = 0; i < 4; i++) { if (fabsf(p[i]) < 1e-6f) { if (q[i] < 0.0f) return false; continue; } float r = q[i] / p[i]; if (p[i] < 0.0f) { if (r > u2) return false; if (r > u1) u1 = r; } else { if (r < u1) return false; if (r < u2) u2 = r; } } float nx0 = x0 + u1 * dx, ny0 = y0 + u1 * dy; float nx1 = x0 + u2 * dx, ny1 = y0 + u2 * dy; x0 = nx0; y0 = ny0; x1 = nx1; y1 = ny1; return true; } // Project a world-space segment, clipping it at the near plane first. Without // this an edge with one corner behind the camera was dropped entirely, so a // partially visible transition box or radius ring disappeared. bool project_segment(const Vector &a, const Vector &b, int &x0, int &y0, int &x1, int &y1) { if (!g_scr_w || !g_scr_h) return false; float pitch = g_cam_view.x * PI_F / 180.0f; float yaw = g_cam_view.y * PI_F / 180.0f; float cp = cosf(pitch), sp = sinf(pitch); float cy = cosf(yaw), sy = sinf(yaw); Vector fwd{cp * cy, cp * sy, -sp}; Vector da{a.x - g_cam_eye.x, a.y - g_cam_eye.y, a.z - g_cam_eye.z}; Vector db{b.x - g_cam_eye.x, b.y - g_cam_eye.y, b.z - g_cam_eye.z}; const float nearz = 5.0f; float za = da.x * fwd.x + da.y * fwd.y + da.z * fwd.z; float zb = db.x * fwd.x + db.y * fwd.y + db.z * fwd.z; if (za <= nearz && zb <= nearz) return false; Vector ca = a, cb = b; if (za <= nearz) { float t = (nearz - za) / (zb - za); ca = Vector{a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t}; } else if (zb <= nearz) { float t = (nearz - zb) / (za - zb); cb = Vector{b.x + (a.x - b.x) * t, b.y + (a.y - b.y) * t, b.z + (a.z - b.z) * t}; } float ax, ay, bx, by; if (!world_to_screen(ca, ax, ay) && !project_manual(ca, ax, ay)) return false; if (!world_to_screen(cb, bx, by) && !project_manual(cb, bx, by)) return false; if (!clip_line2d(ax, ay, bx, by, -8.0f, -8.0f, (float)g_scr_w + 8.0f, (float)g_scr_h + 8.0f)) return false; x0 = (int)ax; y0 = (int)ay; x1 = (int)bx; y1 = (int)by; return true; } static void draw_transitions() { if (!g_surface || !g_ntrans) return; // Corner bits: +x, +y, +z. Twelve edges connect corners differing by one // bit; projecting the corners and joining them gives a proper 3D wireframe // instead of a screen-space rectangle. static const int edges[12][2] = { {0, 1}, {2, 3}, {4, 5}, {6, 7}, {0, 2}, {1, 3}, {4, 6}, {5, 7}, {0, 4}, {1, 5}, {2, 6}, {3, 7}}; for (int i = 0; i < g_ntrans; i++) { const TransitionBox &b = g_trans[i]; Vector corners[8]; float xs[8], ys[8]; bool ok[8]; int seen = 0; for (int c = 0; c < 8; c++) { corners[c] = Vector{(c & 1) ? b.maxs.x : b.mins.x, (c & 2) ? b.maxs.y : b.mins.y, (c & 4) ? b.maxs.z : b.mins.z}; ok[c] = world_to_screen(corners[c], xs[c], ys[c]) && std::isfinite(xs[c]) && std::isfinite(ys[c]); if (ok[c]) seen++; } if (!seen) continue; // The trigger whose target is the map we came from is the way back. bool back = g_map_prev[0] && strcmp(b.next, g_map_prev) == 0; int r = back ? 200 : 255, g = back ? 200 : 170, bl = back ? 200 : 60; surf_color(r, g, bl, 210); for (int e = 0; e < 12; e++) { int a = edges[e][0], c = edges[e][1]; int lx, ly, hx, hy; if (project_segment(corners[a], corners[c], lx, ly, hx, hy)) surf_line(lx, ly, hx, hy); } // Tag above the highest projected corner. float topx = 0.0f, topy = 1e9f; for (int c = 0; c < 8; c++) { if (ok[c] && ys[c] < topy) { topy = ys[c]; topx = xs[c]; } } char line[128]; snprintf(line, sizeof(line), "%s %.100s", back ? "<-" : "->", b.next); int tx = (int)topx; if (tx < 4) tx = 4; if (tx > g_scr_w - 120) tx = g_scr_w - 120; int ty = (int)topy - 16; if (ty < 4) ty = 4; draw_text_small_col(tx + 1, ty + 1, line, 0, 0, 0); draw_text_small_col(tx, ty, line, r, g, bl); } } void esp_draw() { if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return; if (!esp_update_screen()) return; esp_collect(engine_get_local_player()); static double last_trans = 0.0; double trans_now = esp_now(); if (trans_now - last_trans >= 0.25) { last_trans = trans_now; transitions_scan(); } draw_transitions(); for (int i = 0; i < g_ntargets; i++) { const EspTarget &t = g_targets[i]; if (t.neutral && !esp_option(ESP_OPT_SHOW_NEUTRAL)) continue; if (esp_option(ESP_OPT_SHOW_SKELETON)) { void *ent = entitylist_get(t.idx); if (ent && bones_update(t.idx, ent)) draw_skeleton(t); } int cr, cg, cb; relation_rgb(t, cr, cg, cb); surf_color(cr, cg, cb, 255); float w = t.h * 0.45f; // ponytail: constant aspect, no OBB width int left = (int)(t.sx - w / 2); int right = (int)(t.sx + w / 2); int top = (int)(t.sy - t.h); int bot = (int)t.sy; if (esp_option(ESP_OPT_SHOW_BOX)) surf_rect(0, left, top, right, bot); bool draw_bar = t.show_health && esp_option(ESP_OPT_SHOW_HEALTHBAR); bool draw_num = t.show_health && esp_option(ESP_OPT_SHOW_HEALTHNUM); int num_x = right + 3; if (draw_bar) { float frac = t.health / (t.maxhealth > 1.0f ? t.maxhealth : 100.0f); if (frac > 1) frac = 1; if (frac < 0) frac = 0; // vertical bar just right of the box, HP number beside it int bx0 = right + 2, bx1 = right + 5; surf_color(0, 0, 0, 200); surf_rect(1, bx0, top, bx1, bot); int fill_top = bot - (int)((float)(bot - top) * frac); if (fill_top < top) fill_top = top; if (frac > 0.5f) surf_color(40, 255, 40, 255); else if (frac > 0.25f) surf_color(255, 255, 0, 255); else surf_color(255, 40, 40, 255); if (fill_top < bot) surf_rect(1, bx0, fill_top, bx1, bot); num_x = bx1 + 3; } if (draw_num) { char hptext[16]; snprintf(hptext, sizeof(hptext), "%d", (int)t.health); draw_text_col(num_x, top, hptext, 255, 255, 255); } if (esp_option(ESP_OPT_SHOW_NAME)) draw_text_col(left, top - 14, t.name, cr, cg, cb); int tag_y = top + 2; if (esp_option(ESP_OPT_DAMAGE_TAGS)) { const char *tag = nullptr; int tr = 255, tg = 255, tb = 255; if (t.damage == NPC_DMG_ARMORED) { tag = "ARM"; tr = 255; tg = 170; tb = 60; } else if (t.damage == NPC_DMG_VULNERABLE) { tag = "VULN"; tr = 90; tg = 220; tb = 255; } else if (t.damage == NPC_DMG_IMMUNE) { tag = "IMM"; tr = 180; tg = 180; tb = 180; } if (tag) { draw_left_tag(left, tag_y, tag, tr, tg, tb); tag_y += 12; } } if (esp_option(ESP_OPT_ALERT)) { int ar, ag, ab; const char *tag = alert_tag(t.state, ar, ag, ab); if (tag) { draw_left_tag(left, tag_y, tag, ar, ag, ab); tag_y += 12; } } if (esp_option(ESP_OPT_WEAPON) && t.weapon[0]) { draw_left_tag(left, tag_y, t.weapon, 220, 220, 220); tag_y += 12; } } draw_edge_arrows(); // Groundwork probe: HL2H_BONE_DEBUG=1 draws a white marker at each NPC's // "head" hitbox center (from SetupBones + studio hitboxes). static bool bone_debug = getenv("HL2H_BONE_DEBUG") != nullptr; if (bone_debug) { static bool head_logged = false; for (int i = 0; i < g_ntargets; i++) { const EspTarget &t = g_targets[i]; void *ent = entitylist_get(t.idx); if (!ent || !bones_update(t.idx, ent)) continue; Vector head; if (!head_position(t.idx, head)) { static int no_head = 0; if (no_head < 4) { no_head++; logd("head: idx=%d no head hitbox/bone", t.idx); } continue; } float hx, hy; if (!world_to_screen(head, hx, hy)) continue; surf_color(255, 255, 255, 255); surf_rect(1, (int)hx - 2, (int)hy - 2, (int)hx + 3, (int)hy + 3); if (!head_logged) { head_logged = true; logd("bone debug: idx=%d bones=%d head=(%.1f %.1f %.1f)", t.idx, bones_count(t.idx), head.x, head.y, head.z); } } } draw_speed_graph(); } // --------------------------------------------------------------------------- // Valuable item tags // ---------------------------------------------------------------------------