diff --git a/game_patch/CMakeLists.txt b/game_patch/CMakeLists.txt index 83bae0a6..76ba3dd9 100644 --- a/game_patch/CMakeLists.txt +++ b/game_patch/CMakeLists.txt @@ -101,8 +101,9 @@ set(SRCS graphics/d3d11/gr_d3d11_gamma.h graphics/d3d11/gr_d3d11_gamma.cpp input/input.h - input/mouse.h + input/input.cpp input/mouse.cpp + input/mouse.h input/key.cpp hud/hud_colors.cpp hud/hud_scale.cpp @@ -334,4 +335,13 @@ target_link_libraries(AlpineFaction freetype stb_vorbis stb_image + SDL3::SDL3 ) + +if(WIN32) + target_link_libraries(AlpineFaction + setupapi + imm32 + cfgmgr32 + ) +endif() \ No newline at end of file diff --git a/game_patch/input/input.cpp b/game_patch/input/input.cpp new file mode 100644 index 00000000..3c90bad3 --- /dev/null +++ b/game_patch/input/input.cpp @@ -0,0 +1,28 @@ +#include +#include "input.h" +#include "mouse.h" +#include "../os/os.h" + +void sdl_input_poll() +{ + if (!g_sdl_window) return; + + SDL_Event ev; + while (SDL_PollEvent(&ev)) { + switch (ev.type) { + case SDL_EVENT_TEXT_INPUT: + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + process_keyboard_event(ev); + break; + case SDL_EVENT_MOUSE_MOTION: + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + case SDL_EVENT_MOUSE_WHEEL: + process_mouse_event(ev); + break; + default: + break; + } + } +} diff --git a/game_patch/input/input.h b/game_patch/input/input.h index 347b0bad..c5a0e169 100644 --- a/game_patch/input/input.h +++ b/game_patch/input/input.h @@ -1,8 +1,40 @@ #pragma once +#include #include "../rf/player/control_config.h" +#include "mouse.h" + +// Sentinel scan code injected into Input Rebind UI, allowing additional input bindings. +static constexpr int CTRL_REBIND_SENTINEL = 0x58; // KEY_F12 + +// Extra keyboard scan codes not in RF's original DInput table. +// Placed after CTRL_EXTRA_MOUSE_SCAN (0x75–0x79), before RF extended keys (0x9C+). +static constexpr int CTRL_EXTRA_KEY_SCAN_BASE = 0x7A; +static constexpr int CTRL_EXTRA_KEY_SCAN_COUNT = 14; + +enum ExtraKeyScanOffset : int { + EXTRA_KEY_KP_DIVIDE = 0, // SDL_SCANCODE_KP_DIVIDE + EXTRA_KEY_NONUSBACKSLASH, // SDL_SCANCODE_NONUSBACKSLASH (ISO key between LShift and Z) + EXTRA_KEY_F13, // SDL_SCANCODE_F13 … F24 + EXTRA_KEY_F14, + EXTRA_KEY_F15, + EXTRA_KEY_F16, + EXTRA_KEY_F17, + EXTRA_KEY_F18, + EXTRA_KEY_F19, + EXTRA_KEY_F20, + EXTRA_KEY_F21, + EXTRA_KEY_F22, + EXTRA_KEY_F23, + EXTRA_KEY_F24, +}; rf::ControlConfigAction get_af_control(rf::AlpineControlConfigAction alpine_control); rf::String get_action_bind_name(int action); -void mouse_apply_patch(); +void process_keyboard_event(const SDL_Event& evt); +void sdl_input_poll(); +int key_take_pending_extra_rebind(); +std::string keyboard_take_pending_text(); void key_apply_patch(); +void set_kbm_input_mode(int mode); +void ui_refresh_kbm_input_mode_label(); diff --git a/game_patch/input/key.cpp b/game_patch/input/key.cpp index 15ac1789..530baf0f 100644 --- a/game_patch/input/key.cpp +++ b/game_patch/input/key.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -11,6 +13,7 @@ #include "../misc/waypoints_utils.h" #include "../multi/multi.h" #include "../multi/endgame_votes.h" +#include "input.h" #include "../rf/input.h" #include "../rf/entity.h" #include "../rf/multi.h" @@ -19,10 +22,13 @@ #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" static int starting_alpine_control_index = -1; +static int g_pending_extra_key_rebind = -1; // pending scan code for sentinel rebind pattern +static std::string g_sdl_pending_text; rf::String get_action_bind_name(int action) { @@ -45,80 +51,332 @@ rf::ControlConfigAction get_af_control(rf::AlpineControlConfigAction alpine_cont return static_cast(starting_alpine_control_index + static_cast(alpine_control)); } +static SDL_Scancode rf_key_to_sdl_scancode(int key); // defined below + FunHook key_to_ascii_hook{ 0x0051EFC0, [](int16_t key) { using namespace rf; constexpr int empty_result = 0xFF; - if (!key) { + if (!key) return empty_result; - } - // special handling for Num Lock (because ToAscii API does not support it) + + // Numpad arithmetic keys: same in all modes switch (key & KEY_MASK) { - // Numpad keys that always work case KEY_PADMULTIPLY: return static_cast('*'); - case KEY_PADMINUS: return static_cast('-'); - case KEY_PADPLUS: return static_cast('+'); - // Disable Numpad Enter key because game is not prepared for getting new line character from this function - case KEY_PADENTER: return empty_result; + case KEY_PADMINUS: return static_cast('-'); + case KEY_PADPLUS: return static_cast('+'); + case KEY_PADENTER: return empty_result; // game not prepared for newline from numpad + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_KP_DIVIDE: return static_cast('/'); } - if (GetKeyState(VK_NUMLOCK) & 1) { - switch (key & KEY_MASK) { - case KEY_PAD7: return static_cast('7'); - case KEY_PAD8: return static_cast('8'); - case KEY_PAD9: return static_cast('9'); - case KEY_PAD4: return static_cast('4'); - case KEY_PAD5: return static_cast('5'); - case KEY_PAD6: return static_cast('6'); - case KEY_PAD1: return static_cast('1'); - case KEY_PAD2: return static_cast('2'); - case KEY_PAD3: return static_cast('3'); - case KEY_PAD0: return static_cast('0'); - case KEY_PADPERIOD: return static_cast('.'); + + if (g_alpine_game_config.kbm_input_mode == 2) { + SDL_Keymod sdl_mods = SDL_GetModState(); + + // Treat pure Ctrl shortcuts as non-text, but allow AltGr (often Ctrl+Alt or SDL_KMOD_MODE) + if (key & KEY_CTRLED) { + bool is_altgr = + (sdl_mods & SDL_KMOD_MODE) != 0 || + ((sdl_mods & SDL_KMOD_CTRL) && (sdl_mods & SDL_KMOD_ALT)); + if (!is_altgr) + return empty_result; } + + if (sdl_mods & SDL_KMOD_NUM) { + switch (key & KEY_MASK) { + case KEY_PAD7: return static_cast('7'); + case KEY_PAD8: return static_cast('8'); + case KEY_PAD9: return static_cast('9'); + case KEY_PAD4: return static_cast('4'); + case KEY_PAD5: return static_cast('5'); + case KEY_PAD6: return static_cast('6'); + case KEY_PAD1: return static_cast('1'); + case KEY_PAD2: return static_cast('2'); + case KEY_PAD3: return static_cast('3'); + case KEY_PAD0: return static_cast('0'); + case KEY_PADPERIOD: return static_cast('.'); + } + } + + SDL_Scancode sc = rf_key_to_sdl_scancode(key); + if (sc == SDL_SCANCODE_UNKNOWN) + return empty_result; + + SDL_Keymod mods = SDL_KMOD_NONE; + if (key & KEY_SHIFTED) mods |= SDL_KMOD_SHIFT; + if (key & KEY_ALTED) mods |= SDL_KMOD_RALT; // AltGr on most non-US layouts + + SDL_Keycode kc = SDL_GetKeyFromScancode(sc, mods, false); + if (kc == SDLK_UNKNOWN || kc < 0x20 || kc > 0x7E) + return empty_result; + + return static_cast(kc); + } else { + // Note: broken on Wine with non-US layout (MAPVK_VSC_TO_VK_EX mapping issue) + if (GetKeyState(VK_NUMLOCK) & 1) { + switch (key & KEY_MASK) { + case KEY_PAD7: return static_cast('7'); + case KEY_PAD8: return static_cast('8'); + case KEY_PAD9: return static_cast('9'); + case KEY_PAD4: return static_cast('4'); + case KEY_PAD5: return static_cast('5'); + case KEY_PAD6: return static_cast('6'); + case KEY_PAD1: return static_cast('1'); + case KEY_PAD2: return static_cast('2'); + case KEY_PAD3: return static_cast('3'); + case KEY_PAD0: return static_cast('0'); + case KEY_PADPERIOD: return static_cast('.'); + } + } + BYTE key_state[256] = {0}; + if (key & KEY_SHIFTED) key_state[VK_SHIFT] = 0x80; + if (key & KEY_ALTED) key_state[VK_MENU] = 0x80; + if (key & KEY_CTRLED) key_state[VK_CONTROL] = 0x80; + int scan_code = key & 0x7F; + auto vk = MapVirtualKeyA(scan_code, MAPVK_VSC_TO_VK); + WCHAR unicode_chars[3]; + auto num_unicode_chars = ToUnicode(vk, scan_code, key_state, unicode_chars, std::size(unicode_chars), 0); + if (num_unicode_chars < 1) + return empty_result; + if (static_cast(unicode_chars[0]) >= 0x80 || !std::isprint(unicode_chars[0])) + return empty_result; + return static_cast(unicode_chars[0]); } - BYTE key_state[256] = {0}; - if (key & KEY_SHIFTED) { - key_state[VK_SHIFT] = 0x80; - } - if (key & KEY_ALTED) { - key_state[VK_MENU] = 0x80; - } - if (key & KEY_CTRLED) { - key_state[VK_CONTROL] = 0x80; - } - int scan_code = key & 0x7F; - auto vk = MapVirtualKeyA(scan_code, MAPVK_VSC_TO_VK); - WCHAR unicode_chars[3]; - auto num_unicode_chars = ToUnicode(vk, scan_code, key_state, unicode_chars, std::size(unicode_chars), 0); - if (num_unicode_chars < 1) { - return empty_result; - } - char ansi_char; -#if 0 // Windows-1252 codepage support - disabled because callers of this function expects ASCII - int num_ansi_chars = WideCharToMultiByte(1252, 0, unicode_chars, num_unicode_chars, - &ansi_char, sizeof(ansi_char), nullptr, nullptr); - if (num_ansi_chars == 0) { - return empty_result; - } -#else - if (static_cast(unicode_chars[0]) >= 0x80 || !std::isprint(unicode_chars[0])) { - return empty_result; - } - ansi_char = static_cast(unicode_chars[0]); -#endif - xlog::trace("vk {:x} ({}) char {}", vk, vk, ansi_char); - return static_cast(ansi_char); }, }; +static SDL_Scancode rf_key_to_sdl_scancode(int key) +{ + using namespace rf; + switch (key & KEY_MASK) { + case KEY_ESC: return SDL_SCANCODE_ESCAPE; + case KEY_1: return SDL_SCANCODE_1; + case KEY_2: return SDL_SCANCODE_2; + case KEY_3: return SDL_SCANCODE_3; + case KEY_4: return SDL_SCANCODE_4; + case KEY_5: return SDL_SCANCODE_5; + case KEY_6: return SDL_SCANCODE_6; + case KEY_7: return SDL_SCANCODE_7; + case KEY_8: return SDL_SCANCODE_8; + case KEY_9: return SDL_SCANCODE_9; + case KEY_0: return SDL_SCANCODE_0; + case KEY_MINUS: return SDL_SCANCODE_MINUS; + case KEY_EQUAL: return SDL_SCANCODE_EQUALS; + case KEY_BACKSP: return SDL_SCANCODE_BACKSPACE; + case KEY_TAB: return SDL_SCANCODE_TAB; + case KEY_Q: return SDL_SCANCODE_Q; + case KEY_W: return SDL_SCANCODE_W; + case KEY_E: return SDL_SCANCODE_E; + case KEY_R: return SDL_SCANCODE_R; + case KEY_T: return SDL_SCANCODE_T; + case KEY_Y: return SDL_SCANCODE_Y; + case KEY_U: return SDL_SCANCODE_U; + case KEY_I: return SDL_SCANCODE_I; + case KEY_O: return SDL_SCANCODE_O; + case KEY_P: return SDL_SCANCODE_P; + case KEY_LBRACKET: return SDL_SCANCODE_LEFTBRACKET; + case KEY_RBRACKET: return SDL_SCANCODE_RIGHTBRACKET; + case KEY_ENTER: return SDL_SCANCODE_RETURN; + case KEY_LCTRL: return SDL_SCANCODE_LCTRL; + case KEY_A: return SDL_SCANCODE_A; + case KEY_S: return SDL_SCANCODE_S; + case KEY_D: return SDL_SCANCODE_D; + case KEY_F: return SDL_SCANCODE_F; + case KEY_G: return SDL_SCANCODE_G; + case KEY_H: return SDL_SCANCODE_H; + case KEY_J: return SDL_SCANCODE_J; + case KEY_K: return SDL_SCANCODE_K; + case KEY_L: return SDL_SCANCODE_L; + case KEY_SEMICOL: return SDL_SCANCODE_SEMICOLON; + case KEY_RAPOSTRO: return SDL_SCANCODE_APOSTROPHE; + case KEY_LAPOSTRO_DBG: return SDL_SCANCODE_GRAVE; + case KEY_LSHIFT: return SDL_SCANCODE_LSHIFT; case KEY_SLASH: return SDL_SCANCODE_BACKSLASH; + case KEY_Z: return SDL_SCANCODE_Z; + case KEY_X: return SDL_SCANCODE_X; + case KEY_C: return SDL_SCANCODE_C; + case KEY_V: return SDL_SCANCODE_V; + case KEY_B: return SDL_SCANCODE_B; + case KEY_N: return SDL_SCANCODE_N; + case KEY_M: return SDL_SCANCODE_M; + case KEY_COMMA: return SDL_SCANCODE_COMMA; + case KEY_PERIOD: return SDL_SCANCODE_PERIOD; + case KEY_DIVIDE: return SDL_SCANCODE_SLASH; + case KEY_RSHIFT: return SDL_SCANCODE_RSHIFT; + case KEY_PADMULTIPLY: return SDL_SCANCODE_KP_MULTIPLY; + case KEY_LALT: return SDL_SCANCODE_LALT; + case KEY_SPACEBAR: return SDL_SCANCODE_SPACE; + case KEY_CAPSLOCK: return SDL_SCANCODE_CAPSLOCK; + case KEY_F1: return SDL_SCANCODE_F1; + case KEY_F2: return SDL_SCANCODE_F2; + case KEY_F3: return SDL_SCANCODE_F3; + case KEY_F4: return SDL_SCANCODE_F4; + case KEY_F5: return SDL_SCANCODE_F5; + case KEY_F6: return SDL_SCANCODE_F6; + case KEY_F7: return SDL_SCANCODE_F7; + case KEY_F8: return SDL_SCANCODE_F8; + case KEY_F9: return SDL_SCANCODE_F9; + case KEY_F10: return SDL_SCANCODE_F10; + case KEY_PAUSE: return SDL_SCANCODE_PAUSE; + case KEY_SCROLLLOCK: return SDL_SCANCODE_SCROLLLOCK; + case KEY_PAD7: return SDL_SCANCODE_KP_7; + case KEY_PAD8: return SDL_SCANCODE_KP_8; + case KEY_PAD9: return SDL_SCANCODE_KP_9; + case KEY_PADMINUS: return SDL_SCANCODE_KP_MINUS; + case KEY_PAD4: return SDL_SCANCODE_KP_4; + case KEY_PAD5: return SDL_SCANCODE_KP_5; + case KEY_PAD6: return SDL_SCANCODE_KP_6; + case KEY_PADPLUS: return SDL_SCANCODE_KP_PLUS; + case KEY_PAD1: return SDL_SCANCODE_KP_1; + case KEY_PAD2: return SDL_SCANCODE_KP_2; + case KEY_PAD3: return SDL_SCANCODE_KP_3; + case KEY_PAD0: return SDL_SCANCODE_KP_0; + case KEY_PADPERIOD: return SDL_SCANCODE_KP_PERIOD; + case KEY_F11: return SDL_SCANCODE_F11; + case KEY_F12: return SDL_SCANCODE_F12; + case KEY_PADENTER: return SDL_SCANCODE_KP_ENTER; + case KEY_RCTRL: return SDL_SCANCODE_RCTRL; + case KEY_PRINT_SCRN: return SDL_SCANCODE_PRINTSCREEN; + case KEY_RALT: return SDL_SCANCODE_RALT; + case KEY_NUMLOCK: return SDL_SCANCODE_NUMLOCKCLEAR; + case KEY_BREAK: return SDL_SCANCODE_PAUSE; + case KEY_HOME: return SDL_SCANCODE_HOME; + case KEY_UP: return SDL_SCANCODE_UP; + case KEY_PAGEUP: return SDL_SCANCODE_PAGEUP; + case KEY_LEFT: return SDL_SCANCODE_LEFT; + case KEY_RIGHT: return SDL_SCANCODE_RIGHT; + case KEY_END: return SDL_SCANCODE_END; + case KEY_DOWN: return SDL_SCANCODE_DOWN; + case KEY_PAGEDOWN: return SDL_SCANCODE_PAGEDOWN; + case KEY_INSERT: return SDL_SCANCODE_INSERT; + case KEY_DELETE: return SDL_SCANCODE_DELETE; + // Extra keyboard keys (custom scan codes not in RF's original DInput table) + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_KP_DIVIDE: return SDL_SCANCODE_KP_DIVIDE; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_NONUSBACKSLASH: return SDL_SCANCODE_NONUSBACKSLASH; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F13: return SDL_SCANCODE_F13; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F14: return SDL_SCANCODE_F14; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F15: return SDL_SCANCODE_F15; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F16: return SDL_SCANCODE_F16; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F17: return SDL_SCANCODE_F17; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F18: return SDL_SCANCODE_F18; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F19: return SDL_SCANCODE_F19; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F20: return SDL_SCANCODE_F20; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F21: return SDL_SCANCODE_F21; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F22: return SDL_SCANCODE_F22; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F23: return SDL_SCANCODE_F23; + case CTRL_EXTRA_KEY_SCAN_BASE + EXTRA_KEY_F24: return SDL_SCANCODE_F24; + default: return SDL_SCANCODE_UNKNOWN; + } +} + +static rf::Key sdl_scancode_to_rf_key(SDL_Scancode sc) +{ + static rf::Key table[SDL_SCANCODE_COUNT] = {}; + static bool built = false; + if (!built) { + for (int k = 1; k <= static_cast(rf::KEY_MASK); ++k) { + SDL_Scancode mapped = rf_key_to_sdl_scancode(k); + if (mapped != SDL_SCANCODE_UNKNOWN && table[mapped] == rf::KEY_NONE) { + table[mapped] = static_cast(k); + } + } + built = true; + } + if (sc == SDL_SCANCODE_UNKNOWN || static_cast(sc) >= SDL_SCANCODE_COUNT) + return rf::KEY_NONE; + return table[static_cast(sc)]; +} + +// Returns true for scan codes that RF's rebind UI ignores (extra keys + Alt). +static bool scan_needs_rebind_sentinel(int scan) +{ + if (scan >= CTRL_EXTRA_KEY_SCAN_BASE && + scan < CTRL_EXTRA_KEY_SCAN_BASE + CTRL_EXTRA_KEY_SCAN_COUNT) + return true; + return scan == rf::KEY_LALT || scan == rf::KEY_RALT; +} + +void process_keyboard_event(const SDL_Event& evt) +{ + switch (evt.type) { + case SDL_EVENT_TEXT_INPUT: + // Always buffer text — on-screen keyboard works in all input modes. + g_sdl_pending_text += evt.text.text; + break; + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: { + if (g_alpine_game_config.kbm_input_mode != 2) + break; // Win32 keyboard handles key events in modes 0/1 + if (evt.key.repeat) + break; // ignore OS key repeat; RF tracks state itself + const bool down = (evt.type == SDL_EVENT_KEY_DOWN); + const rf::Key rf_key = sdl_scancode_to_rf_key(evt.key.scancode); + if (rf_key == rf::KEY_NONE) + break; + int scan = static_cast(rf_key); + // Keys that RF's rebind UI can't handle use the sentinel pattern. + if (scan_needs_rebind_sentinel(scan) && down && g_pending_extra_key_rebind < 0 && + rf::ui::options_controls_waiting_for_key) { + g_pending_extra_key_rebind = scan; + rf::key_process_event(CTRL_REBIND_SENTINEL, 1, 0); + } else { + rf::key_process_event(scan, down ? 1 : 0, 0); + } + break; + } + default: + break; + } +} + +int key_take_pending_extra_rebind() +{ + int sc = g_pending_extra_key_rebind; + g_pending_extra_key_rebind = -1; + return sc; +} + +std::string keyboard_take_pending_text() +{ + return std::exchange(g_sdl_pending_text, {}); +} + int get_key_name(int key, char* buf, size_t buf_len) { - LONG lparam = (key & 0x7F) << 16; + // Extra mouse buttons (Mouse 4+) + if (key >= CTRL_EXTRA_MOUSE_SCAN_BASE && key < CTRL_EXTRA_MOUSE_SCAN_BASE + CTRL_EXTRA_MOUSE_SCAN_COUNT) { + int n = std::snprintf(buf, buf_len, "Mouse %d", (key - CTRL_EXTRA_MOUSE_SCAN_BASE) + 4); + return n > 0 ? n : 0; + } + // Extra keyboard keys + if (key >= CTRL_EXTRA_KEY_SCAN_BASE && key < CTRL_EXTRA_KEY_SCAN_BASE + CTRL_EXTRA_KEY_SCAN_COUNT) { + static const char* names[] = { + "Keypad /", "ISO \\", + "F13", "F14", "F15", "F16", "F17", "F18", + "F19", "F20", "F21", "F22", "F23", "F24", + }; + int n = std::snprintf(buf, buf_len, "%s", names[key - CTRL_EXTRA_KEY_SCAN_BASE]); + return n > 0 ? n : 0; + } + if (g_alpine_game_config.kbm_input_mode == 2) { + SDL_Scancode sc = rf_key_to_sdl_scancode(key); + if (sc == SDL_SCANCODE_UNKNOWN) { + buf[0] = '\0'; + return 0; + } + const char* name = SDL_GetKeyName(SDL_GetKeyFromScancode(sc, SDL_KMOD_NONE, false)); + if (!name || name[0] == '\0') { + buf[0] = '\0'; + return 0; + } + SDL_strlcpy(buf, name, buf_len); + return static_cast(SDL_strlen(name)); + } + // Modes 0/1: use Win32 key names + // Note: it seems broken on Wine with non-US layout due to MAPVK_VSC_TO_VK_EX mapping + LONG lparam = (key & 0x7F) << 16; if (key & 0x80) { lparam |= 1 << 24; } - // Note: it seems broken on Wine with non-US layout (most likely broken MAPVK_VSC_TO_VK_EX mapping is responsible) int ret = GetKeyNameTextA(lparam, buf, buf_len); if (ret <= 0) { WARN_ONCE("GetKeyNameTextA failed for 0x{:X}", key); @@ -495,15 +753,30 @@ FunHook key_msg_handler_hook{ case WM_KEYDOWN: case WM_SYSKEYDOWN: case WM_KEYUP: - case WM_SYSKEYUP: { - // For num pads, RF requires `KF_EXTENDED` to be set. - if (w_param == VK_PRIOR - || w_param == VK_NEXT - || w_param == VK_END - || w_param == VK_HOME) { + case WM_SYSKEYUP: + if (g_alpine_game_config.kbm_input_mode == 2 + && (SDL_WasInit(SDL_INIT_VIDEO) & SDL_INIT_VIDEO) != 0 + && SDL_GetKeyboardFocus() != nullptr) + return; // SDL handles keyboard in mode 2 + + // RF requires KF_EXTENDED for numpad-derived navigation keys + if (w_param == VK_PRIOR || w_param == VK_NEXT + || w_param == VK_END || w_param == VK_HOME) l_param |= KF_EXTENDED << 16; + + // Sentinel pattern for keys RF's rebind UI rejects (Alt, extra keys) + if ((msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN) + && rf::ui::options_controls_waiting_for_key + && g_pending_extra_key_rebind < 0) { + int win32_scan = (l_param >> 16) & 0x1FF; + int rf_scan = (win32_scan & 0x7F) | ((win32_scan & 0x100) ? 0x80 : 0); + if (scan_needs_rebind_sentinel(rf_scan)) { + g_pending_extra_key_rebind = rf_scan; + rf::key_process_event(CTRL_REBIND_SENTINEL, 1, 0); + return; + } } - } + break; } key_msg_handler_hook.call_target(msg, w_param, l_param); }, @@ -537,6 +810,5 @@ void key_apply_patch() // Support suppress autoswitch bind item_touch_weapon_autoswitch_patch.install(); - // Num pads need a patch to support `PgUp`, `PgDown`, `End`, and `Home`. key_msg_handler_hook.install(); -} +} \ No newline at end of file diff --git a/game_patch/input/mouse.cpp b/game_patch/input/mouse.cpp index f56dd11d..1c932ddf 100644 --- a/game_patch/input/mouse.cpp +++ b/game_patch/input/mouse.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include "input.h" #include "../os/console.h" #include "../rf/input.h" #include "../rf/entity.h" @@ -11,11 +13,21 @@ #include "../rf/multi.h" #include "../rf/player/player.h" #include "../rf/player/camera.h" +#include "../rf/ui.h" #include "../misc/alpine_settings.h" #include "../main/main.h" +#include "../os/os.h" #include "mouse.h" #include "../multi/multi.h" -#include "input.h" + +// SDL window and mouse motion state (used in SDL input mode only) +static bool g_relative_mouse_mode_window_missing_logged = false; +static float g_sdl_mouse_dx_rem = 0.0f, g_sdl_mouse_dy_rem = 0.0f; +static float g_sdl_mouse_wheel_rem = 0.0f; // sub-notch accumulator for smooth-scroll devices +static int g_sdl_mouse_dx = 0, g_sdl_mouse_dy = 0; + +// Extra mouse button rebind state (Mouse 4-8, used with SDL input mode) +static int g_pending_mouse_extra_btn_rebind = -1; // Raw mouse delta accumulators — captured in mouse_get_delta_hook, then consumed // by consume_raw_mouse_deltas() (via linear_pitch_patch for the player entity, or @@ -155,6 +167,51 @@ bool set_direct_input_enabled(bool enabled) return true; } +void set_kbm_input_mode(int mode) +{ + mode = std::clamp(mode, 0, 2); + + const int old_mode = g_alpine_game_config.kbm_input_mode; + g_alpine_game_config.kbm_input_mode = mode; + + if (!rf::is_dedicated_server) { + if (g_sdl_window) { + if (rf::keep_mouse_centered) + SDL_SetWindowRelativeMouseMode(g_sdl_window, mode == 2); + else + SDL_SetWindowMouseGrab(g_sdl_window, g_game_config.wnd_mode == GameConfig::STRETCHED); + } + + // Handle DirectInput transitions + if (mode == 1 && rf::keep_mouse_centered) { + set_direct_input_enabled(true); + } else if (old_mode == 1 && mode != 1) { + set_direct_input_enabled(false); + } + } + + // Clear SDL state when leaving SDL mode + if (old_mode == 2 && mode != 2) { + g_sdl_mouse_dx = 0; + g_sdl_mouse_dy = 0; + g_sdl_mouse_dx_rem = 0.0f; + g_sdl_mouse_dy_rem = 0.0f; + g_sdl_mouse_wheel_rem = 0.0f; + } + + // Release held extra scan codes so they don't stay stuck after mode switch + if (old_mode != mode) { + for (int i = 0; i < CTRL_EXTRA_MOUSE_SCAN_COUNT; ++i) + rf::key_process_event(CTRL_EXTRA_MOUSE_SCAN_BASE + i, 0, 0); + for (int i = 0; i < CTRL_EXTRA_KEY_SCAN_COUNT; ++i) + rf::key_process_event(CTRL_EXTRA_KEY_SCAN_BASE + i, 0, 0); + g_pending_mouse_extra_btn_rebind = -1; + } + + // Keep the Alpine Settings UI button label in sync + ui_refresh_kbm_input_mode_label(); +} + FunHook mouse_eval_deltas_hook{ 0x0051DC70, []() { @@ -162,9 +219,29 @@ FunHook mouse_eval_deltas_hook{ return; } - // disable mouse when window is not active - if (rf::os_foreground() || g_alpine_game_config.background_mouse) { - mouse_eval_deltas_hook.call_target(); + if (!rf::os_foreground() && !g_alpine_game_config.background_mouse) { + g_sdl_mouse_dx_rem = 0.0f; + g_sdl_mouse_dy_rem = 0.0f; + return; + } + + if (g_alpine_game_config.kbm_input_mode == 2) { + g_sdl_mouse_dx = static_cast(g_sdl_mouse_dx_rem); + g_sdl_mouse_dy = static_cast(g_sdl_mouse_dy_rem); + g_sdl_mouse_dx_rem -= g_sdl_mouse_dx; + g_sdl_mouse_dy_rem -= g_sdl_mouse_dy; + } + + mouse_eval_deltas_hook.call_target(); + + // Cursor centering fallback for SDL mode when relative mouse mode is unavailable (e.g. no SDL window) + if (rf::keep_mouse_centered && g_alpine_game_config.kbm_input_mode == 2 && (!g_sdl_window || !SDL_GetWindowRelativeMouseMode(g_sdl_window))) { + RECT rect{}; + GetClientRect(rf::main_wnd, &rect); + POINT pt{rect.right / 2, rect.bottom / 2}; + ClientToScreen(rf::main_wnd, &pt); + SetCursorPos(pt.x, pt.y); + SDL_FlushEvents(SDL_EVENT_MOUSE_MOTION, SDL_EVENT_MOUSE_MOTION); } }, }; @@ -180,10 +257,17 @@ FunHook mouse_eval_deltas_di_hook{ mouse_eval_deltas_di_hook.call_target(); - // Fix invalid mouse scroll delta, when DirectInput is turned off. - rf::mouse_old_z = rf::mouse_wheel_pos; + if (g_alpine_game_config.kbm_input_mode == 2 && rf::keep_mouse_centered) { + rf::mouse_dz = rf::mouse_wheel_pos - rf::mouse_old_z; + rf::mouse_old_z = rf::mouse_wheel_pos; + return; + } - // center cursor if in game + // Fix invalid mouse scroll delta when DirectInput is not active (mode 0 or SDL menu mode) + if (g_alpine_game_config.kbm_input_mode != 1) + rf::mouse_old_z = rf::mouse_wheel_pos; + + // Keep Win32 cursor at window centre so delta-from-centre aiming stays accurate if (rf::keep_mouse_centered) { POINT pt{rf::gr::screen_width() / 2, rf::gr::screen_height() / 2}; ClientToScreen(rf::main_wnd, &pt); @@ -192,17 +276,49 @@ FunHook mouse_eval_deltas_di_hook{ }, }; +FunHook mouse_set_visible_hook{ + 0x0051E680, + [](bool visible) { + mouse_set_visible_hook.call_target(visible); + if (g_sdl_window) { + if (visible) + SDL_ShowCursor(); + else + SDL_HideCursor(); + } + }, +}; + FunHook mouse_keep_centered_enable_hook{ 0x0051E690, []() { if (is_headless_mode()) { rf::keep_mouse_centered = false; + if (g_sdl_window) + SDL_SetWindowRelativeMouseMode(g_sdl_window, false); set_direct_input_enabled(false); return; } - if (!rf::keep_mouse_centered && !rf::is_dedicated_server) - set_direct_input_enabled(g_alpine_game_config.direct_input); + // keep_mouse_centered is still false here; call_target sets it + if (!rf::keep_mouse_centered && !rf::is_dedicated_server) { + // Release menu grab when entering gameplay; only set in stretched (borderless) mode. + if (g_sdl_window && g_game_config.wnd_mode == GameConfig::STRETCHED) + SDL_SetWindowMouseGrab(g_sdl_window, false); + switch (g_alpine_game_config.kbm_input_mode) { + case 1: // DirectInput mouse + set_direct_input_enabled(true); + break; + case 2: // SDL mouse + if (g_sdl_window) { + SDL_SetWindowRelativeMouseMode(g_sdl_window, true); + } else if (!g_relative_mouse_mode_window_missing_logged) { + xlog::warn("mouse_keep_centered_enable_hook: SDL window is null, cannot enable relative mouse mode"); + g_relative_mouse_mode_window_missing_logged = true; + } + break; + } + } mouse_keep_centered_enable_hook.call_target(); }, }; @@ -212,12 +328,30 @@ FunHook mouse_keep_centered_disable_hook{ []() { if (is_headless_mode()) { rf::keep_mouse_centered = false; + if (g_sdl_window) + SDL_SetWindowRelativeMouseMode(g_sdl_window, false); set_direct_input_enabled(false); return; } - if (rf::keep_mouse_centered) { - set_direct_input_enabled(false); + // keep_mouse_centered is still true here; call_target clears it + if (rf::keep_mouse_centered && !rf::is_dedicated_server) { + switch (g_alpine_game_config.kbm_input_mode) { + case 1: // DirectInput mouse + set_direct_input_enabled(false); + break; + case 2: // SDL mouse + if (g_sdl_window) { + SDL_SetWindowRelativeMouseMode(g_sdl_window, false); + } else if (!g_relative_mouse_mode_window_missing_logged) { + xlog::warn("mouse_keep_centered_disable_hook: SDL window is null, cannot disable relative mouse mode"); + g_relative_mouse_mode_window_missing_logged = true; + } + break; + } + // Confine cursor in stretched (borderless) mode; SDL cursor navigation is active in all input modes. + if (g_sdl_window && g_game_config.wnd_mode == GameConfig::STRETCHED) + SDL_SetWindowMouseGrab(g_sdl_window, true); reset_mouse_delta_accumulators(); } mouse_keep_centered_disable_hook.call_target(); @@ -229,6 +363,14 @@ FunHook mouse_get_delta_hook{ [](int& dx, int& dy, int& dz) { mouse_get_delta_hook.call_target(dx, dy, dz); // fills dz (scroll wheel) + if (g_alpine_game_config.kbm_input_mode == 2 && g_sdl_window) { + dx = g_sdl_mouse_dx; + dy = g_sdl_mouse_dy; + g_sdl_mouse_dx = 0; + g_sdl_mouse_dy = 0; + dz = rf::mouse_dz; + } + // Nothing to do in Classic mode or outside gameplay. if (!rf::keep_mouse_centered || g_alpine_game_config.mouse_scale == 0) { reset_mouse_delta_accumulators(); @@ -256,10 +398,9 @@ FunHook mouse_get_delta_hook{ // In Raw/Modern mode: capture raw deltas for centralized angle // computation and zero them so RF does not apply its own scaling. - // Skip when in a vehicle (RF needs the deltas to steer), but scale + // Skip zeroing when in a vehicle (RF needs the deltas to steer), but scale // them down to stay consistent with the camera formula feel. - bool in_vehicle = rf::local_player_entity && - rf::entity_in_vehicle(rf::local_player_entity); + bool in_vehicle = rf::local_player_entity && rf::entity_in_vehicle(rf::local_player_entity); if (!in_vehicle) { g_camera_mouse_dx += dx; g_camera_mouse_dy += dy; @@ -283,32 +424,30 @@ FunHook mouse_get_delta_hook{ }, }; -ConsoleCommand2 input_mode_cmd{ +ConsoleCommand2 kbm_input_mode_cmd{ "inputmode", - []() { + [](std::optional mode_opt) { + static constexpr const char* mode_names[] = {"Legacy", "DirectInput", "SDL"}; + if (is_headless_mode()) { - g_alpine_game_config.direct_input = false; - set_direct_input_enabled(false); - rf::console::print("DirectInput is disabled in headless bot mode"); + set_kbm_input_mode(0); + rf::console::print("KB/M Input mode is disabled in headless bot mode"); return; } - g_alpine_game_config.direct_input = !g_alpine_game_config.direct_input; - - if (g_alpine_game_config.direct_input) { - if (!set_direct_input_enabled(g_alpine_game_config.direct_input)) { - rf::console::print("Failed to initialize DirectInput"); - } - else { - set_direct_input_enabled(rf::keep_mouse_centered); - rf::console::print("DirectInput is enabled"); - } - } - else { - rf::console::print("DirectInput is disabled"); + if (!mode_opt) { + // No argument: print current mode + int cur = g_alpine_game_config.kbm_input_mode; + rf::console::print("KB/M Input mode: {} ({})", cur, mode_names[cur]); + return; } + + int new_mode = std::clamp(mode_opt.value(), 0, 2); + set_kbm_input_mode(new_mode); + rf::console::print("KB/M Input mode: {} ({})", new_mode, mode_names[new_mode]); }, - "Toggles DirectInput mouse mode", + "Gets or sets KB/M input mode: 0=Legacy Win32, 1=DirectInput mouse+Legacy keyboard, 2=SDL mouse+keyboard", + "inputmode [0|1|2]", }; ConsoleCommand2 ms_cmd{ @@ -420,7 +559,116 @@ CodeInjection static_zoom_sensitivity_patch2 { }, }; +void process_extra_mouse_button(const SDL_Event& ev) +{ + if (g_alpine_game_config.kbm_input_mode != 2) + return; + if (ev.button.button < SDL_BUTTON_X1 || + ev.button.button >= SDL_BUTTON_X1 + CTRL_EXTRA_MOUSE_SCAN_COUNT) + return; + + int rf_btn = static_cast(ev.button.button) - 1; // SDL 4→rf 3, SDL 5→rf 4 ... + + if (ev.button.down && g_pending_mouse_extra_btn_rebind < 0 + && rf::ui::options_controls_waiting_for_key) { + // Rebind UI active: inject the sentinel key so RF processes the rebind, + // then ui.cpp's falling-edge handler replaces it with our custom scan code. + g_pending_mouse_extra_btn_rebind = rf_btn; + rf::key_process_event(CTRL_REBIND_SENTINEL, 1, 0); + } else { + int scan_code = CTRL_EXTRA_MOUSE_SCAN_BASE + (rf_btn - 3); + rf::key_process_event(scan_code, ev.button.down ? 1 : 0, 0); + } +} + +void process_mouse_event(const SDL_Event& ev) +{ + switch (ev.type) { + case SDL_EVENT_MOUSE_MOTION: + if (ev.motion.which == SDL_TOUCH_MOUSEID || ev.motion.which == SDL_PEN_MOUSEID) { + // Touch/pen: sync Win32 cursor position in menus. + if (!rf::keep_mouse_centered) { + POINT pt{static_cast(ev.motion.x), static_cast(ev.motion.y)}; + ClientToScreen(rf::main_wnd, &pt); + SetCursorPos(pt.x, pt.y); + } + } else if (g_alpine_game_config.kbm_input_mode == 2 && rf::os_foreground()) { + g_sdl_mouse_dx_rem += ev.motion.xrel; + g_sdl_mouse_dy_rem += ev.motion.yrel; + } + break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + if (ev.button.which == SDL_TOUCH_MOUSEID || ev.button.which == SDL_PEN_MOUSEID) { + // Touch/pen taps: synthesise a left-click at the tap position in menus. + if (!rf::keep_mouse_centered && ev.button.button == SDL_BUTTON_LEFT) { + POINT pt{static_cast(ev.button.x), static_cast(ev.button.y)}; + ClientToScreen(rf::main_wnd, &pt); + SetCursorPos(pt.x, pt.y); + UINT wmsg = (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN) ? WM_LBUTTONDOWN : WM_LBUTTONUP; + WPARAM wp = (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN) ? MK_LBUTTON : 0; + PostMessageA(rf::main_wnd, wmsg, wp, + MAKELPARAM(static_cast(ev.button.x), static_cast(ev.button.y))); + } + } else { + process_extra_mouse_button(ev); + } + break; + case SDL_EVENT_MOUSE_WHEEL: + // Feed scroll into rf::mouse_wheel_pos (120 units per notch, matching Win32/DInput). + if (g_alpine_game_config.kbm_input_mode == 2 + && ev.wheel.which != SDL_TOUCH_MOUSEID + && ev.wheel.which != SDL_PEN_MOUSEID) { + float dy = (ev.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) + ? -ev.wheel.y : ev.wheel.y; + g_sdl_mouse_wheel_rem += dy; + int notches = static_cast(g_sdl_mouse_wheel_rem); + if (notches != 0) { + rf::mouse_wheel_pos += notches * 120; + g_sdl_mouse_wheel_rem -= static_cast(notches); + } + } + break; + default: + break; + } +} + +int mouse_take_pending_rebind() +{ + int btn = g_pending_mouse_extra_btn_rebind; + g_pending_mouse_extra_btn_rebind = -1; + return btn; +} + +void mouse_on_focus_changed(bool focused) +{ + if (!g_sdl_window) + return; + + // Reset SDL delta accumulators on focus change to prevent stale deltas replaying. + g_sdl_mouse_dx = 0; + g_sdl_mouse_dy = 0; + g_sdl_mouse_dx_rem = 0.0f; + g_sdl_mouse_dy_rem = 0.0f; + + if (focused) { + // Gameplay (mode 2): re-enable relative mouse mode; SDL3 may have cleared it on focus loss. + if (g_alpine_game_config.kbm_input_mode == 2 && rf::keep_mouse_centered) + SDL_SetWindowRelativeMouseMode(g_sdl_window, true); + // Menu: confine cursor in stretched (borderless) mode to prevent escape to other monitors. + if (!rf::keep_mouse_centered && g_game_config.wnd_mode == GameConfig::STRETCHED) + SDL_SetWindowMouseGrab(g_sdl_window, true); + SDL_FlushEvents(SDL_EVENT_MOUSE_MOTION, SDL_EVENT_MOUSE_MOTION); + } else { + // Show cursor so it's visible in other applications after alt-tab. + SDL_ShowCursor(); + // Only release grab in stretched (borderless) mode; fullscreen conflicts with D3D11's exclusive swap chain. + if (g_game_config.wnd_mode == GameConfig::STRETCHED) + SDL_SetWindowMouseGrab(g_sdl_window, false); + } +} void mouse_apply_patch() { @@ -435,8 +683,13 @@ void mouse_apply_patch() // Disable mouse when window is not active mouse_eval_deltas_hook.install(); - // Add DirectInput mouse support + // Scroll-wheel fix and Win32 cursor centering for Legacy/DInput modes (0 and 1) mouse_eval_deltas_di_hook.install(); + + // Sync SDL cursor visibility with the game's own cursor show/hide calls + mouse_set_visible_hook.install(); + + // Mouse mode hooks (DInput or SDL depending on kbm_input_mode) mouse_keep_centered_enable_hook.install(); mouse_keep_centered_disable_hook.install(); mouse_get_delta_hook.install(); @@ -444,14 +697,11 @@ void mouse_apply_patch() // Do not limit the cursor to the game window if in menu (Win32 mouse) AsmWriter(0x0051DD7C).jmp(0x0051DD8E); - // Use exclusive DirectInput mode so cursor cannot exit game window - //write_mem(0x0051E14B + 1, 5); // DISCL_EXCLUSIVE|DISCL_FOREGROUND - // Commands - input_mode_cmd.register_cmd(); + kbm_input_mode_cmd.register_cmd(); ms_cmd.register_cmd(); + ms_scale_cmd.register_cmd(); static_scope_sens_cmd.register_cmd(); scope_sens_cmd.register_cmd(); scanner_sens_cmd.register_cmd(); - ms_scale_cmd.register_cmd(); } diff --git a/game_patch/input/mouse.h b/game_patch/input/mouse.h index 14ff3e51..0648ea8c 100644 --- a/game_patch/input/mouse.h +++ b/game_patch/input/mouse.h @@ -1,4 +1,15 @@ #pragma once +#include + +// Custom scan codes for extra mouse buttons (Mouse 4 and above), stored in scan_codes[0]. +// Range placed after CTRL_GAMEPAD_RIGHT_TRIGGER (0x74) to avoid conflicts. +static constexpr int CTRL_EXTRA_MOUSE_SCAN_BASE = 0x75; // scan code for rf_btn 3 (Mouse 4) +static constexpr int CTRL_EXTRA_MOUSE_SCAN_COUNT = 5; // covers Mouse 4-8 (rf indices 3-7) + void mouse_apply_patch(); +void mouse_on_focus_changed(bool focused); +int mouse_take_pending_rebind(); +void process_extra_mouse_button(const SDL_Event& ev); +void process_mouse_event(const SDL_Event& ev); void consume_raw_mouse_deltas(float& out_pitch, float& out_yaw, bool apply_scope_sens); diff --git a/game_patch/main/main.cpp b/game_patch/main/main.cpp index d6e7334c..5d0fd6c8 100644 --- a/game_patch/main/main.cpp +++ b/game_patch/main/main.cpp @@ -76,6 +76,9 @@ CallHook rf_init_hook{ xlog::info("Initializing game..."); initialize_alpine_core_config(); rf_init_hook.call_target(); + if (!rf::is_dedicated_server && rf::main_wnd) { + os_init_sdl_window(); + } vpackfile_disable_overriding(); xlog::info("Game initialized ({} ms).", GetTickCount64() - start_ticks); }, diff --git a/game_patch/misc/alpine_settings.cpp b/game_patch/misc/alpine_settings.cpp index 1b89709a..993518a2 100644 --- a/game_patch/misc/alpine_settings.cpp +++ b/game_patch/misc/alpine_settings.cpp @@ -6,6 +6,7 @@ #include "alpine_settings.h" #include "alpine_options.h" #include +#include "../input/input.h" #include "../os/console.h" #include "../os/os.h" #include "../rf/ui.h" @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -1076,8 +1078,18 @@ bool alpine_player_settings_load(rf::Player* player) player->settings.controls.axes[1].invert = std::stoi(settings["MouseYInvert"]); processed_keys.insert("MouseYInvert"); } - if (settings.count("DirectInput")) { - g_alpine_game_config.direct_input = std::stoi(settings["DirectInput"]); + if (settings.count("KBMInputMode")) { + int kbm_input_mode = std::stoi(settings["KBMInputMode"]); + set_kbm_input_mode(std::clamp(kbm_input_mode, 0, 2)); + processed_keys.insert("KBMInputMode"); + } else if (settings.count("SDL")) { + int sdl_mode = std::stoi(settings["SDL"]); + set_kbm_input_mode((sdl_mode != 0) ? 2 : 1); + processed_keys.insert("SDL"); + } else if (settings.count("DirectInput")) { + // Legacy: DirectInput=0 was Legacy/Win32 (mode 0), DirectInput=1 was DInput (mode 1) + int direct_input = std::stoi(settings["DirectInput"]); + set_kbm_input_mode((direct_input != 0) ? 1 : 0); processed_keys.insert("DirectInput"); } if (settings.count("MouseLinearPitch")) { @@ -1188,7 +1200,7 @@ void alpine_control_config_serialize(std::ofstream& file, const rf::ControlConfi file << "\n[InputSettings]\n"; file << "MouseSensitivity=" << cc.mouse_sensitivity << "\n"; file << "MouseYInvert=" << cc.axes[1].invert << "\n"; - file << "DirectInput=" << g_alpine_game_config.direct_input << "\n"; + file << "KBMInputMode=" << g_alpine_game_config.kbm_input_mode << "\n"; file << "MouseLinearPitch=" << g_alpine_game_config.mouse_linear_pitch << "\n"; file << "MouseScale=" << g_alpine_game_config.mouse_scale << "\n"; file << "SwapARBinds=" << g_alpine_game_config.swap_ar_controls << "\n"; @@ -1537,7 +1549,7 @@ static void set_headless_defaults(rf::Player* player, const char* player_name, u g_alpine_game_config.swap_ar_controls = false; g_alpine_game_config.swap_gn_controls = false; g_alpine_game_config.swap_sg_controls = false; - g_alpine_game_config.direct_input = false; + g_alpine_game_config.kbm_input_mode = 0; // headless mode uses legacy mouse path g_alpine_game_config.save_console_history = false; g_alpine_game_config.set_max_fps(max_fps); g_alpine_game_config.dbg_bot = false; diff --git a/game_patch/misc/alpine_settings.h b/game_patch/misc/alpine_settings.h index 564f6e5a..837aa68e 100644 --- a/game_patch/misc/alpine_settings.h +++ b/game_patch/misc/alpine_settings.h @@ -150,7 +150,7 @@ struct AlpineGameSettings bool display_target_player_names = true; bool verbose_time_left_display = true; bool nearest_texture_filtering = false; - bool direct_input = true; + int kbm_input_mode = 2; // 0=Win32 mouse+keyboard (referred as Classic), 1=DInput mouse+Win32 keyboard, 2=SDL mouse+keyboard bool scoreboard_anim = true; bool legacy_bob = false; bool scoreboard_split_simple = true; diff --git a/game_patch/misc/ui.cpp b/game_patch/misc/ui.cpp index 44fb5034..73a62aa1 100644 --- a/game_patch/misc/ui.cpp +++ b/game_patch/misc/ui.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -19,6 +20,7 @@ #include "../rf/misc.h" #include "../rf/os/os.h" #include "../object/object.h" +#include "../input/input.h" #include "../graphics/d3d11/gr_d3d11_mesh.h" #include "../rf/level.h" @@ -91,8 +93,12 @@ static char ao_simdist_butlabel_text[9]; // alpine options checkboxes and labels static rf::ui::Checkbox ao_mpcharlod_cbox; static rf::ui::Label ao_mpcharlod_label; -static rf::ui::Checkbox ao_dinput_cbox; -static rf::ui::Label ao_dinput_label; +static rf::ui::Checkbox ao_kbm_input_mode_cbox; +static rf::ui::Label ao_kbm_input_mode_label; +static rf::ui::Label ao_kbm_input_mode_butlabel; +static char ao_kbm_input_mode_butlabel_text[16]; +static bool g_alpine_options_just_switched_kbm_input_mode = false; +static constexpr const char* kbm_input_mode_names[] = {"Classic", "DInput", "SDL"}; static rf::ui::Checkbox ao_linearpitch_cbox; static rf::ui::Label ao_linearpitch_label; static rf::ui::Checkbox ao_mousecamerascale_cbox; @@ -372,6 +378,8 @@ void __fastcall UiInputBox_create(rf::ui::InputBox& this_, int, rf::ui::Gadget * } FunHook UiInputBox_create_hook{0x00456FE0, UiInputBox_create}; +auto UiInputBox_add_char = reinterpret_cast(0x00457260); + void __fastcall UiInputBox_render(rf::ui::InputBox& this_, void*) { if (this_.enabled && this_.highlighted) { @@ -398,6 +406,25 @@ void __fastcall UiInputBox_render(rf::ui::InputBox& this_, void*) } rf::gr::set_clip(clip_x, clip_y, clip_w, clip_h); + static const rf::ui::InputBox* s_text_input_owner = nullptr; + if (this_.enabled && this_.highlighted && g_sdl_window && SDL_HasScreenKeyboardSupport()) { + if (s_text_input_owner != &this_) { + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, SDL_TEXTINPUT_TYPE_NUMBER); + SDL_StartTextInputWithProperties(g_sdl_window, props); + SDL_DestroyProperties(props); + s_text_input_owner = &this_; + } + auto pending = keyboard_take_pending_text(); + for (char c : pending) + UiInputBox_add_char(&this_, c); + } else if (s_text_input_owner == &this_) { + keyboard_take_pending_text(); + if (g_sdl_window) + SDL_StopTextInput(g_sdl_window); + s_text_input_owner = nullptr; + } + debug_ui_layout(this_); } FunHook UiInputBox_render_hook{0x004570E0, UiInputBox_render}; @@ -490,8 +517,6 @@ FunHook menu_init_hook{ }, }; -auto UiInputBox_add_char = reinterpret_cast(0x00457260); - extern FunHook UiInputBox_process_key_hook; bool __fastcall UiInputBox_process_key_new(void *this_, int edx, rf::Key key) { @@ -550,11 +575,19 @@ void ao_bighud_cbox_on_click(int x, int y) { ao_play_button_snd(g_alpine_game_config.big_hud); } -void ao_dinput_cbox_on_click(int x, int y) +void ui_refresh_kbm_input_mode_label() { - g_alpine_game_config.direct_input = !g_alpine_game_config.direct_input; - ao_dinput_cbox.checked = g_alpine_game_config.direct_input; - ao_play_button_snd(g_alpine_game_config.direct_input); + int mode_index = std::clamp(g_alpine_game_config.kbm_input_mode, 0, 2); + snprintf(ao_kbm_input_mode_butlabel_text, sizeof(ao_kbm_input_mode_butlabel_text), "%s", + kbm_input_mode_names[mode_index]); + ao_kbm_input_mode_butlabel.text = ao_kbm_input_mode_butlabel_text; +} + +void ao_kbm_input_mode_cbox_on_click([[maybe_unused]] int x, [[maybe_unused]] int y) { + set_kbm_input_mode((g_alpine_game_config.kbm_input_mode + 1) % 3); + ui_refresh_kbm_input_mode_label(); + g_alpine_options_just_switched_kbm_input_mode = true; + ao_play_button_snd(true); } void ao_linearpitch_cbox_on_click(int x, int y) { @@ -1009,10 +1042,18 @@ void alpine_options_panel_handle_key(rf::Key* key){ // todo: more key support (tab, etc.) // close panel on escape if (*key == rf::Key::KEY_ESC) { + if (g_alpine_options_just_switched_kbm_input_mode) { + // Ignore the ESC stutter that can occur when switching to SDL mode. + g_alpine_options_just_switched_kbm_input_mode = false; + return; + } + rf::ui::options_close_current_panel(); rf::snd_play(43, 0, 0.0f, 1.0f); return; } + + g_alpine_options_just_switched_kbm_input_mode = false; } void alpine_options_panel_handle_mouse(int x, int y) { @@ -1213,8 +1254,9 @@ void alpine_options_panel_init() { &ao_always_show_spectators_cbox, &ao_always_show_spectators_label, &alpine_options_panel1, ao_always_show_spectators_cbox_on_click, g_alpine_game_config.always_show_spectators, 280, 264, "Show spectators"); // panel 2 - alpine_options_panel_checkbox_init( - &ao_dinput_cbox, &ao_dinput_label, &alpine_options_panel2, ao_dinput_cbox_on_click, g_alpine_game_config.direct_input, 112, 54, "DirectInput"); + alpine_options_panel_inputbox_init( + &ao_kbm_input_mode_cbox, &ao_kbm_input_mode_label, &ao_kbm_input_mode_butlabel, &alpine_options_panel2, ao_kbm_input_mode_cbox_on_click, 112, 54, "KB/M Input"); + ui_refresh_kbm_input_mode_label(); alpine_options_panel_checkbox_init( &ao_linearpitch_cbox, &ao_linearpitch_label, &alpine_options_panel2, ao_linearpitch_cbox_on_click, g_alpine_game_config.mouse_linear_pitch, 112, 84, "Linear pitch"); alpine_options_panel_checkbox_init( @@ -1537,8 +1579,45 @@ CodeInjection options_render_alpine_panel_patch{ int index = rf::ui::options_current_panel; //xlog::warn("render index {}", index); - if (index == 3 && !rf::ui::options_controls_waiting_for_key) { - render_ctrl_camscale_btns(); + // handle key rebinding in input options panel + if (index == 3) { + static bool s_was_waiting = false; + bool now_waiting = rf::ui::options_controls_waiting_for_key; + if (s_was_waiting && !now_waiting) { + int16_t new_sc = -1; + int xbtn = mouse_take_pending_rebind(); + if (xbtn >= 0) { + new_sc = static_cast(CTRL_EXTRA_MOUSE_SCAN_BASE + (xbtn - 3)); + } else { + int extra_key = key_take_pending_extra_rebind(); + if (extra_key >= 0) + new_sc = static_cast(extra_key); + } + if (new_sc >= 0 && rf::local_player) { + auto& cc = rf::local_player->settings.controls; + int n = std::min(cc.num_bindings, 128); + bool found = false; + for (int i = 0; i < n && !found; ++i) { + for (int slot = 0; slot < 2 && !found; ++slot) { + if (cc.bindings[i].scan_codes[slot] == static_cast(CTRL_REBIND_SENTINEL)) { + for (int j = 0; j < n; ++j) + for (int s = 0; s < 2; ++s) + if ((j != i || s != slot) && cc.bindings[j].scan_codes[s] == new_sc) + cc.bindings[j].scan_codes[s] = -1; + cc.bindings[i].scan_codes[slot] = new_sc; + found = true; + } + } + } + rf::key_process_event(CTRL_REBIND_SENTINEL, 0, 0); + } + } + s_was_waiting = now_waiting; + + // Render the mouse scale control button in the controls tab when user is not rebinding + if (!rf::ui::options_controls_waiting_for_key) { + render_ctrl_camscale_btns(); + } } // render alpine options panel diff --git a/game_patch/os/frametime.cpp b/game_patch/os/frametime.cpp index 7cb5e677..a81eba2a 100644 --- a/game_patch/os/frametime.cpp +++ b/game_patch/os/frametime.cpp @@ -11,6 +11,7 @@ #include "../rf/hud.h" #include "../rf/entity.h" #include "../rf/os/frametime.h" +#include "os.h" #include "../multi/multi.h" #include "../main/main.h" #include "../misc/alpine_settings.h" diff --git a/game_patch/os/os.cpp b/game_patch/os/os.cpp index dcdfdcde..01e0159b 100644 --- a/game_patch/os/os.cpp +++ b/game_patch/os/os.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -7,6 +8,8 @@ #include "../rf/os/os.h" #include "../rf/multi.h" #include "../rf/input.h" +#include "../input/input.h" +#include "../misc/alpine_settings.h" #include "../rf/crt.h" #include "../main/main.h" #include "../multi/multi.h" @@ -17,6 +20,9 @@ const char* get_win_msg_name(UINT msg); +static bool g_sdl_video_initialized = false; +SDL_Window* g_sdl_window = nullptr; + FunHook os_poll_hook{ 0x00524B60, []() { @@ -34,6 +40,16 @@ FunHook os_poll_hook{ if (win32_console_is_enabled()) { win32_console_poll_input(); } + + if (g_sdl_video_initialized) { + // Flush WM_MOUSEMOVE spam before SDL drains the queue + if (g_alpine_game_config.kbm_input_mode == 2 && rf::keep_mouse_centered) { + MSG flush_msg; + while (PeekMessageA(&flush_msg, nullptr, WM_MOUSEMOVE, WM_MOUSEMOVE, PM_REMOVE)) + ; + } + sdl_input_poll(); + } }, }; @@ -60,19 +76,8 @@ LRESULT WINAPI wnd_proc(HWND wnd_handle, UINT msg, WPARAM w_param, LPARAM l_para return 0; } - if (!rf::is_dedicated_server) { - // Show cursor if window is not active - if (w_param) { - ShowCursor(FALSE); - while (ShowCursor(FALSE) >= 0) - ; - } - else { - ShowCursor(TRUE); - while (ShowCursor(TRUE) < 0) - ; - } - } + if (g_sdl_video_initialized) + mouse_on_focus_changed(w_param != 0); rf::is_main_wnd_active = w_param; return 0; //DefWindowProcA(wnd_handle, msg, w_param, l_param); @@ -92,6 +97,11 @@ 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_QUIT: case WM_CLOSE: case WM_DESTROY: @@ -247,6 +257,7 @@ static FunHook os_close_hook{ []() { os_close_hook.call_target(); win32_console_close(); + SDL_Quit(); }, }; @@ -297,7 +308,7 @@ void wait_for(const float ms, const WaitableTimer& timer) { } Sleep(static_cast(ms)); } else { - // `SetWaitableTimer` requires 100-nanosecond intervals. + // SetWaitableTimer requires 100-nanosecond intervals. // Negative values indicate relative time. LARGE_INTEGER dur{ .QuadPart = -static_cast(static_cast(ms) * 10'000.) @@ -315,8 +326,46 @@ void wait_for(const float ms, const WaitableTimer& timer) { } } +void os_init_sdl_video() +{ + if (rf::is_dedicated_server) + return; + g_sdl_video_initialized = SDL_Init(SDL_INIT_VIDEO); + if (!g_sdl_video_initialized) + xlog::error("SDL_Init(SDL_INIT_VIDEO) failed: {}", SDL_GetError()); +} + +void os_init_sdl_window() +{ + // Deferred after D3D device creation so SDL's hidden helper window does not + // interfere with exclusive fullscreen acquisition. + os_init_sdl_video(); + if (!g_sdl_video_initialized) + return; + + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER, rf::main_wnd); + g_sdl_window = SDL_CreateWindowWithProperties(props); + SDL_DestroyProperties(props); + if (!g_sdl_window) { + xlog::error("SDL_CreateWindowWithProperties failed: {}", SDL_GetError()); + return; + } +} + void os_apply_patch() { + if (!is_d3d11()) { + // Fix bitmap scaling on legacy D3D8/9 renderer + if (auto* set_dpi_ctx = reinterpret_cast( + GetProcAddress(GetModuleHandleA("user32.dll"), "SetProcessDpiAwarenessContext"))) { + set_dpi_ctx(reinterpret_cast(-1)); // DPI_AWARENESS_CONTEXT_UNAWARE + } + } + + // SDL video and window init is deferred to os_init_sdl_window, after D3D device creation, + // to avoid SDL's hidden helper window interfering with exclusive fullscreen acquisition. + // Process messages in the same thread as DX processing (alternative: D3DCREATE_MULTITHREADED) AsmWriter(0x00524C48, 0x00524C83).nop(); // disable msg loop thread AsmWriter(0x00524C48).call(0x00524E40); // os_create_main_window diff --git a/game_patch/os/os.h b/game_patch/os/os.h index 37214882..47887d02 100644 --- a/game_patch/os/os.h +++ b/game_patch/os/os.h @@ -7,7 +7,12 @@ #include #include +struct SDL_Window; +extern SDL_Window* g_sdl_window; + void os_apply_patch(); +void os_init_sdl_video(); +void os_init_sdl_window(); void frametime_render_ui(); float get_maximum_fps(); void apply_maximum_fps(); diff --git a/game_patch/rf/input.h b/game_patch/rf/input.h index eda4decd..1e4698b9 100644 --- a/game_patch/rf/input.h +++ b/game_patch/rf/input.h @@ -1,7 +1,7 @@ #pragma once // RF uses DirectInput 8 -#define DIRECTINPUT_VERSION 0x0800 +#define DIRECTINPUT_VERSION 0x0800 #include #include diff --git a/resources/licensing-info.txt b/resources/licensing-info.txt index ca164937..55e35890 100644 --- a/resources/licensing-info.txt +++ b/resources/licensing-info.txt @@ -514,3 +514,28 @@ freely, subject to the following restrictions: 3. This notice may not be removed or altered from any source distribution. René Nyffenegger rene.nyffenegger@adp-gmbh.ch + +############################################################################### +## sdl +############################################################################### + +Source code: https://github.com/libsdl-org/SDL + +Copyright (C) 1997-2026 Sam Lantinga + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + diff --git a/vendor/CMakeLists.txt b/vendor/CMakeLists.txt index 2814f31d..646d49aa 100644 --- a/vendor/CMakeLists.txt +++ b/vendor/CMakeLists.txt @@ -12,3 +12,4 @@ add_subdirectory(sha1) add_subdirectory(ed25519) add_subdirectory(base64) add_subdirectory(stb) +add_subdirectory(sdl) \ No newline at end of file diff --git a/vendor/sdl/CMakeLists.txt b/vendor/sdl/CMakeLists.txt new file mode 100644 index 00000000..f372dc92 --- /dev/null +++ b/vendor/sdl/CMakeLists.txt @@ -0,0 +1,19 @@ +# To update or downgrade SDL3, change the SDL3_VERSION and SDL3_HASH (sha256) variables below. +# Version histories and sha256 hashes can be found at SDL release pages: https://github.com/libsdl-org/SDL/releases +# Read the sdl wiki for more info about CMake-configurations: https://wiki.libsdl.org/SDL3/README-cmake + +set(SDL3_VERSION "3.4.8") +set(SDL3_HASH "e9fff7467fb60f037e6708da18b25560649e4c63edc2a69bb871b960d9cbfbba") + +include(FetchContent) +FetchContent_Declare( + SDL3 + URL "https://github.com/libsdl-org/SDL/releases/download/release-${SDL3_VERSION}/SDL3-${SDL3_VERSION}.tar.gz" + URL_HASH SHA256=${SDL3_HASH} + DOWNLOAD_EXTRACT_TIMESTAMP FALSE +) + +set(SDL_STATIC ON CACHE BOOL "" FORCE) +set(SDL_SHARED OFF CACHE BOOL "" FORCE) +set(SDL_TEST OFF CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(SDL3) \ No newline at end of file diff --git a/vendor/sdl/LICENSE.txt b/vendor/sdl/LICENSE.txt new file mode 100644 index 00000000..e9adee44 --- /dev/null +++ b/vendor/sdl/LICENSE.txt @@ -0,0 +1,18 @@ +Copyright (C) 1997-2026 Sam Lantinga + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. +