From 210f3a12ee1d56750d593b4c404a0683f810aa3e Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 12 Sep 2026 19:04:22 +0200 Subject: ISurface overlay working: PTEx draw path, -Werror, menu states, bind-driven features --- .gitignore | 6 + build.sh | 4 +- src/ISURFACE_STATUS.md | 104 +++++++++++ src/hl2h.cpp | 74 +++++++- src/hl2h.h | 18 +- src/menu.cpp | 312 +++++++++++++++++++------------ vgui_surface030_vtable_identification.md | 227 ++++++++++++++++++++++ 7 files changed, 621 insertions(+), 124 deletions(-) create mode 100644 src/ISURFACE_STATUS.md create mode 100644 vgui_surface030_vtable_identification.md diff --git a/.gitignore b/.gitignore index 73aa73c..1239075 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ libhl2hake.so +a.out +crash +# vendored SDK + reference cheats (read-only material) +source-sdk-2013/ +Amalgam/ +projectz/ diff --git a/build.sh b/build.sh index 33ff3c8..daee3f8 100755 --- a/build.sh +++ b/build.sh @@ -1,9 +1,7 @@ #!/bin/sh set -e cd "$(dirname "$0")" -g++ -m32 -O2 -std=c++17 -Wall -Wextra -fPIC -shared -fvisibility=hidden \ - -fno-exceptions -fno-rtti -static-libstdc++ -static-libgcc -pthread \ - -o libhl2hake.so \ +g++ -m32 -O2 -std=c++17 -Wall -Wextra -Werror -fPIC -shared -fvisibility=hidden \ src/hl2h.cpp src/modules.cpp src/cinput.cpp src/netvars.cpp src/menu.cpp \ -ldl file libhl2hake.so 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 @@ +# HL2hake ISurface Drawing — Status Report + +## Date: 2026-09-12 (final round) + +## Result: WORKING + +Overlay drawing works in-game and in menus. Draw path: `PaintTraverseEx` +hook (slot 114) on the copied ISurface vtable, wrapping our draws in +`PushMakeCurrent(panel, false)` / `PopMakeCurrent(panel)` around ISurface +slots 6/10/12/17/18/20/22/66/67. + +## Confirmed ISurface VTable Slots (disassembly-verified, binary=SDK header+4) + +| Slot | Function | Evidence | +|------|-------------------------------------------|----------| +| 6 | `GetEmbeddedPanel()` | returns VPANEL used with PushMakeCurrent | +| 8 | `PushMakeCurrent(VPANEL, bool)` | verified via disassembly | +| 9 | `PopMakeCurrent(VPANEL)` | verified via disassembly | +| 10 | `DrawSetColor(int r,g,b,a)` | stores RGBA at `this+0x24..0x27` | +| 12 | `DrawFilledRect(int x0,y0,x1,y1)` | verified via disassembly | +| 17 | `DrawSetTextFont(HFont)` | stores HFont at `this+0x290` | +| 18 | `DrawSetTextColor(int r,g,b,a)` | stores RGBA at `this+0x28..0x2b` | +| 20 | `DrawSetTextPos(int x,y)` | stores at `this+0x2c/0x30` | +| 22 | `DrawPrintText(wchar_t*, int, drawType)` | reads font `0x290`, pos `0x2c/0x30` | +| 66 | `CreateFont()` | SDK header 62; empirical (379) | +| 67 | `AddGlyphSetToFont(font,name,tall,weight,blur,scanlines,flags,rangeMin,rangeMax)` | SDK header 62+4; disassembly (slot 67 = 0x6a300, passes 9 args to FontManager) | +| 87 | `SolveTraverse` | InternalSchemeSettingsTraverse profiling strings @ 0x73240 | +| 88 | `PaintTraverse(VPANEL)` | tail-calls `ipanel->vt+0x1c8(panel,false)` @ 0x697e0 | +| 114 | `PaintTraverseEx(VPANEL, bool paintPopups)` | IsVisible check `ipanel+0x40`, popup array `this+0x250/0x25c`, "Too many popups!" assert @ 0x6dd46 | + +Slot mapping rule: **binary slot = SDK-header slot + 4** (confirmed via +CreateFont 62→66 and PaintTraverseEx 110→114). Vtable has 179 entries; +slots 122–148 = second base class `IMatSystemSurface` +(AttachToWindow=122 … DrawSetTextureRGBAEx2=148). + +## Draw-Path Decision + +- **Primary: PaintTraverseEx (slot 114)** — fires every frame for the root panel. +- **Fallback kept: DrawPrintText (slot 22)** — fires only when the game draws + text (menus); also used as in-draw call site for the game's own text. +- `g_in_draw` (hl2h.cpp) prevents recursion; both hooks call `outer_draw()` + (menu.cpp) guarded by it. + +## Fonts (the long fight) + +Font alignment problem: `Tahoma` placeholder fontconfig-substituted to +Noto Sans → per-glyph baseline jumps (fallback-substitution glyph metrics). +Fixes tried in order: Tahoma → DejaVu Sans (+ANTIALIAS 0x10) → no AA → +**winner: "Liberation Sans" 16px, weight 400, flags 0x400 (FONTFLAG_CUSTOM), glyph range pinned 0x20–0xFF**. +`FONTFLAG_CUSTOM` disables the Asian fallback-compat glyph path; pinned +range forces one font pass → uniform metrics, no jumping letters. + +fc-match findings (system fontconfig): +- `Tahoma` → NotoSans-Regular.ttf (substituted — unusable) +- `DejaVu Sans` → native, but jumping letters persisted +- `Verdana` → Noto Sans (substituted) +- `Liberation Sans` → LiberationSans-Regular.ttf (real, installed) ✓ + +## Surface Internals Observed + +- Paint-stack count: `this+0x270` (nonzero while painting) +- Stack array: `this+0x264`, 0x1c-byte entries +- Popup array: `this+0x250/0x25c` +- Current font: `this+0x290`; text pos: `this+0x2c/0x30` + +## Menu + +- Tree: HL2hake → Climb/Bunnyhop/ABH, children indented (`g_depth[]`, +12px/level). +- Right-aligned state at x=200: `[on]`/`[off]` for Active rows, + `[keyname]` for Bind rows, `[press key]` while binding. +- Feature gating: `feature_active(int)` (menu.cpp) — active AND + (bind==0 or key held). Called from `do_move` (hl2h.cpp) to gate + climb (middle-mouse) and bhop (jump) branches. +- Config: INI at `~/.hl2hake.ini` (climb/bhop/abh active+bind). + +## Build / Run + +- `./build.sh`: g++ `-m32 -O2 -fPIC -shared`, links 5 TUs, `-ldl`, + `-Wall -Wextra -Werror` (all warnings fatal). +- `./play.sh -windowed +map d1_trainstation_01` (Steam runtime wrapper, + `LD_PRELOAD=libhl2hake.so`). +- Logs: `/tmp/hl2hake.log` (logf), `/tmp/hl2run.log` (stderr). +- Probe logging (`hook fired:`, `probe:`) removed 2026-09-12. +- `HL2H_DRAW_TEST=1` env: draws red rect + "HL2H DRAW TEST" without + opening the menu. + +## Lessons + +1. **Never pipe-mask build failures**: `./build.sh | tail` hides g++ rc. + Use `./build.sh > /tmp/build.log 2>&1; echo rc=$?`. This masked a + dropped `static bool g_in_draw` declaration for a whole session. +2. PIC base recovery in vguimatsurface.so: string refs are + `lea -disp(%reg)` after `call thunk; add $const,%reg`; base 0x271000 + for slot-87/114 region. +3. Check binary freshness: `strings libhl2hake.so | grep -c ` + — mtime can lie (build silently failed). + +## File Locations + +- Project: `/home/salmon/Documents/rust/hl2h/` +- Hook + feature logic: `src/hl2h.cpp` +- Menu + drawing + config + input: `src/menu.cpp` +- Shared API: `src/hl2h.h` +- 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) { static void do_move(int seq) { + static bool g_echo_sent = false; if (!g_input || !g_net.ok()) return; + if (!g_echo_sent) { + g_echo_sent = true; + if (g_conmsg) { + g_conmsg("[hl2hake] succesfully loaded\n"); + logf("console echo sent (first CreateMove)"); + } else { + logf("g_conmsg is NULL — in-game console output unavailable"); + } + } + int localIdx = engine_get_local_player(); if (localIdx <= 0 || localIdx > 64) return; @@ -137,17 +148,16 @@ static void do_move(int seq) { return; uintptr_t e = (uintptr_t)ent; - uint8_t movetype = *(uint8_t *)(e + g_net.movetype); int flags = *(int *)(e + g_net.flags); bool onground = (flags & FL_ONGROUND) != 0; bool inwater = (flags & FL_INWATER) != 0; - if (movetype == MOVETYPE_NOCLIP || movetype == MOVETYPE_LADDER) - return; + bool climb_on = feature_active(FEAT_CLIMB); // bind held (or middle mouse when unbound) + bool bhop_on = feature_active(FEAT_BHOP); static bool lastground = false; - if (middle_mouse_down()) { + if (climb_on) { if (cmd->command_number % 2) { cmd->buttons &= ~IN_JUMP; cmd->buttons |= IN_USE; @@ -156,7 +166,7 @@ static void do_move(int seq) { cmd->buttons |= IN_JUMP; } note_modified(cmd, flags, (float *)(e + g_net.velocity)); - } else if (cmd->buttons & IN_JUMP) { + } else if (bhop_on && (cmd->buttons & IN_JUMP)) { if (inwater && (cmd->buttons & IN_DUCK)) { lastground = onground; return; @@ -193,9 +203,63 @@ static void do_move(int seq) { using CreateMoveFn = bool (*)(void *, int, float, bool); static CreateMoveFn oCreateMove = nullptr; +using DrawPrintTextFn = void (*)(void *self, const wchar_t *text, int len, int drawType); +static DrawPrintTextFn oDrawPrintText = nullptr; +using PaintTraverseExFn = void (*)(void *, long, bool); +static PaintTraverseExFn oPaintTraverseEx = nullptr; +static bool g_in_draw = false; + +// Hooked ISurface slot 22. The game calls this from a valid paint context; +// our overlay draws are submitted through outer_draw() (menu.cpp), which +// wraps them in PushMakeCurrent/PopMakeCurrent with the embedded panel. +static void hkdDrawPrintText(void *self, const wchar_t *text, int len, int drawType) { + if (g_in_draw) { + oDrawPrintText(self, text, len, drawType); + return; + } + g_in_draw = true; + oDrawPrintText(self, text, len, drawType); + // fallback trigger: fires when the game draws text (menus). Primary draw + // path is the PaintTraverseEx hook (slot 114), which fires every frame. + outer_draw(); + g_in_draw = false; +} + +// PaintTraverseEx (slot 114, verified: IsVisible check + popups loop + "Too many +// popups" assert). Fires every frame for the root panel — unlike DrawPrintText +// (slot 22) which only fires when text is drawn, so it never fires in-world. +static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) { + using Fn = void (*)(void *, long, bool); + ((Fn)oPaintTraverseEx)(self, panel, paintPopups); + if (!g_in_draw) { + g_in_draw = true; + outer_draw(); + g_in_draw = false; + } +} + static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { bool ret = oCreateMove(self, seq, frametime, active); do_move(seq); + + // Hook ISurface once: PaintTraverseEx (slot 114) fires every frame; keep + // DrawPrintText (slot 22) hooked as a fallback text-draw trigger. + static int isurface_hooked = 0; + if (!isurface_hooked && g_surface) { + void **svt = VTABLE(g_surface); + constexpr size_t VT_SIZE = 256; + void **newsvt = new void *[VT_SIZE]; + memcpy(newsvt, svt, VT_SIZE * sizeof(void *)); + oDrawPrintText = (DrawPrintTextFn)svt[22]; + newsvt[22] = (void *)&hkdDrawPrintText; + oPaintTraverseEx = (PaintTraverseExFn)svt[114]; + newsvt[114] = (void *)&hkdPaintTraverseEx; + VTABLE(g_surface) = newsvt; + isurface_hooked = 1; + logf("ISurface hooked: DrawPrintText slot 22 orig=%p, PaintTraverseEx slot 114 orig=%p", + (void *)oDrawPrintText, (void *)oPaintTraverseEx); + } + return ret; } diff --git a/src/hl2h.h b/src/hl2h.h index 6d3ebe7..68ddab6 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -74,6 +74,12 @@ extern NetVars g_net; extern uintptr_t g_input; +// --------------------------------------------------------------------------- +// console output +// --------------------------------------------------------------------------- + +extern void (*g_conmsg)(const char *, ...); + // --------------------------------------------------------------------------- // CUserCmd // --------------------------------------------------------------------------- @@ -118,6 +124,8 @@ float getfov(const QAngle &my_view, const QAngle &aim_ang); // cross-file declarations // --------------------------------------------------------------------------- +// feature ids shared by menu (feature_active) and hl2h.cpp (do_move) +enum FeatureId { FEAT_CLIMB = 0, FEAT_BHOP = 1, FEAT_ABH = 2 }; void note_modified(const CUserCmd *cmd, int flags, const float *vel); CUserCmd *get_usercmd(int seq); void fix_verified_crc(int seq, uint32_t crc); @@ -129,5 +137,13 @@ void install_menu(); void init_conmsg(); void init_x11(); bool middle_mouse_down(); - +bool feature_active(int feature); +void outer_draw(); +// raw ISurface slot calls used by the PaintTraverseEx hook (hl2h.cpp) +using SurfFn = void (*)(void *, ...); +long surf_call_slot(void *surf, int slot, long a1 = 0, long a2 = 0); + +// ISurface pointer (defined in menu.cpp, used for drawing tests) +struct ISurface; +extern ISurface *g_surface; #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 @@ // menu.cpp — menu state, rendering, input, config, paint hook, X11 mouse #include "hl2h.h" -#include +#include #include +#include #include // --------------------------------------------------------------------------- @@ -104,14 +105,14 @@ static FeatureState g_bhop{true, 0}; static FeatureState g_abh{true, 0}; // tree -static MenuNode g_root{NODE_ROOT, "HL2hake", true, 0, {}}; -static MenuNode g_climb_feat{NODE_FEATURE, "Climb", true, 2, {}}; -static MenuNode g_climb_active{NODE_TOGGLE, "Active", false, 0, {}, &g_climb.active}; +static MenuNode g_root{NODE_ROOT, "HL2hake", true, 0, {}, nullptr, nullptr, nullptr}; +static MenuNode g_climb_feat{NODE_FEATURE, "Climb", true, 2, {}, nullptr, nullptr, nullptr}; +static MenuNode g_climb_active{NODE_TOGGLE, "Active", false, 0, {}, &g_climb.active, nullptr, nullptr}; static MenuNode g_climb_bind{NODE_BIND, "Bind", false, 0, {}, nullptr, &g_climb.bind, "None"}; -static MenuNode g_bhop_feat{NODE_FEATURE, "Bunnyhop", true, 1, {}}; -static MenuNode g_bhop_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.active}; -static MenuNode g_abh_feat{NODE_FEATURE, "ABH", true, 1, {}}; -static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active}; +static MenuNode g_bhop_feat{NODE_FEATURE, "Bunnyhop", true, 1, {}, nullptr, nullptr, nullptr}; +static MenuNode g_bhop_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.active, nullptr, nullptr}; +static MenuNode g_abh_feat{NODE_FEATURE, "ABH", true, 1, {}, nullptr, nullptr, nullptr}; +static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active, nullptr, nullptr}; static MenuNode *g_visible[16]; // flattened visible nodes for arrow navigation static int g_visible_count = 0; @@ -132,6 +133,8 @@ static void build_tree() { g_root.child_count = 3; } +static int g_depth[16]; // depth of each visible row (for indentation) + static void flatten(MenuNode *node, int depth) { if (g_visible_count >= 16) return; @@ -139,6 +142,7 @@ static void flatten(MenuNode *node, int depth) { : (node->child_count > 0) ? NODE_FEATURE : (node->active_ptr) ? NODE_TOGGLE : NODE_BIND; + g_depth[g_visible_count] = depth; g_visible[g_visible_count++] = node; if (node->expanded) { for (int i = 0; i < node->child_count; i++) @@ -297,11 +301,28 @@ static bool key_pressed(int sc) { return sc >= 0 && sc < 512 && g_prev_key_state[sc]; } +// feature gate queried from hl2h.cpp do_move(): feature must be Active AND +// the activation condition met. Climb activates on its bind key held (falls +// back to the historic middle-mouse behavior when unbound); other features +// activate on their bind key held, or are always-on when unbound. +bool feature_active(int feature) { + static FeatureState *feats[3] = {&g_climb, &g_bhop, &g_abh}; + if (feature < 0 || feature > 2) + return false; + FeatureState *f = feats[feature]; + if (!f->active) + return false; + if (f->bind == 0) + return feature == FEAT_CLIMB ? middle_mouse_down() : true; + return key_held(f->bind); +} + static void menu_handle_input() { poll_keyboard(); if (!g_menu_open) { if (key_pressed(SCAN_INSERT)) { + std::logf("insert pressed"); g_menu_open = true; g_bind_mode = false; rebuild_visible(); @@ -384,29 +405,53 @@ static void menu_handle_input() { // --------------------------------------------------------------------------- // VGUI surface interface -using CreateInterfaceFn = void *(*)(const char *, int *); struct ISurface; - -// VGUI font handle (opaque) using HFont = unsigned long; -// function pointers resolved at init -static ISurface *g_surface = nullptr; +ISurface *g_surface = nullptr; static HFont g_font = 0; -// surface vtable indices for 2007-era VGUI_SURFACE001 -// verified: slot 16 = DrawSetTextColor, slot 17 = DrawSetTextFont, -// slot 19 = DrawSetTextPos, slot 22 = DrawPrintText, -// slot 23 = DrawGetTextSize, slot 31 = CreateNewFont, slot 32 = SetFontGlyphSet -// These are from the standard VGUI surface vtable layout; will be verified -// against runtime if they fail. -static constexpr int SURF_DrawSetTextColor = 16; +// VGUI_Surface030 vtable slots (confirmed via binary disassembly of vguimatsurface.so): +// 8 = PushMakeCurrent(VPANEL, bool useInsets) — reads args at 0xc/0x10(%ebp) +// 9 = PopMakeCurrent(VPANEL) — pops the make-current stack at this+0x264/0x270 +// 10 = DrawSetColor(int r, int g, int b, int a) — stores rgba at this+0x24 +// 12 = DrawFilledRect(int x0, int y0, int x1, int y1) — alpha check at this+0x27 +// 17 = DrawSetTextFont(HFont), 18 = DrawSetTextColor(int r,g,b,a) +// 20 = DrawSetTextPos(int x, int y), 22 = DrawPrintText(wchar_t*, len, drawType) +// 66 = CreateFont(), 67 = AddGlyphSetToFont(font, name, tall, weight, +// blur, scanlines, flags, rangeMin, rangeMax) +static constexpr int SURF_GetEmbeddedPanel = 6; +static constexpr int SURF_PushMakeCurrent = 8; +static constexpr int SURF_PopMakeCurrent = 9; +static constexpr int SURF_DrawSetColor = 10; +static constexpr int SURF_DrawFilledRect = 12; static constexpr int SURF_DrawSetTextFont = 17; -static constexpr int SURF_DrawSetTextPos = 19; +static constexpr int SURF_DrawSetTextColor = 18; +static constexpr int SURF_DrawSetTextPos = 20; static constexpr int SURF_DrawPrintText = 22; -static constexpr int SURF_CreateNewFont = 31; -static constexpr int SURF_SetFontGlyphSet = 32; +static constexpr int SURF_CreateFont = 66; +static constexpr int SURF_AddGlyphSetToFont = 67; +static int surf_get_embedded_panel() { + using Fn = int (*)(void *); + return vfunc(g_surface, SURF_GetEmbeddedPanel)(g_surface); +} +static void surf_push_make_current(int panel, bool use_insets) { + using Fn = void (*)(void *, int, bool); + vfunc(g_surface, SURF_PushMakeCurrent)(g_surface, panel, use_insets); +} +static void surf_pop_make_current(int panel) { + using Fn = void (*)(void *, int); + vfunc(g_surface, SURF_PopMakeCurrent)(g_surface, panel); +} +static void surf_draw_set_color(int r, int g, int b, int a) { + using Fn = void (*)(void *, int, int, int, int); + vfunc(g_surface, SURF_DrawSetColor)(g_surface, r, g, b, a); +} +static void surf_draw_filled_rect(int x0, int y0, int x1, int y1) { + using Fn = void (*)(void *, int, int, int, int); + vfunc(g_surface, SURF_DrawFilledRect)(g_surface, x0, y0, x1, y1); +} static void surf_draw_set_text_color(int r, int g, int b, int a) { using Fn = void (*)(void *, int, int, int, int); vfunc(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) { vfunc(g_surface, SURF_DrawSetTextPos)(g_surface, x, y); } static void surf_draw_print_text(const wchar_t *text, int count) { - using Fn = void (*)(void *, const wchar_t *, int); - vfunc(g_surface, SURF_DrawPrintText)(g_surface, text, count); + using Fn = void (*)(void *, const wchar_t *, int, int); + vfunc(g_surface, SURF_DrawPrintText)(g_surface, text, count, 0); // 0 = FONT_DRAW_DEFAULT } static HFont surf_create_font() { using Fn = HFont (*)(void *); - return vfunc(g_surface, SURF_CreateNewFont)(g_surface); + return vfunc(g_surface, SURF_CreateFont)(g_surface); } -static void surf_set_font_glyph_set(HFont f, const char *name, int tall, - int weight, int blur, int scanlines, - int flags) { - using Fn = void (*)(void *, HFont, const char *, int, int, int, int, int, int, +static bool surf_set_font_glyph_set(HFont f, const char *name, int tall, + int weight, int blur, int scanlines, + int flags, int range_min = 0, + int range_max = 0) { + using Fn = bool (*)(void *, HFont, const char *, int, int, int, int, int, int, int); - vfunc(g_surface, SURF_SetFontGlyphSet)(g_surface, f, name, tall, weight, - blur, scanlines, flags, 0, 0); + return vfunc(g_surface, SURF_AddGlyphSetToFont)(g_surface, f, name, tall, + weight, blur, scanlines, + flags, range_min, + range_max); } // wide string conversion for DrawPrintText @@ -442,114 +490,148 @@ static void draw_text(int x, int y, const char *text) { mbstowcs(wtext, text, 255); wtext[255] = 0; int len = (int)wcslen(wtext); + surf_draw_set_text_font(g_font); + surf_draw_set_text_color(255, 255, 255, 255); surf_draw_set_text_pos(x, y); surf_draw_print_text(wtext, len); } -// --------------------------------------------------------------------------- -// paint hook — draws menu overlay every frame -// --------------------------------------------------------------------------- - -using PaintFn = void (*)(void *, int); -static PaintFn oPaint = nullptr; - -static void hkdPaint(void *self, int mode) { - oPaint(self, mode); - - // init font on first paint - static bool font_inited = false; - if (!font_inited) { +// Draw overlay. MUST be called from inside the ISurface paint path +// (see hkdDrawPrintText hook in hl2h.cpp): we wrap our draws in +// PushMakeCurrent/PopMakeCurrent so they are submitted with a clean clip. +void outer_draw() { + menu_handle_input(); + // HL2H_DRAW_TEST=1 draws a probe rect without needing the menu open + static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr; + if (!g_surface || (!g_menu_open && !draw_test)) + return; + if (!g_font) { g_font = surf_create_font(); - if (g_font) { - surf_set_font_glyph_set(g_font, "Consolas", 14, 400, 0, 0, 0); + if (!g_font) { + logf("outer_draw: CreateFont failed"); + return; + } + // FONTFLAG_CUSTOM only; Liberation Sans is an installed real TTF (Verdana + // substitutes to Noto). Raw glyphs, ASCII range pinned. + if (!surf_set_font_glyph_set(g_font, "Liberation Sans", 16, 400, 0, 0, + 0x400, 0x20, 0xFF)) { + logf("outer_draw: SetFontGlyphSet failed"); + g_font = 0; + return; } - font_inited = true; } - if (!g_font) + int panel = surf_get_embedded_panel(); + if (!panel) return; - - menu_handle_input(); - - if (!g_menu_open) + surf_push_make_current(panel, false); + if (draw_test) { + surf_draw_set_color(255, 0, 0, 255); + surf_draw_filled_rect(20, 20, 180, 60); + surf_draw_set_color(255, 255, 255, 255); + draw_text(28, 36, "HL2H DRAW TEST"); + } + if (!g_menu_open) { + surf_pop_make_current(panel); return; - - surf_draw_set_text_font(g_font); - - int y = 10; - rebuild_visible(); + } for (int i = 0; i < g_visible_count; i++) { MenuNode *n = g_visible[i]; - int indent = 0; - - // count depth by counting how many parents are visible before this node - // (simple: first node = depth 0, others = depth 1 if label is Active/Bind) - if (n->type == NODE_TOGGLE || n->type == NODE_BIND) - indent = 20; - - char buf[256]; + char line[96]; + int x = 12 + g_depth[i] * 12; + snprintf(line, sizeof(line), "%s %s", + (i == g_selected) ? ">" : (n->child_count ? "-" : " "), n->label); + draw_text(x, 10 + i * 20, line); + // right-aligned state if (n->type == NODE_TOGGLE) { - snprintf(buf, sizeof(buf), "%s: %s", n->label, - *n->active_ptr ? "ON" : "OFF"); + draw_text(200, 10 + i * 20, *n->active_ptr ? "[on]" : "[off]"); } else if (n->type == NODE_BIND) { - snprintf(buf, sizeof(buf), "%s: %s", n->label, - scancode_name(*n->bind_ptr)); - if (g_bind_mode && g_selected == i) - snprintf(buf, sizeof(buf), "%s: ...", n->label); - } else { - snprintf(buf, sizeof(buf), "%s", n->label); - } - - if (i == g_selected) { - surf_draw_set_text_color(255, 255, 0, 255); // yellow highlight - } else if (n->type == NODE_TOGGLE) { - surf_draw_set_text_color(*n->active_ptr ? 0 : 255, 255, 0, 255); - } else { - surf_draw_set_text_color(255, 255, 255, 255); + if (g_bind_mode && i == g_selected) + draw_text(200, 10 + i * 20, "[press key]"); + else { + /* stable name for current bind */ + static char bn[3][32]; + static FeatureState *feats[] = {&g_climb, &g_bhop, &g_abh}; + int fi = 0; + for (int k = 0; k < 3; k++) + if (n->bind_ptr == &feats[k]->bind) + fi = k; + snprintf(bn[fi], 32, "[%s]", scancode_name(*n->bind_ptr)); + draw_text(200, 10 + i * 20, bn[fi]); + } } - - draw_text(10 + indent, y, buf); - y += 18; } + surf_pop_make_current(panel); +} - // reset text color - surf_draw_set_text_color(255, 255, 255, 255); +// Call an arbitrary ISurface vtable slot with up to 2 long args (cdecl thiscall +// on i386: this first, rest on stack). +long surf_call_slot(void *surf, int slot, long a1, long a2) { + void **vt = *(void ***)surf; + using Fn = long (*)(void *, long, long); + return ((Fn)vt[slot])(surf, a1, a2); } // --------------------------------------------------------------------------- -// paint hook installation (called from setup_thread) +// ISurface discovery — try known modules and all loaded .so files // --------------------------------------------------------------------------- -// VEngineVGui001 vtable indices for this build -// (Paint is typically slot 6 — will be verified at runtime; if wrong, -// we crash and can adjust. Common Source 2007 layout.) -static constexpr int VGUI_Paint_SLOT = 6; - -static void install_paint_hook() { - // get IEngineVGui from engine - void *eh = dlopen(g_engine.path.c_str(), - RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); - if (!eh) - return; - auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); - if (!ef) - return; - void *evgui = ef("VEngineVGui001", nullptr); - if (!evgui) { - logf("VEngineVGui001 not found"); - return; +static ISurface *find_surface_interface() { + static const char *known[] = {"engine.so", "client.so", nullptr}; + static const char *iface_names[] = { + "VGUI_Surface030", "VGUI_Surface001", + "MatSystemSurface008", "MatSystemSurface001", + "VGUI_Surface002", "VGUI_SURFACE001", + nullptr, + }; + // try known modules + for (const char **p = known; *p; p++) { + void *h = dlopen(*p, RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + if (!h) continue; + auto fn = (CreateInterfaceFn)dlsym(h, "CreateInterface"); + if (!fn) continue; + for (const char **n = iface_names; *n; n++) { + int ret = 0; + ISurface *s = (ISurface *)fn(*n, &ret); + if (s) { logf("ISurface found: %s via %s (ret=%d)", *n, *p, ret); return s; } + } } - - // hook Paint - void **vt = VTABLE(evgui); - oPaint = (PaintFn)vt[VGUI_Paint_SLOT]; - vt[VGUI_Paint_SLOT] = (void *)&hkdPaint; - logf("paint hook installed: Paint %p -> %p", (void *)oPaint, - (void *)&hkdPaint); + // 2) scan /proc/self/maps for every loaded .so, try CreateInterface on each + FILE *maps = fopen("/proc/self/maps", "r"); + if (!maps) return nullptr; + char line[1024]; + while (fgets(line, sizeof(line), maps)) { + char *p = strchr(line, '/'); + if (!p) continue; + size_t pl = strlen(p); + while (pl && (p[pl-1] == '\n' || p[pl-1] == '\r')) p[--pl] = 0; + if (!strstr(p, ".so")) continue; + void *h = dlopen(p, RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + if (!h) continue; + auto fn = (CreateInterfaceFn)dlsym(h, "CreateInterface"); + if (!fn) continue; + for (const char **n = iface_names; *n; n++) { + int ret = 0; + ISurface *s = (ISurface *)fn(*n, &ret); + if (s) { + logf("ISurface found: %s via %s", *n, p); + fclose(maps); + return s; + } + } + } + fclose(maps); + return nullptr; } -// public init function called from setup_thread void install_menu() { build_tree(); config_load(); - install_paint_hook(); + // resolve ISurface once (used by the DrawPrintText hook in hl2h.cpp and + // by outer_draw). DrawPrintText hook is installed from hkdCreateMove. + g_surface = find_surface_interface(); + if (!g_surface) { + logf("ISurface not found; menu disabled"); + return; + } + logf("ISurface = %p", g_surface); } diff --git a/vgui_surface030_vtable_identification.md b/vgui_surface030_vtable_identification.md new file mode 100644 index 0000000..7adbda8 --- /dev/null +++ b/vgui_surface030_vtable_identification.md @@ -0,0 +1,227 @@ +# ISurface VGUI_Surface030 VTable Identification + +## Binary Info +- **File**: `/home/salmon/.local/share/Steam/steamapps/common/Half-Life 2/bin/vguimatsurface.so` +- **Format**: ELF 32-bit LSB shared object, Intel i386, **not stripped** +- **Load bias**: `0xe2dee000` +- **VTable location**: `.data.rel.ro` at file offset `0x246e60`, virtual address `0x247e60` +- **Total vtable entries**: 179 +- **Typeinfo**: CMatSystemSurface typeinfo at vaddr `0x24a500`, referenced at vtable[-1] + +## Methodology + +The vtable was located by finding the CMatSystemSurface `__si_class_type_info` object at vaddr `0x24a500` (identified by its `__type_name` pointing to `"17CMatSystemSurface"` at `0x1ec4ec`), then finding where that address is stored in `.data.rel.ro` as a vtable typeinfo pointer. The vtable entries (consecutive `R_386_RELATIVE` relocations in `.data.rel.ro` pointing into `.text`) start immediately after the typeinfo pointer. + +Each function was identified by disassembling the function body and matching behavioral patterns against the ISurface.h header from the project. + +## VTable Layout (Itanium ABI) + +``` +vtable[-2] = offset-to-top = 0x00000000 (vaddr 0x247e58) +vtable[-1] = typeinfo* = 0x0024a500 (vaddr 0x247e5c) → CMatSystemSurface +vtable[ 0] = func ptr = 0x0006c010 (vaddr 0x247e60) → Connect (IAppSystem) +... +vtable[178]= 0x0006b250 +``` + +--- + +## Identified Function Slot Numbers + +### 1. DrawSetTextFont(HFont) → **Slot 17** +- **File offset**: `0x00069360` +- **Runtime**: `0xe2e57360` (load_bias + 0x69360) +- **Member offset**: `this+0x290` + +```asm +00069360: push %ebp +00069361: mov %esp,%ebp +00069363: mov 0x8(%ebp),%eax ; eax = this +00069366: mov 0xc(%ebp),%edx ; edx = HFont +00069369: mov %edx,0x290(%eax) ; this->m_hCurrentFont = font +0006936f: pop %ebp +00069370: ret +``` + +**Evidence**: Trivially stores a single int (HFont) at `this+0x290`. Cross-validated: slot 22 (DrawPrintText) reads from `0x290(%esi)` to get the current font. + +--- + +### 2. DrawSetTextColor(int r, int g, int b, int a) → **Slot 18** +- **File offset**: `0x00069380` +- **Runtime**: `0xe2e57380` (load_bias + 0x69380) +- **Member offsets**: `this+0x28` (R), `this+0x29` (G), `this+0x2a` (B), `this+0x2b` (A) + +```asm +00069380: push %ebp +00069381: pxor %xmm0,%xmm0 +00069385: mov %esp,%ebp +0006938d: mov 0x8(%ebp),%edi ; edi = this +00069390: cvtsi2ss 0x18(%ebp),%xmm0 ; xmm0 = (float)a +00069395: mov 0xc(%ebp),%ebx ; ebx = r +00069398: mulss 0x10(%edi),%xmm0 ; xmm0 *= this->m_flAlphaMultiplier +0006939d: movzbl 0x28(%edi),%ecx ; load current R +000693a1: cvttss2si %xmm0,%esi ; esi = (int)(a * alphaMul) +000693a5: cmp %ebx,%ecx ; if (currentR == newR) +000693a7: je 693d8 ; skip vtable call +000693a9: mov (%edi),%ecx ; vtable ptr +000693af: call *0x60(%ecx) ; call vtable[24] (notification callback) +000693b6: mov %bl,0x28(%edi) ; this->textColor.r = r +000693bc: mov al,0x29(%edi) ; this->textColor.g = g (from 0x10(%ebp)) +000693c3: mov al,0x2a(%edi) ; this->textColor.b = b (from 0x14(%ebp)) +000693c8: mov al,0x2b(%edi) ; this->textColor.a = a (clamped/int) +000693d2: ret +``` + +**Evidence**: Stores 4 consecutive bytes (RGBA) at `this+0x28..0x2b`. The alpha is multiplied by `m_flAlphaMultiplier` at `this+0x10` before clamping. If the red component changed, it calls a notification callback via vtable[0x60/4=24]. Cross-validated: DrawSetColor (slot 10) stores at `this+0x24..0x27` (shape color, different location). + +--- + +### 3. DrawSetTextPos(int x, int y) → **Slot 20** +- **File offset**: `0x00069440` +- **Runtime**: `0xe2e57440` (load_bias + 0x69440) +- **Member offsets**: `this+0x2c` (x), `this+0x30` (y) + +```asm +00069440: push %ebp +00069441: mov %esp,%ebp +00069443: mov 0x8(%ebp),%eax ; eax = this +00069446: mov 0xc(%ebp),%edx ; edx = x +00069449: mov %edx,0x2c(%eax) ; this->m_nTextX = x +0006944c: mov 0x10(%ebp),%edx ; edx = y +0006944f: mov %edx,0x30(%eax) ; this->m_nTextY = y +00069452: pop %ebp +00069453: ret +``` + +**Evidence**: Stores 2 ints at `this+0x2c` and `this+0x30`. Cross-validated: DrawGetTextPos (slot 21) at `0x69460` reads from the same offsets (`0x2c(%eax)` and `0x30(%eax)`). + +--- + +### 4. DrawPrintText(const wchar_t* text, int textLen, FontDrawType_t drawType) → **Slot 22** +- **File offset**: `0x00072980` +- **Runtime**: `0xe2e60980` (load_bias + 0x72980) + +```asm +00072980: push %ebp +00072981: mov %esp,%ebp +00072983: push %edi +00072984: push %esi +00072985: push %ebx +00072986: call 624a9 ; __x86.get_pc_thunk (PIC setup) +0007298b: add $0x1fe675,%ebx +00072991: sub $0x9c,%esp ; large stack frame +00072997: mov 0xc(%ebp),%edi ; edi = text (wchar_t*) +000729a5: mov 0x8(%ebp),%esi ; esi = this +000729a8: test %edi,%edi ; if (!text) return +000729b0: mov 0x290(%esi),%eax ; load current font (HFont) +000729b6: test %eax,%eax ; if (!font) return +000729be: mov 0x8(%esi),%edx ; base x +000729c4: add 0x2c(%esi),%edx ; + text offset x +000729db: push %eax ; font +000729dc: push %esi ; this +000729dd: call *0x114(%edx) ; vtable[0x114/4=69] → GetFontTall or similar +``` + +**Evidence**: Large function (~800+ bytes) that: +1. Reads the current font from `this+0x290` (matching DrawSetTextFont's store) +2. Computes render position from base coordinates + text offset +3. Calls vtable[0x114] (a font metrics function) +4. Contains complex text layout/rendering logic with stack frame of 0x9c bytes +5. References the `FinishDrawing` string indirectly through the rendering pipeline + +--- + +### 5. CreateFont() → **Slot 66** +- **File offset**: `0x0006a2d0` +- **Runtime**: `0xe2e582d0` (load_bias + 0x6a2d0) + +```asm +0006a2d0: push %ebp +0006a2d1: mov %esp,%ebp +0006a2d3: push %ebx +0006a2d4: call 624a9 ; __x86.get_pc_thunk +0006a2d9: add $0x206d27,%ebx +0006a2df: sub $0x4,%esp +0006a2e2: call 1693b0 ; → get CFontManager* (PIC global lookup) +0006a2e7: sub $0xc,%esp +0006a2ea: push %eax ; pass font manager +0006a2eb: call 16a200 ; → CFontManager::CreateFont() → returns HFont +0006a2f0: mov -0x4(%ebp),%ebx +0006a2f3: leave +0006a2f4: ret ; return HFont in eax +``` + +**Evidence**: Calls `0x1693b0` which returns a pointer to the global CFontManager singleton (PIC-computed from GOT), then calls `0x16a200` which allocates a new font entry and returns its HFont handle. Simple wrapper — no arguments besides `this`. + +--- + +### 6. AddGlyphSetToFont(HFont font, const char* windowsFontName, int tall, int weight, int blur, int scanlines, int flags, int nRangeMin, int nRangeMax) → **Slot 67** +- **File offset**: `0x0006a300` +- **Runtime**: `0xe2e58300` (load_bias + 0x6a300) + +```asm +0006a300: push %ebp +0006a301: mov %esp,%ebp +0006a303: push %ebx +0006a304: call 624a9 ; __x86.get_pc_thunk +0006a309: add $0x206cf7,%ebx +0006a30f: sub $0x4,%esp +0006a312: call 1693b0 ; → get CFontManager* +0006a317: sub $8,%esp +0006a31a: push 0x2c(%ebp) ; arg 9: nRangeMax +0006a31d: push 0x28(%ebp) ; arg 8: nRangeMin +0006a320: push 0x24(%ebp) ; arg 7: flags +0006a323: push 0x20(%ebp) ; arg 6: scanlines +0006a326: push 0x1c(%ebp) ; arg 5: blur +0006a329: push 0x18(%ebp) ; arg 4: weight +0006a32c: push 0x14(%ebp) ; arg 3: tall +0006a32f: push 0x10(%ebp) ; arg 2: windowsFontName (const char*) +0006a332: push 0x0c(%ebp) ; arg 1: font (HFont) +0006a335: push %eax ; font manager +0006a336: call 16a550 ; → CFontManager::SetFontGlyphSet() +0006a33b: mov -0x4(%ebp),%ebx +0006a33e: leave +0006a33f: ret +``` + +**Evidence**: Pushes all 9 user-visible arguments (HFont + const char* + 7 ints) plus the font manager pointer onto the stack and calls `0x16a550`. That inner function: +- Multiplies HFont by `0x3c` (60 bytes per font entry) to index into the font array +- Calls `0x169ec0` to resolve the font name string +- Calls `0x16a220` to configure the font with FreeType parameters + +Note: The ISurface.h header in the project names this `SetFontGlyphSet`, but older Source SDK versions (VGUI_Surface030 from HL2) call it `AddGlyphSetToFont`. Same function, different name. + +--- + +## Summary Table + +| Slot | Function | File Offset | Runtime Address | Member Offset | +|------|----------|-------------|-----------------|---------------| +| 17 | `DrawSetTextFont(HFont)` | `0x00069360` | `0xe2e57360` | `this+0x290` | +| 18 | `DrawSetTextColor(int,int,int,int)` | `0x00069380` | `0xe2e57380` | `this+0x28..0x2b` | +| 20 | `DrawSetTextPos(int,int)` | `0x00069440` | `0xe2e57440` | `this+0x2c, +0x30` | +| 22 | `DrawPrintText(wchar_t*,int,FontDrawType_t)` | `0x00072980` | `0xe2e60980` | reads `this+0x290` | +| 66 | `CreateFont()` | `0x0006a2d0` | `0xe2e582d0` | returns HFont | +| 67 | `AddGlyphSetToFont(HFont, const char*, int×7)` | `0x0006a300` | `0xe2e58300` | calls `0x16a550` | + +## Related Slots + +| Slot | Function | Evidence | +|------|----------|----------| +| 10 | `DrawSetColor(int,int,int,int)` | stores at `this+0x24..0x27` (shape color) | +| 11 | `DrawSetColor(Color_t)` | extracts packed int, stores at `this+0x24..0x27` | +| 19 | `DrawSetTextColor(Color_t)` | stores at `this+0x28..0x2b` (text color, via packed Color_t) | +| 21 | `DrawGetTextPos(int&,int&)` | reads from `this+0x2c, +0x30` (inverse of DrawSetTextPos) | + +## CMatSystemSurface Member Layout (inferred) + +| Offset | Type | Field | +|--------|------|-------| +| `+0x00` | `void**` | vtable pointer | +| `+0x10` | `float` | `m_flAlphaMultiplier` | +| `+0x24` | `uint8[4]` | `drawColor` (RGBA for shapes) | +| `+0x28` | `uint8[4]` | `textColor` (RGBA for text) | +| `+0x2c` | `int` | `m_nTextX` | +| `+0x30` | `int` | `m_nTextY` | +| `+0x290` | `HFont` | `m_hCurrentFont` | -- cgit v1.2.3