Skill hotkeys cast the skill; skills on 1-5, weapon sets on 6-0

A digit that selects a combat-art slot now also presses the right mouse
button (posted WM_RBUTTONDOWN, released with the key), so the game casts
the art at the cursor like a right-click. SwapDigits trades the digit
rows; the key rewrite shares the WASD remap in the engine/UI receivers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-14 23:48:11 +02:00
1 parent b32b39f7cb
commit aa59680fb1
4 files changed
+165 -3

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
// Skill hotkeys 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
#include <atomic>
#include <cstdint>
#include <thread>
#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 {
// cUI_Taskbar2 fields.
constexpr size_t kTaskbarSlotCount = 0x15c; // uint16: combat-art slots the hero has
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).
HWND GameWindow() {
HWND window = GetForegroundWindow();
DWORD process = 0;
if (!window || !GetWindowThreadProcessId(window, &process) || process != GetCurrentProcessId()) {
return nullptr;
}
return window;
}
LPARAM CursorClientPos(HWND window) {
POINT cursor = {};
GetCursorPos(&cursor);
ScreenToClient(window, &cursor);
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);
} // 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']);
}
// After a slot was selected: cast if its digit is what selected it (the slot
// can also be clicked with the mouse).
void OnSlotSelected(void* taskbar, unsigned slot) {
if (!Enabled() || slot >= kSlots) {
return;
}
const auto* bar = static_cast<const uint8_t*>(taskbar);
if (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);
}
}
} // namespace mods::skillkeys
namespace game {
// cUI_Taskbar2::SelectSkillSlot (0x6e17d0): makes combat-art slot `slot` the
// right mouse button's art (digits 6-0, 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) {
UiTaskbar_SelectSkillSlot_original(self, edx, slot, notify);
if (self) {
mods::skillkeys::OnSlotSelected(self, slot & 0xffff);
}
}
} // namespace game