summaryrefslogtreecommitdiffstats
path: root/src/gui.cpp
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:09:55 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:09:55 +0100
commit33f5ed9ef58f26c6b3b52cf605dd4311d658a60d (patch)
treea97254872e465c9107b7ef19695ab56bb076d7e2 /src/gui.cpp
parentItems: log the usable ammo table once (diff)
downloadhl2h-33f5ed9ef58f26c6b3b52cf605dd4311d658a60d.tar.gz
hl2h-33f5ed9ef58f26c6b3b52cf605dd4311d658a60d.zip
Fix audit findings: crash safety, correctness, rendering, performance
Crash/UB: - gate every hook behind an init-stage flag until config/surface/input are ready - publish module maps as immutable atomic snapshots instead of reallocating shared vectors under unlocked readers - validate the %ebp walk in the RandomSeed/RandomFloat hooks against the stack - bounds-check every RecvTable/RecvProp/string dereference with module_range() - preserve the Itanium vtable header when cloning (client, CInput, model render, studio render) - reject negative usercmd sequence numbers; guard config_load against a null section target; cap the bone parent walk; check mprotect; drop PROT_EXEC on the GOT patch; absorb server.so .bss tail Correctness: - filter dormant entities across ESP, aimbot, triggerbot, melee, items and projectiles (slot self-tested against the local player) - reset the server-health latch per map and only latch on valid model indices - bone cache sized for the 128-target scan; set valid=false on every failure; head_position keeps searching past an invalid bone - vischeck steps past glass by probing until clear, and ignores entities owned by the target (held weapons, bone followers) - aimbot: maxs no longer disables it, AABB-aware FOV prefilter, lock hysteresis, lead point re-checked for FOV and visibility - spread: seed hook arms on the no-spread toggle alone; engine toggle only controls the RandomFloat zeroing - resolve m_hOwnerEntity/m_hOwner; NPC lead velocity from the server entity - vector_angles pitch range, point_ground normal test, underwater jump+duck no longer skips combat, semi-auto press/release edge for synthetic fire - item/projectile markers and blast rings refresh origin per frame while discovery stays at 10 Hz; trajectory uses the real m_DmgRadius and honours the blast-radius toggle Rendering/GUI: - one overlay draw per engine frame (FRAME_RENDER_START boundary), fixing alpha stacking and the speed-graph flicker - clipped lines/outlines and ellipsised text instead of drawing past the content pane or shifting over neighbours - dropdown dismissal consumes the click; hover ignores hidden controls - chams: reset AlphaModulate, reference shared materials once, restore prop tint colour; fonts created once per handle - projection uses Source's 4:3 aspect scaling and roll; screen size refreshed each frame; FOV circle gated on the aim toggles, not the bind Performance: - TTL caches for NPC relation, friendly/neutral and damage class - model names memoised per model index - entity pointer -> index cache for the triggerbot (was a 4096-slot walk/tick) Build: - real Makefile with -MMD dependency tracking, debug/release objects and an always-relink rule; build.sh kept as a thin wrapper
Diffstat (limited to '')
-rw-r--r--src/gui.cpp259
1 files changed, 156 insertions, 103 deletions
diff --git a/src/gui.cpp b/src/gui.cpp
index 454e2cb..8a71ecb 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -19,20 +19,8 @@
19// ISurface drawing (same binary slots as menu.cpp / esp.cpp) 19// ISurface drawing (same binary slots as menu.cpp / esp.cpp)
20// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
21 21
22enum { 22// ISurface access lives in overlay.cpp. These thin wrappers keep the GUI code
23 GS_DrawSetColor = 10, 23// readable without repeating the vtable indices in this file.
24 GS_DrawFilledRect = 12,
25 GS_DrawOutlinedRect = 14,
26 GS_DrawLine = 15,
27 GS_DrawSetTextFont = 17,
28 GS_DrawSetTextColor = 18,
29 GS_DrawSetTextPos = 20,
30 GS_DrawPrintText = 22,
31 GS_GetScreenSize = 38,
32 GS_CreateFont = 66,
33 GS_AddGlyphSet = 67,
34 GS_GetTextSize = 75
35};
36 24
37static unsigned long g_font_body = 0; 25static unsigned long g_font_body = 0;
38static unsigned long g_font_title = 0; 26static unsigned long g_font_title = 0;
@@ -43,57 +31,27 @@ static void *g_font_local = (void *)-1;
43static bool g_font_fail_logged = false; 31static bool g_font_fail_logged = false;
44static int g_scr_w = 0, g_scr_h = 0; 32static int g_scr_w = 0, g_scr_h = 0;
45 33
46static void s_color(int r, int g, int b, int a) { 34static void s_color(int r, int g, int b, int a) { overlay_color(r, g, b, a); }
47 using Fn = void (*)(void *, int, int, int, int);
48 vfunc<Fn>(g_surface, GS_DrawSetColor)(g_surface, r, g, b, a);
49}
50static void s_fill(int x0, int y0, int x1, int y1) { 35static void s_fill(int x0, int y0, int x1, int y1) {
51 using Fn = void (*)(void *, int, int, int, int); 36 overlay_fill(x0, y0, x1, y1);
52 vfunc<Fn>(g_surface, GS_DrawFilledRect)(g_surface, x0, y0, x1, y1);
53}
54static void s_outline(int x0, int y0, int x1, int y1) {
55 using Fn = void (*)(void *, int, int, int, int);
56 vfunc<Fn>(g_surface, GS_DrawOutlinedRect)(g_surface, x0, y0, x1, y1);
57} 37}
58static void s_line(int x0, int y0, int x1, int y1) { 38static void s_line(int x0, int y0, int x1, int y1) {
59 using Fn = void (*)(void *, int, int, int, int); 39 overlay_line(x0, y0, x1, y1);
60 vfunc<Fn>(g_surface, GS_DrawLine)(g_surface, x0, y0, x1, y1);
61}
62static void s_text_font(unsigned long f) {
63 using Fn = void (*)(void *, unsigned long);
64 vfunc<Fn>(g_surface, GS_DrawSetTextFont)(g_surface, f);
65} 40}
41static void s_text_font(HFont f) { overlay_text_font(f); }
66static void s_text_color(int r, int g, int b, int a) { 42static void s_text_color(int r, int g, int b, int a) {
67 using Fn = void (*)(void *, int, int, int, int); 43 overlay_text_color(r, g, b, a);
68 vfunc<Fn>(g_surface, GS_DrawSetTextColor)(g_surface, r, g, b, a);
69}
70static void s_text_pos(int x, int y) {
71 using Fn = void (*)(void *, int, int);
72 vfunc<Fn>(g_surface, GS_DrawSetTextPos)(g_surface, x, y);
73} 44}
74static void s_print(const wchar_t *text, int n) { 45static void s_text_pos(int x, int y) { overlay_text_pos(x, y); }
75 using Fn = void (*)(void *, const wchar_t *, int, int); 46static void s_print(const wchar_t *text, int n) { overlay_print_text(text, n); }
76 vfunc<Fn>(g_surface, GS_DrawPrintText)(g_surface, text, n, 0); 47static HFont s_create_font() { return overlay_create_font(); }
77} 48static bool s_set_font(HFont font, const char *name, int tall, int weight,
78static unsigned long s_create_font() {
79 using Fn = unsigned long (*)(void *);
80 return vfunc<Fn>(g_surface, GS_CreateFont)(g_surface);
81}
82static bool s_set_font(unsigned long font, const char *name, int tall, int weight,
83 int flags, int lo, int hi) { 49 int flags, int lo, int hi) {
84 using Fn = bool (*)(void *, unsigned long, const char *, int, int, int, int, 50 return overlay_add_glyph_set(font, name, tall, weight, 0, 0, flags, lo, hi);
85 int, int, int);
86 return vfunc<Fn>(g_surface, GS_AddGlyphSet)(g_surface, font, name, tall,
87 weight, 0, 0, flags, lo, hi);
88}
89static void s_screen(int &w, int &h) {
90 using Fn = void (*)(void *, int &, int &);
91 vfunc<Fn>(g_surface, GS_GetScreenSize)(g_surface, w, h);
92} 51}
93static void s_text_size(unsigned long font, const wchar_t *text, int &w, 52static void s_screen(int &w, int &h) { overlay_screen_size(w, h); }
94 int &h) { 53static void s_text_size(HFont font, const wchar_t *text, int &w, int &h) {
95 using Fn = void (*)(void *, unsigned long, const wchar_t *, int &, int &); 54 overlay_text_size(font, text, w, h);
96 vfunc<Fn>(g_surface, GS_GetTextSize)(g_surface, font, text, w, h);
97} 55}
98 56
99// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
@@ -137,9 +95,12 @@ static void gui_init_sdl() {
137 g_sdl_init_tried = true; 95 g_sdl_init_tried = true;
138 sd_get_keys = (GetKeysFn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState"); 96 sd_get_keys = (GetKeysFn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState");
139 sd_get_mouse = (GetMouseFn)dlsym(RTLD_DEFAULT, "SDL_GetMouseState"); 97 sd_get_mouse = (GetMouseFn)dlsym(RTLD_DEFAULT, "SDL_GetMouseState");
140 sd_show_cursor = (ShowCursorFn)dlsym(RTLD_DEFAULT, "SDL_ShowCursor"); 98 // RTLD_NEXT, not RTLD_DEFAULT: this object is LD_PRELOADed first, so
141 sd_set_relative = (SetRelativeFn)dlsym(RTLD_DEFAULT, "SDL_SetRelativeMouseMode"); 99 // RTLD_DEFAULT resolves to our own exported wrapper and the "real" pointer
142 sd_get_relative = (GetRelativeFn)dlsym(RTLD_DEFAULT, "SDL_GetRelativeMouseMode"); 100 // would point back into this file.
101 sd_show_cursor = (ShowCursorFn)dlsym(RTLD_NEXT, "SDL_ShowCursor");
102 sd_set_relative = (SetRelativeFn)dlsym(RTLD_NEXT, "SDL_SetRelativeMouseMode");
103 sd_get_relative = (GetRelativeFn)dlsym(RTLD_NEXT, "SDL_GetRelativeMouseMode");
143 sd_add_watch = (AddWatchFn)dlsym(RTLD_DEFAULT, "SDL_AddEventWatch"); 104 sd_add_watch = (AddWatchFn)dlsym(RTLD_DEFAULT, "SDL_AddEventWatch");
144 sd_get_ticks = (GetTicksFn)dlsym(RTLD_DEFAULT, "SDL_GetTicks"); 105 sd_get_ticks = (GetTicksFn)dlsym(RTLD_DEFAULT, "SDL_GetTicks");
145 g_sdl_ready = sd_get_keys && sd_get_mouse; 106 g_sdl_ready = sd_get_keys && sd_get_mouse;
@@ -607,6 +568,10 @@ static int g_mouse_press = 0; // 1..5 on the frame a button goes down
607static unsigned char g_prev_keys[512] = {}; 568static unsigned char g_prev_keys[512] = {};
608static bool g_prev_cursor = true; 569static bool g_prev_cursor = true;
609static bool g_prev_relative = true; 570static bool g_prev_relative = true;
571// Last state the game asked for while the GUI held the mouse; restored on
572// close instead of the (possibly stale) pre-open snapshot.
573static bool g_game_wants_cursor = true;
574static bool g_game_wants_relative = false;
610static double g_hover_since = 0.0; 575static double g_hover_since = 0.0;
611static const char *g_hover_desc = nullptr; 576static const char *g_hover_desc = nullptr;
612static const char *g_hover_prev = nullptr; 577static const char *g_hover_prev = nullptr;
@@ -688,10 +653,12 @@ static unsigned long create_gui_font(int tall) {
688 static const char *families[] = {"Liberation Sans", "DejaVu Sans", 653 static const char *families[] = {"Liberation Sans", "DejaVu Sans",
689 "Noto Sans", "Tahoma", "Verdana", "Arial", 654 "Noto Sans", "Tahoma", "Verdana", "Arial",
690 nullptr}; 655 nullptr};
656 // Create one handle and add glyph sets to it. Creating a handle per family
657 // leaked one HFont per failed family, and ISurface has no DestroyFont.
658 unsigned long f = s_create_font();
659 if (!f)
660 return 0;
691 for (int i = 0; families[i]; i++) { 661 for (int i = 0; families[i]; i++) {
692 unsigned long f = s_create_font();
693 if (!f)
694 break;
695 if (s_set_font(f, families[i], tall, 400, 0x400, 0x20, 0xFF) && 662 if (s_set_font(f, families[i], tall, 400, 0x400, 0x20, 0xFF) &&
696 font_valid(f)) { 663 font_valid(f)) {
697 logd("gui: font %dpx family %s", tall, families[i]); 664 logd("gui: font %dpx family %s", tall, families[i]);
@@ -710,8 +677,13 @@ static void ensure_fonts() {
710 } 677 }
711 if (le != g_font_local) { 678 if (le != g_font_local) {
712 g_font_local = le; 679 g_font_local = le;
713 gui_reset_fonts(); 680 // VGUI fonts are global; a level change alone does not invalidate them.
714 logd("gui: fonts reset (local entity change)"); 681 // Dropping valid handles here leaked a fresh pair on every death, respawn
682 // and map load. Only reset when the manager actually lost them.
683 if (!font_valid(g_font_body) || !font_valid(g_font_title)) {
684 gui_reset_fonts();
685 logd("gui: fonts reset (invalid after local entity change)");
686 }
715 } 687 }
716 688
717 bool scale_changed = fabsf(g_font_scale - g_scale) >= 0.001f; 689 bool scale_changed = fabsf(g_font_scale - g_scale) >= 0.001f;
@@ -871,20 +843,30 @@ static void draw_text_raw(int x, int y, const char *s, unsigned long font,
871 843
872static void text_in(const Rect &clip, int x, int y, const char *s, 844static void text_in(const Rect &clip, int x, int y, const char *s,
873 unsigned long font, int tall, int max_w, const Col &c) { 845 unsigned long font, int tall, int max_w, const Col &c) {
874 (void)max_w; // callers must size the layout; text is never truncated
875 if (!s || !font) 846 if (!s || !font)
876 return; 847 return;
877 if (y < clip.y || y + tall > clip.y + clip.h) 848 if (y < clip.y || y + tall > clip.y + clip.h)
878 return; 849 return;
879 if (x >= clip.x + clip.w) 850 if (x >= clip.x + clip.w)
880 return; 851 return;
881 int w = text_w(s, tall); 852 // Available width: the clip edge, further limited by the layout's column
882 if (x + w > clip.x + clip.w) 853 // width. Overflowing text used to be shifted left over its neighbour; now it
883 x = clip.x + clip.w - w; 854 // is truncated with an ellipsis instead.
884 if (x < clip.x) 855 int avail = clip.x + clip.w - x;
885 x = clip.x; 856 if (max_w > 0 && max_w < avail)
857 avail = max_w;
858 if (avail <= 0)
859 return;
860 std::string out = s;
861 if (text_w(out.c_str(), tall) > avail) {
862 while (!out.empty() && text_w((out + "...").c_str(), tall) > avail)
863 out.pop_back();
864 if (out.empty())
865 return;
866 out += "...";
867 }
886 s_text_color(c.r, c.g, c.b, c.a); 868 s_text_color(c.r, c.g, c.b, c.a);
887 draw_text_raw(x, y, s, font, tall); 869 draw_text_raw(x, y, out.c_str(), font, tall);
888} 870}
889 871
890static Rect clip_rect(const Rect &r, const Rect &clip) { 872static Rect clip_rect(const Rect &r, const Rect &clip) {
@@ -911,13 +893,49 @@ static void fill(const Rect &r, const Col &c, const Rect *clip = nullptr) {
911static void outline(const Rect &r, const Col &c, const Rect *clip = nullptr) { 893static void outline(const Rect &r, const Col &c, const Rect *clip = nullptr) {
912 if (r.w <= 0 || r.h <= 0) 894 if (r.w <= 0 || r.h <= 0)
913 return; 895 return;
914 Rect q = clip ? clip_rect(r, *clip) : r; 896 // Draw the four edges as thin filled rects. Outlining the *clamped* rect used
915 if (q.w <= 0 || q.h <= 0) 897 // to paint a false border along the clip edge whenever a card was partially
916 return; 898 // scrolled out of the content pane.
917 s_color(c.r, c.g, c.b, c.a); 899 int t = S(1.0f);
918 s_outline(q.x, q.y, q.x + q.w, q.y + q.h); 900 if (t < 1) t = 1;
919} 901 fill(Rect{r.x, r.y, r.w, t}, c, clip);
920static void line(int x0, int y0, int x1, int y1, const Col &c) { 902 fill(Rect{r.x, r.y + r.h - t, r.w, t}, c, clip);
903 fill(Rect{r.x, r.y, t, r.h}, c, clip);
904 fill(Rect{r.x + r.w - t, r.y, t, r.h}, c, clip);
905}
906// Liang–Barsky line clip. `clip` is optional so the existing call sites that
907// draw inside an already-safe area keep working unchanged.
908static void line(int x0, int y0, int x1, int y1, const Col &c,
909 const Rect *clip = nullptr) {
910 if (clip) {
911 float dx = (float)(x1 - x0), dy = (float)(y1 - y0);
912 float t0 = 0.0f, t1 = 1.0f;
913 float p[4] = {-dx, dx, -dy, dy};
914 float q[4] = {(float)(x0 - clip->x),
915 (float)(clip->x + clip->w - x0),
916 (float)(y0 - clip->y),
917 (float)(clip->y + clip->h - y0)};
918 for (int i = 0; i < 4; i++) {
919 if (fabsf(p[i]) < 1e-6f) {
920 if (q[i] < 0.0f)
921 return; // parallel and outside
922 continue;
923 }
924 float t = q[i] / p[i];
925 if (p[i] < 0.0f) {
926 if (t > t1) return;
927 if (t > t0) t0 = t;
928 } else {
929 if (t < t0) return;
930 if (t < t1) t1 = t;
931 }
932 }
933 int nx0 = (int)((float)x0 + t0 * dx + 0.5f);
934 int ny0 = (int)((float)y0 + t0 * dy + 0.5f);
935 int nx1 = (int)((float)x0 + t1 * dx + 0.5f);
936 int ny1 = (int)((float)y0 + t1 * dy + 0.5f);
937 x0 = nx0; y0 = ny0; x1 = nx1; y1 = ny1;
938 }
921 s_color(c.r, c.g, c.b, c.a); 939 s_color(c.r, c.g, c.b, c.a);
922 s_line(x0, y0, x1, y1); 940 s_line(x0, y0, x1, y1);
923} 941}
@@ -1298,7 +1316,7 @@ static void draw_picker(const Rect &clip, const Rect &cr, const Ctrl &ct,
1298 outline(p.alpha, Col{0, 0, 0, 255}, &clip); 1316 outline(p.alpha, Col{0, 0, 0, 255}, &clip);
1299 int amx = p.alpha.x + (int)((float)pa / 255.0f * (float)p.alpha.w); 1317 int amx = p.alpha.x + (int)((float)pa / 255.0f * (float)p.alpha.w);
1300 line(amx, p.alpha.y - S(2.0f), amx, p.alpha.y + p.alpha.h + S(2.0f), 1318 line(amx, p.alpha.y - S(2.0f), amx, p.alpha.y + p.alpha.h + S(2.0f),
1301 Col{255, 255, 255, 255}); 1319 Col{255, 255, 255, 255}, &clip);
1302 } 1320 }
1303 1321
1304 // Selection markers. 1322 // Selection markers.
@@ -1311,7 +1329,7 @@ static void draw_picker(const Rect &clip, const Rect &cr, const Ctrl &ct,
1311 1329
1312 int hxm = p.hue.x + (int)((ct.hue / 360.0f) * (float)p.hue.w); 1330 int hxm = p.hue.x + (int)((ct.hue / 360.0f) * (float)p.hue.w);
1313 line(hxm, p.hue.y - S(2.0f), hxm, p.hue.y + p.hue.h + S(2.0f), 1331 line(hxm, p.hue.y - S(2.0f), hxm, p.hue.y + p.hue.h + S(2.0f),
1314 Col{255, 255, 255, 255}); 1332 Col{255, 255, 255, 255}, &clip);
1315 1333
1316 // Preview swatch and hex value. 1334 // Preview swatch and hex value.
1317 draw_checker(p.preview, &clip); 1335 draw_checker(p.preview, &clip);
@@ -1351,6 +1369,23 @@ static Rect card_rect(int index, int y) {
1351 return Rect{x, y, w, card_h(c)}; 1369 return Rect{x, y, w, card_h(c)};
1352} 1370}
1353 1371
1372// Slider track inside a card: the single source of truth for drawing,
1373// hit-testing and dragging. The drag path used to recompute this by hand and
1374// omitted the card's +S(10) left margin, so the value jumped on mouse-down.
1375static Rect slider_track(const Rect &cr, int y, int rh) {
1376 int w = cr.w - S(16.0f) - S(230.0f) - S(64.0f);
1377 if (w < S(30.0f)) w = S(30.0f);
1378 return Rect{cr.x + S(16.0f) + S(230.0f), y + (rh - S(8.0f)) / 2, w,
1379 S(8.0f)};
1380}
1381
1382static int card_for_ctrl(int ci) {
1383 for (int i = 0; i < g_ncards; i++)
1384 if (ci >= g_cards[i].c0 && ci < g_cards[i].c0 + g_cards[i].n)
1385 return i;
1386 return -1;
1387}
1388
1354static bool card_in_category(int i) { return g_cards[i].cat == g_cat; } 1389static bool card_in_category(int i) { return g_cards[i].cat == g_cat; }
1355 1390
1356static int content_height() { 1391static int content_height() {
@@ -1379,10 +1414,13 @@ static void close_gui() {
1379 g_reset_armed = false; 1414 g_reset_armed = false;
1380 input_clear_mouse_states(); 1415 input_clear_mouse_states();
1381 if (g_sdl_ready) { 1416 if (g_sdl_ready) {
1417 // Restore what the game actually wants now, not the pre-open snapshot: the
1418 // game keeps requesting relative mode/cursor state while the GUI is open
1419 // (menu -> map transition), and those requests were being dropped.
1382 if (sd_set_relative) 1420 if (sd_set_relative)
1383 sd_set_relative(g_prev_relative ? 1 : 0); 1421 sd_set_relative(g_game_wants_relative ? 1 : 0);
1384 if (sd_show_cursor) 1422 if (sd_show_cursor)
1385 sd_show_cursor(g_prev_cursor ? 1 : 0); 1423 sd_show_cursor(g_game_wants_cursor ? 1 : 0);
1386 } 1424 }
1387 config_save(); 1425 config_save();
1388 logf("gui: closed"); 1426 logf("gui: closed");
@@ -1410,6 +1448,9 @@ static void open_gui() {
1410 g_prev_relative = sd_get_relative() != 0; 1448 g_prev_relative = sd_get_relative() != 0;
1411 if (sd_show_cursor) 1449 if (sd_show_cursor)
1412 g_prev_cursor = sd_show_cursor(-1) != 0; 1450 g_prev_cursor = sd_show_cursor(-1) != 0;
1451 // Until the game asks for something else, restore what it had before.
1452 g_game_wants_relative = g_prev_relative;
1453 g_game_wants_cursor = g_prev_cursor;
1413 if (sd_set_relative) 1454 if (sd_set_relative)
1414 sd_set_relative(0); 1455 sd_set_relative(0);
1415 if (sd_show_cursor) 1456 if (sd_show_cursor)
@@ -1519,6 +1560,10 @@ static void handle_mouse_click() {
1519 } 1560 }
1520 } 1561 }
1521 g_dropdown_ctrl = -1; 1562 g_dropdown_ctrl = -1;
1563 // Consume the dismissal click: it must not also activate whatever control
1564 // happened to be underneath the popup.
1565 g_mouse_click = false;
1566 return;
1522 } 1567 }
1523 1568
1524 // title bar: drag 1569 // title bar: drag
@@ -1624,9 +1669,7 @@ static void handle_mouse_click() {
1624 return; 1669 return;
1625 } 1670 }
1626 if (ct.kind == CK_SLIDER) { 1671 if (ct.kind == CK_SLIDER) {
1627 Rect track{cr.x + S(16.0f) + S(230.0f), 1672 Rect track = slider_track(cr, by, rh);
1628 by + (rh - S(8.0f)) / 2,
1629 cr.w - S(16.0f) - S(230.0f) - S(64.0f), S(8.0f)};
1630 if (mx >= track.x && mx < track.x + track.w) { 1673 if (mx >= track.x && mx < track.x + track.w) {
1631 float t = (float)(mx - track.x) / (float)track.w; 1674 float t = (float)(mx - track.x) / (float)track.w;
1632 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f); 1675 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f);
@@ -1717,6 +1760,11 @@ static void handle_mouse_click() {
1717static void handle_hover() { 1760static void handle_hover() {
1718 g_hover_desc = nullptr; 1761 g_hover_desc = nullptr;
1719 g_hover_slider = -1; 1762 g_hover_slider = -1;
1763 // An open popup/picker or an active drag owns the cursor; arming hover on the
1764 // controls underneath let Left/Right arrows adjust a hidden slider.
1765 if (g_dropdown_ctrl >= 0 || g_picker_ctrl >= 0 || g_picker_drag ||
1766 g_slider_drag >= 0 || g_dragging || g_scroll_drag)
1767 return;
1720 int mx = g_mouse_x, my = g_mouse_y; 1768 int mx = g_mouse_x, my = g_mouse_y;
1721 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), 1769 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f),
1722 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; 1770 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)};
@@ -1849,14 +1897,15 @@ void gui_handle_input() {
1849 config_save(); 1897 config_save();
1850 } else { 1898 } else {
1851 Ctrl &ct = g_ctrls[g_slider_drag]; 1899 Ctrl &ct = g_ctrls[g_slider_drag];
1852 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), 1900 int ci = card_for_ctrl(g_slider_drag);
1853 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; 1901 if (ci >= 0) {
1854 int x0 = g_win_x + S(190.0f) + S(16.0f) + S(230.0f); 1902 Rect track = slider_track(card_rect(ci, 0), 0, 0);
1855 int x1 = g_win_x + g_win_w - S(28.0f) - S(64.0f); 1903 float t = (float)(g_mouse_x - track.x) / (float)track.w;
1856 (void)content; 1904 if (t < 0.0f) t = 0.0f;
1857 float t = (float)(g_mouse_x - x0) / (float)(x1 - x0); 1905 if (t > 1.0f) t = 1.0f;
1858 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f); 1906 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f);
1859 ctrl_set(ct, v); 1907 ctrl_set(ct, v);
1908 }
1860 } 1909 }
1861 } 1910 }
1862 1911
@@ -1938,18 +1987,15 @@ static void draw_checkbox(int x, int y, bool on,
1938 fill(box, on ? C_ACCENT : Col{30, 34, 42, 255}, clip); 1987 fill(box, on ? C_ACCENT : Col{30, 34, 42, 255}, clip);
1939 outline(box, C_BORDER, clip); 1988 outline(box, C_BORDER, clip);
1940 if (on) { 1989 if (on) {
1941 line(x + S(3.0f), y + S(8.0f), x + S(7.0f), y + S(12.0f), C_BG); 1990 line(x + S(3.0f), y + S(8.0f), x + S(7.0f), y + S(12.0f), C_BG, clip);
1942 line(x + S(7.0f), y + S(12.0f), x + S(13.0f), y + S(4.0f), C_BG); 1991 line(x + S(7.0f), y + S(12.0f), x + S(13.0f), y + S(4.0f), C_BG, clip);
1943 } 1992 }
1944} 1993}
1945 1994
1946static void draw_slider(const Rect &clip, const Rect &cr, int y, int rh, 1995static void draw_slider(const Rect &clip, const Rect &cr, int y, int rh,
1947 const Ctrl &ct) { 1996 const Ctrl &ct) {
1948 int track_x = cr.x + S(16.0f) + S(230.0f); 1997 Rect track = slider_track(cr, y, rh);
1949 int track_w = cr.w - S(16.0f) - S(230.0f) - S(64.0f); 1998 int track_w = track.w;
1950 if (track_w < S(30.0f)) track_w = S(30.0f);
1951 int track_y = y + (rh - S(8.0f)) / 2;
1952 Rect track{track_x, track_y, track_w, S(8.0f)};
1953 fill(track, Col{38, 42, 50, 255}, &clip); 1999 fill(track, Col{38, 42, 50, 255}, &clip);
1954 int value = ctrl_val(ct); 2000 int value = ctrl_val(ct);
1955 float t = (ct.max > ct.min) 2001 float t = (ct.max > ct.min)
@@ -2416,6 +2462,8 @@ extern "C" __attribute__((visibility("default"))) int SDL_ShowCursor(int toggle)
2416 if (!real_show_cursor) 2462 if (!real_show_cursor)
2417 real_show_cursor = (int (*)(int))dlsym(RTLD_NEXT, "SDL_ShowCursor"); 2463 real_show_cursor = (int (*)(int))dlsym(RTLD_NEXT, "SDL_ShowCursor");
2418 if (g_open && toggle != -1) { 2464 if (g_open && toggle != -1) {
2465 // Remember what the game asked for so close_gui() can restore it.
2466 g_game_wants_cursor = toggle != 0;
2419 if (real_show_cursor) 2467 if (real_show_cursor)
2420 real_show_cursor(1); 2468 real_show_cursor(1);
2421 return 1; 2469 return 1;
@@ -2429,6 +2477,7 @@ SDL_SetRelativeMouseMode(int enabled) {
2429 real_set_relative = 2477 real_set_relative =
2430 (int (*)(int))dlsym(RTLD_NEXT, "SDL_SetRelativeMouseMode"); 2478 (int (*)(int))dlsym(RTLD_NEXT, "SDL_SetRelativeMouseMode");
2431 if (g_open) { 2479 if (g_open) {
2480 g_game_wants_relative = enabled != 0;
2432 if (real_set_relative) 2481 if (real_set_relative)
2433 real_set_relative(0); 2482 real_set_relative(0);
2434 return 0; 2483 return 0;
@@ -2520,7 +2569,11 @@ SDL_PollEvent(void *event) {
2520 unsigned int type = *(unsigned int *)event; 2569 unsigned int type = *(unsigned int *)event;
2521 if (type != 0x400) // SDL_MOUSEMOTION 2570 if (type != 0x400) // SDL_MOUSEMOTION
2522 return r; 2571 return r;
2523 // Drop motion while the GUI is open: the game must not accumulate look 2572 // Keep the event (VGUI hover and SDL's internal cursor state need it) but
2524 // deltas. The GUI uses absolute SDL_GetMouseState polling. 2573 // zero the relative deltas so the game does not accumulate look movement.
2574 // SDL_MouseMotionEvent: xrel at +28, yrel at +32.
2575 *(int *)((char *)event + 28) = 0;
2576 *(int *)((char *)event + 32) = 0;
2577 return r;
2525 } 2578 }
2526} 2579}