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

+1
View File
@@ -34,6 +34,7 @@ add_library(sacred_re SHARED
src/mods/widescreen.cpp src/mods/widescreen.cpp
src/mods/hud.cpp src/mods/hud.cpp
src/mods/wasd.cpp src/mods/wasd.cpp
src/mods/skillkeys.cpp
) )
target_include_directories(sacred_re PRIVATE src) target_include_directories(sacred_re PRIVATE src)
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS}) target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
+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
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <cstdint>
namespace mods::skillkeys {
// The key the game should see for `key` from a key event: with SwapDigits,
// 1-5 and 6-0 trade places (skills on 1-5, weapon sets on 6-0).
uint16_t RemapKey(uint16_t key);
} // namespace mods::skillkeys
+6 -3
View File
@@ -32,6 +32,7 @@
#include <windows.h> #include <windows.h>
#include "mods/skillkeys.h"
#include "re/config.h" #include "re/config.h"
#include "re/hook.h" #include "re/hook.h"
#include "re/log.h" #include "re/log.h"
@@ -135,9 +136,11 @@ bool IsMovementKey(int upper) {
return upper == 'W' || upper == 'A' || upper == 'S' || upper == 'D'; return upper == 'W' || upper == 'A' || upper == 'S' || upper == 'D';
} }
// Returns the key the game should see instead of `key` (0: none). // Returns the key the game should see instead of `key` (0: none). Also
// applies the skill hotkeys' digit swap (src/mods/skillkeys.cpp).
uint16_t RemapKey(uint16_t key) { uint16_t RemapKey(uint16_t key) {
if (key >= 0x80) { key = mods::skillkeys::RemapKey(key);
if (key >= 0x80 || !Enabled()) {
return key; return key;
} }
const int upper = toupper(key); const int upper = toupper(key);
@@ -159,7 +162,7 @@ thread_local int g_keyEventDepth = 0;
class KeyEventRemap { class KeyEventRemap {
public: public:
explicit KeyEventRemap(void* event) { explicit KeyEventRemap(void* event) {
if (g_keyEventDepth++ > 0 || !Enabled() || !event || if (g_keyEventDepth++ > 0 || !event ||
*static_cast<uint32_t*>(event) != kEventKeyVtable) { *static_cast<uint32_t*>(event) != kEventKeyVtable) {
return; return;
} }