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(e); } [Fact] public void RetailRadarClass_MakesUiRadar() { var e = DatWidgetFactory.Create( new ElementInfo { Type = UiRadar.RetailClassId, Width = 120, Height = 140 }, NoTex, null); Assert.IsType(e); } // ── Test 2: Unknown type → UiDatElement fallback ───────────────────────── [Fact] public void UnknownType_FallsBackToGeneric() { var e = DatWidgetFactory.Create(new ElementInfo { Type = 999 }, NoTex, null); Assert.IsType(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(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(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(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 ─────────────────────────── /// /// 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. /// [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(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(DatWidgetFactory.Create(withMedia, NoTex, null)); Assert.IsType(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(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(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(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(layout.FindElement(0x100001A3u)); Assert.True(entry.RightAligned); var bar = Assert.IsType(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(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(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(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(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); } // ── Test 6: Meter slice extraction (the important one) ─────────────────── /// /// 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. /// [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(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) ──────── /// /// 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. /// [Fact] public void BuildText_HJustifyCenter_SetsCentered() { var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Center }; var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null)); Assert.True(t.Centered); Assert.False(t.RightAligned); Assert.Equal(VJustify.Center, t.VerticalJustify); } /// /// A Type-12 text element with HJustify=Right must produce a UiText with /// Centered=false and RightAligned=true at build time. /// [Fact] public void BuildText_HJustifyRight_SetsRightAligned() { var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Right }; var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null)); Assert.False(t.Centered); Assert.True(t.RightAligned); } /// /// A Type-12 text element with HJustify=Left must produce a UiText with /// Centered=false and RightAligned=false at build time. /// [Fact] public void BuildText_HJustifyLeft_SetsNeitherCenteredNorRight() { var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Left }; var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null)); Assert.False(t.Centered); Assert.False(t.RightAligned); } /// /// A Type-12 text element with VJustify=Top must produce a UiText with /// VerticalJustify=Top at build time. /// [Fact] public void BuildText_VJustifyTop_SetsVerticalJustifyTop() { var info = new ElementInfo { Type = 12, Width = 100, Height = 55, VJustify = VJustify.Top }; var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null)); Assert.Equal(VJustify.Top, t.VerticalJustify); } /// /// 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. /// [Fact] public void BuildText_ControllerOverrideWinsAfterBuild() { var info = new ElementInfo { Type = 12, Width = 100, Height = 20, HJustify = HJustify.Center }; var t = Assert.IsType(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) ───────────────────── /// /// When ElementInfo.FontColor is set (dat carried 0x1B ColorBaseProperty), /// DatWidgetFactory.BuildText must copy it onto UiText.DefaultColor. /// [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(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( 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 }; /// /// When ElementInfo.FontColor is null (dat carried no 0x1B), DefaultColor must /// stay at white (Vector4.One) — the backward-compatible default. /// [Fact] public void BuildText_FontColorAbsent_DefaultColorIsWhite() { var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontColor = null }; var t = Assert.IsType(DatWidgetFactory.Create(info, NoTex, null)); Assert.Equal(Vector4.One, t.DefaultColor); } /// /// 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. /// [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(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); } }