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

29 KiB
Raw Blame History

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:

// Key: UIStateId.HideDetail = 268435462 (0x10000006)
// Key: UIStateId.ShowDetail = 268435463 (0x10000007)

See §6 for the full UIStateId enum.

Iterating in code:

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:

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 0x1ADataIdBaseProperty.Value = 0x40000000 (font DID)
  • Property 0x1BColorBaseProperty.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):

// 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 genericUiDatElement (drag region) YES — top/bottom drag bars
3 UIElement_Field::Register() UIRegion::DrawHere genericUiDatElement 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 behavioralUiMeter YES — the three vitals bars
8 UIElement_Panel::Register() UIRegion::DrawHere generic → UiDatElement no
9 UIElement_Resizebar::Register() UIRegion::DrawHere genericUiDatElement (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 = 0x10000009gmVitalsUI::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:
    • 0x1AArrayBaseProperty[ DataIdBaseProperty{Value=0x40000000} ]font DID = 0x40000000
    • 0x1BArrayBaseProperty[ ColorBaseProperty{R=255,G=255,B=255,A=255} ] — white
    • 0x14EnumBaseProperty{Value=1} — horizontal justification = 1
    • 0x15EnumBaseProperty{Value=1} — vertical justification = 1
    • 0x23, 0x25IntegerBaseProperty{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=stretchright==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):

// 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:

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.