diff options
| -rw-r--r-- | CONTEXT.md | 7 | ||||
| -rw-r--r-- | src/ISURFACE_STATUS.md | 22 | ||||
| -rw-r--r-- | src/cinput.cpp | 3 | ||||
| -rw-r--r-- | src/gui.cpp | 217 | ||||
| -rw-r--r-- | src/hl2h.cpp | 25 | ||||
| -rw-r--r-- | src/hl2h.h | 9 | ||||
| -rw-r--r-- | src/menu.cpp | 49 | ||||
| -rw-r--r-- | src/server.cpp | 1 |
8 files changed, 309 insertions, 24 deletions
| @@ -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 | |||
| 415 | struct Toast { | ||
| 416 | char text[256]; | ||
| 417 | int level; | ||
| 418 | double born; // 0 = waiting for the first draw | ||
| 419 | double ttl; | ||
| 420 | }; | ||
| 421 | |||
| 422 | static Toast g_toasts[8]; | ||
| 423 | static int g_ntoasts = 0; | ||
| 424 | static pthread_mutex_t g_toast_mu = PTHREAD_MUTEX_INITIALIZER; | ||
| 425 | |||
| 426 | void 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 | |||
| 454 | bool 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 | |||
| 409 | static void ensure_fonts() { | 461 | static 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 | ||
| 444 | static int text_w(const char *s, int tall) { | 498 | static 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 | |||
| 1266 | static 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 | |||
| 1334 | void 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 | |||
| 1201 | void gui_draw() { | 1416 | void 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 | ||
| 469 | static void install_seed_hook() { | 469 | static 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 | ||
| @@ -23,6 +23,15 @@ constexpr float PI_F = 3.14159265f; | |||
| 23 | void logf(const char *fmt, ...); | 23 | void logf(const char *fmt, ...); |
| 24 | 24 | ||
| 25 | // --------------------------------------------------------------------------- | 25 | // --------------------------------------------------------------------------- |
| 26 | // on-screen toasts | ||
| 27 | // --------------------------------------------------------------------------- | ||
| 28 | |||
| 29 | enum ToastLevel { TOAST_INFO = 0, TOAST_WARN, TOAST_ERROR }; | ||
| 30 | void toastf(int level, const char *fmt, ...); | ||
| 31 | bool gui_toasts_active(); | ||
| 32 | void 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 | ||
| 145 | void config_save() { | 150 | void 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 | ||
| 181 | static void config_load() { | 200 | static 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 | ||
| 293 | void config_reload() { config_load(); } | 313 | void 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 | ||
| 295 | const char *config_path_cstr() { | 320 | const 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() { | |||
| 697 | void install_menu() { | 726 | void 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 | } |
