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:
1 parent
9fb6c6b406
commit
cec2556fa3
11 files changed
+409
-279
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user