Options: tabs Allgemein / Beute / Tastenbelegung with key bindings
- Options window in three tabs; the auto-pickup settings move to Beute; sliders move with their page; black sides like the save game screen. - Tastenbelegung: every binding settable (click, press key; Esc cancels, Backspace unbinds), "Alle zurücksetzen" as a game button, saved on OK. - Potions bindable (default unbound), named by the game's potion texts; the game's own potion hotkeys are off; bar tooltips show the bound key. - Map (M), overview map (Tab, held) and labels (Alt, held) bindable; the game's own M / Tab / Alt handling is switched off. - Save game key calls the game's save window code instead of a posted key. - Teleportationstrank: fixed text replacement from startup (also the Ausdauertrank items, potion kind 5 as well). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
72525cd5ba
commit
838b1aa491
27 files changed
+1423
-184
No files matched your search
+383
-69
@@ -6,22 +6,30 @@
|
||||
// 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; keys standing in for an original
|
||||
// hotkey post that hotkey's key event to the game's event kernel, the way the window
|
||||
// procedure does (the kernel dispatches it right away, on the window thread).
|
||||
// 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.)
|
||||
//
|
||||
// sacred_re.ini (key names: A-Z, 0-9, F1-F24, Space, None):
|
||||
// The bindings are set on the options window's key bindings tab
|
||||
// (mods/options.cpp) and kept in sacred_re.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
|
||||
// ActionSlot3=G ; what W did (W is movement)
|
||||
// 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"
|
||||
|
||||
@@ -29,14 +37,16 @@
|
||||
#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 "mods/wasd.h"
|
||||
#include "re/config.h"
|
||||
#include "re/hook.h"
|
||||
#include "re/log.h"
|
||||
@@ -45,69 +55,109 @@
|
||||
namespace mods::input {
|
||||
namespace {
|
||||
|
||||
// --- sacred.exe ---------------------------------------------------------------
|
||||
constexpr uint32_t kUiManager = 0x017ecb3c; // cUI_Manager*
|
||||
constexpr uint32_t kKeyModifiers = 0x0182cdec; // u16, as the in-game window procedure keeps it
|
||||
constexpr uint32_t kEventKeyVtable = 0x00895568;
|
||||
|
||||
// cEvent_key: +4 1 = pressed, 0 = released; +6 / +8 the key (character when
|
||||
// pressed, virtual key when released); +0xa modifiers.
|
||||
struct KeyEvent {
|
||||
uint32_t vtable;
|
||||
uint16_t pressed;
|
||||
uint16_t raw;
|
||||
uint16_t key;
|
||||
uint16_t modifiers;
|
||||
};
|
||||
static_assert(sizeof(KeyEvent) == 0xc);
|
||||
|
||||
// Posts the key event the window procedure would post for `key` (an original
|
||||
// hotkey letter or digit).
|
||||
void PostGameKey(char key, bool pressed) {
|
||||
KeyEvent event = {};
|
||||
event.vtable = kEventKeyVtable;
|
||||
event.pressed = pressed ? 1 : 0;
|
||||
// Pressed: the character (lower case, as without Shift); released: the
|
||||
// virtual key (upper case).
|
||||
const auto code = static_cast<uint16_t>(pressed ? tolower(key) : toupper(key));
|
||||
event.raw = code;
|
||||
event.key = code;
|
||||
event.modifiers = *reinterpret_cast<uint16_t*>(kKeyModifiers);
|
||||
game::PostEvent(&event);
|
||||
}
|
||||
|
||||
// --- bindings ------------------------------------------------------------------
|
||||
|
||||
enum class Kind { SkillSlot, WeaponSet, GameKey, PickUp };
|
||||
// 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)
|
||||
char gameKey; // GameKey: the original hotkey
|
||||
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", Kind::SkillSlot, 0, 0},
|
||||
{"SkillSlot1", "1", Kind::SkillSlot, 1, 0},
|
||||
{"SkillSlot2", "2", Kind::SkillSlot, 2, 0},
|
||||
{"SkillSlot3", "3", Kind::SkillSlot, 3, 0},
|
||||
{"SkillSlot4", "4", Kind::SkillSlot, 4, 0},
|
||||
{"SkillSlot5", "5", Kind::SkillSlot, 5, 0},
|
||||
{"SkillSlot6", "6", Kind::SkillSlot, 6, 0},
|
||||
{"WeaponSet1", "F1", Kind::WeaponSet, 0, 0},
|
||||
{"WeaponSet2", "F2", Kind::WeaponSet, 1, 0},
|
||||
{"WeaponSet3", "F3", Kind::WeaponSet, 2, 0},
|
||||
{"WeaponSet4", "F4", Kind::WeaponSet, 3, 0},
|
||||
{"WeaponSet5", "F5", Kind::WeaponSet, 4, 0},
|
||||
{"WeaponSet6", "F6", Kind::WeaponSet, 5, 0},
|
||||
{"WeaponSet7", "F7", Kind::WeaponSet, 6, 0},
|
||||
{"ActionSlot3", "G", Kind::GameKey, 0, 'W'},
|
||||
{"PickUp", "Q", Kind::PickUp, 0, 0},
|
||||
{"SaveGame", "K", Kind::GameKey, 0, 'S'},
|
||||
{"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]))) {
|
||||
@@ -119,8 +169,10 @@ int ParseKey(const std::string& text) {
|
||||
return VK_F1 + n - 1;
|
||||
}
|
||||
}
|
||||
if (_stricmp(text.c_str(), "Space") == 0) {
|
||||
return VK_SPACE;
|
||||
for (const NamedKey& key : kKeyNames) {
|
||||
if (_stricmp(text.c_str(), key.ini) == 0) {
|
||||
return key.vk;
|
||||
}
|
||||
}
|
||||
return 0; // None / unknown
|
||||
}
|
||||
@@ -145,11 +197,14 @@ const Binding* BindingFor(int vk) {
|
||||
}
|
||||
|
||||
// Keys taken away from the game although nothing is bound to them: the digit
|
||||
// row (the game's slot keys) and the movement keys.
|
||||
// row (the game's slot keys).
|
||||
bool Reserved(int vk) {
|
||||
return (vk >= '0' && vk <= '9') || mods::wasd::IsMovementKey(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
|
||||
@@ -163,8 +218,30 @@ 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;
|
||||
}
|
||||
@@ -175,6 +252,107 @@ void OnTick(uint8_t* engine) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -190,12 +368,24 @@ void Press(const Binding& binding, int vk) {
|
||||
case Kind::WeaponSet:
|
||||
mods::skillbar::SelectWeaponSet(slot);
|
||||
break;
|
||||
case Kind::GameKey:
|
||||
PostGameKey(binding.gameKey, true);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,11 +394,16 @@ void Release(const Binding& binding, int vk) {
|
||||
case Kind::SkillSlot:
|
||||
mods::skillkeys::KeyReleased(vk);
|
||||
break;
|
||||
case Kind::WeaponSet:
|
||||
case Kind::PickUp:
|
||||
case Kind::OverviewMap:
|
||||
ShowOverviewMap(false);
|
||||
break;
|
||||
case Kind::GameKey:
|
||||
PostGameKey(binding.gameKey, false);
|
||||
case Kind::WeaponSet:
|
||||
case Kind::Move:
|
||||
case Kind::Potion:
|
||||
case Kind::PickUp:
|
||||
case Kind::SaveGame:
|
||||
case Kind::Map:
|
||||
case Kind::Labels:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -222,6 +417,17 @@ 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");
|
||||
}
|
||||
@@ -252,7 +458,7 @@ uint32_t ControlFlags(const uint8_t* control) {
|
||||
}
|
||||
|
||||
uint8_t* FocusedTextField() {
|
||||
auto* ui = *reinterpret_cast<uint8_t**>(kUiManager);
|
||||
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;
|
||||
@@ -269,7 +475,7 @@ uint8_t* FocusedTextField() {
|
||||
// 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**>(kUiManager);
|
||||
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;
|
||||
@@ -281,11 +487,119 @@ 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()) {
|
||||
|
||||
Reference in new issue
Block a user