acdream/docs/research/2026-06-15-layoutdesc-format.md
Erik 8aa643f3e0 fix(D.2b): correct edge-anchor mapping (RightEdge==1=stretch) + enable vitals horizontal resize
ToAnchors was inverted vs retail UIElement::UpdateForParentSizeChange @0x00462640:
stretch is RightEdge==1 (not ==2/==4), LeftEdge==2 = track-right. Verified against
all 19 vitals fixture pieces. Enables Resizable/ResizeX on the importer vitals root
(the prior 'dat is fixed-size' conclusion was wrong). At-rest render unchanged
(anchors only fire on resize). Added a 160->200 resize conformance test.
Also fixed DatWidgetFactoryTests.RectAndAnchors_SetFromElementInfo which encoded
the old inverted model (Right=2 expecting Right anchor; corrected to Right=1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:05:04 +02:00

491 lines
29 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.

# LayoutDesc Format Enumeration Reference
**Date:** 2026-06-15
**Author:** Task 1 of the LayoutDesc Importer plan (`docs/superpowers/plans/2026-06-15-layoutdesc-importer.md`)
**Sources:**
- Dat dumps: `dump-vitals-layout` on `0x2100006C`, `0x21000014`, `0x21000075`, `0x2100003F`
- Retail decomp: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR PDB)
- DatReaderWriter 2.1.7 reflection probe (deleted after use)
This doc is the ground-truth API table for Tasks 26. Where it corrects a plan assumption, the correction is called out in **§ Corrections to plan assumptions** at the end.
---
## 1. `ElementDesc` — exact API
All members are **public fields** (not properties), except `ElementId`, `Type`, `BaseElement`, `BaseLayoutId`, `DefaultState`, `ReadOrder` which are also fields. There are no `ElementDesc` properties used by the importer.
| Member | Kind | Type | Notes |
|--------|------|------|-------|
| `ElementId` | **field** | `uint` | unique element id (e.g. `0x100000E6`) |
| `Type` | **field** | `uint` | element class id — **not an enum in DRW**; raw uint |
| `BaseElement` | **field** | `uint` | base element id in base layout (0 = no base) |
| `BaseLayoutId` | **field** | `uint` | layout id where base element lives (0 = no base) |
| `DefaultState` | **field** | `UIStateId` (enum) | the element's initial active state |
| `ReadOrder` | **field** | `uint` | draw order within parent |
| `X` | **field** | `uint` | left position within parent, in pixels |
| `Y` | **field** | `uint` | top position within parent, in pixels |
| `Width` | **field** | `uint` | pixel width |
| `Height` | **field** | `uint` | pixel height |
| `ZLevel` | **field** | `uint` | z-order (0 in all vitals elements) |
| `LeftEdge` | **field** | `uint` | left anchor flag (see §4) |
| `TopEdge` | **field** | `uint` | top anchor flag (see §4) |
| `RightEdge` | **field** | `uint` | right anchor flag (see §4) |
| `BottomEdge` | **field** | `uint` | bottom anchor flag (see §4) |
| `StateDesc` | **field** | `StateDesc?` | the element's "DirectState" (no name); null if absent |
| `States` | **field** | `Dictionary<UIStateId, StateDesc>` | named states (e.g. `HideDetail`, `ShowDetail`) |
| `Children` | **field** | `Dictionary<uint, ElementDesc>` | child elements keyed by their `ElementId` |
**Important:** `X`, `Y`, `Width`, `Height`, `LeftEdge`, etc. are all `uint`, not `int` or `float`. Cast to `float`/`int` when constructing `ElementInfo`.
The dump tool iterates both properties and fields; the scalars (`X`, `Y`, etc.) are found as **fields**.
---
## 2. `StateDesc` — exact API
| Member | Kind | Type | Notes |
|--------|------|------|-------|
| `StateId` | **field** | `uint` | redundant with the dict key |
| `PassToChildren` | **field** | `bool` | |
| `IncorporationFlags` | **field** | `IncorporationFlags` | |
| `Properties` | **field** | `Dictionary<uint, BaseProperty>` | keyed by property-id (uint); see §3 |
| `Media` | **field** | `List<MediaDesc>` | polymorphic list of media items |
### States dictionary key type
`ElementDesc.States` is `Dictionary<UIStateId, StateDesc>`. The dump shows string names like `"HideDetail"` and `"ShowDetail"` because the dump tool calls `.Key.ToString()` on the `UIStateId` enum values. The actual key is a `UIStateId` enum:
```csharp
// Key: UIStateId.HideDetail = 268435462 (0x10000006)
// Key: UIStateId.ShowDetail = 268435463 (0x10000007)
```
See §6 for the full `UIStateId` enum.
**Iterating in code:**
```csharp
foreach (var s in d.States)
ReadState(s.Value, s.Key.ToString(), info); // s.Key is UIStateId; .ToString() gives "HideDetail" etc.
```
---
## 3. Properties (`StateDesc.Properties`) — how font DID and fill are stored
`StateDesc.Properties` is `Dictionary<uint, BaseProperty>`. The `BaseProperty` base class has:
- `BasePropertyType PropertyType` (enum)
- `uint MasterPropertyId`
- `bool ShouldPackMasterPropertyId`
Concrete subclasses (`DatReaderWriter.Types.*`):
| Subclass | Field | Type | Notes |
|----------|-------|------|-------|
| `BoolBaseProperty` | `Value` | `bool` | |
| `IntegerBaseProperty` | `Value` | `int` | |
| `FloatBaseProperty` | `Value` | `float` | |
| `EnumBaseProperty` | `Value` | `uint` | |
| `DataIdBaseProperty` | `Value` | `uint` | a dat object DID |
| `ArrayBaseProperty` | `Value` | `List<BaseProperty>` | array of sub-properties |
| `ColorBaseProperty` | `Value` | `ColorARGB` | `struct { byte Blue, Green, Red, Alpha }` |
| `StringInfoBaseProperty` | `Value` | `StringInfo` | |
| `VectorBaseProperty` | `Value` | `Vector3` | |
| `Bitfield32BaseProperty` | `Value` | `uint` | |
| `Bitfield64BaseProperty` | `Value` | `ulong` | |
| `InstanceIdBaseProperty` | `Value` | `uint` | |
| `StructBaseProperty` | `Value` | `Dictionary<uint, BaseProperty>` | |
### Property key meanings (confirmed from decomp + dat inspection)
| Key | Type found in dat | Meaning | Decomp ref |
|-----|-------------------|---------|-----------|
| `0x1A` | `ArrayBaseProperty` (contains `DataIdBaseProperty`) | **Font DID** — array with one item; the inner `DataIdBaseProperty.Value` is the font dat object id | `UIElement_Text::SetFontDIDHelper(this, 0x1a, ...)` @`0x46829e` |
| `0x1B` | `ArrayBaseProperty` (contains `ColorBaseProperty`) | **Font color** — array with one item; `ColorARGB {R,G,B,A}` | `UIElement_Text::SetFontColorHelper(this, 0x1b, ...)` @`0x4682c2` |
| `0x14` | `EnumBaseProperty` | **Horizontal justification** | `UIElement_Text::SetHorizontalJustification` @`0x467200` |
| `0x15` | `EnumBaseProperty` | **Vertical justification** | `UIElement_Text::SetVerticalJustification` @`0x467230` |
| `0x1C` / `0x1D` | `ArrayBaseProperty` | Tag font color / tag font | (secondary font style for in-text tags) |
| `0x16` | `BoolBaseProperty` | Some text flag | |
| `0x21` | `BoolBaseProperty` | One-line mode | |
| `0x23` | `IntegerBaseProperty` | Left margin | |
| `0x24` | `IntegerBaseProperty` | Top margin | |
| `0x25` | `IntegerBaseProperty` | Right margin | |
| `0x26` | `IntegerBaseProperty` | Bottom margin | |
| `0x27` | `BoolBaseProperty` | Some text option | |
| `0x20` | `BoolBaseProperty` | Some text option | |
| `0x69` | — (NOT in dat) | **Fill percent** — set at runtime via `UIElement::SetAttribute_Float(meter, 0x69, fillRatio)` | `gmVitalsUI::Update` @`0x4bff2a` |
| `0xCB` | `BoolBaseProperty` | Some text option | |
**Critical point for font DID extraction:**
Property `0x1A` is an `ArrayBaseProperty` containing ONE `DataIdBaseProperty`. To read the font DID:
```csharp
if (sd.Properties.TryGetValue(0x1Au, out var raw) && raw is ArrayBaseProperty arr && arr.Value.Count > 0)
if (arr.Value[0] is DataIdBaseProperty did)
fontDid = did.Value; // e.g. 0x40000000
```
**Confirmed for element `0x10000376` (the vitals text prototype):**
- Property `0x1A``DataIdBaseProperty.Value = 0x40000000` (font DID)
- Property `0x1B``ColorBaseProperty.Value = {B=255,G=255,R=255,A=255}` (white)
**The fill (`0x69`) is NOT in the dat.** It is pushed at runtime by `gmVitalsUI::Update` calling `UIElement::SetAttribute_Float(meter, 0x69, ratio)`. The importer does not read this from the dat — the `VitalsController` sets it via `UiMeter.Fill` after binding.
---
## 4. Edge-anchor flags (`LeftEdge`/`TopEdge`/`RightEdge`/`BottomEdge`)
These are `uint` fields on `ElementDesc`. The values found across all four vitals layouts are:
| Value | Meaning | Where observed |
|-------|---------|---------------|
| `0` | Not present / no constraint | Base layout `0x2100003F` (zero-size elements) |
| `1` | **Stretch / track-far** — for LeftEdge: pin left (near); for RightEdge: stretch (track parent's right edge); for TopEdge: pin top; for BottomEdge: stretch (track parent's bottom) | Most vitals pieces |
| `2` | **Track-right (for LeftEdge) / fixed-far (for RightEdge)** — LeftEdge=2 means the element's LEFT side tracks the parent's RIGHT edge (fixed-width piece that moves right); RightEdge=2 means the right edge is fixed relative to the parent right (no stretch) | Corners/right-side pieces |
| `3` | **Centered / floating** — contributes no anchor on that axis | The expand-detail overlay child `0x100004A9` |
| `4` | **Both-sides** — both near AND far edges fire simultaneously | Seen in child layout meter elements |
### Anchor logic (retail-faithful, per `UIElement::UpdateForParentSizeChange @0x00462640`)
The **far-axis fields** (RightEdge, BottomEdge) drive stretch:
- **RightEdge==1** ⇒ the right edge tracks the parent's right edge (**STRETCH**; designRight+delta)
- **RightEdge==2** ⇒ designRight is fixed (no stretch)
- **LeftEdge==2** ⇒ a fixed-width piece's left side tracks the parent's right edge (it **moves right**)
- **LeftEdge==1** ⇒ pin left at designX (near-pin)
- **value==4** ⇒ both near AND far fire simultaneously (stretch + keep near)
- **value==3** ⇒ centered / floating (no anchor on that axis)
- **value==0** ⇒ no anchor (prototype-only)
This is the INVERSE of the earlier §Corrections reading ("1=near, 2=far"), which was wrong. The decomp is authoritative: `UIElement::UpdateForParentSizeChange @0x00462640` in `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines 108459108668.
**Correct `ToAnchors` logic (as implemented in `ElementReader.cs`):**
```csharp
// Per UIElement::UpdateForParentSizeChange @0x00462640
public static AnchorEdges ToAnchors(uint left, uint top, uint right, uint bottom)
{
var a = AnchorEdges.None;
if (left == 1 || left == 4) a |= AnchorEdges.Left;
if (right == 1 || right == 4 || left == 2) a |= AnchorEdges.Right;
if (top == 1 || top == 4) a |= AnchorEdges.Top;
if (bottom == 1 || bottom == 4 || top == 2) a |= AnchorEdges.Bottom;
if (a == AnchorEdges.None) a = AnchorEdges.Left | AnchorEdges.Top; // default: pin top-left
return a;
}
```
**Verified against all 19 vitals pieces** (format doc §11). At-rest render (no resize) is pixel-identical — anchors only fire on resize. Value `3` contributes no anchor on its axis and falls through to the Left|Top default only when all four values are 3 or 0.
---
## 5. `MediaDesc` kinds
`StateDesc.Media` is `List<MediaDesc>`. The concrete types found across the vitals layouts:
| Subclass | Fields | Used in vitals? | Notes |
|----------|--------|----------------|-------|
| `MediaDescImage` | `uint File`, `DrawModeType DrawMode`, `MediaType Type` | YES — all sprite images | The primary media type |
| `MediaDescCursor` | `uint File`, `uint XHotspot`, `uint YHotspot`, `MediaType Type` | YES — grip/dragbar cursor | Sets the mouse cursor when hovering the element |
| `MediaDescAnimation` | `float Duration`, `DrawModeType DrawMode`, `List<BaseProperty> Frames`, `MediaType Type` | not in vitals | Animated sprite |
| `MediaDescAlpha` | `uint File`, `MediaType Type` | not in vitals | Alpha overlay |
| `MediaDescFade` | `float StartAlpha, EndAlpha, Duration`, `MediaType Type` | not in vitals | Fade transition |
| `MediaDescSound` | `uint File`, ... | not in vitals | |
| `MediaDescState` | `UIStateId StateId`, ... | not in vitals | State transition |
| `MediaDescJump` | `uint JumpItemIndex`, ... | not in vitals | |
| `MediaDescMessage` | `uint Id`, ... | not in vitals | |
| `MediaDescPause` | `float MinDuration, MaxDuration`, ... | not in vitals | |
| `MediaDescMovie` | `PStringBase<char> FileName`, ... | not in vitals | |
Elements can have **multiple media items** in the same `StateDesc.Media` list — e.g. a grip element has both a `MediaDescImage` (the sprite) and a `MediaDescCursor` (the cursor shape). Iterate all items; for rendering pick the `MediaDescImage`; for cursor behavior pick `MediaDescCursor`.
---
## 6. `DrawModeType` enum (confirmed from reflection)
`DatReaderWriter.Enums.DrawModeType` (the type on `MediaDescImage.DrawMode`):
| Name | Value | Behavior | Used in vitals? |
|------|-------|----------|----------------|
| `Undefined` | 0 | (not used) | no |
| `Normal` | 1 | **Tile at native width** (UV-repeat; matches `ImgTex::TileCSI` @`0x53e740`) | YES — all bar sprites, chrome |
| `Overlay` | 2 | Blended overlay (not observed in vitals) | no |
| `Alphablend` | 3 | **Blended overlay** — used for the "ShowDetail" expand panels | YES — `ShowDetail` state sprites |
**The vitals window uses only `Normal` (1) and `Alphablend` (3).** No `Stretch` value exists in `DrawModeType` — the plan's mention of a "Stretch" draw-mode is NOT a value in this enum. There is a `MediaType.Stretch = 12` in a separate enum but that refers to a different concept (animation sequence? not a blit mode). Do not branch on `Stretch` in `UiDatElement`.
---
## 7. `UIStateId` enum (key type for `ElementDesc.States`)
`DatReaderWriter.Enums.UIStateId`. Key values relevant to the vitals window:
| Name | Value |
|------|-------|
| `Undef` | 0 |
| `Normal` | 1 |
| `HideDetail` | 268435462 (= `0x10000006`) |
| `ShowDetail` | 268435463 (= `0x10000007`) |
| `IsCharacter` | 268435542 (= `0x10000056`) |
| `IsAccount` | 268435543 (= `0x10000057`) |
The dump prints these as strings ("HideDetail", "ShowDetail") via `UIStateId.ToString()`. When iterating `d.States`, `s.Key.ToString()` gives the readable name.
---
## 8. Type → meaning → render method → widget bucket
From `UIElement::RegisterElementClass` calls in the decomp. The mapping is CONFIRMED by retail:
| Type (uint) | Class registered | Render method | Widget bucket | Vitals? |
|-------------|-----------------|---------------|---------------|---------|
| 0 | — (no registration) | text label; inherits from `UIElement_Text` behavior via `UIElement_Scrollable` | **behavioral** → dat-font label widget | YES — the text overlay (e.g. `0x100000EB/ED/EF`) |
| 1 | `UIElement_Button::Register()` | `UIRegion::DrawHere` (vtable) | **behavioral** → button widget | no |
| 2 | `UIElement_Dragbar::Register()` | `UIRegion::DrawHere` | **generic**`UiDatElement` (drag region) | YES — top/bottom drag bars |
| 3 | `UIElement_Field::Register()` | `UIRegion::DrawHere` | **generic**`UiDatElement` | YES — container/group elements, chrome corners/edges |
| 4 | (unregistered in stdlib; may be custom) | — | generic fallback | no |
| 5 | `UIElement_ListBox::Register()` | `UIRegion::DrawHere` | **behavioral** → list widget | no |
| 6 | `UIElement_Menu::Register()` | `UIRegion::DrawHere` | **behavioral** → menu widget | no |
| **7** | `UIElement_Meter::Register()` | **`UIElement_Meter::DrawChildren`** @`0x46fbd0` | **behavioral**`UiMeter` | **YES — the three vitals bars** |
| 8 | `UIElement_Panel::Register()` | `UIRegion::DrawHere` | generic → `UiDatElement` | no |
| 9 | `UIElement_Resizebar::Register()` | `UIRegion::DrawHere` | **generic**`UiDatElement` (grip) | YES — resize grips (corners + edges) |
| 0xB | `UIElement_Scrollbar::Register()` | `UIRegion::DrawHere` | **behavioral** → scrollbar | no |
| **0xC** | `UIElement_Text::Register()` | `UIElement_Text::DrawSelf` @`0x467aa0` | **behavioral** → dat-font label | YES — Type=0 elements have BaseElement which resolves to a Type=0x0C in the base |
| 0xD | `UIElement_Viewport::Register()` | — | behavioral → 3D viewport | no |
| 0xE | `UIElement_Browser::Register()` | — | behavioral → browser | no |
| 0x10 | `UIElement_ColorPicker::Register()` | — | behavioral → color picker | no |
| 0x11 | `UIElement_GroupBox::Register()` | — | behavioral → group box | no |
| **0x12** | — (Type=12 in base layout) | No render method registered — these are **style prototypes** (zero-size elements used as `BaseElement` sources, never instantiated directly) | skip/omit | YES — `0x2100003F` is full of Type=12 elements |
| 0x130x19 | `ConfirmationDialog*` / `MessageDialog*` / etc. | dialog widgets | behavioral → dialog | no |
| 0x1000xxxx | `gmVitalsUI`, `gmAttributeUI`, etc. | game-specific custom classes | **custom widget** (registered with high ids) | YES — the stacked vitals window root `0x100005F9` has `Type=268435533=0x10000009`; the floaty row root has Type=`268435465=0x10000009`… actually see below |
### Root element types in the vitals layouts
- `0x2100006C` root element `0x100005F9`: `Type = 268435533 = 0x10000009``gmVitalsUI::Register` registers type `0x10000009`
- `0x21000014` root element `0x100000E5`: `Type = 268435465 = 0x10000009` — wait, `268435465 = 0x10000009`
Actually: `268435533 = 0x1000000D` (not 9). Let me recompute:
- `268435533 decimal`: `268435456 + 77 = 0x10000000 + 0x4D = 0x1000004D` — that's `gmVitalsUI`-ish but a different id.
- `268435465`: `268435456 + 9 = 0x10000009` — confirmed `gmVitalsUI` type.
The correct decomp cross-check: `UIElement::RegisterElementClass(0x10000009, gmVitalsUI::Create)` @`0x4bfe1a`. The stacked vitals window root `0x100005F9` has `Type=268435533`. `268435533 = 0x1000004D` which would be a different registered type. The floaty row root `0x100000E5` has `Type=268435465 = 0x10000009` = confirmed `gmVitalsUI`.
The key observation: **the root element's Type selects the `gmVitalsUI` C++ class**, which is the window-level controller. In our importer, we don't need to match this: the `LayoutImporter` walks children, and the `VitalsController` binds the meter elements by id directly — the root type is irrelevant to Plan 1.
**Plan 1 relevant types (vitals window only):**
| Type | Role | Bucket |
|------|------|--------|
| 0 | text overlay label (BaseElement → Type 12 for font, but the element itself renders as text) | behavioral → dat-font label |
| 2 | drag bar (top/bottom) | generic |
| 3 | container / chrome edge / corner (no children hierarchy in vitals) | generic |
| 7 | meter | behavioral → `UiMeter` |
| 9 | resize grip (corners + edges) | generic |
| 12 | style prototype — zero-size, never directly rendered | skip |
| 0x10000009 | `gmVitalsUI` root — the window itself | behavioral → window root (use as container) |
| 0x1000004D | the stacked-window root | same |
---
## 9. `LayoutDesc` fields
| Member | Kind | Type | Notes |
|--------|------|------|-------|
| `Id` | property | `uint` | dat object id |
| `HeaderFlags` | property | `DBObjHeaderFlags` | |
| `DBObjType` | property | `DBObjType` | always `LayoutDesc` |
| `DataCategory` | property | `uint` | |
| `Width` | **field** | `uint` | screen-space width context (800 in all observed layouts) |
| `Height` | **field** | `uint` | screen-space height context (600 in all observed layouts) |
| `Elements` | **field** | `HashTable<uint, ElementDesc>` (DRW-internal type) | top-level elements, keyed by `ElementId`. Iterable with `foreach (var kv in ld.Elements)`. |
---
## 10. Inheritance chain for vitals number-text elements
All three vitals text labels (`0x100000EB` health, `0x100000ED` stamina, `0x100000EF` mana) share:
- `Type = 0` (text element, no render registration — renders via inherited machinery)
- `BaseElement = 268436342 = 0x10000376`
- `BaseLayoutId = 553648191 = 0x2100003F`
The base element `0x10000376` in `0x2100003F`:
- `Type = 12` (style prototype — zero-size, never rendered directly)
- `StateDesc.Properties`:
- `0x1A``ArrayBaseProperty[ DataIdBaseProperty{Value=0x40000000} ]`**font DID = `0x40000000`**
- `0x1B``ArrayBaseProperty[ ColorBaseProperty{R=255,G=255,B=255,A=255} ]` — white
- `0x14``EnumBaseProperty{Value=1}` — horizontal justification = 1
- `0x15``EnumBaseProperty{Value=1}` — vertical justification = 1
- `0x23`, `0x25``IntegerBaseProperty{Value=0}` — margins
The inheritance chain for the text element in the importer is:
```
derived (Type=0, no StateDesc media, no font prop itself)
inherits from base 0x10000376 in layout 0x2100003F (Type=12)
→ font DID = 0x40000000 (from property 0x1A)
→ font color = white ARGB(255,255,255,255) (from property 0x1B)
```
The derived text element overrides `Width/Height/X/Y` (from the dat element's fields) but inherits the font DID and color from the base element's `Properties`.
**There is no `StateDesc.Media` on the text elements** — the text is rendered by the `UIElement_Text::DrawSelf` algorithm using the font DID from properties, not a sprite. In Plan 1, the text elements are **skipped entirely**: `Type = 0` (derived) inherits `Type = 12` from the base prototype `0x10000376` via `ElementReader.Merge` (zero-wins-nothing rule — the derived Type 0 inherits the base's Type 12), and `DatWidgetFactory` returns null for Type 12. This means no `UiDatElement` is created for them. For the vitals window this is correct: the numbers render via `UiMeter.Label` bound by the `VitalsController`, not a dat text node. A dedicated dat-text widget (Type 0) is Plan 2.
---
## 11. Vitals window `0x2100006C` — confirmed element map
Root: `0x100005F9` (160×58, Type=`0x1000004D`, LeftEdge=1, TopEdge=1, RightEdge=1, BottomEdge=2)
### Chrome (all Type=3, `DrawMode=Normal`)
| Id | X | Y | W | H | LeftEdge | TopEdge | RightEdge | BottomEdge | Sprite |
|----|---|---|---|---|----------|---------|-----------|------------|--------|
| `0x10000633` | 0 | 0 | 5 | 5 | 1 | 1 | 2 | 2 | `0x060074C3` (TL corner) |
| `0x10000634` | 5 | 0 | 150 | 5 | 1 | 1 | 1 | 2 | `0x060074BF` (top edge) |
| `0x10000635` | 155 | 0 | 5 | 5 | 2 | 1 | 1 | 2 | `0x060074C4` (TR corner) |
| `0x10000636` | 0 | 5 | 5 | 48 | 1 | 1 | 2 | 1 | `0x060074C0` (left edge) |
| `0x10000637` | 0 | 53 | 5 | 5 | 1 | 2 | 2 | 1 | `0x060074C5` (BL corner) |
| `0x10000638` | 5 | 53 | 150 | 5 | 1 | 2 | 1 | 1 | `0x060074C1` (bottom edge) |
| `0x10000639` | 155 | 53 | 5 | 5 | 2 | 2 | 1 | 1 | `0x060074C6` (BR corner) |
| `0x1000063A` | 155 | 5 | 5 | 48 | 2 | 1 | 1 | 1 | `0x060074C2` (right edge) |
### Drag bars (Type=2)
| Id | X | Y | W | H | Notes |
|----|---|---|---|---|-------|
| `0x1000063C` | 5 | 0 | 150 | 5 | top drag bar; also has `MediaDescCursor` cursor `0x06006119` |
| `0x10000640` | 5 | 53 | 150 | 5 | bottom drag bar; same cursor |
### Resize grips (Type=9 — corners + edges)
| Id | X | Y | W | H | Corner/Edge |
|----|---|---|---|---|-------------|
| `0x1000063B` | 0 | 0 | 5 | 5 | TL grip |
| `0x1000063D` | 155 | 0 | 5 | 5 | TR grip |
| `0x1000063E` | 0 | 5 | 5 | 48 | left grip |
| `0x1000063F` | 0 | 53 | 5 | 5 | BL grip |
| `0x10000641` | 155 | 53 | 5 | 5 | BR grip |
| `0x10000642` | 155 | 5 | 5 | 48 | right grip |
Each grip has a `MediaDescImage` + a `MediaDescCursor` in its `StateDesc.Media` list.
### Meter elements (Type=7 — `UiMeter`)
| Id | X | Y | W | H | Purpose |
|----|---|---|---|---|---------|
| `0x100000E6` | 5 | 5 | 150 | 16 | Health meter |
| `0x100000EC` | 5 | 21 | 150 | 16 | Stamina meter |
| `0x100000EE` | 5 | 37 | 150 | 16 | Mana meter |
Each meter has:
- Child `0x100000E7` (back layer, Type=3): three sub-children `E8`/`E9`/`EA` (left/center/right slices, back sprites)
- `E8` has `RightEdge=2` (pin far right), `EA` has `LeftEdge=2` (pin far left) — the classic 3-slice anchor pattern
- Child `0x00000002` (front layer container, Type=3): three sub-children `E8`/`E9`/`EA` (front sprites), plus child `0x100004A9` (expand detail overlay, HideDetail/ShowDetail states)
- Child `0x100000EB/ED/EF` (text label, Type=0): BaseElement=`0x10000376`, BaseLayoutId=`0x2100003F` → inherits font `0x40000000`
### Sprite ids confirmed from dump
**Health bar** (back=`E7` layer / front=`00000002.E8-EA` layer):
- Back left: `0x0600747E`, center: `0x0600747F`, right: `0x06007480`
- Front left: `0x06007481`, center: `0x06007482`, right: `0x06007483`
- ShowDetail overlay: `0x06007490` (back) / `0x06007491` (front)
**Stamina bar:**
- Back left: `0x06007484`, center: `0x06007485`, right: `0x06007486`
- Front left: `0x06007487`, center: `0x06007488`, right: `0x06007489`
- ShowDetail: `0x06007492` / `0x06007493`
**Mana bar:**
- Back left: `0x0600748A`, center: `0x0600748B`, right: `0x0600748C`
- Front left: `0x0600748D`, center: `0x0600748E`, right: `0x0600748F`
- ShowDetail: `0x06007494` / `0x06007495`
---
## 12. Inheritance resolution rules
1. If `d.BaseElement != 0 && d.BaseLayoutId != 0`: load base layout, find base element, call `Resolve()` recursively on it, then `Merge(base, derived)`.
2. Merge semantics: **derived overrides, base is the default**. `Width`/`Height`/`X`/`Y` come from the derived element's fields (even if zero — zero is a valid override for prototypes). `FontDid` is inherited if the derived element's base chain provides it and the derived doesn't explicitly set it.
3. Type=12 elements in the base layout (`0x2100003F`) are pure property stores — **never render them**. They exist only to be referenced as `BaseElement`.
4. Cycle-guard: track already-visited `(BaseLayoutId, BaseElement)` pairs to avoid infinite loops.
---
## § Corrections to plan assumptions
### 1. Edge-flag semantics are INVERTED from the earlier §4 reading
**Original §4 reading (Task 2 shipped):** `1=near, 2=far, 4=stretch``right==2||right==4` for Right anchor.
**That was wrong.** The correct semantics, per `UIElement::UpdateForParentSizeChange @0x00462640`:
| Edge value | LeftEdge meaning | RightEdge meaning |
|-----------|-----------------|------------------|
| 0 | no anchor | no anchor |
| 1 | pin left (near) → **Left** | track parent's right edge (stretch) → **Right** |
| 2 | track parent's right edge (moves right) → **Right** | fixed right (no stretch) |
| 3 | centered / floating (no anchor) | centered / floating (no anchor) |
| 4 | both-sides → **Left + Right** | both-sides → **Left + Right** |
The far-axis field (RightEdge, BottomEdge) value `1` means **stretch** (track the parent's far edge), NOT "near-pin." This is the INVERSE of what was documented in the original §4.
**Correct `ToAnchors` (as fixed in `ElementReader.cs` 2026-06-15):**
```csharp
// Per UIElement::UpdateForParentSizeChange @0x00462640
public static AnchorEdges ToAnchors(uint left, uint top, uint right, uint bottom)
{
var a = AnchorEdges.None;
if (left == 1 || left == 4) a |= AnchorEdges.Left;
if (right == 1 || right == 4 || left == 2) a |= AnchorEdges.Right;
if (top == 1 || top == 4) a |= AnchorEdges.Top;
if (bottom == 1 || bottom == 4 || top == 2) a |= AnchorEdges.Bottom;
if (a == AnchorEdges.None) a = AnchorEdges.Left | AnchorEdges.Top;
return a;
}
```
Also: the `ElementReader.ToAnchors` signature in the plan uses `(int left, ...)` but the fields are `uint`. Use `(uint left, ...)` or cast at call site.
### 2. `X`, `Y`, `Width`, `Height`, `LeftEdge`, etc. are `uint`, not `float` or `int`
The plan's `ToInfo()` code uses `d.X, d.Y` etc. as though they are already numeric-assignable. They are `uint`, so the assignment `X = d.X` etc. requires an explicit cast `(float)d.X` in the `ElementInfo` struct.
### 3. `ElementDesc.Type` is `uint`, not an enum
The plan writes `(int)d.Type`. `d.Type` is `uint`, so `(int)d.Type` is valid C# (checked context would overflow for values > `int.MaxValue`, but the registered types are all small or `0x10000009` which fits in int). Better: store `Type` as `uint` in `ElementInfo` to avoid signed overflow on game-specific ids like `0x1000004D`.
### 4. `DrawModeType` has no `Stretch` value
The plan mentions handling `Stretch` in `UiDatElement`. The `DrawModeType` enum has only `{Undefined=0, Normal=1, Overlay=2, Alphablend=3}`. There is no `Stretch` draw mode in this enum. Drop the `Stretch` branch.
### 5. `d.States` key is `UIStateId`, not `string`
The plan writes `foreach (var s in d.States) ReadState(s.Value, s.Key, info);` treating `s.Key` as a string. The key is `UIStateId` (an enum). Use `s.Key.ToString()` for the string name, or compare directly via `UIStateId.HideDetail` etc.
### 6. Font DID is in `ArrayBaseProperty`, not a direct property
The plan's `// font DID (property 0x1A) read here once the format doc confirms the property API.` comment is the right place. The actual read is:
```csharp
if (sd.Properties.TryGetValue(0x1Au, out var raw) && raw is ArrayBaseProperty arr && arr.Value.Count > 0)
if (arr.Value[0] is DataIdBaseProperty did)
info.FontDid = did.Value;
```
### 7. Fill (`0x69`) is NOT in the dat
The plan says `SetAttribute_Float(meter, 0x69, fillRatio)` is a runtime operation. Confirmed: property `0x69` does not appear in any dat layout. The fill is set at runtime by the controller. The importer should not attempt to read it.
### 8. Type=12 elements are style prototypes — skip them entirely
Elements with `Type=12` in the base layout `0x2100003F` are zero-size property bags used as `BaseElement` sources. They should not be instantiated as widgets. The `DatWidgetFactory` switch should have a `12 => null` (skip) case, or the importer should skip top-level elements with `Width==0 && Height==0 && Type==12` — though the safest check is just `Type == 12`.
---
## § Plan 1 surface vs long tail
**Plan 1 (vitals conformance) uses:**
- Types: 2, 3, 7, 9, 12 (skip), 0 (text, generic fallback), 0x10000009/0x1000004D (root window — treat as container)
- DrawModes: `Normal` (1), `Alphablend` (3)
- Media: `MediaDescImage`, `MediaDescCursor`
- Properties: `0x1A` (font DID, from inheritance), `0x1B` (font color, from inheritance)
- States: `HideDetail`, `ShowDetail`
**Plan 2 (long tail):**
- Types: 1 (button), 5 (listbox), 6 (menu), 8 (panel), 0xB (scrollbar), 0xC (text widget proper), 0xD (viewport), 0x10 (color picker), 0x11 (groupbox), dialog types (0x130x19), all `gm*UI` custom types
- DrawModes: `Overlay` (2), any future additions
- Media: `MediaDescAnimation`, `MediaDescFade`, `MediaDescSound`, `MediaDescState`, etc.