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>
672 lines
24 KiB
C++
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
|