feat(D.2b): importer renders Type-12-with-sprites + carries DefaultState
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>
This commit is contained in:
parent
c2170ab18f
commit
6e6339b026
6 changed files with 126 additions and 19 deletions
|
|
@ -9,8 +9,10 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <see cref="UiDatElement"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// Type 12 (style prototype / BaseElement store) is never instantiated —
|
||||
/// <see cref="Create"/> returns null and the importer skips it.
|
||||
/// Type 12 elements that carry NO own state media (pure style prototypes /
|
||||
/// BaseElement stores) return null from <see cref="Create"/> and are skipped.
|
||||
/// Type 12 elements that DO carry own sprites (e.g. a chat element whose Type-0
|
||||
/// derived form inherited Type 12 from its base prototype) are rendered normally.
|
||||
/// See docs/research/2026-06-15-layoutdesc-format.md Correction 8.
|
||||
/// </para>
|
||||
///
|
||||
|
|
@ -42,14 +44,16 @@ public static class DatWidgetFactory
|
|||
/// Returns (0,0,0) when the texture is not yet uploaded.</param>
|
||||
/// <param name="datFont">Retail UI font for the meter's "cur/max" number overlay.
|
||||
/// May be null pre-load — the meter falls back to the debug bitmap font.</param>
|
||||
/// <returns>The widget, or <c>null</c> for a Type-12 style prototype (caller skips it).</returns>
|
||||
/// <returns>The widget, or <c>null</c> for a pure Type-12 style prototype with no own sprites (caller skips it).</returns>
|
||||
public static UiElement? Create(ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve, UiDatFont? datFont)
|
||||
{
|
||||
// Type 12 = zero-size style prototype / BaseElement store referenced by
|
||||
// BaseLayoutId. These are property bags, never rendered. See format doc §8
|
||||
// ("style prototypes are Type 12 which must be skipped") and Correction 8.
|
||||
if (info.Type == 12) return null;
|
||||
// Type 12 = style prototype / BaseElement store referenced by BaseLayoutId.
|
||||
// PURE prototypes (no own state media) are property bags — never rendered; skip them.
|
||||
// A Type-12 element that carries its own state media (e.g. a chat Send button whose
|
||||
// Type-0 derived element inherited Type 12 from its base prototype) has sprites to
|
||||
// show and must render. See format doc §8 and the G1 task note.
|
||||
if (info.Type == 12 && info.StateMedia.Count == 0) return null;
|
||||
|
||||
UiElement e = info.Type switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ public sealed class ElementInfo
|
|||
/// </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.
|
||||
|
|
@ -144,7 +152,9 @@ public static class ElementReader
|
|||
Right = derived.Right,
|
||||
Bottom = derived.Bottom,
|
||||
ReadOrder = derived.ReadOrder,
|
||||
FontDid = derived.FontDid != 0 ? derived.FontDid : base_.FontDid,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -208,19 +208,23 @@ public static class LayoutImporter
|
|||
/// </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,
|
||||
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 "").
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@ public sealed class UiDatElement : UiElement
|
|||
_info = info;
|
||||
_resolve = resolve;
|
||||
ClickThrough = true; // generic decoration; behavioral widgets opt back in
|
||||
|
||||
// Pick the initial active state: retail applies DefaultState when set; falls back
|
||||
// to "Normal" when the element has a Normal-state sprite (retail's implicit default
|
||||
// for stateful elements like tabs and buttons); else the unnamed DirectState ("").
|
||||
if (!string.IsNullOrEmpty(info.DefaultStateName))
|
||||
ActiveState = info.DefaultStateName;
|
||||
else if (info.StateMedia.ContainsKey("Normal"))
|
||||
ActiveState = "Normal";
|
||||
// else ActiveState stays "" (DirectState)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -71,6 +71,32 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal(7, e!.ZOrder);
|
||||
}
|
||||
|
||||
// ── Test G1a: Type 12 with own sprites renders; without sprites is skipped ──
|
||||
|
||||
/// <summary>
|
||||
/// Task G1 change 1: only PURE Type-12 prototypes (no state media) are skipped.
|
||||
/// A Type-12 element that carries its own state media must return a non-null widget.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DatWidgetFactory_Type12WithMedia_Renders()
|
||||
{
|
||||
// Type 12 with a "Normal" state sprite — must render (NOT skipped).
|
||||
var withMedia = new ElementInfo
|
||||
{
|
||||
Type = 12,
|
||||
Width = 32,
|
||||
Height = 16,
|
||||
StateMedia = { ["Normal"] = (0x00001234u, 1) },
|
||||
};
|
||||
var e = DatWidgetFactory.Create(withMedia, NoTex, null);
|
||||
Assert.NotNull(e);
|
||||
Assert.IsType<UiDatElement>(e);
|
||||
|
||||
// Type 12 with NO state media — must still be skipped (pure prototype).
|
||||
var noMedia = new ElementInfo { Type = 12 };
|
||||
Assert.Null(DatWidgetFactory.Create(noMedia, NoTex, null));
|
||||
}
|
||||
|
||||
// ── Test 6: Meter slice extraction (the important one) ───────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -33,4 +33,58 @@ public class UiDatElementTests
|
|||
var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "NoSuchState" };
|
||||
Assert.Equal(0x06000005u, e.ActiveMedia().File);
|
||||
}
|
||||
|
||||
// ── G1 tests: DefaultStateName + "Normal" implicit default ───────────────
|
||||
|
||||
/// <summary>
|
||||
/// Task G1 change 5: when an element has no DefaultStateName but does have a "Normal"
|
||||
/// state sprite, the ctor should default ActiveState to "Normal" so the element
|
||||
/// renders its normal-state sprite without requiring explicit state assignment.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UiDatElement_DefaultsActiveStateToNormal_WhenNormalPresent()
|
||||
{
|
||||
var info = new ElementInfo();
|
||||
info.StateMedia["Normal"] = (0x0000AAAAu, 1);
|
||||
info.StateMedia["Hover"] = (0x0000BBBBu, 1);
|
||||
|
||||
var e = new UiDatElement(info, _ => (0, 0, 0));
|
||||
|
||||
// Should have defaulted to "Normal" state.
|
||||
Assert.Equal(0x0000AAAAu, e.ActiveMedia().File);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task G1 change 5: when DefaultStateName is set (e.g. "Minimized"),
|
||||
/// it takes priority over the "Normal" implicit default.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UiDatElement_DefaultsActiveStateToDefaultStateName_WhenSet()
|
||||
{
|
||||
var info = new ElementInfo { DefaultStateName = "Minimized" };
|
||||
info.StateMedia["Minimized"] = (0x0000BBBBu, 1);
|
||||
info.StateMedia["Maximized"] = (0x0000CCCCu, 1);
|
||||
info.StateMedia["Normal"] = (0x0000DDDDu, 1);
|
||||
|
||||
var e = new UiDatElement(info, _ => (0, 0, 0));
|
||||
|
||||
// DefaultStateName "Minimized" wins over "Normal" implicit default.
|
||||
Assert.Equal(0x0000BBBBu, e.ActiveMedia().File);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task G1 change 5: elements with only a DirectState sprite and no "Normal" state
|
||||
/// should still default to "" (DirectState) — no regression for chrome/grip elements.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void UiDatElement_NoDefaultStateName_NoNormal_DefaultsToDirectState()
|
||||
{
|
||||
var info = new ElementInfo();
|
||||
info.StateMedia[""] = (0x06007777u, 1); // DirectState only (e.g. vitals chrome corner)
|
||||
|
||||
var e = new UiDatElement(info, _ => (0, 0, 0));
|
||||
|
||||
// No DefaultStateName, no "Normal" state → ActiveState stays "" (DirectState).
|
||||
Assert.Equal(0x06007777u, e.ActiveMedia().File);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue