diff --git a/CMakeLists.txt b/CMakeLists.txt index 60cfd07..f98812f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(sacred_re SHARED src/mods/widescreen.cpp src/mods/hud.cpp src/mods/wasd.cpp + src/mods/skillkeys.cpp ) target_include_directories(sacred_re PRIVATE src) target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS}) diff --git a/src/mods/skillkeys.cpp b/src/mods/skillkeys.cpp new file mode 100644 index 0000000..f9a8b6c --- /dev/null +++ b/src/mods/skillkeys.cpp @@ -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 +#include +#include + +#include + +#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(slot); + } + return slot < 4 ? '6' + static_cast(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 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(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(taskbar); + if (slot >= *reinterpret_cast(bar + kTaskbarSlotCount) || + !reinterpret_cast(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 diff --git a/src/mods/skillkeys.h b/src/mods/skillkeys.h new file mode 100644 index 0000000..d3da257 --- /dev/null +++ b/src/mods/skillkeys.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +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 diff --git a/src/mods/wasd.cpp b/src/mods/wasd.cpp index 766f105..d1cac30 100644 --- a/src/mods/wasd.cpp +++ b/src/mods/wasd.cpp @@ -32,6 +32,7 @@ #include +#include "mods/skillkeys.h" #include "re/config.h" #include "re/hook.h" #include "re/log.h" @@ -135,9 +136,11 @@ bool IsMovementKey(int upper) { 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) { - if (key >= 0x80) { + key = mods::skillkeys::RemapKey(key); + if (key >= 0x80 || !Enabled()) { return key; } const int upper = toupper(key); @@ -159,7 +162,7 @@ thread_local int g_keyEventDepth = 0; class KeyEventRemap { public: explicit KeyEventRemap(void* event) { - if (g_keyEventDepth++ > 0 || !Enabled() || !event || + if (g_keyEventDepth++ > 0 || !event || *static_cast(event) != kEventKeyVtable) { return; }