Quest dialogs: stay centred, clickable, closable with Esc

- Quest dialogs are tooltips. A tooltip now keeps the offset it gets when it
  becomes visible (the hovered window's, or centred for dialogs) and records
  it for hit tests; its own event handler is shifted too. Hidden windows no
  longer count as hovered.
- Esc closes a visible dialog the way its answer buttons do: hide, then
  cDialogManager::Answer with no answer (ends the input lock, nothing chosen).
- Autotest can select a save game row (-Save).
- Names for the UI manager, tooltip and dialog functions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-16 00:39:29 +02:00
1 parent 1fd89524d3
commit a7f8496b2b
6 files changed
+165 -4

No files matched your search

+98 -2
View File
@@ -159,6 +159,20 @@ volatile LONG g_hoverOffset = 0;
volatile DWORD g_hoverTime = 0;
constexpr DWORD kHoverExpiryMs = 150;
int HoverOffset();
// Tooltips seen so far: whether visible, and the offset chosen when they
// became visible.
struct TooltipState {
void* tooltip;
bool visible;
bool dialog;
int dx;
};
constexpr int kTooltipCount = 64;
TooltipState g_tooltips[kTooltipCount] = {};
constexpr size_t kControlFlags = 0x10; // bit 0: visible
int HoverOffset() {
return GetTickCount() - g_hoverTime < kHoverExpiryMs ? g_hoverOffset : 0;
}
@@ -170,6 +184,9 @@ void TrackHover(void* window, int dx) {
return;
}
const auto* w = static_cast<const uint8_t*>(window);
if ((w[0x10] & 1) == 0) {
return; // hidden windows render too (and some cover the whole frame)
}
const int x = *reinterpret_cast<const int32_t*>(w + 0x24) + dx;
const int y = *reinterpret_cast<const int32_t*>(w + 0x28);
const int width = *reinterpret_cast<const uint16_t*>(w + 0x2c);
@@ -400,6 +417,64 @@ void OnDevice(void* device) {
}
}
// Dialogs among the tooltips: visible and at least kDialogMinWidth wide (the
// quest dialog: 752; hover tooltips and speech bubbles are narrow). The
// hover rule above cannot tell: a full-screen window drawn while hidden counts
// as hovered whenever the cursor is inside the 1024 frame.
constexpr int kDialogMinWidth = 400;
int VisibleDialogTooltipsImpl(void** out, int max) {
int count = 0;
for (const TooltipState& t : g_tooltips) {
if (!t.tooltip) {
continue;
}
const auto* p = static_cast<const uint8_t*>(t.tooltip);
if ((p[kControlFlags] & 1) == 0) {
continue;
}
const int width = *reinterpret_cast<const uint16_t*>(p + 0x2c);
if (width >= kDialogMinWidth && count < max) {
out[count++] = t.tooltip;
}
}
return count;
}
int KnownTooltipOffset(void* tooltip) {
for (const TooltipState& t : g_tooltips) {
if (t.tooltip == tooltip) {
return t.dx;
}
}
return OffsetFor(Anchor::Center);
}
int TooltipOffset(void* tooltip) {
TooltipState* state = nullptr;
for (TooltipState& t : g_tooltips) {
if (t.tooltip == tooltip) {
state = &t;
break;
}
if (!state && !t.tooltip) {
state = &t;
}
}
if (!state) {
return HoverOffset();
}
const bool visible = (static_cast<uint8_t*>(tooltip)[kControlFlags] & 1) != 0;
if (state->tooltip != tooltip || (visible && !state->visible)) {
const int hover = HoverOffset();
state->tooltip = tooltip;
state->dialog = hover == 0;
state->dx = hover != 0 ? hover : OffsetFor(Anchor::Center);
}
state->visible = visible;
return state->dx;
}
} // namespace mods::hud
// --- HUD windows ---------------------------------------------------------------
@@ -516,11 +591,28 @@ HUD_EVENT(0x00753360, UiStatic_HandleEvent, Anchor::Center)
HUD_RENDER(0x007530a0, UiStatic_Render, Anchor::Center)
HUD_RENDER(0x007228b0, UiTextLines_Render, Anchor::Center)
// Tooltips: placed by the hovered window in its own layout (see TrackHover).
// Tooltips (cUI_Tooltip, a pool at cUI_Manager +0x124, one panel each). A
// tooltip is either a hover tooltip placed by the window under the cursor in
// its own layout, or a dialog of its own, e.g. a quest dialog (laid out in the
// 1024 frame, measured: rect 136,306 752x157). When a tooltip becomes visible
// it keeps an offset until it is hidden: the hovered window's (TrackHover) if
// a moved window is under the cursor, otherwise the centre (a dialog). Taking
// the hover offset on every frame made dialogs jump to the 1024 frame when
// the cursor left the UI, and without a recorded offset their hit tests used
// none, so clicks only worked at the unshifted position.
// Its event handler (vf10 0x6e7120) tests the click itself before it calls
// the shared window handler: it needs the same offset as its drawing.
int __fastcall UiTooltip_HandleEvent(void* self, void* edx, void* event);
RE_HOOK(0x006e7120, UiTooltip_HandleEvent);
int __fastcall UiTooltip_HandleEvent(void* self, void* edx, void* event) {
mods::hud::EventOffset offset(event, mods::hud::KnownTooltipOffset(self));
return UiTooltip_HandleEvent_original(self, edx, event);
}
int __fastcall UiTooltip_Render(void* self, void* edx, void* device);
RE_HOOK(0x006e7300, UiTooltip_Render);
int __fastcall UiTooltip_Render(void* self, void* edx, void* device) {
mods::hud::DrawOffset offset(nullptr, mods::hud::HoverOffset());
mods::hud::DrawOffset offset(self, mods::hud::TooltipOffset(self));
return UiTooltip_Render_original(self, edx, device);
}
@@ -544,6 +636,10 @@ int HitTestX(void* window, int x) {
return x - OffsetOf(root);
}
int VisibleDialogTooltips(void** out, int max) {
return VisibleDialogTooltipsImpl(out, max);
}
} // namespace mods::hud
namespace game {