using System;
using System.Collections.Generic;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
namespace AcDream.App.UI.Layout;
///
/// The result of importing a retail LayoutDesc: a tree with
/// an O(1) lookup table for finding any element by its dat id.
///
public sealed class ImportedLayout
{
/// Root widget of the imported tree.
public UiElement Root { get; }
private readonly Dictionary _byId;
public ImportedLayout(UiElement root, Dictionary byId)
{
Root = root;
_byId = byId;
}
/// Find a widget by its dat element id (e.g. 0x100000E6).
/// Returns null if the id was skipped (Type-12 prototype) or not present.
public UiElement? FindElement(uint id)
=> _byId.TryGetValue(id, out var e) ? e : null;
}
///
/// Two-layer layout importer for retail LayoutDesc dat objects.
///
///
/// Pure layer ( / ):
/// converts a pre-resolved tree into a
/// tree via . Testable without dats or OpenGL — all tests
/// in LayoutImporterTests.cs exercise this layer only.
///
///
///
/// Dat shell (): reads a ,
/// converts each top-level to a fully resolved
/// (applying BaseElement / BaseLayoutId
/// inheritance with a cycle guard), then delegates to .
///
///
///
/// Meter elements (Type 7) consume their own dat-children:
/// reads the grandchild slice-sprite ids during construction, so the
/// children must NOT be added as separate nodes in the tree.
/// Every other element type recurses its children generically.
///
///
public static class LayoutImporter
{
// ── Pure layer ────────────────────────────────────────────────────────────
///
/// Convenience for tests: attach to
/// , then call .
/// The children list is set directly on ;
/// any existing children are replaced.
///
public static ImportedLayout BuildFromInfos(
ElementInfo rootInfo,
IEnumerable children,
Func resolve,
UiDatFont? datFont,
Func? fontResolve = null)
{
rootInfo.Children = new List(children);
return Build(rootInfo, resolve, datFont, fontResolve);
}
///
/// Pure builder: produce the widget tree from a fully resolved
/// tree (children already attached).
///
/// Optional per-element font resolver — FontDid →
/// (or null if the font can't be loaded). When supplied,
/// elements with a non-zero get their own dat
/// font at build time instead of the shared fallback.
/// Null preserves the original single-font behavior for all callers that don't
/// pass it — no behavior change for the live game path.
public static ImportedLayout Build(
ElementInfo rootInfo,
Func resolve,
UiDatFont? datFont,
Func? fontResolve = null)
{
var byId = new Dictionary();
// Root is never a Type-12 prototype in practice; fall back to a generic
// container if the factory returns null for an exotic root type.
var root = BuildWidget(rootInfo, resolve, datFont, fontResolve, byId);
if (root is null)
{
Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback.");
root = new UiDatElement(rootInfo, resolve);
}
return new ImportedLayout(root, byId);
}
private static UiElement? BuildWidget(
ElementInfo info,
Func resolve,
UiDatFont? datFont,
Func? fontResolve,
Dictionary byId)
{
var w = DatWidgetFactory.Create(info, resolve, datFont, fontResolve);
if (w is null) return null; // Type-12 style prototype — skip
if (info.Id != 0) byId[info.Id] = w;
// Behavioral widgets that draw their full appearance + reproduce their dat
// sub-elements procedurally (Meter's 3-slice, Menu's label/rows, Field/Text caps,
// Button labels, Scrollbar arrows) CONSUME their dat children — building those as
// separate widgets double-draws and lets an invisible child steal pointer/focus
// from the behavioral widget (e.g. the channel Menu's label child intercepting the
// button click). Only generic containers (UiDatElement, panels) recurse. See
// UiElement.ConsumesDatChildren.
if (!w.ConsumesDatChildren)
{
foreach (var child in info.Children)
{
var cw = BuildWidget(child, resolve, datFont, fontResolve, byId);
if (cw is not null) w.AddChild(cw);
}
}
else if (w is UiMeter)
{
// Fix 5: UiMeter.ConsumesDatChildren=true swallows ALL children, including text
// label/value overlays that are separate renderable widgets (not part of the bar
// art). BuildMeter in DatWidgetFactory already consumed the Type-3 slice containers
// (reads their grandchild sprite ids to populate Back*/Front* properties). The
// remaining non-Type-3 children (typically Type-12 UIElement_Text overlays such
// as the XP meter's 0x10000237 label + 0x10000238 value) ARE renderable and belong
// in the widget tree. We build them here explicitly, registered in byId so
// FindElement can locate them, and attached as children of the meter so they render
// as overlays at their dat-local coordinates. The controller can then locate these
// widgets via FindElement and bind LinesProvider without injecting new runtime nodes.
//
// Type-3 children are SKIPPED here because BuildMeter already consumed them (they
// carry the 3-slice sprite ids, not text content; building them again would
// double-draw the bar art). All other child types are built normally.
//
// Safe for vitals: the health/stamina/mana meters have ONLY Type-3 slice children
// (no text children). This loop finds nothing for them → no change to vitals.
foreach (var child in info.Children)
{
if (child.Type == 3) continue; // slice containers: already consumed by BuildMeter
var cw = BuildWidget(child, resolve, datFont, fontResolve, byId);
if (cw is not null) w.AddChild(cw);
}
}
return w;
}
// ── Dat shell ─────────────────────────────────────────────────────────────
///
/// Dat shell, ElementInfo half: load the layout + resolve inheritance + build the
/// ElementInfo tree (no widgets). Exposed for fixture generation + conformance tests.
/// Returns null if the layout is missing.
///
/// The dat collection to read the LayoutDesc from.
/// The LayoutDesc dat id to read.
public static ElementInfo? ImportInfos(DatCollection dats, uint layoutId)
{
var ld = dats.Get(layoutId);
if (ld is null) return null;
// Collect the set of element ids that are referenced as a BaseElement by ANY
// element in THIS layout (where BaseLayoutId == layoutId). Such elements are
// purely inheritance templates ("prototypes") — retail never instantiates them
// as live widgets. Example: the toolbar slot prototype 0x100001B2 in LayoutDesc
// 0x21000016, which all 18 slot elements inherit from and which has no own media.
//
// NOTE: the Resolve path reads BaseElement from the raw dat directly (via
// dats.Get), so the prototype never needs to appear in the built
// widget tree for inheritance to work. Skipping it here is safe.
var referencedAsBase = new HashSet();
foreach (var kv in ld.Elements)
CollectBaseRefsInDesc(kv.Value, layoutId, referencedAsBase);
var tops = new List();
foreach (var kv in ld.Elements)
{
// Skip pure prototype elements: top-level elements that are referenced as a
// base template by another element in this same layout AND have no own state
// media (so they draw nothing and contribute nothing but their inherited shape).
var d = kv.Value;
if (referencedAsBase.Contains(d.ElementId) && HasNoOwnMedia(d))
{
Console.WriteLine($"[D.2b] LayoutImporter: skipping prototype element 0x{d.ElementId:X8} in layout 0x{layoutId:X8} (no own media, referenced as BaseElement).");
continue;
}
tops.Add(Resolve(dats, d, new HashSet<(uint, uint)>()));
}
if (tops.Count == 1)
return tops[0];
foreach (var top in tops)
SetOriginalParentSize(top, ld.Width, ld.Height);
return new ElementInfo
{
Id = 0,
Type = 3,
Width = ld.Width,
Height = ld.Height,
Children = tops,
};
}
///
/// Dat shell: load the LayoutDesc, resolve inheritance for every top-level
/// element, and build the widget tree. Returns null if the layout is absent
/// from the dats.
///
///
/// Dat UIState visibility model (2026-06-26 audit):
/// The dat's ElementDesc.DefaultState field specifies which SPRITE/MEDIA
/// state an element starts in (e.g., Normal, Minimized). It does
/// NOT encode visibility of sibling Group containers. The StateDesc's
/// contains X/Y/Width/Height/
/// ZLevel/PassToChildren — there is no Visible flag.
///
///
///
/// Windows that display multiple sibling Group containers at the same position
/// (the character footer's three state-groups; the tab-page content areas) manage
/// visibility purely at runtime via C++ controller code. Retail uses
/// UIElement::SetState(stateId) on the parent to propagate state, then
/// C++ getters access the right sub-group by element id. All groups are shipped
/// as visible in the imported widget tree; the relevant controllers
/// () perform the initial show/hide.
///
///
/// Optional per-element font resolver (see
/// for details). Null = original single-font behavior.
public static ImportedLayout? Import(
DatCollection dats,
uint layoutId,
Func resolve,
UiDatFont? datFont,
Func? fontResolve = null)
{
var rootInfo = ImportInfos(dats, layoutId);
if (rootInfo is null) return null;
return Build(rootInfo, resolve, datFont, fontResolve);
}
// ── Inheritance resolution ────────────────────────────────────────────────
/// True when a pure-container leaf should inherit its base's subtree (the
/// gmInventoryUI sub-window mount): the derived element has no own children, no own
/// state media, and the base resolved to ≥1 child. Inert for media-bearing inheritors
/// (close button / title), elements with their own children, and childless style
/// prototypes (vitals/chat/toolbar text — the base prototype has no children).
internal static bool ShouldMountBaseChildren(int derivedChildCount, int derivedMediaCount, int baseChildCount)
=> derivedChildCount == 0 && derivedMediaCount == 0 && baseChildCount > 0;
///
/// Converts an to a resolved :
/// reads own fields + media, applies the BaseElement / BaseLayoutId chain
/// (cycle-guarded by ), then resolves + attaches children.
///
private static ElementInfo Resolve(
DatCollection dats,
ElementDesc d,
HashSet<(uint layoutId, uint elementId)> baseChain)
{
// Read this element's own fields + media (no inheritance, no children yet).
var self = ToInfo(d);
var result = self;
List? baseChildren = null;
// Apply BaseElement / BaseLayoutId inheritance if present.
if (d.BaseElement != 0 && d.BaseLayoutId != 0
&& baseChain.Add((d.BaseLayoutId, d.BaseElement)))
{
var baseLd = dats.Get(d.BaseLayoutId);
var baseDesc = baseLd is null ? null : FindDesc(baseLd, d.BaseElement);
if (baseDesc is not null)
{
// Recurse the base chain (already guarded by the HashSet add above).
var baseInfo = Resolve(dats, baseDesc, baseChain);
// Derived fields override the base; children are attached below.
result = ElementReader.Merge(baseInfo, self);
baseChildren = baseInfo.Children; // capture for the sub-window mount
}
}
// Resolve + attach children. Each child gets a FRESH base-chain set:
// the cycle guard is per-element, not shared across siblings.
foreach (var kv in d.Children)
{
var child = Resolve(dats, kv.Value, new HashSet<(uint, uint)>());
SetOriginalParentSize(child, result.Width, result.Height);
result.Children.Add(child);
}
// Sub-window mount: a pure-container leaf (no own children, no own media) that inherits
// from a base WITH content attaches the base's subtree. Targets the gmInventoryUI panels
// (0x100001CD/CE/CF — Type-0, media-less, childless, BaseLayoutId → a gm*UI window);
// inert for media-bearing inheritors (close button/title) and childless style prototypes
// (vitals/chat/toolbar). See ShouldMountBaseChildren + the B-Grid spec.
if (baseChildren is not null
&& ShouldMountBaseChildren(d.Children.Count, self.StateMedia.Count, baseChildren.Count))
{
result.Children.AddRange(baseChildren);
// The mounted slot's layer WITHIN THE FRAME is its OWN ZLevel, not the mounted
// sub-window root's. The gm*UI sub-window roots carry ZLevel 1000 (their standalone
// top-window layer); ElementReader.Merge's zero-wins-base rule made the slot (own
// ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000)
// turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's
// Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the
// panel's captions/meter/cells (the wash-out bug; the paperdoll root happens to be
// ZLevel 0 so it escaped). Restore the slot's own frame-layer so the panel sits in
// FRONT of the backdrop. (B-Controller debug 2026-06-21; continuation of #145.)
result.ZLevel = self.ZLevel;
// Mounted descendants retain the base layout's design parent size. The runtime
// UiLayoutPolicy therefore performs the retail parent-size update from the correct
// baseline when this slot is larger than the mounted layout.
}
return result;
}
///
/// Read an 's own scalar fields + state media into a
/// fresh . No inheritance is applied; children are not
/// attached (the caller handles those).
///
private static ElementInfo ToInfo(ElementDesc d)
{
// Normalize DefaultState: UIStateId.ToString() gives "Undef"/"Undefined" or "0" when
// no default is set; map those to "" so UiDatElement treats them as "no preference".
var defState = d.DefaultState.ToString();
var info = new ElementInfo
{
Id = d.ElementId,
Type = d.Type,
X = (float)d.X,
Y = (float)d.Y,
Width = (float)d.Width,
Height = (float)d.Height,
Left = d.LeftEdge,
Top = d.TopEdge,
Right = d.RightEdge,
Bottom = d.BottomEdge,
ReadOrder = d.ReadOrder,
ZLevel = d.ZLevel,
DefaultStateId = (uint)d.DefaultState,
DefaultStateName = (defState is "Undef" or "Undefined" or "0") ? "" : defState,
};
// DirectState (unnamed, key "").
if (d.StateDesc is not null)
ReadState(d.StateDesc, UiStateInfo.DirectStateId, "", info);
// Named states (e.g. UIStateId.HideDetail → "HideDetail").
foreach (var s in d.States)
ReadState(s.Value, (uint)s.Key, s.Key.ToString(), info);
ElementReader.ApplyCanonicalLegacyProjection(info);
return info;
}
///
/// Read the first from into
/// info.StateMedia[name], read any into
/// info.StateCursors[name], and extract the font DID from property 0x1A
/// (ArrayBaseProperty → DataIdBaseProperty) if not yet set.
///
private static void ReadState(StateDesc sd, uint stateId, string name, ElementInfo info)
{
var state = new UiStateInfo
{
Id = stateId,
Name = name,
PassToChildren = sd.PassToChildren,
IncorporationFlags = (uint)sd.IncorporationFlags,
};
bool imageRead = false;
foreach (var m in sd.Media)
{
if (!imageRead && m is MediaDescImage img && img.File != 0)
{
info.StateMedia[name] = (img.File, (int)img.DrawMode);
state.Image = new UiImageMedia(img.File, (int)img.DrawMode);
imageRead = true;
}
if (m is MediaDescCursor cursor && cursor.File != 0)
{
info.StateCursors[name] = new UiCursorMedia(
cursor.File,
checked((int)cursor.XHotspot),
checked((int)cursor.YHotspot));
state.Cursor = info.StateCursors[name];
}
}
if (sd.Properties is not null)
{
foreach (var (propertyId, property) in sd.Properties)
state.Properties.Values[propertyId] = ConvertProperty(property);
}
info.States[stateId] = state;
// Font DID: Properties[0x1A] is ArrayBaseProperty{ DataIdBaseProperty }.
// Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty".
if (info.FontDid == 0 && sd.Properties is not null
&& sd.Properties.TryGetValue(0x1Au, out var raw)
&& raw is ArrayBaseProperty arr && arr.Value.Count > 0
&& arr.Value[0] is DataIdBaseProperty did)
{
info.FontDid = did.Value;
}
if (sd.Properties is not null)
{
// HorizontalJustification (0x14): EnumBaseProperty.
// Retail values: 0=Left, 1=Center, 3=Right, 5=Right (treat 5 as Right per spec).
// Only update if still at the default (Center); derived-wins handled in Merge.
if (info.HJustify == HJustify.Center
&& sd.Properties.TryGetValue(0x14u, out var hRaw)
&& hRaw is EnumBaseProperty hEnum)
{
info.HJustify = hEnum.Value switch
{
0u => HJustify.Left,
1u => HJustify.Center,
3u => HJustify.Right,
5u => HJustify.Right,
_ => HJustify.Center, // unknown → center (safe default)
};
}
// VerticalJustification (0x15): EnumBaseProperty.
// Retail values: 2=Top, 4=Bottom; absent/other = Center.
if (info.VJustify == VJustify.Center
&& sd.Properties.TryGetValue(0x15u, out var vRaw)
&& vRaw is EnumBaseProperty vEnum)
{
info.VJustify = vEnum.Value switch
{
2u => VJustify.Top,
4u => VJustify.Bottom,
_ => VJustify.Center,
};
}
// ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A).
// Only read when not already set (first dat state wins; Merge propagates from base).
if (info.FontColor is null
&& sd.Properties.TryGetValue(0x1Bu, out var cRaw)
&& cRaw is ColorBaseProperty cProp)
{
var c = cProp.Value;
// ColorARGB stores components as bytes (0–255); normalize to [0,1] for Vector4.
// Alpha=0 in the dat typically means fully opaque (retail convention: 0 → 255).
float a = c.Alpha == 0 ? 1f : c.Alpha / 255f;
info.FontColor = new System.Numerics.Vector4(c.Red / 255f, c.Green / 255f, c.Blue / 255f, a);
}
}
}
internal static UiPropertyValue ConvertProperty(BaseProperty property)
{
var value = new UiPropertyValue { MasterPropertyId = property.MasterPropertyId };
switch (property)
{
case EnumBaseProperty p:
value.Kind = UiPropertyKind.Enum;
value.UnsignedValue = p.Value;
break;
case BoolBaseProperty p:
value.Kind = UiPropertyKind.Bool;
value.BoolValue = p.Value;
break;
case DataIdBaseProperty p:
value.Kind = UiPropertyKind.DataId;
value.UnsignedValue = p.Value;
break;
case FloatBaseProperty p:
value.Kind = UiPropertyKind.Float;
value.FloatValue = p.Value;
break;
case IntegerBaseProperty p:
value.Kind = UiPropertyKind.Integer;
value.IntegerValue = p.Value;
break;
case StringInfoBaseProperty p:
value.Kind = UiPropertyKind.StringInfo;
value.StringInfoValue = new UiStringInfoValue(
p.Value.Token,
p.Value.StringId,
p.Value.TableId.DataId,
(byte)p.Value.Override,
p.Value.English,
p.Value.Comment);
break;
case ColorBaseProperty p:
value.Kind = UiPropertyKind.Color;
value.ColorValue = new UiColorValue(
p.Value.Blue,
p.Value.Green,
p.Value.Red,
p.Value.Alpha);
break;
case ArrayBaseProperty p:
value.Kind = UiPropertyKind.Array;
foreach (var item in p.Value)
value.ArrayValue.Add(ConvertProperty(item));
break;
case StructBaseProperty p:
value.Kind = UiPropertyKind.Struct;
foreach (var (key, item) in p.Value)
value.StructValue[key] = ConvertProperty(item);
break;
case VectorBaseProperty p:
value.Kind = UiPropertyKind.Vector;
value.VectorValue = p.Value;
break;
case Bitfield32BaseProperty p:
value.Kind = UiPropertyKind.Bitfield32;
value.UnsignedValue = p.Value;
break;
case Bitfield64BaseProperty p:
value.Kind = UiPropertyKind.Bitfield64;
value.UnsignedValue = p.Value;
break;
case InstanceIdBaseProperty p:
value.Kind = UiPropertyKind.InstanceId;
value.UnsignedValue = p.Value;
break;
default:
throw new NotSupportedException($"Unsupported UI base-property type {property.GetType().FullName}.");
}
return value;
}
// ── Prototype detection helpers ───────────────────────────────────────────
///
/// Recursively walks and all its children, adding to
/// the BaseElement of every descriptor that
/// references this layout (BaseLayoutId == layoutId). Used by
/// to identify pure prototype/template elements that
/// should not be instantiated as live widgets.
///
private static void CollectBaseRefsInDesc(ElementDesc d, uint layoutId, HashSet result)
{
if (d.BaseElement != 0 && d.BaseLayoutId == layoutId)
result.Add(d.BaseElement);
foreach (var kv in d.Children)
CollectBaseRefsInDesc(kv.Value, layoutId, result);
}
///
/// Returns true when carries no own state media — i.e. its
/// StateDesc (DirectState) and States (named states) yield no
/// entries with a non-zero file id.
/// Such elements are pure inheritance templates with no rendering content.
///
private static bool HasNoOwnMedia(ElementDesc d)
{
// Re-use ToInfo's media extraction: if the resulting StateMedia is empty the
// element has no renderable image in any state.
var info = ToInfo(d);
return info.StateMedia.Count == 0;
}
// ── Element tree search ───────────────────────────────────────────────────
///
/// Find an by id anywhere in the top-level tree of
/// (depth-first). Returns null if not found.
///
private static ElementDesc? FindDesc(LayoutDesc ld, uint id)
{
foreach (var kv in ld.Elements)
{
var f = FindDescIn(kv.Value, id);
if (f is not null) return f;
}
return null;
}
private static ElementDesc? FindDescIn(ElementDesc d, uint id)
{
if (d.ElementId == id) return d;
foreach (var kv in d.Children)
{
var f = FindDescIn(kv.Value, id);
if (f is not null) return f;
}
return null;
}
// ── Raw-edge layout provenance ────────────────────────────────────────────
private static void SetOriginalParentSize(ElementInfo child, float width, float height)
{
child.OriginalParentWidth = width;
child.OriginalParentHeight = height;
child.HasOriginalParentSize = true;
}
}