using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; using AcDream.Core.Chat; using AcDream.Core.Items; using AcDream.Core.Properties; using AcDream.Core.Selection; using AcDream.Runtime.Gameplay; using AcDream.UI.Abstractions; using AcDream.UI.Abstractions.Panels.Chat; namespace AcDream.App.Tests.UI.Layout; /// /// CT-GF1 fix round (S1): a draw-capture regression sweep mounting each major /// retained window through its REAL controller (the same production Bind /// entry points other suites already exercise individually) with a NON-ZERO /// sprite resolver, drawing through a , and /// asserting a per-window VERTEX-COUNT FLOOR plus the presence of one KEY /// sprite id. Every key sprite id is read LIVE off the bound /// controller/element after Bind — never a hardcoded numeric guess (this /// project's workflow forbids guessing dat ids) — so the assertion tracks /// whatever the real import/bind pipeline actually resolved, not an /// assumption about it. /// /// This is the class of coverage the CH6a/b BLOCKER 1 bug slipped past: an /// authored non-zero sprite id sitting right there on the ElementInfo, with /// nothing actually reaching DrawSprite because the widget was built /// without its resolve delegate. A structural/geometry test (FindElement, /// property checks) cannot see that class of regression; only an actual draw /// pass through a real render context can. /// /// /// Windows covered: Character (this is ALSO CT-GF1's own motivating /// case — its chrome draws via /// , the exact code path /// the S2/item-1 fix-round caution note calls out as needing re-proof after /// moving the ambient clip to wrap it), Chat (Imported chrome, real /// committed fixture), Vendor (Imported chrome, real committed /// fixture), Options (the 4-tab panel host — no /// wrapper; mounts as a bare /// , matching OptionsPanelControllerTests's /// own established pattern). SKIPPED for lack of a reusable, fixture-driven, /// SINGLE top-level Bind entry point at the time of writing: Inventory/ /// Paperdoll (composed from several independent controllers, no single /// window-level Bind), the social panel and the map/house host (their own /// mount probes are Lane=Manual, live-DAT-only — see /// SocialPanelLiveMountProbeTests/MapHousePanelSlotProbeTests). /// /// /// /// Floors are set at roughly 40-45% of this session's OBSERVED vertex count /// per window (Character 588, Chat 162, Vendor 54, Options 240 — see each /// Build* method's own trailing comment) — per the fix-round /// instruction, loose enough to survive legitimate content growth/shrinkage, /// tight enough to still catch "half (or all) of this window stopped /// drawing" (a resolve-wiring regression), not exact counts. /// /// public sealed class UiWindowDrawCaptureSweepTests { private sealed class NullGpuFrameSource : ICurrentGpuFrameSource { public IGpuFrame? CurrentFrame => null; } private static (RecordingGpuDevice device, TextRenderer renderer, UiRenderContext ctx) MakeContext( float w, float h) { var device = new RecordingGpuDevice(); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(w, h)); var ctx = new UiRenderContext(renderer, new Vector2(w, h)); return (device, renderer, ctx); } /// Total vertex count across every recorded sprite segment — the /// same per-segment Verts.Count / 8 convention every other test in /// this suite uses (8 floats packed per vertex). private static int TotalVertexCount(TextRenderer renderer) { int total = 0; foreach (var seg in renderer.DebugSpriteSegmentVerts) total += seg.Verts.Count / 8; return total; } public static IEnumerable Windows() { yield return new object[] { "Character" }; yield return new object[] { "Chat" }; yield return new object[] { "Vendor" }; yield return new object[] { "Options" }; } [Theory] [MemberData(nameof(Windows))] public void MountedWindow_DrawsAVertexFloor_AndItsLiveKeySpriteId(string window) { (UiElement drawRoot, uint keySprite, int vertexFloor) = window switch { "Character" => BuildCharacter(), "Chat" => BuildChat(), "Vendor" => BuildVendor(), "Options" => BuildOptions(), _ => throw new ArgumentOutOfRangeException(nameof(window), window, null), }; Assert.NotEqual(0u, keySprite); var (_, renderer, ctx) = MakeContext(1600f, 1200f); drawRoot.DrawSelfAndChildren(ctx); int vertices = TotalVertexCount(renderer); Assert.True( vertices >= vertexFloor, $"{window}: expected at least {vertexFloor} drawn vertices, got {vertices} " + "-- a resolve-wiring regression (CH6a/b BLOCKER 1's class) would show up as a " + "near-zero count here."); Assert.Contains( renderer.DebugSpriteSegmentVerts, s => s.Texture == keySprite); } private static (UiElement drawRoot, uint keySprite, int vertexFloor) BuildCharacter() { ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadCharacterInfos(), id => (id, 8, 8), null); CharacterStatController.Bind( layout, SampleData.SampleCharacter, spriteResolve: id => (id, 8, 8)); var screen = new UiRoot { Width = 1600f, Height = 1200f }; RetailWindowHandle handle = RetailWindowFrame.Mount( screen, layout.Root, id => (id, 8, 8), new RetailWindowFrame.Options { WindowName = WindowNames.Character, Chrome = RetailWindowChrome.NineSlice, ContentHeight = 362f, MinWidth = 310f, MaxWidth = 310f, MinHeight = 372f, MaxHeight = 1000f, ResizeX = false, ResizeY = true, ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, }); // Key sprite: RetailChromeSprites.TopEdge -- drawn by // UiNineSlicePanel.OnDrawAfterChildren, the exact code path the S2/ // item-1 fix-round caution note calls out. Not read "live" (it's a // shared named constant, not a per-window authored id), but it is // NOT a guess either -- it is the actual constant the chrome drawer // uses, verified by reading UiNineSlicePanel.OnDrawAfterChildren. return (handle.OuterFrame, RetailChromeSprites.TopEdge, 250); // observed baseline 588 } private static (UiElement drawRoot, uint keySprite, int vertexFloor) BuildChat() { var infos = FixtureLoader.LoadChatInfos(); ImportedLayout layout = LayoutImporter.Build(infos, id => (id, 8, 8), null); var controller = ChatWindowController.Bind( infos, layout, new ChatVM(new ChatLog()), () => NullCommandBus.Instance, new ChatWindowState(), null, null, id => (id, 8, 8)); Assert.NotNull(controller); var root = new UiRoot { Width = 1600f, Height = 1200f }; RetailWindowHandle handle = RetailWindowFrame.Mount( root, controller!.Root, id => (id, 8, 8), new RetailWindowFrame.Options { WindowName = WindowNames.Chat, Chrome = RetailWindowChrome.Imported, Left = 10f, Top = 10f, DatConstraintSource = controller.DatWindowInfo, }); controller.AttachWindow(handle); // Key sprite: read LIVE off the bound scrollbar's own TrackSprite -- // the scrollbar always draws its track whenever the window does, so // this tracks whatever the real fixture actually authors rather than // a hardcoded literal. return (handle.OuterFrame, controller.Scrollbar.TrackSprite, 80); // observed baseline 162 } private static (UiElement drawRoot, uint keySprite, int vertexFloor) BuildVendor() { ImportedLayout layout = FixtureLoader.LoadVendor(); var screen = new UiRoot { Width = 1600f, Height = 1200f }; RetailWindowHandle window = RetailWindowFrame.Mount( screen, layout.Root, id => (id, 8, 8), new RetailWindowFrame.Options { WindowName = "vendor-sweep", Chrome = RetailWindowChrome.Imported, Visible = true, }); var objects = new ClientObjectTable(); var itemInteraction = new ItemInteractionController( objects, new RuntimeInteractionTransactionState(new InventoryTransactionState(objects)), new InteractionState(), playerGuid: static () => 0u, sendUse: null, sendUseWithTarget: null, sendWield: null, sendDrop: null); VendorUiController? controller = VendorUiController.Bind( layout, new VendorState(), window, static (_, iconId, _, _, _) => iconId, objects, static () => 0u, itemInteraction, new SelectionState(), new StackSplitQuantityState(), datFont: null, debugFont: null, id => (id, 8, 8)); Assert.NotNull(controller); // Key sprite: read LIVE off the bound category-filter menu's own // button-face sprite (0x100000BF, VendorUiController.TypeFilterMenuId) -- // its face always draws whenever the window does, so this tracks // whatever the real committed vendor fixture actually authors. var typeMenu = Assert.IsType(layout.FindElement(VendorUiController.TypeFilterMenuId)); return (window.OuterFrame, typeMenu.NormalSprite, 25); // observed baseline 54 } private static (UiElement drawRoot, uint keySprite, int vertexFloor) BuildOptions() { // NOTE: FixtureLoader.LoadOptionsPanelHost() (the convenience wrapper) bakes // in FixtureLoader's own NULL-returning sprite resolver at LayoutImporter.Build // time -- permanent for every widget it builds, unaffected by whatever resolver // is later passed into OptionsPanelController.Bind (which only reaches content // that controller creates itself, e.g. the per-page footer backing). Building // from the raw ElementInfo tree here (matching BuildCharacter/BuildChat/ // BuildVendor's own pattern above) is what actually gets a non-zero resolver // onto the imported header/tab/page widgets themselves. ImportedLayout layout = LayoutImporter.Build( FixtureLoader.LoadOptionsPanelHostInfos(), id => (id, 8, 8), null); var callbacks = new OptionsPanelController.Callbacks( Toggle: () => { }, RequestExitToCharacterSelection: () => { }, ExitGame: () => { }, UseMouseTurningSettings: () => { }, DisplaySystemMessage: _ => { }); OptionsPanelController? controller = OptionsPanelController.Bind( layout, callbacks, resolveSprite: id => (id, 8, 8)); Assert.NotNull(controller); controller!.ActivateTabs(); // Key sprite: RetailChromeSprites.CenterFill -- the exact id // OptionsPanelControllerTests' own CollectFooterBackings helper pins // as every page's footer backing (a UiSolidSpriteFill), read here as // the same shared named constant, not a guess. return (layout.Root, RetailChromeSprites.CenterFill, 120); // observed baseline 240 } }