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

+1
View File
@@ -35,6 +35,7 @@ add_library(sacred_re SHARED
src/mods/hud.cpp
src/mods/wasd.cpp
src/mods/skillkeys.cpp
src/mods/input.cpp
)
target_include_directories(sacred_re PRIVATE src)
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
+4 -4
View File
@@ -33,11 +33,11 @@ W, A and S were game hotkeys. With WASD movement (`src/mods/wasd.cpp`) their
actions moved to other keys for now; choose final bindings (and update the
in-game texts/tooltips that name the old keys):
| Key | Action | Temporary key (`[wasd]` in sacred_re.ini) |
| Key | Action | Temporary key (sacred_re.ini) |
|---|---|---|
| W | action slot 3 (`cEngine::UseActionSlot`, like Q/E/R/B/Space) | G (`ActionKeyW`) |
| A | pick up items nearby (creature event 0x113) | V (`ActionKeyA`) |
| S | save game window (`UI_WND_SAVEGAME`) | K (`ActionKeyS`) |
| W | action slot 3 (`cEngine::UseActionSlot`, like Q/E/R/B/Space) | G (`[keys] ActionSlot3`) |
| A | pick up items nearby (creature event 0x113) | V (`[keys] PickUp`) |
| S | save game window (`UI_WND_SAVEGAME`) | K (`[keys] SaveGame`) |
| D | unbound | - |
Globally bound letters (engine `0x618130`, UI manager `0x757120`): A B C E F
+7 -3
View File
@@ -49,9 +49,13 @@ badly; targets near the stepwise-updated position make it arrive and stop.
Keys reach the game as `cEvent_key` (category 4; key at +6/+8, modifiers at
+0xa) through the kernel queue to `cEngine::ReceiveEvent` (`0x618130`), which
offers them to `cUI_Manager::ReceiveEvent` (`0x757120`) first. Both compare
`toupper(key)` (`0x84aa86`). The mod swallows W/A/S/D there (auto-repeat events
would trigger the hotkeys) and rewrites the replacement keys into the old
letters; the temporary bindings are listed in `docs/todo.md`.
`toupper(key)` (`0x84aa86`). Presses come from `WM_CHAR` (the character), releases
from `WM_KEYUP` (the virtual key), so keys without a character (F-keys) never
reach the hotkeys. The key bindings (`src/mods/input.cpp`) handle the raw
`WM_KEYDOWN`/`WM_KEYUP` instead: W/A/S/D and the digit row are kept from the
game, bound keys post the original hotkey's key event (`Kernel_Instance`
`0x808490`, `cKernel::PostEvent` `0x808930`); the temporary bindings for the
old W/A/S actions are listed in `docs/todo.md`.
Hotkeys found: engine A (pick up nearby), P (pause), Q/W/E/R/B/Space
(`cEngine::UseActionSlot`), ^ (console), Esc, Ctrl+B/F/X/Z (Z quicksave); UI
+4
View File
@@ -34,6 +34,7 @@
#include "game/engine/input.h"
#include "game/render/dx_driver7.h"
#include "mods/hud.h"
#include "mods/input.h"
#include "mods/widescreen.h"
#include "re/config.h"
#include "game/addresses.h"
@@ -215,6 +216,9 @@ void __cdecl GetMouseClientPos(HWND window, POINT* position) {
}
LRESULT CALLBACK MainWindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) {
if (mods::input::HandleWindowMessage(window, message, wParam, lParam)) {
return 0;
}
if (!g_viewport.active || window != g_viewport.window) {
return MainWindowProc_original(window, message, wParam, lParam);
}
+294
View File
@@ -0,0 +1,294 @@
// Key bindings.
//
// The game only sees key presses as characters (WM_CHAR -> cEvent_key), so it
// cannot see keys without one (F-keys), and its hotkeys are hardcoded in
// several event receivers (engine 0x618130, UI manager 0x757120, taskbar
// 0x6e1970). This mod reads the raw key messages instead (virtual keys,
// independent of the keyboard layout): a bound key's messages are swallowed
// and its action runs, usually by posting the key event of the original
// hotkey to the game's event kernel, the way the window procedure does (the
// kernel dispatches it right away, on the window thread).
//
// While a text field has the keyboard, or Ctrl / Alt is held, keys pass
// through unchanged.
//
// sacred_re.ini (key names: A-Z, 0-9, F1-F24, Space, None):
// [keys]
// SkillSlot1=1 ... SkillSlot7=7 ; select and cast combat-art slot
// WeaponSet1=F1 ... WeaponSet7=F7 ; select weapon set
// ActionSlot3=G ; what W did (W is movement)
// PickUp=V ; what A did (pick up items nearby)
// SaveGame=K ; what S did (save game window)
#include "mods/input.h"
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <string>
#include "mods/skillkeys.h"
#include "mods/wasd.h"
#include "re/config.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::input {
namespace {
// --- sacred.exe ---------------------------------------------------------------
constexpr uint32_t kUiManager = 0x017ecb3c; // cUI_Manager*: focused control at +0x148
constexpr uint32_t kKeyModifiers = 0x0182cdec; // u16, as the in-game window procedure keeps it
constexpr uint32_t kEventKeyVtable = 0x00895568;
constexpr uint32_t kTypeControl = 0x009de168;
constexpr uint32_t kTypeEditControl = 0x009e3608;
using DynamicCastFn = void*(__cdecl*)(void* object, int vfDelta, uint32_t source, uint32_t target, int isReference);
using KernelInstanceFn = void*(__cdecl*)();
using PostEventFn = void(__fastcall*)(void* kernel, void* edx, void* event, int a, int b);
const auto DynamicCast = reinterpret_cast<DynamicCastFn>(0x00849701);
const auto KernelInstance = reinterpret_cast<KernelInstanceFn>(0x00808490);
const auto PostEvent = reinterpret_cast<PostEventFn>(0x00808930); // copies the event
// cEvent_key: +4 1 = pressed, 0 = released; +6 / +8 the key (character when
// pressed, virtual key when released); +0xa modifiers.
struct KeyEvent {
uint32_t vtable;
uint16_t pressed;
uint16_t raw;
uint16_t key;
uint16_t modifiers;
};
static_assert(sizeof(KeyEvent) == 0xc);
// Posts the key event the window procedure would post for `key` (an original
// hotkey letter or digit).
void PostGameKey(char key, bool pressed) {
KeyEvent event = {};
event.vtable = kEventKeyVtable;
event.pressed = pressed ? 1 : 0;
// Pressed: the character (lower case, as without Shift); released: the
// virtual key (upper case).
const auto code = static_cast<uint16_t>(pressed ? tolower(key) : toupper(key));
event.raw = code;
event.key = code;
event.modifiers = *reinterpret_cast<uint16_t*>(kKeyModifiers);
PostEvent(KernelInstance(), nullptr, &event, 1, 0);
}
// --- bindings ------------------------------------------------------------------
enum class Kind { SkillSlot, WeaponSet, GameKey };
struct Binding {
const char* name;
const char* fallback;
Kind kind;
int index; // slot / set (0-based)
char gameKey; // GameKey: the original hotkey
int vk = 0;
};
Binding g_bindings[] = {
{"SkillSlot1", "1", Kind::SkillSlot, 0, 0},
{"SkillSlot2", "2", Kind::SkillSlot, 1, 0},
{"SkillSlot3", "3", Kind::SkillSlot, 2, 0},
{"SkillSlot4", "4", Kind::SkillSlot, 3, 0},
{"SkillSlot5", "5", Kind::SkillSlot, 4, 0},
{"SkillSlot6", "6", Kind::SkillSlot, 5, 0},
{"SkillSlot7", "7", Kind::SkillSlot, 6, 0},
{"WeaponSet1", "F1", Kind::WeaponSet, 0, 0},
{"WeaponSet2", "F2", Kind::WeaponSet, 1, 0},
{"WeaponSet3", "F3", Kind::WeaponSet, 2, 0},
{"WeaponSet4", "F4", Kind::WeaponSet, 3, 0},
{"WeaponSet5", "F5", Kind::WeaponSet, 4, 0},
{"WeaponSet6", "F6", Kind::WeaponSet, 5, 0},
{"WeaponSet7", "F7", Kind::WeaponSet, 6, 0},
{"ActionSlot3", "G", Kind::GameKey, 0, 'W'},
{"PickUp", "V", Kind::GameKey, 0, 'A'},
{"SaveGame", "K", Kind::GameKey, 0, 'S'},
};
// Taskbar digits: weapon sets 1-5 on '1'-'5', combat-art slots 1-5 on
// '6'-'9', '0'.
constexpr unsigned kGameSlots = 5;
char WeaponSetDigit(int set) { return static_cast<char>('1' + set); }
char SkillSlotDigit(int slot) { return slot < 4 ? static_cast<char>('6' + slot) : '0'; }
int ParseKey(const std::string& text) {
if (text.size() == 1 && isalnum(static_cast<unsigned char>(text[0]))) {
return toupper(static_cast<unsigned char>(text[0])); // VK of A-Z / 0-9
}
if ((text[0] == 'F' || text[0] == 'f') && text.size() <= 3) {
const int n = atoi(text.c_str() + 1);
if (n >= 1 && n <= 24) {
return VK_F1 + n - 1;
}
}
if (_stricmp(text.c_str(), "Space") == 0) {
return VK_SPACE;
}
return 0; // None / unknown
}
void LoadBindings() {
for (Binding& binding : g_bindings) {
const std::string text = re::ConfigString("keys", binding.name, binding.fallback);
binding.vk = text.empty() ? 0 : ParseKey(text);
if (binding.vk == 0 && !text.empty() && _stricmp(text.c_str(), "None") != 0) {
re::Log("input: [keys] %s=%s: unknown key name", binding.name, text.c_str());
}
}
}
const Binding* BindingFor(int vk) {
for (const Binding& binding : g_bindings) {
if (binding.vk == vk) {
return &binding;
}
}
return nullptr;
}
// Keys taken away from the game although nothing is bound to them: the digit
// row (the game's slot keys) and the movement keys.
bool Reserved(int vk) {
return (vk >= '0' && vk <= '9') || mods::wasd::IsMovementKey(vk);
}
void Press(const Binding& binding, int vk) {
switch (binding.kind) {
case Kind::SkillSlot:
if (binding.index < static_cast<int>(kGameSlots)) {
mods::skillkeys::CastOnSelect(static_cast<unsigned>(binding.index), vk);
PostGameKey(SkillSlotDigit(binding.index), true);
}
break;
case Kind::WeaponSet:
if (binding.index < static_cast<int>(kGameSlots)) {
PostGameKey(WeaponSetDigit(binding.index), true);
}
break;
case Kind::GameKey:
PostGameKey(binding.gameKey, true);
break;
}
}
void Release(const Binding& binding, int vk) {
switch (binding.kind) {
case Kind::SkillSlot:
mods::skillkeys::KeyReleased(vk);
if (binding.index < static_cast<int>(kGameSlots)) {
PostGameKey(SkillSlotDigit(binding.index), false);
}
break;
case Kind::WeaponSet:
if (binding.index < static_cast<int>(kGameSlots)) {
PostGameKey(WeaponSetDigit(binding.index), false);
}
break;
case Kind::GameKey:
PostGameKey(binding.gameKey, false);
break;
}
}
bool ModifierHeld() {
return (GetKeyState(VK_CONTROL) & 0x8000) != 0 || (GetKeyState(VK_MENU) & 0x8000) != 0;
}
// Virtual keys whose WM_KEYDOWN was taken: their WM_CHAR / WM_KEYUP are
// swallowed too, even if the state changed in between.
bool g_taken[256] = {};
// The character of the last taken key, to swallow the WM_CHAR TranslateMessage
// makes of it.
bool g_swallowChar = false;
void Startup() {
LoadBindings();
re::Log("input: key bindings active");
}
RE_STARTUP(InputStartup, Startup);
} // namespace
bool TypingIntoTextField() {
auto* ui = *reinterpret_cast<uint8_t**>(kUiManager);
if (!ui) {
return false;
}
void* focus = *reinterpret_cast<void**>(ui + 0x148);
if (!focus) {
return false;
}
auto* edit = static_cast<uint8_t*>(DynamicCast(focus, 0, kTypeControl, kTypeEditControl, 0));
return edit && ((*reinterpret_cast<uint32_t*>(edit + 0x10) >> 16) & 1) == 0;
}
bool HandleWindowMessage(HWND, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_KEYDOWN: {
const int vk = static_cast<int>(wParam & 0xff);
const bool repeat = (lParam & (1 << 30)) != 0;
if (!repeat) {
g_taken[vk] = false;
if (ModifierHeld() || TypingIntoTextField()) {
return false;
}
const Binding* binding = BindingFor(vk);
if (!binding && !Reserved(vk)) {
return false;
}
g_taken[vk] = true;
if (binding) {
Press(*binding, vk);
}
}
if (!g_taken[vk]) {
return false;
}
g_swallowChar = true;
return true;
}
case WM_CHAR:
if (g_swallowChar) {
g_swallowChar = false;
return true;
}
return false;
case WM_KEYUP: {
g_swallowChar = false;
const int vk = static_cast<int>(wParam & 0xff);
if (!g_taken[vk]) {
return false;
}
g_taken[vk] = false;
if (const Binding* binding = BindingFor(vk)) {
Release(*binding, vk);
}
return true;
}
}
return false;
}
} // namespace mods::input
namespace game {
// The game switches its window between two procedures (SetWindowLong at
// 0x811fbc / 0x81326f): 0x8110d0, active in-game, translates keys itself (dead
// keys, modifiers at 0x182cdec); 0x8122a0 (hooked by the borderless mod, which
// calls HandleWindowMessage) is the other one.
LRESULT CALLBACK GameWindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam);
RE_HOOK(0x008110d0, GameWindowProc);
LRESULT CALLBACK GameWindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) {
if (mods::input::HandleWindowMessage(window, message, wParam, lParam)) {
return 0;
}
return GameWindowProc_original(window, message, wParam, lParam);
}
} // namespace game
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <windows.h>
namespace mods::input {
// Whether a text field has the keyboard (chat, console, save names).
bool TypingIntoTextField();
// Called first by the main window procedure. Returns true if the message was
// handled and must not reach the game.
bool HandleWindowMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam);
} // namespace mods::input
+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) {
+4 -5
View File
@@ -1,11 +1,10 @@
#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);
// The skill key `vk` was pressed to select combat-art slot `slot`: when the
// game selects it, press the right mouse button until KeyReleased(vk).
void CastOnSelect(unsigned slot, int vk);
void KeyReleased(int vk);
} // namespace mods::skillkeys
+14 -182
View File
@@ -9,47 +9,28 @@
// it when sent on their own.) Releasing the keys sends the target-less walk
// order a released mouse button sends, which stops it in place.
//
// W, A and S are game hotkeys: W = action slot 3 (like Q/E/R/B/Space,
// 0x613040), A = pick up items nearby, S = save game window. Key events are
// filtered before the engine and UI see them: W/A/S/D are swallowed (key
// repeat would otherwise reach the hotkeys), and replacement keys are rewritten
// into the old letters (temporary choices, see docs/todo.md). Typing into a
// text field (chat, console, save names) and Ctrl/Alt combinations are left
// alone. See docs/wasd.md.
//
// sacred_re.ini:
// [wasd]
// Enabled=1
// ActionKeyW=G ; key that now does what W did
// ActionKeyA=V ; ... A (pick up nearby)
// ActionKeyS=K ; ... S (save game window)
// W, A and S are game hotkeys; the key bindings (src/mods/input.cpp) keep
// W/A/S/D away from the game and move those actions to other keys. Typing into
// a text field (chat, console, save names) is left alone. See docs/wasd.md.
#include "mods/wasd.h"
#include <cctype>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string>
#include <windows.h>
#include "mods/skillkeys.h"
#include "re/config.h"
#include "mods/input.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::wasd {
namespace {
// --- sacred.exe ---------------------------------------------------------------
constexpr uint32_t kObjectManager = 0x00ad3bc0; // object manager**
constexpr uint32_t kUiManager = 0x017ecb3c; // cUI_Manager*: focused control at +0x148
constexpr uint32_t kEventKeyVtable = 0x00895568;
constexpr uint32_t kEventCreatureVtable = 0x0088e95c;
constexpr uint32_t kTypeObject = 0x008e9648; // RTTI type descriptors
constexpr uint32_t kTypeCreature = 0x008e9660;
constexpr uint32_t kTypeControl = 0x009de168;
constexpr uint32_t kTypeEditControl = 0x009e3608;
using GetObjectFn = void*(__fastcall*)(void* manager, void* edx, uint32_t id);
using DynamicCastFn = void*(__cdecl*)(void* object, int vfDelta, uint32_t source, uint32_t target, int isReference);
@@ -75,133 +56,14 @@ struct CreatureEvent {
};
static_assert(sizeof(CreatureEvent) == 0x44);
// cEvent_key: +4 1 = pressed / character, 0 = released; +6 and +8 the key
// (character for WM_CHAR, virtual key for WM_KEYUP); +0xa modifiers
// (0x3 Shift, 0xc Ctrl, 0x30 Alt).
constexpr size_t kKeyRaw = 0x6;
constexpr size_t kKey = 0x8;
constexpr size_t kKeyModifiers = 0xa;
constexpr uint16_t kModifiersCtrlAlt = 0x3c;
// Walk orders aim this far (world units) from the creature, about where the
// cursor would be.
constexpr double kTargetDistance = 300.0;
bool Enabled() {
static const bool enabled = re::ConfigInt("wasd", "Enabled", 1) != 0;
return enabled;
}
bool KeyDown(int key) {
return (GetAsyncKeyState(key) & 0x8000) != 0;
}
// Whether a text field has the keyboard (as the window procedure checks it).
bool TypingIntoTextField() {
auto* ui = *reinterpret_cast<uint8_t**>(kUiManager);
if (!ui) {
return false;
}
void* focus = *reinterpret_cast<void**>(ui + 0x148);
if (!focus) {
return false;
}
auto* edit = static_cast<uint8_t*>(DynamicCast(focus, 0, kTypeControl, kTypeEditControl, 0));
return edit && ((*reinterpret_cast<uint32_t*>(edit + 0x10) >> 16) & 1) == 0;
}
// --- hotkey remapping ----------------------------------------------------------
struct Remap {
char from; // replacement key the player presses
char to; // original hotkey letter
};
Remap g_remaps[3];
char ConfigKey(const char* key, char fallback) {
const std::string value = re::ConfigString("wasd", key, "");
if (value.size() == 1 && isalpha(static_cast<unsigned char>(value[0]))) {
return static_cast<char>(toupper(static_cast<unsigned char>(value[0])));
}
return fallback;
}
void LoadRemaps() {
g_remaps[0] = {ConfigKey("ActionKeyW", 'G'), 'W'};
g_remaps[1] = {ConfigKey("ActionKeyA", 'V'), 'A'};
g_remaps[2] = {ConfigKey("ActionKeyS", 'K'), 'S'};
}
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). Also
// applies the skill hotkeys' digit swap (src/mods/skillkeys.cpp).
uint16_t RemapKey(uint16_t key) {
key = mods::skillkeys::RemapKey(key);
if (key >= 0x80 || !Enabled()) {
return key;
}
const int upper = toupper(key);
const bool lower = upper != key;
if (IsMovementKey(upper)) {
return 0;
}
for (const Remap& remap : g_remaps) {
if (upper == remap.from) {
return static_cast<uint16_t>(lower ? tolower(remap.to) : remap.to);
}
}
return key;
}
thread_local int g_keyEventDepth = 0;
// RAII: rewrite a key event for the scope of the outermost receiver.
class KeyEventRemap {
public:
explicit KeyEventRemap(void* event) {
if (g_keyEventDepth++ > 0 || !event ||
*static_cast<uint32_t*>(event) != kEventKeyVtable) {
return;
}
auto* bytes = static_cast<uint8_t*>(event);
const uint16_t modifiers = *reinterpret_cast<uint16_t*>(bytes + kKeyModifiers);
if ((modifiers & kModifiersCtrlAlt) != 0 || TypingIntoTextField()) {
return;
}
key_ = reinterpret_cast<uint16_t*>(bytes + kKey);
raw_ = reinterpret_cast<uint16_t*>(bytes + kKeyRaw);
originalKey_ = *key_;
originalRaw_ = *raw_;
*key_ = RemapKey(originalKey_);
*raw_ = RemapKey(originalRaw_);
// W/A/S/D: the game must not see these at all (key repeat while held
// interrupts walking); movement polls the keys itself.
swallow_ = *key_ == 0;
}
bool Swallow() const { return swallow_; }
~KeyEventRemap() {
--g_keyEventDepth;
if (key_) {
*key_ = originalKey_;
*raw_ = originalRaw_;
}
}
KeyEventRemap(const KeyEventRemap&) = delete;
KeyEventRemap& operator=(const KeyEventRemap&) = delete;
private:
uint16_t* key_ = nullptr;
uint16_t* raw_ = nullptr;
uint16_t originalKey_ = 0;
uint16_t originalRaw_ = 0;
bool swallow_ = false;
};
// --- movement ------------------------------------------------------------------
void* ControlledCreature(uint8_t* engine) {
void* manager = *reinterpret_cast<void**>(kObjectManager);
if (!manager) {
@@ -235,6 +97,12 @@ struct MoveState {
};
MoveState g_move;
} // namespace
bool IsMovementKey(int vk) {
return (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D');
}
// Called once per engine tick (10 Hz), after the game's own input handling.
void Update(uint8_t* engine) {
// World axes on screen: (-1,-1) up, (1,1) down, (-1,1) left, (1,-1) right.
@@ -242,7 +110,7 @@ void Update(uint8_t* engine) {
int dy = 0;
const uint32_t flags = *reinterpret_cast<uint32_t*>(engine + kEngineFlags);
const bool mouseMoving = (flags & 0x10) != 0;
if (!mouseMoving && (flags & 0x4000) == 0 && !TypingIntoTextField()) {
if (!mouseMoving && (flags & 0x4000) == 0 && !mods::input::TypingIntoTextField()) {
if (KeyDown('W')) { dx -= 1; dy -= 1; }
if (KeyDown('S')) { dx += 1; dy += 1; }
if (KeyDown('A')) { dx -= 1; dy += 1; }
@@ -269,17 +137,6 @@ void Update(uint8_t* engine) {
g_move.moving = true;
}
void Startup() {
if (!Enabled()) {
return;
}
LoadRemaps();
re::Log("wasd: movement on W/A/S/D; old W/A/S actions on %c/%c/%c", g_remaps[0].from, g_remaps[1].from,
g_remaps[2].from);
}
RE_STARTUP(WasdStartup, Startup);
} // namespace
} // namespace mods::wasd
namespace game {
@@ -290,34 +147,9 @@ void __fastcall Engine_Tick(void* self, void* edx, void* event);
RE_HOOK(0x0060f1d0, Engine_Tick);
void __fastcall Engine_Tick(void* self, void* edx, void* event) {
Engine_Tick_original(self, edx, event);
if (mods::wasd::Enabled() && self) {
if (self) {
mods::wasd::Update(static_cast<uint8_t*>(self));
}
}
// cEngine::vf00 (0x618130), the engine's event receiver: hotkeys, after
// offering every event to the UI manager.
char __fastcall Engine_ReceiveEvent(void* self, void* edx, void* event);
RE_HOOK(0x00618130, Engine_ReceiveEvent);
char __fastcall Engine_ReceiveEvent(void* self, void* edx, void* event) {
mods::wasd::KeyEventRemap remap(event);
if (remap.Swallow()) {
return 1;
}
return Engine_ReceiveEvent_original(self, edx, event);
}
// cUI_Manager::vf00 (0x757120): UI hotkeys (S = save game window), also
// reached without the engine.
char __fastcall UiManager_ReceiveEvent(void* self, void* edx, void* event);
RE_HOOK(0x00757120, UiManager_ReceiveEvent);
char __fastcall UiManager_ReceiveEvent(void* self, void* edx, void* event) {
mods::wasd::KeyEventRemap remap(event);
if (remap.Swallow()) {
return 1;
}
return UiManager_ReceiveEvent_original(self, edx, event);
}
} // namespace game
+8
View File
@@ -0,0 +1,8 @@
#pragma once
namespace mods::wasd {
// Whether `vk` is one of the movement keys W/A/S/D.
bool IsMovementKey(int vk);
} // namespace mods::wasd
+1
View File
@@ -103,3 +103,4 @@
0x005de8a0 cInventoryEntry::FixInventory
0x005da880 cInventoryEntry::GetPotionRef
0x0057f750 SlotCountForLevel # table 0x95503c, max 5
0x008110d0 GameWindowProc # in-game window procedure: keys (dead keys, modifiers 0x182cdec)