acdream/docs/research/2026-08-13-retail-ui-display-change.md
Erik 2153bee247 fix #390: retail display-change UI cascade — clamp + per-res reload
Decomp-first per the block's rule: the research doc
(docs/research/2026-08-13-retail-ui-display-change.md, committed here)
pulled retail's actual mechanism before any code. A display change runs
UIElementManager::RefreshEvent @0x0045C530 ->
UIElement::UpdateForParentSizeChange @0x00462640, which unconditionally
re-applies every floating window's own clamping MoveTo override
(x = max(0, min(x, parentW - selfW)) - top-left priority, oversized
windows pin to 0), then broadcasts global message 0xE whose sole
listener reloads the per-resolution auto layout. No proportional moves,
no resets; retail saves layouts only via @saveui.

Port: RetailWindowLayoutPersistence.ClampAllToScreen() is the cascade
clamp (no store I/O; _restoring suppresses the per-move save so a live
drag-resize cannot write settings.json per frame), and
RetailUiRuntime.Draw carries a two-step screen-size edge detector:
change frame -> clamp; first stable frame -> one
RestoreAll(saveBack:false) per-resolution reload (the 0xE analog; no
lazy save-back, matching retail's save-only-on-command). The login
restore path already used retail's exact clamp math (Apply) - the live
trigger was the missing half, which is precisely the stranding the user
reported.

Deliberate deviation, register AD-91: retail's gmFloatyChatUI windows
have NO clamp and can strand; the block's requirement ("UI windows must
stay reachable") clamps every registered window uniformly.

Tests: 5 new persistence facts (clamp/top-left-pin/no-move/no-save-on-
clamp/no-save-on-live-reload). App suite 4,967/3 skips. Gate script
section D3 filled in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:42:50 +02:00

384 lines
22 KiB
Markdown
Raw 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.

# Retail UI behavior on display-resolution change (research for #390)
**Date:** 2026-08-13
**Oracle:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR build,
PDB-named), cross-checked against the raw PDB-paired binary
(`C:\Users\erikn\Downloads\acclient.exe`, v11.4186) where the BN export lacked bodies.
All addresses are the 2013 build's VAs.
**Question:** what does the retail client do to its UI windows when the display
resolution changes, so floating windows never end up stranded off-screen?
(acdream's floating retail-UI windows keep absolute pixel positions across resolution
changes; shrinking strands them — user gate report 2026-08-13.)
---
## Executive summary
Retail has **three cooperating mechanisms**, and all three matter:
1. **A per-element re-anchoring cascade** that runs on every parent resize.
`UIElement::ResizeTo` iterates its children and calls
`UIElement::UpdateForParentSizeChange` on each; that function recomputes the child's
box from its **authored ElementDesc position**, its four **edge-anchor modes**
(`m_leftEdge`/`m_rightEdge`/`m_topEdge`/`m_bottomEdge`), and the delta between the
**authored reference resolution stored in the LayoutDesc**
(`LayoutDesc::m_displayWidth/m_displayHeight`) and the actual display size — then
applies the result through the element's **virtual** `MoveTo` + `ResizeTo`.
2. **A clamp-to-parent rule baked into every floating window's `MoveTo` override**:
`x = max(0, min(x, parentW - selfW))`, `y = max(0, min(y, parentH - selfH))`.
Because the cascade above ends in an *unconditional* virtual `MoveTo`, the clamp runs
for every floating window on every resolution change — even when nothing else would
have moved it. This is the mechanism that un-strands windows on shrink.
3. **Per-resolution layout persistence**: a plain-text auto-layout file keyed by
**character + world + resolution** (`UI-<char>-<world>-<W>-<H>.txt`), reloaded
automatically after every resolution change via a dedicated global UI message (0xE),
plus a server-side fallback (PlayerModule "chat window options") applied through the
same clamping `MoveTo`.
There is **no proportional rescaling of window positions** and **no reset-to-default on
display change** (defaults only apply when an element's authored anchors say so).
Positions persist per-resolution; the clamp is the safety net.
---
## 1. Trigger paths into a resolution change
| Path | Function | Address | Notes |
|---|---|---|---|
| In-game Options change | `Render::UpdateFromPreferences` | 0x0054D850 | Diffs `Device::m_DisplayPrefs.Resolution/FullScreen/RefreshRate/SyncToRefresh/Antialiasing` against `Current_Display_*` statics; any diff → `Device::ChangePresentation()` at 0x0054DA29. |
| Console command | `Device::ConsoleCommand_ForceDisplayResolution` | 0x0043A830 | `ForceDisplayResolution [w h]``Device::ForceDisplayResolution` (0x0043A750) → `ChangePresentation` when the size actually differs. |
| Login/char-select ↔ gameplay | `gmGamePlayUI::gmGamePlayUI` / `~gmGamePlayUI` | 0x004EA010 / 0x004EA2A0 | Pre-gameplay screens run force-800x600 (`ForceDisplayResolution(1, 0x320, 0x258)`); the gameplay ctor un-forces (0x004EA093) → switch to the user's preferred resolution; the dtor re-forces (0x004EA353). |
| Device lost (alt-tab etc.) | `Render::CheckForLostDevice` | 0x0054E890 | Same-resolution `Render::RestartRenderingSystem()`; runs the UI refresh cascade below but does **not** broadcast 0xE (no layout reload). |
## 2. `Device::ChangePresentation` (0x0043A2D0) — the device-side sequence
1. `Device::LoadDisplayPreferences` — read the desired presentation (the
`Display.Resolution` preference packs W/H into one dword: `W = res >> 16`,
`H = res & 0xFFFF`, see 0x00439070).
2. Strip/refresh window styles (`SetWindowLongA`, `SetWindowPos`).
3. **`Render::RestartRenderingSystem(presentation, config)`** (0x0054D6B0):
`Render::RestartDevice` + `GraphicsResource::RestoreLostResources()` + fire every
registered **RGR callback** ("restore graphics resources", `Render::LinkRGRCallback`
0x0054F000). The UI's callback is described in §3.
4. Compute the new OS window rect (windowed mode): client size + dialog-frame/caption
metrics (`GetSystemMetrics`), centered on the previous window rect (or the screen),
then **clamped into the desktop work area** (`SystemParametersInfoA(SPI_GETWORKAREA)`)
— left/top-priority clamp, 0x0043A459..0x0043A4A9. `SetWindowPos` applies it.
5. On success: **`UIElementManager::BroadcastGlobalMessage(0xE, 0)`** (0x0043A4E6).
On failure: fatal error dialog + `exit(1)`.
Note the ordering: the UI refresh cascade (§3) runs **inside** step 3; the 0xE
broadcast (§5's layout reload) happens **after** it.
## 3. The UI refresh cascade — `UIElementManager::RefreshEvent` (0x0045C530)
`UIElementManager::Init` (0x0045EE10) registers `RefreshEvent_g` as an RGR callback at
0x0045EEB9. When the device restarts:
1. `BroadcastGlobalMessage(5, 0)` — no registrant of global message 5 was found in this
build (see UNKNOWNs).
2. **`m_pRootElement->vtable->ResizeTo(GetDisplayWidth(), GetDisplayHeight())`** — the
manager's root ("hollow element", a plain full-screen `UIElement` created at display
size by `CreateHollowElement` 0x0045D0E0) is resized to the new display size.
3. `UIRegion::ForceUpdate(root, 7)`, `DrawDirtyRegions`, cursor re-set.
### 3a. `UIElement::ResizeTo` (0x00463C30) cascades to children
After clamping the requested size against the element's min/max attributes
(attr ids 0x3C..0x3F) and updating its own box, **if the size actually changed** it
walks `m_children` and calls **`UIElement::UpdateForParentSizeChange(child)`**
(loop at 0x00463E25..0x00463E52). All UI pages are under the manager root (mouse
hit-testing recurses from `m_pRootElement`), so a display change reflows the whole tree.
`UIRegion::MoveTo` (0x0069F830) and `UIRegion::ResizeTo` (0x0069F8C0) themselves are
pure box math — x0/y0/x1/y1 updates plus dirty-region invalidation, **no children, no
clamping** (verified by disassembling the PDB-paired binary; the BN export lacks these
two bodies).
### 3b. `UIElement::UpdateForParentSizeChange` (0x00462640) — the actual math
Inputs per element:
- `GetOriginalPosition(this)` — the **authored** ElementDesc box
(origX0, origY0, origX1, origY1) + authored z-level.
- The **old reference frame**:
- Root-level element (no parent, or `__inner23` bit 21 set — set by
`SetIsRootElement`): the LayoutDesc's authored reference resolution —
old frame = (0,0)..(`m_layout->m_displayWidth - 1`, `m_layout->m_displayHeight - 1`).
**Retail .uil LayoutDescs embed the resolution they were authored at**
(fields at LayoutDesc+0x30/+0x34, serialized at 0x0069A4C3/0x0069A4EB).
- Otherwise: the parent's **original** box (`GetOriginalPosition(parent)`).
- The **new reference frame**:
- Root-level: (0,0)..(displayW-1, displayH-1) from the live `RenderDevice`.
- Otherwise: the parent's **current** box (`GetCurrentPosition(parent)`).
Derived:
```
deltaW = (newFrameWidth - oldFrameWidth) // ebx_6 at 0x00462766
deltaH = (newFrameHeight - oldFrameHeight) // ecx_12 at 0x00462762
scale = newFrameWidth / oldFrameWidth // float, 0x00462794 (mode-4 only)
```
Per-edge application (values are the raw `m_desc.m_leftEdge` etc. — verbatim retail
field names from the PDB):
| edge value | left/top coordinate | right/bottom coordinate |
|---|---|---|
| 0 | authored; **but if the element already has a size: keep the CURRENT runtime coordinate** (0x00462959..) — this is the "free-floating window" mode | same rule (keep current x1/y1) |
| 1 | authored (left/top-anchored) | `orig + delta` (right/bottom-anchored — stretches with the frame) |
| 2 | `orig + delta` (right/bottom-anchored) | authored |
| 3 | centered: `(newSize>>1) - (origSize>>1)` | centered: `(newSize>>1) + (origSize>>1) - 1` |
| 4 | proportional: `ftol(scale * ...)` (exact float expr elided by BN) | proportional |
Finally — and this is the load-bearing part:
```
this->vtable->MoveTo(newX0, newY0); // 0x00462998 — VIRTUAL
this->vtable->ResizeTo(newX1-newX0+1, newY1-newY0+1); // 0x004629A1 — VIRTUAL
```
`MoveTo` is called **unconditionally** — even for a free-floating (all-edges-0) window
whose coordinates come out unchanged. The virtual dispatch lands in the floating-window
overrides below, so **the clamp runs for every floating window on every display change**.
## 4. The clamp rule — floating-window `MoveTo` overrides
Every "floaty" window class overrides `MoveTo` with the same exact math:
```
if (GetParent() != null) {
if (x > parentWidth - selfWidth) x = parentWidth - selfWidth;
if (x < 0) x = 0;
if (y > parentHeight - selfHeight) y = parentHeight - selfHeight;
if (y < 0) y = 0;
}
UIElement::MoveTo(x, y);
if (m_eWindowID != 0 && PlayerSystem exists)
write clamped X/Y into PlayerModule chat-window options // see §5b
```
Order of the two clamps means **top-left wins** when the window is larger than the
parent (x first clamped possibly negative, then floored to 0).
Clamping classes (all confirmed in the pseudo-C):
| class | MoveTo addr |
|---|---|
| `gmFloatyToolbarUI` | 0x004CFE80 |
| `gmFloatyPowerBarUI` | 0x004D15D0 |
| `gmFloatyPanelUI` (the tabbed side panel host) | 0x004D20B0 |
| `gmFloatyMainChatUI` (main chat when floating) | 0x004D2D10 |
| `gmFloatyIndicatorsUI` | 0x004D38C0 |
| `gmFloatyExaminationUI` | 0x004D45E0 |
| `gmFloatyCombatPanelUI` | 0x004D5190 |
| `gmSmartBoxUI` (the 3D viewport window) | 0x004D67E0 |
| `gmRadarUI` | 0x004D81A0 |
**NOT clamping** (verified):
- `gmMainChatUI::MoveTo` (0x004CCCC0) — docked main chat; persists dock offsets into
element attributes 0x54/0x55, no clamp.
- `gmFloatyChatUI::MoveTo` (0x004CE840) — the extra floating chat windows (FCH1FCH4
in the layout file): persists X/Y to PlayerModule but does **not** clamp. Retail's
floating chat windows 14 genuinely can sit off-screen; only the auto-layout file or
a re-anchor can recover them. (Retail quirk — see "what acdream should do".)
- Base `UIElement::MoveTo` (0x004633E0): no positional clamp at all.
- `UIElement::ResizeTo` clamps only against min/max size attributes (0x3C..0x3F),
never against the screen.
The same overrides are the **drag** path (title-bar drag ends in the virtual `MoveTo`),
so interactive dragging obeys the same bounds — drag clamping and display-change
clamping are literally the same code.
## 5. Per-resolution layout persistence
### 5a. The screen-layout file (gmGamePlayUI)
`gmGamePlayUI` is the **only** registrant of global message 0xE
(`RegisterForGlobalMessage(this, 0xE)` at 0x004EA0B1). Its handler
(`ListenToGlobalMessage` 0x004EB5A0):
```
case 0xE: if (!m_endingSession) LoadScreenLayout("#auto");
```
`CreateScreenLayoutPath` (0x004EA690) builds the file path in the **directory of the
user-preferences file** (`PSUtils::get_directory(UserPreferences::sm_strDefaultFile)`):
| name argument | format (string addr) | result |
|---|---|---|
| `"#auto"` (0x007C2A3C) | `"%sUI-%s-%s-%d-%d.txt"` (0x007C2A24) | `UI-<charName>-<worldName>-<W>-<H>.txt` where W/H = `UIRegion::GetWidth/GetHeight(m_pGameplayUI)` — the gameplay page's CURRENT size == the display resolution. **Layouts are keyed per character + world + resolution.** |
| empty | `"%sUI-Default.txt"` (0x007C2A10) | shared default file |
| anything else | `"%s%s.txt"` (0x007C2A04) | named file (`@saveui <name>`) |
`SaveScreenLayout` (0x004EAD50) writes one text line per movable window:
`<TAG> X:%d Y: %d W: %d H: %d` (screen-space X0/Y0 + width/height), for exactly 16
elements. `LoadScreenLayout` (0x004EA8F0) reads lines with
`sscanf("%s X:%d Y: %d W: %d H: %d")` (0x007C2AC4), maps the 6-char tag to an element
id, then applies **`ResizeTo(W,H)` first, `MoveTo(X,Y)` second** (0x004EAC8E) — both
virtual, so the §4 clamp applies to everything loaded. If the file does not exist the
function returns 0 and touches nothing.
Tags (strings at 0x007C2A44..0x007C2ABC): `<SBOX>` `<CHAT>` `<FCH1>`..`<FCH4>`
`<EXAM>` `<VITS>` `<SVIT>` `<ENVP>` `<PANS>` `<TBAR>` `<INDI>` `<PBAR>` `<COMB>`
`<RADA>`. Element ids in save order: 0x1000049A (smartbox), 0x10000601 (main chat),
0x10000505/0x1000050E/0x1000050F/0x10000510 (floaty chat 14), 0x100005F7,
0x100005FA (stacked vitals), 0x100006D5 (side-by-side vitals), 0x100005FD, 0x100005FF,
0x10000603, 0x10000611, 0x10000613, 0x100006B5, 0x100006D2 (radar).
Load triggers:
- **`RecvNotice_PlayerDescReceived`** (0x004EB660) — login: `LoadScreenLayout("#auto")`;
the boolean result is stored in `CPlayerSystem::m_layoutFromFile`.
- **Global message 0xE** — every resolution change (this is the "layout follows the
resolution" behavior the `@saveautoui` help text describes).
- `@loadui [<name>]` / `@loadautoui` (handlers `ClientCommunicationSystem::DoLoadUI`
0x00570150; `saveautoui`/`loadautoui` registered at 0x00584FBD/0x00585029).
Save triggers: **only** `@saveui [<name>]` (`DoSaveUI` 0x0056FFF0 →
`CM_UI::SendNotice_SaveUI``gmGamePlayUI::RecvNotice_SaveUI` 0x004EB600) and
`@saveautoui` (passes `"#auto"`). **Retail never auto-saves the layout file** — not on
exit, not on drag. Ambient persistence is PlayerModule's job (§5b).
Help text (verbatim, data 0x007DCB90): *"@saveautoui - Stores the current layout to a
character and resolution specific file. This layout will automatically be used when the
resolution changes for this character to the current size."*
### 5b. Server-side fallback — PlayerModule chat-window options
Every floaty window with a nonzero `m_eWindowID` writes its (clamped) geometry into the
PlayerModule on every move/resize via `PlayerModule::SetChatWindowOption`:
| property | id |
|---|---|
| X | 0x10000086 |
| Y | 0x10000087 |
| W | 0x10000088 |
| H | 0x10000089 |
This rides the PlayerModule blob (the same `0x01A1` save-path Campaign OP ported).
On login / player-option notices, each floaty's `UpdateFromPlayerModule` (e.g.
`gmFloatyVitalsUI` 0x004CF140, called from the tail of each floaty's setup and from
notice 0x4DD1F0 handlers) applies the stored geometry — **but only when
`CPlayerSystem::m_layoutFromFile == 0`** (the auto-layout file wins when it exists) —
again via the virtual, clamping `MoveTo`/`ResizeTo`.
So the restore priority is: **per-resolution auto file > PlayerModule blob > authored
LayoutDesc defaults**, and every path funnels through the clamp.
## 6. So what exactly happens to a stranded window?
Scenario: 1600x1200 → 1024x768, window at (1400, 900).
1. `ChangePresentation` restarts the device; `RefreshEvent` resizes the manager root to
1024x768; `UIElement::ResizeTo` cascades `UpdateForParentSizeChange` down the tree.
2. The gameplay page (a root element) re-derives its box from its authored anchors
against (authored LayoutDesc resolution → 1024x768) and shrinks; its own `ResizeTo`
cascades to its children — the floating windows.
3. Each floating window's anchors are typically "free" (edge mode 0 → keep current
coordinates), so the re-derivation yields its old (1400, 900) — but the result is
applied through the window's clamping `MoveTo`, which pins it to
`(1024 - w, 768 - h)`. **Clamp on display-change broadcast, not on load, and not a
reset to default.** The clamped position is immediately written back to PlayerModule.
4. *Then* the 0xE broadcast fires and, **if** `UI-<char>-<world>-1024-768.txt` exists,
the user's saved 1024x768 layout overrides the clamped positions (ResizeTo before
MoveTo, both clamped again).
5. Exception: the floating chat windows FCH1FCH4 (`gmFloatyChatUI`) skip the clamp and
genuinely can stay stranded in retail.
No file, no PlayerModule entry → the window keeps its authored-anchor-derived position,
clamped. There is no proportional reposition of free-floating windows (mode 4 exists in
the anchor system but retail's floating windows don't use it — they'd move on every
change otherwise).
## 7. What acdream's port should do (mechanism only — no code here)
1. **Keep per-resolution keying** in `RetailWindowLayoutPersistence` — retail keys the
auto layout by character + world + resolution. If we key only by resolution, that is
a (small) divergence worth a register row; retail's key includes character and world.
2. **Port the clamp into the floating-window move seam** (the equivalent of the
`MoveTo` override): `x = max(0, min(x, parentW - selfW))`, `y` likewise, top-left
priority, applied on *every* programmatic or interactive move — restore-from-disk,
restore-from-server, drag, and display-change reflow must all funnel through it.
3. **On display change, run the reflow then the reload, in that order**: (a) resize the
UI root to the new display size and propagate a parent-size-change pass that ends in
the clamping move for every floating window; (b) then load the per-resolution saved
layout if one exists and apply it size-first, position-second, through the same
clamping move.
4. **Do not reset to authored defaults on display change** and **do not scale positions
proportionally** — retail does neither for free-floating windows.
5. Apply-order detail worth copying: retail applies `ResizeTo(W,H)` **before**
`MoveTo(X,Y)` when restoring a window, so the clamp evaluates against the restored
size, not the stale one.
6. Decision point (flag for the user): retail's floating chat windows FCH1FCH4 do NOT
clamp and can be stranded. Matching that exactly is retail-faithful; clamping them
too is a deliberate quality divergence that needs a register row.
7. The anchor system (edge modes + LayoutDesc authored reference resolution) is the
general mechanism that keeps docked/authored HUD elements correct across resolutions.
If acdream's importer currently bakes absolute pixel positions at import time, the
full fix for authored (non-floating) elements is the `UpdateForParentSizeChange`
equivalent — worth its own slice; the floating-window stranding fix only needs
items 15.
## 8. UNKNOWNs / ambiguities
- **Global message 5** (broadcast by `RefreshEvent`): no `RegisterForGlobalMessage(_, 5)`
site found; presumed a no-op in the 2013 build. (Message ids seen in use: 1 keypress,
3 per-frame UseTime, 0xB, 0xC, 0xD = UI-lock toggled (`@lockui`
`UpdateLockedStatus`), 0xE display-changed.)
- **Exact tag↔element-id mapping** for the ten middle entries of the layout file: the
per-tag format-string operands are BN-mislabeled vtable-relative constants. SBOX,
CHAT, FCH14 are confirmed by adjacent data; RADA=0x100006D2 (radar), VITS=0x100005FA
(stacked vitals) and SVIT=0x100006D5 (side-by-side vitals) are high-confidence from
`SetupChildren` member names; TBAR/PBAR/PANS/INDI/EXAM/COMB/ENVP assignments to
0x100005F7/0x100005FD/0x100005FF/0x10000603/0x10000611/0x10000613/0x100006B5 are
inferred from class names, not proven.
- **Mode-4 (proportional) exact float expression** in `UpdateForParentSizeChange`: BN
elided the x87 sequence (`_ftol2` with prior float state). The scale factor
`newFrameW/oldFrameW` is visible; whether Y uses an independent Y-scale was not
recoverable from the export (almost certainly yes by symmetry).
- **`__inner23` bit 21** ("use display frame instead of parent frame" in
`UpdateForParentSizeChange`) is inferred to be the `SetIsRootElement` flag; the bit
arithmetic is consistent but the setter's exact bit position was not independently
confirmed.
- **`UIElement::SetSaveLocation`/`SetSaveSize`** (LayoutDesc attrs 0x12/0x13, setters
0x0045F930/0x0045F950 → `__inner23` bits 4/5): no named consumer found; presumed
vestigial or consumed by unlabeled code. They are NOT the persistence mechanism —
that is the hardcoded 16-window list + PlayerModule.
- **`@saveautoui` handler** passing `"#auto"` is inferred from the `#auto` string, the
`DoSaveAutoUI` neighborhood (0x005702C5..0x005702FA) and the help text; the exact
call site line was not read end-to-end.
- The order of `%s` args (char vs world) in `UI-%s-%s-%d-%d.txt` was not provable from
the BN export's local-variable listing; both names and both dimensions are in the
filename, which is what matters for the port.
## 9. Key address appendix
| Function | Address |
|---|---|
| `Device::ForceDisplayResolution` | 0x0043A750 |
| `Device::ChangePresentation` | 0x0043A2D0 (0xE broadcast at 0x0043A4E6; work-area clamp 0x0043A459) |
| `Render::UpdateFromPreferences` | 0x0054D850 (→ ChangePresentation 0x0054DA29) |
| `Render::RestartRenderingSystem` | 0x0054D6B0 / wrapper 0x0054D710 |
| `Render::CheckForLostDevice` | 0x0054E890 |
| `Render::LinkRGRCallback` | 0x0054F000 |
| `UIElementManager::Init` | 0x0045EE10 (RGR hook 0x0045EEB9) |
| `UIElementManager::RefreshEvent` | 0x0045C530 |
| `UIElementManager::CreateHollowElement` | 0x0045D0E0 |
| `UIElement::UpdateForParentSizeChange` | 0x00462640 |
| `UIElement::MoveTo` / `ResizeTo` | 0x004633E0 / 0x00463C30 (child cascade 0x00463E25) |
| `UIElement::SetParent` (also triggers re-anchor) | 0x00462A50 |
| `UIRegion::MoveTo` / `ResizeTo` (box math only; from binary disasm) | 0x0069F830 / 0x0069F8C0 |
| `gmGamePlayUI` ctor / dtor | 0x004EA010 / 0x004EA2A0 |
| `gmGamePlayUI::ListenToGlobalMessage` | 0x004EB5A0 |
| `gmGamePlayUI::CreateScreenLayoutPath` | 0x004EA690 |
| `gmGamePlayUI::LoadScreenLayout` | 0x004EA8F0 (apply site 0x004EAC8E) |
| `gmGamePlayUI::SaveScreenLayout` | 0x004EAD50 |
| `gmGamePlayUI::RecvNotice_PlayerDescReceived` (`m_layoutFromFile`) | 0x004EB660 |
| `ClientCommunicationSystem::DoSaveUI` / `DoLoadUI` | 0x0056FFF0 / 0x00570150 |
| Clamping `MoveTo` overrides | see table in §4 |
| `gmFloatyVitalsUI::UpdateFromPlayerModule` (restore pattern) | 0x004CF140 |
| Path/tag/format strings | 0x007C2A04..0x007C2CC0 |
| `@saveautoui` help text | 0x007DCB90 |