// menu.cpp — menu state, rendering, input, config, paint hook, X11 mouse #include "hl2h.h" #include #include #include // --------------------------------------------------------------------------- // ConMsg (tier0 console printer — prints to in-game console) // --------------------------------------------------------------------------- void (*g_conmsg)(const char *fmt, ...) = nullptr; void init_conmsg() { g_conmsg = (decltype(g_conmsg))dlsym(RTLD_DEFAULT, "_Z6ConMsgPKcz"); if (g_conmsg) return; // fallback: dlopen libtier0.so from the engine directory size_t slash = g_engine.path.find_last_of('/'); if (slash == std::string::npos) return; std::string tier0 = g_engine.path.substr(0, slash + 1) + "libtier0.so"; void *h = dlopen(tier0.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); if (!h) h = dlopen(tier0.c_str(), RTLD_LAZY | RTLD_GLOBAL); if (h) g_conmsg = (decltype(g_conmsg))dlsym(h, "_Z6ConMsgPKcz"); } // --------------------------------------------------------------------------- // X11 middle mouse (for climb feature) // --------------------------------------------------------------------------- static struct { void *handle = nullptr; void *dpy = nullptr; int (*query)(void *, unsigned long, unsigned long *, unsigned long *, int *, int *, int *, int *, unsigned int *) = nullptr; int (*default_screen)(void *) = nullptr; unsigned long (*root_window)(void *, int) = nullptr; bool ok = false; } g_x11; static constexpr unsigned int BUTTON2_MASK = 1u << 9; // X11 Button2Mask void init_x11() { g_x11.handle = dlopen("libX11.so.6", RTLD_LAZY | RTLD_GLOBAL); if (!g_x11.handle) return; g_x11.query = (decltype(g_x11.query))dlsym(g_x11.handle, "XQueryPointer"); g_x11.default_screen = (decltype(g_x11.default_screen))dlsym(g_x11.handle, "XDefaultScreen"); g_x11.root_window = (decltype(g_x11.root_window))dlsym(g_x11.handle, "XRootWindow"); auto open_display = (void *(*)(const char *))dlsym(g_x11.handle, "XOpenDisplay"); if (!g_x11.query || !g_x11.default_screen || !g_x11.root_window || !open_display) return; g_x11.dpy = open_display(nullptr); if (g_x11.dpy) g_x11.ok = true; } bool middle_mouse_down() { if (!g_x11.ok) return false; unsigned long root = g_x11.root_window(g_x11.dpy, g_x11.default_screen(g_x11.dpy)); unsigned long rret, cret; int rx, ry, wx, wy; unsigned int mask = 0; if (!g_x11.query(g_x11.dpy, root, &rret, &cret, &rx, &ry, &wx, &wy, &mask)) return false; return (mask & BUTTON2_MASK) != 0; } // --------------------------------------------------------------------------- // menu tree structure // --------------------------------------------------------------------------- enum NodeType { NODE_ROOT, NODE_FEATURE, NODE_TOGGLE, NODE_BIND }; struct MenuNode { NodeType type; const char *label; bool expanded; // for root/feature: whether children are visible int child_count; MenuNode *children[4]; // max children per node // leaf data bool *active_ptr; // NODE_TOGGLE: pointer to the active flag int *bind_ptr; // NODE_BIND: pointer to the key binding (SDL scancode) const char *bind_name; // NODE_BIND: current bind display name }; // feature data struct FeatureState { bool active; int bind; // SDL scancode, 0 = none }; static FeatureState g_climb{true, 0}; 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_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_visible[16]; // flattened visible nodes for arrow navigation static int g_visible_count = 0; static int g_selected = 0; bool g_menu_open = false; // bind-select state static bool g_bind_mode = false; // true = waiting for key press static void build_tree() { g_climb_feat.children[0] = &g_climb_active; g_climb_feat.children[1] = &g_climb_bind; g_bhop_feat.children[0] = &g_bhop_active; g_abh_feat.children[0] = &g_abh_active; g_root.children[0] = &g_climb_feat; g_root.children[1] = &g_bhop_feat; g_root.children[2] = &g_abh_feat; g_root.child_count = 3; } static void flatten(MenuNode *node, int depth) { if (g_visible_count >= 16) return; node->type = (depth == 0) ? NODE_ROOT : (node->child_count > 0) ? NODE_FEATURE : (node->active_ptr) ? NODE_TOGGLE : NODE_BIND; g_visible[g_visible_count++] = node; if (node->expanded) { for (int i = 0; i < node->child_count; i++) flatten(node->children[i], depth + 1); } } static void rebuild_visible() { g_visible_count = 0; flatten(&g_root, 0); if (g_selected >= g_visible_count) g_selected = g_visible_count - 1; if (g_selected < 0) g_selected = 0; } // --------------------------------------------------------------------------- // config persistence (INI format) // --------------------------------------------------------------------------- static std::string config_path() { size_t slash = g_engine.path.find_last_of('/'); if (slash == std::string::npos) return "hl2hake.cfg"; return g_engine.path.substr(0, slash + 1) + "hl2hake.cfg"; } [[maybe_unused]] static const char *sdl_scancode_name(int sc) { if (sc == 0) return "None"; static char buf[32]; snprintf(buf, sizeof(buf), "Key%d", sc); return buf; } [[maybe_unused]] static int parse_sdl_scancode(const char *s) { if (strcmp(s, "None") == 0 || strcmp(s, "") == 0) return 0; if (strncmp(s, "Key", 3) == 0) return atoi(s + 3); return 0; } static void config_save() { FILE *f = fopen(config_path().c_str(), "w"); if (!f) return; fprintf(f, "[climb]\nactive=%d\nbind=%d\n", g_climb.active, g_climb.bind); fprintf(f, "[bhop]\nactive=%d\nbind=%d\n", g_bhop.active, g_bhop.bind); fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind); fclose(f); } static void config_load() { FILE *f = fopen(config_path().c_str(), "r"); if (!f) return; char line[256]; FeatureState *cur = nullptr; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { if (strstr(line, "[climb]")) cur = &g_climb; else if (strstr(line, "[bhop]")) cur = &g_bhop; else if (strstr(line, "[abh]")) cur = &g_abh; else cur = nullptr; } else if (cur) { char *eq = strchr(line, '='); if (!eq) continue; *eq = 0; const char *key = line, *val = eq + 1; if (strcmp(key, "active") == 0) cur->active = atoi(val) != 0; else if (strcmp(key, "bind") == 0) cur->bind = atoi(val); } } fclose(f); } // --------------------------------------------------------------------------- // menu input handling // --------------------------------------------------------------------------- // SDL scancodes for navigation keys (matches the game's scancode set) static constexpr int SCAN_INSERT = 73; static constexpr int SCAN_UP = 82; // these are SDL scancodes static constexpr int SCAN_DOWN = 81; static constexpr int SCAN_LEFT = 80; static constexpr int SCAN_RIGHT = 79; static constexpr int SCAN_ENTER = 40; static constexpr int SCAN_ESCAPE = 41; // simplified scancode-to-name for common keys static const char *scancode_name(int sc) { switch (sc) { case 0: return "None"; case 4: return "A"; case 5: return "B"; case 6: return "C"; case 7: return "D"; case 8: return "E"; case 9: return "F"; case 10: return "G"; case 11: return "H"; case 12: return "I"; case 13: return "J"; case 14: return "K"; case 15: return "L"; case 16: return "M"; case 17: return "N"; case 18: return "O"; case 19: return "P"; case 20: return "Q"; case 21: return "R"; case 22: return "S"; case 23: return "T"; case 24: return "U"; case 25: return "V"; case 26: return "W"; case 27: return "X"; case 28: return "Y"; case 29: return "Z"; case 30: return "1"; case 31: return "2"; case 32: return "3"; case 33: return "4"; case 34: return "5"; case 35: return "6"; case 36: return "7"; case 37: return "8"; case 38: return "9"; case 39: return "0"; case 40: return "Enter"; case 41: return "Escape"; case 42: return "Backspace"; case 43: return "Tab"; case 44: return "Space"; case 79: return "Right"; case 80: return "Left"; case 81: return "Down"; case 82: return "Up"; case 87: return "F1"; case 88: return "F2"; case 89: return "F3"; case 90: return "F4"; case 91: return "F5"; case 92: return "F6"; case 93: return "F7"; case 94: return "F8"; default: return scancode_name(0); // "None" for unmapped } } // get a snapshot of key states: returns array of currently-held scancodes static bool g_prev_key_state[512] = {}; static bool g_key_down[512] = {}; // true for ONE frame on press // read SDL_GetKeyboardState — the game's own SDL is in-process using SDL_GetKeyboardState_fn = const uint8_t *(*)(int *); static SDL_GetKeyboardState_fn SDL_GetKeyboardState_real = nullptr; static void poll_keyboard() { if (!SDL_GetKeyboardState_real) SDL_GetKeyboardState_real = (SDL_GetKeyboardState_fn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState"); if (!SDL_GetKeyboardState_real) return; int numkeys = 0; const uint8_t *state = SDL_GetKeyboardState_real(&numkeys); if (!state) return; for (int i = 0; i < numkeys && i < 512; i++) { g_key_down[i] = state[i] && !g_prev_key_state[i]; g_prev_key_state[i] = state[i]; } } static bool key_pressed(int sc) { return sc >= 0 && sc < 512 && g_key_down[sc]; } [[maybe_unused]] static bool key_held(int sc) { return sc >= 0 && sc < 512 && g_prev_key_state[sc]; } static void menu_handle_input() { poll_keyboard(); if (!g_menu_open) { if (key_pressed(SCAN_INSERT)) { g_menu_open = true; g_bind_mode = false; rebuild_visible(); } return; } // menu is open if (g_bind_mode) { // wait for any key press (except Insert, Up, Down, Escape, Enter) for (int i = 0; i < 512; i++) { if (!g_key_down[i]) continue; if (i == SCAN_INSERT || i == SCAN_UP || i == SCAN_DOWN || i == SCAN_LEFT || i == SCAN_RIGHT) { g_bind_mode = false; return; // cancel bind select } if (i == SCAN_ESCAPE) { // set bind to None MenuNode *n = g_visible[g_selected]; if (n->bind_ptr) *n->bind_ptr = 0; g_bind_mode = false; config_save(); rebuild_visible(); return; } if (i == SCAN_ENTER) { g_bind_mode = false; return; // cancel, keep old bind } // any other key = new bind MenuNode *n = g_visible[g_selected]; if (n->bind_ptr) { // clear same bind from other features FeatureState *features[] = {&g_climb, &g_bhop, &g_abh}; for (auto *f : features) if (&f->bind != n->bind_ptr && f->bind == i) f->bind = 0; *n->bind_ptr = i; } g_bind_mode = false; config_save(); rebuild_visible(); return; } return; // no key pressed yet } // normal navigation if (key_pressed(SCAN_INSERT) || key_pressed(SCAN_ESCAPE)) { g_menu_open = false; return; } if (key_pressed(SCAN_UP) && g_selected > 0) { g_selected--; return; } if (key_pressed(SCAN_DOWN) && g_selected < g_visible_count - 1) { g_selected++; return; } if (key_pressed(SCAN_ENTER)) { MenuNode *n = g_visible[g_selected]; if (n->child_count > 0) { n->expanded = !n->expanded; rebuild_visible(); } else if (n->type == NODE_TOGGLE && n->active_ptr) { *n->active_ptr = !(*n->active_ptr); config_save(); } else if (n->type == NODE_BIND) { g_bind_mode = true; } } } // --------------------------------------------------------------------------- // menu rendering (VGUI ISurface) // --------------------------------------------------------------------------- // 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; 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; static constexpr int SURF_DrawSetTextFont = 17; static constexpr int SURF_DrawSetTextPos = 19; static constexpr int SURF_DrawPrintText = 22; static constexpr int SURF_CreateNewFont = 31; static constexpr int SURF_SetFontGlyphSet = 32; 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); } static void surf_draw_set_text_font(HFont f) { using Fn = void (*)(void *, HFont); vfunc(g_surface, SURF_DrawSetTextFont)(g_surface, f); } static void surf_draw_set_text_pos(int x, int y) { using Fn = void (*)(void *, int, int); 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); } static HFont surf_create_font() { using Fn = HFont (*)(void *); return vfunc(g_surface, SURF_CreateNewFont)(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, int); vfunc(g_surface, SURF_SetFontGlyphSet)(g_surface, f, name, tall, weight, blur, scanlines, flags, 0, 0); } // wide string conversion for DrawPrintText static void draw_text(int x, int y, const char *text) { wchar_t wtext[256]; mbstowcs(wtext, text, 255); wtext[255] = 0; int len = (int)wcslen(wtext); 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) { g_font = surf_create_font(); if (g_font) { surf_set_font_glyph_set(g_font, "Consolas", 14, 400, 0, 0, 0); } font_inited = true; } if (!g_font) return; menu_handle_input(); if (!g_menu_open) 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]; if (n->type == NODE_TOGGLE) { snprintf(buf, sizeof(buf), "%s: %s", n->label, *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); } draw_text(10 + indent, y, buf); y += 18; } // reset text color surf_draw_set_text_color(255, 255, 255, 255); } // --------------------------------------------------------------------------- // paint hook installation (called from setup_thread) // --------------------------------------------------------------------------- // 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; } // 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); } // public init function called from setup_thread void install_menu() { build_tree(); config_load(); install_paint_hook(); }