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) { rootInfo.Children = new List(children); return Build(rootInfo, resolve, datFont); } /// /// Pure builder: produce the widget tree from a fully resolved /// tree (children already attached). /// public static ImportedLayout Build( ElementInfo rootInfo, Func resolve, UiDatFont? datFont) { 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, 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, Dictionary byId) { var w = DatWidgetFactory.Create(info, resolve, datFont); if (w is null) return null; // Type-12 style prototype — skip if (info.Id != 0) byId[info.Id] = w; // Meters consume their own children: DatWidgetFactory already extracted the // slice-sprite ids from the grandchild image elements during UiMeter construction. // Adding those children as separate UiElement nodes would produce duplicate // geometry and wrong widget semantics. Every other element type recurses normally. if (w is not UiMeter) { foreach (var child in info.Children) { var cw = BuildWidget(child, resolve, datFont, 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; var tops = new List(); foreach (var kv in ld.Elements) tops.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>())); return tops.Count == 1 ? tops[0] : new ElementInfo { Id = 0, Type = 3, 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. /// public static ImportedLayout? Import( DatCollection dats, uint layoutId, Func resolve, UiDatFont? datFont) { var rootInfo = ImportInfos(dats, layoutId); if (rootInfo is null) return null; return Build(rootInfo, resolve, datFont); } // ── Inheritance resolution ──────────────────────────────────────────────── /// /// 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; // 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; result.Children is still empty here // — children are attached below from the DERIVED element's own tree. result = ElementReader.Merge(baseInfo, self); } } // 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) result.Children.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>())); 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, DefaultStateName = (defState is "Undef" or "Undefined" or "0") ? "" : defState, }; // DirectState (unnamed, key ""). if (d.StateDesc is not null) ReadState(d.StateDesc, "", info); // Named states (e.g. UIStateId.HideDetail → "HideDetail"). foreach (var s in d.States) ReadState(s.Value, s.Key.ToString(), info); return info; } /// /// Read the first from into /// info.StateMedia[name] and extract the font DID from property 0x1A /// (ArrayBaseProperty → DataIdBaseProperty) if not yet set. /// private static void ReadState(StateDesc sd, string name, ElementInfo info) { // Only MediaDescImage is read for rendering; MediaDescCursor items (on grips/drag bars) // are intentionally skipped — cursor behavior is Plan 2. foreach (var m in sd.Media) { if (m is MediaDescImage img && img.File != 0) { info.StateMedia[name] = (img.File, (int)img.DrawMode); break; } } // 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; } } // ── 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; } }