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>
491 lines
29 KiB
Markdown
491 lines
29 KiB
Markdown
# 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 2–6. 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 108459–108668.
|
||
|
||
**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 |
|
||
| 0x13–0x19 | `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 (0x13–0x19), all `gm*UI` custom types
|
||
- DrawModes: `Overlay` (2), any future additions
|
||
- Media: `MediaDescAnimation`, `MediaDescFade`, `MediaDescSound`, `MediaDescState`, etc.
|