Files
sacred_k/src/mods/input.cpp
T
khallaandClaude Opus 5.5 c7d54a6cfd Megamap: the zoom button zooms on the hero and follows him
The zoom button never set the zoom point, so it zoomed on the last
magnifier point (or 0,0). While the button's zoom is on, the point is put on
the hero's map pixel every engine tick; the held mouse button still zooms on
the mouse. game::UiWindow / UiWindowShown shared from windows.h.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 11:54:59 +01:00

662 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 ; save the game at once (S opened the save 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/characters.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 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 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;
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[game::kUiSlotMegamap]) {
return;
}
if (game::UiWindowShown(ui, game::kUiSlotMegamap)) {
game::SendWindowCommand(ui, "UI_WND_MEGAMAP", game::WindowCommand::Hide);
return;
}
for (const int slot : kMapBlockingSlots) {
if (game::UiWindowShown(ui, slot)) {
return;
}
}
if (!Flag(kMapBlockedFlag) && !Flag(kMapDisabledFlag) && !game::UiWindowShown(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};
// Set by mods/lootfilter.cpp around DrawObjectLabels to draw the labels of
// items shown without the key (game thread, like the polls).
bool g_labelsForced = false;
SHORT WINAPI LabelsKeyState(int) {
if (g_labelsForced) {
return static_cast<SHORT>(0x8000);
}
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:
characters::RequestSave(); // at once, no save window
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;
}
bool LabelsKeyHeld() {
return (LabelsKeyState(VK_MENU) & 0x8000) != 0;
}
void ForceLabels(bool forced) {
g_labelsForced = forced;
}
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