Key bindings layer: skills on 1-7, weapon sets on F1-F7

Raw WM_KEYDOWN/WM_KEYUP are handled in both window procedures (the game
swaps 0x8110d0 and 0x8122a0 with SetWindowLong; 0x8110d0 is the in-game
one). Bound keys are swallowed and post the original hotkey's key event
to the kernel, so F-keys work and bindings are layout independent
([keys] in sacred_re.ini). Replaces the WASD hotkey remap and the digit
swap; W/A/S/D and the digit row no longer reach the game. Skill keys
still cast through the right mouse button. Enabled switches removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-15 00:11:55 +02:00
1 parent 9fb6c6b406
commit cec2556fa3
11 files changed
+409 -279

No files matched your search

+58 -85
View File
@@ -1,34 +1,22 @@
// Skill hotkeys cast the skill, not only select it.
// Skill keys cast the skill, not only select it.
//
// Digits 6-9 and 0 select one of the five combat-art slots on the taskbar
// (cUI_Taskbar2::HandleEvent -> cUI_Taskbar2::SelectSkillSlot 0x6e17d0); the
// selected art is then used with the right mouse button. With this mod a
// digit also presses the right mouse button: a WM_RBUTTONDOWN is posted to the
// game window, so the game handles it exactly like a real right-click at the
// cursor (UI hit tests, target under the cursor, cEvent_creature 0x105). The
// button is released when the digit is, so holding the digit is holding the
// button.
//
// SwapDigits trades 1-5 (weapon sets) and 6-0 (combat-art slots), so the
// skills sit on 1-5. Key events are rewritten where the WASD mod rewrites its
// keys (Engine/UI manager ReceiveEvent, src/mods/wasd.cpp).
//
// sacred_re.ini:
// [skillkeys]
// Enabled=1
// SwapDigits=1
// The taskbar's combat-art slots are selected with a key (cUI_Taskbar2::
// HandleEvent -> SelectSkillSlot 0x6e17d0); the selected art is then used with
// the right mouse button. When a skill key (src/mods/input.cpp) selects a slot,
// this mod also presses the right mouse button: a WM_RBUTTONDOWN is posted to
// the game window, so the game handles it exactly like a real right-click at
// the cursor (UI hit tests, target under the cursor, cEvent_creature 0x105).
// The button is released with the key, so holding the key is holding the
// button. Slots selected by clicking them with the mouse are not cast.
#include "mods/skillkeys.h"
#include <atomic>
#include <cstdint>
#include <thread>
#include <mutex>
#include <windows.h>
#include "mods/skillkeys.h"
#include "re/config.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::skillkeys {
namespace {
@@ -38,29 +26,7 @@ constexpr size_t kTaskbarSlotCount = 0x15c; // uint16: combat-art slots the her
constexpr size_t kTaskbarSkillSlots = 0x178; // 5 slot controls (null: none)
constexpr unsigned kSlots = 5;
bool Enabled() {
static const bool enabled = re::ConfigInt("skillkeys", "Enabled", 1) != 0;
return enabled;
}
bool SwapDigits() {
static const bool swap = re::ConfigInt("skillkeys", "SwapDigits", 1) != 0;
return swap;
}
// Digit the player presses to select a slot: 6 7 8 9 0, or 1-5 when swapped.
int SlotKey(unsigned slot) {
if (SwapDigits()) {
return '1' + static_cast<int>(slot);
}
return slot < 4 ? '6' + static_cast<int>(slot) : '0';
}
bool KeyDown(int key) {
return (GetAsyncKeyState(key) & 0x8000) != 0;
}
// The game window, if it has the focus (it does while a hotkey is pressed).
// The game window, if it has the focus (it does while a skill key is used).
HWND GameWindow() {
HWND window = GetForegroundWindow();
DWORD process = 0;
@@ -77,55 +43,62 @@ LPARAM CursorClientPos(HWND window) {
return MAKELPARAM(cursor.x, cursor.y);
}
std::atomic<bool> g_held{false};
// Presses the right mouse button and releases it once `key` is released.
void PressRightButtonWhileHeld(int key) {
HWND window = GameWindow();
if (!window || g_held.exchange(true)) {
return; // not focused, or already held (key repeat)
}
PostMessageW(window, WM_RBUTTONDOWN, MK_RBUTTON, CursorClientPos(window));
std::thread([window, key] {
while (KeyDown(key) && IsWindow(window)) {
Sleep(10);
}
PostMessageW(window, WM_RBUTTONUP, 0, CursorClientPos(window));
g_held = false;
}).detach();
}
void Startup() {
if (Enabled()) {
re::Log("skillkeys: digits %s cast the skill in their slot", SwapDigits() ? "1-5" : "6-0");
}
}
RE_STARTUP(SkillKeysStartup, Startup);
// The key that is to cast slot `slot` when it gets selected, then the key
// holding the right mouse button down. Set on the window thread, used on the
// thread that handles UI events.
struct State {
std::mutex lock;
int pendingKey = 0;
unsigned pendingSlot = 0;
bool keyDown = false;
int heldKey = 0; // right button is down for this key
HWND window = nullptr;
};
State g_state;
} // namespace
uint16_t RemapKey(uint16_t key) {
if (!Enabled() || !SwapDigits() || key < '0' || key > '9') {
return key;
}
static constexpr char kSwapped[] = "5678901234"; // indexed by key - '0'
return static_cast<uint16_t>(kSwapped[key - '0']);
void CastOnSelect(unsigned slot, int vk) {
std::lock_guard guard(g_state.lock);
g_state.pendingKey = vk;
g_state.pendingSlot = slot;
g_state.keyDown = true;
}
// After a slot was selected: cast if its digit is what selected it (the slot
// can also be clicked with the mouse).
void KeyReleased(int vk) {
std::lock_guard guard(g_state.lock);
if (g_state.pendingKey == vk) {
g_state.keyDown = false; // released before the slot was selected
}
if (g_state.heldKey == vk) {
g_state.heldKey = 0;
PostMessageW(g_state.window, WM_RBUTTONUP, 0, CursorClientPos(g_state.window));
}
}
// After a slot was selected: cast if a skill key asked for it.
void OnSlotSelected(void* taskbar, unsigned slot) {
if (!Enabled() || slot >= kSlots) {
std::lock_guard guard(g_state.lock);
if (!g_state.pendingKey || g_state.pendingSlot != slot) {
return;
}
const int key = g_state.pendingKey;
g_state.pendingKey = 0;
const auto* bar = static_cast<const uint8_t*>(taskbar);
if (slot >= *reinterpret_cast<const uint16_t*>(bar + kTaskbarSlotCount) ||
if (slot >= kSlots || slot >= *reinterpret_cast<const uint16_t*>(bar + kTaskbarSlotCount) ||
!reinterpret_cast<void* const*>(bar + kTaskbarSkillSlots)[slot]) {
return; // empty slot: nothing was selected
}
const int key = SlotKey(slot);
if (KeyDown(key)) {
PressRightButtonWhileHeld(key);
HWND window = GameWindow();
if (!window || g_state.heldKey) {
return;
}
PostMessageW(window, WM_RBUTTONDOWN, MK_RBUTTON, CursorClientPos(window));
if (g_state.keyDown) {
g_state.heldKey = key;
g_state.window = window;
} else {
PostMessageW(window, WM_RBUTTONUP, 0, CursorClientPos(window));
}
}
@@ -134,7 +107,7 @@ void OnSlotSelected(void* taskbar, unsigned slot) {
namespace game {
// cUI_Taskbar2::SelectSkillSlot (0x6e17d0): makes combat-art slot `slot` the
// right mouse button's art (digits 6-0, clicks on the slot).
// right mouse button's art (keys, clicks on the slot).
void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, unsigned slot, int notify);
RE_HOOK(0x006e17d0, UiTaskbar_SelectSkillSlot);
void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, unsigned slot, int notify) {