summaryrefslogtreecommitdiffstats
path: root/src/hl2h.cpp
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:09:55 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:09:55 +0100
commit33f5ed9ef58f26c6b3b52cf605dd4311d658a60d (patch)
treea97254872e465c9107b7ef19695ab56bb076d7e2 /src/hl2h.cpp
parentItems: log the usable ammo table once (diff)
downloadhl2h-33f5ed9ef58f26c6b3b52cf605dd4311d658a60d.tar.gz
hl2h-33f5ed9ef58f26c6b3b52cf605dd4311d658a60d.zip
Fix audit findings: crash safety, correctness, rendering, performance
Crash/UB: - gate every hook behind an init-stage flag until config/surface/input are ready - publish module maps as immutable atomic snapshots instead of reallocating shared vectors under unlocked readers - validate the %ebp walk in the RandomSeed/RandomFloat hooks against the stack - bounds-check every RecvTable/RecvProp/string dereference with module_range() - preserve the Itanium vtable header when cloning (client, CInput, model render, studio render) - reject negative usercmd sequence numbers; guard config_load against a null section target; cap the bone parent walk; check mprotect; drop PROT_EXEC on the GOT patch; absorb server.so .bss tail Correctness: - filter dormant entities across ESP, aimbot, triggerbot, melee, items and projectiles (slot self-tested against the local player) - reset the server-health latch per map and only latch on valid model indices - bone cache sized for the 128-target scan; set valid=false on every failure; head_position keeps searching past an invalid bone - vischeck steps past glass by probing until clear, and ignores entities owned by the target (held weapons, bone followers) - aimbot: maxs no longer disables it, AABB-aware FOV prefilter, lock hysteresis, lead point re-checked for FOV and visibility - spread: seed hook arms on the no-spread toggle alone; engine toggle only controls the RandomFloat zeroing - resolve m_hOwnerEntity/m_hOwner; NPC lead velocity from the server entity - vector_angles pitch range, point_ground normal test, underwater jump+duck no longer skips combat, semi-auto press/release edge for synthetic fire - item/projectile markers and blast rings refresh origin per frame while discovery stays at 10 Hz; trajectory uses the real m_DmgRadius and honours the blast-radius toggle Rendering/GUI: - one overlay draw per engine frame (FRAME_RENDER_START boundary), fixing alpha stacking and the speed-graph flicker - clipped lines/outlines and ellipsised text instead of drawing past the content pane or shifting over neighbours - dropdown dismissal consumes the click; hover ignores hidden controls - chams: reset AlphaModulate, reference shared materials once, restore prop tint colour; fonts created once per handle - projection uses Source's 4:3 aspect scaling and roll; screen size refreshed each frame; FOV circle gated on the aim toggles, not the bind Performance: - TTL caches for NPC relation, friendly/neutral and damage class - model names memoised per model index - entity pointer -> index cache for the triggerbot (was a 4096-slot walk/tick) Build: - real Makefile with -MMD dependency tracking, debug/release objects and an always-relink rule; build.sh kept as a thin wrapper
Diffstat (limited to '')
-rw-r--r--src/hl2h.cpp214
1 files changed, 159 insertions, 55 deletions
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index ddfc02a..6697778 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -1,5 +1,6 @@
1// hl2h.cpp — core: logging, vector math, globals, feature logic, setup 1// hl2h.cpp — core: logging, vector math, globals, feature logic, setup
2#include "hl2h.h" 2#include "hl2h.h"
3#include <atomic>
3#include <cmath> 4#include <cmath>
4#include <cstdarg> 5#include <cstdarg>
5#include <cstdlib> 6#include <cstdlib>
@@ -7,6 +8,41 @@
7#include <time.h> 8#include <time.h>
8 9
9// --------------------------------------------------------------------------- 10// ---------------------------------------------------------------------------
11// init readiness
12// ---------------------------------------------------------------------------
13
14static std::atomic<int> g_init_stage{INIT_NONE};
15
16void init_set_stage(int stage) {
17 g_init_stage.store(stage, std::memory_order_release);
18}
19
20int init_stage() { return g_init_stage.load(std::memory_order_acquire); }
21
22// ---------------------------------------------------------------------------
23// vtable cloning
24// ---------------------------------------------------------------------------
25//
26// Under the Itanium C++ ABI the two words before slot 0 are offset-to-top and
27// the type_info pointer. Copying only the slots and repointing the object's
28// vptr leaves those words pointing at allocator metadata, which breaks any
29// RTTI the host performs on the interface. Keep the header words with the
30// clone and hand back slot 0.
31
32void **vtable_clone(void *iface, size_t slots) {
33 if (!iface || slots == 0)
34 return nullptr;
35 void **vt = *(void ***)iface;
36 if (!vt)
37 return nullptr;
38 void **out = new void *[slots + 2];
39 out[0] = vt[-2];
40 out[1] = vt[-1];
41 memcpy(out + 2, vt, slots * sizeof(void *));
42 return out + 2;
43}
44
45// ---------------------------------------------------------------------------
10// logging 46// logging
11// --------------------------------------------------------------------------- 47// ---------------------------------------------------------------------------
12 48
@@ -67,7 +103,6 @@ void logd(const char *fmt, ...) {
67// globals 103// globals
68// --------------------------------------------------------------------------- 104// ---------------------------------------------------------------------------
69 105
70Module g_client, g_engine, g_server;
71GameInterfaces g_iface; 106GameInterfaces g_iface;
72NetVars g_net; 107NetVars g_net;
73Vector g_cam_eye{0, 0, 0}; 108Vector g_cam_eye{0, 0, 0};
@@ -87,11 +122,11 @@ QAngle vector_angles(const Vector &forward) {
87 a.y = atan2f(forward.y, forward.x) * 180.f / PI_F; 122 a.y = atan2f(forward.y, forward.x) * 180.f / PI_F;
88 if (a.y < 0) 123 if (a.y < 0)
89 a.y += 360.f; 124 a.y += 360.f;
125 // Pitch stays in [-90, 90] like Source's QAngle; wrapping negatives to
126 // [270, 360) made every upward aim compare as a steeply negative angle.
90 a.x = atan2f(-forward.z, 127 a.x = atan2f(-forward.z,
91 sqrtf(forward.x * forward.x + forward.y * forward.y)) * 128 sqrtf(forward.x * forward.x + forward.y * forward.y)) *
92 180.f / PI_F; 129 180.f / PI_F;
93 if (a.x < 0)
94 a.x += 360.f;
95 } 130 }
96 return a; 131 return a;
97} 132}
@@ -485,7 +520,31 @@ static bool g_seed_hook_installed = false;
485// Pellet-spread window armed by the player's FireBullets RandomSeed call. 520// Pellet-spread window armed by the player's FireBullets RandomSeed call.
486static volatile int g_spread_rand_left = 0; 521static volatile int g_spread_rand_left = 0;
487 522
523// Identify the caller of a random-stream function by reading the caller's
524// stack frame. The callers live in Valve's binaries, which are not built with
525// the guarantee our own -fno-omit-frame-pointer gives us, so treat the walk as
526// untrusted: it may only stay inside the current stack, and the object it
527// yields is used for nothing but comparison against g_local_server_player.
528static void *caller_self_arg() {
529 void *sp = nullptr, *bp = nullptr;
530 __asm__ volatile("movl %%esp, %0" : "=r"(sp));
531 __asm__ volatile("movl %%ebp, %0" : "=r"(bp));
532 uintptr_t spv = (uintptr_t)sp, bpv = (uintptr_t)bp;
533 constexpr uintptr_t MAX_DEPTH = 0x100000; // 1 MiB of stack frames
534 if (!bpv || (bpv & 3) || bpv <= spv || bpv - spv > MAX_DEPTH)
535 return nullptr;
536 uintptr_t caller = *(uintptr_t *)bpv;
537 if (!caller || (caller & 3) || caller < bpv || caller - spv > MAX_DEPTH)
538 return nullptr;
539 return *(void **)(caller + 8); // first argument slot of the caller
540}
541
488static void hkdSrvRandomSeed(int seed) { 542static void hkdSrvRandomSeed(int seed) {
543 if (!init_live()) {
544 if (g_real_random_seed)
545 g_real_random_seed(seed);
546 return;
547 }
489 uintptr_t ret = (uintptr_t)__builtin_return_address(0); 548 uintptr_t ret = (uintptr_t)__builtin_return_address(0);
490 long off = (long)(ret - g_srv_base); 549 long off = (long)(ret - g_srv_base);
491 static bool trace = getenv("HL2H_SEED_TRACE") != nullptr; 550 static bool trace = getenv("HL2H_SEED_TRACE") != nullptr;
@@ -494,16 +553,16 @@ static void hkdSrvRandomSeed(int seed) {
494 // The caller's first argument is `this`. Only FireBullets is called on the 553 // The caller's first argument is `this`. Only FireBullets is called on the
495 // player entity; the CUniformRandomStream wrappers and the constructor-time 554 // player entity; the CUniformRandomStream wrappers and the constructor-time
496 // RandomSeed(0) reset have a different object there. 555 // RandomSeed(0) reset have a different object there.
497 void *frame = nullptr; 556 void *self = caller_self_arg();
498 __asm__ volatile("movl %%ebp, %0" : "=r"(frame));
499 if (frame)
500 frame = *(void **)frame; // caller's ebp (CBaseEntity::FireBullets)
501 void *self = frame ? *(void **)((char *)frame + 8) : nullptr;
502 bool local = self && g_local_server_player && 557 bool local = self && g_local_server_player &&
503 (uintptr_t)self == g_local_server_player; 558 (uintptr_t)self == g_local_server_player;
504 559
505 if (local && g_server.contains(ret) && feature_active(FEAT_SPREAD) && 560 // Seed selection and engine spread zeroing are independent toggles: "engine
506 spread_no_spread_enabled() && spread_engine_enabled()) { 561 // override" only decides whether RandomFloat is forced to 0, it must not
562 // disable the seed hook (without the seed hook the server ignores
563 // cmd->random_seed entirely).
564 if (local && mod_server().contains(ret) && feature_active(FEAT_SPREAD) &&
565 spread_no_spread_enabled()) {
507 g_spread_rand_left = 8; 566 g_spread_rand_left = 8;
508 int pellets = g_spread_pellets; 567 int pellets = g_spread_pellets;
509 if (pellets < 1) 568 if (pellets < 1)
@@ -533,12 +592,12 @@ static void hkdSrvRandomSeed(int seed) {
533 592
534static void install_seed_hook() { 593static void install_seed_hook() {
535 static bool failed = false; 594 static bool failed = false;
536 if (g_seed_hook_installed || failed || !g_server.valid()) 595 if (g_seed_hook_installed || failed || !mod_server().valid())
537 return; 596 return;
538 g_srv_base = g_server.ranges[0].lo; 597 g_srv_base = mod_server().ranges[0].lo;
539 uintptr_t reloc = 0, bias = 0; 598 uintptr_t reloc = 0, bias = 0;
540 uintptr_t slot = 0; 599 uintptr_t slot = 0;
541 if (module_jump_slot(g_server.path.c_str(), "RandomSeed", &reloc, &bias)) { 600 if (module_jump_slot(mod_server().path.c_str(), "RandomSeed", &reloc, &bias)) {
542 slot = g_srv_base + reloc - bias; 601 slot = g_srv_base + reloc - bias;
543 logf("seed hook: RandomSeed GOT reloc=%#lx bias=%#lx -> %#lx", 602 logf("seed hook: RandomSeed GOT reloc=%#lx bias=%#lx -> %#lx",
544 (unsigned long)reloc, (unsigned long)bias, (unsigned long)slot); 603 (unsigned long)reloc, (unsigned long)bias, (unsigned long)slot);
@@ -547,13 +606,13 @@ static void install_seed_hook() {
547 static const uintptr_t known[] = {0xce5348, 0xce1348}; 606 static const uintptr_t known[] = {0xce5348, 0xce1348};
548 for (size_t i = 0; i < sizeof(known) / sizeof(known[0]) && !slot; i++) { 607 for (size_t i = 0; i < sizeof(known) / sizeof(known[0]) && !slot; i++) {
549 uintptr_t cand = g_srv_base + known[i]; 608 uintptr_t cand = g_srv_base + known[i];
550 if (g_server.contains(cand)) 609 if (mod_server().contains(cand))
551 slot = cand; 610 slot = cand;
552 } 611 }
553 logf("seed hook: ELF reloc scan failed, fallback slot=%#lx", 612 logf("seed hook: ELF reloc scan failed, fallback slot=%#lx",
554 (unsigned long)slot); 613 (unsigned long)slot);
555 } 614 }
556 if (!slot || !g_server.contains(slot)) { 615 if (!slot || !mod_server().contains(slot)) {
557 failed = true; 616 failed = true;
558 logf("seed hook: no writable RandomSeed slot"); 617 logf("seed hook: no writable RandomSeed slot");
559 return; 618 return;
@@ -568,8 +627,15 @@ static void install_seed_hook() {
568 toastf(TOAST_WARN, "Spread seed hook unavailable"); 627 toastf(TOAST_WARN, "Spread seed hook unavailable");
569 return; 628 return;
570 } 629 }
571 mprotect((void *)(slot & ~(uintptr_t)0xFFF), 0x1000, 630 // The slot is 4-byte aligned, so a single page always covers the 4-byte
572 PROT_READ | PROT_WRITE | PROT_EXEC); 631 // write. No PROT_EXEC: this is a data slot. Permissions are intentionally
632 // not restored because .got.plt stays writable for lazy binding.
633 uintptr_t page = slot & ~(uintptr_t)0xFFF;
634 if (mprotect((void *)page, 0x1000, PROT_READ | PROT_WRITE) != 0) {
635 failed = true;
636 logf("seed hook: mprotect failed for page %#lx", (unsigned long)page);
637 return;
638 }
573 *(uint32_t *)slot = (uint32_t)(uintptr_t)&hkdSrvRandomSeed; 639 *(uint32_t *)slot = (uint32_t)(uintptr_t)&hkdSrvRandomSeed;
574 g_seed_hook_installed = true; 640 g_seed_hook_installed = true;
575 logf("seed hook: server GOT %#lx -> %p (real %p)", 641 logf("seed hook: server GOT %#lx -> %p (real %p)",
@@ -598,19 +664,16 @@ static bool g_uniform_hook_installed = false;
598// (the compiler may outline it), so this window is the reliable signal. 664// (the compiler may outline it), so this window is the reliable signal.
599 665
600static float hkdUniformRandomFloat(void *self, float mn, float mx) { 666static float hkdUniformRandomFloat(void *self, float mn, float mx) {
667 if (!init_live())
668 return g_orig_uniform_float ? g_orig_uniform_float(self, mn, mx) : mn;
601 bool enabled = g_local_server_player && feature_active(FEAT_SPREAD) && 669 bool enabled = g_local_server_player && feature_active(FEAT_SPREAD) &&
602 spread_no_spread_enabled() && spread_engine_enabled(); 670 spread_no_spread_enabled() && spread_engine_enabled();
603 bool hit = false; 671 bool hit = false;
604 void *who = nullptr; 672 void *who = nullptr;
605 if (enabled) { 673 if (enabled) {
606 void *frame = nullptr; 674 who = caller_self_arg();
607 __asm__ volatile("movl %%ebp, %0" : "=r"(frame)); 675 if (who && (uintptr_t)who == g_local_server_player)
608 if (frame) { 676 hit = true;
609 frame = *(void **)frame; // caller's ebp
610 who = frame ? *(void **)((char *)frame + 8) : nullptr;
611 if (who && (uintptr_t)who == g_local_server_player)
612 hit = true;
613 }
614 if (!hit && g_spread_rand_left > 0) { 677 if (!hit && g_spread_rand_left > 0) {
615 // The next few draws after the player's FireBullets RandomSeed are the 678 // The next few draws after the player's FireBullets RandomSeed are the
616 // pellet spread, regardless of which function emits them. 679 // pellet spread, regardless of which function emits them.
@@ -748,9 +811,26 @@ static bool edge_jump_apply(CUserCmd *cmd, void *ent, int flags,
748 return true; 811 return true;
749} 812}
750 813
814// Everything tied to one map load / local player lifetime. Called when the
815// local entity pointer changes (map load, respawn, disconnect).
816static void session_reset() {
817 bones_clear();
818 aimbot_reset_lock();
819 silentaim_reset_lock();
820 esp_reset_session();
821 fullbright_invalidate();
822 world_tint_invalidate();
823 fog_invalidate();
824 skybox_invalidate();
825 logd("session: reset (local entity changed)");
826}
827
751static void do_move(int seq, float frametime) { 828static void do_move(int seq, float frametime) {
752 install_seed_hook(); 829 install_seed_hook();
753 install_uniform_random_hook(); 830 install_uniform_random_hook();
831 // The pellet window is armed inside FireBullets during this tick; it must
832 // never survive into the next one.
833 g_spread_rand_left = 0;
754 static bool g_echo_sent = false; 834 static bool g_echo_sent = false;
755 if (!g_input || !g_net.ok()) 835 if (!g_input || !g_net.ok())
756 return; 836 return;
@@ -779,17 +859,13 @@ static void do_move(int seq, float frametime) {
779 859
780 view_settings_apply(ent); 860 view_settings_apply(ent);
781 861
782 // New map / respawn: drop stale bone caches and target locks. 862 // New map / respawn: drop stale caches and locks. Any per-map state must be
863 // cleared here; caches that are not listed will leak across level changes
864 // (that is how the server-health latch and the fuse table broke before).
783 static void *last_local_ent = nullptr; 865 static void *last_local_ent = nullptr;
784 if (ent != last_local_ent) { 866 if (ent != last_local_ent) {
785 last_local_ent = ent; 867 last_local_ent = ent;
786 bones_clear(); 868 session_reset();
787 aimbot_reset_lock();
788 silentaim_reset_lock();
789 fullbright_invalidate();
790 world_tint_invalidate();
791 fog_invalidate();
792 skybox_invalidate();
793 } 869 }
794 870
795 if (feature_active(FEAT_SPREAD)) { 871 if (feature_active(FEAT_SPREAD)) {
@@ -854,8 +930,9 @@ static void do_move(int seq, float frametime) {
854 930
855 if ((bhop_on || abh_on) && (cmd->buttons & IN_JUMP)) { 931 if ((bhop_on || abh_on) && (cmd->buttons & IN_JUMP)) {
856 if (inwater && (cmd->buttons & IN_DUCK)) { 932 if (inwater && (cmd->buttons & IN_DUCK)) {
933 // Not a bhop tick, but combat and jump assist still have to run.
857 lastground = onground; 934 lastground = onground;
858 return; 935 goto bhop_done;
859 } 936 }
860 float speed = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]); 937 float speed = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]);
861 bool moving = speed > 0.1f; 938 bool moving = speed > 0.1f;
@@ -899,6 +976,7 @@ static void do_move(int seq, float frametime) {
899 } 976 }
900 } 977 }
901 978
979bhop_done:
902 bool jump_assist = crouch_jump_apply(cmd, flags, vel); 980 bool jump_assist = crouch_jump_apply(cmd, flags, vel);
903 if (edge_jump_apply(cmd, ent, flags, vel)) 981 if (edge_jump_apply(cmd, ent, flags, vel))
904 jump_assist = true; 982 jump_assist = true;
@@ -975,7 +1053,12 @@ void set_silent_restore(const QAngle &angles) {
975static void hkdFrameStageNotify(void *self, int stage) { 1053static void hkdFrameStageNotify(void *self, int stage) {
976 if (oFrameStageNotify) 1054 if (oFrameStageNotify)
977 oFrameStageNotify(self, stage); 1055 oFrameStageNotify(self, stage);
1056 if (!init_live())
1057 return;
978 if (stage == 5) { 1058 if (stage == 5) {
1059 // FRAME_RENDER_START is the real frame boundary: it lets the paint hooks
1060 // below draw exactly once per frame instead of once per VGUI panel.
1061 overlay_frame_mark();
979 // Skybox texture uploads happen here, not in the VGUI paint hook: the 1062 // Skybox texture uploads happen here, not in the VGUI paint hook: the
980 // material system only downloads procedural textures on a render-safe 1063 // material system only downloads procedural textures on a render-safe
981 // frame stage. 1064 // frame stage.
@@ -1006,14 +1089,15 @@ static bool g_in_draw = false;
1006// our overlay draws are submitted through outer_draw() (menu.cpp), which 1089// our overlay draws are submitted through outer_draw() (menu.cpp), which
1007// wraps them in PushMakeCurrent/PopMakeCurrent with the embedded panel. 1090// wraps them in PushMakeCurrent/PopMakeCurrent with the embedded panel.
1008static void hkdDrawPrintText(void *self, const wchar_t *text, int len, int drawType) { 1091static void hkdDrawPrintText(void *self, const wchar_t *text, int len, int drawType) {
1009 if (g_in_draw) { 1092 if (g_in_draw || !init_live()) {
1010 oDrawPrintText(self, text, len, drawType); 1093 oDrawPrintText(self, text, len, drawType);
1011 return; 1094 return;
1012 } 1095 }
1013 g_in_draw = true; 1096 g_in_draw = true;
1014 oDrawPrintText(self, text, len, drawType); 1097 oDrawPrintText(self, text, len, drawType);
1015 // fallback trigger: fires when the game draws text (menus). Primary draw 1098 // Fallback trigger for frames where nothing paints (menus). The per-frame
1016 // path is the PaintTraverseEx hook (slot 114), which fires every frame. 1099 // budget inside outer_draw() makes this a no-op if PaintTraverseEx already
1100 // drew this frame.
1017 outer_draw(); 1101 outer_draw();
1018 g_in_draw = false; 1102 g_in_draw = false;
1019} 1103}
@@ -1024,7 +1108,7 @@ static void hkdDrawPrintText(void *self, const wchar_t *text, int len, int drawT
1024static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) { 1108static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) {
1025 using Fn = void (*)(void *, long, bool); 1109 using Fn = void (*)(void *, long, bool);
1026 ((Fn)oPaintTraverseEx)(self, panel, paintPopups); 1110 ((Fn)oPaintTraverseEx)(self, panel, paintPopups);
1027 if (!g_in_draw) { 1111 if (!g_in_draw && init_live()) {
1028 g_in_draw = true; 1112 g_in_draw = true;
1029 outer_draw(); 1113 outer_draw();
1030 g_in_draw = false; 1114 g_in_draw = false;
@@ -1032,6 +1116,8 @@ static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) {
1032} 1116}
1033 1117
1034static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { 1118static bool hkdCreateMove(void *self, int seq, float frametime, bool active) {
1119 if (!init_live())
1120 return oCreateMove(self, seq, frametime, active);
1035 bool ret = oCreateMove(self, seq, frametime, active); 1121 bool ret = oCreateMove(self, seq, frametime, active);
1036 do_move(seq, frametime); 1122 do_move(seq, frametime);
1037 1123
@@ -1041,8 +1127,12 @@ static bool hkdCreateMove(void *self, int seq, float frametime, bool active) {
1041 if (!isurface_hooked && g_surface) { 1127 if (!isurface_hooked && g_surface) {
1042 void **svt = VTABLE(g_surface); 1128 void **svt = VTABLE(g_surface);
1043 constexpr size_t VT_SIZE = 256; 1129 constexpr size_t VT_SIZE = 256;
1044 void **newsvt = new void *[VT_SIZE]; 1130 void **newsvt = vtable_clone(g_surface, VT_SIZE);
1045 memcpy(newsvt, svt, VT_SIZE * sizeof(void *)); 1131 if (!newsvt) {
1132 isurface_hooked = 1; // don't retry every tick
1133 logf("ISurface hook: vtable unavailable");
1134 return ret;
1135 }
1046 oDrawPrintText = (DrawPrintTextFn)svt[22]; 1136 oDrawPrintText = (DrawPrintTextFn)svt[22];
1047 newsvt[22] = (void *)&hkdDrawPrintText; 1137 newsvt[22] = (void *)&hkdDrawPrintText;
1048 oPaintTraverseEx = (PaintTraverseExFn)svt[114]; 1138 oPaintTraverseEx = (PaintTraverseExFn)svt[114];
@@ -1059,8 +1149,11 @@ static bool hkdCreateMove(void *self, int seq, float frametime, bool active) {
1059static void install_hook() { 1149static void install_hook() {
1060 void **vt = VTABLE(g_iface.client); 1150 void **vt = VTABLE(g_iface.client);
1061 constexpr size_t SLOTS = 256; 1151 constexpr size_t SLOTS = 256;
1062 void **newvt = new void *[SLOTS]; 1152 void **newvt = vtable_clone(g_iface.client, SLOTS);
1063 memcpy(newvt, vt, SLOTS * sizeof(void *)); 1153 if (!newvt) {
1154 logf("hook install: client vtable unavailable");
1155 return;
1156 }
1064 oCreateMove = (CreateMoveFn)vt[21]; 1157 oCreateMove = (CreateMoveFn)vt[21];
1065 newvt[21] = (void *)&hkdCreateMove; 1158 newvt[21] = (void *)&hkdCreateMove;
1066 oFrameStageNotify = (FrameStageFn)vt[35]; 1159 oFrameStageNotify = (FrameStageFn)vt[35];
@@ -1097,26 +1190,28 @@ static bool is_hl2_process() {
1097static void *setup_thread(void *) { 1190static void *setup_thread(void *) {
1098 for (int i = 0; i < 6000; i++) { 1191 for (int i = 0; i < 6000; i++) {
1099 find_modules(); 1192 find_modules();
1100 if (g_client.valid() && g_engine.valid()) 1193 if (mod_client().valid() && mod_engine().valid())
1101 break; 1194 break;
1102 usleep(200 * 1000); 1195 usleep(200 * 1000);
1103 } 1196 }
1104 if (!g_client.valid() || !g_engine.valid()) { 1197 if (!mod_client().valid() || !mod_engine().valid()) {
1198 init_set_stage(INIT_FAILED);
1105 logf("client.so/engine.so never appeared"); 1199 logf("client.so/engine.so never appeared");
1106 toastf(TOAST_ERROR, "client.so/engine.so not found; hl2hake inactive"); 1200 toastf(TOAST_ERROR, "client.so/engine.so not found; hl2hake inactive");
1107 return nullptr; 1201 return nullptr;
1108 } 1202 }
1109 logf("client.so: %d mapped range(s), %s", (int)g_client.ranges.size(), 1203 logf("client.so: %d mapped range(s), %s", (int)mod_client().ranges.size(),
1110 g_client.path.c_str()); 1204 mod_client().path.c_str());
1111 logf("engine.so: %d mapped range(s), %s", (int)g_engine.ranges.size(), 1205 logf("engine.so: %d mapped range(s), %s", (int)mod_engine().ranges.size(),
1112 g_engine.path.c_str()); 1206 mod_engine().path.c_str());
1113 1207
1114 // resolve factories via dlopen handles 1208 // resolve factories via dlopen handles
1115 void *ch = dlopen(g_client.path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); 1209 void *ch = dlopen(mod_client().path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1116 void *eh = dlopen(g_engine.path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); 1210 void *eh = dlopen(mod_engine().path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1117 auto cf = (CreateInterfaceFn)dlsym(ch, "CreateInterface"); 1211 auto cf = (CreateInterfaceFn)dlsym(ch, "CreateInterface");
1118 auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); 1212 auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface");
1119 if (!cf || !ef) { 1213 if (!cf || !ef) {
1214 init_set_stage(INIT_FAILED);
1120 logf("CreateInterface not found"); 1215 logf("CreateInterface not found");
1121 toastf(TOAST_ERROR, "CreateInterface not found; hl2hake inactive"); 1216 toastf(TOAST_ERROR, "CreateInterface not found; hl2hake inactive");
1122 return nullptr; 1217 return nullptr;
@@ -1134,6 +1229,7 @@ static void *setup_thread(void *) {
1134 usleep(100 * 1000); 1229 usleep(100 * 1000);
1135 } 1230 }
1136 if (!g_iface.client || !g_iface.entitylist || !g_iface.engine) { 1231 if (!g_iface.client || !g_iface.entitylist || !g_iface.engine) {
1232 init_set_stage(INIT_FAILED);
1137 logf("interfaces unresolved: VClient017=%p VClientEntityList003=%p " 1233 logf("interfaces unresolved: VClient017=%p VClientEntityList003=%p "
1138 "VEngineClient014=%p", 1234 "VEngineClient014=%p",
1139 g_iface.client, g_iface.entitylist, g_iface.engine); 1235 g_iface.client, g_iface.entitylist, g_iface.engine);
@@ -1159,10 +1255,10 @@ static void *setup_thread(void *) {
1159 g_iface.cvar = cf(cvar_names[i], nullptr); 1255 g_iface.cvar = cf(cvar_names[i], nullptr);
1160 } 1256 }
1161 if (!g_iface.cvar) { 1257 if (!g_iface.cvar) {
1162 size_t slash = g_engine.path.find_last_of('/'); 1258 size_t slash = mod_engine().path.find_last_of('/');
1163 if (slash != std::string::npos) { 1259 if (slash != std::string::npos) {
1164 std::string path = 1260 std::string path =
1165 g_engine.path.substr(0, slash + 1) + "libvstdlib.so"; 1261 mod_engine().path.substr(0, slash + 1) + "libvstdlib.so";
1166 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); 1262 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1167 if (!h) 1263 if (!h)
1168 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); 1264 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
@@ -1195,15 +1291,23 @@ static void *setup_thread(void *) {
1195 toastf(TOAST_WARN, "Debug overlay missing; ESP projection uses fallback"); 1291 toastf(TOAST_WARN, "Debug overlay missing; ESP projection uses fallback");
1196 if (!g_iface.enginetrace) 1292 if (!g_iface.enginetrace)
1197 toastf(TOAST_WARN, "Engine trace missing; vischeck fails open"); 1293 toastf(TOAST_WARN, "Engine trace missing; vischeck fails open");
1294 init_set_stage(INIT_INTERFACES);
1295
1296 init_conmsg();
1297 // Config load and ISurface/font discovery must finish before any hook is
1298 // allowed past the init_live() gate: the game thread keeps calling
1299 // CreateMove/PaintTraverse while this thread runs.
1300 install_menu();
1301 init_set_stage(INIT_READY);
1198 1302
1199 if (!find_input_global()) { 1303 if (!find_input_global()) {
1304 init_set_stage(INIT_FAILED);
1200 toastf(TOAST_ERROR, "Input hook failed; movement features disabled"); 1305 toastf(TOAST_ERROR, "Input hook failed; movement features disabled");
1201 return nullptr; 1306 return nullptr;
1202 } 1307 }
1203
1204 init_conmsg();
1205 install_hook(); 1308 install_hook();
1206 install_menu(); 1309 init_set_stage(INIT_LIVE);
1310 logf("init: live (surface %p)", (void *)g_surface);
1207 if (g_surface) 1311 if (g_surface)
1208 toastf(TOAST_INFO, "hl2hake loaded - Insert opens the menu"); 1312 toastf(TOAST_INFO, "hl2hake loaded - Insert opens the menu");
1209 return nullptr; 1313 return nullptr;