Files
sacred_k/src/mods/input.cpp
T
khallaandClaude Opus 5.5 70673dc301 Rename project to sacredK
sacred_re.dll/.exe/.ini/.log/_crash files are now sacredK.*; inventory saves
with the old "sacred_re.inventory" format id still load.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 22:07:14 +02:00

672 lines
24 KiB
C++

// 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. Skill keys cast their slot (src/mods/skillkeys.cpp),
// weapon set keys select it on the taskbar; the other actions call the game
// function its hotkey ran. The game's own hotkeys for bound actions are
// switched off (potions) or kept from it (the keys are swallowed).
//
// While a text field has the keyboard, keys pass through unchanged. (Keys
// with Ctrl / Alt held used to pass too; GetKeyState reports Alt as held after
// Alt+Tab until it is pressed again in the game, which switched the bindings
// off. Alt+key arrives as WM_SYSKEYDOWN and is not handled here anyway.)
//
// The bindings are set on the options window's key bindings tab
// (mods/options.cpp) and kept in sacredK.ini (key names: kKeyNames below,
// None):
// [keys]
// SkillSlot0=0 ... SkillSlot6=6 ; cast combat-art slot (0 = right mouse button art)
// WeaponSet1=F1 ... WeaponSet7=F7 ; select weapon set
// MoveUp=W, MoveLeft=A, MoveDown=S, MoveRight=D ; direction movement (mods/wasd.cpp polls them)
// Potion1=None ... Potion5=None ; drink a potion of kind 1-5
// AllyPotion=None ; kind 1 on the weakest ally (the game's own potion keys
// ; Space/B/Q/W/E/R are switched off)
// PickUp=Q ; what A did (pick up items nearby; Q's own action slot is given up)
// SaveGame=K ; what S did (save game window)
// Map=M ; open / close the map (the game's M is switched off)
// OverviewMap=Tab ; the overview map while held (the game's Tab is switched off)
// Labels=Alt ; names of items and creatures while held (the game's Alt is switched off)
#include "mods/input.h"
#include <atomic>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <iterator>
#include <string>
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "game/engine/items.h"
#include "game/engine/orders.h"
#include "game/ui/windows.h"
#include "mods/skillbar.h"
#include "mods/skillkeys.h"
#include "re/config.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::input {
namespace {
// --- bindings ------------------------------------------------------------------
// Move: a held key polled by mods/wasd.cpp; its messages are only swallowed.
// Labels: a held key the game polls (LabelsKeyState); its messages are only
// swallowed.
enum class Kind { SkillSlot, WeaponSet, Move, Potion, PickUp, SaveGame, Map, OverviewMap, Labels };
// Potion index: the potion kind, with this flag on the weakest ally.
constexpr int kAllyPotion = 0x100;
struct Binding {
const char* name;
const char* fallback;
const wchar_t* label;
Kind kind;
int index; // slot / set (0-based), Move direction
int vk = 0;
};
// Display order (the key bindings page shows them in groups by kind).
Binding g_bindings[] = {
{"SkillSlot0", "0", L"Kampfkunst 0 (Rechtsklick)", Kind::SkillSlot, 0},
{"SkillSlot1", "1", L"Kampfkunst 1", Kind::SkillSlot, 1},
{"SkillSlot2", "2", L"Kampfkunst 2", Kind::SkillSlot, 2},
{"SkillSlot3", "3", L"Kampfkunst 3", Kind::SkillSlot, 3},
{"SkillSlot4", "4", L"Kampfkunst 4", Kind::SkillSlot, 4},
{"SkillSlot5", "5", L"Kampfkunst 5", Kind::SkillSlot, 5},
{"SkillSlot6", "6", L"Kampfkunst 6", Kind::SkillSlot, 6},
{"WeaponSet1", "F1", L"Waffenset 1", Kind::WeaponSet, 0},
{"WeaponSet2", "F2", L"Waffenset 2", Kind::WeaponSet, 1},
{"WeaponSet3", "F3", L"Waffenset 3", Kind::WeaponSet, 2},
{"WeaponSet4", "F4", L"Waffenset 4", Kind::WeaponSet, 3},
{"WeaponSet5", "F5", L"Waffenset 5", Kind::WeaponSet, 4},
{"WeaponSet6", "F6", L"Waffenset 6", Kind::WeaponSet, 5},
{"WeaponSet7", "F7", L"Waffenset 7", Kind::WeaponSet, 6},
{"MoveUp", "W", L"Nach oben", Kind::Move, static_cast<int>(Move::Up)},
{"MoveLeft", "A", L"Nach links", Kind::Move, static_cast<int>(Move::Left)},
{"MoveDown", "S", L"Nach unten", Kind::Move, static_cast<int>(Move::Down)},
{"MoveRight", "D", L"Nach rechts", Kind::Move, static_cast<int>(Move::Right)},
{"Potion1", "None", L"Trank 1", Kind::Potion, 1},
{"Potion2", "None", L"Trank 2", Kind::Potion, 2},
{"Potion3", "None", L"Trank 3", Kind::Potion, 3},
{"Potion4", "None", L"Trank 4", Kind::Potion, 4},
{"Potion5", "None", L"Trank 5", Kind::Potion, 5},
{"AllyPotion", "None", L"Trank 1", Kind::Potion, 1 | kAllyPotion},
{"PickUp", "Q", L"Gegenstände aufheben", Kind::PickUp, 0},
{"SaveGame", "K", L"Spiel speichern", Kind::SaveGame, 0},
{"Map", "M", L"Karte", Kind::Map, 0},
{"OverviewMap", "Tab", L"Minikarte (halten)", Kind::OverviewMap, 0},
{"Labels", "Alt", L"Beschriftungen (halten)", Kind::Labels, 0},
};
constexpr int kBindingCount = static_cast<int>(std::size(g_bindings));
// Keys that can be bound besides A-Z, 0-9 and F1-F24 (named by their letter /
// number): ini name, shown name. Esc and Backspace stay free for the key
// bindings page (cancel / unbind), Enter for the game's chat.
struct NamedKey {
int vk;
const char* ini;
const wchar_t* label;
};
constexpr NamedKey kKeyNames[] = {
{VK_SPACE, "Space", L"Leertaste"}, {VK_TAB, "Tab", L"Tab"},
{VK_MENU, "Alt", L"Alt"},
{VK_INSERT, "Insert", L"Einfg"}, {VK_DELETE, "Delete", L"Entf"},
{VK_HOME, "Home", L"Pos1"}, {VK_END, "End", L"Ende"},
{VK_PRIOR, "PageUp", L"Bild auf"}, {VK_NEXT, "PageDown", L"Bild ab"},
{VK_UP, "Up", L"Pfeil hoch"}, {VK_DOWN, "Down", L"Pfeil runter"},
{VK_LEFT, "Left", L"Pfeil links"}, {VK_RIGHT, "Right", L"Pfeil rechts"},
{VK_NUMPAD0, "Num0", L"Num 0"}, {VK_NUMPAD1, "Num1", L"Num 1"},
{VK_NUMPAD2, "Num2", L"Num 2"}, {VK_NUMPAD3, "Num3", L"Num 3"},
{VK_NUMPAD4, "Num4", L"Num 4"}, {VK_NUMPAD5, "Num5", L"Num 5"},
{VK_NUMPAD6, "Num6", L"Num 6"}, {VK_NUMPAD7, "Num7", L"Num 7"},
{VK_NUMPAD8, "Num8", L"Num 8"}, {VK_NUMPAD9, "Num9", L"Num 9"},
};
bool LetterOrDigit(int vk) {
return (vk >= 'A' && vk <= 'Z') || (vk >= '0' && vk <= '9');
}
bool FunctionKey(int vk) {
return vk >= VK_F1 && vk <= VK_F24;
}
const NamedKey* NamedKeyFor(int vk) {
for (const NamedKey& key : kKeyNames) {
if (key.vk == vk) {
return &key;
}
}
return nullptr;
}
// The ini name of a bindable key ("None" for 0).
std::string IniName(int vk) {
if (LetterOrDigit(vk)) {
return std::string(1, static_cast<char>(vk));
}
if (FunctionKey(vk)) {
return "F" + std::to_string(vk - VK_F1 + 1);
}
const NamedKey* key = NamedKeyFor(vk);
return key ? key->ini : "None";
}
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;
}
}
for (const NamedKey& key : kKeyNames) {
if (_stricmp(text.c_str(), key.ini) == 0) {
return key.vk;
}
}
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).
bool Reserved(int vk) {
return vk >= '0' && vk <= '9';
}
// The key bindings page's key capture.
void (*g_capture)(int vk) = nullptr;
// Pick up items nearby: the order the engine's A hotkey sends (creature order
// 0x113, cEngine::ReceiveEvent 0x618c41; the hero collects, mode +0xfc 0xc),
// given to the controlled creature on the next engine tick. (Posting the A
// key event did not reach it: no pickup was logged.)
constexpr uint32_t kOrderPickUp = 0x113;
std::atomic<bool> g_pickUp{false};
// The collect mode (0x4ec3a0, substate 0 without a target) looks for an item
// in square rings of map cells around the hero, nearest ring first, while the
// ring's low offset is above -20 (cmp edi,-20 at 0x4ec8c6). Twice as far.
constexpr uint32_t kCollectRingLimit = 0x004ec8c6;
constexpr uint8_t kCollectRingsOriginal = 0xec; // -20
constexpr uint8_t kCollectRings = 0xd8; // -40
// Drink a potion: cEngine::UseActionSlot (0x613040, thiscall (potion kind,
// on an ally), ret 8), as the engine's potion hotkeys call it, on the next
// engine tick. Those hotkeys (cEngine::ReceiveEvent, a jump table on the
// upper-case character - 0x20: index bytes at 0x619adc, targets at 0x619ac0)
// are switched off: Space kind 1 (Shift: 5), B kind 1 on the weakest ally,
// Q 2, W 3, E 4, R 5 now go to index 6, the table's "no hotkey".
constexpr uint32_t kHotkeyIndices = 0x00619adc;
constexpr uint8_t kNoHotkey = 6;
constexpr re::BytePatch kPotionHotkeys[] = {
{kHotkeyIndices + ' ' - 0x20, 0, 0}, {kHotkeyIndices + 'B' - 0x20, 0, 1},
{kHotkeyIndices + 'E' - 0x20, 0, 2}, {kHotkeyIndices + 'Q' - 0x20, 0, 3},
{kHotkeyIndices + 'R' - 0x20, 0, 4}, {kHotkeyIndices + 'W' - 0x20, 0, 5},
};
std::atomic<int> g_potion{0};
void UseActionSlot(uint8_t* engine, int kind, bool ally) {
using UseFn = int(__fastcall*)(void*, void*, int, int);
reinterpret_cast<UseFn>(0x00613040)(engine, nullptr, kind, ally ? 1 : 0);
}
void OnTick(uint8_t* engine) {
if (const int potion = g_potion.exchange(0)) {
UseActionSlot(engine, potion & 0xff, (potion & kAllyPotion) != 0);
}
if (!g_pickUp.exchange(false)) {
return;
}
if (void* hero = game::ControlledCreature(engine)) {
game::CreatureOrder order;
order.command = kOrderPickUp;
game::SendCreatureOrder(engine, hero, order);
}
}
// The save game window, as the UI manager's S hotkey opens it (0x757775):
// only with the window built (manager slot 0x2b) and no blocking window open;
// it posts an engine event (code 0x13, 5, 1; with 0 for the kernel's second
// argument) and shows UI_WND_SAVEGAME.
constexpr int kSaveGameSlot = 0x2b;
constexpr uint32_t kEngineEventVtable = 0x0088eb38;
constexpr uint32_t kEngineEventSaveGame = 0x13;
void OpenSaveGame() {
uint32_t* ui = game::UiManager();
if (!ui || !ui[kSaveGameSlot] || !game::UiCanOpenWindow(ui)) {
return;
}
uint32_t event[9] = {kEngineEventVtable, kEngineEventSaveGame, 5, 1};
game::PostEvent(event, 0);
game::SendWindowCommand(ui, "UI_WND_SAVEGAME", game::WindowCommand::Show);
}
// The map (cUI_Megamap, manager slot 0x25), as the UI manager's M hotkey
// toggles it (cUI_Manager::ReceiveEvent 0x757120): open, M closes it; else
// nothing while one of the windows it checks first is open (slots 0x2b save
// game, 0x2c, 0x2f options, or the flag 0x17e82c4); it opens unless the flag
// 0x17e8264 is set or slot 0x36 is open. That hotkey is switched off: its
// letter switch (upper-case character - 'C': index bytes at 0x75868c, targets
// at 0x758660) sends M to index 10, "no hotkey", and the closing branch
// compares with -1 instead of 'M' (cmp eax, 4Dh at 0x757f86).
constexpr int kMegamapSlot = 0x25;
constexpr int kMapBlockingSlots[] = {0x2b, 0x2c, 0x2f};
constexpr uint32_t kMapBlockedFlag = 0x017e82c4;
constexpr uint32_t kMapDisabledFlag = 0x017e8264;
constexpr int kMapDisabledSlot = 0x36;
constexpr uint32_t kUiLetterIndices = 0x0075868c;
constexpr uint8_t kUiLetterMap = 5;
constexpr uint8_t kUiNoHotkey = 10;
constexpr uint32_t kUiMapCloseCompare = 0x00757f86;
constexpr size_t kControlFlagsOffset = 0x10; // bit 0: visible
bool WindowShown(const uint32_t* ui, int slot) {
const auto* window = reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(ui[slot]));
return window && (*reinterpret_cast<const uint32_t*>(window + kControlFlagsOffset) & 1) != 0;
}
bool Flag(uint32_t address) {
return *reinterpret_cast<const uint8_t*>(static_cast<uintptr_t>(address)) != 0;
}
void ToggleMap() {
uint32_t* ui = game::UiManager();
if (!ui || !ui[kMegamapSlot]) {
return;
}
if (WindowShown(ui, kMegamapSlot)) {
game::SendWindowCommand(ui, "UI_WND_MEGAMAP", game::WindowCommand::Hide);
return;
}
for (const int slot : kMapBlockingSlots) {
if (WindowShown(ui, slot)) {
return;
}
}
if (!Flag(kMapBlockedFlag) && !Flag(kMapDisabledFlag) && !WindowShown(ui, kMapDisabledSlot)) {
game::SendWindowCommand(ui, "UI_WND_MEGAMAP", game::WindowCommand::Show);
}
}
// The overview map (cUI_OverviewMap) while the key is held, as the game's
// window procedure (0x8110d0) shows it on Tab down (0x81146d) and hides it on
// Tab up (0x8117f8). Both are switched off: their switches on the virtual key
// - 9 (down: index bytes at 0x812124, targets at 0x812110; up: 0x812270 /
// 0x812260) send Tab to their "other key" index.
constexpr uint32_t kKeyDownIndices = 0x00812124;
constexpr uint32_t kKeyUpIndices = 0x00812270;
constexpr uint8_t kKeyDownOther = 4;
constexpr uint8_t kKeyUpOther = 3;
void ShowOverviewMap(bool show) {
if (uint32_t* ui = game::UiManager()) {
game::SendWindowCommand(ui, "UI_WND_OVERVIEWMAP",
show ? game::WindowCommand::Show : game::WindowCommand::Hide);
}
}
// Labels: the game asks GetAsyncKeyState(VK_MENU) for its name overlay
// (cWorldView0::DrawObjectLabels 0x627260 and PickObjectAt 0x626d30, cEngine
// 0x60f1d0 / 0x611f10 / 0x617360, cCreature 0x599880; and the map's debug
// teleport, cUI_Megamap 0x6c7ec0); those calls (call [import]) ask
// LabelsKeyState instead.
constexpr uint32_t kGetAsyncKeyStateImport = 0x0088e2b0;
constexpr uint32_t kLabelsKeyCalls[] = {0x00599ebf, 0x0060f227, 0x00612015, 0x006176ec,
0x00626ffe, 0x00627299, 0x006c80b0};
SHORT WINAPI LabelsKeyState(int) {
for (const Binding& binding : g_bindings) {
if (binding.kind == Kind::Labels) {
return binding.vk ? GetAsyncKeyState(binding.vk) : 0;
}
}
return 0;
}
SHORT(WINAPI* const g_labelsKeyState)(int) = &LabelsKeyState;
struct Registration {
Registration() {
game::OnEngineTick(&OnTick);
}
} g_registration;
void Press(const Binding& binding, int vk) {
const auto slot = static_cast<unsigned>(binding.index);
switch (binding.kind) {
case Kind::SkillSlot:
mods::skillkeys::KeyPressed(slot, vk);
break;
case Kind::WeaponSet:
mods::skillbar::SelectWeaponSet(slot);
break;
case Kind::SaveGame:
OpenSaveGame();
break;
case Kind::PickUp:
g_pickUp = true;
break;
case Kind::Potion:
g_potion = binding.index;
break;
case Kind::Map:
ToggleMap();
break;
case Kind::OverviewMap:
ShowOverviewMap(true);
break;
case Kind::Move:
case Kind::Labels:
break;
}
}
void Release(const Binding& binding, int vk) {
switch (binding.kind) {
case Kind::SkillSlot:
mods::skillkeys::KeyReleased(vk);
break;
case Kind::OverviewMap:
ShowOverviewMap(false);
break;
case Kind::WeaponSet:
case Kind::Move:
case Kind::Potion:
case Kind::PickUp:
case Kind::SaveGame:
case Kind::Map:
case Kind::Labels:
break;
}
}
// 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();
if (re::PatchAll(kPotionHotkeys, kNoHotkey) != 0) {
re::Log("input: potion hotkey patch FAILED");
}
if (!re::PatchInstructionByte(kUiLetterIndices + 'M' - 'C', 0, kUiLetterMap, kUiNoHotkey) ||
!re::PatchInstructionByte(kUiMapCloseCompare, 2, 'M', 0xff) ||
!re::PatchInstructionByte(kKeyDownIndices + VK_TAB - 9, 0, 0, kKeyDownOther) ||
!re::PatchInstructionByte(kKeyUpIndices + VK_TAB - 9, 0, 0, kKeyUpOther) ||
re::PatchAll(kLabelsKeyCalls, kGetAsyncKeyStateImport,
static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&g_labelsKeyState))) != 0) {
re::Log("input: map / overview map / labels key patch FAILED");
}
if (!re::PatchInstructionByte(kCollectRingLimit, 2, kCollectRingsOriginal, kCollectRings)) {
re::Log("input: pickup range patch FAILED");
}
re::Log("input: key bindings active");
}
RE_STARTUP(InputStartup, Startup);
// The text field that has the keyboard: the UI manager's focused control
// (+0x148, set by UI event 0x10 in cUI_Manager::ReceiveEvent 0x757120), when
// it is an edit control without the read-only bit (+0x10 bit 16) that is on
// screen. Neither the focus nor the control's own flags change when its window
// closes (measured: flags 0x8005 and the focus kept after closing the save
// window; the control has no parent at +0x50), so "on screen" is measured:
// cUI_EditControl::Render (0x737ae0) drew it in the last kDrawnRecentlyMs. (The manager's dialog slot +0x144 is not it either: it
// held a hidden cUI_BusyDlg while typing in character creation and the save
// window.)
constexpr size_t kUiFocus = 0x148;
constexpr size_t kControlFlags = 0x10;
constexpr uint32_t kControlVisible = 1;
constexpr uint32_t kEditReadOnly = 1u << 16;
constexpr DWORD kDrawnRecentlyMs = 250;
std::atomic<void*> g_drawnEdit{nullptr};
std::atomic<DWORD> g_drawnEditTick{0};
uint32_t ControlFlags(const uint8_t* control) {
return *reinterpret_cast<const uint32_t*>(control + kControlFlags);
}
uint8_t* FocusedTextField() {
auto* ui = reinterpret_cast<uint8_t*>(game::UiManager());
void* focus = ui ? *reinterpret_cast<void**>(ui + kUiFocus) : nullptr;
if (!focus || focus != g_drawnEdit.load() || GetTickCount() - g_drawnEditTick.load() > kDrawnRecentlyMs) {
return nullptr;
}
auto* edit = static_cast<uint8_t*>(game::DynamicCast(focus, game::kTypeControl, game::kTypeEditControl));
if (!edit || (ControlFlags(edit) & kEditReadOnly) != 0) {
return nullptr;
}
return edit;
}
} // namespace
// Only the focused control counts: a window draws several edit controls per
// frame (measured: the save window), the last one drawn is not the focus.
void NoteEditDrawn(void* edit) {
auto* ui = reinterpret_cast<uint8_t*>(game::UiManager());
if (ui && edit == *reinterpret_cast<void**>(ui + kUiFocus) &&
(ControlFlags(static_cast<uint8_t*>(edit)) & kControlVisible) != 0) {
g_drawnEdit = edit;
g_drawnEditTick = GetTickCount();
}
}
bool TypingIntoTextField() {
return FocusedTextField() != nullptr;
}
int MovementKey(Move direction) {
for (const Binding& binding : g_bindings) {
if (binding.kind == Kind::Move && binding.index == static_cast<int>(direction)) {
return binding.vk;
}
}
return 0;
}
int BindingCount() {
return kBindingCount;
}
std::wstring BindingLabel(int index) {
const Binding& binding = g_bindings[index];
if (binding.kind != Kind::Potion) {
return binding.label;
}
// The potion kind's name as the game shows it ("Trank N" until known).
std::wstring name = game::PotionKindName(binding.index & 0xff);
if (name.empty()) {
name = binding.label;
}
return (binding.index & kAllyPotion) ? name + L" für Gefährten" : name;
}
int PotionKey(int kind) {
for (const Binding& binding : g_bindings) {
if (binding.kind == Kind::Potion && binding.index == kind) {
return binding.vk;
}
}
return 0;
}
const wchar_t* BindingGroup(int index) {
switch (g_bindings[index].kind) {
case Kind::SkillSlot:
return L"Kampfkünste";
case Kind::WeaponSet:
return L"Waffensets";
case Kind::Move:
return L"Bewegung";
case Kind::Potion:
return L"Tränke";
case Kind::PickUp:
case Kind::SaveGame:
case Kind::Map:
case Kind::OverviewMap:
case Kind::Labels:
break;
}
return L"Aktionen";
}
int BindingKey(int index) {
return g_bindings[index].vk;
}
int DefaultBindingKey(int index) {
return ParseKey(g_bindings[index].fallback);
}
void SetBindingKeys(const int* keys) {
for (int i = 0; i < kBindingCount; ++i) {
Binding& binding = g_bindings[i];
binding.vk = Bindable(keys[i]) ? keys[i] : 0;
re::SetConfigString("keys", binding.name, IniName(binding.vk).c_str());
}
re::Log("input: key bindings saved");
}
bool Bindable(int vk) {
return LetterOrDigit(vk) || FunctionKey(vk) || NamedKeyFor(vk);
}
std::wstring KeyLabel(int vk) {
if (LetterOrDigit(vk)) {
return std::wstring(1, static_cast<wchar_t>(vk));
}
if (FunctionKey(vk)) {
return L"F" + std::to_wstring(vk - VK_F1 + 1);
}
const NamedKey* key = NamedKeyFor(vk);
return key ? key->label : L"";
}
void SetKeyCapture(void (*handler)(int vk)) {
g_capture = handler;
}
bool HandleWindowMessage(HWND, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_SYSKEYDOWN:
// Alt and F10 arrive as system keys: only the key capture takes them.
if (g_capture) {
if ((lParam & (1 << 30)) == 0) {
g_capture(static_cast<int>(wParam & 0xff));
}
return true;
}
return false;
case WM_KEYDOWN: {
const int vk = static_cast<int>(wParam & 0xff);
const bool repeat = (lParam & (1 << 30)) != 0;
if (g_capture) {
g_taken[vk] = true;
g_swallowChar = true;
if (!repeat) {
g_capture(vk);
}
return true;
}
if (!repeat) {
g_taken[vk] = false;
if (FocusedTextField()) {
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);
}
// cUI_EditControl::Render (vf14, thiscall (renderer), ret 4; returns right
// away while the control is hidden).
void __fastcall EditControl_Render(void* self, void* edx, void* renderer);
RE_HOOK(0x00737ae0, EditControl_Render);
void __fastcall EditControl_Render(void* self, void* edx, void* renderer) {
mods::input::NoteEditDrawn(self);
EditControl_Render_original(self, edx, renderer);
}
} // namespace game