Files
sacred_k/docs/hud.md
T
khallaandClaude Opus 5.5 72525cd5ba Export screen: widescreen frame, black sides, slots can be cleared
- cUI_Character (export) and cUI_Savegame draw inside the centred 1024x768
  frame (viewport) with the render target beside it cleared black; the
  export screen's 3D heroes were stretched across the wide viewport, away
  from their labels and hit areas.
- Export screen shows the game's erase button for an occupied slot and
  reloads the heroes after a delete. Overwriting was already the game's
  own path once clicks hit the right slot.
- game/ui/control.h: shared show/hide for cUI_Control2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 23:26:53 +02:00

8.2 KiB
Raw Blame History

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.

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).