diff --git a/docs/wasd.md b/docs/wasd.md index 03632b6..dda76ff 100644 --- a/docs/wasd.md +++ b/docs/wasd.md @@ -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 `cEngine::ScrollInput` (`0x610650`) walk object #2 or scroll the camera; not 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. diff --git a/src/mods/input.cpp b/src/mods/input.cpp index 2ba1b04..8b8de10 100644 --- a/src/mods/input.cpp +++ b/src/mods/input.cpp @@ -229,29 +229,36 @@ void Startup() { } RE_STARTUP(InputStartup, Startup); -// The text field that has the keyboard, as the game decides it: the UI -// manager hands key events to its dialog (+0x144) while that is shown -// (+0x10 bit 0; cUI_Manager::ReceiveEvent 0x7573ba), and the dialog's focused -// control (+0x148) is an edit control without the read-only bit (+0x10 bit -// 16). The focus pointer alone stays set after the dialog closed (measured: -// the save window's 'DEFAULT_EDIT' after closing it), which switched the -// bindings off. -constexpr size_t kUiDialog = 0x144; +// 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(kUiManager); - auto* dialog = ui ? *reinterpret_cast(ui + kUiDialog) : nullptr; - if (!dialog || (*reinterpret_cast(dialog + kControlFlags) & kControlVisible) == 0) { + void* focus = ui ? *reinterpret_cast(ui + kUiFocus) : nullptr; + if (!focus || focus != g_drawnEdit.load() || GetTickCount() - g_drawnEditTick.load() > kDrawnRecentlyMs) { return nullptr; } - void* focus = *reinterpret_cast(ui + kUiFocus); - auto* edit = static_cast(focus ? game::DynamicCast(focus, game::kTypeControl, game::kTypeEditControl) - : nullptr); - if (!edit || (*reinterpret_cast(edit + kControlFlags) & kEditReadOnly) != 0) { + auto* edit = static_cast(game::DynamicCast(focus, game::kTypeControl, game::kTypeEditControl)); + if (!edit || (ControlFlags(edit) & kEditReadOnly) != 0) { return nullptr; } return edit; @@ -259,6 +266,17 @@ uint8_t* FocusedTextField() { } // 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(kUiManager); + if (ui && edit == *reinterpret_cast(ui + kUiFocus) && + (ControlFlags(static_cast(edit)) & kControlVisible) != 0) { + g_drawnEdit = edit; + g_drawnEditTick = GetTickCount(); + } +} + bool TypingIntoTextField() { return FocusedTextField() != nullptr; } @@ -327,4 +345,13 @@ LRESULT CALLBACK GameWindowProc(HWND window, UINT message, WPARAM 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 diff --git a/src/mods/input.h b/src/mods/input.h index 10e494e..46cbaf8 100644 --- a/src/mods/input.h +++ b/src/mods/input.h @@ -7,6 +7,9 @@ namespace mods::input { // Whether a text field has the keyboard (chat, console, save names). 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 // handled and must not reach the game. bool HandleWindowMessage(HWND window, UINT message, WPARAM wParam, LPARAM lParam);