The zoomed map view resets the viewport to the full render size when done; the HUD layer shifted that reset past the render target, SetViewport failed and the map's 256x256 viewport stayed set, clipping the cursor. Full-width viewports at x 0 are no longer shifted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
129 lines
7.2 KiB
Markdown
129 lines
7.2 KiB
Markdown
# HUD layout in widened views (`src/mods/hud.cpp`)
|
||
|
||
The UI is laid out for 1024x768. In a widened render target (e.g. 1835x768)
|
||
the windows are moved as a whole, without touching their own coordinates:
|
||
the bottom bar and all dialogs are centred, the party panel (portrait, health)
|
||
sits at the right edge. Main menu and loading screen keep the classic 1024x768
|
||
frame (see `widescreen.md`).
|
||
|
||
## How a window is moved
|
||
|
||
A window's render and event methods are hooked (`HUD_RENDER`, `HUD_RENDER3`,
|
||
`HUD_EVENT`). While one of them runs on a thread, everything the window sees or
|
||
draws is translated by its offset `dx` (outermost window wins, children and
|
||
shared base methods are inside the scope):
|
||
|
||
| What | Where | Translation |
|
||
|---|---|---|
|
||
| screen-space vertices (`XYZRHW`) | `IDirect3DDevice7::DrawPrimitive` / `DrawIndexedPrimitive` (vtable 25 / 26) | x + dx |
|
||
| 3D items (belt, equipment) | ortho projection centred on the screen | none needed |
|
||
| mouse events | `cEvent_mouseMove` x at +4 (previous) and +0xc; button down / up x at +8 | x - dx, restored afterwards |
|
||
| mouse cursor object (`*0xcdbadc`, accessor `0x6550f0`) | x at +4, previous x at +0xc (polled for hover, item pick-up `0x6559a0`) | the accessor returns a per-thread copy with x - dx; dwords the window changed are written back afterwards |
|
||
| `GetMouseClientPos` (`0x66e500`) | polled position | x - dx |
|
||
|
||
## Calls outside a window's scope
|
||
|
||
- Tooltips (`cUI_Tooltip::vf14` `0x6e7300`) are separate windows that the
|
||
hovered window positions in its own layout: they draw with the offset of the
|
||
moved window under the cursor (noted while windows render, expires after
|
||
150 ms).
|
||
- Hit tests (`vf1c`: `0x731d40`, taskbar `0x6dfb10`) are also called by the UI
|
||
manager with plain render coordinates, e.g. "is UI under the cursor" for
|
||
world picking (`0x75a370`). They use the offset the top-level window (parent
|
||
chain at +0x50) was last drawn with.
|
||
- The item carried by the cursor (`0x6555e0`, drawn by the render loop) and the
|
||
manager's status line / message box (`0x75bbf0`, `0x75c200`, laid out around
|
||
x = 512) draw with the centre offset.
|
||
|
||
## UI events
|
||
|
||
`vf04` of an event is its category: 1 mouse move, 2 button down, 3 button up,
|
||
4 key (code at +8), 5 UI command (`cEventUI2`: id at +4, vf08 subtype). Only
|
||
mouse events carry positions; category 5 events with subtype 0x10 test a point
|
||
at +0x48 in `cUI_Window2::vf10`, but commands such as "open ESC menu" (0x11)
|
||
keep unrelated data there.
|
||
|
||
## Why the cursor is copied, not shifted
|
||
|
||
UI windows render on the render thread while world input (picking,
|
||
hold-to-move, `0x611f10`) reads the same cursor object on another thread.
|
||
Shifting the shared object for the duration of a window's scope let the world
|
||
sample a position ~400 px off now and then (jerky hold-to-move). Every user of
|
||
the cursor goes through the accessor `0x6550f0`, so inside a moved window it
|
||
returns a thread-local copy instead (with its own CRITICAL_SECTION at +0x24;
|
||
copying and writing back hold the real object's lock).
|
||
|
||
## Full-screen layers
|
||
|
||
A primitive drawn inside a moved window that covers the whole 1024x768 layout
|
||
(e.g. the dimming behind the exit confirmation) is stretched across the render
|
||
width instead of shifted.
|
||
|
||
## Standalone controls (quest dialog)
|
||
|
||
Some dialogs (e.g. accepting a quest) are not one of the hooked window
|
||
classes: their buttons and texts sit directly in the UI manager as top-level
|
||
controls (measured: the clicked button's root window was the cUI_Button2
|
||
itself). They were drawn unshifted or with whichever window's scope happened
|
||
to be active, so the dialog jumped between the 1024 layout and the centre and
|
||
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.
|
||
|
||
## Large map (M) across the render width
|
||
|
||
`mods/megamap.cpp`. cUI_Megamap (vtable 0x894714) is not moved like the other
|
||
windows (`Anchor::Screen`: drawn and clicked unshifted); its 1024x768 view is
|
||
widened instead, when the render size is applied (`PatchLayout`):
|
||
|
||
| What | Where | Game | Now |
|
||
|---|---|---|---|
|
||
| cursor at the right edge (edge scrolling, cursor shape) | 9 × `cmp reg, 3FFh` 0x6c7625..0x6c9e32 | 1023 | width − 1 |
|
||
| tiles right of the view skipped; labels flipped left | `cmp reg, 400h` 0x6c9aa3, 0x6ca2c6, 0x6ca2e5 | 1024 | width |
|
||
| largest scroll x = map width (+0xc0160, 2048) − view | `add eax, -400h` 0x6c9edd | −1024 | −width |
|
||
| view centre (zoom frame, centred image) | 0x6ca53f, 0x6ca637, 0x6ca645 | 0x200 | width / 2 |
|
||
| scroll centred on the hero; zoomed view; its quest markers | `[0x88e6fc]` (512.0) at 0x6c7bfe, 0x6cb27a, 0x6ccb7f, 0x6cf782 | 512 | own float |
|
||
| zoomed view: other markers at 768 − d (centre + 256) | `[0x890040]` (768.0) at 0x6ce156 | 768 | width / 2 + 256 |
|
||
| dark band, right outer / inner edge | vertex immediates 0x6ca774, 0x6ca808 / 0x6ca7bc, 0x6ca84c | 1024 / 954 | width / width − 70 |
|
||
|
||
The frame is child statics of the window (vector at +0x78..+0x7c; measured:
|
||
corners 41×44, 32 px tiles, left side x < 48, right side from x 976). On
|
||
Show (vf24 0x6c7930) the right side moves to the screen edge, the top and
|
||
bottom tiles stretch over the wider span, and the four controls the map holds
|
||
itself (+0xc0440, +0xc04bc statics, +0xc0544, +0xc0600 buttons: the zoom
|
||
controls, absolute positions) stay centred, and so does the rectangle around
|
||
them at +0xc0538: the map passes only clicks inside it to the buttons
|
||
(0x6c7f32). Each control is placed once (the
|
||
game lays them out at creation only).
|
||
|
||
A window anchored `Screen` still counts as the outermost scope for events, so
|
||
its child buttons (hooked as standalone controls, `Center`) don't shift again.
|
||
|
||
## Viewports (cursor in the quest book, 2026-10-17)
|
||
|
||
`SetViewport` inside a moved window is shifted like its vertices. The quest
|
||
book's map tab ("V") draws the zoomed map view (`0x6cc950`), which sets a
|
||
256x256 viewport and, when done, resets it to the full render size
|
||
(`DxDriver7` `0x646510`: 0, 0, width x height). Shifted by the window's
|
||
offset, that reset reached past the render target, `SetViewport` failed and
|
||
the small viewport stayed set: the cursor (drawn after the UI) was only
|
||
visible inside it. A viewport at x 0 across the whole render width is now
|
||
passed unshifted.
|