diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/ISURFACE_STATUS.md | 104 | ||||
| -rw-r--r-- | src/hl2h.cpp | 74 | ||||
| -rw-r--r-- | src/hl2h.h | 18 | ||||
| -rw-r--r-- | src/menu.cpp | 312 |
4 files changed, 387 insertions, 121 deletions
diff --git a/src/ISURFACE_STATUS.md b/src/ISURFACE_STATUS.md new file mode 100644 index 0000000..32a27bb --- /dev/null +++ b/src/ISURFACE_STATUS.md | |||
| @@ -0,0 +1,104 @@ | |||
| 1 | # HL2hake ISurface Drawing — Status Report | ||
| 2 | |||
| 3 | ## Date: 2026-09-12 (final round) | ||
| 4 | |||
| 5 | ## Result: WORKING | ||
| 6 | |||
| 7 | Overlay drawing works in-game and in menus. Draw path: `PaintTraverseEx` | ||
| 8 | hook (slot 114) on the copied ISurface vtable, wrapping our draws in | ||
| 9 | `PushMakeCurrent(panel, false)` / `PopMakeCurrent(panel)` around ISurface | ||
| 10 | slots 6/10/12/17/18/20/22/66/67. | ||
| 11 | |||
| 12 | ## Confirmed ISurface VTable Slots (disassembly-verified, binary=SDK header+4) | ||
| 13 | |||
| 14 | | Slot | Function | Evidence | | ||
| 15 | |------|-------------------------------------------|----------| | ||
| 16 | | 6 | `GetEmbeddedPanel()` | returns VPANEL used with PushMakeCurrent | | ||
| 17 | | 8 | `PushMakeCurrent(VPANEL, bool)` | verified via disassembly | | ||
| 18 | | 9 | `PopMakeCurrent(VPANEL)` | verified via disassembly | | ||
| 19 | | 10 | `DrawSetColor(int r,g,b,a)` | stores RGBA at `this+0x24..0x27` | | ||
| 20 | | 12 | `DrawFilledRect(int x0,y0,x1,y1)` | verified via disassembly | | ||
| 21 | | 17 | `DrawSetTextFont(HFont)` | stores HFont at `this+0x290` | | ||
| 22 | | 18 | `DrawSetTextColor(int r,g,b,a)` | stores RGBA at `this+0x28..0x2b` | | ||
| 23 | | 20 | `DrawSetTextPos(int x,y)` | stores at `this+0x2c/0x30` | | ||
| 24 | | 22 | `DrawPrintText(wchar_t*, int, drawType)` | reads font `0x290`, pos `0x2c/0x30` | | ||
| 25 | | 66 | `CreateFont()` | SDK header 62; empirical (379) | | ||
| 26 | | 67 | `AddGlyphSetToFont(font,name,tall,weight,blur,scanlines,flags,rangeMin,rangeMax)` | SDK header 62+4; disassembly (slot 67 = 0x6a300, passes 9 args to FontManager) | | ||
| 27 | | 87 | `SolveTraverse` | InternalSchemeSettingsTraverse profiling strings @ 0x73240 | | ||
| 28 | | 88 | `PaintTraverse(VPANEL)` | tail-calls `ipanel->vt+0x1c8(panel,false)` @ 0x697e0 | | ||
| 29 | | 114 | `PaintTraverseEx(VPANEL, bool paintPopups)` | IsVisible check `ipanel+0x40`, popup array `this+0x250/0x25c`, "Too many popups!" assert @ 0x6dd46 | | ||
| 30 | |||
| 31 | Slot mapping rule: **binary slot = SDK-header slot + 4** (confirmed via | ||
| 32 | CreateFont 62→66 and PaintTraverseEx 110→114). Vtable has 179 entries; | ||
| 33 | slots 122–148 = second base class `IMatSystemSurface` | ||
| 34 | (AttachToWindow=122 … DrawSetTextureRGBAEx2=148). | ||
| 35 | |||
| 36 | ## Draw-Path Decision | ||
| 37 | |||
| 38 | - **Primary: PaintTraverseEx (slot 114)** — fires every frame for the root panel. | ||
| 39 | - **Fallback kept: DrawPrintText (slot 22)** — fires only when the game draws | ||
| 40 | text (menus); also used as in-draw call site for the game's own text. | ||
| 41 | - `g_in_draw` (hl2h.cpp) prevents recursion; both hooks call `outer_draw()` | ||
| 42 | (menu.cpp) guarded by it. | ||
| 43 | |||
| 44 | ## Fonts (the long fight) | ||
| 45 | |||
| 46 | Font alignment problem: `Tahoma` placeholder fontconfig-substituted to | ||
| 47 | Noto Sans → per-glyph baseline jumps (fallback-substitution glyph metrics). | ||
| 48 | Fixes tried in order: Tahoma → DejaVu Sans (+ANTIALIAS 0x10) → no AA → | ||
| 49 | **winner: "Liberation Sans" 16px, weight 400, flags 0x400 (FONTFLAG_CUSTOM), glyph range pinned 0x20–0xFF**. | ||
| 50 | `FONTFLAG_CUSTOM` disables the Asian fallback-compat glyph path; pinned | ||
| 51 | range forces one font pass → uniform metrics, no jumping letters. | ||
| 52 | |||
| 53 | fc-match findings (system fontconfig): | ||
| 54 | - `Tahoma` → NotoSans-Regular.ttf (substituted — unusable) | ||
| 55 | - `DejaVu Sans` → native, but jumping letters persisted | ||
| 56 | - `Verdana` → Noto Sans (substituted) | ||
| 57 | - `Liberation Sans` → LiberationSans-Regular.ttf (real, installed) ✓ | ||
| 58 | |||
| 59 | ## Surface Internals Observed | ||
| 60 | |||
| 61 | - Paint-stack count: `this+0x270` (nonzero while painting) | ||
| 62 | - Stack array: `this+0x264`, 0x1c-byte entries | ||
| 63 | - Popup array: `this+0x250/0x25c` | ||
| 64 | - Current font: `this+0x290`; text pos: `this+0x2c/0x30` | ||
| 65 | |||
| 66 | ## Menu | ||
| 67 | |||
| 68 | - Tree: HL2hake → Climb/Bunnyhop/ABH, children indented (`g_depth[]`, +12px/level). | ||
| 69 | - Right-aligned state at x=200: `[on]`/`[off]` for Active rows, | ||
| 70 | `[keyname]` for Bind rows, `[press key]` while binding. | ||
| 71 | - Feature gating: `feature_active(int)` (menu.cpp) — active AND | ||
| 72 | (bind==0 or key held). Called from `do_move` (hl2h.cpp) to gate | ||
| 73 | climb (middle-mouse) and bhop (jump) branches. | ||
| 74 | - Config: INI at `~/.hl2hake.ini` (climb/bhop/abh active+bind). | ||
| 75 | |||
| 76 | ## Build / Run | ||
| 77 | |||
| 78 | - `./build.sh`: g++ `-m32 -O2 -fPIC -shared`, links 5 TUs, `-ldl`, | ||
| 79 | `-Wall -Wextra -Werror` (all warnings fatal). | ||
| 80 | - `./play.sh -windowed +map d1_trainstation_01` (Steam runtime wrapper, | ||
| 81 | `LD_PRELOAD=libhl2hake.so`). | ||
| 82 | - Logs: `/tmp/hl2hake.log` (logf), `/tmp/hl2run.log` (stderr). | ||
| 83 | - Probe logging (`hook fired:`, `probe:`) removed 2026-09-12. | ||
| 84 | - `HL2H_DRAW_TEST=1` env: draws red rect + "HL2H DRAW TEST" without | ||
| 85 | opening the menu. | ||
| 86 | |||
| 87 | ## Lessons | ||
| 88 | |||
| 89 | 1. **Never pipe-mask build failures**: `./build.sh | tail` hides g++ rc. | ||
| 90 | Use `./build.sh > /tmp/build.log 2>&1; echo rc=$?`. This masked a | ||
| 91 | dropped `static bool g_in_draw` declaration for a whole session. | ||
| 92 | 2. PIC base recovery in vguimatsurface.so: string refs are | ||
| 93 | `lea -disp(%reg)` after `call thunk; add $const,%reg`; base 0x271000 | ||
| 94 | for slot-87/114 region. | ||
| 95 | 3. Check binary freshness: `strings libhl2hake.so | grep -c <newstr>` | ||
| 96 | — mtime can lie (build silently failed). | ||
| 97 | |||
| 98 | ## File Locations | ||
| 99 | |||
| 100 | - Project: `/home/salmon/Documents/rust/hl2h/` | ||
| 101 | - Hook + feature logic: `src/hl2h.cpp` | ||
| 102 | - Menu + drawing + config + input: `src/menu.cpp` | ||
| 103 | - Shared API: `src/hl2h.h` | ||
| 104 | - SDK headers: `source-sdk-2013/src/public/vgui/` | ||
diff --git a/src/hl2h.cpp b/src/hl2h.cpp index 0261ff2..a4a1a88 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp | |||
| @@ -117,9 +117,20 @@ static void *entitylist_get(int idx) { | |||
| 117 | 117 | ||
| 118 | 118 | ||
| 119 | static void do_move(int seq) { | 119 | static void do_move(int seq) { |
| 120 | static bool g_echo_sent = false; | ||
| 120 | if (!g_input || !g_net.ok()) | 121 | if (!g_input || !g_net.ok()) |
| 121 | return; | 122 | return; |
| 122 | 123 | ||
| 124 | if (!g_echo_sent) { | ||
| 125 | g_echo_sent = true; | ||
| 126 | if (g_conmsg) { | ||
| 127 | g_conmsg("[hl2hake] succesfully loaded\n"); | ||
| 128 | logf("console echo sent (first CreateMove)"); | ||
| 129 | } else { | ||
| 130 | logf("g_conmsg is NULL — in-game console output unavailable"); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 123 | int localIdx = engine_get_local_player(); | 134 | int localIdx = engine_get_local_player(); |
| 124 | if (localIdx <= 0 || localIdx > 64) | 135 | if (localIdx <= 0 || localIdx > 64) |
| 125 | return; | 136 | return; |
| @@ -137,17 +148,16 @@ static void do_move(int seq) { | |||
| 137 | return; | 148 | return; |
| 138 | 149 | ||
| 139 | uintptr_t e = (uintptr_t)ent; | 150 | uintptr_t e = (uintptr_t)ent; |
| 140 | uint8_t movetype = *(uint8_t *)(e + g_net.movetype); | ||
| 141 | int flags = *(int *)(e + g_net.flags); | 151 | int flags = *(int *)(e + g_net.flags); |
| 142 | bool onground = (flags & FL_ONGROUND) != 0; | 152 | bool onground = (flags & FL_ONGROUND) != 0; |
| 143 | bool inwater = (flags & FL_INWATER) != 0; | 153 | bool inwater = (flags & FL_INWATER) != 0; |
| 144 | 154 | ||
| 145 | if (movetype == MOVETYPE_NOCLIP || movetype == MOVETYPE_LADDER) | 155 | bool climb_on = feature_active(FEAT_CLIMB); // bind held (or middle mouse when unbound) |
| 146 | return; | 156 | bool bhop_on = feature_active(FEAT_BHOP); |
| 147 | 157 | ||
| 148 | static bool lastground = false; | 158 | static bool lastground = false; |
| 149 | 159 | ||
| 150 | if (middle_mouse_down()) { | 160 | if (climb_on) { |
| 151 | if (cmd->command_number % 2) { | 161 | if (cmd->command_number % 2) { |
| 152 | cmd->buttons &= ~IN_JUMP; | 162 | cmd->buttons &= ~IN_JUMP; |
| 153 | cmd->buttons |= IN_USE; | 163 | cmd->buttons |= IN_USE; |
| @@ -156,7 +166,7 @@ static void do_move(int seq) { | |||
| 156 | cmd->buttons |= IN_JUMP; | 166 | cmd->buttons |= IN_JUMP; |
| 157 | } | 167 | } |
| 158 | note_modified(cmd, flags, (float *)(e + g_net.velocity)); | 168 | note_modified(cmd, flags, (float *)(e + g_net.velocity)); |
| 159 | } else if (cmd->buttons & IN_JUMP) { | 169 | } else if (bhop_on && (cmd->buttons & IN_JUMP)) { |
| 160 | if (inwater && (cmd->buttons & IN_DUCK)) { | 170 | if (inwater && (cmd->buttons & IN_DUCK)) { |
| 161 | lastground = onground; | 171 | lastground = onground; |
| 162 | return; | 172 | return; |
| @@ -193,9 +203,63 @@ static void do_move(int seq) { | |||
| 193 | using CreateMoveFn = bool (*)(void *, int, float, bool); | 203 | using CreateMoveFn = bool (*)(void *, int, float, bool); |
| 194 | static CreateMoveFn oCreateMove = nullptr; | 204 | static CreateMoveFn oCreateMove = nullptr; |
| 195 | 205 | ||
| 206 | using DrawPrintTextFn = void (*)(void *self, const wchar_t *text, int len, int drawType); | ||
| 207 | static DrawPrintTextFn oDrawPrintText = nullptr; | ||
| 208 | using PaintTraverseExFn = void (*)(void *, long, bool); | ||
| 209 | static PaintTraverseExFn oPaintTraverseEx = nullptr; | ||
| 210 | static bool g_in_draw = false; | ||
| 211 | |||
| 212 | // Hooked ISurface slot 22. The game calls this from a valid paint context; | ||
| 213 | // our overlay draws are submitted through outer_draw() (menu.cpp), which | ||
| 214 | // wraps them in PushMakeCurrent/PopMakeCurrent with the embedded panel. | ||
| 215 | static void hkdDrawPrintText(void *self, const wchar_t *text, int len, int drawType) { | ||
| 216 | if (g_in_draw) { | ||
| 217 | oDrawPrintText(self, text, len, drawType); | ||
| 218 | return; | ||
| 219 | } | ||
| 220 | g_in_draw = true; | ||
| 221 | oDrawPrintText(self, text, len, drawType); | ||
| 222 | // fallback trigger: fires when the game draws text (menus). Primary draw | ||
| 223 | // path is the PaintTraverseEx hook (slot 114), which fires every frame. | ||
| 224 | outer_draw(); | ||
| 225 | g_in_draw = false; | ||
| 226 | } | ||
| 227 | |||
| 228 | // PaintTraverseEx (slot 114, verified: IsVisible check + popups loop + "Too many | ||
| 229 | // popups" assert). Fires every frame for the root panel — unlike DrawPrintText | ||
| 230 | // (slot 22) which only fires when text is drawn, so it never fires in-world. | ||
| 231 | static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) { | ||
| 232 | using Fn = void (*)(void *, long, bool); | ||
| 233 | ((Fn)oPaintTraverseEx)(self, panel, paintPopups); | ||
| 234 | if (!g_in_draw) { | ||
| 235 | g_in_draw = true; | ||
| 236 | outer_draw(); | ||
| 237 | g_in_draw = false; | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 196 | static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { | 241 | static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { |
| 197 | bool ret = oCreateMove(self, seq, frametime, active); | 242 | bool ret = oCreateMove(self, seq, frametime, active); |
| 198 | do_move(seq); | 243 | do_move(seq); |
| 244 | |||
| 245 | // Hook ISurface once: PaintTraverseEx (slot 114) fires every frame; keep | ||
| 246 | // DrawPrintText (slot 22) hooked as a fallback text-draw trigger. | ||
| 247 | static int isurface_hooked = 0; | ||
| 248 | if (!isurface_hooked && g_surface) { | ||
| 249 | void **svt = VTABLE(g_surface); | ||
| 250 | constexpr size_t VT_SIZE = 256; | ||
| 251 | void **newsvt = new void *[VT_SIZE]; | ||
| 252 | memcpy(newsvt, svt, VT_SIZE * sizeof(void *)); | ||
| 253 | oDrawPrintText = (DrawPrintTextFn)svt[22]; | ||
| 254 | newsvt[22] = (void *)&hkdDrawPrintText; | ||
| 255 | oPaintTraverseEx = (PaintTraverseExFn)svt[114]; | ||
| 256 | newsvt[114] = (void *)&hkdPaintTraverseEx; | ||
| 257 | VTABLE(g_surface) = newsvt; | ||
| 258 | isurface_hooked = 1; | ||
| 259 | logf("ISurface hooked: DrawPrintText slot 22 orig=%p, PaintTraverseEx slot 114 orig=%p", | ||
| 260 | (void *)oDrawPrintText, (void *)oPaintTraverseEx); | ||
| 261 | } | ||
| 262 | |||
| 199 | return ret; | 263 | return ret; |
| 200 | } | 264 | } |
| 201 | 265 | ||
| @@ -75,6 +75,12 @@ extern NetVars g_net; | |||
| 75 | extern uintptr_t g_input; | 75 | extern uintptr_t g_input; |
| 76 | 76 | ||
| 77 | // --------------------------------------------------------------------------- | 77 | // --------------------------------------------------------------------------- |
| 78 | // console output | ||
| 79 | // --------------------------------------------------------------------------- | ||
| 80 | |||
| 81 | extern void (*g_conmsg)(const char *, ...); | ||
| 82 | |||
| 83 | // --------------------------------------------------------------------------- | ||
| 78 | // CUserCmd | 84 | // CUserCmd |
| 79 | // --------------------------------------------------------------------------- | 85 | // --------------------------------------------------------------------------- |
| 80 | 86 | ||
| @@ -118,6 +124,8 @@ float getfov(const QAngle &my_view, const QAngle &aim_ang); | |||
| 118 | // cross-file declarations | 124 | // cross-file declarations |
| 119 | // --------------------------------------------------------------------------- | 125 | // --------------------------------------------------------------------------- |
| 120 | 126 | ||
| 127 | // feature ids shared by menu (feature_active) and hl2h.cpp (do_move) | ||
| 128 | enum FeatureId { FEAT_CLIMB = 0, FEAT_BHOP = 1, FEAT_ABH = 2 }; | ||
| 121 | void note_modified(const CUserCmd *cmd, int flags, const float *vel); | 129 | void note_modified(const CUserCmd *cmd, int flags, const float *vel); |
| 122 | CUserCmd *get_usercmd(int seq); | 130 | CUserCmd *get_usercmd(int seq); |
| 123 | void fix_verified_crc(int seq, uint32_t crc); | 131 | void fix_verified_crc(int seq, uint32_t crc); |
| @@ -129,5 +137,13 @@ void install_menu(); | |||
| 129 | void init_conmsg(); | 137 | void init_conmsg(); |
| 130 | void init_x11(); | 138 | void init_x11(); |
| 131 | bool middle_mouse_down(); | 139 | bool middle_mouse_down(); |
| 132 | 140 | bool feature_active(int feature); | |
| 141 | void outer_draw(); | ||
| 142 | // raw ISurface slot calls used by the PaintTraverseEx hook (hl2h.cpp) | ||
| 143 | using SurfFn = void (*)(void *, ...); | ||
| 144 | long surf_call_slot(void *surf, int slot, long a1 = 0, long a2 = 0); | ||
| 145 | |||
| 146 | // ISurface pointer (defined in menu.cpp, used for drawing tests) | ||
| 147 | struct ISurface; | ||
| 148 | extern ISurface *g_surface; | ||
| 133 | #endif | 149 | #endif |
diff --git a/src/menu.cpp b/src/menu.cpp index 91a0948..c62db47 100644 --- a/src/menu.cpp +++ b/src/menu.cpp | |||
| @@ -1,7 +1,8 @@ | |||
| 1 | // menu.cpp — menu state, rendering, input, config, paint hook, X11 mouse | 1 | // menu.cpp — menu state, rendering, input, config, paint hook, X11 mouse |
| 2 | #include "hl2h.h" | 2 | #include "hl2h.h" |
| 3 | #include <cstdarg> | 3 | #include <cmath> |
| 4 | #include <cstdio> | 4 | #include <cstdio> |
| 5 | #include <cstdlib> | ||
| 5 | #include <cstring> | 6 | #include <cstring> |
| 6 | 7 | ||
| 7 | // --------------------------------------------------------------------------- | 8 | // --------------------------------------------------------------------------- |
| @@ -104,14 +105,14 @@ static FeatureState g_bhop{true, 0}; | |||
| 104 | static FeatureState g_abh{true, 0}; | 105 | static FeatureState g_abh{true, 0}; |
| 105 | 106 | ||
| 106 | // tree | 107 | // tree |
| 107 | static MenuNode g_root{NODE_ROOT, "HL2hake", true, 0, {}}; | 108 | static MenuNode g_root{NODE_ROOT, "HL2hake", true, 0, {}, nullptr, nullptr, nullptr}; |
| 108 | static MenuNode g_climb_feat{NODE_FEATURE, "Climb", true, 2, {}}; | 109 | static MenuNode g_climb_feat{NODE_FEATURE, "Climb", true, 2, {}, nullptr, nullptr, nullptr}; |
| 109 | static MenuNode g_climb_active{NODE_TOGGLE, "Active", false, 0, {}, &g_climb.active}; | 110 | static MenuNode g_climb_active{NODE_TOGGLE, "Active", false, 0, {}, &g_climb.active, nullptr, nullptr}; |
| 110 | static MenuNode g_climb_bind{NODE_BIND, "Bind", false, 0, {}, nullptr, &g_climb.bind, "None"}; | 111 | static MenuNode g_climb_bind{NODE_BIND, "Bind", false, 0, {}, nullptr, &g_climb.bind, "None"}; |
| 111 | static MenuNode g_bhop_feat{NODE_FEATURE, "Bunnyhop", true, 1, {}}; | 112 | static MenuNode g_bhop_feat{NODE_FEATURE, "Bunnyhop", true, 1, {}, nullptr, nullptr, nullptr}; |
| 112 | static MenuNode g_bhop_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.active}; | 113 | static MenuNode g_bhop_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.active, nullptr, nullptr}; |
| 113 | static MenuNode g_abh_feat{NODE_FEATURE, "ABH", true, 1, {}}; | 114 | static MenuNode g_abh_feat{NODE_FEATURE, "ABH", true, 1, {}, nullptr, nullptr, nullptr}; |
| 114 | static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active}; | 115 | static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active, nullptr, nullptr}; |
| 115 | 116 | ||
| 116 | static MenuNode *g_visible[16]; // flattened visible nodes for arrow navigation | 117 | static MenuNode *g_visible[16]; // flattened visible nodes for arrow navigation |
| 117 | static int g_visible_count = 0; | 118 | static int g_visible_count = 0; |
| @@ -132,6 +133,8 @@ static void build_tree() { | |||
| 132 | g_root.child_count = 3; | 133 | g_root.child_count = 3; |
| 133 | } | 134 | } |
| 134 | 135 | ||
| 136 | static int g_depth[16]; // depth of each visible row (for indentation) | ||
| 137 | |||
| 135 | static void flatten(MenuNode *node, int depth) { | 138 | static void flatten(MenuNode *node, int depth) { |
| 136 | if (g_visible_count >= 16) | 139 | if (g_visible_count >= 16) |
| 137 | return; | 140 | return; |
| @@ -139,6 +142,7 @@ static void flatten(MenuNode *node, int depth) { | |||
| 139 | : (node->child_count > 0) ? NODE_FEATURE | 142 | : (node->child_count > 0) ? NODE_FEATURE |
| 140 | : (node->active_ptr) ? NODE_TOGGLE | 143 | : (node->active_ptr) ? NODE_TOGGLE |
| 141 | : NODE_BIND; | 144 | : NODE_BIND; |
| 145 | g_depth[g_visible_count] = depth; | ||
| 142 | g_visible[g_visible_count++] = node; | 146 | g_visible[g_visible_count++] = node; |
| 143 | if (node->expanded) { | 147 | if (node->expanded) { |
| 144 | for (int i = 0; i < node->child_count; i++) | 148 | for (int i = 0; i < node->child_count; i++) |
| @@ -297,11 +301,28 @@ static bool key_pressed(int sc) { | |||
| 297 | return sc >= 0 && sc < 512 && g_prev_key_state[sc]; | 301 | return sc >= 0 && sc < 512 && g_prev_key_state[sc]; |
| 298 | } | 302 | } |
| 299 | 303 | ||
| 304 | // feature gate queried from hl2h.cpp do_move(): feature must be Active AND | ||
| 305 | // the activation condition met. Climb activates on its bind key held (falls | ||
| 306 | // back to the historic middle-mouse behavior when unbound); other features | ||
| 307 | // activate on their bind key held, or are always-on when unbound. | ||
| 308 | bool feature_active(int feature) { | ||
| 309 | static FeatureState *feats[3] = {&g_climb, &g_bhop, &g_abh}; | ||
| 310 | if (feature < 0 || feature > 2) | ||
| 311 | return false; | ||
| 312 | FeatureState *f = feats[feature]; | ||
| 313 | if (!f->active) | ||
| 314 | return false; | ||
| 315 | if (f->bind == 0) | ||
| 316 | return feature == FEAT_CLIMB ? middle_mouse_down() : true; | ||
| 317 | return key_held(f->bind); | ||
| 318 | } | ||
| 319 | |||
| 300 | static void menu_handle_input() { | 320 | static void menu_handle_input() { |
| 301 | poll_keyboard(); | 321 | poll_keyboard(); |
| 302 | 322 | ||
| 303 | if (!g_menu_open) { | 323 | if (!g_menu_open) { |
| 304 | if (key_pressed(SCAN_INSERT)) { | 324 | if (key_pressed(SCAN_INSERT)) { |
| 325 | std::logf("insert pressed"); | ||
| 305 | g_menu_open = true; | 326 | g_menu_open = true; |
| 306 | g_bind_mode = false; | 327 | g_bind_mode = false; |
| 307 | rebuild_visible(); | 328 | rebuild_visible(); |
| @@ -384,29 +405,53 @@ static void menu_handle_input() { | |||
| 384 | // --------------------------------------------------------------------------- | 405 | // --------------------------------------------------------------------------- |
| 385 | 406 | ||
| 386 | // VGUI surface interface | 407 | // VGUI surface interface |
| 387 | using CreateInterfaceFn = void *(*)(const char *, int *); | ||
| 388 | struct ISurface; | 408 | struct ISurface; |
| 389 | |||
| 390 | // VGUI font handle (opaque) | ||
| 391 | using HFont = unsigned long; | 409 | using HFont = unsigned long; |
| 392 | 410 | ||
| 393 | // function pointers resolved at init | 411 | ISurface *g_surface = nullptr; |
| 394 | static ISurface *g_surface = nullptr; | ||
| 395 | static HFont g_font = 0; | 412 | static HFont g_font = 0; |
| 396 | 413 | ||
| 397 | // surface vtable indices for 2007-era VGUI_SURFACE001 | 414 | // VGUI_Surface030 vtable slots (confirmed via binary disassembly of vguimatsurface.so): |
| 398 | // verified: slot 16 = DrawSetTextColor, slot 17 = DrawSetTextFont, | 415 | // 8 = PushMakeCurrent(VPANEL, bool useInsets) — reads args at 0xc/0x10(%ebp) |
| 399 | // slot 19 = DrawSetTextPos, slot 22 = DrawPrintText, | 416 | // 9 = PopMakeCurrent(VPANEL) — pops the make-current stack at this+0x264/0x270 |
| 400 | // slot 23 = DrawGetTextSize, slot 31 = CreateNewFont, slot 32 = SetFontGlyphSet | 417 | // 10 = DrawSetColor(int r, int g, int b, int a) — stores rgba at this+0x24 |
| 401 | // These are from the standard VGUI surface vtable layout; will be verified | 418 | // 12 = DrawFilledRect(int x0, int y0, int x1, int y1) — alpha check at this+0x27 |
| 402 | // against runtime if they fail. | 419 | // 17 = DrawSetTextFont(HFont), 18 = DrawSetTextColor(int r,g,b,a) |
| 403 | static constexpr int SURF_DrawSetTextColor = 16; | 420 | // 20 = DrawSetTextPos(int x, int y), 22 = DrawPrintText(wchar_t*, len, drawType) |
| 421 | // 66 = CreateFont(), 67 = AddGlyphSetToFont(font, name, tall, weight, | ||
| 422 | // blur, scanlines, flags, rangeMin, rangeMax) | ||
| 423 | static constexpr int SURF_GetEmbeddedPanel = 6; | ||
| 424 | static constexpr int SURF_PushMakeCurrent = 8; | ||
| 425 | static constexpr int SURF_PopMakeCurrent = 9; | ||
| 426 | static constexpr int SURF_DrawSetColor = 10; | ||
| 427 | static constexpr int SURF_DrawFilledRect = 12; | ||
| 404 | static constexpr int SURF_DrawSetTextFont = 17; | 428 | static constexpr int SURF_DrawSetTextFont = 17; |
| 405 | static constexpr int SURF_DrawSetTextPos = 19; | 429 | static constexpr int SURF_DrawSetTextColor = 18; |
| 430 | static constexpr int SURF_DrawSetTextPos = 20; | ||
| 406 | static constexpr int SURF_DrawPrintText = 22; | 431 | static constexpr int SURF_DrawPrintText = 22; |
| 407 | static constexpr int SURF_CreateNewFont = 31; | 432 | static constexpr int SURF_CreateFont = 66; |
| 408 | static constexpr int SURF_SetFontGlyphSet = 32; | 433 | static constexpr int SURF_AddGlyphSetToFont = 67; |
| 409 | 434 | ||
| 435 | static int surf_get_embedded_panel() { | ||
| 436 | using Fn = int (*)(void *); | ||
| 437 | return vfunc<Fn>(g_surface, SURF_GetEmbeddedPanel)(g_surface); | ||
| 438 | } | ||
| 439 | static void surf_push_make_current(int panel, bool use_insets) { | ||
| 440 | using Fn = void (*)(void *, int, bool); | ||
| 441 | vfunc<Fn>(g_surface, SURF_PushMakeCurrent)(g_surface, panel, use_insets); | ||
| 442 | } | ||
| 443 | static void surf_pop_make_current(int panel) { | ||
| 444 | using Fn = void (*)(void *, int); | ||
| 445 | vfunc<Fn>(g_surface, SURF_PopMakeCurrent)(g_surface, panel); | ||
| 446 | } | ||
| 447 | static void surf_draw_set_color(int r, int g, int b, int a) { | ||
| 448 | using Fn = void (*)(void *, int, int, int, int); | ||
| 449 | vfunc<Fn>(g_surface, SURF_DrawSetColor)(g_surface, r, g, b, a); | ||
| 450 | } | ||
| 451 | static void surf_draw_filled_rect(int x0, int y0, int x1, int y1) { | ||
| 452 | using Fn = void (*)(void *, int, int, int, int); | ||
| 453 | vfunc<Fn>(g_surface, SURF_DrawFilledRect)(g_surface, x0, y0, x1, y1); | ||
| 454 | } | ||
| 410 | static void surf_draw_set_text_color(int r, int g, int b, int a) { | 455 | static void surf_draw_set_text_color(int r, int g, int b, int a) { |
| 411 | using Fn = void (*)(void *, int, int, int, int); | 456 | using Fn = void (*)(void *, int, int, int, int); |
| 412 | vfunc<Fn>(g_surface, SURF_DrawSetTextColor)(g_surface, r, g, b, a); | 457 | vfunc<Fn>(g_surface, SURF_DrawSetTextColor)(g_surface, r, g, b, a); |
| @@ -420,20 +465,23 @@ static void surf_draw_set_text_pos(int x, int y) { | |||
| 420 | vfunc<Fn>(g_surface, SURF_DrawSetTextPos)(g_surface, x, y); | 465 | vfunc<Fn>(g_surface, SURF_DrawSetTextPos)(g_surface, x, y); |
| 421 | } | 466 | } |
| 422 | static void surf_draw_print_text(const wchar_t *text, int count) { | 467 | static void surf_draw_print_text(const wchar_t *text, int count) { |
| 423 | using Fn = void (*)(void *, const wchar_t *, int); | 468 | using Fn = void (*)(void *, const wchar_t *, int, int); |
| 424 | vfunc<Fn>(g_surface, SURF_DrawPrintText)(g_surface, text, count); | 469 | vfunc<Fn>(g_surface, SURF_DrawPrintText)(g_surface, text, count, 0); // 0 = FONT_DRAW_DEFAULT |
| 425 | } | 470 | } |
| 426 | static HFont surf_create_font() { | 471 | static HFont surf_create_font() { |
| 427 | using Fn = HFont (*)(void *); | 472 | using Fn = HFont (*)(void *); |
| 428 | return vfunc<Fn>(g_surface, SURF_CreateNewFont)(g_surface); | 473 | return vfunc<Fn>(g_surface, SURF_CreateFont)(g_surface); |
| 429 | } | 474 | } |
| 430 | static void surf_set_font_glyph_set(HFont f, const char *name, int tall, | 475 | static bool surf_set_font_glyph_set(HFont f, const char *name, int tall, |
| 431 | int weight, int blur, int scanlines, | 476 | int weight, int blur, int scanlines, |
| 432 | int flags) { | 477 | int flags, int range_min = 0, |
| 433 | using Fn = void (*)(void *, HFont, const char *, int, int, int, int, int, int, | 478 | int range_max = 0) { |
| 479 | using Fn = bool (*)(void *, HFont, const char *, int, int, int, int, int, int, | ||
| 434 | int); | 480 | int); |
| 435 | vfunc<Fn>(g_surface, SURF_SetFontGlyphSet)(g_surface, f, name, tall, weight, | 481 | return vfunc<Fn>(g_surface, SURF_AddGlyphSetToFont)(g_surface, f, name, tall, |
| 436 | blur, scanlines, flags, 0, 0); | 482 | weight, blur, scanlines, |
| 483 | flags, range_min, | ||
| 484 | range_max); | ||
| 437 | } | 485 | } |
| 438 | 486 | ||
| 439 | // wide string conversion for DrawPrintText | 487 | // wide string conversion for DrawPrintText |
| @@ -442,114 +490,148 @@ static void draw_text(int x, int y, const char *text) { | |||
| 442 | mbstowcs(wtext, text, 255); | 490 | mbstowcs(wtext, text, 255); |
| 443 | wtext[255] = 0; | 491 | wtext[255] = 0; |
| 444 | int len = (int)wcslen(wtext); | 492 | int len = (int)wcslen(wtext); |
| 493 | surf_draw_set_text_font(g_font); | ||
| 494 | surf_draw_set_text_color(255, 255, 255, 255); | ||
| 445 | surf_draw_set_text_pos(x, y); | 495 | surf_draw_set_text_pos(x, y); |
| 446 | surf_draw_print_text(wtext, len); | 496 | surf_draw_print_text(wtext, len); |
| 447 | } | 497 | } |
| 448 | 498 | ||
| 449 | // --------------------------------------------------------------------------- | 499 | // Draw overlay. MUST be called from inside the ISurface paint path |
| 450 | // paint hook — draws menu overlay every frame | 500 | // (see hkdDrawPrintText hook in hl2h.cpp): we wrap our draws in |
| 451 | // --------------------------------------------------------------------------- | 501 | // PushMakeCurrent/PopMakeCurrent so they are submitted with a clean clip. |
| 452 | 502 | void outer_draw() { | |
| 453 | using PaintFn = void (*)(void *, int); | 503 | menu_handle_input(); |
| 454 | static PaintFn oPaint = nullptr; | 504 | // HL2H_DRAW_TEST=1 draws a probe rect without needing the menu open |
| 455 | 505 | static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr; | |
| 456 | static void hkdPaint(void *self, int mode) { | 506 | if (!g_surface || (!g_menu_open && !draw_test)) |
| 457 | oPaint(self, mode); | 507 | return; |
| 458 | 508 | if (!g_font) { | |
| 459 | // init font on first paint | ||
| 460 | static bool font_inited = false; | ||
| 461 | if (!font_inited) { | ||
| 462 | g_font = surf_create_font(); | 509 | g_font = surf_create_font(); |
| 463 | if (g_font) { | 510 | if (!g_font) { |
| 464 | surf_set_font_glyph_set(g_font, "Consolas", 14, 400, 0, 0, 0); | 511 | logf("outer_draw: CreateFont failed"); |
| 512 | return; | ||
| 513 | } | ||
| 514 | // FONTFLAG_CUSTOM only; Liberation Sans is an installed real TTF (Verdana | ||
| 515 | // substitutes to Noto). Raw glyphs, ASCII range pinned. | ||
| 516 | if (!surf_set_font_glyph_set(g_font, "Liberation Sans", 16, 400, 0, 0, | ||
| 517 | 0x400, 0x20, 0xFF)) { | ||
| 518 | logf("outer_draw: SetFontGlyphSet failed"); | ||
| 519 | g_font = 0; | ||
| 520 | return; | ||
| 465 | } | 521 | } |
| 466 | font_inited = true; | ||
| 467 | } | 522 | } |
| 468 | if (!g_font) | 523 | int panel = surf_get_embedded_panel(); |
| 524 | if (!panel) | ||
| 469 | return; | 525 | return; |
| 470 | 526 | surf_push_make_current(panel, false); | |
| 471 | menu_handle_input(); | 527 | if (draw_test) { |
| 472 | 528 | surf_draw_set_color(255, 0, 0, 255); | |
| 473 | if (!g_menu_open) | 529 | surf_draw_filled_rect(20, 20, 180, 60); |
| 530 | surf_draw_set_color(255, 255, 255, 255); | ||
| 531 | draw_text(28, 36, "HL2H DRAW TEST"); | ||
| 532 | } | ||
| 533 | if (!g_menu_open) { | ||
| 534 | surf_pop_make_current(panel); | ||
| 474 | return; | 535 | return; |
| 475 | 536 | } | |
| 476 | surf_draw_set_text_font(g_font); | ||
| 477 | |||
| 478 | int y = 10; | ||
| 479 | rebuild_visible(); | ||
| 480 | for (int i = 0; i < g_visible_count; i++) { | 537 | for (int i = 0; i < g_visible_count; i++) { |
| 481 | MenuNode *n = g_visible[i]; | 538 | MenuNode *n = g_visible[i]; |
| 482 | int indent = 0; | 539 | char line[96]; |
| 483 | 540 | int x = 12 + g_depth[i] * 12; | |
| 484 | // count depth by counting how many parents are visible before this node | 541 | snprintf(line, sizeof(line), "%s %s", |
| 485 | // (simple: first node = depth 0, others = depth 1 if label is Active/Bind) | 542 | (i == g_selected) ? ">" : (n->child_count ? "-" : " "), n->label); |
| 486 | if (n->type == NODE_TOGGLE || n->type == NODE_BIND) | 543 | draw_text(x, 10 + i * 20, line); |
| 487 | indent = 20; | 544 | // right-aligned state |
| 488 | |||
| 489 | char buf[256]; | ||
| 490 | if (n->type == NODE_TOGGLE) { | 545 | if (n->type == NODE_TOGGLE) { |
| 491 | snprintf(buf, sizeof(buf), "%s: %s", n->label, | 546 | draw_text(200, 10 + i * 20, *n->active_ptr ? "[on]" : "[off]"); |
| 492 | *n->active_ptr ? "ON" : "OFF"); | ||
| 493 | } else if (n->type == NODE_BIND) { | 547 | } else if (n->type == NODE_BIND) { |
| 494 | snprintf(buf, sizeof(buf), "%s: %s", n->label, | 548 | if (g_bind_mode && i == g_selected) |
| 495 | scancode_name(*n->bind_ptr)); | 549 | draw_text(200, 10 + i * 20, "[press key]"); |
| 496 | if (g_bind_mode && g_selected == i) | 550 | else { |
| 497 | snprintf(buf, sizeof(buf), "%s: ...", n->label); | 551 | /* stable name for current bind */ |
| 498 | } else { | 552 | static char bn[3][32]; |
| 499 | snprintf(buf, sizeof(buf), "%s", n->label); | 553 | static FeatureState *feats[] = {&g_climb, &g_bhop, &g_abh}; |
| 500 | } | 554 | int fi = 0; |
| 501 | 555 | for (int k = 0; k < 3; k++) | |
| 502 | if (i == g_selected) { | 556 | if (n->bind_ptr == &feats[k]->bind) |
| 503 | surf_draw_set_text_color(255, 255, 0, 255); // yellow highlight | 557 | fi = k; |
| 504 | } else if (n->type == NODE_TOGGLE) { | 558 | snprintf(bn[fi], 32, "[%s]", scancode_name(*n->bind_ptr)); |
| 505 | surf_draw_set_text_color(*n->active_ptr ? 0 : 255, 255, 0, 255); | 559 | draw_text(200, 10 + i * 20, bn[fi]); |
| 506 | } else { | 560 | } |
| 507 | surf_draw_set_text_color(255, 255, 255, 255); | ||
| 508 | } | 561 | } |
| 509 | |||
| 510 | draw_text(10 + indent, y, buf); | ||
| 511 | y += 18; | ||
| 512 | } | 562 | } |
| 563 | surf_pop_make_current(panel); | ||
| 564 | } | ||
| 513 | 565 | ||
| 514 | // reset text color | 566 | // Call an arbitrary ISurface vtable slot with up to 2 long args (cdecl thiscall |
| 515 | surf_draw_set_text_color(255, 255, 255, 255); | 567 | // on i386: this first, rest on stack). |
| 568 | long surf_call_slot(void *surf, int slot, long a1, long a2) { | ||
| 569 | void **vt = *(void ***)surf; | ||
| 570 | using Fn = long (*)(void *, long, long); | ||
| 571 | return ((Fn)vt[slot])(surf, a1, a2); | ||
| 516 | } | 572 | } |
| 517 | 573 | ||
| 518 | // --------------------------------------------------------------------------- | 574 | // --------------------------------------------------------------------------- |
| 519 | // paint hook installation (called from setup_thread) | 575 | // ISurface discovery — try known modules and all loaded .so files |
| 520 | // --------------------------------------------------------------------------- | 576 | // --------------------------------------------------------------------------- |
| 521 | 577 | ||
| 522 | // VEngineVGui001 vtable indices for this build | 578 | static ISurface *find_surface_interface() { |
| 523 | // (Paint is typically slot 6 — will be verified at runtime; if wrong, | 579 | static const char *known[] = {"engine.so", "client.so", nullptr}; |
| 524 | // we crash and can adjust. Common Source 2007 layout.) | 580 | static const char *iface_names[] = { |
| 525 | static constexpr int VGUI_Paint_SLOT = 6; | 581 | "VGUI_Surface030", "VGUI_Surface001", |
| 526 | 582 | "MatSystemSurface008", "MatSystemSurface001", | |
| 527 | static void install_paint_hook() { | 583 | "VGUI_Surface002", "VGUI_SURFACE001", |
| 528 | // get IEngineVGui from engine | 584 | nullptr, |
| 529 | void *eh = dlopen(g_engine.path.c_str(), | 585 | }; |
| 530 | RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | 586 | // try known modules |
| 531 | if (!eh) | 587 | for (const char **p = known; *p; p++) { |
| 532 | return; | 588 | void *h = dlopen(*p, RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); |
| 533 | auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); | 589 | if (!h) continue; |
| 534 | if (!ef) | 590 | auto fn = (CreateInterfaceFn)dlsym(h, "CreateInterface"); |
| 535 | return; | 591 | if (!fn) continue; |
| 536 | void *evgui = ef("VEngineVGui001", nullptr); | 592 | for (const char **n = iface_names; *n; n++) { |
| 537 | if (!evgui) { | 593 | int ret = 0; |
| 538 | logf("VEngineVGui001 not found"); | 594 | ISurface *s = (ISurface *)fn(*n, &ret); |
| 539 | return; | 595 | if (s) { logf("ISurface found: %s via %s (ret=%d)", *n, *p, ret); return s; } |
| 596 | } | ||
| 540 | } | 597 | } |
| 541 | 598 | // 2) scan /proc/self/maps for every loaded .so, try CreateInterface on each | |
| 542 | // hook Paint | 599 | FILE *maps = fopen("/proc/self/maps", "r"); |
| 543 | void **vt = VTABLE(evgui); | 600 | if (!maps) return nullptr; |
| 544 | oPaint = (PaintFn)vt[VGUI_Paint_SLOT]; | 601 | char line[1024]; |
| 545 | vt[VGUI_Paint_SLOT] = (void *)&hkdPaint; | 602 | while (fgets(line, sizeof(line), maps)) { |
| 546 | logf("paint hook installed: Paint %p -> %p", (void *)oPaint, | 603 | char *p = strchr(line, '/'); |
| 547 | (void *)&hkdPaint); | 604 | if (!p) continue; |
| 605 | size_t pl = strlen(p); | ||
| 606 | while (pl && (p[pl-1] == '\n' || p[pl-1] == '\r')) p[--pl] = 0; | ||
| 607 | if (!strstr(p, ".so")) continue; | ||
| 608 | void *h = dlopen(p, RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | ||
| 609 | if (!h) continue; | ||
| 610 | auto fn = (CreateInterfaceFn)dlsym(h, "CreateInterface"); | ||
| 611 | if (!fn) continue; | ||
| 612 | for (const char **n = iface_names; *n; n++) { | ||
| 613 | int ret = 0; | ||
| 614 | ISurface *s = (ISurface *)fn(*n, &ret); | ||
| 615 | if (s) { | ||
| 616 | logf("ISurface found: %s via %s", *n, p); | ||
| 617 | fclose(maps); | ||
| 618 | return s; | ||
| 619 | } | ||
| 620 | } | ||
| 621 | } | ||
| 622 | fclose(maps); | ||
| 623 | return nullptr; | ||
| 548 | } | 624 | } |
| 549 | 625 | ||
| 550 | // public init function called from setup_thread | ||
| 551 | void install_menu() { | 626 | void install_menu() { |
| 552 | build_tree(); | 627 | build_tree(); |
| 553 | config_load(); | 628 | config_load(); |
| 554 | install_paint_hook(); | 629 | // resolve ISurface once (used by the DrawPrintText hook in hl2h.cpp and |
| 630 | // by outer_draw). DrawPrintText hook is installed from hkdCreateMove. | ||
| 631 | g_surface = find_surface_interface(); | ||
| 632 | if (!g_surface) { | ||
| 633 | logf("ISurface not found; menu disabled"); | ||
| 634 | return; | ||
| 635 | } | ||
| 636 | logf("ISurface = %p", g_surface); | ||
| 555 | } | 637 | } |
