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

+32
View File
@@ -12,6 +12,7 @@
#include <cstdint>
#include <cstring>
#include "mods/hud.h"
#include "re/hook.h"
#include "re/log.h"
@@ -31,6 +32,27 @@ constexpr ClosableWindow kClosable[] = {
{0x31, "UI_WND_HORSE"}, {0x35, "UI_WND_CUBE"},
};
constexpr int kEscMenuSlot = 0x2c;
// A dialog tooltip answers a click on one of its entries (cUI_Tooltip vf10,
// 0x6e71e2): it hides itself (cUI_Control2 hide 0x731ee0, flag 1), tells the
// dialog manager (0x49a090 on 0xaaaf00, (owner +0x21cc, answer)) and clears
// 0xab5314. The manager sends every player hero a cEvent_creature 0x10c, which
// ends the dialog's input lock, and passes answers below 100000 to the quest
// script (0x46bd80). Esc does the same with an answer of 100000 or more: the
// dialog closes and the heroes are free, nothing is chosen.
constexpr uint32_t kHideVisible = 1;
constexpr uint32_t kDialogManager = 0x00aaaf00;
constexpr size_t kTooltipDialogOwner = 0x21cc;
constexpr uint32_t kNoAnswer = 0xffffffff; // >= 100000: no script callback
constexpr uint32_t kDialogPending = 0x00ab5314;
void CloseDialog(uint8_t* tooltip) {
using HideFn = void(__fastcall*)(void*, void*, uint32_t);
reinterpret_cast<HideFn>(0x00731ee0)(tooltip, nullptr, kHideVisible);
using AnswerFn = void(__fastcall*)(void*, void*, uint32_t owner, uint32_t answer);
reinterpret_cast<AnswerFn>(0x0049a090)(reinterpret_cast<void*>(kDialogManager), nullptr,
*reinterpret_cast<uint32_t*>(tooltip + kTooltipDialogOwner), kNoAnswer);
*reinterpret_cast<uint32_t*>(kDialogPending) = 0;
}
constexpr size_t kControlFlags = 0x10; // bit 0: visible
// cEventUI2 as HandleEsc builds it (event at [esp+10h] there).
@@ -73,6 +95,16 @@ bool CloseWindows(uint32_t* manager) {
if (Visible(reinterpret_cast<uint8_t*>(manager[kEscMenuSlot]))) {
return false;
}
// Quest dialogs are tooltips (src/mods/hud.cpp) that block all input
// until answered: closed first.
void* dialogs[8];
const int dialogCount = mods::hud::VisibleDialogTooltips(dialogs, 8);
for (int i = 0; i < dialogCount; ++i) {
CloseDialog(static_cast<uint8_t*>(dialogs[i]));
}
if (dialogCount > 0) {
return true;
}
bool closed = false;
for (const ClosableWindow& w : kClosable) {
auto* window = reinterpret_cast<uint8_t*>(manager[w.slot]);
+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 {
+4
View File
@@ -13,4 +13,8 @@ int CursorOffsetX();
// window, a private copy with x shifted by CursorOffsetX().
void* CursorFor(void* cursor);
// Tooltips that are dialogs of their own (e.g. quest dialogs: they became
// visible while no moved window was under the cursor) and are visible now.
int VisibleDialogTooltips(void** out, int max);
} // namespace mods::hud