summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-27 18:53:32 +0100
committersalmonik <salmonik@tuta.io>2026-10-27 18:53:32 +0100
commit6b5425ebd35c28e20680f5f28c887b705827ba1c (patch)
tree9b9c4b74c597b4d225e65a04e8812de784d91f01
parentGUI: fix footer clipping; remove the old text menu (diff)
downloadhl2h-6b5425ebd35c28e20680f5f28c887b705827ba1c.tar.gz
hl2h-6b5425ebd35c28e20680f5f28c887b705827ba1c.zip
GUI: add on-screen toasts and atomic config writes
- Toast queue drawn in the top-right by the overlay, independent of the GUI being open. Info/warn/error colors and 6/9/12 s lifetimes; timers start on the first painted frame so startup messages are not missed. - Queue a load toast after install_menu, plus warnings/errors for missing modules/interfaces/netvars, input hooks, fonts, ISurface, server datamap, and spread seed support. - Toast on config reload, reset, bind/unbind and config save failure. - Config saves now write `config.tmp`, flush, then rename it over the real file; failures leave the previous config intact. - config_load reports whether a file was found so Reload can distinguish a successful reload from a missing file. - Update CONTEXT and ISURFACE_STATUS for the single GUI, toasts and atomic config writes.
-rw-r--r--CONTEXT.md7
-rw-r--r--src/ISURFACE_STATUS.md22
-rw-r--r--src/cinput.cpp3
-rw-r--r--src/gui.cpp217
-rw-r--r--src/hl2h.cpp25
-rw-r--r--src/hl2h.h9
-rw-r--r--src/menu.cpp49
-rw-r--r--src/server.cpp1
8 files changed, 309 insertions, 24 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 5e0e4b2..78f7c75 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -116,6 +116,11 @@ vocabulary / decisions.
116 and ESP/FOV circle/speed graph keep drawing. No invulnerability. 116 and ESP/FOV circle/speed graph keep drawing. No invulnerability.
117- **Native cursor**: on open, leave SDL relative-mouse mode and show the OS 117- **Native cursor**: on open, leave SDL relative-mouse mode and show the OS
118 cursor; restore on close. Absolute mouse coordinates are used. 118 cursor; restore on close. Absolute mouse coordinates are used.
119- **Toast**: transient top-right notification drawn by the overlay even when
120 the GUI is closed. Info/warn/error colors; lifetimes 6/9/12 s. Toasts queued
121 before the first paint (load errors) start their timer when first drawn.
122 Used for the load message, config problems, missing interfaces/netvars,
123 bind changes, Reload and Reset.
119- **Bind capture**: click Bind, press a key or any mouse button; Esc cancels 124- **Bind capture**: click Bind, press a key or any mouse button; Esc cancels
120 capture but does not close the GUI; the small x clears the bind (None = 125 capture but does not close the GUI; the small x clears the bind (None =
121 always-on). Insert is reserved for the GUI. 126 always-on). Insert is reserved for the GUI.
@@ -134,6 +139,8 @@ vocabulary / decisions.
134 `x`, `y`, `category` and an `expanded` bitmask (card index order). Every 139 `x`, `y`, `category` and an `expanded` bitmask (card index order). Every
135 feature now persists an `active` and `bind` pair, including movement, melee 140 feature now persists an `active` and `bind` pair, including movement, melee
136 and spread. 141 and spread.
142- Config writes are atomic: `config.tmp` is written and flushed, then
143 `rename()`d over the real file. A failed write raises an error toast.
137 144
138 145
139## Bones / ESP 146## Bones / ESP
diff --git a/src/ISURFACE_STATUS.md b/src/ISURFACE_STATUS.md
index 00a7adf..d553af0 100644
--- a/src/ISURFACE_STATUS.md
+++ b/src/ISURFACE_STATUS.md
@@ -1,6 +1,6 @@
1# HL2hake ISurface Drawing — Status Report 1# HL2hake ISurface Drawing — Status Report
2 2
3## Date: 2026-09-12, updated 2026-10-26 3## Date: 2026-09-12, updated 2026-10-27
4 4
5## Result: WORKING 5## Result: WORKING
6 6
@@ -67,21 +67,21 @@ fc-match findings (system fontconfig):
67 67
68## Menu 68## Menu
69 69
70- Starts fully collapsed on first open; `Insert` toggles it. 70- Custom immediate-mode GUI; `Insert` is the only toggle.
71- Tree: HL2hake -> Bunnyhop (Active, Auto-strafe), ABH (Active), ESP (Active, 71- Sidebar: Movement / Combat / Visuals / Weapons / System. Cards show
72 Show neutral/healthbar/healthnum/name/skeleton/box), Aimbot (Active, Bind, 72 Active + Bind and collapsible settings; System has config path, build info,
73 Vischeck, Bones, Smoothness), Triggerbot (Active, Bind), Silent aim 73 Reload and two-click Reset.
74 (Active, Bind). Children indented by `g_depth[]` (+12px/level).
75- Right-aligned state at x=270: `[on]`/`[off]` for toggles, `[keyname]` for
76 binds, `[press key]` while binding, `N deg/tick` for values.
77- Feature gating: `feature_active(int)` (menu.cpp) — active AND 74- Feature gating: `feature_active(int)` (menu.cpp) — active AND
78 (bind==0 or key held), used by `do_move` and the aim features. 75 (bind==0 or key held), used by `do_move` and the aim features.
79- Config: `$XDG_CONFIG_HOME/hl2hake/config` (fallback `~/.config/hl2hake/config`). 76- Layout (`x`, `y`, `category`, `expanded` bitmask) persists under `[gui]`.
77- Config: `$XDG_CONFIG_HOME/hl2hake/config` (fallback `~/.config/hl2hake/config`);
78 writes are atomic (`.tmp` + rename).
79- Toasts are drawn by `gui.cpp` even when the menu is closed.
80 80
81## Build / Run 81## Build / Run
82 82
83- `./build.sh`: g++ `-m32 -O2 -std=c++17 -fPIC -shared`, 9 TUs 83- `./build.sh`: g++ `-m32 -O2 -std=c++17 -fPIC -shared`, 10 TUs
84 (`hl2h modules cinput netvars menu esp server bones vischeck`), `-ldl`, 84 (`hl2h modules cinput netvars menu gui esp server bones vischeck`), `-ldl`,
85 `-Wall -Wextra -Werror` (all warnings fatal). 85 `-Wall -Wextra -Werror` (all warnings fatal).
86- `./play.sh -windowed +map d1_trainstation_01` (Steam runtime wrapper, 86- `./play.sh -windowed +map d1_trainstation_01` (Steam runtime wrapper,
87 `LD_PRELOAD=libhl2hake.so`). 87 `LD_PRELOAD=libhl2hake.so`).
diff --git a/src/cinput.cpp b/src/cinput.cpp
index ad7face..f466c53 100644
--- a/src/cinput.cpp
+++ b/src/cinput.cpp
@@ -228,7 +228,8 @@ bool find_input_global() {
228 logf("CInput* = %#lx (ptr var %#lx, vfunc[3] %#lx)", (unsigned long)input, 228 logf("CInput* = %#lx (ptr var %#lx, vfunc[3] %#lx)", (unsigned long)input,
229 (unsigned long)var, (unsigned long)f3); 229 (unsigned long)var, (unsigned long)f3);
230 g_input = input; 230 g_input = input;
231 install_input_hook(); 231 if (!install_input_hook())
232 toastf(TOAST_ERROR, "Input vtable hook failed; features disabled");
232 return true; 233 return true;
233 } 234 }
234 } 235 }
diff --git a/src/gui.cpp b/src/gui.cpp
index 132068e..0d5b458 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -6,6 +6,7 @@
6#include "hl2h.h" 6#include "hl2h.h"
7 7
8#include <atomic> 8#include <atomic>
9#include <cstdarg>
9#include <cmath> 10#include <cmath>
10#include <cstdio> 11#include <cstdio>
11#include <cstdlib> 12#include <cstdlib>
@@ -136,6 +137,7 @@ static void gui_init_sdl() {
136 g_sdl_ready = sd_get_keys && sd_get_mouse; 137 g_sdl_ready = sd_get_keys && sd_get_mouse;
137 if (!g_sdl_ready) { 138 if (!g_sdl_ready) {
138 logf("gui: SDL polling unavailable"); 139 logf("gui: SDL polling unavailable");
140 toastf(TOAST_ERROR, "GUI input unavailable: SDL not found");
139 return; 141 return;
140 } 142 }
141 if (sd_add_watch) 143 if (sd_add_watch)
@@ -406,6 +408,56 @@ static float now_sec() {
406 return (float)ts.tv_sec + (float)ts.tv_nsec * 1e-9f; 408 return (float)ts.tv_sec + (float)ts.tv_nsec * 1e-9f;
407} 409}
408 410
411// ---------------------------------------------------------------------------
412// Toasts (shown even when the GUI is closed)
413// ---------------------------------------------------------------------------
414
415struct Toast {
416 char text[256];
417 int level;
418 double born; // 0 = waiting for the first draw
419 double ttl;
420};
421
422static Toast g_toasts[8];
423static int g_ntoasts = 0;
424static pthread_mutex_t g_toast_mu = PTHREAD_MUTEX_INITIALIZER;
425
426void toastf(int level, const char *fmt, ...) {
427 char buf[256];
428 va_list ap;
429 va_start(ap, fmt);
430 vsnprintf(buf, sizeof(buf), fmt, ap);
431 va_end(ap);
432
433 pthread_mutex_lock(&g_toast_mu);
434 for (int i = 0; i < g_ntoasts; i++) {
435 if (strcmp(g_toasts[i].text, buf) == 0) {
436 g_toasts[i].level = level;
437 g_toasts[i].born = 0.0;
438 pthread_mutex_unlock(&g_toast_mu);
439 return;
440 }
441 }
442 if (g_ntoasts == 8) {
443 memmove(&g_toasts[0], &g_toasts[1], sizeof(Toast) * 7);
444 g_ntoasts = 7;
445 }
446 Toast &t = g_toasts[g_ntoasts++];
447 snprintf(t.text, sizeof(t.text), "%s", buf);
448 t.level = level;
449 t.born = 0.0;
450 t.ttl = level == TOAST_ERROR ? 12.0 : level == TOAST_WARN ? 9.0 : 6.0;
451 pthread_mutex_unlock(&g_toast_mu);
452}
453
454bool gui_toasts_active() {
455 pthread_mutex_lock(&g_toast_mu);
456 bool active = g_ntoasts > 0;
457 pthread_mutex_unlock(&g_toast_mu);
458 return active;
459}
460
409static void ensure_fonts() { 461static void ensure_fonts() {
410 if (g_font_body && fabsf(g_font_scale - g_scale) < 0.001f) 462 if (g_font_body && fabsf(g_font_scale - g_scale) < 0.001f)
411 return; 463 return;
@@ -437,8 +489,10 @@ static void ensure_fonts() {
437 } 489 }
438 if (!g_font_title) 490 if (!g_font_title)
439 g_font_title = g_font_body; 491 g_font_title = g_font_body;
440 if (!g_font_body) 492 if (!g_font_body) {
441 logf("gui: no usable body font"); 493 logf("gui: no usable body font");
494 toastf(TOAST_ERROR, "No usable GUI font");
495 }
442} 496}
443 497
444static int text_w(const char *s, int tall) { 498static int text_w(const char *s, int tall) {
@@ -698,6 +752,8 @@ static void handle_capture(const unsigned char *keys) {
698 g_mouse_rclick = false; 752 g_mouse_rclick = false;
699 g_capture_card = -1; 753 g_capture_card = -1;
700 config_save(); 754 config_save();
755 toastf(TOAST_INFO, "%s bound to %s", card.title,
756 scancode_name(card.feat->bind));
701 return; 757 return;
702 } 758 }
703 for (int k = 0; k < 512; k++) { 759 for (int k = 0; k < 512; k++) {
@@ -709,6 +765,9 @@ static void handle_capture(const unsigned char *keys) {
709 card.feat->bind = k; 765 card.feat->bind = k;
710 g_capture_card = -1; 766 g_capture_card = -1;
711 config_save(); 767 config_save();
768 if (card.feat)
769 toastf(TOAST_INFO, "%s bound to %s", card.title,
770 scancode_name(card.feat->bind));
712 return; 771 return;
713 } 772 }
714} 773}
@@ -795,6 +854,7 @@ static void handle_mouse_click() {
795 if (mx >= bind.x + bind.w - S(18.0f)) { 854 if (mx >= bind.x + bind.w - S(18.0f)) {
796 c.feat->bind = 0; 855 c.feat->bind = 0;
797 config_save(); 856 config_save();
857 toastf(TOAST_INFO, "%s unbound (always-on)", c.title);
798 return; 858 return;
799 } 859 }
800 g_capture_card = i; 860 g_capture_card = i;
@@ -844,6 +904,7 @@ static void handle_mouse_click() {
844 config_reset_defaults(); 904 config_reset_defaults();
845 g_reset_armed = false; 905 g_reset_armed = false;
846 logf("gui: defaults restored"); 906 logf("gui: defaults restored");
907 toastf(TOAST_INFO, "Feature defaults restored");
847 } else { 908 } else {
848 g_reset_armed = true; 909 g_reset_armed = true;
849 g_reset_armed_at = now; 910 g_reset_armed_at = now;
@@ -1198,6 +1259,160 @@ static void draw_tooltip() {
1198 inner.w, C_TEXT); 1259 inner.w, C_TEXT);
1199} 1260}
1200 1261
1262// ---------------------------------------------------------------------------
1263// Toast drawing
1264// ---------------------------------------------------------------------------
1265
1266static int wrap_toast(const char *text, int max_w, int tall,
1267 char lines[][256], int max_lines) {
1268 int n = 0;
1269 char cur[256] = {};
1270 bool truncated = false;
1271 const char *p = text;
1272 while (*p) {
1273 while (*p == ' ')
1274 p++;
1275 if (!*p)
1276 break;
1277 const char *start = p;
1278 while (*p && *p != ' ')
1279 p++;
1280 size_t wl = (size_t)(p - start);
1281 if (wl > 120)
1282 wl = 120;
1283 char word[128];
1284 memcpy(word, start, wl);
1285 word[wl] = 0;
1286
1287 char cand[256];
1288 if (cur[0])
1289 snprintf(cand, sizeof(cand), "%s %s", cur, word);
1290 else
1291 snprintf(cand, sizeof(cand), "%s", word);
1292
1293 if (text_w(cand, tall) <= max_w || !cur[0]) {
1294 snprintf(cur, sizeof(cur), "%s", cand);
1295 if (text_w(cur, tall) > max_w) {
1296 // A single word longer than the panel: hard-truncate it.
1297 int len = (int)strlen(cur);
1298 while (len > 3 && text_w(cur, tall) > max_w)
1299 cur[--len] = 0;
1300 if (len > 3) {
1301 cur[len - 3] = '.';
1302 cur[len - 2] = '.';
1303 cur[len - 1] = '.';
1304 }
1305 truncated = true;
1306 }
1307 } else {
1308 if (n < max_lines)
1309 snprintf(lines[n++], 256, "%s", cur);
1310 else
1311 truncated = true;
1312 snprintf(cur, sizeof(cur), "%s", word);
1313 }
1314 }
1315 if (cur[0]) {
1316 if (n < max_lines)
1317 snprintf(lines[n++], 256, "%s", cur);
1318 else
1319 truncated = true;
1320 }
1321 if (truncated && n > 0) {
1322 char *last = lines[n - 1];
1323 int len = (int)strlen(last);
1324 if (len + 3 < 256) {
1325 last[len++] = '.';
1326 last[len++] = '.';
1327 last[len++] = '.';
1328 last[len] = 0;
1329 }
1330 }
1331 return n;
1332}
1333
1334void gui_draw_toasts() {
1335 if (!g_surface)
1336 return;
1337 ensure_fonts();
1338 if (!g_font_body || g_scr_w <= 0 || g_scr_h <= 0)
1339 return;
1340
1341 Toast local[8];
1342 int n = 0;
1343 double now = now_sec();
1344 pthread_mutex_lock(&g_toast_mu);
1345 for (int i = 0; i < g_ntoasts; i++) {
1346 Toast &t = g_toasts[i];
1347 if (t.born == 0.0)
1348 t.born = now;
1349 if (now - t.born > t.ttl)
1350 continue;
1351 local[n++] = t;
1352 }
1353 g_ntoasts = 0;
1354 for (int i = 0; i < n; i++)
1355 g_toasts[g_ntoasts++] = local[i];
1356 pthread_mutex_unlock(&g_toast_mu);
1357
1358 int margin = S(16.0f);
1359 int max_w = S(420.0f);
1360 if (max_w > g_scr_w - margin * 2)
1361 max_w = g_scr_w - margin * 2;
1362 int y = margin;
1363 for (int i = 0; i < n; i++) {
1364 const Toast &t = local[i];
1365 float age = (float)(now - t.born);
1366 float a = 1.0f;
1367 if (age < 0.2f)
1368 a = age / 0.2f;
1369 else if (age > t.ttl - 0.5f)
1370 a = (float)((t.ttl - age) / 0.5);
1371 if (a < 0.0f) a = 0.0f;
1372 if (a > 1.0f) a = 1.0f;
1373 int alpha = (int)(a * 255.0f);
1374
1375 char lines[4][256];
1376 int nl = wrap_toast(t.text, max_w, g_body_h, lines, 4);
1377 if (nl == 0)
1378 continue;
1379 int line_w = 0;
1380 for (int j = 0; j < nl; j++) {
1381 int w = text_w(lines[j], g_body_h);
1382 if (w > line_w)
1383 line_w = w;
1384 }
1385 int pad = S(12.0f);
1386 int line_h = g_body_h + S(4.0f);
1387 int panel_w = line_w + pad * 2 + S(6.0f);
1388 int panel_h = nl * line_h + pad * 2 - S(4.0f);
1389 int x = g_scr_w - margin - panel_w;
1390 if (x < margin)
1391 x = margin;
1392
1393 Col accent = t.level == TOAST_ERROR
1394 ? Col{240, 90, 90, 255}
1395 : t.level == TOAST_WARN ? Col{255, 200, 80, 255}
1396 : C_ACCENT;
1397 fill(Rect{x, y, panel_w, panel_h}, Col{16, 18, 22, (int)(235 * a)});
1398 outline(Rect{x, y, panel_w, panel_h}, Col{accent.r, accent.g, accent.b, alpha});
1399 fill(Rect{x, y, S(4.0f), panel_h},
1400 Col{accent.r, accent.g, accent.b, alpha});
1401 s_text_font(g_font_body);
1402 s_text_color(C_TEXT.r, C_TEXT.g, C_TEXT.b, alpha);
1403 for (int j = 0; j < nl; j++) {
1404 s_text_pos(x + pad + S(6.0f), y + pad + j * line_h);
1405 wchar_t w[256];
1406 size_t wn = mbstowcs(w, lines[j], 255);
1407 if (wn == (size_t)-1)
1408 continue;
1409 w[wn] = 0;
1410 s_print(w, (int)wn);
1411 }
1412 y += panel_h + S(8.0f);
1413 }
1414}
1415
1201void gui_draw() { 1416void gui_draw() {
1202 if (!g_open || !g_surface) 1417 if (!g_open || !g_surface)
1203 return; 1418 return;
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index 1460577..c51d35e 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -467,7 +467,8 @@ static void hkdSrvRandomSeed(int seed) {
467} 467}
468 468
469static void install_seed_hook() { 469static void install_seed_hook() {
470 if (g_seed_hook_installed || !g_server.valid()) 470 static bool failed = false;
471 if (g_seed_hook_installed || failed || !g_server.valid())
471 return; 472 return;
472 g_srv_base = g_server.ranges[0].lo; 473 g_srv_base = g_server.ranges[0].lo;
473 uintptr_t slot = g_srv_base + 0xce1348; // R_386_JUMP_SLOT RandomSeed 474 uintptr_t slot = g_srv_base + 0xce1348; // R_386_JUMP_SLOT RandomSeed
@@ -478,7 +479,9 @@ static void install_seed_hook() {
478 if (!g_real_random_seed) 479 if (!g_real_random_seed)
479 g_real_random_seed = (void (*)(int))dlsym(RTLD_DEFAULT, "RandomSeed"); 480 g_real_random_seed = (void (*)(int))dlsym(RTLD_DEFAULT, "RandomSeed");
480 if (!g_real_random_seed) { 481 if (!g_real_random_seed) {
482 failed = true;
481 logf("seed hook: real RandomSeed not found"); 483 logf("seed hook: real RandomSeed not found");
484 toastf(TOAST_WARN, "Spread seed hook unavailable");
482 return; 485 return;
483 } 486 }
484 mprotect((void *)(slot & ~(uintptr_t)0xFFF), 0x1000, 487 mprotect((void *)(slot & ~(uintptr_t)0xFFF), 0x1000,
@@ -808,6 +811,7 @@ static void *setup_thread(void *) {
808 } 811 }
809 if (!g_client.valid() || !g_engine.valid()) { 812 if (!g_client.valid() || !g_engine.valid()) {
810 logf("client.so/engine.so never appeared"); 813 logf("client.so/engine.so never appeared");
814 toastf(TOAST_ERROR, "client.so/engine.so not found; hl2hake inactive");
811 return nullptr; 815 return nullptr;
812 } 816 }
813 logf("client.so: %d mapped range(s), %s", (int)g_client.ranges.size(), 817 logf("client.so: %d mapped range(s), %s", (int)g_client.ranges.size(),
@@ -822,6 +826,7 @@ static void *setup_thread(void *) {
822 auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); 826 auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface");
823 if (!cf || !ef) { 827 if (!cf || !ef) {
824 logf("CreateInterface not found"); 828 logf("CreateInterface not found");
829 toastf(TOAST_ERROR, "CreateInterface not found; hl2hake inactive");
825 return nullptr; 830 return nullptr;
826 } 831 }
827 832
@@ -840,6 +845,7 @@ static void *setup_thread(void *) {
840 logf("interfaces unresolved: VClient017=%p VClientEntityList003=%p " 845 logf("interfaces unresolved: VClient017=%p VClientEntityList003=%p "
841 "VEngineClient014=%p", 846 "VEngineClient014=%p",
842 g_iface.client, g_iface.entitylist, g_iface.engine); 847 g_iface.client, g_iface.entitylist, g_iface.engine);
848 toastf(TOAST_ERROR, "Game interfaces unresolved; hl2hake inactive");
843 return nullptr; 849 return nullptr;
844 } 850 }
845 logf("interfaces: VClient017=%p VClientEntityList003=%p " 851 logf("interfaces: VClient017=%p VClientEntityList003=%p "
@@ -856,12 +862,27 @@ static void *setup_thread(void *) {
856 g_iface.engineserver, g_iface.debugoverlay, g_iface.enginetrace); 862 g_iface.engineserver, g_iface.debugoverlay, g_iface.enginetrace);
857 863
858 resolve_netvars(); 864 resolve_netvars();
859 if (!find_input_global()) 865 if (!g_net.ok())
866 toastf(TOAST_ERROR, "Netvars incomplete; features may be disabled");
867 else if (g_net.absvelocity < 0)
868 toastf(TOAST_WARN, "Jumpbug assist disabled: velocity netvar missing");
869 if (!g_iface.engineserver)
870 toastf(TOAST_WARN, "Server interfaces missing; jumpbug/spread limited");
871 if (!g_iface.debugoverlay)
872 toastf(TOAST_WARN, "Debug overlay missing; ESP projection uses fallback");
873 if (!g_iface.enginetrace)
874 toastf(TOAST_WARN, "Engine trace missing; vischeck fails open");
875
876 if (!find_input_global()) {
877 toastf(TOAST_ERROR, "Input hook failed; movement features disabled");
860 return nullptr; 878 return nullptr;
879 }
861 880
862 init_conmsg(); 881 init_conmsg();
863 install_hook(); 882 install_hook();
864 install_menu(); 883 install_menu();
884 if (g_surface)
885 toastf(TOAST_INFO, "hl2hake loaded - Insert opens the menu");
865 return nullptr; 886 return nullptr;
866} 887}
867 888
diff --git a/src/hl2h.h b/src/hl2h.h
index 0f03925..e390aa0 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -23,6 +23,15 @@ constexpr float PI_F = 3.14159265f;
23void logf(const char *fmt, ...); 23void logf(const char *fmt, ...);
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26// on-screen toasts
27// ---------------------------------------------------------------------------
28
29enum ToastLevel { TOAST_INFO = 0, TOAST_WARN, TOAST_ERROR };
30void toastf(int level, const char *fmt, ...);
31bool gui_toasts_active();
32void gui_draw_toasts();
33
34// ---------------------------------------------------------------------------
26// module discovery 35// module discovery
27// --------------------------------------------------------------------------- 36// ---------------------------------------------------------------------------
28 37
diff --git a/src/menu.cpp b/src/menu.cpp
index 5fc325e..2021218 100644
--- a/src/menu.cpp
+++ b/src/menu.cpp
@@ -116,12 +116,14 @@ void register_bundled_font() {
116 } 116 }
117 if (path.empty()) { 117 if (path.empty()) {
118 logf("font: bundled LiberationSans-Regular.ttf not found"); 118 logf("font: bundled LiberationSans-Regular.ttf not found");
119 toastf(TOAST_WARN, "Bundled font missing; overlay may not render");
119 return; 120 return;
120 } 121 }
121 122
122 void *fc = dlopen("libfontconfig.so.1", RTLD_LAZY | RTLD_GLOBAL); 123 void *fc = dlopen("libfontconfig.so.1", RTLD_LAZY | RTLD_GLOBAL);
123 if (!fc) { 124 if (!fc) {
124 logf("font: libfontconfig.so.1 not available"); 125 logf("font: libfontconfig.so.1 not available");
126 toastf(TOAST_WARN, "fontconfig unavailable; overlay font not registered");
125 return; 127 return;
126 } 128 }
127 using InitFn = int (*)(); 129 using InitFn = int (*)();
@@ -132,20 +134,27 @@ void register_bundled_font() {
132 auto add = (AddFileFn)dlsym(fc, "FcConfigAppFontAddFile"); 134 auto add = (AddFileFn)dlsym(fc, "FcConfigAppFontAddFile");
133 if (!init || !get || !add) { 135 if (!init || !get || !add) {
134 logf("font: fontconfig symbols missing"); 136 logf("font: fontconfig symbols missing");
137 toastf(TOAST_WARN, "fontconfig symbols missing; overlay font not registered");
135 return; 138 return;
136 } 139 }
137 init(); 140 init();
138 void *cfg = get(); 141 void *cfg = get();
139 if (cfg && add(cfg, path.c_str())) 142 if (cfg && add(cfg, path.c_str()))
140 logf("font: registered %s", path.c_str()); 143 logf("font: registered %s", path.c_str());
141 else 144 else {
142 logf("font: failed to register %s", path.c_str()); 145 logf("font: failed to register %s", path.c_str());
146 toastf(TOAST_WARN, "Could not register the overlay font");
147 }
143} 148}
144 149
145void config_save() { 150void config_save() {
146 FILE *f = fopen(config_path().c_str(), "w"); 151 std::string path = config_path();
147 if (!f) 152 std::string tmp = path + ".tmp";
153 FILE *f = fopen(tmp.c_str(), "w");
154 if (!f) {
155 toastf(TOAST_ERROR, "Config save failed: %s", path.c_str());
148 return; 156 return;
157 }
149 fprintf(f, "[bhop]\nactive=%d\nbind=%d\nautostrafe=%d\n", g_bhop.active, 158 fprintf(f, "[bhop]\nactive=%d\nbind=%d\nautostrafe=%d\n", g_bhop.active,
150 g_bhop.bind, g_bhop_autostrafe); 159 g_bhop.bind, g_bhop_autostrafe);
151 fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind); 160 fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind);
@@ -175,13 +184,23 @@ void config_save() {
175 g_esp_opt[ESP_OPT_SHOW_SKELETON], g_esp_opt[ESP_OPT_SHOW_BOX], 184 g_esp_opt[ESP_OPT_SHOW_SKELETON], g_esp_opt[ESP_OPT_SHOW_BOX],
176 g_esp_opt[ESP_OPT_SHOW_SPEEDGRAPH]); 185 g_esp_opt[ESP_OPT_SHOW_SPEEDGRAPH]);
177 gui_config_write(f); 186 gui_config_write(f);
178 fclose(f); 187 bool ok = ferror(f) == 0;
188 if (fflush(f) != 0)
189 ok = false;
190 if (ok)
191 (void)fsync(fileno(f)); // best effort; the rename is what matters
192 if (fclose(f) != 0)
193 ok = false;
194 if (!ok || rename(tmp.c_str(), path.c_str()) != 0) {
195 remove(tmp.c_str());
196 toastf(TOAST_ERROR, "Config save failed: %s", path.c_str());
197 }
179} 198}
180 199
181static void config_load() { 200static bool config_load() {
182 FILE *f = fopen(config_path().c_str(), "r"); 201 FILE *f = fopen(config_path().c_str(), "r");
183 if (!f) 202 if (!f)
184 return; 203 return false;
185 char line[256]; 204 char line[256];
186 FeatureState *cur = nullptr; 205 FeatureState *cur = nullptr;
187 bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false, 206 bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false,
@@ -288,9 +307,15 @@ static void config_load() {
288 } 307 }
289 } 308 }
290 fclose(f); 309 fclose(f);
310 return true;
291} 311}
292 312
293void config_reload() { config_load(); } 313void config_reload() {
314 if (config_load())
315 toastf(TOAST_INFO, "Config reloaded");
316 else
317 toastf(TOAST_WARN, "Config not found; kept current settings");
318}
294 319
295const char *config_path_cstr() { 320const char *config_path_cstr() {
296 static std::string path; 321 static std::string path;
@@ -583,7 +608,8 @@ void outer_draw() {
583 // HL2H_DRAW_TEST=1 draws a probe rect without needing the GUI open 608 // HL2H_DRAW_TEST=1 draws a probe rect without needing the GUI open
584 static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr; 609 static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr;
585 bool esp_on = feature_active(FEAT_ESP); 610 bool esp_on = feature_active(FEAT_ESP);
586 if (!g_surface || (!gui_is_open() && !draw_test && !esp_on)) 611 bool toasts = gui_toasts_active();
612 if (!g_surface || (!gui_is_open() && !draw_test && !esp_on && !toasts))
587 return; 613 return;
588 614
589 // Level/menu -> game transitions can invalidate the VGUI font handle. 615 // Level/menu -> game transitions can invalidate the VGUI font handle.
@@ -622,6 +648,7 @@ void outer_draw() {
622 } 648 }
623 if (!font_ok) { 649 if (!font_ok) {
624 logf("outer_draw: no usable font"); 650 logf("outer_draw: no usable font");
651 toastf(TOAST_ERROR, "No usable overlay font");
625 return; 652 return;
626 } 653 }
627 } 654 }
@@ -639,6 +666,8 @@ void outer_draw() {
639 } 666 }
640 if (gui_is_open()) 667 if (gui_is_open())
641 gui_draw(); 668 gui_draw();
669 if (toasts)
670 gui_draw_toasts();
642 surf_pop_make_current(panel); 671 surf_pop_make_current(panel);
643} 672}
644 673
@@ -697,13 +726,15 @@ static ISurface *find_surface_interface() {
697void install_menu() { 726void install_menu() {
698 register_bundled_font(); 727 register_bundled_font();
699 logf("config: %s", config_path().c_str()); 728 logf("config: %s", config_path().c_str());
700 config_load(); 729 if (!config_load())
730 toastf(TOAST_INFO, "No config found; using defaults");
701 gui_config_init(); 731 gui_config_init();
702 // resolve ISurface once (used by the DrawPrintText hook in hl2h.cpp and 732 // resolve ISurface once (used by the DrawPrintText hook in hl2h.cpp and
703 // by outer_draw). DrawPrintText hook is installed from hkdCreateMove. 733 // by outer_draw). DrawPrintText hook is installed from hkdCreateMove.
704 g_surface = find_surface_interface(); 734 g_surface = find_surface_interface();
705 if (!g_surface) { 735 if (!g_surface) {
706 logf("ISurface not found; menu disabled"); 736 logf("ISurface not found; menu disabled");
737 toastf(TOAST_ERROR, "Overlay unavailable: ISurface not found");
707 return; 738 return;
708 } 739 }
709 logf("ISurface = %p", g_surface); 740 logf("ISurface = %p", g_surface);
diff --git a/src/server.cpp b/src/server.cpp
index 896ce3e..cfb4dd1 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -123,6 +123,7 @@ bool resolve_server_netvars() {
123 fail_logged = true; 123 fail_logged = true;
124 logf("server datamap: scan failed (server.so at %s)", 124 logf("server datamap: scan failed (server.so at %s)",
125 g_server.path.c_str()); 125 g_server.path.c_str());
126 toastf(TOAST_WARN, "Server datamap scan failed; jumpbug assist limited");
126 } 127 }
127 return g_srv_ready; 128 return g_srv_ready;
128} 129}