docs(D.2b): LayoutDesc format enumeration (importer groundwork)
Resolves all 6 open unknowns for Tasks 2–6 of the LayoutDesc importer plan:
1. Edge-anchor flags: 1=near-pin, 2=far-pin, 3=float-center, 4=stretch.
The plan's assumption of 4="pinned to that side" is corrected — 1 is
the near-pin, 4 is stretch (both sides). Revised ToAnchors signature given.
2. ElementDesc members: all are public FIELDS (not properties). X/Y/Width/
Height/LeftEdge/etc. are uint. Type is uint (not enum). States is
Dictionary<UIStateId, StateDesc>. Children is Dictionary<uint, ElementDesc>.
3. StateDesc shape: Properties is Dictionary<uint, BaseProperty> with concrete
subclasses (ArrayBaseProperty, DataIdBaseProperty, IntegerBaseProperty, etc.).
Font DID (0x1A) is ArrayBaseProperty[ DataIdBaseProperty{Value=0x40000000} ].
Font color (0x1B) is ArrayBaseProperty[ ColorBaseProperty ]. Fill (0x69) is
NOT in the dat — pushed at runtime by gmVitalsUI::Update.
4. DrawModeType enum: Undefined=0, Normal=1, Overlay=2, Alphablend=3.
No "Stretch" value exists. Vitals uses Normal(1) and Alphablend(3) only.
5. Type values confirmed from RegisterElementClass: 3=Field/container,
7=Meter→UiMeter, 9=Resizebar, 0xC=Text, 2=Dragbar, 12=style prototype (skip).
6. Inheritance chain: vitals text labels (Type=0) inherit from base element
0x10000376 in layout 0x2100003F (Type=12), which carries font DID 0x40000000.
The full per-vital sprite id tables for 0x2100006C are confirmed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a7875cde22
commit
67819f35a4
1 changed files with 486 additions and 0 deletions
486
docs/research/2026-06-15-layoutdesc-format.md
Normal file
486
docs/research/2026-06-15-layoutdesc-format.md
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
# 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` | **Pinned to near edge** (left for LeftEdge, top for TopEdge) | Everywhere in vitals |
|
||||
| `2` | **Pinned to far edge** (right for LeftEdge, bottom for TopEdge) | Corners/bottom elements |
|
||||
| `3` | **Centered / pinned to both far edges** (floated, centered between two sides) | The expand-detail overlay child `0x100004A9` |
|
||||
| `4` | **Stretch / pinned to BOTH sides** | Meter elements in `0x21000014`/`0x21000075`; means the element stretches with parent resize |
|
||||
|
||||
### Anchor logic (correcting the plan's assumption)
|
||||
|
||||
**The plan assumed value `4` = "pinned to that side."** The correct semantics are:
|
||||
|
||||
- `1` = pinned to the **near** edge of that axis (left, or top)
|
||||
- `2` = pinned to the **far** edge (right, or bottom)
|
||||
- `3` = pinned to BOTH far edges (centered/floating between the two anchors on that axis)
|
||||
- `4` = stretch anchor: pinned to BOTH the near AND far edges simultaneously (element stretches)
|
||||
- `0` = no anchor (zero-size elements used as font/style prototypes in the base layout)
|
||||
|
||||
Evidence from the `0x21000014` dump: the health meter (`0x100000E6`) has `LeftEdge=1, RightEdge=4` meaning "pin left edge, stretch right" — the meter fills from the left to the window's right edge. The stamina meter (`0x100000EC`) has `LeftEdge=4, RightEdge=4` meaning it stretches on both sides (centered at 270px, fills width with parent).
|
||||
|
||||
**Revised `ToAnchors` logic:**
|
||||
```csharp
|
||||
public static AnchorEdges ToAnchors(uint left, uint top, uint right, uint bottom)
|
||||
{
|
||||
// 1 = near-pin, 2 = far-pin, 3 = both-far (floating center), 4 = stretch (both sides)
|
||||
var a = AnchorEdges.None;
|
||||
if (left == 1 || left == 4) a |= AnchorEdges.Left;
|
||||
if (top == 1 || top == 4) a |= AnchorEdges.Top;
|
||||
if (right == 2 || right == 4) a |= AnchorEdges.Right;
|
||||
if (bottom == 2 || bottom == 4) a |= AnchorEdges.Bottom;
|
||||
if (a == AnchorEdges.None) a = AnchorEdges.Left | AnchorEdges.Top; // default: pin top-left
|
||||
return a;
|
||||
}
|
||||
```
|
||||
Value `3` (floating center) is a "pin far but not near" on both axes — maps to Right+Bottom anchors but NOT Left+Top. This shows up only on the hide/show-detail overlay child (`0x100004A9`) which is visually centered in the bar.
|
||||
|
||||
---
|
||||
|
||||
## 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 render as `UiDatElement` (generic fallback) until a dedicated text widget is implemented in 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 "pinned" value is NOT simply `4`
|
||||
|
||||
**Plan assumed:** `if (left == 4) a |= AnchorEdges.Left;`
|
||||
**Correct semantics:**
|
||||
|
||||
| Edge value | Meaning |
|
||||
|-----------|---------|
|
||||
| 0 | no anchor (prototype-only elements) |
|
||||
| 1 | pinned to **near** edge (left/top) |
|
||||
| 2 | pinned to **far** edge (right/bottom) |
|
||||
| 3 | pinned to BOTH far edges (centered/floating) |
|
||||
| 4 | stretch: pinned to BOTH near AND far edges simultaneously |
|
||||
|
||||
**Fix for Task 2:**
|
||||
```csharp
|
||||
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 (top == 1 || top == 4) a |= AnchorEdges.Top;
|
||||
if (right == 2 || right == 4) a |= AnchorEdges.Right;
|
||||
if (bottom == 2 || bottom == 4) 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue