feat(D.2b): LayoutImporter — read layout + resolve inheritance + build tree
Implements Task 5 of the LayoutDesc Importer (Plan 1 — vitals conformance). Pure layer (BuildFromInfos / Build): - ImportedLayout result type: UiElement root + O(1) FindElement(uint id) lookup - BuildWidget dispatches via DatWidgetFactory.Create; skips Type-12 prototypes (null) - Meters consume their children (DatWidgetFactory already extracted slice ids — adding the dat children as UiElement nodes would duplicate geometry) - All other element types recurse children generically via AddChild Dat shell (Import): - Loads LayoutDesc from dats; null-safe if layout is absent - Resolves each top-level ElementDesc to ElementInfo via Resolve(): BaseElement/BaseLayoutId chain with (layoutId,elementId) cycle guard - ToInfo(): reads ElementDesc scalar fields (uint → float cast) + DirectState + named States (UIStateId.ToString() as key) - ReadState(): extracts first MediaDescImage (File + DrawMode) per state + font DID from Properties[0x1A] → ArrayBaseProperty → DataIdBaseProperty.Value - Each sibling element gets a fresh base-chain set (siblings don't share guards) DRW API: all members confirmed from VitalsLayoutDump.cs usings — no adjustments needed: LayoutDesc in DBObjs; ElementDesc/StateDesc/MediaDescImage/ ArrayBaseProperty/DataIdBaseProperty in Types; DrawModeType/UIStateId in Enums. Tests (3/3 green): - BuildFromInfos_HealthMeter_IsUiMeterAtRect — Type-7 child → UiMeter, Left=5, Width=150 - BuildFromInfos_Type12Child_IsSkipped_Type3Present — prototype absent, container present - BuildFromInfos_MeterWithChildren_MeterPresent_ChildrenNotInTree — meter findable, both dat-children absent, UiMeter.Children empty Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fc79fd519d
commit
bd01a29eb2
2 changed files with 383 additions and 0 deletions
278
src/AcDream.App/UI/Layout/LayoutImporter.cs
Normal file
278
src/AcDream.App/UI/Layout/LayoutImporter.cs
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
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)
|
||||
{
|
||||
rootInfo.Children = new List<ElementInfo>(children);
|
||||
return Build(rootInfo, resolve, datFont);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure builder: produce the widget tree from a fully resolved
|
||||
/// <see cref="ElementInfo"/> tree (children already attached).
|
||||
/// </summary>
|
||||
public static ImportedLayout Build(
|
||||
ElementInfo rootInfo,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
UiDatFont? datFont)
|
||||
{
|
||||
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, byId)
|
||||
?? new UiDatElement(rootInfo, resolve);
|
||||
return new ImportedLayout(root, byId);
|
||||
}
|
||||
|
||||
private static UiElement? BuildWidget(
|
||||
ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
UiDatFont? datFont,
|
||||
Dictionary<uint, UiElement> 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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public static ImportedLayout? Import(
|
||||
DatCollection dats,
|
||||
uint layoutId,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
UiDatFont? datFont)
|
||||
{
|
||||
var ld = dats.Get<LayoutDesc>(layoutId);
|
||||
if (ld is null) return null;
|
||||
|
||||
// Build a resolved ElementInfo for every top-level element in the layout.
|
||||
var tops = new List<ElementInfo>();
|
||||
foreach (var kv in ld.Elements)
|
||||
tops.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>()));
|
||||
|
||||
// If there is exactly one top-level element use it directly as the root;
|
||||
// otherwise wrap the tops in a synthetic zero-id container.
|
||||
ElementInfo rootInfo = tops.Count == 1
|
||||
? tops[0]
|
||||
: new ElementInfo { Id = 0, Type = 3, Children = tops };
|
||||
|
||||
return Build(rootInfo, resolve, datFont);
|
||||
}
|
||||
|
||||
// ── Inheritance resolution ────────────────────────────────────────────────
|
||||
|
||||
/// <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;
|
||||
|
||||
// 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; 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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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,
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the first <see cref="MediaDescImage"/> from <paramref name="sd"/> into
|
||||
/// <c>info.StateMedia[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, string name, ElementInfo info)
|
||||
{
|
||||
// First MediaDescImage in this state's Media list wins (format doc §5).
|
||||
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 ───────────────────────────────────────────────────
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue