Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9d0ca84
added SDL
AL2009man Mar 10, 2026
67ef09c
replace DirectInput Mouse with SDL Mouse
AL2009man Mar 14, 2026
7fe9de1
attempt to replace Win32 DPI Lock
AL2009man Mar 14, 2026
5cfb5d1
replace Win32 Cursor with SDL Cursor event
AL2009man Mar 14, 2026
4e34806
applied copilot suggestions
AL2009man Mar 14, 2026
4d472c9
restoring D3D Bitt-map scaling
AL2009man Mar 15, 2026
712dff6
restores stock/sdl mouse button
AL2009man Mar 15, 2026
a4c1f5a
upgrade SDL package with CMake FetchContent
AL2009man Mar 15, 2026
2a93e57
adds SDL Keyboard
AL2009man Mar 16, 2026
37b1795
backport timer fixes
AL2009man Mar 16, 2026
587c3c8
move minor SDL Mouse config ordering
AL2009man Mar 16, 2026
a374ba2
fixed SDL cursor centering fallback
AL2009man Mar 16, 2026
7db1050
You can now swap between Input Modes
AL2009man Mar 17, 2026
f3d03b0
Additional Mouse buttons, refactor SDL input handling, fixed input mo…
AL2009man Mar 17, 2026
11dde05
Reset accumulated SDL mouse motion when unfocused
AL2009man Mar 17, 2026
ce4d3c3
added Alt key binding to both Legacy and SDL Keyboard
AL2009man Mar 17, 2026
063a5dc
Merge branch 'master' into SDL-Mouse
AL2009man Mar 18, 2026
c9fed47
centralize SDL Mouse accumulators
AL2009man Mar 19, 2026
be4ecea
replace leftover code during the InputMode expansion
AL2009man Mar 19, 2026
302453a
fix merge conflict after the multiplayer bot update
AL2009man Mar 20, 2026
00894db
Merge branch 'master' into SDL-Mouse
AL2009man Mar 20, 2026
a43cf10
fixed build error after multiplayer bot support
Mar 20, 2026
703dc94
fix camera spinning when in SDL Input mode
Mar 21, 2026
b5dbf84
Merge branch 'master' into SDL-Mouse
AL2009man Mar 24, 2026
68ee6fc
fix regression with Mouse Scale button disappearing in the Controls tab
AL2009man Mar 24, 2026
12a9cbf
Merge branch 'master' into SDL-Mouse
AL2009man Mar 26, 2026
9274bff
restored sdl window/mouse state during the merge
AL2009man Mar 26, 2026
d9aca94
adds ALT tab camera freeze fix
AL2009man Mar 26, 2026
a1d770b
fixed multiple regressions after mouse-scale migration
AL2009man Mar 26, 2026
2cf5a27
change SDL3 `CMakeLists.txt` to include hash
AL2009man Mar 27, 2026
3ebaa5e
update to SDL 3.4.4
AL2009man Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions game_patch/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ set(SRCS
graphics/d3d11/gr_d3d11_hooks.cpp
graphics/d3d11/gr_d3d11_hooks.h
input/input.h
input/input.cpp
input/mouse.cpp
input/key.cpp
hud/hud_colors.cpp
Expand Down Expand Up @@ -264,4 +265,13 @@ target_link_libraries(AlpineFaction
ws2_32
freetype
stb_vorbis
SDL3::SDL3-static
)

if(WIN32)
target_link_libraries(AlpineFaction
setupapi
imm32
cfgmgr32
)
endif()
13 changes: 13 additions & 0 deletions game_patch/input/input.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Central SDL event pump for all input subsystems.
#include <SDL3/SDL.h>
#include "input.h"
#include "../misc/alpine_settings.h"

void sdl_input_poll()
{
if (SDL_IsMainThread())
SDL_PumpEvents();
if (g_alpine_game_config.input_mode == 2)
keyboard_sdl_poll();
mouse_sdl_poll();
}
5 changes: 5 additions & 0 deletions game_patch/input/input.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@
rf::ControlConfigAction get_af_control(rf::AlpineControlConfigAction alpine_control);
rf::String get_action_bind_name(int action);
void mouse_apply_patch();
void mouse_init_sdl_window();
void mouse_sdl_poll();
void keyboard_sdl_poll();
void sdl_input_poll();
void key_apply_patch();
void set_input_mode(int mode);
307 changes: 248 additions & 59 deletions game_patch/input/key.cpp

Large diffs are not rendered by default.

188 changes: 158 additions & 30 deletions game_patch/input/mouse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <patch_common/CodeInjection.h>
#include <patch_common/AsmWriter.h>
#include <xlog/xlog.h>
#include <SDL3/SDL.h>
#include "../os/console.h"
#include "../rf/input.h"
#include "../rf/os/os.h"
Expand All @@ -14,12 +15,17 @@
#include "../misc/alpine_settings.h"
#include "../main/main.h"

static SDL_Window* g_sdl_window = nullptr;
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 int g_sdl_mouse_dx = 0, g_sdl_mouse_dy = 0;

static float scope_sensitivity_value = 0.25f;
static float scanner_sensitivity_value = 0.25f;
static float applied_static_sensitivity_value = 0.25f; // value written by AsmWriter
static float applied_dynamic_sensitivity_value = 1.0f; // value written by AsmWriter

bool set_direct_input_enabled(bool enabled)
static bool set_direct_input_enabled(bool enabled)
{
auto direct_input_initialized = addr_as_ref<bool>(0x01885460);
auto mouse_di_init = addr_as_ref<int()>(0x0051E070);
Expand All @@ -40,25 +46,81 @@ bool set_direct_input_enabled(bool enabled)
return true;
}

void set_input_mode(int mode)
{
const int old_mode = g_alpine_game_config.input_mode;
g_alpine_game_config.input_mode = mode;

Comment on lines +88 to +94
if (!rf::is_dedicated_server) {
// Handle SDL relative mouse mode
if (g_sdl_window) {
SDL_SetWindowRelativeMouseMode(g_sdl_window, mode == 2 && rf::keep_mouse_centered);
}

// 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 deltas 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;
}
Comment on lines +88 to +116
}

FunHook<void()> mouse_eval_deltas_hook{
0x0051DC70,
[]() {
// 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) {
return;
}

if (g_alpine_game_config.input_mode == 2) {
// SDL mode: accumulate SDL motion events into integer deltas for this frame
g_sdl_mouse_dx = static_cast<int>(g_sdl_mouse_dx_rem);
g_sdl_mouse_dy = static_cast<int>(g_sdl_mouse_dy_rem);
g_sdl_mouse_dx_rem -= g_sdl_mouse_dx;
g_sdl_mouse_dy_rem -= g_sdl_mouse_dy;

if (rf::keep_mouse_centered) {
rf::mouse_old_z = rf::mouse_wheel_pos; // keep scroll delta tracking consistent
}
}

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.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_PumpEvents();
SDL_FlushEvents(SDL_EVENT_MOUSE_MOTION, SDL_EVENT_MOUSE_MOTION);
}
},
};

