Task G1: two gaps blocked chat window static sprite elements from rendering. Change 1 — DatWidgetFactory: only skip Type-12 elements that have no own state media (pure style prototypes). A Type-12 element that carries sprites (e.g. a chat Send button whose derived Type-0 element inherited Type 12 from its base prototype) now renders as a UiDatElement. Change 2 — ElementInfo: add DefaultStateName field (string, default ""). Change 3 — LayoutImporter.ToInfo: read ElementDesc.DefaultState.ToString() into DefaultStateName; normalize Undef/Undefined/0 sentinels to "". Change 4 — ElementReader.Merge: inherit DefaultStateName (derived wins if non-empty, else base). Change 5 — UiDatElement ctor: initialize ActiveState to DefaultStateName when set; else "Normal" when a Normal-state sprite is present (retail's implicit default for buttons/tabs); else "" (DirectState). This makes the Send button, max/min button, and numbered tabs render their default sprite without requiring explicit state assignment at runtime. Vitals neutrality: all vitals chrome/grip elements carry DirectState-only sprites with no "Normal" named state and DefaultStateName="" (Undef in dat), so their ActiveState stays "" and their existing conformance tests are unaffected. Vitals text labels (Type 0→12 via Merge, no StateMedia) are still skipped by the refined Type-12 guard (StateMedia.Count==0). Tests: 4 new tests (2 in DatWidgetFactoryTests, 3 in UiDatElementTests). All 386 pass; 387 total (1 pre-existing skip). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
170 lines
8.9 KiB
C#
170 lines
8.9 KiB
C#
using System.Collections.Generic;
|
||
|
||
namespace AcDream.App.UI.Layout;
|
||
|
||
/// <summary>
|
||
/// GL-free, dat-free snapshot of a resolved layout element.
|
||
/// Populated by the LayoutDesc importer from <c>DatReaderWriter.ElementDesc</c>
|
||
/// after inheritance is applied. The pure transforms on <see cref="ElementReader"/>
|
||
/// 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.
|
||
/// </summary>
|
||
public sealed class ElementInfo
|
||
{
|
||
/// <summary>Dat element id (e.g. <c>0x100000E6</c>).</summary>
|
||
public uint Id;
|
||
|
||
/// <summary>
|
||
/// Raw element class id as a uint.
|
||
/// Game-specific ids like <c>0x1000004D</c> (gmVitalsUI root) and <c>0x10000009</c>
|
||
/// overflow <c>int</c> when treated as signed, so this stays <c>uint</c>.
|
||
/// Known values: 0=text, 2=dragbar, 3=container/chrome, 7=meter,
|
||
/// 9=resize-grip, 12=style-prototype (skip), 0x10000009/0x1000004D=window root.
|
||
/// </summary>
|
||
public uint Type;
|
||
|
||
/// <summary>Position and size within the parent, in pixels (cast from dat uint fields).</summary>
|
||
public float X, Y, Width, Height;
|
||
|
||
/// <summary>
|
||
/// Raw edge-anchor flag values from the dat (<c>LeftEdge</c>, <c>TopEdge</c>,
|
||
/// <c>RightEdge</c>, <c>BottomEdge</c> fields of <c>ElementDesc</c>).
|
||
/// Values 0–4; map to <see cref="AnchorEdges"/> bit-flags via
|
||
/// <see cref="ElementReader.ToAnchors"/>.
|
||
/// </summary>
|
||
public uint Left, Top, Right, Bottom;
|
||
|
||
/// <summary>Draw order within the parent (lower = drawn first / behind).</summary>
|
||
public uint ReadOrder;
|
||
|
||
/// <summary>
|
||
/// Font dat object id inherited from the base element's <c>Properties[0x1A]</c>
|
||
/// (<c>ArrayBaseProperty → DataIdBaseProperty</c>). 0 = none / not inherited.
|
||
/// </summary>
|
||
public uint FontDid;
|
||
|
||
/// <summary>
|
||
/// Sprite per state: state name → (RenderSurface file id, DrawMode int).
|
||
/// The <c>""</c> key represents the unnamed DirectState (<c>ElementDesc.StateDesc</c>).
|
||
/// Named states use the <c>UIStateId.ToString()</c> value as the key
|
||
/// (e.g. <c>"HideDetail"</c>, <c>"ShowDetail"</c>).
|
||
/// </summary>
|
||
public Dictionary<string, (uint File, int DrawMode)> StateMedia = new();
|
||
|
||
/// <summary>
|
||
/// The element's initial active state name, taken from <c>ElementDesc.DefaultState.ToString()</c>.
|
||
/// Normalized to <c>""</c> when the dat carries Undef/Undefined/0 (no default set).
|
||
/// Used by <see cref="UiDatElement"/> to pick which state's sprite to render initially.
|
||
/// Examples: <c>"Normal"</c> (Send button), <c>"Minimized"</c> (max/min button), <c>""</c> (DirectState).
|
||
/// </summary>
|
||
public string DefaultStateName = "";
|
||
|
||
/// <summary>
|
||
/// Resolved child elements (populated by the importer in Task 5).
|
||
/// Children come from the derived element's own tree, not the base element's.
|
||
/// </summary>
|
||
public List<ElementInfo> Children = new();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Pure, GL-free, dat-free transforms for the LayoutDesc importer.
|
||
/// All methods are static and operate on <see cref="ElementInfo"/> POCOs.
|
||
/// No OpenGL, no DatReaderWriter types, no rendering dependencies beyond
|
||
/// the <see cref="AnchorEdges"/> bit-flag enum from <c>AcDream.App.UI</c>.
|
||
/// </summary>
|
||
public static class ElementReader
|
||
{
|
||
/// <summary>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).</summary>
|
||
/// <param name="left">LeftEdge dat field value (0–4).</param>
|
||
/// <param name="top">TopEdge dat field value (0–4).</param>
|
||
/// <param name="right">RightEdge dat field value (0–4).</param>
|
||
/// <param name="bottom">BottomEdge dat field value (0–4).</param>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Merges a base element snapshot with a derived element snapshot, mirroring
|
||
/// the <c>BaseElement</c> / <c>BaseLayoutId</c> inheritance chain in the dat.
|
||
///
|
||
/// <para>
|
||
/// Rules:
|
||
/// <list type="bullet">
|
||
/// <item><description>
|
||
/// Scalar fields (<see cref="ElementInfo.Id"/>, <see cref="ElementInfo.Type"/>,
|
||
/// <see cref="ElementInfo.Width"/>, <see cref="ElementInfo.Height"/>,
|
||
/// <see cref="ElementInfo.FontDid"/>): derived wins if non-zero; otherwise
|
||
/// inherited from base.
|
||
/// </description></item>
|
||
/// <item><description>
|
||
/// Position (<see cref="ElementInfo.X"/>, <see cref="ElementInfo.Y"/>) and
|
||
/// edge flags (<see cref="ElementInfo.Left"/> etc.) and
|
||
/// <see cref="ElementInfo.ReadOrder"/>: always taken from the derived element
|
||
/// (derived placement, not the base prototype's geometry).
|
||
/// </description></item>
|
||
/// <item><description>
|
||
/// <see cref="ElementInfo.StateMedia"/>: base entries are the default; derived
|
||
/// entries override (or add) per state name key.
|
||
/// </description></item>
|
||
/// <item><description>
|
||
/// <see cref="ElementInfo.Children"/>: come from the derived element's own tree only.
|
||
/// </description></item>
|
||
/// </list>
|
||
/// </para>
|
||
/// </summary>
|
||
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<ElementInfo>(derived.Children),
|
||
};
|
||
// Start with base StateMedia as defaults, then let derived entries override.
|
||
m.StateMedia = new Dictionary<string, (uint, int)>(base_.StateMedia);
|
||
foreach (var kv in derived.StateMedia)
|
||
m.StateMedia[kv.Key] = kv.Value;
|
||
return m;
|
||
}
|
||
}
|