Import gmFloatyPowerBarUI LayoutDesc 0x21000072, teach the meter factory its stateful single-image shape, and project the movement-owned retail jump charge through a focused retained controller. Preserve authored resize constraints and state-managed visibility, with named-decomp pseudocode plus controller, movement, and production-DAT conformance coverage. Co-Authored-By: Codex <codex@openai.com>
598 lines
25 KiB
C#
598 lines
25 KiB
C#
using System.Numerics;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
public class DatWidgetFactoryTests
|
|
{
|
|
private static (uint, int, int) NoTex(uint _) => (0, 0, 0);
|
|
|
|
// ── Test 1: Type 7 → UiMeter ─────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Type7_Meter_MakesUiMeter()
|
|
{
|
|
var e = DatWidgetFactory.Create(new ElementInfo { Type = 7, Width = 150, Height = 16 }, NoTex, null);
|
|
Assert.IsType<UiMeter>(e);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetailRadarClass_MakesUiRadar()
|
|
{
|
|
var e = DatWidgetFactory.Create(
|
|
new ElementInfo { Type = UiRadar.RetailClassId, Width = 120, Height = 140 },
|
|
NoTex,
|
|
null);
|
|
|
|
Assert.IsType<UiRadar>(e);
|
|
}
|
|
|
|
// ── Test 2: Unknown type → UiDatElement fallback ─────────────────────────
|
|
|
|
[Fact]
|
|
public void UnknownType_FallsBackToGeneric()
|
|
{
|
|
var e = DatWidgetFactory.Create(new ElementInfo { Type = 999 }, NoTex, null);
|
|
Assert.IsType<UiDatElement>(e);
|
|
}
|
|
|
|
// ── Test 3: Type 12 → UiText (behavioral text widget) ────────────────────
|
|
|
|
[Fact]
|
|
public void Type12_Text_MakesUiText()
|
|
{
|
|
var e = DatWidgetFactory.Create(new ElementInfo { Type = 12, Width = 100, Height = 40 }, NoTex, null);
|
|
var text = Assert.IsType<UiText>(e);
|
|
Assert.False(text.Selectable);
|
|
Assert.True(text.ClickThrough);
|
|
Assert.False(text.AcceptsFocus);
|
|
Assert.False(text.CapturesPointerDrag);
|
|
}
|
|
|
|
[Fact]
|
|
public void Type12_EditableProperty_MakesUiFieldInPlace()
|
|
{
|
|
var info = TextInfo(
|
|
(0x16u, Bool(true)),
|
|
(0x20u, Bool(true)),
|
|
(0x27u, Bool(true)),
|
|
(0x1Eu, Integer(80)));
|
|
|
|
var field = Assert.IsType<UiField>(DatWidgetFactory.Create(info, NoTex, null));
|
|
|
|
Assert.Equal(info.Id, field.ElementId);
|
|
Assert.True(field.OneLine);
|
|
Assert.True(field.Selectable);
|
|
Assert.Equal(80, field.MaxCharacters);
|
|
}
|
|
|
|
[Fact]
|
|
public void Type12_SelectableProperty_MakesSelectableUiText()
|
|
{
|
|
var text = Assert.IsType<UiText>(DatWidgetFactory.Create(
|
|
TextInfo((0x27u, Bool(true))),
|
|
NoTex,
|
|
null));
|
|
|
|
Assert.True(text.Selectable);
|
|
Assert.False(text.ClickThrough);
|
|
Assert.True(text.AcceptsFocus);
|
|
Assert.True(text.CapturesPointerDrag);
|
|
}
|
|
|
|
// ── Test 4: Rect + anchors set from ElementInfo ───────────────────────────
|
|
|
|
/// <summary>
|
|
/// A Type-3 element with X=5,Y=21,W=150,H=16, Left=1,Top=1,Right=1 should have
|
|
/// its rect + anchors copied onto the returned widget.
|
|
/// Per UIElement::UpdateForParentSizeChange @0x00462640:
|
|
/// Left=1 → AnchorEdges.Left (near-pin); Top=1 → AnchorEdges.Top;
|
|
/// Right=1 → AnchorEdges.Right (stretch / track parent right); Bottom=0 → neither.
|
|
/// Combined: Left | Top | Right.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RectAndAnchors_SetFromElementInfo()
|
|
{
|
|
var info = new ElementInfo
|
|
{
|
|
Type = 3,
|
|
X = 5, Y = 21,
|
|
Width = 150, Height = 16,
|
|
Left = 1, Top = 1,
|
|
Right = 1, Bottom = 0,
|
|
};
|
|
var e = DatWidgetFactory.Create(info, NoTex, null)!;
|
|
Assert.Equal(5f, e.Left);
|
|
Assert.Equal(21f, e.Top);
|
|
Assert.Equal(150f, e.Width);
|
|
Assert.Equal(16f, e.Height);
|
|
Assert.Equal(AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right, e.Anchors);
|
|
}
|
|
|
|
[Fact]
|
|
public void ImportedDescendant_PreservesExactRawEdgePolicy()
|
|
{
|
|
var info = new ElementInfo
|
|
{
|
|
Type = 3,
|
|
X = 10,
|
|
Y = 20,
|
|
Width = 30,
|
|
Height = 40,
|
|
Left = 4,
|
|
Top = 3,
|
|
Right = 1,
|
|
Bottom = 0,
|
|
OriginalParentWidth = 100,
|
|
OriginalParentHeight = 200,
|
|
HasOriginalParentSize = true,
|
|
};
|
|
|
|
var element = DatWidgetFactory.Create(info, NoTex, null)!;
|
|
var policy = Assert.IsType<UiLayoutPolicy>(element.LayoutPolicy);
|
|
|
|
Assert.Equal(4u, policy.LeftMode);
|
|
Assert.Equal(3u, policy.TopMode);
|
|
Assert.Equal(1u, policy.RightMode);
|
|
Assert.Equal(0u, policy.BottomMode);
|
|
Assert.Equal(new UiPixelRect(0, 0, 99, 199), policy.OriginalParent);
|
|
}
|
|
|
|
[Fact]
|
|
public void ImportedRoot_HasNoRawEdgePolicy()
|
|
{
|
|
var element = DatWidgetFactory.Create(
|
|
new ElementInfo { Type = 3, Width = 100, Height = 200 },
|
|
NoTex,
|
|
null)!;
|
|
|
|
Assert.Null(element.LayoutPolicy);
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_PropagatesStateCursors()
|
|
{
|
|
var info = new ElementInfo { Type = 3 };
|
|
info.StateCursors["Drag_rollover_accept"] = new UiCursorMedia(0x06008888u, 7, 8);
|
|
|
|
var e = DatWidgetFactory.Create(info, NoTex, null)!;
|
|
|
|
Assert.Equal(new UiCursorMedia(0x06008888u, 7, 8),
|
|
e.CursorForState("Drag_rollover_accept", allowFallback: false));
|
|
}
|
|
|
|
// ── Test 5: ReadOrder propagated to ZOrder ───────────────────────────────
|
|
|
|
[Fact]
|
|
public void Create_PropagatesReadOrderToZOrder()
|
|
{
|
|
var e = DatWidgetFactory.Create(new ElementInfo { Type = 3, ReadOrder = 7 }, NoTex, null);
|
|
Assert.Equal(7, e!.ZOrder);
|
|
}
|
|
|
|
// ── Test G1a: Type 12 always produces UiText (with or without own sprites) ──
|
|
|
|
[Fact]
|
|
public void DatWidgetFactory_Type12_AlwaysMakesUiText()
|
|
{
|
|
var withMedia = new ElementInfo { Type = 12, Width = 32, Height = 16,
|
|
StateMedia = { ["Normal"] = (0x00001234u, 1) } };
|
|
Assert.IsType<UiText>(DatWidgetFactory.Create(withMedia, NoTex, null));
|
|
Assert.IsType<UiText>(DatWidgetFactory.Create(new ElementInfo { Type = 12 }, NoTex, null));
|
|
}
|
|
|
|
// ── Test 5c: Type 1 → UiButton ──────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Type1_Button_MakesUiButton()
|
|
{
|
|
var e = DatWidgetFactory.Create(new ElementInfo { Type = 1, Width = 46, Height = 18 }, NoTex, null);
|
|
Assert.IsType<UiButton>(e);
|
|
}
|
|
|
|
// ── Test 5b: Type 11 → UiScrollbar ──────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Type11_Scrollbar_MakesUiScrollbar()
|
|
{
|
|
var e = DatWidgetFactory.Create(new ElementInfo { Type = 11, Width = 16, Height = 68 }, NoTex, null);
|
|
Assert.IsType<UiScrollbar>(e);
|
|
}
|
|
|
|
[Fact]
|
|
public void Type11_HorizontalScrollbar_usesDirectRetailTrackAndThumbChildren()
|
|
{
|
|
const uint Track = 0x06004CF6u;
|
|
const uint Thumb = 0x06005DC3u;
|
|
var info = new ElementInfo { Type = 11, Id = 0x100001A4u, Width = 90, Height = 14 };
|
|
var track = new ElementInfo { Id = 4u, Type = 3u, Width = 90, Height = 14 };
|
|
track.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
|
{ Id = UiStateInfo.DirectStateId, Image = new UiImageMedia(Track, 1) };
|
|
var thumb = new ElementInfo { Id = 1u, Type = 3u, Width = 16, Height = 14 };
|
|
thumb.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
|
{ Id = UiStateInfo.DirectStateId, Image = new UiImageMedia(Thumb, 1) };
|
|
info.Children.Add(track);
|
|
info.Children.Add(thumb);
|
|
|
|
var bar = Assert.IsType<UiScrollbar>(DatWidgetFactory.Create(info, NoTex, null));
|
|
|
|
Assert.True(bar.Horizontal);
|
|
Assert.Equal(Track, bar.TrackSprite);
|
|
Assert.Equal(Thumb, bar.ThumbSprite);
|
|
Assert.Equal(0u, bar.UpSprite);
|
|
Assert.Equal(0u, bar.DownSprite);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetailToolbarFixture_buildsEditableStackEntry_andAuthoredHorizontalSlider()
|
|
{
|
|
ImportedLayout layout = FixtureLoader.LoadToolbar();
|
|
|
|
var entry = Assert.IsType<UiField>(layout.FindElement(0x100001A3u));
|
|
Assert.True(entry.RightAligned);
|
|
var bar = Assert.IsType<UiScrollbar>(layout.FindElement(0x100001A4u));
|
|
Assert.True(bar.Horizontal);
|
|
Assert.Equal(0x06004CF6u, bar.TrackSprite);
|
|
Assert.Equal(0x06005DC3u, bar.ThumbSprite);
|
|
}
|
|
|
|
// ── Test 5e: Type 3 is NOT registered — chrome/containers stay generic ────
|
|
//
|
|
// Retail Type 3 = UIElement_Field, but acdream's Type-3 dat elements (vitals/chat
|
|
// bevel chrome + the transcript/input container panels) are inert sprite-bearing
|
|
// chrome, not editable fields. They stay on the UiDatElement fallback so their
|
|
// sprites render and they gain no spurious focus/edit affordance. The one true
|
|
// editable field (the chat input, 0x10000016) resolves to Type 12 and is
|
|
// controller-placed as a UiField. Register Type 3 → UiField only when a window
|
|
// carries a factory-built editable Type-3 field.
|
|
|
|
[Fact]
|
|
public void Type3_NotRegistered_FallsBackToGeneric()
|
|
{
|
|
var e = DatWidgetFactory.Create(new ElementInfo { Type = 3, Width = 200, Height = 16 }, NoTex, null);
|
|
Assert.IsType<UiDatElement>(e);
|
|
}
|
|
|
|
// ── Test 5d: Type 6 → UiMenu ─────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Type6_Menu_MakesUiMenu()
|
|
{
|
|
var e = DatWidgetFactory.Create(new ElementInfo { Type = 6, Width = 46, Height = 18 }, NoTex, null);
|
|
Assert.IsType<UiMenu>(e);
|
|
}
|
|
|
|
// ── Test 7: Type 0x10000031 → UiItemList ────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Create_buildsUiItemList_forItemListClassId()
|
|
{
|
|
var info = new AcDream.App.UI.Layout.ElementInfo { Id = 0x100001A7u, Type = 0x10000031u, Width = 32, Height = 32 };
|
|
var w = AcDream.App.UI.Layout.DatWidgetFactory.Create(info, _ => (0u, 0, 0), null);
|
|
Assert.IsType<AcDream.App.UI.UiItemList>(w);
|
|
}
|
|
|
|
// ── Test M1: Single-image meter (toolbar selected-object meters) ────────
|
|
//
|
|
// The toolbar health/mana meters (0x100001A1 / 0x100001A2) use a DIFFERENT
|
|
// shape from the vitals 3-slice meters: the back-track sprite lives on the
|
|
// meter ELEMENT's own DirectState ("" key), and there is exactly ONE Type-3
|
|
// child whose own DirectState ("" key) carries the fill sprite. That child
|
|
// has no image grandchildren, so SliceIds would return all-zero — the new
|
|
// Count==1 branch reads the StateMedia entries directly instead.
|
|
// The sprites go in the TILE slot (Back/FrontTile), NOT the cap slot: DrawMode=Normal
|
|
// tiles at native width across the full bar geometry (UIElement_Meter::DrawChildren),
|
|
// so the back spans all 140px and the fill clips to 140*fraction for any native width.
|
|
// Back/FrontLeft + Back/FrontRight must be 0 (no caps on a single-image bar).
|
|
|
|
[Fact]
|
|
public void BuildMeter_SingleImageShape_ReadsDirectStateFromElementAndFillChild()
|
|
{
|
|
const uint BackFile = 0x0600193Eu; // health back-track (from toolbar dump)
|
|
const uint FillFile = 0x0600193Fu; // health fill (from toolbar dump)
|
|
|
|
// Meter element: Type 7, own DirectState = back-track sprite.
|
|
var meter = new ElementInfo { Type = 7, Id = 0x100001A1u, Width = 140, Height = 31 };
|
|
meter.StateMedia[""] = (BackFile, 1);
|
|
|
|
// Single Type-3 fill container: own DirectState = fill sprite, no grandchildren.
|
|
var fillContainer = new ElementInfo { Type = 3, ReadOrder = 1 };
|
|
fillContainer.StateMedia[""] = (FillFile, 1);
|
|
meter.Children.Add(fillContainer);
|
|
|
|
var e = DatWidgetFactory.Create(meter, NoTex, null);
|
|
|
|
var m = Assert.IsType<UiMeter>(e);
|
|
// Back-track on the meter element's own DirectState, fill on the single child —
|
|
// both in the TILE slot so they tile across the full 140px bar (DrawMode=Normal).
|
|
Assert.Equal(BackFile, m.BackTile);
|
|
Assert.Equal(0u, m.BackLeft);
|
|
Assert.Equal(0u, m.BackRight);
|
|
Assert.Equal(FillFile, m.FrontTile);
|
|
Assert.Equal(0u, m.FrontLeft);
|
|
Assert.Equal(0u, m.FrontRight);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildMeter_StatefulSingleImageShape_MapsNumericFillStatesAndHidesDirectRange()
|
|
{
|
|
const uint TrackFile = 0x06004D0Bu;
|
|
const uint JumpState = 0x10000042u;
|
|
const uint JumpFile = 0x06001354u;
|
|
const uint RangeFile = 0x0600715Eu;
|
|
|
|
var meter = new ElementInfo { Type = 7, Id = 0x10000034u, Width = 600, Height = 15 };
|
|
meter.StateMedia[""] = (TrackFile, 1);
|
|
|
|
var fills = new ElementInfo { Type = 3, Id = 2u, ReadOrder = 2 };
|
|
fills.States[JumpState] = new UiStateInfo { Id = JumpState, Name = "JumpMode" };
|
|
fills.StateMedia["JumpMode"] = (JumpFile, 1);
|
|
meter.Children.Add(fills);
|
|
|
|
var range = new ElementInfo { Type = 3, Id = 0x100005EEu, ReadOrder = 1 };
|
|
range.StateMedia[""] = (RangeFile, 1);
|
|
meter.Children.Add(range);
|
|
|
|
var result = Assert.IsType<UiMeter>(DatWidgetFactory.Create(meter, NoTex, null));
|
|
|
|
Assert.Equal(TrackFile, result.BackTile);
|
|
Assert.Equal(0u, result.FrontTile);
|
|
Assert.True(result.TrySetRetailState(JumpState));
|
|
Assert.Equal(JumpState, result.ActiveRetailStateId);
|
|
Assert.Equal(JumpFile, result.FrontTile);
|
|
Assert.NotEqual(RangeFile, result.BackTile);
|
|
Assert.NotEqual(RangeFile, result.FrontTile);
|
|
}
|
|
|
|
// ── Test 6: Meter slice extraction (the important one) ───────────────────
|
|
|
|
/// <summary>
|
|
/// A meter (Type 7) whose two Type-3 containers each carry 3 image children
|
|
/// (ordered by X, bearing a DirectState "" sprite), plus the front container
|
|
/// has a fourth expand-overlay child with ONLY a named "ShowDetail" state —
|
|
/// that overlay must be excluded from the slice count.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MeterSliceExtraction_ReadsGrandchildImageIds_IgnoresOverlay()
|
|
{
|
|
// Slice ids sourced from format doc §11 — real health-bar ids.
|
|
const uint BackL = 0x0600747Eu, BackT = 0x0600747Fu, BackR = 0x06007480u;
|
|
const uint FrontL = 0x06007481u, FrontT = 0x06007482u, FrontR = 0x06007483u;
|
|
const uint OverlayFile = 0x06007490u;
|
|
|
|
// Back container (ReadOrder 0 — drawn first / behind)
|
|
var backChild = new ElementInfo { Type = 3, ReadOrder = 0 };
|
|
backChild.Children.Add(new ElementInfo { X = 0, StateMedia = { [""] = (BackL, 1) } });
|
|
backChild.Children.Add(new ElementInfo { X = 10, StateMedia = { [""] = (BackT, 1) } });
|
|
backChild.Children.Add(new ElementInfo { X = 140, StateMedia = { [""] = (BackR, 1) } });
|
|
|
|
// Front container (ReadOrder 1 — drawn on top)
|
|
var frontChild = new ElementInfo { Type = 3, ReadOrder = 1 };
|
|
frontChild.Children.Add(new ElementInfo { X = 0, StateMedia = { [""] = (FrontL, 1) } });
|
|
frontChild.Children.Add(new ElementInfo { X = 10, StateMedia = { [""] = (FrontT, 1) } });
|
|
frontChild.Children.Add(new ElementInfo { X = 140, StateMedia = { [""] = (FrontR, 1) } });
|
|
// Expand-detail overlay: named state only — NO DirectState "" — must be ignored.
|
|
frontChild.Children.Add(new ElementInfo
|
|
{
|
|
X = 0,
|
|
StateMedia = { ["ShowDetail"] = (OverlayFile, 3) }
|
|
});
|
|
|
|
var meter = new ElementInfo { Type = 7, Width = 150, Height = 16 };
|
|
meter.Children.Add(backChild);
|
|
meter.Children.Add(frontChild);
|
|
|
|
var e = DatWidgetFactory.Create(meter, NoTex, null);
|
|
|
|
var m = Assert.IsType<UiMeter>(e);
|
|
Assert.Equal(BackL, m.BackLeft);
|
|
Assert.Equal(BackT, m.BackTile);
|
|
Assert.Equal(BackR, m.BackRight);
|
|
Assert.Equal(FrontL, m.FrontLeft);
|
|
Assert.Equal(FrontT, m.FrontTile);
|
|
Assert.Equal(FrontR, m.FrontRight);
|
|
// Overlay (ShowDetail-only, no DirectState "") must not leak into any slice slot.
|
|
Assert.NotEqual(OverlayFile, m.FrontRight);
|
|
Assert.NotEqual(OverlayFile, m.FrontTile);
|
|
}
|
|
|
|
// ── Justification build-time application (new for importer Fix A) ────────
|
|
|
|
/// <summary>
|
|
/// A Type-12 text element with HJustify=Center (the default) must produce a
|
|
/// UiText with Centered=true and RightAligned=false at build time.
|
|
/// This proves BuildText applies the dat's HJustify at construction without a
|
|
/// controller binding step.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildText_HJustifyCenter_SetsCentered()
|
|
{
|
|
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Center };
|
|
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
|
Assert.True(t.Centered);
|
|
Assert.False(t.RightAligned);
|
|
Assert.Equal(VJustify.Center, t.VerticalJustify);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A Type-12 text element with HJustify=Right must produce a UiText with
|
|
/// Centered=false and RightAligned=true at build time.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildText_HJustifyRight_SetsRightAligned()
|
|
{
|
|
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Right };
|
|
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
|
Assert.False(t.Centered);
|
|
Assert.True(t.RightAligned);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A Type-12 text element with HJustify=Left must produce a UiText with
|
|
/// Centered=false and RightAligned=false at build time.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildText_HJustifyLeft_SetsNeitherCenteredNorRight()
|
|
{
|
|
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Left };
|
|
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
|
Assert.False(t.Centered);
|
|
Assert.False(t.RightAligned);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A Type-12 text element with VJustify=Top must produce a UiText with
|
|
/// VerticalJustify=Top at build time.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildText_VJustifyTop_SetsVerticalJustifyTop()
|
|
{
|
|
var info = new ElementInfo { Type = 12, Width = 100, Height = 55, VJustify = VJustify.Top };
|
|
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
|
Assert.Equal(VJustify.Top, t.VerticalJustify);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A controller override: build-time sets Centered=true (HJustify=Center), then
|
|
/// a controller sets Centered=false afterward. The controller's override wins —
|
|
/// this is the backward-compatibility guarantee.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildText_ControllerOverrideWinsAfterBuild()
|
|
{
|
|
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Center };
|
|
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
|
Assert.True(t.Centered); // build-time value
|
|
t.Centered = false; // controller override
|
|
Assert.False(t.Centered); // override wins
|
|
}
|
|
|
|
// ── DefaultColor from dat FontColor (importer Fix B) ─────────────────────
|
|
|
|
/// <summary>
|
|
/// When ElementInfo.FontColor is set (dat carried 0x1B ColorBaseProperty),
|
|
/// DatWidgetFactory.BuildText must copy it onto UiText.DefaultColor.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildText_FontColorPresent_SetsDefaultColor()
|
|
{
|
|
var gold = new Vector4(1f, 0.82f, 0.36f, 1f);
|
|
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontColor = gold };
|
|
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
|
Assert.Equal(gold, t.DefaultColor);
|
|
}
|
|
|
|
[Fact]
|
|
public void HorizontalScrollbar_PreservesNestedCombatMeterFillSprite()
|
|
{
|
|
var track = new ElementInfo { Id = 4, Type = 3, Width = 507, Height = 14 };
|
|
track.StateMedia[""] = (0x06001919u, 1);
|
|
track.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
|
{ Id = UiStateInfo.DirectStateId, Image = new UiImageMedia(0x06001919u, 1) };
|
|
var thumb = new ElementInfo { Id = 1, Type = 1, Width = 12, Height = 14 };
|
|
thumb.StateMedia[""] = (0x06001923u, 1);
|
|
thumb.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
|
{ Id = UiStateInfo.DirectStateId, Image = new UiImageMedia(0x06001923u, 1) };
|
|
var fill = new ElementInfo { Id = 2, Type = 3, Width = 507, Height = 14 };
|
|
fill.StateMedia[""] = (0x06001200u, 1);
|
|
fill.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
|
{ Id = UiStateInfo.DirectStateId, Image = new UiImageMedia(0x06001200u, 1) };
|
|
var range = new ElementInfo
|
|
{
|
|
Id = 0x100005EFu,
|
|
Type = 3,
|
|
X = 63,
|
|
Width = 407,
|
|
Height = 14,
|
|
OriginalParentWidth = 507,
|
|
OriginalParentHeight = 14,
|
|
HasOriginalParentSize = true,
|
|
};
|
|
range.StateMedia[""] = (0x0600715Eu, 1);
|
|
range.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
|
{ Id = UiStateInfo.DirectStateId, Image = new UiImageMedia(0x0600715Eu, 1) };
|
|
var meter = new ElementInfo
|
|
{
|
|
Id = CombatUiController.PowerControlId + 1,
|
|
Type = 7,
|
|
Width = 507,
|
|
Height = 14,
|
|
Children = [fill, range],
|
|
};
|
|
var meterState = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
|
meterState.Properties.Values[0x6Fu] = new UiPropertyValue
|
|
{ Kind = UiPropertyKind.Enum, UnsignedValue = 1u };
|
|
meter.States[UiStateInfo.DirectStateId] = meterState;
|
|
var info = new ElementInfo
|
|
{
|
|
Id = CombatUiController.PowerControlId,
|
|
Type = 11,
|
|
Width = 507,
|
|
Height = 14,
|
|
Children = [track, meter, thumb],
|
|
};
|
|
|
|
var bar = Assert.IsType<UiScrollbar>(
|
|
DatWidgetFactory.Create(info, NoTex, null));
|
|
|
|
Assert.True(bar.Horizontal);
|
|
Assert.Equal(0x06001919u, bar.TrackSprite);
|
|
Assert.Equal(0x06001923u, bar.ThumbSprite);
|
|
Assert.Equal(0x06001200u, bar.ScalarFillSprite);
|
|
Assert.Equal(0x0600715Eu, bar.ScalarRangeSprite);
|
|
Assert.NotNull(bar.ScalarRangeLayoutPolicy);
|
|
Assert.False(bar.ScalarFillFromRight);
|
|
}
|
|
|
|
private static ElementInfo TextInfo(params (uint Id, UiPropertyValue Value)[] properties)
|
|
{
|
|
var info = new ElementInfo { Id = 0x10000016u, Type = 12, Width = 100, Height = 20 };
|
|
var state = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
|
foreach (var property in properties)
|
|
state.Properties.Values[property.Id] = property.Value;
|
|
info.States[UiStateInfo.DirectStateId] = state;
|
|
return info;
|
|
}
|
|
|
|
private static UiPropertyValue Bool(bool value)
|
|
=> new() { Kind = UiPropertyKind.Bool, BoolValue = value };
|
|
|
|
private static UiPropertyValue Integer(int value)
|
|
=> new() { Kind = UiPropertyKind.Integer, IntegerValue = value };
|
|
|
|
/// <summary>
|
|
/// When ElementInfo.FontColor is null (dat carried no 0x1B), DefaultColor must
|
|
/// stay at white (Vector4.One) — the backward-compatible default.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildText_FontColorAbsent_DefaultColorIsWhite()
|
|
{
|
|
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontColor = null };
|
|
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
|
Assert.Equal(Vector4.One, t.DefaultColor);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Backward-compat guarantee: a controller that sets LinesProvider with an
|
|
/// explicit per-line color AFTER build is unaffected by DefaultColor.
|
|
/// The controller's per-line color is stored in the Line record, not DefaultColor,
|
|
/// so DefaultColor changing from dat does not touch existing controller bindings.
|
|
/// </summary>
|
|
[Fact]
|
|
public void BuildText_ControllerExplicitLineColor_UnaffectedByDefaultColor()
|
|
{
|
|
// Dat sets a parchment color.
|
|
var parchment = new Vector4(0.92f, 0.90f, 0.82f, 1f);
|
|
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontColor = parchment };
|
|
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
|
|
|
// Controller overrides with an explicit gold color in the Line record.
|
|
var gold = new Vector4(1f, 0.82f, 0.36f, 1f);
|
|
t.LinesProvider = () => new[] { new UiText.Line("text", gold) };
|
|
|
|
// The line's color is gold (controller wins), NOT the dat parchment.
|
|
Assert.Equal(gold, t.LinesProvider()[0].Color);
|
|
// DefaultColor still holds the dat parchment (unmodified by the controller binding).
|
|
Assert.Equal(parchment, t.DefaultColor);
|
|
}
|
|
}
|