diff --git a/src/AcDream.App/UI/Layout/CharacterController.cs b/src/AcDream.App/UI/Layout/CharacterController.cs index 6067525d..709c8262 100644 --- a/src/AcDream.App/UI/Layout/CharacterController.cs +++ b/src/AcDream.App/UI/Layout/CharacterController.cs @@ -61,15 +61,28 @@ public static class CharacterController Func data, UiDatFont? datFont = null) { - if (layout.FindElement(MainTextId) is not UiText text) - return; + // Retail's gmCharacterInfoUI CREATES m_pMainText (0x1000011d) at RUNTIME — it is NOT a static + // element in any LayoutDesc (confirmed: acdream's dat-import of 0x2100002E AND 0x2100006E both + // lack it; only a runtime UI-tree capture has it). So replicate that: build the report text + // element ourselves and place it in the panel body, exactly as the gm*UI does at runtime. + var root = layout.Root; + if (root is null) return; - text.DatFont = datFont; - text.ClickThrough = false; // retail allows text selection in the report - text.AcceptsFocus = true; - text.IsEditControl = true; - text.CapturesPointerDrag = true; + var text = new UiText + { + EventId = MainTextId, + Name = "m_pMainText", + Left = 12f, + Top = 44f, // below the window header row + Width = System.Math.Max(40f, root.Width - 24f), + Height = System.Math.Max(40f, root.Height - 56f), + Anchors = AnchorEdges.None, + ZOrder = 1_000_000, // draw above the static chrome + DatFont = datFont, + ClickThrough = false, + }; text.LinesProvider = () => BuildReport(data()); + root.AddChild(text); } // ── Report builder ──────────────────────────────────────────────────────── diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs index ed61904a..0f92ca07 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs @@ -6,23 +6,31 @@ namespace AcDream.App.Tests.UI.Layout; /// /// Unit tests for and . +/// The controller CREATES the m_pMainText report element (0x1000011d) at bind time — retail's +/// gm*UI builds it at runtime; it is not a static dat element — and attaches it to the layout root. /// No dats, no GL — pure data-wiring + report-content tests. /// public class CharacterControllerTests { - // ── Test 1: Bind finds the main text element and sets LinesProvider ─────── + /// Bind on an empty layout, then return the created m_pMainText element. + private static UiText BindAndGetText(System.Func data) + { + var layout = FakeLayout(); + CharacterController.Bind(layout, data); + return layout.Root.Children.OfType() + .First(t => t.EventId == CharacterController.MainTextId); + } + + private static string Report(System.Func data) + => string.Join("\n", BindAndGetText(data).LinesProvider().Select(l => l.Text)); + + // ── Test 1: Bind creates m_pMainText with a non-empty report ────────────── [Fact] - public void Bind_SetsLinesProviderOnMainText() + public void Bind_CreatesMainTextWithReport() { - var text = new UiText(); - var layout = FakeLayout((CharacterController.MainTextId, text)); - - CharacterController.Bind(layout, SampleData.SampleCharacter); - - // LinesProvider must be replaced (default returns empty). - var lines = text.LinesProvider(); - Assert.True(lines.Count > 0, "Expected non-empty report after Bind"); + var text = BindAndGetText(SampleData.SampleCharacter); + Assert.True(text.LinesProvider().Count > 0, "Expected non-empty report after Bind"); } // ── Test 2: Report contains all six attribute names (decomp order 1,2,4,3,5,6) ─── @@ -30,13 +38,7 @@ public class CharacterControllerTests [Fact] public void Bind_ReportContainsAllSixAttributes() { - var text = new UiText(); - var layout = FakeLayout((CharacterController.MainTextId, text)); - - CharacterController.Bind(layout, SampleData.SampleCharacter); - - var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text)); - + var allText = Report(SampleData.SampleCharacter); Assert.Contains("Strength", allText); Assert.Contains("Endurance", allText); Assert.Contains("Quickness", allText); @@ -50,13 +52,7 @@ public class CharacterControllerTests [Fact] public void Bind_ReportContainsVitals() { - var text = new UiText(); - var layout = FakeLayout((CharacterController.MainTextId, text)); - - CharacterController.Bind(layout, SampleData.SampleCharacter); - - var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text)); - + var allText = Report(SampleData.SampleCharacter); Assert.Contains("Health", allText); Assert.Contains("Stamina", allText); Assert.Contains("Mana", allText); @@ -67,13 +63,7 @@ public class CharacterControllerTests [Fact] public void Bind_ReportContainsBurden() { - var text = new UiText(); - var layout = FakeLayout((CharacterController.MainTextId, text)); - - CharacterController.Bind(layout, SampleData.SampleCharacter); - - var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text)); - + var allText = Report(SampleData.SampleCharacter); Assert.Contains("Burden", allText); Assert.Contains("Capacity", allText); } @@ -82,42 +72,24 @@ public class CharacterControllerTests [Fact] public void Bind_ReportContainsSampleName() - { - var text = new UiText(); - var layout = FakeLayout((CharacterController.MainTextId, text)); + => Assert.Contains("Studio Player", Report(SampleData.SampleCharacter)); - CharacterController.Bind(layout, SampleData.SampleCharacter); - - var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text)); - - // SampleData.SampleCharacter().Name == "Studio Player" - Assert.Contains("Studio Player", allText); - } - - // ── Test 6: Missing element id is a no-op (no throw) ───────────────────── + // ── Test 6: Bind on a layout WITHOUT the element creates it (no throw) ──── [Fact] - public void Bind_MissingElement_DoesNotThrow() + public void Bind_OnEmptyLayout_CreatesElement() { - // Layout does not contain MainTextId — Bind must silently skip. - var layout = FakeLayout(); // empty layout - - // Must not throw. + var layout = FakeLayout(); // no MainTextId present CharacterController.Bind(layout, SampleData.SampleCharacter); + Assert.Contains(layout.Root.Children.OfType(), + t => t.EventId == CharacterController.MainTextId); } - // ── Test 7: Element that is NOT a UiText is silently skipped ───────────── + // ── Test 7: The created element carries the m_pMainText id ─────────────── [Fact] - public void Bind_WrongElementType_DoesNotThrow() - { - // Place a UiMeter at the MainTextId — Bind must skip (not crash on wrong type). - var meter = new UiMeter(); - var layout = FakeLayout((CharacterController.MainTextId, meter)); - - CharacterController.Bind(layout, SampleData.SampleCharacter); - // No exception; meter is unchanged. - } + public void Bind_CreatedElementHasMainTextId() + => Assert.Equal(CharacterController.MainTextId, BindAndGetText(SampleData.SampleCharacter).EventId); // ── Test 8: SampleCharacter fixture has non-zero attribute values ───────── @@ -125,7 +97,6 @@ public class CharacterControllerTests public void SampleCharacter_AttributesAreNonZero() { var s = SampleData.SampleCharacter(); - Assert.True(s.Strength > 0, "Strength must be > 0"); Assert.True(s.Endurance > 0, "Endurance must be > 0"); Assert.True(s.Quickness > 0, "Quickness must be > 0"); @@ -140,7 +111,6 @@ public class CharacterControllerTests public void SampleCharacter_VitalsAreCoherent() { var s = SampleData.SampleCharacter(); - Assert.True(s.HealthCurrent > 0 && s.HealthCurrent <= s.HealthMax, "Health cur must be in [1, max]"); Assert.True(s.StaminaCurrent > 0 && s.StaminaCurrent <= s.StaminaMax, "Stamina cur must be in [1, max]"); Assert.True(s.ManaCurrent > 0 && s.ManaCurrent <= s.ManaMax, "Mana cur must be in [1, max]"); @@ -151,15 +121,8 @@ public class CharacterControllerTests [Fact] public void Bind_ReportContainsSampleAttributeValues() { - var text = new UiText(); - var layout = FakeLayout((CharacterController.MainTextId, text)); - var sheet = SampleData.SampleCharacter(); - CharacterController.Bind(layout, () => sheet); - - var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text)); - - // All six attribute values should appear as strings in the report. + var allText = Report(() => sheet); Assert.Contains(sheet.Strength.ToString(), allText); Assert.Contains(sheet.Endurance.ToString(), allText); Assert.Contains(sheet.Quickness.ToString(), allText);