using System.Collections.Generic; namespace AcDream.App.UI.Layout; /// /// GL-free, dat-free snapshot of a resolved layout element. /// Populated by the LayoutDesc importer from DatReaderWriter.ElementDesc /// after inheritance is applied. The pure transforms on /// operate on this type so they can be unit-tested without the dats or OpenGL. /// /// IMPORTANT: Tasks 3–6 depend on this shape exactly. Do not add members without /// updating the plan spec and downstream consumers. /// public sealed class ElementInfo { /// Dat element id (e.g. 0x100000E6). public uint Id; /// /// Raw element class id as a uint. /// Game-specific ids like 0x1000004D (gmVitalsUI root) and 0x10000009 /// overflow int when treated as signed, so this stays uint. /// Known values: 0=text, 2=dragbar, 3=container/chrome, 7=meter, /// 9=resize-grip, 12=style-prototype (skip), 0x10000009/0x1000004D=window root. /// public uint Type; /// Position and size within the parent, in pixels (cast from dat uint fields). public float X, Y, Width, Height; /// /// Raw edge-anchor flag values from the dat (LeftEdge, TopEdge, /// RightEdge, BottomEdge fields of ElementDesc). /// Values 0–4; map to bit-flags via /// . /// public uint Left, Top, Right, Bottom; /// Draw order within the parent (lower = drawn first / behind). public uint ReadOrder; /// /// Font dat object id inherited from the base element's Properties[0x1A] /// (ArrayBaseProperty → DataIdBaseProperty). 0 = none / not inherited. /// public uint FontDid; /// /// Sprite per state: state name → (RenderSurface file id, DrawMode int). /// The "" key represents the unnamed DirectState (ElementDesc.StateDesc). /// Named states use the UIStateId.ToString() value as the key /// (e.g. "HideDetail", "ShowDetail"). /// public Dictionary StateMedia = new(); /// /// The element's initial active state name, taken from ElementDesc.DefaultState.ToString(). /// Normalized to "" when the dat carries Undef/Undefined/0 (no default set). /// Used by to pick which state's sprite to render initially. /// Examples: "Normal" (Send button), "Minimized" (max/min button), "" (DirectState). /// public string DefaultStateName = ""; /// /// Resolved child elements (populated by the importer in Task 5). /// Children come from the derived element's own tree, not the base element's. /// public List Children = new(); } /// /// Pure, GL-free, dat-free transforms for the LayoutDesc importer. /// All methods are static and operate on POCOs. /// No OpenGL, no DatReaderWriter types, no rendering dependencies beyond /// the bit-flag enum from AcDream.App.UI. /// public static class ElementReader { /// Edge-anchor flags → AnchorEdges, per retail UIElement::UpdateForParentSizeChange /// @0x00462640. The far-axis fields drive stretch: RightEdge==1 ⇒ the right edge tracks the /// parent's right edge (stretch); LeftEdge==2 ⇒ a fixed-width element's left tracks the right /// edge (it moves right). ==4 (not present in the vitals layout) = both-sides stretch; ==3 = /// centered (no edge anchor → falls back to pin-top-left). This is the INVERSE of the earlier /// format-doc §4 reading, which was wrong (it made every piece fixed-width). /// LeftEdge dat field value (0–4). /// TopEdge dat field value (0–4). /// RightEdge dat field value (0–4). /// BottomEdge dat field value (0–4). 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; } /// /// Merges a base element snapshot with a derived element snapshot, mirroring /// the BaseElement / BaseLayoutId inheritance chain in the dat. /// /// /// Rules: /// /// /// Scalar fields (, , /// , , /// ): derived wins if non-zero; otherwise /// inherited from base. /// /// /// Position (, ) and /// edge flags ( etc.) and /// : always taken from the derived element /// (derived placement, not the base prototype's geometry). /// /// /// : base entries are the default; derived /// entries override (or add) per state name key. /// /// /// : come from the derived element's own tree only. /// /// /// /// public static ElementInfo Merge(ElementInfo base_, ElementInfo derived) { var m = new ElementInfo { Id = derived.Id != 0 ? derived.Id : base_.Id, // Type: derived wins if non-zero; Type 0 (text element per format §8) inherits the base's Type. // For a text element whose base prototype is Type 12 (style prototype), this yields Type 12 — // which DatWidgetFactory skips (returns null). That is intentional for Plan 1: vitals text // numbers render via UiMeter.Label bound by VitalsController, not a dat text node. // A Plan-2 standalone text element would need a type-preserving path (e.g. float? nullable // Width/Height, or explicit handling of Type 0 before the merge). Type = derived.Type != 0 ? derived.Type : base_.Type, X = derived.X, Y = derived.Y, // NOTE: 0 is the "not set, inherit from base" sentinel for Width/Height. This // diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is // indistinguishable for Plan 1 (all base elements are zero-size Type-12 prototypes). // If a real zero-size derived element ever needs to override a non-zero base in // Plan 2, switch Width/Height to float? + null-coalescing (and update Tasks 3-5). Width = derived.Width != 0 ? derived.Width : base_.Width, Height = derived.Height != 0 ? derived.Height : base_.Height, Left = derived.Left, Top = derived.Top, Right = derived.Right, Bottom = derived.Bottom, ReadOrder = derived.ReadOrder, FontDid = derived.FontDid != 0 ? derived.FontDid : base_.FontDid, // DefaultStateName: derived wins if set; otherwise inherit the base's default. DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName, // Children come from the derived element's own tree, not the base prototype's. // Defensive copy: prevent a later mutation of either the merged result or the input // from corrupting the other. Safe for the Task-5 flow (derived.Children is fully // populated by the recursive importer BEFORE Merge is called and never mutated after). Children = new List(derived.Children), }; // Start with base StateMedia as defaults, then let derived entries override. m.StateMedia = new Dictionary(base_.StateMedia); foreach (var kv in derived.StateMedia) m.StateMedia[kv.Key] = kv.Value; return m; } }