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:
1 parent
1fd89524d3
commit
a7f8496b2b
6 files changed
+165
-4
No files matched your search
+8
-2
@@ -29,5 +29,11 @@ HandleEsc is hooked: unless the ESC menu is open, every visible window of a
|
||||
table (slot, name) gets that hide command, and Esc does nothing else. If none
|
||||
is open, the game handles Esc as before.
|
||||
|
||||
Not covered: dialogs that are not one of these windows, e.g. quest dialogs
|
||||
(standalone buttons and texts in the UI manager, docs/hud.md).
|
||||
Quest dialogs are tooltips (docs/hud.md) that block all input until answered.
|
||||
A click on one of their entries (cUI_Tooltip::HandleEvent 0x6e71e2) hides the
|
||||
tooltip (0x731ee0, flag 1), calls cDialogManager::Answer (0x49a090 on
|
||||
0xaaaf00, (owner = tooltip +0x21cc, answer)) and clears 0xab5314. Answer sends
|
||||
every player hero a cEvent_creature 0x10c (ends the input lock) and passes
|
||||
answers below 100000 to the quest script (0x46bd80). Esc does the same with
|
||||
answer 0xffffffff: the dialog closes, nothing is chosen. (Hiding the tooltip
|
||||
alone left the input lock; measured with a write watch on its flags.)
|
||||
+16
@@ -70,3 +70,19 @@ clicks only hit at the unshifted position. `cUI_Button2`, `cUI_Static2`,
|
||||
`cUI_StaticText64`, `cUI_StaticText64FX` (render and event) and
|
||||
`cUI_TextLines` (render) are hooked as centred windows too; inside other
|
||||
windows they are in that window's scope already (outermost wins).
|
||||
|
||||
## Tooltips and quest dialogs (2026-10-16)
|
||||
|
||||
Quest dialogs are tooltips: `cUI_Manager::tooltipCreate` (`0x75a870`) hands
|
||||
out a panel from a pool at cUI_Manager +0x124 (`UI_WND_TOOLTIP`, created
|
||||
1024x768, flags 0x804 while hidden); the quest dialog used one at 136,306,
|
||||
752x157 (flags 0x805). Tooltips used to be drawn with the offset of the moved
|
||||
window under the cursor in every frame, and without recording an offset for
|
||||
hit tests: dialogs jumped to the 1024 frame when the cursor left the UI, and
|
||||
their clicks only hit at the unshifted position. Now a tooltip keeps the
|
||||
offset it gets when it becomes visible (the hovered window's, or the centre
|
||||
when no moved window is under the cursor: a dialog) and records it for hit
|
||||
tests. Visible dialog tooltips: `mods::hud::VisibleDialogTooltips`.
|
||||
|
||||
The standalone control hooks (buttons, texts) above turned out not to be what
|
||||
the quest dialog is made of; they stay, for top-level controls in general.
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -167,3 +167,10 @@
|
||||
0x00536f20 cCreature::DistanceTo # (object id)
|
||||
0x00ad1c60 g_nearbyCreatures # {object id, ?} x g_nearbyCreatureCount, creatures near the player
|
||||
0x00ad1b48 g_nearbyCreatureCount
|
||||
0x0049a090 cDialogManager::Answer # (owner, answer): cEvent_creature 0x10c to all heroes (ends the dialog lock); answers < 100000 -> quest script 0x46bd80
|
||||
0x006e7120 cUI_Tooltip::HandleEvent # dialog answers: click on an entry (+0x2188 list) -> hide, cDialogManager::Answer
|
||||
0x0075a870 cUI_Manager::TooltipCreate # a panel from the pool at +0x124
|
||||
0x00756ba0 cUI_Manager::HandleEsc
|
||||
0x00731ee0 cUI_Control2::ClearFlags # (flags): bit 0 = visible (hide)
|
||||
0x00aaaf00 g_dialogManager
|
||||
0x006360e0 cRecordCache::Get # map at +0xde5c, LRU (see docs/performance.md)
|
||||
Reference in new issue
Block a user