summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:19:25 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:19:25 +0100
commit9f0724dcd25a06c5674d30d045dc485a341253b1 (patch)
treee7587dd03c0b164113d3b879b18d81fccf332c05
parentChams: leave engine-forced material passes alone (diff)
downloadhl2h-9f0724dcd25a06c5674d30d045dc485a341253b1.tar.gz
hl2h-9f0724dcd25a06c5674d30d045dc485a341253b1.zip
Hooks: central vtable install with restore support
Move vtable patching into hooks.cpp: one call clones the table, validates every slot, records the originals and installs the clone. Installs happen from the setup, game and paint threads, so the bookkeeping is mutex-protected. Client, CInput, IVModelRender, IStudioRender and ISurface all use it, and hooks_restore_all() can now put every interface back (previously impossible).
-rw-r--r--FIX_PLAN.md10
-rw-r--r--src/cinput.cpp22
-rw-r--r--src/hl2h.cpp44
-rw-r--r--src/hl2h.h1
-rw-r--r--src/hooks.cpp59
-rw-r--r--src/hooks.h28
-rw-r--r--src/studio.cpp44
7 files changed, 147 insertions, 61 deletions
diff --git a/FIX_PLAN.md b/FIX_PLAN.md
index 2c84122..3ebbd17 100644
--- a/FIX_PLAN.md
+++ b/FIX_PLAN.md
@@ -788,6 +788,14 @@ the engine's override cleared for every model drawn afterwards. This is the fix
788for the corruption described in P2-6; it needs an in-game check because it 788for the corruption described in P2-6; it needs an in-game check because it
789relies on the engine always going through the vtable for that call. 789relies on the engine always going through the vtable for that call.
790 790
791### Batch 6 — hook registry
792
793`hooks.h` / `hooks.cpp` own vtable installation: one call clones, bounds-checks
794every slot, records the originals and installs the clone, with a mutex because
795installs happen from the setup, game and paint threads. `hooks_restore_all()`
796puts every interface back, which previously was impossible. Client, CInput,
797IVModelRender, IStudioRender and ISurface all go through it now.
798
791### Known remaining work 799### Known remaining work
792 800
793Deliberately deferred, with reasons: 801Deliberately deferred, with reasons:
@@ -800,7 +808,7 @@ Deliberately deferred, with reasons:
800- **P3-2 declarative settings table** — large rewrite of `config_save`, 808- **P3-2 declarative settings table** — large rewrite of `config_save`,
801 `config_load`, `config_reset_defaults` and `build_cards`. It can regress 809 `config_load`, `config_reset_defaults` and `build_cards`. It can regress
802 config persistence, so it should follow an in-game pass. 810 config persistence, so it should follow an in-game pass.
803- **P3-3 `esp.cpp` split**, **P3-5 header split**, **P3-6 hook registry** — 811- **P3-3 `esp.cpp` split**, **P3-5 header split** —
804 behaviour-neutral moves with a large diff; best done after the current build 812 behaviour-neutral moves with a large diff; best done after the current build
805 has been validated in-game so a regression is easy to bisect. 813 has been validated in-game so a regression is easy to bisect.
806 814
diff --git a/src/cinput.cpp b/src/cinput.cpp
index 45e2b04..9cad30d 100644
--- a/src/cinput.cpp
+++ b/src/cinput.cpp
@@ -159,22 +159,18 @@ static void hkdInputCreateMove(void *self, int seq, float frametime,
159static bool install_input_hook() { 159static bool install_input_hook() {
160 if (!g_input) 160 if (!g_input)
161 return false; 161 return false;
162 void **vt = VTABLE((void *)g_input); 162 if (!*(void ***)g_input)
163 if (!vt)
164 return false; 163 return false;
165 constexpr size_t N = 64; 164 VtablePatch patches[] = {
166 void **nvt = vtable_clone((void *)g_input, N); 165 {3, (void *)&hkdInputCreateMove,
167 if (!nvt) { 166 reinterpret_cast<void **>(&oInputCreateMove)},
168 logf("CInput vtable clone failed"); 167 {4, (void *)&hkdInputExtraMouseSample,
168 reinterpret_cast<void **>(&oInputExtraMouseSample)},
169 };
170 if (!hooks_install_vtable((void *)g_input, 64, patches, 2)) {
171 logf("CInput vtable patch failed");
169 return false; 172 return false;
170 } 173 }
171 oInputCreateMove = (InputCreateMoveFn)vt[3];
172 nvt[3] = (void *)&hkdInputCreateMove;
173 if (N > 4) {
174 oInputExtraMouseSample = (InputExtraMouseFn)vt[4];
175 nvt[4] = (void *)&hkdInputExtraMouseSample;
176 }
177 VTABLE((void *)g_input) = nvt;
178 logf("CInput CreateMove hooked: slot 3 orig=%p; ExtraMouseSample slot 4 " 174 logf("CInput CreateMove hooked: slot 3 orig=%p; ExtraMouseSample slot 4 "
179 "orig=%p", 175 "orig=%p",
180 (void *)oInputCreateMove, (void *)oInputExtraMouseSample); 176 (void *)oInputCreateMove, (void *)oInputExtraMouseSample);
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index 6697778..354b3e6 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -1125,20 +1125,17 @@ static bool hkdCreateMove(void *self, int seq, float frametime, bool active) {
1125 // DrawPrintText (slot 22) hooked as a fallback text-draw trigger. 1125 // DrawPrintText (slot 22) hooked as a fallback text-draw trigger.
1126 static int isurface_hooked = 0; 1126 static int isurface_hooked = 0;
1127 if (!isurface_hooked && g_surface) { 1127 if (!isurface_hooked && g_surface) {
1128 void **svt = VTABLE(g_surface); 1128 VtablePatch patches[] = {
1129 constexpr size_t VT_SIZE = 256; 1129 {22, (void *)&hkdDrawPrintText,
1130 void **newsvt = vtable_clone(g_surface, VT_SIZE); 1130 reinterpret_cast<void **>(&oDrawPrintText)},
1131 if (!newsvt) { 1131 {114, (void *)&hkdPaintTraverseEx,
1132 isurface_hooked = 1; // don't retry every tick 1132 reinterpret_cast<void **>(&oPaintTraverseEx)},
1133 logf("ISurface hook: vtable unavailable"); 1133 };
1134 isurface_hooked = 1; // one attempt only
1135 if (!hooks_install_vtable(g_surface, 256, patches, 2)) {
1136 logf("ISurface hook: vtable patch failed");
1134 return ret; 1137 return ret;
1135 } 1138 }
1136 oDrawPrintText = (DrawPrintTextFn)svt[22];
1137 newsvt[22] = (void *)&hkdDrawPrintText;
1138 oPaintTraverseEx = (PaintTraverseExFn)svt[114];
1139 newsvt[114] = (void *)&hkdPaintTraverseEx;
1140 VTABLE(g_surface) = newsvt;
1141 isurface_hooked = 1;
1142 logf("ISurface hooked: DrawPrintText slot 22 orig=%p, PaintTraverseEx slot 114 orig=%p", 1139 logf("ISurface hooked: DrawPrintText slot 22 orig=%p, PaintTraverseEx slot 114 orig=%p",
1143 (void *)oDrawPrintText, (void *)oPaintTraverseEx); 1140 (void *)oDrawPrintText, (void *)oPaintTraverseEx);
1144 } 1141 }
@@ -1147,21 +1144,18 @@ static bool hkdCreateMove(void *self, int seq, float frametime, bool active) {
1147} 1144}
1148 1145
1149static void install_hook() { 1146static void install_hook() {
1150 void **vt = VTABLE(g_iface.client); 1147 VtablePatch patches[] = {
1151 constexpr size_t SLOTS = 256; 1148 {21, (void *)&hkdCreateMove, reinterpret_cast<void **>(&oCreateMove)},
1152 void **newvt = vtable_clone(g_iface.client, SLOTS); 1149 {35, (void *)&hkdFrameStageNotify,
1153 if (!newvt) { 1150 reinterpret_cast<void **>(&oFrameStageNotify)},
1154 logf("hook install: client vtable unavailable"); 1151 };
1152 if (!hooks_install_vtable(g_iface.client, 256, patches, 2)) {
1153 logf("hook install: client vtable patch failed");
1155 return; 1154 return;
1156 } 1155 }
1157 oCreateMove = (CreateMoveFn)vt[21]; 1156 logf("hook installed: CreateMove -> %p (orig %p), FrameStageNotify -> %p "
1158 newvt[21] = (void *)&hkdCreateMove; 1157 "(orig %p)",
1159 oFrameStageNotify = (FrameStageFn)vt[35]; 1158 (void *)&hkdCreateMove, (void *)oCreateMove,
1160 newvt[35] = (void *)&hkdFrameStageNotify;
1161 VTABLE(g_iface.client) = newvt;
1162 logf("hook installed: CreateMove %p -> %p (orig %p), FrameStageNotify %p "
1163 "-> %p (orig %p)",
1164 vt[21], (void *)&hkdCreateMove, (void *)oCreateMove, vt[35],
1165 (void *)&hkdFrameStageNotify, (void *)oFrameStageNotify); 1159 (void *)&hkdFrameStageNotify, (void *)oFrameStageNotify);
1166} 1160}
1167 1161
diff --git a/src/hl2h.h b/src/hl2h.h
index f1a1ea0..541b31b 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -11,6 +11,7 @@
11#include <pthread.h> 11#include <pthread.h>
12#include <unistd.h> 12#include <unistd.h>
13 13
14#include "hooks.h"
14#include "overlay.h" 15#include "overlay.h"
15 16
16// Shared float-precision constant. More digits than a float can hold are 17// Shared float-precision constant. More digits than a float can hold are
diff --git a/src/hooks.cpp b/src/hooks.cpp
new file mode 100644
index 0000000..215ecfd
--- /dev/null
+++ b/src/hooks.cpp
@@ -0,0 +1,59 @@
1// hooks.cpp — vtable hook installation and teardown bookkeeping.
2#include "hl2h.h"
3
4#include <mutex>
5#include <vector>
6
7namespace {
8
9struct Installed {
10 void *iface;
11 void **old_vt;
12};
13
14std::mutex g_hook_mu;
15std::vector<Installed> g_installed;
16
17} // namespace
18
19bool hooks_install_vtable(void *iface, size_t slots, const VtablePatch *patches,
20 int count) {
21 if (!iface || !patches || count <= 0)
22 return false;
23 void **old = *(void ***)iface;
24 if (!old)
25 return false;
26 for (int i = 0; i < count; i++) {
27 if (patches[i].slot < 0 || (size_t)patches[i].slot >= slots) {
28 logf("hooks: slot %d out of range (table has %u)", patches[i].slot,
29 (unsigned)slots);
30 return false;
31 }
32 }
33
34 void **clone = vtable_clone(iface, slots);
35 if (!clone)
36 return false;
37 for (int i = 0; i < count; i++) {
38 if (patches[i].original)
39 *patches[i].original = old[patches[i].slot];
40 clone[patches[i].slot] = patches[i].replacement;
41 }
42
43 std::lock_guard<std::mutex> lock(g_hook_mu);
44 *(void ***)iface = clone;
45 g_installed.push_back({iface, old});
46 return true;
47}
48
49void hooks_restore_all() {
50 std::lock_guard<std::mutex> lock(g_hook_mu);
51 for (auto it = g_installed.rbegin(); it != g_installed.rend(); ++it)
52 *(void ***)it->iface = it->old_vt;
53 g_installed.clear();
54}
55
56int hooks_installed_count() {
57 std::lock_guard<std::mutex> lock(g_hook_mu);
58 return (int)g_installed.size();
59}
diff --git a/src/hooks.h b/src/hooks.h
new file mode 100644
index 0000000..084698f
--- /dev/null
+++ b/src/hooks.h
@@ -0,0 +1,28 @@
1#ifndef HL2H_HOOKS_H
2#define HL2H_HOOKS_H
3
4#include <cstddef>
5
6// One patched vtable slot. `original` receives the untouched function pointer
7// (it may be null if the caller does not need it).
8struct VtablePatch {
9 int slot;
10 void *replacement;
11 void **original;
12};
13
14// Clone `iface`'s vtable (preserving the Itanium ABI header), apply all
15// patches, and install the clone on the object. All slots belong to the same
16// interface. Returns false and leaves the object untouched if cloning fails or
17// any slot is out of range.
18bool hooks_install_vtable(void *iface, size_t slots, const VtablePatch *patches,
19 int count);
20
21// Restore every patched interface's original vtable in reverse install order.
22// Intended for tests and shutdown; the cheat normally lives as long as the
23// process does.
24void hooks_restore_all();
25
26int hooks_installed_count();
27
28#endif
diff --git a/src/studio.cpp b/src/studio.cpp
index 8b0a710..275bfba 100644
--- a/src/studio.cpp
+++ b/src/studio.cpp
@@ -597,15 +597,17 @@ void chams_init() {
597 (void *)g_force_override, (void *)g_orig_model_exec); 597 (void *)g_force_override, (void *)g_orig_model_exec);
598 return; 598 return;
599 } 599 }
600 void **nvt = vtable_clone(g_iface.modelrender, 32); 600 VtablePatch patches[] = {
601 if (!nvt) { 601 {1, (void *)&hkdForceOverride,
602 logf("chams: vtable clone failed"); 602 reinterpret_cast<void **>(&g_real_force_override)}, // track forced passes
603 {19, (void *)&hkdModelExecute,
604 reinterpret_cast<void **>(&g_orig_model_exec)},
605 };
606 if (!hooks_install_vtable(g_iface.modelrender, 32, patches, 2)) {
607 logf("chams: vtable patch failed");
603 return; 608 return;
604 } 609 }
605 nvt[1] = (void *)&hkdForceOverride; // track engine-forced passes
606 nvt[19] = (void *)&hkdModelExecute;
607 g_modelrender = g_iface.modelrender; 610 g_modelrender = g_iface.modelrender;
608 *(void ***)g_modelrender = nvt;
609 logf("chams: IVModelRender %p hooked (draw=%p override=%p)", g_modelrender, 611 logf("chams: IVModelRender %p hooked (draw=%p override=%p)", g_modelrender,
610 (void *)g_orig_model_exec, (void *)g_force_override); 612 (void *)g_orig_model_exec, (void *)g_force_override);
611} 613}
@@ -641,24 +643,22 @@ void studio_props_init() {
641 logf("studio: interface has no vtable"); 643 logf("studio: interface has no vtable");
642 return; 644 return;
643 } 645 }
644 void **vt = vtable_clone(sr, 49); 646 VtablePatch patches[] = {
645 if (!vt) { 647 {27, (void *)&hook_set_color,
646 logf("studio: vtable clone failed"); 648 reinterpret_cast<void **>(&g_orig_set_color)},
649 {28, (void *)&hook_set_alpha,
650 reinterpret_cast<void **>(&g_orig_set_alpha)},
651 {29, (void *)&hook_draw_model,
652 reinterpret_cast<void **>(&g_orig_draw_model)},
653 {30, (void *)&hook_draw_static_prop,
654 reinterpret_cast<void **>(&g_orig_draw_static)},
655 {46, (void *)&hook_draw_array,
656 reinterpret_cast<void **>(&g_orig_draw_array)},
657 };
658 if (!hooks_install_vtable(sr, 49, patches, 5)) {
659 logf("studio: vtable patch failed");
647 return; 660 return;
648 } 661 }
649
650 g_orig_set_color = (SetColorFn)vt[27];
651 g_orig_set_alpha = (SetAlphaFn)vt[28];
652 g_orig_draw_model = (DrawModelFn)vt[29];
653 g_orig_draw_static = (DrawStaticFn)vt[30];
654 g_orig_draw_array = (DrawArrayFn)vt[46];
655 vt[27] = (void *)&hook_set_color;
656 vt[28] = (void *)&hook_set_alpha;
657 vt[29] = (void *)&hook_draw_model;
658 vt[30] = (void *)&hook_draw_static_prop;
659 vt[46] = (void *)&hook_draw_array;
660
661 *(void ***)sr = vt;
662 g_installed = true; 662 g_installed = true;
663 logf("studio: IStudioRender %p hooked (color=%p draw=%p static=%p " 663 logf("studio: IStudioRender %p hooked (color=%p draw=%p static=%p "
664 "array=%p)", sr, (void *)g_orig_set_color, (void *)g_orig_draw_model, 664 "array=%p)", sr, (void *)g_orig_set_color, (void *)g_orig_draw_model,