diff --git a/crash_handler_stub/WatchDogTimer.cpp b/crash_handler_stub/WatchDogTimer.cpp index b680cc5d8..fdf7fed8a 100644 --- a/crash_handler_stub/WatchDogTimer.cpp +++ b/crash_handler_stub/WatchDogTimer.cpp @@ -21,6 +21,9 @@ class WatchDogTimer::Impl std::mutex m_mutex; std::atomic m_exiting; bool m_running = false; + // Touched only by the checker thread: Alt kill-switch arming state, reset when no hang is active + bool m_kill_armed = false; + bool m_hang_reported = false; public: Impl(std::chrono::milliseconds timeout) : m_timeout(timeout) @@ -77,6 +80,12 @@ class WatchDogTimer::Impl if (check_for_time_out()) { handle_time_out(); } + else { + // No hang in progress (or it recovered) - the next hang requires a fresh + // Alt press before it can be killed + m_kill_armed = false; + m_hang_reported = false; + } std::unique_lock lk(m_mutex); m_cond_var.wait_for(lk, check_interval); } @@ -91,12 +100,20 @@ class WatchDogTimer::Impl void handle_time_out() { - if (GetAsyncKeyState(VK_MENU) & 0x8000) { + // Only treat Alt as a kill request if it was pressed after the hang began: Alt can be + // held as a regular game binding, so an Alt that was already down when the game stopped + // responding must be observed released once before it arms the kill switch. + bool alt_down = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; + if (!alt_down) { + m_kill_armed = true; + } + if (alt_down && m_kill_armed) { xlog::info("Crash of not responding process has been requested"); crash_observed_thread(); } - else { - xlog::info("Process is not responding! Hold the Alt key for a few seconds to kill the process and generate a crash report that will allow to debug the problem..."); + else if (!m_hang_reported) { + xlog::info("Process is not responding! Release the Alt key if it is held, then hold it for a few seconds to kill the process and generate a crash report that will help in debugging the problem..."); + m_hang_reported = true; } } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0f3aa0957..b8415ebb3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -32,6 +32,7 @@ Version 1.4.0 (Lupin): Not yet released - Add `Sort` options (by name or by UID, with an optional `Group by type` toggle) to the editor's Select Objects and Show/Hide Objects windows - Add `camera4` console command for static camera in single player - Add `camera5` console command for tripod (follow player) camera in single player +- Require a fresh `Alt` press to kill an unresponsive process [@is-this-c](https://github.com/is-this-c) - Allow players to join between levels @@ -46,6 +47,9 @@ Version 1.4.0 (Lupin): Not yet released - Add detection of manually loaded levels - Highlight an active level in a server's rotation via background color instead of text color +[@AL2009man](https://github.com/AL2009man) +- Add support for binding controls to additional mouse buttons and `Alt` keys + ### Bug fixes [@GooberRF](https://github.com/GooberRF) - Fix team balance not properly randomizing the distribution order of equal-scoring human players @@ -68,6 +72,9 @@ Version 1.4.0 (Lupin): Not yet released - Disable weapon cycle selection, if `Mouse 3` is pressed - For `Run` games, rename `Score` column to `Deaths`, and compare `Loads` in `std::ranges::sort` +[@AL2009man](https://github.com/AL2009man) +- Fix brief game freeze whenever an `Alt` key is pressed + Version 1.3.0 (Bakeapple): Released Apr-22-2026 -------------------------------- ### Major features diff --git a/game_patch/input/input.h b/game_patch/input/input.h index 347b0badb..10dbc26b2 100644 --- a/game_patch/input/input.h +++ b/game_patch/input/input.h @@ -2,6 +2,12 @@ #include "../rf/player/control_config.h" +// Custom scan codes for extra mouse buttons (Mouse 4 and above). Placed in the extended +// range (0x80 | scan) at slots no physical keyboard emits, so they cannot collide with +// real key presses. +static constexpr int CTRL_EXTRA_MOUSE_SCAN_BASE = 0xF5; +static constexpr int CTRL_EXTRA_MOUSE_SCAN_COUNT = 5; + rf::ControlConfigAction get_af_control(rf::AlpineControlConfigAction alpine_control); rf::String get_action_bind_name(int action); void mouse_apply_patch(); diff --git a/game_patch/input/key.cpp b/game_patch/input/key.cpp index 15ac17891..caddc83da 100644 --- a/game_patch/input/key.cpp +++ b/game_patch/input/key.cpp @@ -19,8 +19,10 @@ #include "../rf/player/player.h" #include "../rf/os/console.h" #include "../rf/os/os.h" +#include "../rf/ui.h" #include "../multi/alpine_packets.h" #include "../os/console.h" +#include "input.h" static int starting_alpine_control_index = -1; @@ -114,7 +116,12 @@ FunHook key_to_ascii_hook{ int get_key_name(int key, char* buf, size_t buf_len) { - LONG lparam = (key & 0x7F) << 16; + // Extra mouse buttons (Mouse 4+): stored as custom scan codes + if (key >= CTRL_EXTRA_MOUSE_SCAN_BASE && key < CTRL_EXTRA_MOUSE_SCAN_BASE + CTRL_EXTRA_MOUSE_SCAN_COUNT) { + int n = snprintf(buf, buf_len, "Mouse %d", (key - CTRL_EXTRA_MOUSE_SCAN_BASE) + 4); + return n > 0 ? n : 0; + } + LONG lparam = (key & 0x7F) << 16; if (key & 0x80) { lparam |= 1 << 24; } @@ -509,6 +516,26 @@ FunHook key_msg_handler_hook{ }, }; +// Records the pressed key into the selected binding row when the controls panel is +// waiting for input (scan code from the key queue, or -1 to check mouse buttons). +// Stock RF rejects Left/Right Alt and any scan code its internal key name table has +// no name for, which blocks Alpine's extra mouse button scan codes — accept both +// directly; everything else keeps stock behavior (including the Esc/PrtScn/Pause +// blacklist and mouse button handling). +FunHook options_controls_record_binding_hook{ + 0x0044FE60, + [](int key) { + bool is_extra_mouse_scan = key >= CTRL_EXTRA_MOUSE_SCAN_BASE + && key < CTRL_EXTRA_MOUSE_SCAN_BASE + CTRL_EXTRA_MOUSE_SCAN_COUNT; + if (is_extra_mouse_scan || key == rf::KEY_LALT || key == rf::KEY_RALT) { + rf::ui::options_controls_assign_binding(key, -1); + rf::ui::options_controls_stop_waiting_for_key(); + return; + } + options_controls_record_binding_hook.call_target(key); + }, +}; + void key_apply_patch() { // Handle Alpine chat menus @@ -539,4 +566,7 @@ void key_apply_patch() // Num pads need a patch to support `PgUp`, `PgDown`, `End`, and `Home`. key_msg_handler_hook.install(); + + // Allow binding Alt keys and extra mouse buttons in the controls options panel + options_controls_record_binding_hook.install(); } diff --git a/game_patch/input/mouse.cpp b/game_patch/input/mouse.cpp index aad8db7e7..bd5cdd1f2 100644 --- a/game_patch/input/mouse.cpp +++ b/game_patch/input/mouse.cpp @@ -420,6 +420,16 @@ CodeInjection static_zoom_sensitivity_patch2 { }, }; +// Feed extra mouse buttons (Mouse 4+) into RF's key system as custom scan codes. +// The controls binding UI picks them up from the key queue like any key press. +void mouse_handle_xbutton_wm(int rf_btn, bool down) +{ + int extra = rf_btn - 3; + if (extra < 0 || extra >= CTRL_EXTRA_MOUSE_SCAN_COUNT) + return; + rf::key_process_event(CTRL_EXTRA_MOUSE_SCAN_BASE + extra, down ? 1 : 0, 0); +} + void mouse_apply_patch() { // Handle zoom sens customization diff --git a/game_patch/input/mouse.h b/game_patch/input/mouse.h index 14ff3e510..cdb03ae28 100644 --- a/game_patch/input/mouse.h +++ b/game_patch/input/mouse.h @@ -2,3 +2,4 @@ void mouse_apply_patch(); void consume_raw_mouse_deltas(float& out_pitch, float& out_yaw, bool apply_scope_sens); +void mouse_handle_xbutton_wm(int rf_btn, bool down); diff --git a/game_patch/os/os.cpp b/game_patch/os/os.cpp index 69d51dda1..ce2f15a08 100644 --- a/game_patch/os/os.cpp +++ b/game_patch/os/os.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "../rf/os/os.h" #include "../rf/multi.h" @@ -12,7 +13,7 @@ #include "../multi/multi.h" #include "os.h" #include "win32_console.h" -#include +#include "../input/mouse.h" FunHook os_poll_hook{ 0x00524B60, @@ -89,6 +90,19 @@ LRESULT WINAPI wnd_proc(HWND wnd_handle, UINT msg, WPARAM w_param, LPARAM l_para } return DefWindowProcA(wnd_handle, msg, w_param, l_param); + case WM_SYSCOMMAND: + if ((w_param & 0xFFF0) == SC_KEYMENU) + return 0; + return DefWindowProcA(wnd_handle, msg, w_param, l_param); + + case WM_XBUTTONDOWN: + mouse_handle_xbutton_wm(2 + HIWORD(w_param), true); + return TRUE; + + case WM_XBUTTONUP: + mouse_handle_xbutton_wm(2 + HIWORD(w_param), false); + return TRUE; + case WM_QUIT: case WM_CLOSE: case WM_DESTROY: diff --git a/game_patch/rf/ui.h b/game_patch/rf/ui.h index fea23ef73..23f426f62 100644 --- a/game_patch/rf/ui.h +++ b/game_patch/rf/ui.h @@ -288,6 +288,11 @@ namespace rf::ui static auto& options_controls_bindings_down_on_click = addr_as_ref(0x00450130); static auto& options_controls_current_gadget = addr_as_ref(0x0063FFE8); static auto& options_controls_waiting_for_key = addr_as_ref(0x00640060); + + // Assigns to the currently selected binding row and clears the same scan code / + // mouse button from all other bindings (pass -1 for the unused parameter) + static auto& options_controls_assign_binding = addr_as_ref(0x0044FFD0); + static auto& options_controls_stop_waiting_for_key = addr_as_ref(0x0044FC00); // multi -> create game static auto& create_game_map_list_up_on_click = addr_as_ref(0x004451F0);