summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-27 14:52:00 +0100
committersalmonik <salmonik@tuta.io>2026-10-27 14:52:24 +0100
commit293eda2bdce9d732fad168b5b6838bd89ce59fe8 (patch)
tree9f345d4a19d1957ccfd7764012221d62eb2df328
parentMenu: latch the Insert key and expand the root on open (diff)
downloadhl2h-293eda2bdce9d732fad168b5b6838bd89ce59fe8.tar.gz
hl2h-293eda2bdce9d732fad168b5b6838bd89ce59fe8.zip
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=...
-rw-r--r--CONTEXT.md25
-rw-r--r--src/esp.cpp258
-rw-r--r--src/hl2h.cpp42
-rw-r--r--src/hl2h.h7
-rw-r--r--src/menu.cpp25
-rw-r--r--src/netvars.cpp7
-rw-r--r--src/server.cpp22
7 files changed, 351 insertions, 35 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 38d9373..8fb2197 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -81,6 +81,7 @@ Relevant vtable slots (this build):
81| `m_lifeState` | `0x7F` | 81| `m_lifeState` | `0x7F` |
82| `m_nModelIndex` | `0x7C` | 82| `m_nModelIndex` | `0x7C` |
83| `m_vecAbsVelocity` (client local, not netvar) | `0x11C` | 83| `m_vecAbsVelocity` (client local, not netvar) | `0x11C` |
84| `m_hActiveWeapon` (client netvar; server fallback `0x824`) | resolved by name |
84 85
85- Eye position = `m_vecOrigin + m_vecViewOffset` on **all three axes**. 86- Eye position = `m_vecOrigin + m_vecViewOffset` on **all three axes**.
86 (The old code added the X component to Z, which left the eye ~64 units low.) 87 (The old code added the X component to Z, which left the eye ~64 units low.)
@@ -108,6 +109,8 @@ HL2hake
108 Jumpbug 109 Jumpbug
109 Active 110 Active
110 Assist timing 111 Assist timing
112 Melee killbot
113 Active
111 ESP 114 ESP
112 Active 115 Active
113 Show neutral / healthbar / healthnum / name / skeleton / box / speed graph 116 Show neutral / healthbar / healthnum / name / skeleton / box / speed graph
@@ -163,6 +166,26 @@ Binds use SDL scancodes; mouse buttons are pseudo-scancodes 600..604.
163 zero forward/side movement; HL2's jump speed-bound subtraction then 166 zero forward/side movement; HL2's jump speed-bound subtraction then
164 accelerates along the velocity. Camera restored at `FRAME_RENDER_START`. 167 accelerates along the velocity. Camera restored at `FRAME_RENDER_START`.
165 168
169## Melee killbot
170
171- Active only while the local player's active weapon is the crowbar. The
172 client `m_hActiveWeapon` handle is used first; if it is missing, the
173 authoritative server `m_hActiveWeapon` handle (0x824) is used to get the
174 weapon entity index.
175- Crowbar range is `CROWBAR_RANGE` 75 units from `Weapon_ShootPosition`. The
176 selector computes the closest point on each hostile NPC's AABB to the eye,
177 rejects anything farther than 75, sorts by that distance, and picks the
178 nearest visible one.
179- The usercmd view is pointed at the target for one command with `IN_ATTACK`;
180 movement keys are rotated with `preserve_movement`, and the rendered view is
181 restored immediately and at `FRAME_RENDER_START` (same silent-aim path).
182 This gives a 360-degree silent melee attack.
183- While the crowbar is active and the killbot feature is on, the gun aimbot,
184 triggerbot and silent aim are skipped to avoid fighting over the usercmd
185 view.
186- Vischeck uses the shared `MASK_VISIBLE`/world-only path (static props are
187 ignored).
188
166## Jumpbug 189## Jumpbug
167 190
168- Automatic while falling when the predicted landing speed 191- Automatic while falling when the predicted landing speed
@@ -212,8 +235,6 @@ Binds use SDL scancodes; mouse buttons are pseudo-scancodes 600..604.
212 235
213## Roadmap / TODO 236## Roadmap / TODO
214 237
215- **Melee killbot**: 360° silent aim for hostile NPCs inside crowbar/melee
216 range, sorted by distance, only while the crowbar is held.
217- **GUI**: replace/augment the VGUI ISurface menu with an immediate-mode GUI 238- **GUI**: replace/augment the VGUI ISurface menu with an immediate-mode GUI
218 (Dear ImGui or a custom implementation); needs a design grill first. 239 (Dear ImGui or a custom implementation); needs a design grill first.
219- **Distance weapon spread thesis**: TBD — user will explain. Likely models 240- **Distance weapon spread thesis**: TBD — user will explain. Likely models
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) {
784 return true; 784 return true;
785} 785}
786 786
787// ---------------------------------------------------------------------------
788// melee killbot
789// ---------------------------------------------------------------------------
790//
791// The crowbar's CBaseHLBludgeonWeapon::Swing traces 75 units from
792// Weapon_ShootPosition along the view forward (CROWBAR_RANGE in
793// weapon_crowbar.h). It first tries a line, then a +/-16 hull backed off by
794// ~28 units, and ignores targets more than ~45 degrees off the aim direction.
795// We therefore only need to point the usercmd at the target for one command;
796// the rendered view is restored exactly like silent aim.
797
798static constexpr float MELEE_RANGE = 75.0f;
799
800static float vec_dist(const Vector &a, const Vector &b) {
801 float dx = a.x - b.x, dy = a.y - b.y, dz = a.z - b.z;
802 return sqrtf(dx * dx + dy * dy + dz * dz);
803}
804
805bool melee_weapon_active(void *local_ent) {
806 if (!local_ent)
807 return false;
808 int local = engine_get_local_player();
809
810 auto check = [&](int idx, const char **cls_out,
811 const char **model_out) -> bool {
812 if (idx <= 0 || idx > 4096)
813 return false;
814 void *wep = entitylist_get(idx);
815 void *net = entity_networkable(idx);
816 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
817 const char *model = wep ? entity_model_name(wep) : nullptr;
818 *cls_out = cls;
819 *model_out = model;
820 return (cls && strstr(cls, "Crowbar")) ||
821 (model && strstr(model, "crowbar"));
822 };
823
824 int idx = -1;
825 if (g_net.activeweapon >= 0) {
826 uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon);
827 if (h != 0xFFFFFFFFu)
828 idx = (int)(h & 0xFFFF);
829 }
830 const char *cls = nullptr, *model = nullptr;
831 bool crowbar = check(idx, &cls, &model);
832
833 if (!crowbar) {
834 // Fallback: the authoritative server player's m_hActiveWeapon handle.
835 // Also covers a client netvar offset that resolves but does not point at
836 // the weapon the server is actually using.
837 if (local > 0) {
838 resolve_server_netvars();
839 int si = server_active_weapon(local);
840 if (si > 0 && si != idx) {
841 idx = si;
842 crowbar = check(idx, &cls, &model);
843 }
844 }
845 }
846
847 static int last_idx = -2;
848 static bool last_crowbar = false;
849 if (idx != last_idx || crowbar != last_crowbar) {
850 last_idx = idx;
851 last_crowbar = crowbar;
852 logf("melee: active idx=%d cls=%s model=%s crowbar=%d", idx,
853 cls ? cls : "?", model ? model : "?", (int)crowbar);
854 }
855 return crowbar;
856}
857
858bool melee_aimbot_apply(CUserCmd *cmd) {
859 if (!g_iface.entitylist || g_net.origin < 0)
860 return false;
861 int local = engine_get_local_player();
862 if (local <= 0)
863 return false;
864 void *local_ent = entitylist_get(local);
865 if (!local_ent)
866 return false;
867 if (g_net.health >= 0 &&
868 *(int *)((uintptr_t)local_ent + g_net.health) <= 0)
869 return false;
870
871 using Fn = int (*)(void *);
872 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
873 if (hi < 0 || hi > 4096)
874 hi = 4096;
875
876 struct Cand {
877 int idx;
878 float dist;
879 Vector closest;
880 Vector center;
881 };
882 Cand cands[16];
883 int n = 0;
884
885 for (int idx = 1; idx <= hi; idx++) {
886 if (idx == local)
887 continue;
888 void *ent = entitylist_get(idx);
889 if (!ent)
890 continue;
891 void *net = entity_networkable(idx);
892 if (!net)
893 continue;
894 const char *cls = entity_class_name(net, nullptr);
895 if (!cls || !is_npc_class(cls))
896 continue;
897 uintptr_t e = (uintptr_t)ent;
898 int life = g_net.lifestate >= 0
899 ? (int)*(unsigned char *)(e + g_net.lifestate)
900 : -1;
901 if (life != LIFE_ALIVE)
902 continue;
903 const char *model = entity_model_name(ent);
904 if (model_is_friendly(model) || model_is_neutral(model))
905 continue;
906
907 Vector origin{*(float *)(e + g_net.origin),
908 *(float *)(e + g_net.origin + 4),
909 *(float *)(e + g_net.origin + 8)};
910 float minx = -16.0f, miny = -16.0f, minz = 0.0f;
911 float maxx = 16.0f, maxy = 16.0f, maxz = 72.0f;
912 if (g_net.mins >= 0) {
913 minx = *(float *)(e + g_net.mins);
914 miny = *(float *)(e + g_net.mins + 4);
915 minz = *(float *)(e + g_net.mins + 8);
916 }
917 if (g_net.maxs >= 0) {
918 maxx = *(float *)(e + g_net.maxs);
919 maxy = *(float *)(e + g_net.maxs + 4);
920 maxz = *(float *)(e + g_net.maxs + 8);
921 }
922 if (!(maxz > minz + 4.0f && maxz < 512.0f)) {
923 minz = 0.0f;
924 maxz = 72.0f;
925 }
926 if (!(maxx > minx + 1.0f && maxx < 256.0f)) {
927 minx = -16.0f;
928 maxx = 16.0f;
929 }
930 if (!(maxy > miny + 1.0f && maxy < 256.0f)) {
931 miny = -16.0f;
932 maxy = 16.0f;
933 }
934
935 Vector lo{origin.x + minx, origin.y + miny, origin.z + minz};
936 Vector hiV{origin.x + maxx, origin.y + maxy, origin.z + maxz};
937 Vector closest{g_cam_eye.x, g_cam_eye.y, g_cam_eye.z};
938 if (closest.x < lo.x) closest.x = lo.x;
939 if (closest.x > hiV.x) closest.x = hiV.x;
940 if (closest.y < lo.y) closest.y = lo.y;
941 if (closest.y > hiV.y) closest.y = hiV.y;
942 if (closest.z < lo.z) closest.z = lo.z;
943 if (closest.z > hiV.z) closest.z = hiV.z;
944 float d = vec_dist(g_cam_eye, closest);
945 if (d > MELEE_RANGE)
946 continue;
947
948 Vector center{(lo.x + hiV.x) * 0.5f, (lo.y + hiV.y) * 0.5f,
949 (lo.z + hiV.z) * 0.5f};
950
951 int pos = n;
952 while (pos > 0 && cands[pos - 1].dist > d)
953 pos--;
954 if (n < 16)
955 n++;
956 else if (pos >= 16)
957 continue;
958 for (int j = n - 1; j > pos; j--)
959 cands[j] = cands[j - 1];
960 cands[pos] = {idx, d, closest, center};
961 }
962
963 for (int ci = 0; ci < n; ci++) {
964 const Cand &c = cands[ci];
965 Vector aim{};
966 bool have_aim = false;
967
968 // Prefer the nearest visible hitbox center. This handles small NPCs
969 // (headcrabs, antlions) whose AABB center can be less useful than the
970 // actual body hitboxes.
971 void *tent = entitylist_get(c.idx);
972 if (tent && bones_update(c.idx, tent)) {
973 Vector pts[32];
974 int np = aim_points(c.idx, 0xFE, pts, 32);
975 float bestd = 1e9f;
976 for (int i = 0; i < np; i++) {
977 float pd = vec_dist(g_cam_eye, pts[i]);
978 if (pd > MELEE_RANGE || pd >= bestd)
979 continue;
980 if (!is_visible(g_cam_eye, pts[i]))
981 continue;
982 bestd = pd;
983 aim = pts[i];
984 have_aim = true;
985 }
986 }
987
988 if (!have_aim) {
989 aim = c.center;
990 float ad = vec_dist(g_cam_eye, aim);
991 if (ad > MELEE_RANGE) {
992 // The body is in range but its center is not: aim just inside the
993 // closest point on the AABB.
994 aim = c.closest;
995 aim.x += (c.center.x - c.closest.x) * 0.25f;
996 aim.y += (c.center.y - c.closest.y) * 0.25f;
997 aim.z += (c.center.z - c.closest.z) * 0.25f;
998 ad = vec_dist(g_cam_eye, aim);
999 if (ad > MELEE_RANGE)
1000 aim = c.closest;
1001 }
1002 if (!is_visible(g_cam_eye, aim))
1003 continue;
1004 }
1005
1006 QAngle orig{g_cam_view.x, g_cam_view.y, g_cam_view.z};
1007 QAngle desired = angle_to(g_cam_eye, aim);
1008 float np = desired.x;
1009 if (np > 89.0f) np = 89.0f;
1010 if (np < -89.0f) np = -89.0f;
1011 float ny = desired.y;
1012 while (ny > 180.0f) ny -= 360.0f;
1013 while (ny < -180.0f) ny += 360.0f;
1014
1015 preserve_movement(cmd, orig.y, ny);
1016 cmd->viewangles[0] = np;
1017 cmd->viewangles[1] = ny;
1018 cmd->viewangles[2] = orig.z;
1019 set_silent_restore(orig);
1020 if (g_iface.engine) {
1021 QAngle restore{orig.x, orig.y, orig.z};
1022 using SetViewFn = void (*)(void *, QAngle *);
1023 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore);
1024 }
1025 cmd->buttons |= IN_ATTACK;
1026
1027 static int logs = 0;
1028 if (logs < 10) {
1029 logs++;
1030 logf("melee: swing idx=%d dist=%.1f aim=(%.1f %.1f %.1f)", c.idx,
1031 c.dist, aim.x, aim.y, aim.z);
1032 }
1033 return true;
1034 }
1035
1036 static double last_none = 0.0;
1037 double now = esp_now();
1038 if (now - last_none >= 2.0) {
1039 last_none = now;
1040 logf("melee: no visible target within %.0f", MELEE_RANGE);
1041 }
1042 return false;
1043}
1044
787bool triggerbot_apply(CUserCmd *cmd) { 1045bool triggerbot_apply(CUserCmd *cmd) {
788 if (!g_iface.enginetrace || !g_iface.entitylist) 1046 if (!g_iface.enginetrace || !g_iface.entitylist)
789 return false; 1047 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) {
500 if (jumpbug_apply(cmd, localIdx, ent, flags, vel, frametime)) 500 if (jumpbug_apply(cmd, localIdx, ent, flags, vel, frametime))
501 note_modified(cmd, flags, vel); 501 note_modified(cmd, flags, vel);
502 502
503 if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT)) 503 bool melee = feature_active(FEAT_MELEE) && melee_weapon_active(ent);
504
505 if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT) || melee)
504 aimbot_reset_lock(); 506 aimbot_reset_lock();
505 if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM)) 507 if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM) ||
508 melee)
506 silentaim_reset_lock(); 509 silentaim_reset_lock();
507 510
508 if (feature_active(FEAT_AIMBOT)) { 511 if (melee) {
509 static bool aim_feature_logged = false; 512 if (melee_aimbot_apply(cmd))
510 if (!aim_feature_logged) {
511 aim_feature_logged = true;
512 logf("aimbot: feature active (bind held)");
513 }
514 if (aimbot_apply(cmd)) {
515 const float *vel = (const float *)(e + g_net.velocity);
516 note_modified(cmd, flags, vel); 513 note_modified(cmd, flags, vel);
514 } else {
515 if (feature_active(FEAT_AIMBOT)) {
516 static bool aim_feature_logged = false;
517 if (!aim_feature_logged) {
518 aim_feature_logged = true;
519 logf("aimbot: feature active (bind held)");
520 }
521 if (aimbot_apply(cmd))
522 note_modified(cmd, flags, vel);
517 } 523 }
518 }
519 524
520 if (feature_active(FEAT_TRIGGERBOT)) { 525 if (feature_active(FEAT_TRIGGERBOT)) {
521 if (triggerbot_apply(cmd)) { 526 if (triggerbot_apply(cmd))
522 const float *vel = (const float *)(e + g_net.velocity); 527 note_modified(cmd, flags, vel);
523 note_modified(cmd, flags, vel);
524 } 528 }
525 }
526 529
527 if (feature_active(FEAT_SILENTAIM)) { 530 if (feature_active(FEAT_SILENTAIM)) {
528 if (silent_aim_apply(cmd)) { 531 if (silent_aim_apply(cmd))
529 const float *vel = (const float *)(e + g_net.velocity); 532 note_modified(cmd, flags, vel);
530 note_modified(cmd, flags, vel);
531 } 533 }
532 } 534 }
533} 535}
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 {
82 int lifestate = -1; 82 int lifestate = -1;
83 int modelindex = -1; 83 int modelindex = -1;
84 int absvelocity = -1; // client C_BaseEntity::m_vecAbsVelocity (prediction) 84 int absvelocity = -1; // client C_BaseEntity::m_vecAbsVelocity (prediction)
85 int activeweapon = -1; // client m_hActiveWeapon handle
85 // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 86 // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72
86 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } 87 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; }
87 // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100. 88 // 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);
147bool aimbot_apply(CUserCmd *cmd); 148bool aimbot_apply(CUserCmd *cmd);
148bool triggerbot_apply(CUserCmd *cmd); 149bool triggerbot_apply(CUserCmd *cmd);
149bool silent_aim_apply(CUserCmd *cmd); 150bool silent_aim_apply(CUserCmd *cmd);
151bool melee_weapon_active(void *local_ent);
152bool melee_aimbot_apply(CUserCmd *cmd);
150enum FeatureId { 153enum FeatureId {
151 FEAT_BHOP = 0, 154 FEAT_BHOP = 0,
152 FEAT_ABH = 1, 155 FEAT_ABH = 1,
@@ -154,7 +157,8 @@ enum FeatureId {
154 FEAT_AIMBOT = 3, 157 FEAT_AIMBOT = 3,
155 FEAT_TRIGGERBOT = 4, 158 FEAT_TRIGGERBOT = 4,
156 FEAT_SILENTAIM = 5, 159 FEAT_SILENTAIM = 5,
157 FEAT_JUMPBUG = 6 160 FEAT_JUMPBUG = 6,
161 FEAT_MELEE = 7
158}; 162};
159enum EspOption { 163enum EspOption {
160 ESP_OPT_SHOW_NEUTRAL = 0, 164 ESP_OPT_SHOW_NEUTRAL = 0,
@@ -179,6 +183,7 @@ void resolve_netvars();
179bool resolve_server_netvars(); 183bool resolve_server_netvars();
180int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); 184int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out);
181bool server_set_velocity_z(int idx, float vz); 185bool server_set_velocity_z(int idx, float vz);
186int server_active_weapon(int idx);
182int server_model_index(void *base); 187int server_model_index(void *base);
183void find_modules(); 188void find_modules();
184void init_conmsg(); 189void 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;
64static FeatureState g_abh{true, 0}; 64static FeatureState g_abh{true, 0};
65static FeatureState g_jumpbug{true, 0}; 65static FeatureState g_jumpbug{true, 0};
66static bool g_jumpbug_assist = false; 66static bool g_jumpbug_assist = false;
67static FeatureState g_melee{true, 0};
67static FeatureState g_esp{true, 0}; 68static FeatureState g_esp{true, 0};
68static FeatureState g_aimbot{true, 604}; // default Mouse5, rebindable in menu 69static FeatureState g_aimbot{true, 604}; // default Mouse5, rebindable in menu
69static FeatureState g_triggerbot{true, 603}; // default Mouse4 70static FeatureState g_triggerbot{true, 603}; // default Mouse4
@@ -101,6 +102,8 @@ static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active,
101static MenuNode g_jumpbug_feat{NODE_FEATURE, "Jumpbug", false, 1, {}, nullptr, nullptr, nullptr}; 102static MenuNode g_jumpbug_feat{NODE_FEATURE, "Jumpbug", false, 1, {}, nullptr, nullptr, nullptr};
102static MenuNode g_jumpbug_active{NODE_TOGGLE, "Active", false, 0, {}, &g_jumpbug.active, nullptr, nullptr}; 103static MenuNode g_jumpbug_active{NODE_TOGGLE, "Active", false, 0, {}, &g_jumpbug.active, nullptr, nullptr};
103static MenuNode g_jumpbug_assist_node{NODE_TOGGLE, "Assist timing", false, 0, {}, &g_jumpbug_assist, nullptr, nullptr}; 104static MenuNode g_jumpbug_assist_node{NODE_TOGGLE, "Assist timing", false, 0, {}, &g_jumpbug_assist, nullptr, nullptr};
105static MenuNode g_melee_feat{NODE_FEATURE, "Melee killbot", false, 1, {}, nullptr, nullptr, nullptr};
106static MenuNode g_melee_active{NODE_TOGGLE, "Active", false, 0, {}, &g_melee.active, nullptr, nullptr};
104static MenuNode g_esp_feat{NODE_FEATURE, "ESP", false, 1, {}, nullptr, nullptr, nullptr}; 107static MenuNode g_esp_feat{NODE_FEATURE, "ESP", false, 1, {}, nullptr, nullptr, nullptr};
105static MenuNode g_esp_active{NODE_TOGGLE, "Active", false, 0, {}, &g_esp.active, nullptr, nullptr}; 108static MenuNode g_esp_active{NODE_TOGGLE, "Active", false, 0, {}, &g_esp.active, nullptr, nullptr};
106static MenuNode g_esp_opt_nodes[ESP_OPT_COUNT] = { 109static MenuNode g_esp_opt_nodes[ESP_OPT_COUNT] = {
@@ -156,6 +159,8 @@ static void build_tree() {
156 g_jumpbug_feat.children[0] = &g_jumpbug_active; 159 g_jumpbug_feat.children[0] = &g_jumpbug_active;
157 g_jumpbug_feat.children[1] = &g_jumpbug_assist_node; 160 g_jumpbug_feat.children[1] = &g_jumpbug_assist_node;
158 g_jumpbug_feat.child_count = 2; 161 g_jumpbug_feat.child_count = 2;
162 g_melee_feat.children[0] = &g_melee_active;
163 g_melee_feat.child_count = 1;
159 g_esp_feat.children[0] = &g_esp_active; 164 g_esp_feat.children[0] = &g_esp_active;
160 g_aimbot_feat.children[0] = &g_aimbot_active; 165 g_aimbot_feat.children[0] = &g_aimbot_active;
161 g_aimbot_feat.children[1] = &g_aimbot_bind; 166 g_aimbot_feat.children[1] = &g_aimbot_bind;
@@ -181,11 +186,12 @@ static void build_tree() {
181 g_root.children[0] = &g_bhop_feat; 186 g_root.children[0] = &g_bhop_feat;
182 g_root.children[1] = &g_abh_feat; 187 g_root.children[1] = &g_abh_feat;
183 g_root.children[2] = &g_jumpbug_feat; 188 g_root.children[2] = &g_jumpbug_feat;
184 g_root.children[3] = &g_esp_feat; 189 g_root.children[3] = &g_melee_feat;
185 g_root.children[4] = &g_aimbot_feat; 190 g_root.children[4] = &g_esp_feat;
186 g_root.children[5] = &g_triggerbot_feat; 191 g_root.children[5] = &g_aimbot_feat;
187 g_root.children[6] = &g_silentaim_feat; 192 g_root.children[6] = &g_triggerbot_feat;
188 g_root.child_count = 7; 193 g_root.children[7] = &g_silentaim_feat;
194 g_root.child_count = 8;
189} 195}
190 196
191static int g_depth[48]; // depth of each visible row (for indentation) 197static int g_depth[48]; // depth of each visible row (for indentation)
@@ -309,6 +315,7 @@ static void config_save() {
309 fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind); 315 fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind);
310 fprintf(f, "[jumpbug]\nactive=%d\nassist=%d\n", g_jumpbug.active, 316 fprintf(f, "[jumpbug]\nactive=%d\nassist=%d\n", g_jumpbug.active,
311 (int)g_jumpbug_assist); 317 (int)g_jumpbug_assist);
318 fprintf(f, "[melee]\nactive=%d\n", g_melee.active);
312 fprintf(f, "[aimbot]\nactive=%d\nbind=%d\nsmooth=%d\nvischeck=%d\n" 319 fprintf(f, "[aimbot]\nactive=%d\nbind=%d\nsmooth=%d\nvischeck=%d\n"
313 "fov=%d\nfov_circle=%d\n" 320 "fov=%d\nfov_circle=%d\n"
314 "bone_head=%d\nbone_torso=%d\nbone_larm=%d\nbone_rarm=%d\n" 321 "bone_head=%d\nbone_torso=%d\nbone_larm=%d\nbone_rarm=%d\n"
@@ -349,6 +356,8 @@ static void config_load() {
349 cur = &g_abh; 356 cur = &g_abh;
350 else if (strstr(line, "[jumpbug]")) 357 else if (strstr(line, "[jumpbug]"))
351 cur = &g_jumpbug; 358 cur = &g_jumpbug;
359 else if (strstr(line, "[melee]"))
360 cur = &g_melee;
352 else if (strstr(line, "[esp]")) 361 else if (strstr(line, "[esp]"))
353 cur = &g_esp; 362 cur = &g_esp;
354 else if (strstr(line, "[aimbot]")) 363 else if (strstr(line, "[aimbot]"))
@@ -537,10 +546,10 @@ static bool key_pressed(int sc) {
537// feature gate queried from hl2h.cpp do_move(): feature must be Active AND 546// feature gate queried from hl2h.cpp do_move(): feature must be Active AND
538// its bind key held (always-on when unbound). 547// its bind key held (always-on when unbound).
539static FeatureState *feature_state(int feature) { 548static FeatureState *feature_state(int feature) {
540 static FeatureState *feats[7] = {&g_bhop, &g_abh, &g_esp, 549 static FeatureState *feats[8] = {&g_bhop, &g_abh, &g_esp,
541 &g_aimbot, &g_triggerbot, &g_silentaim, 550 &g_aimbot, &g_triggerbot, &g_silentaim,
542 &g_jumpbug}; 551 &g_jumpbug, &g_melee};
543 if (feature < 0 || feature > 6) 552 if (feature < 0 || feature > 7)
544 return nullptr; 553 return nullptr;
545 return feats[feature]; 554 return feats[feature];
546} 555}
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) {
89 g_net.health = (int)(base_off + (uint32_t)poff); 89 g_net.health = (int)(base_off + (uint32_t)poff);
90 } else if (strcmp(pname2, "m_iMaxHealth") == 0 && g_net.maxhealth < 0) { 90 } else if (strcmp(pname2, "m_iMaxHealth") == 0 && g_net.maxhealth < 0) {
91 g_net.maxhealth = (int)(base_off + (uint32_t)poff); 91 g_net.maxhealth = (int)(base_off + (uint32_t)poff);
92 } else if (strcmp(pname2, "m_hActiveWeapon") == 0 &&
93 g_net.activeweapon < 0) {
94 g_net.activeweapon = (int)(base_off + (uint32_t)poff);
92 } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) { 95 } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) {
93 g_net.teamnum = (int)(base_off + (uint32_t)poff); 96 g_net.teamnum = (int)(base_off + (uint32_t)poff);
94 } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) { 97 } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) {
@@ -269,9 +272,9 @@ void resolve_netvars() {
269 logf("m_vecAbsVelocity unresolved; jumpbug assist disabled"); 272 logf("m_vecAbsVelocity unresolved; jumpbug assist disabled");
270 logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " 273 logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d "
271 "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d " 274 "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d "
272 "modelindex=%d absvelocity=%d", 275 "modelindex=%d absvelocity=%d activeweapon=%d",
273 g_net.flags, g_net.velocity, g_net.movetype, g_net.health, 276 g_net.flags, g_net.velocity, g_net.movetype, g_net.health,
274 g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, 277 g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset,
275 g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex, 278 g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex,
276 g_net.absvelocity); 279 g_net.absvelocity, g_net.activeweapon);
277} 280}
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;
33int g_srv_modelindex = -1; 33int g_srv_modelindex = -1;
34int g_srv_absvelocity = -1; 34int g_srv_absvelocity = -1;
35int g_srv_velocity = -1; 35int g_srv_velocity = -1;
36int g_srv_activeweapon = -1;
36static bool g_srv_ready = false; 37static bool g_srv_ready = false;
37static bool g_srv_checked = false; 38static bool g_srv_checked = false;
38 39
@@ -109,12 +110,14 @@ bool resolve_server_netvars() {
109 g_srv_modelindex = datamap_offset("m_nModelIndex", 7); // FIELD_SHORT 110 g_srv_modelindex = datamap_offset("m_nModelIndex", 7); // FIELD_SHORT
110 g_srv_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR 111 g_srv_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR
111 g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR 112 g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR
113 g_srv_activeweapon = datamap_offset("m_hActiveWeapon", 13); // FIELD_EHANDLE
112 g_srv_ready = g_srv_health > 0; 114 g_srv_ready = g_srv_health > 0;
113 if (g_srv_ready) { 115 if (g_srv_ready) {
114 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " 116 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d "
115 "modelindex=%d absvel=%d vel=%d", 117 "modelindex=%d absvel=%d vel=%d activeweapon=%d",
116 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, 118 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate,
117 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity); 119 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity,
120 g_srv_activeweapon);
118 fail_logged = false; 121 fail_logged = false;
119 } else if (!fail_logged) { 122 } else if (!fail_logged) {
120 fail_logged = true; 123 fail_logged = true;
@@ -194,3 +197,18 @@ bool server_set_velocity_z(int idx, float vz) {
194 *(float *)((uintptr_t)base + g_srv_absvelocity + 8) = vz; 197 *(float *)((uintptr_t)base + g_srv_absvelocity + 8) = vz;
195 return true; 198 return true;
196} 199}
200
201// Entity index of the player's active weapon from the authoritative server
202// entity. Used as a fallback if the client m_hActiveWeapon netvar is missing.
203int server_active_weapon(int idx) {
204 if (!g_srv_ready || !g_iface.engineserver || g_srv_activeweapon <= 0)
205 return -1;
206 void *base = nullptr;
207 if (server_entity_health(idx, nullptr, nullptr, &base) < 0 || !base)
208 return -1;
209 uint32_t h = *(uint32_t *)((uintptr_t)base + g_srv_activeweapon);
210 if (h == 0xFFFFFFFFu)
211 return -1;
212 int wi = (int)(h & 0xFFFF);
213 return (wi > 0 && wi <= 4096) ? wi : -1;
214}