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
@@ -35,6 +35,7 @@ add_library(sacred_re SHARED
|
|||||||
src/mods/hud.cpp
|
src/mods/hud.cpp
|
||||||
src/mods/wasd.cpp
|
src/mods/wasd.cpp
|
||||||
src/mods/skillkeys.cpp
|
src/mods/skillkeys.cpp
|
||||||
|
src/mods/input.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})
|
||||||
|
|||||||
+4
-4
@@ -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
|
actions moved to other keys for now; choose final bindings (and update the
|
||||||
in-game texts/tooltips that name the old keys):
|
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`) |
|
| W | action slot 3 (`cEngine::UseActionSlot`, like Q/E/R/B/Space) | G (`[keys] ActionSlot3`) |
|
||||||
| A | pick up items nearby (creature event 0x113) | V (`ActionKeyA`) |
|
| A | pick up items nearby (creature event 0x113) | V (`[keys] PickUp`) |
|
||||||
| S | save game window (`UI_WND_SAVEGAME`) | K (`ActionKeyS`) |
|
| S | save game window (`UI_WND_SAVEGAME`) | K (`[keys] SaveGame`) |
|
||||||
| D | unbound | - |
|
| D | unbound | - |
|
||||||
|
|
||||||
Globally bound letters (engine `0x618130`, UI manager `0x757120`): A B C E F
|
Globally bound letters (engine `0x618130`, UI manager `0x757120`): A B C E F
|
||||||
|
|||||||
+7
-3
@@ -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
|
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
|
+0xa) through the kernel queue to `cEngine::ReceiveEvent` (`0x618130`), which
|
||||||
offers them to `cUI_Manager::ReceiveEvent` (`0x757120`) first. Both compare
|
offers them to `cUI_Manager::ReceiveEvent` (`0x757120`) first. Both compare
|
||||||
`toupper(key)` (`0x84aa86`). The mod swallows W/A/S/D there (auto-repeat events
|
`toupper(key)` (`0x84aa86`). Presses come from `WM_CHAR` (the character), releases
|
||||||
would trigger the hotkeys) and rewrites the replacement keys into the old
|
from `WM_KEYUP` (the virtual key), so keys without a character (F-keys) never
|
||||||
letters; the temporary bindings are listed in `docs/todo.md`.
|
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
|
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
|
(`cEngine::UseActionSlot`), ^ (console), Esc, Ctrl+B/F/X/Z (Z quicksave); UI
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
#include "game/engine/input.h"
|
#include "game/engine/input.h"
|
||||||
#include "game/render/dx_driver7.h"
|
#include "game/render/dx_driver7.h"
|
||||||
#include "mods/hud.h"
|
#include "mods/hud.h"
|
||||||
|
#include "mods/input.h"
|
||||||
#include "mods/widescreen.h"
|
#include "mods/widescreen.h"
|
||||||
#include "re/config.h"
|
#include "re/config.h"
|
||||||
#include "game/addresses.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) {
|
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) {
|
if (!g_viewport.active || window != g_viewport.window) {
|
||||||
return MainWindowProc_original(window, message, wParam, lParam);
|
return MainWindowProc_original(window, message, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
// The taskbar's combat-art slots are selected with a key (cUI_Taskbar2::
|
||||||
// (cUI_Taskbar2::HandleEvent -> cUI_Taskbar2::SelectSkillSlot 0x6e17d0); the
|
// HandleEvent -> SelectSkillSlot 0x6e17d0); the selected art is then used with
|
||||||
// selected art is then used with the right mouse button. With this mod a
|
// the right mouse button. When a skill key (src/mods/input.cpp) selects a slot,
|
||||||
// digit also presses the right mouse button: a WM_RBUTTONDOWN is posted to the
|
// this mod also presses the right mouse button: a WM_RBUTTONDOWN is posted to
|
||||||
// game window, so the game handles it exactly like a real right-click at the
|
// the game window, so the game handles it exactly like a real right-click at
|
||||||
// cursor (UI hit tests, target under the cursor, cEvent_creature 0x105). The
|
// the cursor (UI hit tests, target under the cursor, cEvent_creature 0x105).
|
||||||
// button is released when the digit is, so holding the digit is holding the
|
// The button is released with the key, so holding the key is holding the
|
||||||
// button.
|
// button. Slots selected by clicking them with the mouse are not cast.
|
||||||
//
|
|
||||||
// SwapDigits trades 1-5 (weapon sets) and 6-0 (combat-art slots), so the
|
#include "mods/skillkeys.h"
|
||||||
// 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 <cstdint>
|
||||||
#include <thread>
|
#include <mutex>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "mods/skillkeys.h"
|
|
||||||
#include "re/config.h"
|
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
#include "re/log.h"
|
|
||||||
#include "re/patch.h"
|
|
||||||
|
|
||||||
namespace mods::skillkeys {
|
namespace mods::skillkeys {
|
||||||
namespace {
|
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 size_t kTaskbarSkillSlots = 0x178; // 5 slot controls (null: none)
|
||||||
constexpr unsigned kSlots = 5;
|
constexpr unsigned kSlots = 5;
|
||||||
|
|
||||||
bool Enabled() {
|
// The game window, if it has the focus (it does while a skill key is used).
|
||||||
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 GameWindow() {
|
||||||
HWND window = GetForegroundWindow();
|
HWND window = GetForegroundWindow();
|
||||||
DWORD process = 0;
|
DWORD process = 0;
|
||||||
@@ -77,55 +43,62 @@ LPARAM CursorClientPos(HWND window) {
|
|||||||
return MAKELPARAM(cursor.x, cursor.y);
|
return MAKELPARAM(cursor.x, cursor.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::atomic<bool> g_held{false};
|
// 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
|
||||||
// Presses the right mouse button and releases it once `key` is released.
|
// thread that handles UI events.
|
||||||
void PressRightButtonWhileHeld(int key) {
|
struct State {
|
||||||
HWND window = GameWindow();
|
std::mutex lock;
|
||||||
if (!window || g_held.exchange(true)) {
|
int pendingKey = 0;
|
||||||
return; // not focused, or already held (key repeat)
|
unsigned pendingSlot = 0;
|
||||||
}
|
bool keyDown = false;
|
||||||
PostMessageW(window, WM_RBUTTONDOWN, MK_RBUTTON, CursorClientPos(window));
|
int heldKey = 0; // right button is down for this key
|
||||||
std::thread([window, key] {
|
HWND window = nullptr;
|
||||||
while (KeyDown(key) && IsWindow(window)) {
|
};
|
||||||
Sleep(10);
|
State g_state;
|
||||||
}
|
|
||||||
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
|
} // namespace
|
||||||
|
|
||||||
uint16_t RemapKey(uint16_t key) {
|
void CastOnSelect(unsigned slot, int vk) {
|
||||||
if (!Enabled() || !SwapDigits() || key < '0' || key > '9') {
|
std::lock_guard guard(g_state.lock);
|
||||||
return key;
|
g_state.pendingKey = vk;
|
||||||
}
|
g_state.pendingSlot = slot;
|
||||||
static constexpr char kSwapped[] = "5678901234"; // indexed by key - '0'
|
g_state.keyDown = true;
|
||||||
return static_cast<uint16_t>(kSwapped[key - '0']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// After a slot was selected: cast if its digit is what selected it (the slot
|
void KeyReleased(int vk) {
|
||||||
// can also be clicked with the mouse).
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
const int key = g_state.pendingKey;
|
||||||
|
g_state.pendingKey = 0;
|
||||||
const auto* bar = static_cast<const uint8_t*>(taskbar);
|
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]) {
|
!reinterpret_cast<void* const*>(bar + kTaskbarSkillSlots)[slot]) {
|
||||||
return; // empty slot: nothing was selected
|
return; // empty slot: nothing was selected
|
||||||
}
|
}
|
||||||
const int key = SlotKey(slot);
|
HWND window = GameWindow();
|
||||||
if (KeyDown(key)) {
|
if (!window || g_state.heldKey) {
|
||||||
PressRightButtonWhileHeld(key);
|
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 {
|
namespace game {
|
||||||
|
|
||||||
// cUI_Taskbar2::SelectSkillSlot (0x6e17d0): makes combat-art slot `slot` the
|
// 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);
|
void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, unsigned slot, int notify);
|
||||||
RE_HOOK(0x006e17d0, UiTaskbar_SelectSkillSlot);
|
RE_HOOK(0x006e17d0, UiTaskbar_SelectSkillSlot);
|
||||||
void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, unsigned slot, int notify) {
|
void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, unsigned slot, int notify) {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
namespace mods::skillkeys {
|
namespace mods::skillkeys {
|
||||||
|
|
||||||
// The key the game should see for `key` from a key event: with SwapDigits,
|
// The skill key `vk` was pressed to select combat-art slot `slot`: when the
|
||||||
// 1-5 and 6-0 trade places (skills on 1-5, weapon sets on 6-0).
|
// game selects it, press the right mouse button until KeyReleased(vk).
|
||||||
uint16_t RemapKey(uint16_t key);
|
void CastOnSelect(unsigned slot, int vk);
|
||||||
|
void KeyReleased(int vk);
|
||||||
|
|
||||||
} // namespace mods::skillkeys
|
} // namespace mods::skillkeys
|
||||||
+14
-182
@@ -9,47 +9,28 @@
|
|||||||
// it when sent on their own.) Releasing the keys sends the target-less walk
|
// 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.
|
// 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,
|
// W, A and S are game hotkeys; the key bindings (src/mods/input.cpp) keep
|
||||||
// 0x613040), A = pick up items nearby, S = save game window. Key events are
|
// W/A/S/D away from the game and move those actions to other keys. Typing into
|
||||||
// filtered before the engine and UI see them: W/A/S/D are swallowed (key
|
// a text field (chat, console, save names) is left alone. See docs/wasd.md.
|
||||||
// repeat would otherwise reach the hotkeys), and replacement keys are rewritten
|
|
||||||
// into the old letters (temporary choices, see docs/todo.md). Typing into a
|
#include "mods/wasd.h"
|
||||||
// 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)
|
|
||||||
|
|
||||||
#include <cctype>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "mods/skillkeys.h"
|
#include "mods/input.h"
|
||||||
#include "re/config.h"
|
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
#include "re/log.h"
|
|
||||||
#include "re/patch.h"
|
|
||||||
|
|
||||||
namespace mods::wasd {
|
namespace mods::wasd {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// --- sacred.exe ---------------------------------------------------------------
|
// --- sacred.exe ---------------------------------------------------------------
|
||||||
constexpr uint32_t kObjectManager = 0x00ad3bc0; // object manager**
|
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 kEventCreatureVtable = 0x0088e95c;
|
||||||
constexpr uint32_t kTypeObject = 0x008e9648; // RTTI type descriptors
|
constexpr uint32_t kTypeObject = 0x008e9648; // RTTI type descriptors
|
||||||
constexpr uint32_t kTypeCreature = 0x008e9660;
|
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 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);
|
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);
|
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
|
// Walk orders aim this far (world units) from the creature, about where the
|
||||||
// cursor would be.
|
// cursor would be.
|
||||||
constexpr double kTargetDistance = 300.0;
|
constexpr double kTargetDistance = 300.0;
|
||||||
|
|
||||||
bool Enabled() {
|
|
||||||
static const bool enabled = re::ConfigInt("wasd", "Enabled", 1) != 0;
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool KeyDown(int key) {
|
bool KeyDown(int key) {
|
||||||
return (GetAsyncKeyState(key) & 0x8000) != 0;
|
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* ControlledCreature(uint8_t* engine) {
|
||||||
void* manager = *reinterpret_cast<void**>(kObjectManager);
|
void* manager = *reinterpret_cast<void**>(kObjectManager);
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
@@ -235,6 +97,12 @@ struct MoveState {
|
|||||||
};
|
};
|
||||||
MoveState g_move;
|
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.
|
// Called once per engine tick (10 Hz), after the game's own input handling.
|
||||||
void Update(uint8_t* engine) {
|
void Update(uint8_t* engine) {
|
||||||
// World axes on screen: (-1,-1) up, (1,1) down, (-1,1) left, (1,-1) right.
|
// 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;
|
int dy = 0;
|
||||||
const uint32_t flags = *reinterpret_cast<uint32_t*>(engine + kEngineFlags);
|
const uint32_t flags = *reinterpret_cast<uint32_t*>(engine + kEngineFlags);
|
||||||
const bool mouseMoving = (flags & 0x10) != 0;
|
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('W')) { dx -= 1; dy -= 1; }
|
||||||
if (KeyDown('S')) { dx += 1; dy += 1; }
|
if (KeyDown('S')) { dx += 1; dy += 1; }
|
||||||
if (KeyDown('A')) { dx -= 1; dy += 1; }
|
if (KeyDown('A')) { dx -= 1; dy += 1; }
|
||||||
@@ -269,17 +137,6 @@ void Update(uint8_t* engine) {
|
|||||||
g_move.moving = true;
|
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 mods::wasd
|
||||||
|
|
||||||
namespace game {
|
namespace game {
|
||||||
@@ -290,34 +147,9 @@ void __fastcall Engine_Tick(void* self, void* edx, void* event);
|
|||||||
RE_HOOK(0x0060f1d0, Engine_Tick);
|
RE_HOOK(0x0060f1d0, Engine_Tick);
|
||||||
void __fastcall Engine_Tick(void* self, void* edx, void* event) {
|
void __fastcall Engine_Tick(void* self, void* edx, void* event) {
|
||||||
Engine_Tick_original(self, edx, event);
|
Engine_Tick_original(self, edx, event);
|
||||||
if (mods::wasd::Enabled() && self) {
|
if (self) {
|
||||||
mods::wasd::Update(static_cast<uint8_t*>(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
|
} // namespace game
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -103,3 +103,4 @@
|
|||||||
0x005de8a0 cInventoryEntry::FixInventory
|
0x005de8a0 cInventoryEntry::FixInventory
|
||||||
0x005da880 cInventoryEntry::GetPotionRef
|
0x005da880 cInventoryEntry::GetPotionRef
|
||||||
0x0057f750 SlotCountForLevel # table 0x95503c, max 5
|
0x0057f750 SlotCountForLevel # table 0x95503c, max 5
|
||||||
|
0x008110d0 GameWindowProc # in-game window procedure: keys (dead keys, modifiers 0x182cdec)
|
||||||
Reference in new issue
Block a user