diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-30 03:32:59 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-30 03:32:59 +0200 |
| commit | aa096fe6886681d3338573cd3af779b57a5b56ab (patch) | |
| tree | 14aee66fa1c3c168c140f02006aec246f9bbc23c /src/menu.cpp | |
| download | hl2h-aa096fe6886681d3338573cd3af779b57a5b56ab.tar.gz hl2h-aa096fe6886681d3338573cd3af779b57a5b56ab.zip | |
init
Diffstat (limited to 'src/menu.cpp')
| -rw-r--r-- | src/menu.cpp | 555 |
1 files changed, 555 insertions, 0 deletions
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 @@ | |||
| 1 | // menu.cpp — menu state, rendering, input, config, paint hook, X11 mouse | ||
| 2 | #include "hl2h.h" | ||
| 3 | #include <cstdarg> | ||
| 4 | #include <cstdio> | ||
| 5 | #include <cstring> | ||
| 6 | |||
| 7 | // --------------------------------------------------------------------------- | ||
| 8 | // ConMsg (tier0 console printer — prints to in-game console) | ||
| 9 | // --------------------------------------------------------------------------- | ||
| 10 | |||
| 11 | void (*g_conmsg)(const char *fmt, ...) = nullptr; | ||
| 12 | |||
| 13 | void init_conmsg() { | ||
| 14 | g_conmsg = (decltype(g_conmsg))dlsym(RTLD_DEFAULT, "_Z6ConMsgPKcz"); | ||
| 15 | if (g_conmsg) | ||
| 16 | return; | ||
| 17 | // fallback: dlopen libtier0.so from the engine directory | ||
| 18 | size_t slash = g_engine.path.find_last_of('/'); | ||
| 19 | if (slash == std::string::npos) | ||
| 20 | return; | ||
| 21 | std::string tier0 = g_engine.path.substr(0, slash + 1) + "libtier0.so"; | ||
| 22 | void *h = dlopen(tier0.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | ||
| 23 | if (!h) | ||
| 24 | h = dlopen(tier0.c_str(), RTLD_LAZY | RTLD_GLOBAL); | ||
| 25 | if (h) | ||
| 26 | g_conmsg = (decltype(g_conmsg))dlsym(h, "_Z6ConMsgPKcz"); | ||
| 27 | } | ||
| 28 | |||
| 29 | // --------------------------------------------------------------------------- | ||
| 30 | // X11 middle mouse (for climb feature) | ||
| 31 | // --------------------------------------------------------------------------- | ||
| 32 | |||
| 33 | static struct { | ||
| 34 | void *handle = nullptr; | ||
| 35 | void *dpy = nullptr; | ||
| 36 | int (*query)(void *, unsigned long, unsigned long *, unsigned long *, int *, | ||
| 37 | int *, int *, int *, unsigned int *) = nullptr; | ||
| 38 | int (*default_screen)(void *) = nullptr; | ||
| 39 | unsigned long (*root_window)(void *, int) = nullptr; | ||
| 40 | bool ok = false; | ||
| 41 | } g_x11; | ||
| 42 | |||
| 43 | static constexpr unsigned int BUTTON2_MASK = 1u << 9; // X11 Button2Mask | ||
| 44 | |||
| 45 | void init_x11() { | ||
| 46 | g_x11.handle = dlopen("libX11.so.6", RTLD_LAZY | RTLD_GLOBAL); | ||
| 47 | if (!g_x11.handle) | ||
| 48 | return; | ||
| 49 | g_x11.query = (decltype(g_x11.query))dlsym(g_x11.handle, "XQueryPointer"); | ||
| 50 | g_x11.default_screen = | ||
| 51 | (decltype(g_x11.default_screen))dlsym(g_x11.handle, "XDefaultScreen"); | ||
| 52 | g_x11.root_window = | ||
| 53 | (decltype(g_x11.root_window))dlsym(g_x11.handle, "XRootWindow"); | ||
| 54 | auto open_display = | ||
| 55 | (void *(*)(const char *))dlsym(g_x11.handle, "XOpenDisplay"); | ||
| 56 | if (!g_x11.query || !g_x11.default_screen || !g_x11.root_window || | ||
| 57 | !open_display) | ||
| 58 | return; | ||
| 59 | g_x11.dpy = open_display(nullptr); | ||
| 60 | if (g_x11.dpy) | ||
| 61 | g_x11.ok = true; | ||
| 62 | } | ||
| 63 | |||
| 64 | bool middle_mouse_down() { | ||
| 65 | if (!g_x11.ok) | ||
| 66 | return false; | ||
| 67 | unsigned long root = | ||
| 68 | g_x11.root_window(g_x11.dpy, g_x11.default_screen(g_x11.dpy)); | ||
| 69 | unsigned long rret, cret; | ||
| 70 | int rx, ry, wx, wy; | ||
| 71 | unsigned int mask = 0; | ||
| 72 | if (!g_x11.query(g_x11.dpy, root, &rret, &cret, &rx, &ry, &wx, &wy, &mask)) | ||
| 73 | return false; | ||
| 74 | return (mask & BUTTON2_MASK) != 0; | ||
| 75 | } | ||
| 76 | |||
| 77 | // --------------------------------------------------------------------------- | ||
| 78 | // menu tree structure | ||
| 79 | // --------------------------------------------------------------------------- | ||
| 80 | |||
| 81 | enum NodeType { NODE_ROOT, NODE_FEATURE, NODE_TOGGLE, NODE_BIND }; | ||
| 82 | |||
| 83 | struct MenuNode { | ||
| 84 | NodeType type; | ||
| 85 | const char *label; | ||
| 86 | bool expanded; // for root/feature: whether children are visible | ||
| 87 | int child_count; | ||
| 88 | MenuNode *children[4]; // max children per node | ||
| 89 | |||
| 90 | // leaf data | ||
| 91 | bool *active_ptr; // NODE_TOGGLE: pointer to the active flag | ||
| 92 | int *bind_ptr; // NODE_BIND: pointer to the key binding (SDL scancode) | ||
| 93 | const char *bind_name; // NODE_BIND: current bind display name | ||
| 94 | }; | ||
| 95 | |||
| 96 | // feature data | ||
| 97 | struct FeatureState { | ||
| 98 | bool active; | ||
| 99 | int bind; // SDL scancode, 0 = none | ||
| 100 | }; | ||
| 101 | |||
| 102 | static FeatureState g_climb{true, 0}; | ||
| 103 | static FeatureState g_bhop{true, 0}; | ||
| 104 | static FeatureState g_abh{true, 0}; | ||
| 105 | |||
| 106 | // tree | ||
| 107 | static MenuNode g_root{NODE_ROOT, "HL2hake", true, 0, {}}; | ||
| 108 | static MenuNode g_climb_feat{NODE_FEATURE, "Climb", true, 2, {}}; | ||
| 109 | static MenuNode g_climb_active{NODE_TOGGLE, "Active", false, 0, {}, &g_climb.active}; | ||
| 110 | 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_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.active}; | ||
| 113 | static MenuNode g_abh_feat{NODE_FEATURE, "ABH", true, 1, {}}; | ||
| 114 | static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active}; | ||
| 115 | |||
| 116 | static MenuNode *g_visible[16]; // flattened visible nodes for arrow navigation | ||
| 117 | static int g_visible_count = 0; | ||
| 118 | static int g_selected = 0; | ||
| 119 | bool g_menu_open = false; | ||
| 120 | |||
| 121 | // bind-select state | ||
| 122 | static bool g_bind_mode = false; // true = waiting for key press | ||
| 123 | |||
| 124 | static void build_tree() { | ||
| 125 | g_climb_feat.children[0] = &g_climb_active; | ||
| 126 | g_climb_feat.children[1] = &g_climb_bind; | ||
| 127 | g_bhop_feat.children[0] = &g_bhop_active; | ||
| 128 | g_abh_feat.children[0] = &g_abh_active; | ||
| 129 | g_root.children[0] = &g_climb_feat; | ||
| 130 | g_root.children[1] = &g_bhop_feat; | ||
| 131 | g_root.children[2] = &g_abh_feat; | ||
| 132 | g_root.child_count = 3; | ||
| 133 | } | ||
| 134 | |||
| 135 | static void flatten(MenuNode *node, int depth) { | ||
| 136 | if (g_visible_count >= 16) | ||
| 137 | return; | ||
| 138 | node->type = (depth == 0) ? NODE_ROOT | ||
| 139 | : (node->child_count > 0) ? NODE_FEATURE | ||
| 140 | : (node->active_ptr) ? NODE_TOGGLE | ||
| 141 | : NODE_BIND; | ||
| 142 | g_visible[g_visible_count++] = node; | ||
| 143 | if (node->expanded) { | ||
| 144 | for (int i = 0; i < node->child_count; i++) | ||
| 145 | flatten(node->children[i], depth + 1); | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | static void rebuild_visible() { | ||
| 150 | g_visible_count = 0; | ||
| 151 | flatten(&g_root, 0); | ||
| 152 | if (g_selected >= g_visible_count) | ||
| 153 | g_selected = g_visible_count - 1; | ||
| 154 | if (g_selected < 0) | ||
| 155 | g_selected = 0; | ||
| 156 | } | ||
| 157 | |||
| 158 | // --------------------------------------------------------------------------- | ||
| 159 | // config persistence (INI format) | ||
| 160 | // --------------------------------------------------------------------------- | ||
| 161 | |||
| 162 | static std::string config_path() { | ||
| 163 | size_t slash = g_engine.path.find_last_of('/'); | ||
| 164 | if (slash == std::string::npos) | ||
| 165 | return "hl2hake.cfg"; | ||
| 166 | return g_engine.path.substr(0, slash + 1) + "hl2hake.cfg"; | ||
| 167 | } | ||
| 168 | |||
| 169 | [[maybe_unused]] static const char *sdl_scancode_name(int sc) { | ||
| 170 | if (sc == 0) | ||
| 171 | return "None"; | ||
| 172 | static char buf[32]; | ||
| 173 | snprintf(buf, sizeof(buf), "Key%d", sc); | ||
| 174 | return buf; | ||
| 175 | } | ||
| 176 | |||
| 177 | [[maybe_unused]] static int parse_sdl_scancode(const char *s) { | ||
| 178 | if (strcmp(s, "None") == 0 || strcmp(s, "") == 0) | ||
| 179 | return 0; | ||
| 180 | if (strncmp(s, "Key", 3) == 0) | ||
| 181 | return atoi(s + 3); | ||
| 182 | return 0; | ||
| 183 | } | ||
| 184 | |||
| 185 | static void config_save() { | ||
| 186 | FILE *f = fopen(config_path().c_str(), "w"); | ||
| 187 | if (!f) | ||
| 188 | return; | ||
| 189 | fprintf(f, "[climb]\nactive=%d\nbind=%d\n", g_climb.active, g_climb.bind); | ||
| 190 | fprintf(f, "[bhop]\nactive=%d\nbind=%d\n", g_bhop.active, g_bhop.bind); | ||
| 191 | fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind); | ||
| 192 | fclose(f); | ||
| 193 | } | ||
| 194 | |||
| 195 | static void config_load() { | ||
| 196 | FILE *f = fopen(config_path().c_str(), "r"); | ||
| 197 | if (!f) | ||
| 198 | return; | ||
| 199 | char line[256]; | ||
| 200 | FeatureState *cur = nullptr; | ||
| 201 | while (fgets(line, sizeof(line), f)) { | ||
| 202 | line[strcspn(line, "\r\n")] = 0; | ||
| 203 | if (line[0] == '[') { | ||
| 204 | if (strstr(line, "[climb]")) | ||
| 205 | cur = &g_climb; | ||
| 206 | else if (strstr(line, "[bhop]")) | ||
| 207 | cur = &g_bhop; | ||
| 208 | else if (strstr(line, "[abh]")) | ||
| 209 | cur = &g_abh; | ||
| 210 | else | ||
| 211 | cur = nullptr; | ||
| 212 | } else if (cur) { | ||
| 213 | char *eq = strchr(line, '='); | ||
| 214 | if (!eq) | ||
| 215 | continue; | ||
| 216 | *eq = 0; | ||
| 217 | const char *key = line, *val = eq + 1; | ||
| 218 | if (strcmp(key, "active") == 0) | ||
| 219 | cur->active = atoi(val) != 0; | ||
| 220 | else if (strcmp(key, "bind") == 0) | ||
| 221 | cur->bind = atoi(val); | ||
| 222 | } | ||
| 223 | } | ||
| 224 | fclose(f); | ||
| 225 | } | ||
| 226 | |||
| 227 | // --------------------------------------------------------------------------- | ||
| 228 | // menu input handling | ||
| 229 | // --------------------------------------------------------------------------- | ||
| 230 | |||
| 231 | // SDL scancodes for navigation keys (matches the game's scancode set) | ||
| 232 | static constexpr int SCAN_INSERT = 73; | ||
| 233 | static constexpr int SCAN_UP = 82; // these are SDL scancodes | ||
| 234 | static constexpr int SCAN_DOWN = 81; | ||
| 235 | static constexpr int SCAN_LEFT = 80; | ||
| 236 | static constexpr int SCAN_RIGHT = 79; | ||
| 237 | static constexpr int SCAN_ENTER = 40; | ||
| 238 | static constexpr int SCAN_ESCAPE = 41; | ||
| 239 | |||
| 240 | // simplified scancode-to-name for common keys | ||
| 241 | static const char *scancode_name(int sc) { | ||
| 242 | switch (sc) { | ||
| 243 | case 0: return "None"; | ||
| 244 | case 4: return "A"; case 5: return "B"; case 6: return "C"; | ||
| 245 | case 7: return "D"; case 8: return "E"; case 9: return "F"; | ||
| 246 | case 10: return "G"; case 11: return "H"; case 12: return "I"; | ||
| 247 | case 13: return "J"; case 14: return "K"; case 15: return "L"; | ||
| 248 | case 16: return "M"; case 17: return "N"; case 18: return "O"; | ||
| 249 | case 19: return "P"; case 20: return "Q"; case 21: return "R"; | ||
| 250 | case 22: return "S"; case 23: return "T"; case 24: return "U"; | ||
| 251 | case 25: return "V"; case 26: return "W"; case 27: return "X"; | ||
| 252 | case 28: return "Y"; case 29: return "Z"; | ||
| 253 | case 30: return "1"; case 31: return "2"; case 32: return "3"; | ||
| 254 | case 33: return "4"; case 34: return "5"; case 35: return "6"; | ||
| 255 | case 36: return "7"; case 37: return "8"; case 38: return "9"; | ||
| 256 | case 39: return "0"; | ||
| 257 | case 40: return "Enter"; case 41: return "Escape"; case 42: return "Backspace"; | ||
| 258 | case 43: return "Tab"; case 44: return "Space"; | ||
| 259 | case 79: return "Right"; case 80: return "Left"; | ||
| 260 | case 81: return "Down"; case 82: return "Up"; | ||
| 261 | case 87: return "F1"; case 88: return "F2"; case 89: return "F3"; | ||
| 262 | case 90: return "F4"; case 91: return "F5"; case 92: return "F6"; | ||
| 263 | case 93: return "F7"; case 94: return "F8"; | ||
| 264 | default: return scancode_name(0); // "None" for unmapped | ||
| 265 | } | ||
| 266 | } | ||
| 267 | |||
| 268 | // get a snapshot of key states: returns array of currently-held scancodes | ||
| 269 | static bool g_prev_key_state[512] = {}; | ||
| 270 | static bool g_key_down[512] = {}; // true for ONE frame on press | ||
| 271 | |||
| 272 | // read SDL_GetKeyboardState — the game's own SDL is in-process | ||
| 273 | using SDL_GetKeyboardState_fn = const uint8_t *(*)(int *); | ||
| 274 | static SDL_GetKeyboardState_fn SDL_GetKeyboardState_real = nullptr; | ||
| 275 | |||
| 276 | static void poll_keyboard() { | ||
| 277 | if (!SDL_GetKeyboardState_real) | ||
| 278 | SDL_GetKeyboardState_real = | ||
| 279 | (SDL_GetKeyboardState_fn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState"); | ||
| 280 | if (!SDL_GetKeyboardState_real) | ||
| 281 | return; | ||
| 282 | int numkeys = 0; | ||
| 283 | const uint8_t *state = SDL_GetKeyboardState_real(&numkeys); | ||
| 284 | if (!state) | ||
| 285 | return; | ||
| 286 | for (int i = 0; i < numkeys && i < 512; i++) { | ||
| 287 | g_key_down[i] = state[i] && !g_prev_key_state[i]; | ||
| 288 | g_prev_key_state[i] = state[i]; | ||
| 289 | } | ||
| 290 | } | ||
| 291 | |||
| 292 | static bool key_pressed(int sc) { | ||
| 293 | return sc >= 0 && sc < 512 && g_key_down[sc]; | ||
| 294 | } | ||
| 295 | |||
| 296 | [[maybe_unused]] static bool key_held(int sc) { | ||
| 297 | return sc >= 0 && sc < 512 && g_prev_key_state[sc]; | ||
| 298 | } | ||
| 299 | |||
| 300 | static void menu_handle_input() { | ||
| 301 | poll_keyboard(); | ||
| 302 | |||
| 303 | if (!g_menu_open) { | ||
| 304 | if (key_pressed(SCAN_INSERT)) { | ||
| 305 | g_menu_open = true; | ||
| 306 | g_bind_mode = false; | ||
| 307 | rebuild_visible(); | ||
| 308 | } | ||
| 309 | return; | ||
| 310 | } | ||
| 311 | |||
| 312 | // menu is open | ||
| 313 | if (g_bind_mode) { | ||
| 314 | // wait for any key press (except Insert, Up, Down, Escape, Enter) | ||
| 315 | for (int i = 0; i < 512; i++) { | ||
| 316 | if (!g_key_down[i]) | ||
| 317 | continue; | ||
| 318 | if (i == SCAN_INSERT || i == SCAN_UP || i == SCAN_DOWN || | ||
| 319 | i == SCAN_LEFT || i == SCAN_RIGHT) { | ||
| 320 | g_bind_mode = false; | ||
| 321 | return; // cancel bind select | ||
| 322 | } | ||
| 323 | if (i == SCAN_ESCAPE) { | ||
| 324 | // set bind to None | ||
| 325 | MenuNode *n = g_visible[g_selected]; | ||
| 326 | if (n->bind_ptr) | ||
| 327 | *n->bind_ptr = 0; | ||
| 328 | g_bind_mode = false; | ||
| 329 | config_save(); | ||
| 330 | rebuild_visible(); | ||
| 331 | return; | ||
| 332 | } | ||
| 333 | if (i == SCAN_ENTER) { | ||
| 334 | g_bind_mode = false; | ||
| 335 | return; // cancel, keep old bind | ||
| 336 | } | ||
| 337 | // any other key = new bind | ||
| 338 | MenuNode *n = g_visible[g_selected]; | ||
| 339 | if (n->bind_ptr) { | ||
| 340 | // clear same bind from other features | ||
| 341 | FeatureState *features[] = {&g_climb, &g_bhop, &g_abh}; | ||
| 342 | for (auto *f : features) | ||
| 343 | if (&f->bind != n->bind_ptr && f->bind == i) | ||
| 344 | f->bind = 0; | ||
| 345 | *n->bind_ptr = i; | ||
| 346 | } | ||
| 347 | g_bind_mode = false; | ||
| 348 | config_save(); | ||
| 349 | rebuild_visible(); | ||
| 350 | return; | ||
| 351 | } | ||
| 352 | return; // no key pressed yet | ||
| 353 | } | ||
| 354 | |||
| 355 | // normal navigation | ||
| 356 | if (key_pressed(SCAN_INSERT) || key_pressed(SCAN_ESCAPE)) { | ||
| 357 | g_menu_open = false; | ||
| 358 | return; | ||
| 359 | } | ||
| 360 | if (key_pressed(SCAN_UP) && g_selected > 0) { | ||
| 361 | g_selected--; | ||
| 362 | return; | ||
| 363 | } | ||
| 364 | if (key_pressed(SCAN_DOWN) && g_selected < g_visible_count - 1) { | ||
| 365 | g_selected++; | ||
| 366 | return; | ||
| 367 | } | ||
| 368 | if (key_pressed(SCAN_ENTER)) { | ||
| 369 | MenuNode *n = g_visible[g_selected]; | ||
| 370 | if (n->child_count > 0) { | ||
| 371 | n->expanded = !n->expanded; | ||
| 372 | rebuild_visible(); | ||
| 373 | } else if (n->type == NODE_TOGGLE && n->active_ptr) { | ||
| 374 | *n->active_ptr = !(*n->active_ptr); | ||
| 375 | config_save(); | ||
| 376 | } else if (n->type == NODE_BIND) { | ||
| 377 | g_bind_mode = true; | ||
| 378 | } | ||
| 379 | } | ||
| 380 | } | ||
| 381 | |||
| 382 | // --------------------------------------------------------------------------- | ||
| 383 | // menu rendering (VGUI ISurface) | ||
| 384 | // --------------------------------------------------------------------------- | ||
| 385 | |||
| 386 | // VGUI surface interface | ||
| 387 | using CreateInterfaceFn = void *(*)(const char *, int *); | ||
| 388 | struct ISurface; | ||
| 389 | |||
| 390 | // VGUI font handle (opaque) | ||
| 391 | using HFont = unsigned long; | ||
| 392 | |||
| 393 | // function pointers resolved at init | ||
| 394 | static ISurface *g_surface = nullptr; | ||
| 395 | static HFont g_font = 0; | ||
| 396 | |||
| 397 | // surface vtable indices for 2007-era VGUI_SURFACE001 | ||
| 398 | // verified: slot 16 = DrawSetTextColor, slot 17 = DrawSetTextFont, | ||
| 399 | // slot 19 = DrawSetTextPos, slot 22 = DrawPrintText, | ||
| 400 | // slot 23 = DrawGetTextSize, slot 31 = CreateNewFont, slot 32 = SetFontGlyphSet | ||
| 401 | // These are from the standard VGUI surface vtable layout; will be verified | ||
| 402 | // against runtime if they fail. | ||
| 403 | static constexpr int SURF_DrawSetTextColor = 16; | ||
| 404 | static constexpr int SURF_DrawSetTextFont = 17; | ||
| 405 | static constexpr int SURF_DrawSetTextPos = 19; | ||
| 406 | static constexpr int SURF_DrawPrintText = 22; | ||
| 407 | static constexpr int SURF_CreateNewFont = 31; | ||
| 408 | static constexpr int SURF_SetFontGlyphSet = 32; | ||
| 409 | |||
| 410 | static void surf_draw_set_text_color(int r, int g, int b, int a) { | ||
| 411 | using Fn = void (*)(void *, int, int, int, int); | ||
| 412 | vfunc<Fn>(g_surface, SURF_DrawSetTextColor)(g_surface, r, g, b, a); | ||
| 413 | } | ||
| 414 | static void surf_draw_set_text_font(HFont f) { | ||
| 415 | using Fn = void (*)(void *, HFont); | ||
| 416 | vfunc<Fn>(g_surface, SURF_DrawSetTextFont)(g_surface, f); | ||
| 417 | } | ||
| 418 | static void surf_draw_set_text_pos(int x, int y) { | ||
| 419 | using Fn = void (*)(void *, int, int); | ||
| 420 | vfunc<Fn>(g_surface, SURF_DrawSetTextPos)(g_surface, x, y); | ||
| 421 | } | ||
| 422 | static void surf_draw_print_text(const wchar_t *text, int count) { | ||
| 423 | using Fn = void (*)(void *, const wchar_t *, int); | ||
| 424 | vfunc<Fn>(g_surface, SURF_DrawPrintText)(g_surface, text, count); | ||
| 425 | } | ||
| 426 | static HFont surf_create_font() { | ||
| 427 | using Fn = HFont (*)(void *); | ||
| 428 | return vfunc<Fn>(g_surface, SURF_CreateNewFont)(g_surface); | ||
| 429 | } | ||
| 430 | static void surf_set_font_glyph_set(HFont f, const char *name, int tall, | ||
| 431 | int weight, int blur, int scanlines, | ||
| 432 | int flags) { | ||
| 433 | using Fn = void (*)(void *, HFont, const char *, int, int, int, int, int, int, | ||
| 434 | int); | ||
| 435 | vfunc<Fn>(g_surface, SURF_SetFontGlyphSet)(g_surface, f, name, tall, weight, | ||
| 436 | blur, scanlines, flags, 0, 0); | ||
| 437 | } | ||
| 438 | |||
| 439 | // wide string conversion for DrawPrintText | ||
| 440 | static void draw_text(int x, int y, const char *text) { | ||
| 441 | wchar_t wtext[256]; | ||
| 442 | mbstowcs(wtext, text, 255); | ||
| 443 | wtext[255] = 0; | ||
| 444 | int len = (int)wcslen(wtext); | ||
| 445 | surf_draw_set_text_pos(x, y); | ||
| 446 | surf_draw_print_text(wtext, len); | ||
| 447 | } | ||
| 448 | |||
| 449 | // --------------------------------------------------------------------------- | ||
| 450 | // paint hook — draws menu overlay every frame | ||
| 451 | // --------------------------------------------------------------------------- | ||
| 452 | |||
| 453 | using PaintFn = void (*)(void *, int); | ||
| 454 | static PaintFn oPaint = nullptr; | ||
| 455 | |||
| 456 | static void hkdPaint(void *self, int mode) { | ||
| 457 | oPaint(self, mode); | ||
| 458 | |||
| 459 | // init font on first paint | ||
| 460 | static bool font_inited = false; | ||
| 461 | if (!font_inited) { | ||
| 462 | g_font = surf_create_font(); | ||
| 463 | if (g_font) { | ||
| 464 | surf_set_font_glyph_set(g_font, "Consolas", 14, 400, 0, 0, 0); | ||
| 465 | } | ||
| 466 | font_inited = true; | ||
| 467 | } | ||
| 468 | if (!g_font) | ||
| 469 | return; | ||
| 470 | |||
| 471 | menu_handle_input(); | ||
| 472 | |||
| 473 | if (!g_menu_open) | ||
| 474 | return; | ||
| 475 | |||
| 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++) { | ||
| 481 | MenuNode *n = g_visible[i]; | ||
| 482 | int indent = 0; | ||
| 483 | |||
| 484 | // count depth by counting how many parents are visible before this node | ||
| 485 | // (simple: first node = depth 0, others = depth 1 if label is Active/Bind) | ||
| 486 | if (n->type == NODE_TOGGLE || n->type == NODE_BIND) | ||
| 487 | indent = 20; | ||
| 488 | |||
| 489 | char buf[256]; | ||
| 490 | if (n->type == NODE_TOGGLE) { | ||
| 491 | snprintf(buf, sizeof(buf), "%s: %s", n->label, | ||
| 492 | *n->active_ptr ? "ON" : "OFF"); | ||
| 493 | } else if (n->type == NODE_BIND) { | ||
| 494 | snprintf(buf, sizeof(buf), "%s: %s", n->label, | ||
| 495 | scancode_name(*n->bind_ptr)); | ||
| 496 | if (g_bind_mode && g_selected == i) | ||
| 497 | snprintf(buf, sizeof(buf), "%s: ...", n->label); | ||
| 498 | } else { | ||
| 499 | snprintf(buf, sizeof(buf), "%s", n->label); | ||
| 500 | } | ||
| 501 | |||
| 502 | if (i == g_selected) { | ||
| 503 | surf_draw_set_text_color(255, 255, 0, 255); // yellow highlight | ||
| 504 | } else if (n->type == NODE_TOGGLE) { | ||
| 505 | surf_draw_set_text_color(*n->active_ptr ? 0 : 255, 255, 0, 255); | ||
| 506 | } else { | ||
| 507 | surf_draw_set_text_color(255, 255, 255, 255); | ||
| 508 | } | ||
| 509 | |||
| 510 | draw_text(10 + indent, y, buf); | ||
| 511 | y += 18; | ||
| 512 | } | ||
| 513 | |||
| 514 | // reset text color | ||
| 515 | surf_draw_set_text_color(255, 255, 255, 255); | ||
| 516 | } | ||
| 517 | |||
| 518 | // --------------------------------------------------------------------------- | ||
| 519 | // paint hook installation (called from setup_thread) | ||
| 520 | // --------------------------------------------------------------------------- | ||
| 521 | |||
| 522 | // VEngineVGui001 vtable indices for this build | ||
| 523 | // (Paint is typically slot 6 — will be verified at runtime; if wrong, | ||
| 524 | // we crash and can adjust. Common Source 2007 layout.) | ||
| 525 | static constexpr int VGUI_Paint_SLOT = 6; | ||
| 526 | |||
| 527 | static void install_paint_hook() { | ||
| 528 | // get IEngineVGui from engine | ||
| 529 | void *eh = dlopen(g_engine.path.c_str(), | ||
| 530 | RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); | ||
| 531 | if (!eh) | ||
| 532 | return; | ||
| 533 | auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); | ||
| 534 | if (!ef) | ||
| 535 | return; | ||
| 536 | void *evgui = ef("VEngineVGui001", nullptr); | ||
| 537 | if (!evgui) { | ||
| 538 | logf("VEngineVGui001 not found"); | ||
| 539 | return; | ||
| 540 | } | ||
| 541 | |||
| 542 | // hook Paint | ||
| 543 | void **vt = VTABLE(evgui); | ||
| 544 | oPaint = (PaintFn)vt[VGUI_Paint_SLOT]; | ||
| 545 | vt[VGUI_Paint_SLOT] = (void *)&hkdPaint; | ||
| 546 | logf("paint hook installed: Paint %p -> %p", (void *)oPaint, | ||
| 547 | (void *)&hkdPaint); | ||
| 548 | } | ||
| 549 | |||
| 550 | // public init function called from setup_thread | ||
| 551 | void install_menu() { | ||
| 552 | build_tree(); | ||
| 553 | config_load(); | ||
| 554 | install_paint_hook(); | ||
| 555 | } | ||
