From 293eda2bdce9d732fad168b5b6838bd89ce59fe8 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 27 Oct 2026 14:52:00 +0100 Subject: Melee killbot: 360-degree crowbar silent aim - New feature (default on): while the local player's active weapon is the crowbar, pick the nearest visible hostile NPC within the 75-unit crowbar range, sorted by distance to its collision AABB, and attack it through the normal silent-aim path (usercmd view + IN_ATTACK, movement preserved, rendered view restored at FRAME_RENDER_START). - Prefer the nearest visible hitbox center; fall back to the AABB center / closest point when bones are unavailable. Reuses the existing friendly / neutral and life-state filters and the world-only vischeck. - Detect the active weapon from the client m_hActiveWeapon handle, with the authoritative server m_hActiveWeapon handle as a fallback (client entity index from the low 16 bits). - While the crowbar is active and this feature is on, skip the gun aimbot, triggerbot and silent aim so they do not fight over the usercmd view. - Menu: Melee killbot -> Active; config [melee] active=... --- src/esp.cpp | 258 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/hl2h.cpp | 42 ++++----- src/hl2h.h | 7 +- src/menu.cpp | 25 ++++-- src/netvars.cpp | 7 +- src/server.cpp | 22 ++++- 6 files changed, 328 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/src/esp.cpp b/src/esp.cpp index 1e00a92..47df57d 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -784,6 +784,264 @@ bool silent_aim_apply(CUserCmd *cmd) { return true; } +// --------------------------------------------------------------------------- +// melee killbot +// --------------------------------------------------------------------------- +// +// The crowbar's CBaseHLBludgeonWeapon::Swing traces 75 units from +// Weapon_ShootPosition along the view forward (CROWBAR_RANGE in +// weapon_crowbar.h). It first tries a line, then a +/-16 hull backed off by +// ~28 units, and ignores targets more than ~45 degrees off the aim direction. +// We therefore only need to point the usercmd at the target for one command; +// the rendered view is restored exactly like silent aim. + +static constexpr float MELEE_RANGE = 75.0f; + +static float vec_dist(const Vector &a, const Vector &b) { + float dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z; + return sqrtf(dx * dx + dy * dy + dz * dz); +} + +bool melee_weapon_active(void *local_ent) { + if (!local_ent) + return false; + int local = engine_get_local_player(); + + auto check = [&](int idx, const char **cls_out, + const char **model_out) -> bool { + if (idx <= 0 || idx > 4096) + return false; + void *wep = entitylist_get(idx); + void *net = entity_networkable(idx); + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + const char *model = wep ? entity_model_name(wep) : nullptr; + *cls_out = cls; + *model_out = model; + return (cls && strstr(cls, "Crowbar")) || + (model && strstr(model, "crowbar")); + }; + + int idx = -1; + if (g_net.activeweapon >= 0) { + uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); + if (h != 0xFFFFFFFFu) + idx = (int)(h & 0xFFFF); + } + const char *cls = nullptr, *model = nullptr; + bool crowbar = check(idx, &cls, &model); + + if (!crowbar) { + // Fallback: the authoritative server player's m_hActiveWeapon handle. + // Also covers a client netvar offset that resolves but does not point at + // the weapon the server is actually using. + if (local > 0) { + resolve_server_netvars(); + int si = server_active_weapon(local); + if (si > 0 && si != idx) { + idx = si; + crowbar = check(idx, &cls, &model); + } + } + } + + static int last_idx = -2; + static bool last_crowbar = false; + if (idx != last_idx || crowbar != last_crowbar) { + last_idx = idx; + last_crowbar = crowbar; + logf("melee: active idx=%d cls=%s model=%s crowbar=%d", idx, + cls ? cls : "?", model ? model : "?", (int)crowbar); + } + return crowbar; +} + +bool melee_aimbot_apply(CUserCmd *cmd) { + if (!g_iface.entitylist || g_net.origin < 0) + return false; + int local = engine_get_local_player(); + if (local <= 0) + return false; + void *local_ent = entitylist_get(local); + if (!local_ent) + return false; + if (g_net.health >= 0 && + *(int *)((uintptr_t)local_ent + g_net.health) <= 0) + return false; + + using Fn = int (*)(void *); + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); + if (hi < 0 || hi > 4096) + hi = 4096; + + struct Cand { + int idx; + float dist; + Vector closest; + Vector center; + }; + Cand cands[16]; + int n = 0; + + for (int idx = 1; idx <= hi; idx++) { + if (idx == local) + continue; + void *ent = entitylist_get(idx); + if (!ent) + continue; + void *net = entity_networkable(idx); + if (!net) + continue; + const char *cls = entity_class_name(net, nullptr); + if (!cls || !is_npc_class(cls)) + continue; + uintptr_t e = (uintptr_t)ent; + int life = g_net.lifestate >= 0 + ? (int)*(unsigned char *)(e + g_net.lifestate) + : -1; + if (life != LIFE_ALIVE) + continue; + const char *model = entity_model_name(ent); + if (model_is_friendly(model) || model_is_neutral(model)) + continue; + + Vector origin{*(float *)(e + g_net.origin), + *(float *)(e + g_net.origin + 4), + *(float *)(e + g_net.origin + 8)}; + float minx = -16.0f, miny = -16.0f, minz = 0.0f; + float maxx = 16.0f, maxy = 16.0f, maxz = 72.0f; + if (g_net.mins >= 0) { + minx = *(float *)(e + g_net.mins); + miny = *(float *)(e + g_net.mins + 4); + minz = *(float *)(e + g_net.mins + 8); + } + if (g_net.maxs >= 0) { + maxx = *(float *)(e + g_net.maxs); + maxy = *(float *)(e + g_net.maxs + 4); + maxz = *(float *)(e + g_net.maxs + 8); + } + if (!(maxz > minz + 4.0f && maxz < 512.0f)) { + minz = 0.0f; + maxz = 72.0f; + } + if (!(maxx > minx + 1.0f && maxx < 256.0f)) { + minx = -16.0f; + maxx = 16.0f; + } + if (!(maxy > miny + 1.0f && maxy < 256.0f)) { + miny = -16.0f; + maxy = 16.0f; + } + + Vector lo{origin.x + minx, origin.y + miny, origin.z + minz}; + Vector hiV{origin.x + maxx, origin.y + maxy, origin.z + maxz}; + Vector closest{g_cam_eye.x, g_cam_eye.y, g_cam_eye.z}; + if (closest.x < lo.x) closest.x = lo.x; + if (closest.x > hiV.x) closest.x = hiV.x; + if (closest.y < lo.y) closest.y = lo.y; + if (closest.y > hiV.y) closest.y = hiV.y; + if (closest.z < lo.z) closest.z = lo.z; + if (closest.z > hiV.z) closest.z = hiV.z; + float d = vec_dist(g_cam_eye, closest); + if (d > MELEE_RANGE) + continue; + + Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f, + (lo.z + hiV.z) * 0.5f}; + + int pos = n; + while (pos > 0 && cands[pos - 1].dist > d) + pos--; + if (n < 16) + n++; + else if (pos >= 16) + continue; + for (int j = n - 1; j > pos; j--) + cands[j] = cands[j - 1]; + cands[pos] = {idx, d, closest, center}; + } + + for (int ci = 0; ci < n; ci++) { + const Cand &c = cands[ci]; + Vector aim{}; + bool have_aim = false; + + // Prefer the nearest visible hitbox center. This handles small NPCs + // (headcrabs, antlions) whose AABB center can be less useful than the + // actual body hitboxes. + void *tent = entitylist_get(c.idx); + if (tent && bones_update(c.idx, tent)) { + Vector pts[32]; + int np = aim_points(c.idx, 0xFE, pts, 32); + float bestd = 1e9f; + for (int i = 0; i < np; i++) { + float pd = vec_dist(g_cam_eye, pts[i]); + if (pd > MELEE_RANGE || pd >= bestd) + continue; + if (!is_visible(g_cam_eye, pts[i])) + continue; + bestd = pd; + aim = pts[i]; + have_aim = true; + } + } + + if (!have_aim) { + aim = c.center; + float ad = vec_dist(g_cam_eye, aim); + if (ad > MELEE_RANGE) { + // The body is in range but its center is not: aim just inside the + // closest point on the AABB. + aim = c.closest; + aim.x += (c.center.x - c.closest.x) * 0.25f; + aim.y += (c.center.y - c.closest.y) * 0.25f; + aim.z += (c.center.z - c.closest.z) * 0.25f; + ad = vec_dist(g_cam_eye, aim); + if (ad > MELEE_RANGE) + aim = c.closest; + } + if (!is_visible(g_cam_eye, aim)) + continue; + } + + QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z}; + QAngle desired = angle_to(g_cam_eye, aim); + float np = desired.x; + if (np > 89.0f) np = 89.0f; + if (np < -89.0f) np = -89.0f; + float ny = desired.y; + while (ny > 180.0f) ny -= 360.0f; + while (ny < -180.0f) ny += 360.0f; + + preserve_movement(cmd, orig.y, ny); + cmd->viewangles[0] = np; + cmd->viewangles[1] = ny; + cmd->viewangles[2] = orig.z; + set_silent_restore(orig); + if (g_iface.engine) { + QAngle restore{orig.x, orig.y, orig.z}; + using SetViewFn = void (*)(void *, QAngle *); + vfunc(g_iface.engine, 20)(g_iface.engine, &restore); + } + cmd->buttons |= IN_ATTACK; + + static int logs = 0; + if (logs < 10) { + logs++; + logf("melee: swing idx=%d dist=%.1f aim=(%.1f %.1f %.1f)", c.idx, + c.dist, aim.x, aim.y, aim.z); + } + return true; + } + + static double last_none = 0.0; + double now = esp_now(); + if (now - last_none >= 2.0) { + last_none = now; + logf("melee: no visible target within %.0f", MELEE_RANGE); + } + return false; +} + bool triggerbot_apply(CUserCmd *cmd) { if (!g_iface.enginetrace || !g_iface.entitylist) return false; diff --git a/src/hl2h.cpp b/src/hl2h.cpp index 4ba30bc..ee2e990 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -500,34 +500,36 @@ static void do_move(int seq, float frametime) { if (jumpbug_apply(cmd, localIdx, ent, flags, vel, frametime)) note_modified(cmd, flags, vel); - if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT)) + bool melee = feature_active(FEAT_MELEE) && melee_weapon_active(ent); + + if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT) || melee) aimbot_reset_lock(); - if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM)) + if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM) || + melee) silentaim_reset_lock(); - if (feature_active(FEAT_AIMBOT)) { - static bool aim_feature_logged = false; - if (!aim_feature_logged) { - aim_feature_logged = true; - logf("aimbot: feature active (bind held)"); - } - if (aimbot_apply(cmd)) { - const float *vel = (const float *)(e + g_net.velocity); + if (melee) { + if (melee_aimbot_apply(cmd)) note_modified(cmd, flags, vel); + } else { + if (feature_active(FEAT_AIMBOT)) { + static bool aim_feature_logged = false; + if (!aim_feature_logged) { + aim_feature_logged = true; + logf("aimbot: feature active (bind held)"); + } + if (aimbot_apply(cmd)) + note_modified(cmd, flags, vel); } - } - if (feature_active(FEAT_TRIGGERBOT)) { - if (triggerbot_apply(cmd)) { - const float *vel = (const float *)(e + g_net.velocity); - note_modified(cmd, flags, vel); + if (feature_active(FEAT_TRIGGERBOT)) { + if (triggerbot_apply(cmd)) + note_modified(cmd, flags, vel); } - } - if (feature_active(FEAT_SILENTAIM)) { - if (silent_aim_apply(cmd)) { - const float *vel = (const float *)(e + g_net.velocity); - note_modified(cmd, flags, vel); + if (feature_active(FEAT_SILENTAIM)) { + if (silent_aim_apply(cmd)) + note_modified(cmd, flags, vel); } } } diff --git a/src/hl2h.h b/src/hl2h.h index 08eb2ef..2680591 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -82,6 +82,7 @@ struct NetVars { int lifestate = -1; int modelindex = -1; int absvelocity = -1; // client C_BaseEntity::m_vecAbsVelocity (prediction) + int activeweapon = -1; // client m_hActiveWeapon handle // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100. @@ -147,6 +148,8 @@ float getfov(const QAngle &my_view, const QAngle &aim_ang); bool aimbot_apply(CUserCmd *cmd); bool triggerbot_apply(CUserCmd *cmd); bool silent_aim_apply(CUserCmd *cmd); +bool melee_weapon_active(void *local_ent); +bool melee_aimbot_apply(CUserCmd *cmd); enum FeatureId { FEAT_BHOP = 0, FEAT_ABH = 1, @@ -154,7 +157,8 @@ enum FeatureId { FEAT_AIMBOT = 3, FEAT_TRIGGERBOT = 4, FEAT_SILENTAIM = 5, - FEAT_JUMPBUG = 6 + FEAT_JUMPBUG = 6, + FEAT_MELEE = 7 }; enum EspOption { ESP_OPT_SHOW_NEUTRAL = 0, @@ -179,6 +183,7 @@ void resolve_netvars(); bool resolve_server_netvars(); int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); bool server_set_velocity_z(int idx, float vz); +int server_active_weapon(int idx); int server_model_index(void *base); void find_modules(); void init_conmsg(); diff --git a/src/menu.cpp b/src/menu.cpp index 8df67fe..f590d69 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -64,6 +64,7 @@ static bool g_bhop_autostrafe = true; static FeatureState g_abh{true, 0}; static FeatureState g_jumpbug{true, 0}; static bool g_jumpbug_assist = false; +static FeatureState g_melee{true, 0}; static FeatureState g_esp{true, 0}; static FeatureState g_aimbot{true, 604}; // default Mouse5, rebindable in menu static FeatureState g_triggerbot{true, 603}; // default Mouse4 @@ -101,6 +102,8 @@ static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active, static MenuNode g_jumpbug_feat{NODE_FEATURE, "Jumpbug", false, 1, {}, nullptr, nullptr, nullptr}; static MenuNode g_jumpbug_active{NODE_TOGGLE, "Active", false, 0, {}, &g_jumpbug.active, nullptr, nullptr}; static MenuNode g_jumpbug_assist_node{NODE_TOGGLE, "Assist timing", false, 0, {}, &g_jumpbug_assist, nullptr, nullptr}; +static MenuNode g_melee_feat{NODE_FEATURE, "Melee killbot", false, 1, {}, nullptr, nullptr, nullptr}; +static MenuNode g_melee_active{NODE_TOGGLE, "Active", false, 0, {}, &g_melee.active, nullptr, nullptr}; static MenuNode g_esp_feat{NODE_FEATURE, "ESP", false, 1, {}, nullptr, nullptr, nullptr}; static MenuNode g_esp_active{NODE_TOGGLE, "Active", false, 0, {}, &g_esp.active, nullptr, nullptr}; static MenuNode g_esp_opt_nodes[ESP_OPT_COUNT] = { @@ -156,6 +159,8 @@ static void build_tree() { g_jumpbug_feat.children[0] = &g_jumpbug_active; g_jumpbug_feat.children[1] = &g_jumpbug_assist_node; g_jumpbug_feat.child_count = 2; + g_melee_feat.children[0] = &g_melee_active; + g_melee_feat.child_count = 1; g_esp_feat.children[0] = &g_esp_active; g_aimbot_feat.children[0] = &g_aimbot_active; g_aimbot_feat.children[1] = &g_aimbot_bind; @@ -181,11 +186,12 @@ static void build_tree() { g_root.children[0] = &g_bhop_feat; g_root.children[1] = &g_abh_feat; g_root.children[2] = &g_jumpbug_feat; - g_root.children[3] = &g_esp_feat; - g_root.children[4] = &g_aimbot_feat; - g_root.children[5] = &g_triggerbot_feat; - g_root.children[6] = &g_silentaim_feat; - g_root.child_count = 7; + g_root.children[3] = &g_melee_feat; + g_root.children[4] = &g_esp_feat; + g_root.children[5] = &g_aimbot_feat; + g_root.children[6] = &g_triggerbot_feat; + g_root.children[7] = &g_silentaim_feat; + g_root.child_count = 8; } static int g_depth[48]; // depth of each visible row (for indentation) @@ -309,6 +315,7 @@ static void config_save() { fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind); fprintf(f, "[jumpbug]\nactive=%d\nassist=%d\n", g_jumpbug.active, (int)g_jumpbug_assist); + fprintf(f, "[melee]\nactive=%d\n", g_melee.active); fprintf(f, "[aimbot]\nactive=%d\nbind=%d\nsmooth=%d\nvischeck=%d\n" "fov=%d\nfov_circle=%d\n" "bone_head=%d\nbone_torso=%d\nbone_larm=%d\nbone_rarm=%d\n" @@ -349,6 +356,8 @@ static void config_load() { cur = &g_abh; else if (strstr(line, "[jumpbug]")) cur = &g_jumpbug; + else if (strstr(line, "[melee]")) + cur = &g_melee; else if (strstr(line, "[esp]")) cur = &g_esp; else if (strstr(line, "[aimbot]")) @@ -537,10 +546,10 @@ static bool key_pressed(int sc) { // feature gate queried from hl2h.cpp do_move(): feature must be Active AND // its bind key held (always-on when unbound). static FeatureState *feature_state(int feature) { - static FeatureState *feats[7] = {&g_bhop, &g_abh, &g_esp, + static FeatureState *feats[8] = {&g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot, &g_silentaim, - &g_jumpbug}; - if (feature < 0 || feature > 6) + &g_jumpbug, &g_melee}; + if (feature < 0 || feature > 7) return nullptr; return feats[feature]; } diff --git a/src/netvars.cpp b/src/netvars.cpp index 98ebe31..3b2d3b8 100644 --- a/src/netvars.cpp +++ b/src/netvars.cpp @@ -89,6 +89,9 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { g_net.health = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_iMaxHealth") == 0 && g_net.maxhealth < 0) { g_net.maxhealth = (int)(base_off + (uint32_t)poff); + } else if (strcmp(pname2, "m_hActiveWeapon") == 0 && + g_net.activeweapon < 0) { + g_net.activeweapon = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) { g_net.teamnum = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) { @@ -269,9 +272,9 @@ void resolve_netvars() { logf("m_vecAbsVelocity unresolved; jumpbug assist disabled"); logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d " - "modelindex=%d absvelocity=%d", + "modelindex=%d absvelocity=%d activeweapon=%d", g_net.flags, g_net.velocity, g_net.movetype, g_net.health, g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex, - g_net.absvelocity); + g_net.absvelocity, g_net.activeweapon); } diff --git a/src/server.cpp b/src/server.cpp index 3766906..896ce3e 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -33,6 +33,7 @@ int g_srv_lifestate = -1; int g_srv_modelindex = -1; int g_srv_absvelocity = -1; int g_srv_velocity = -1; +int g_srv_activeweapon = -1; static bool g_srv_ready = false; static bool g_srv_checked = false; @@ -109,12 +110,14 @@ bool resolve_server_netvars() { g_srv_modelindex = datamap_offset("m_nModelIndex", 7); // FIELD_SHORT g_srv_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR + g_srv_activeweapon = datamap_offset("m_hActiveWeapon", 13); // FIELD_EHANDLE g_srv_ready = g_srv_health > 0; if (g_srv_ready) { logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " - "modelindex=%d absvel=%d vel=%d", + "modelindex=%d absvel=%d vel=%d activeweapon=%d", g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, - g_srv_modelindex, g_srv_absvelocity, g_srv_velocity); + g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, + g_srv_activeweapon); fail_logged = false; } else if (!fail_logged) { fail_logged = true; @@ -194,3 +197,18 @@ bool server_set_velocity_z(int idx, float vz) { *(float *)((uintptr_t)base + g_srv_absvelocity + 8) = vz; return true; } + +// Entity index of the player's active weapon from the authoritative server +// entity. Used as a fallback if the client m_hActiveWeapon netvar is missing. +int server_active_weapon(int idx) { + if (!g_srv_ready || !g_iface.engineserver || g_srv_activeweapon <= 0) + return -1; + void *base = nullptr; + if (server_entity_health(idx, nullptr, nullptr, &base) < 0 || !base) + return -1; + uint32_t h = *(uint32_t *)((uintptr_t)base + g_srv_activeweapon); + if (h == 0xFFFFFFFFu) + return -1; + int wi = (int)(h & 0xFFFF); + return (wi > 0 && wi <= 4096) ? wi : -1; +} -- cgit v1.2.3