Key bindings: pass keys to text fields that are on screen
Character creation and the save window were not detected (the UI manager's dialog slot held a hidden busy dialog); bound keys were swallowed or posted their hotkey into the field. A text field now counts while it is the focused edit control and was drawn in the last 250 ms. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
075f9e2c8a
commit
432cac2ccf
3 files changed
+61
-14
No files matched your search
@@ -62,3 +62,20 @@ Hotkeys found: engine A (pick up nearby), P (pause), Q/W/E/R/B/Space
|
|||||||
manager C F H I L M N O S T (S save game window). Arrow keys in
|
manager C F H I L M N O S T (S save game window). Arrow keys in
|
||||||
`cEngine::ScrollInput` (`0x610650`) walk object #2 or scroll the camera; not
|
`cEngine::ScrollInput` (`0x610650`) walk object #2 or scroll the camera; not
|
||||||
reachable in normal play.
|
reachable in normal play.
|
||||||
|
|
||||||
|
## Text fields (2026-10-18)
|
||||||
|
|
||||||
|
Request: in character creation and the save window, bound keys were ignored or
|
||||||
|
typed something else (K -> s: K posts the game's S hotkey).
|
||||||
|
|
||||||
|
Measured (key-press log): the UI manager's dialog slot (+0x144) held a hidden
|
||||||
|
`cUI_BusyDlg` in both cases, so the old "dialog shown + focused edit control"
|
||||||
|
test never found the field. The focused control (+0x148) is the right
|
||||||
|
`cUI_EditControl`, but neither the focus nor the control's flags (0x8005)
|
||||||
|
change when its window closes, and the control has no parent at +0x50. The
|
||||||
|
save window draws two edit controls per frame.
|
||||||
|
|
||||||
|
Now (`src/mods/input.cpp`, `FocusedTextField`): keys pass to the game while the
|
||||||
|
focused control is a writable edit control that `cUI_EditControl::Render`
|
||||||
|
(0x737ae0, thiscall (renderer), ret 4, returns at once while hidden) drew in
|
||||||
|
the last 250 ms. The render hook only notes the focused control.
|
||||||
+41
-14
@@ -229,29 +229,36 @@ void Startup() {
|
|||||||
}
|
}
|
||||||
RE_STARTUP(InputStartup, Startup);
|
RE_STARTUP(InputStartup, Startup);
|
||||||
|
|
||||||
// The text field that has the keyboard, as the game decides it: the UI
|
// The text field that has the keyboard: the UI manager's focused control
|
||||||
// manager hands key events to its dialog (+0x144) while that is shown
|
// (+0x148, set by UI event 0x10 in cUI_Manager::ReceiveEvent 0x757120), when
|
||||||
// (+0x10 bit 0; cUI_Manager::ReceiveEvent 0x7573ba), and the dialog's focused
|
// it is an edit control without the read-only bit (+0x10 bit 16) that is on
|
||||||
// control (+0x148) is an edit control without the read-only bit (+0x10 bit
|
// screen. Neither the focus nor the control's own flags change when its window
|
||||||
// 16). The focus pointer alone stays set after the dialog closed (measured:
|
// closes (measured: flags 0x8005 and the focus kept after closing the save
|
||||||
// the save window's 'DEFAULT_EDIT' after closing it), which switched the
|
// window; the control has no parent at +0x50), so "on screen" is measured:
|
||||||
// bindings off.
|
// cUI_EditControl::Render (0x737ae0) drew it in the last kDrawnRecentlyMs. (The manager's dialog slot +0x144 is not it either: it
|
||||||
constexpr size_t kUiDialog = 0x144;
|
// held a hidden cUI_BusyDlg while typing in character creation and the save
|
||||||
|
// window.)
|
||||||
constexpr size_t kUiFocus = 0x148;
|
constexpr size_t kUiFocus = 0x148;
|
||||||
constexpr size_t kControlFlags = 0x10;
|
constexpr size_t kControlFlags = 0x10;
|
||||||
constexpr uint32_t kControlVisible = 1;
|
constexpr uint32_t kControlVisible = 1;
|
||||||
constexpr uint32_t kEditReadOnly = 1u << 16;
|
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() {
|
uint8_t* FocusedTextField() {
|
||||||
auto* ui = *reinterpret_cast<uint8_t**>(kUiManager);
|
auto* ui = *reinterpret_cast<uint8_t**>(kUiManager);
|
||||||
auto* dialog = ui ? *reinterpret_cast<uint8_t**>(ui + kUiDialog) : nullptr;
|
void* focus = ui ? *reinterpret_cast<void**>(ui + kUiFocus) : nullptr;
|
||||||
if (!dialog || (*reinterpret_cast<uint32_t*>(dialog + kControlFlags) & kControlVisible) == 0) {
|
if (!focus || focus != g_drawnEdit.load() || GetTickCount() - g_drawnEditTick.load() > kDrawnRecentlyMs) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
void* focus = *reinterpret_cast<void**>(ui + kUiFocus);
|
auto* edit = static_cast<uint8_t*>(game::DynamicCast(focus, game::kTypeControl, game::kTypeEditControl));
|
||||||
auto* edit = static_cast<uint8_t*>(focus ? game::DynamicCast(focus, game::kTypeControl, game::kTypeEditControl)
|
if (!edit || (ControlFlags(edit) & kEditReadOnly) != 0) {
|
||||||
: nullptr);
|
|
||||||
if (!edit || (*reinterpret_cast<uint32_t*>(edit + kControlFlags) & kEditReadOnly) != 0) {
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return edit;
|
return edit;
|
||||||
@@ -259,6 +266,17 @@ uint8_t* FocusedTextField() {
|
|||||||
|
|
||||||
} // namespace
|
} // 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**>(kUiManager);
|
||||||
|
if (ui && edit == *reinterpret_cast<void**>(ui + kUiFocus) &&
|
||||||
|
(ControlFlags(static_cast<uint8_t*>(edit)) & kControlVisible) != 0) {
|
||||||
|
g_drawnEdit = edit;
|
||||||
|
g_drawnEditTick = GetTickCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool TypingIntoTextField() {
|
bool TypingIntoTextField() {
|
||||||
return FocusedTextField() != nullptr;
|
return FocusedTextField() != nullptr;
|
||||||
}
|
}
|
||||||
@@ -327,4 +345,13 @@ LRESULT CALLBACK GameWindowProc(HWND window, UINT message, WPARAM wParam, LPARAM
|
|||||||
return GameWindowProc_original(window, message, wParam, lParam);
|
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
|
} // namespace game
|
||||||
@@ -7,6 +7,9 @@ namespace mods::input {
|
|||||||
// Whether a text field has the keyboard (chat, console, save names).
|
// Whether a text field has the keyboard (chat, console, save names).
|
||||||
bool TypingIntoTextField();
|
bool TypingIntoTextField();
|
||||||
|
|
||||||
|
// cUI_EditControl::Render hook (input.cpp): the control was drawn.
|
||||||
|
void NoteEditDrawn(void* edit);
|
||||||
|
|
||||||
// Called first by the main window procedure. Returns true if the message was
|
// Called first by the main window procedure. Returns true if the message was
|
||||||
// handled and must not reach the game.
|
// handled and must not reach the game.
|
||||||
bool HandleWindowMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam);
|
bool HandleWindowMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam);
|
||||||
|
|||||||
Reference in new issue
Block a user