From aa096fe6886681d3338573cd3af779b57a5b56ab Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 30 Aug 2026 03:32:59 +0200 Subject: init --- src/menu.cpp | 555 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 src/menu.cpp (limited to 'src/menu.cpp') diff --git a/src/menu.cpp b/src/menu.cpp new file mode 100644 index 0000000..91a0948 --- /dev/null +++ b/src/menu.cpp @@ -0,0 +1,555 @@ +// 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(); +} -- cgit v1.2.3