summaryrefslogtreecommitdiffstats
path: root/src/menu.cpp
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-30 03:32:59 +0200
committersalmonik <salmonik@tuta.io>2026-08-30 03:32:59 +0200
commitaa096fe6886681d3338573cd3af779b57a5b56ab (patch)
tree14aee66fa1c3c168c140f02006aec246f9bbc23c /src/menu.cpp
downloadhl2h-aa096fe6886681d3338573cd3af779b57a5b56ab.tar.gz
hl2h-aa096fe6886681d3338573cd3af779b57a5b56ab.zip
init
Diffstat (limited to '')
-rw-r--r--src/menu.cpp555
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
11void (*g_conmsg)(const char *fmt, ...) = nullptr;
12
13void 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
33static 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
43static constexpr unsigned int BUTTON2_MASK = 1u << 9; // X11 Button2Mask
44
45void 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
64bool 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
81enum NodeType { NODE_ROOT, NODE_FEATURE, NODE_TOGGLE, NODE_BIND };
82
83struct 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
97struct FeatureState {
98 bool active;
99 int bind; // SDL scancode, 0 = none
100};
101
102static FeatureState g_climb{true, 0};
103static FeatureState g_bhop{true, 0};
104static FeatureState g_abh{true, 0};
105
106// tree
107static MenuNode g_root{NODE_ROOT, "HL2hake", true, 0, {}};
108static MenuNode g_climb_feat{NODE_FEATURE, "Climb", true, 2, {}};
109static MenuNode g_climb_active{NODE_TOGGLE, "Active", false, 0, {}, &g_climb.active};
110static MenuNode g_climb_bind{NODE_BIND, "Bind", false, 0, {}, nullptr, &g_climb.bind, "None"};
111static MenuNode g_bhop_feat{NODE_FEATURE, "Bunnyhop", true, 1, {}};
112static MenuNode g_bhop_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.active};
113static MenuNode g_abh_feat{NODE_FEATURE, "ABH", true, 1, {}};
114static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active};
115
116static MenuNode *g_visible[16]; // flattened visible nodes for arrow navigation
117static int g_visible_count = 0;
118static int g_selected = 0;
119bool g_menu_open = false;
120
121// bind-select state
122static bool g_bind_mode = false; // true = waiting for key press
123
124static 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
135static 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
149static 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
162static 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
185static 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
195static 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)
232static constexpr int SCAN_INSERT = 73;
233static constexpr int SCAN_UP = 82; // these are SDL scancodes
234static constexpr int SCAN_DOWN = 81;
235static constexpr int SCAN_LEFT = 80;
236static constexpr int SCAN_RIGHT = 79;
237static constexpr int SCAN_ENTER = 40;
238static constexpr int SCAN_ESCAPE = 41;
239
240// simplified scancode-to-name for common keys
241static 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
269static bool g_prev_key_state[512] = {};
270static bool g_key_down[512] = {}; // true for ONE frame on press
271
272// read SDL_GetKeyboardState — the game's own SDL is in-process
273using SDL_GetKeyboardState_fn = const uint8_t *(*)(int *);
274static SDL_GetKeyboardState_fn SDL_GetKeyboardState_real = nullptr;
275
276static 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
292static 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
300static 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
387using CreateInterfaceFn = void *(*)(const char *, int *);
388struct ISurface;
389
390// VGUI font handle (opaque)
391using HFont = unsigned long;
392
393// function pointers resolved at init
394static ISurface *g_surface = nullptr;
395static 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.
403static constexpr int SURF_DrawSetTextColor = 16;
404static constexpr int SURF_DrawSetTextFont = 17;
405static constexpr int SURF_DrawSetTextPos = 19;
406static constexpr int SURF_DrawPrintText = 22;
407static constexpr int SURF_CreateNewFont = 31;
408static constexpr int SURF_SetFontGlyphSet = 32;
409
410static 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}
414static 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}
418static 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}
422static 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}
426static HFont surf_create_font() {
427 using Fn = HFont (*)(void *);
428 return vfunc<Fn>(g_surface, SURF_CreateNewFont)(g_surface);
429}
430static 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
440static 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
453using PaintFn = void (*)(void *, int);
454static PaintFn oPaint = nullptr;
455
456static 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.)
525static constexpr int VGUI_Paint_SLOT = 6;
526
527static 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
551void install_menu() {
552 build_tree();
553 config_load();
554 install_paint_hook();
555}