621 lines
28 KiB
C#
621 lines
28 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using DatReaderWriter;
|
||
using DatReaderWriter.DBObjs;
|
||
using DatReaderWriter.Enums;
|
||
using DatReaderWriter.Types;
|
||
|
||
namespace AcDream.App.UI.Layout;
|
||
|
||
/// <summary>
|
||
/// The result of importing a retail LayoutDesc: a <see cref="UiElement"/> tree with
|
||
/// an O(1) lookup table for finding any element by its dat id.
|
||
/// </summary>
|
||
public sealed class ImportedLayout
|
||
{
|
||
/// <summary>Root widget of the imported tree.</summary>
|
||
public UiElement Root { get; }
|
||
|
||
private readonly Dictionary<uint, UiElement> _byId;
|
||
|
||
public ImportedLayout(UiElement root, Dictionary<uint, UiElement> byId)
|
||
{
|
||
Root = root;
|
||
_byId = byId;
|
||
}
|
||
|
||
/// <summary>Find a widget by its dat element id (e.g. <c>0x100000E6</c>).
|
||
/// Returns null if the id was skipped (Type-12 prototype) or not present.</summary>
|
||
public UiElement? FindElement(uint id)
|
||
=> _byId.TryGetValue(id, out var e) ? e : null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Two-layer layout importer for retail LayoutDesc dat objects.
|
||
///
|
||
/// <para>
|
||
/// <strong>Pure layer</strong> (<see cref="Build"/> / <see cref="BuildFromInfos"/>):
|
||
/// converts a pre-resolved <see cref="ElementInfo"/> tree into a <see cref="UiElement"/>
|
||
/// tree via <see cref="DatWidgetFactory"/>. Testable without dats or OpenGL — all tests
|
||
/// in <c>LayoutImporterTests.cs</c> exercise this layer only.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <strong>Dat shell</strong> (<see cref="Import"/>): reads a <see cref="LayoutDesc"/>,
|
||
/// converts each top-level <see cref="ElementDesc"/> to a fully resolved
|
||
/// <see cref="ElementInfo"/> (applying <c>BaseElement</c> / <c>BaseLayoutId</c>
|
||
/// inheritance with a cycle guard), then delegates to <see cref="Build"/>.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Meter elements (Type 7) consume their own dat-children: <see cref="DatWidgetFactory"/>
|
||
/// reads the grandchild slice-sprite ids during <see cref="UiMeter"/> construction, so the
|
||
/// children must NOT be added as separate <see cref="UiElement"/> nodes in the tree.
|
||
/// Every other element type recurses its children generically.
|
||
/// </para>
|
||
/// </summary>
|
||
public static class LayoutImporter
|
||
{
|
||
// ── Pure layer ────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Convenience for tests: attach <paramref name="children"/> to
|
||
/// <paramref name="rootInfo"/>, then call <see cref="Build"/>.
|
||
/// The children list is set directly on <paramref name="rootInfo"/>;
|
||
/// any existing children are replaced.
|
||
/// </summary>
|
||
public static ImportedLayout BuildFromInfos(
|
||
ElementInfo rootInfo,
|
||
IEnumerable<ElementInfo> children,
|
||
Func<uint, (uint, int, int)> resolve,
|
||
UiDatFont? datFont,
|
||
Func<uint, UiDatFont?>? fontResolve = null)
|
||
{
|
||
rootInfo.Children = new List<ElementInfo>(children);
|
||
return Build(rootInfo, resolve, datFont, fontResolve);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Pure builder: produce the widget tree from a fully resolved
|
||
/// <see cref="ElementInfo"/> tree (children already attached).
|
||
/// </summary>
|
||
/// <param name="fontResolve">Optional per-element font resolver — FontDid →
|
||
/// <see cref="UiDatFont"/> (or null if the font can't be loaded). When supplied,
|
||
/// elements with a non-zero <see cref="ElementInfo.FontDid"/> get their own dat
|
||
/// font at build time instead of the shared <paramref name="datFont"/> fallback.
|
||
/// Null preserves the original single-font behavior for all callers that don't
|
||
/// pass it — no behavior change for the live game path.</param>
|
||
public static ImportedLayout Build(
|
||
ElementInfo rootInfo,
|
||
Func<uint, (uint, int, int)> resolve,
|
||
UiDatFont? datFont,
|
||
Func<uint, UiDatFont?>? fontResolve = null)
|
||
{
|
||
var byId = new Dictionary<uint, UiElement>();
|
||
// 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<uint, (uint, int, int)> resolve,
|
||
UiDatFont? datFont,
|
||
Func<uint, UiDatFont?>? fontResolve,
|
||
Dictionary<uint, UiElement> 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 ─────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <param name="dats">The dat collection to read the LayoutDesc from.</param>
|
||
/// <param name="layoutId">The LayoutDesc dat id to read.</param>
|
||
public static ElementInfo? ImportInfos(DatCollection dats, uint layoutId)
|
||
{
|
||
var ld = dats.Get<LayoutDesc>(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<LayoutDesc>), 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<uint>();
|
||
foreach (var kv in ld.Elements)
|
||
CollectBaseRefsInDesc(kv.Value, layoutId, referencedAsBase);
|
||
|
||
var tops = new List<ElementInfo>();
|
||
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,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
///
|
||
/// <para>
|
||
/// <b>Dat UIState visibility model (2026-06-26 audit):</b>
|
||
/// The dat's <c>ElementDesc.DefaultState</c> field specifies which SPRITE/MEDIA
|
||
/// state an element starts in (e.g., <c>Normal</c>, <c>Minimized</c>). It does
|
||
/// NOT encode visibility of sibling Group containers. The <c>StateDesc</c>'s
|
||
/// <see cref="DatReaderWriter.Enums.IncorporationFlags"/> contains X/Y/Width/Height/
|
||
/// ZLevel/PassToChildren — there is no Visible flag.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// 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
|
||
/// <c>UIElement::SetState(stateId)</c> 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
|
||
/// (<see cref="CharacterStatController"/>) perform the initial show/hide.
|
||
/// </para>
|
||
/// </summary>
|
||
/// <param name="fontResolve">Optional per-element font resolver (see
|
||
/// <see cref="Build"/> for details). Null = original single-font behavior.</param>
|
||
public static ImportedLayout? Import(
|
||
DatCollection dats,
|
||
uint layoutId,
|
||
Func<uint, (uint, int, int)> resolve,
|
||
UiDatFont? datFont,
|
||
Func<uint, UiDatFont?>? fontResolve = null)
|
||
{
|
||
var rootInfo = ImportInfos(dats, layoutId);
|
||
if (rootInfo is null) return null;
|
||
return Build(rootInfo, resolve, datFont, fontResolve);
|
||
}
|
||
|
||
// ── Inheritance resolution ────────────────────────────────────────────────
|
||
|
||
/// <summary>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).</summary>
|
||
internal static bool ShouldMountBaseChildren(int derivedChildCount, int derivedMediaCount, int baseChildCount)
|
||
=> derivedChildCount == 0 && derivedMediaCount == 0 && baseChildCount > 0;
|
||
|
||
/// <summary>
|
||
/// Converts an <see cref="ElementDesc"/> to a resolved <see cref="ElementInfo"/>:
|
||
/// reads own fields + media, applies the BaseElement / BaseLayoutId chain
|
||
/// (cycle-guarded by <paramref name="baseChain"/>), then resolves + attaches children.
|
||
/// </summary>
|
||
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<ElementInfo>? 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<LayoutDesc>(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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Read an <see cref="ElementDesc"/>'s own scalar fields + state media into a
|
||
/// fresh <see cref="ElementInfo"/>. No inheritance is applied; children are not
|
||
/// attached (the caller handles those).
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Read the first <see cref="MediaDescImage"/> from <paramref name="sd"/> into
|
||
/// <c>info.StateMedia[name]</c>, read any <see cref="MediaDescCursor"/> into
|
||
/// <c>info.StateCursors[name]</c>, and extract the font DID from property 0x1A
|
||
/// (<c>ArrayBaseProperty → DataIdBaseProperty</c>) if not yet set.
|
||
/// </summary>
|
||
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 ───────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Recursively walks <paramref name="d"/> and all its children, adding to
|
||
/// <paramref name="result"/> the <c>BaseElement</c> of every descriptor that
|
||
/// references this layout (<c>BaseLayoutId == layoutId</c>). Used by
|
||
/// <see cref="ImportInfos"/> to identify pure prototype/template elements that
|
||
/// should not be instantiated as live widgets.
|
||
/// </summary>
|
||
private static void CollectBaseRefsInDesc(ElementDesc d, uint layoutId, HashSet<uint> result)
|
||
{
|
||
if (d.BaseElement != 0 && d.BaseLayoutId == layoutId)
|
||
result.Add(d.BaseElement);
|
||
foreach (var kv in d.Children)
|
||
CollectBaseRefsInDesc(kv.Value, layoutId, result);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns true when <paramref name="d"/> carries no own state media — i.e. its
|
||
/// <c>StateDesc</c> (DirectState) and <c>States</c> (named states) yield no
|
||
/// <see cref="MediaDescImage"/> entries with a non-zero file id.
|
||
/// Such elements are pure inheritance templates with no rendering content.
|
||
/// </summary>
|
||
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 ───────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Find an <see cref="ElementDesc"/> by id anywhere in the top-level tree of
|
||
/// <paramref name="ld"/> (depth-first). Returns null if not found.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|