Files
khallaandClaude Opus 5.5 c7d54a6cfd Megamap: the zoom button zooms on the hero and follows him
The zoom button never set the zoom point, so it zoomed on the last
magnifier point (or 0,0). While the button's zoom is on, the point is put on
the hero's map pixel every engine tick; the held mouse button still zooms on
the mouse. game::UiWindow / UiWindowShown shared from windows.h.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 11:54:59 +01:00

160 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |
| zoomed view: heroes' markers, 6 px quad at d + 509 / 515 (2026-10-25) | `[0x894780]` (509.0) at 0x6cf1bd, `[0x894778]` (515.0) at 0x6cf1dd | 512 ± 3 | centre ± 3 |
| zoomed view: other symbols (quad, icons, labels) as integers (2026-10-25) | mov / add reg, 200h at 0x6ced82, 0x6cedb0, 0x6cf47a, 0x6cf4ac, 0x6cf5e4 | 0x200 | centre, rewritten when it changes |
| 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.
### Zoom on the hero (2026-10-25)
Reported: the zoomed view did not start centred on the hero. Read: the map
zooms on its zoom point +0xc0438 / +0xc043c (screen) plus the scroll
+0xc016c / +0xc0170 (render 0x6ca4d6). Holding the mouse button on the map
(+0xc06bc, 0x6c8274) moves the point with the mouse (outside the zoom
controls, 0x6ca47b); the zoom button (flag 0x100 in +0xc0154, 0x6c7fac)
never sets it, so it zoomed on the last held point, or 0,0 after the map was
created. Now, while the button zoom is on and the mouse is not held, every
engine tick puts the point on the hero: map pixel by 0x6cb130 (thiscall
(out, world x/y/level floats), ret 8; as opening the map centres on the hero,
0x6c7bad..) minus the scroll. The held zoom still follows the mouse.
## 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.
## Full-screen windows: frame viewport and black sides (2026-10-18)
The character export screen (Esc menu "Exportieren", cUI_Character) and the
save game window (cUI_Savegame) cover the 1024 frame with a picture. Request:
black instead of the world beside it, and the export screen adapted to the
wide view. Measured on a screenshot: the export screen's 3D scene (heroes on
their stones) is projected for a 1024x768 viewport; across the widened one
it was stretched 1.8x wide, away from the slot labels and their hit areas.
Their render hooks (`HUD_RENDER_FRAMED`, `HUD_RENDER3_FRAMED`) run the window
inside `FrameScope`: while the window is visible, the render target left and
right of the centred frame is cleared black, then the viewport is set to the
frame (dx, 0, 1024x768) and restored afterwards. `Clear` clips to the
viewport, so the sides are cleared first. Both calls go to the unhooked
device methods (`IDirect3DDevice7` 10 Clear, 13 SetViewport, 15 GetViewport).