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); } // ── 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); Assert.IsType(e); } // ── 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); } // ── 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); } // ── 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); } }