using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.UI; namespace AcDream.App.UI.Layout; /// /// Per-window controller for the Character window (LayoutDesc 0x2100002E, gmCharacterInfoUI). /// /// Retail fills a single scrollable UIElement_Text element (id 0x1000011d, /// m_pMainText) by calling a sequence of Update* methods that each /// AppendStringInfo into the same element. This controller replicates that report /// structure using acdream's . /// /// Section order (ported from gmCharacterInfoUI::Update 0x004ba790): /// /// Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) /// Vitals / endurance (UpdateEnduranceInfo 0x004b8eb0) /// Innate attributes (UpdateInnateAttributeInfo 0x004b87e0): /// InqAttribute 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, Focus, Self /// Skills summary (UpdateFakeSkills 0x004b8930) /// Augmentations (UpdateAugmentations 0x004b9000) /// Burden / load (UpdateLoad 0x004b8a20) /// /// Each section is separated by a blank line (retail: AppendStringInfo(m_pMainText, &var_120) /// with an empty StringInfo between sections). /// /// StringInfo resolution is NOT ported — the full dat string-table lookup is a /// later refinement. For this pilot, labels are composed directly from the well-known /// AC attribute / stat names confirmed in the decomp string literals and data identifiers. /// public static class CharacterController { /// Dat element id for the main report text (m_pMainText). Confirmed in /// gmCharacterInfoUI::PostInit 0x004b86f0: GetChildRecursive(this, 0x1000011d). public const uint MainTextId = 0x1000011du; /// White body text — matches retail's default UIElement_Text foreground. private static readonly Vector4 BodyColor = new(1f, 1f, 1f, 1f); /// Yellow section header — retail uses a different StringInfo color per section header. private static readonly Vector4 HeaderColor = new(1f, 0.85f, 0.35f, 1f); /// /// Bind the character report text element found in to /// . The report is regenerated each frame via /// so the Studio or a live session can push a fresh /// without re-binding. /// /// The element must resolve as a (Type 12 in the dat). /// If 0x1000011d is absent from the layout the bind is a no-op — partial layouts /// (e.g. test fakes) don't cause errors. /// /// Imported 0x2100002E layout tree. /// Provider returning the current . /// Retail dat font forwarded from the render stack. May be null /// (falls back to the BitmapFont debug path). public static void Bind( ImportedLayout layout, Func data, UiDatFont? datFont = null) { // 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; 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 ──────────────────────────────────────────────────────── /// /// Build the complete character report as a line list. /// Order mirrors gmCharacterInfoUI::Update (0x004ba790). /// private static IReadOnlyList BuildReport(CharacterSheet s) { var lines = new List(); // ── Section 1: Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ── // Retail InqInt(0x62) = DateOfBirth (unix timestamp); InqInt(0x7d) = TotalPlayTime; // InqInt(0x2b) = NumDeaths. AppendHeader(lines, s.Name); Append(lines, $"Level {s.Level}"); if (s.Race is not null) Append(lines, s.Race); if (s.Heritage is not null) Append(lines, s.Heritage); if (s.Title is not null) Append(lines, s.Title); AppendBlank(lines); if (s.BirthDate is not null) Append(lines, $"Birth: {s.BirthDate}"); if (s.PlayTime is not null) Append(lines, $"Age: {s.PlayTime}"); Append(lines, $"Deaths: {s.Deaths}"); AppendBlank(lines); // ── Section 2: Vitals / endurance (UpdateEnduranceInfo 0x004b8eb0) ── // Retail InqAttribute(1) = Strength base, InqAttribute(2) = Endurance base. // Computes max-stamina tier and max-health tier from (Str+End) and (End+2*End) sums. AppendHeader(lines, "Vitals"); Append(lines, $"Health: {s.HealthCurrent} / {s.HealthMax}"); Append(lines, $"Stamina: {s.StaminaCurrent} / {s.StaminaMax}"); Append(lines, $"Mana: {s.ManaCurrent} / {s.ManaMax}"); AppendBlank(lines); // ── Section 3: Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ── // InqAttribute order: 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, // Focus, Self (confirmed from the AddVariable_Int sequence in the decomp). AppendHeader(lines, "Attributes"); Append(lines, $"Strength: {s.Strength}"); Append(lines, $"Endurance: {s.Endurance}"); Append(lines, $"Quickness: {s.Quickness}"); Append(lines, $"Coordination: {s.Coordination}"); Append(lines, $"Focus: {s.Focus}"); Append(lines, $"Self: {s.Self}"); AppendBlank(lines); // ── Section 4: Skills summary (UpdateFakeSkills 0x004b8930) ── // Retail InqInt(0xb5) = SkillCredits remaining; InqInt(0xc0) = AvailableSkillCredits. AppendHeader(lines, "Skills"); Append(lines, $"Unspent skill credits: {s.UnspentSkillCredits}"); if (s.SpecializedSkillCredits > 0) Append(lines, $"Specialized credits: {s.SpecializedSkillCredits}"); AppendBlank(lines); // ── Section 5: Augmentations (UpdateAugmentations 0x004b9000) ── // Retail InqInt(0x162) = AugmentationStat; string-switch on value 1..0xb + Unknown. AppendHeader(lines, "Augmentations"); if (s.AugmentationName is not null) Append(lines, s.AugmentationName); else Append(lines, "None"); AppendBlank(lines); // ── Section 6: Burden / load (UpdateLoad 0x004b8a20) ── // Retail InqLoad → EncumbranceCapacity(Strength, AugEncumbrance). // When load >= 1.0 (overloaded): shows "X burden over capacity; Y% speed penalty". // When aug > 0: shows "N augmentations (X.X% bonus)". AppendHeader(lines, "Encumbrance"); Append(lines, $"Burden: {s.BurdenCurrent}"); Append(lines, $"Capacity: {s.BurdenMax}"); if (s.BurdenMax > 0) { int pct = (int)Math.Round(100.0 * s.BurdenCurrent / s.BurdenMax); Append(lines, $"Load: {pct}%"); } return lines; } // ── Line helpers ────────────────────────────────────────────────────────── private static void AppendHeader(List lines, string text) => lines.Add(new UiText.Line(text, HeaderColor)); private static void Append(List lines, string text) => lines.Add(new UiText.Line(text, BodyColor)); private static void AppendBlank(List lines) => lines.Add(new UiText.Line(string.Empty, BodyColor)); }