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>
384 lines
22 KiB
Markdown
384 lines
22 KiB
Markdown
# 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 (FCH1–FCH4
|
||
in the layout file): persists X/Y to PlayerModule but does **not** clamp. Retail's
|
||
floating chat windows 1–4 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 1–4), 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 FCH1–FCH4 (`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 FCH1–FCH4 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 1–5.
|
||
|
||
## 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, FCH1–4 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 |
|