// Handles scroll-wheel delta fix and Win32 cursor centering for stock/DInput modes (0 and 1).
// In SDL mode (2) this hook fires but we skip its extra work — SDL manages it instead.
FunHook<void()> mouse_eval_deltas_di_hook{
0x0051DEB0,
[]() {
mouse_eval_deltas_di_hook.call_target();
if (g_alpine_game_config.input_mode == 2)
return; // SDL mode handles its own cursor management and scroll tracking

// Fix invalid mouse scroll delta, when DirectInput is turned off.
// Fix invalid mouse scroll delta when DirectInput is off (mode 0)
rf::mouse_old_z = rf::mouse_wheel_pos;

// center cursor if in game
// 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);
Expand All @@ -70,40 +132,72 @@ FunHook<void()> mouse_eval_deltas_di_hook{
FunHook<void()> mouse_keep_centered_enable_hook{
0x0051E690,
[]() {
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) {
switch (g_alpine_game_config.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();
},
};

FunHook<void()> mouse_keep_centered_disable_hook{
0x0051E6A0,
[]() {
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.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;
}
}
mouse_keep_centered_disable_hook.call_target();
},
};

FunHook<void(int&, int&, int&)> mouse_get_delta_hook{
0x0051E630,
[](int& dx, int& dy, int& dz) {
mouse_get_delta_hook.call_target(dx, dy, dz); // fills dz (scroll wheel)
if (g_alpine_game_config.input_mode == 2) {
// SDL mode: override dx/dy with SDL-sourced deltas
dx = g_sdl_mouse_dx;
dy = g_sdl_mouse_dy;
g_sdl_mouse_dx = 0;
g_sdl_mouse_dy = 0;
}
},
};

ConsoleCommand2 input_mode_cmd{
"inputmode",
[]() {
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");
}
static constexpr const char* mode_names[] = {"stock", "DirectInput", "SDL"};
int new_mode = (g_alpine_game_config.input_mode + 1) % 3;
set_input_mode(new_mode);
rf::console::print("Input mode: {} ({})", new_mode, mode_names[new_mode]);
},
"Toggles input mode",
"Cycles input mode: 0=stock Win32 mouse+keyboard, 1=DirectInput mouse+stock keyboard, 2=SDL mouse+keyboard",
};

ConsoleCommand2 ms_cmd{
Expand Down Expand Up @@ -351,6 +445,40 @@ ConsoleCommand2 linear_pitch_cmd{
"Toggles mouse linear pitch angle",
};

void mouse_sdl_poll()
{
if (!g_sdl_window) return;

SDL_Event events[16];
int n;
while ((n = SDL_PeepEvents(events, static_cast<int>(std::size(events)),
SDL_GETEVENT, SDL_EVENT_MOUSE_MOTION,
SDL_EVENT_MOUSE_REMOVED)) > 0) {
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mouse_sdl_poll() uses std::size(events) but the file doesn't include the standard header that provides std::size for arrays ( in C++17+, or ). Add the correct include explicitly to avoid non-portable reliance on transitive includes.

Copilot uses AI. Check for mistakes.
for (int i = 0; i < n; ++i) {
const SDL_Event& ev = events[i];
if (ev.type == SDL_EVENT_MOUSE_MOTION && g_alpine_game_config.input_mode == 2) {
// SDL mode only: accumulate raw motion for this frame
g_sdl_mouse_dx_rem += ev.motion.xrel;
g_sdl_mouse_dy_rem += ev.motion.yrel;
}
// Always drain events to prevent SDL queue buildup in non-SDL modes
}
}
}

void mouse_init_sdl_window()
{
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;
}
SDL_StartTextInput(g_sdl_window);
}

void mouse_apply_patch()
{
// Handle zoom sens customization
Expand All @@ -364,17 +492,17 @@ 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 stock/DInput modes (0 and 1)
mouse_eval_deltas_di_hook.install();

// Mouse mode hooks (DInput or SDL depending on input_mode)
mouse_keep_centered_enable_hook.install();
mouse_keep_centered_disable_hook.install();
mouse_get_delta_hook.install();

// 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<u8>(0x0051E14B + 1, 5); // DISCL_EXCLUSIVE|DISCL_FOREGROUND

// Linear vertical rotation (pitch)
linear_pitch_patch.install();

Expand All @@ -385,4 +513,4 @@ void mouse_apply_patch()
scope_sens_cmd.register_cmd();
scanner_sens_cmd.register_cmd();
linear_pitch_cmd.register_cmd();
}
}
1 change: 1 addition & 0 deletions game_patch/main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ CallHook<void()> rf_init_hook{
xlog::info("Initializing game...");
initialize_alpine_core_config();
rf_init_hook.call_target();
mouse_init_sdl_window();
vpackfile_disable_overriding();
xlog::info("Game initialized ({} ms).", GetTickCount64() - start_ticks);
},
Expand Down
16 changes: 13 additions & 3 deletions game_patch/misc/alpine_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -954,8 +954,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("InputMode")) {
g_alpine_game_config.input_mode = std::stoi(settings["InputMode"]);
processed_keys.insert("InputMode");
} else if (settings.count("SDLMouse")) {
// Legacy: SDLMouse=0 was "non-SDL" (closest = DInput mode 1), SDLMouse=1 was SDL (mode 2)
int sdl_mouse = std::stoi(settings["SDLMouse"]);
g_alpine_game_config.input_mode = (sdl_mouse != 0) ? 2 : 1;
processed_keys.insert("SDLMouse");
} else if (settings.count("DirectInput")) {
// Legacy: DirectInput=0 was stock/Win32 (mode 0), DirectInput=1 was DInput (mode 1)
int direct_input = std::stoi(settings["DirectInput"]);
g_alpine_game_config.input_mode = (direct_input != 0) ? 1 : 0;
processed_keys.insert("DirectInput");
Comment on lines +994 to 1006
}
if (settings.count("MouseLinearPitch")) {
Expand Down Expand Up @@ -1057,7 +1067,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 << "InputMode=" << g_alpine_game_config.input_mode << "\n";
file << "MouseLinearPitch=" << g_alpine_game_config.mouse_linear_pitch << "\n";
file << "SwapARBinds=" << g_alpine_game_config.swap_ar_controls << "\n";
file << "SwapGNBinds=" << g_alpine_game_config.swap_gn_controls << "\n";
Expand Down
2 changes: 1 addition & 1 deletion game_patch/misc/alpine_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,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 input_mode = 2; // 0=stock Win32 mouse+keyboard, 1=DirectInput mouse+stock keyboard, 2=SDL mouse+keyboard
bool scoreboard_anim = true;
bool legacy_bob = false;
bool scoreboard_split_simple = true;
Expand Down
Loading
Loading