diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-06 16:19:25 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-06 16:19:25 +0100 |
| commit | 9f0724dcd25a06c5674d30d045dc485a341253b1 (patch) | |
| tree | e7587dd03c0b164113d3b879b18d81fccf332c05 | |
| parent | Chams: leave engine-forced material passes alone (diff) | |
| download | hl2h-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.md | 10 | ||||
| -rw-r--r-- | src/cinput.cpp | 22 | ||||
| -rw-r--r-- | src/hl2h.cpp | 44 | ||||
| -rw-r--r-- | src/hl2h.h | 1 | ||||
| -rw-r--r-- | src/hooks.cpp | 59 | ||||
| -rw-r--r-- | src/hooks.h | 28 | ||||
| -rw-r--r-- | src/studio.cpp | 44 |
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 | |||
| 788 | for the corruption described in P2-6; it needs an in-game check because it | 788 | for the corruption described in P2-6; it needs an in-game check because it |
| 789 | relies on the engine always going through the vtable for that call. | 789 | relies 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 | ||
| 794 | every slot, records the originals and installs the clone, with a mutex because | ||
| 795 | installs happen from the setup, game and paint threads. `hooks_restore_all()` | ||
| 796 | puts every interface back, which previously was impossible. Client, CInput, | ||
| 797 | IVModelRender, IStudioRender and ISurface all go through it now. | ||
| 798 | |||
| 791 | ### Known remaining work | 799 | ### Known remaining work |
| 792 | 800 | ||
| 793 | Deliberately deferred, with reasons: | 801 | Deliberately 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, | |||
| 159 | static bool install_input_hook() { | 159 | static 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 | ||
| 1149 | static void install_hook() { | 1146 | static 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 | ||
| @@ -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 | |||
| 7 | namespace { | ||
| 8 | |||
| 9 | struct Installed { | ||
| 10 | void *iface; | ||
| 11 | void **old_vt; | ||
| 12 | }; | ||
| 13 | |||
| 14 | std::mutex g_hook_mu; | ||
| 15 | std::vector<Installed> g_installed; | ||
| 16 | |||
| 17 | } // namespace | ||
| 18 | |||
| 19 | bool 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 | |||
| 49 | void 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 | |||
| 56 | int 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). | ||
| 8 | struct 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. | ||
| 18 | bool 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. | ||
| 24 | void hooks_restore_all(); | ||
| 25 | |||
| 26 | int 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, |
