// 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 #include #include #include #include #include #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(Move::Up)}, {"MoveLeft", "A", L"Nach links", Kind::Move, static_cast(Move::Left)}, {"MoveDown", "S", L"Nach unten", Kind::Move, static_cast(Move::Down)}, {"MoveRight", "D", L"Nach rechts", Kind::Move, static_cast(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(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(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(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; } } 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 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 g_potion{0}; void UseActionSlot(uint8_t* engine, int kind, bool ally) { using UseFn = int(__fastcall*)(void*, void*, int, int); reinterpret_cast(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(static_cast(ui[slot])); return window && (*reinterpret_cast(window + kControlFlagsOffset) & 1) != 0; } bool Flag(uint32_t address) { return *reinterpret_cast(static_cast(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}; // 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(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(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(reinterpret_cast(&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 g_drawnEdit{nullptr}; std::atomic g_drawnEditTick{0}; uint32_t ControlFlags(const uint8_t* control) { return *reinterpret_cast(control + kControlFlags); } uint8_t* FocusedTextField() { auto* ui = reinterpret_cast(game::UiManager()); void* focus = ui ? *reinterpret_cast(ui + kUiFocus) : nullptr; if (!focus || focus != g_drawnEdit.load() || GetTickCount() - g_drawnEditTick.load() > kDrawnRecentlyMs) { return nullptr; } auto* edit = static_cast(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(game::UiManager()); if (ui && edit == *reinterpret_cast(ui + kUiFocus) && (ControlFlags(static_cast(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(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(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(wParam & 0xff)); } return true; } return false; case WM_KEYDOWN: { const int vk = static_cast(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(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