diff --git a/CMakeLists.txt b/CMakeLists.txt index f98812f..66ed072 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/docs/todo.md b/docs/todo.md index 6a5a36c..836768c 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -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 diff --git a/docs/wasd.md b/docs/wasd.md index d6bf12e..03632b6 100644 --- a/docs/wasd.md +++ b/docs/wasd.md @@ -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 diff --git a/src/mods/borderless.cpp b/src/mods/borderless.cpp index 3603a8e..9eade17 100644 --- a/src/mods/borderless.cpp +++ b/src/mods/borderless.cpp @@ -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); } diff --git a/src/mods/input.cpp b/src/mods/input.cpp new file mode 100644 index 0000000..f0bd272 --- /dev/null +++ b/src/mods/input.cpp @@ -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 +#include +#include +#include + +#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(0x00849701); +const auto KernelInstance = reinterpret_cast(0x00808490); +const auto PostEvent = reinterpret_cast(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(pressed ? tolower(key) : toupper(key)); + event.raw = code; + event.key = code; + event.modifiers = *reinterpret_cast(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('1' + set); } +char SkillSlotDigit(int slot) { return slot < 4 ? static_cast('6' + slot) : '0'; } + +int ParseKey(const std::string& text) { + if (text.size() == 1 && isalnum(static_cast(text[0]))) { + return toupper(static_cast(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(kGameSlots)) { + mods::skillkeys::CastOnSelect(static_cast(binding.index), vk); + PostGameKey(SkillSlotDigit(binding.index), true); + } + break; + case Kind::WeaponSet: + if (binding.index < static_cast(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(kGameSlots)) { + PostGameKey(SkillSlotDigit(binding.index), false); + } + break; + case Kind::WeaponSet: + if (binding.index < static_cast(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(kUiManager); + if (!ui) { + return false; + } + void* focus = *reinterpret_cast(ui + 0x148); + if (!focus) { + return false; + } + auto* edit = static_cast(DynamicCast(focus, 0, kTypeControl, kTypeEditControl, 0)); + return edit && ((*reinterpret_cast(edit + 0x10) >> 16) & 1) == 0; +} + +bool HandleWindowMessage(HWND, UINT message, WPARAM wParam, LPARAM lParam) { + switch (message) { + case WM_KEYDOWN: { + const int vk = static_cast(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(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 diff --git a/src/mods/input.h b/src/mods/input.h new file mode 100644 index 0000000..10e494e --- /dev/null +++ b/src/mods/input.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +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 diff --git a/src/mods/skillkeys.cpp b/src/mods/skillkeys.cpp index f9a8b6c..61eb707 100644 --- a/src/mods/skillkeys.cpp +++ b/src/mods/skillkeys.cpp @@ -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 #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 { @@ -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(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). +// 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 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(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(taskbar); - if (slot >= *reinterpret_cast(bar + kTaskbarSlotCount) || + if (slot >= kSlots || 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); + 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) { diff --git a/src/mods/skillkeys.h b/src/mods/skillkeys.h index d3da257..e36c135 100644 --- a/src/mods/skillkeys.h +++ b/src/mods/skillkeys.h @@ -1,11 +1,10 @@ #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); +// 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 diff --git a/src/mods/wasd.cpp b/src/mods/wasd.cpp index d1cac30..879216d 100644 --- a/src/mods/wasd.cpp +++ b/src/mods/wasd.cpp @@ -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 #include #include -#include -#include #include -#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(kUiManager); - if (!ui) { - return false; - } - void* focus = *reinterpret_cast(ui + 0x148); - if (!focus) { - return false; - } - auto* edit = static_cast(DynamicCast(focus, 0, kTypeControl, kTypeEditControl, 0)); - return edit && ((*reinterpret_cast(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(value[0]))) { - return static_cast(toupper(static_cast(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(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(event) != kEventKeyVtable) { - return; - } - auto* bytes = static_cast(event); - const uint16_t modifiers = *reinterpret_cast(bytes + kKeyModifiers); - if ((modifiers & kModifiersCtrlAlt) != 0 || TypingIntoTextField()) { - return; - } - key_ = reinterpret_cast(bytes + kKey); - raw_ = reinterpret_cast(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(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(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(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 - diff --git a/src/mods/wasd.h b/src/mods/wasd.h new file mode 100644 index 0000000..d663921 --- /dev/null +++ b/src/mods/wasd.h @@ -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 diff --git a/tools/names.txt b/tools/names.txt index 7a8e701..bb7fc01 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -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)