feat(ui): Campaign CT slice CT4 — header identity block retail-exact
Retires the rest of AP-109's UI half: the character panel's Name/Heritage/ PkStatus/Level header identity block is now live and DAT-faithful on both Attributes and Skills pages (verified: CharacterStatController.Bind already scopes Label/LabelAuthoredColor to the ONE physically-visible page container, so both tabs share the same bound widgets). - Name/Heritage/PkStatus/Level switch from hand-picked Body/Gold runtime colors to the widget's own authored DefaultColor (LabelAuthoredColor) — CT1's live-DAT pin (HeaderElements_AuthorExpectedFontsAndColors) confirmed all four already carry the correct FontColor (white/white/white/pale-gold with Outline); the former "runtime color, dat carries none" comment was false. - PkStatus resolves through StringTable 0x23000001 by key (ID_StatManagement_Header_PKStatus_PK/_PKL/_NPK) with a bitwise IsPK/IsPKLite test (gmStatManagementUI::UpdatePKStatus @0x004F00A0) instead of the prior exact-equality switch, which silently dropped combined-flag PlayerKillerStatus values. Live-DAT-verified strings: "Player Killer" / "Player Killer Lite" / "Non-Player Killer" (new InstalledDat pin PkStatusKeys_ResolveExpectedAuthoredStrings). - Level shows "%d"-formatted InqInt(0x19) or the PE-recovered literal "???" when absent (CharacterSheet.Level is now int?). - Heritage line appends CT2/CT3's resolved RuntimeCharacterTitleState display title through CharacterTitleResolver, refreshing live on both TableReplaced (0x0029) and DisplayTitleChanged (0x002B) — CharacterSheetProvider's ChangeBinding now subscribes to both. - Name-line ruling: ships the PLAIN-NAME case only. Retail's allegiance rank-title prefix (AllegianceData::GetFullName @0x005B6950 -> AllegianceSystem::GetTitle @0x005B8DD0) needs a ~200-string, 22-function heritage x gender table (verbatim decomp literals, e.g. GetAluvianMaleTitle @0x005B7BC0's Yeoman/Baronet/.../High King) judged out of reasonable size for this slice. RuntimeAllegianceState already carries the local player's own rank; only the string table is missing. Registered, not silently omitted. - Luminance pair (0x100005C5/0x100005C6): CharacterSheet.AvailableLuminance/ MaximumLuminance (PropertyInt64 6/7) already flow generically through both the PlayerDescription snapshot and the live 0x02CF private-update parsers (no wiring gap). The retail show/hide gate (Level >= 200 && MaximumLuminance != 0, UpdateExperience @0x004F0A70) is wired and toggles Visible on both elements every sheet refresh; the exact caption/value text could not be recovered this slice (retail's SetText source resolves through a Binary-Ninja-mislabeled data pointer, not a StringTable key — a DAT string-table sweep found no match), so content stays unbound rather than guessed. - AP-109 narrowed accordingly (register row amended in the same commit). Tests: CharacterStatControllerTests (heritage composition + live title update, name stays plain, level int/"???" with authored — not constant — color across 3 cases, PK line shows resolved text in authored color across 3 statuses, luminance visibility across 5 level/luminance combinations) and CharacterSheetProviderTests (PK key-by-status resolution including a combined-flag case, no-resolver leaves PkStatus null, Level null-vs-present, title resolution + live refresh on both title events + unsubscribe-on- dispose, luminance Int64 read-through). Full hermetic solution suite green under Release (0 failures across all 14 test projects); InstalledDat pins green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
aa8106d57a
commit
ed652ed8ad
10 changed files with 646 additions and 32 deletions
|
|
@ -70,6 +70,17 @@ public static class CharacterStatController
|
|||
public const uint ListScrollbarId = 0x1000023Eu; // m_pListBox vertical scrollbar gutter
|
||||
public const uint ListDividerId = 0x1000023Fu; // bottom divider above footer
|
||||
|
||||
/// <summary>Campaign CT slice CT4: the luminance pair
|
||||
/// (m_pLuminanceLabelText/m_pLuminanceText), shown only past level 200
|
||||
/// with nonzero MaximumLuminance — see
|
||||
/// <c>gmStatManagementUI::UpdateExperience</c> (0x004f0a70)'s luminance
|
||||
/// branch. The label's own retail caption/value StringInfo could not be
|
||||
/// recovered this slice (its SetText calls resolve through a
|
||||
/// Binary-Ninja-mislabeled data pointer, not a StringTable key — see the
|
||||
/// Bind method's own remarks); only the show/hide gate is wired here.</summary>
|
||||
public const uint LuminanceLabelId = 0x100005C5u;
|
||||
public const uint LuminanceValueId = 0x100005C6u;
|
||||
|
||||
// ── Footer STATE-A container id ──────────────────────────────────────────
|
||||
// 0x10000240 is the "nothing selected" footer group. Its children (0x1000024E label row,
|
||||
// 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider
|
||||
|
|
@ -111,7 +122,10 @@ public static class CharacterStatController
|
|||
public const uint RaiseTenId = 0x100005EBu; // raise × 10
|
||||
|
||||
private static readonly Vector4 Body = new(0.92f, 0.90f, 0.82f, 1f); // parchment-white body text
|
||||
private static readonly Vector4 Gold = new(1f, 0.82f, 0.36f, 1f); // section / emphasis gold
|
||||
// Campaign CT slice CT4 (2026-08-24): the former hand-picked "Gold"
|
||||
// header-level color constant is deleted — CT1's live-DAT pin confirmed
|
||||
// the level element authors its own pale-gold FontColor (+ Outline);
|
||||
// LabelAuthoredColor reads it from the widget instead.
|
||||
|
||||
/// <summary>Row highlight color — semi-translucent gold, matches retail
|
||||
/// UIStateId.Highlight (0x06) sprite 0x06001397 visual intent.</summary>
|
||||
|
|
@ -292,11 +306,22 @@ public static class CharacterStatController
|
|||
|
||||
// Name (18px from dat FontDid), Heritage (14px), PkStatus (14px):
|
||||
// Fix C: pass null → Label's null-guard keeps the build-time dat font.
|
||||
// Controllers still own the text color and the LinesProvider.
|
||||
// Name = WHITE (retail "Horan" is white — confirmed 2026-06-26).
|
||||
Label(layout, contentPage, NameId, null, Vector4.One, () => data().Name);
|
||||
Label(layout, contentPage, HeritageId, null, Body, () => CharacterIdentityText.StatHeaderLine(data()));
|
||||
Label(layout, contentPage, PkStatusId, null, Body, () => data().PkStatus ?? string.Empty);
|
||||
// Campaign CT slice CT4 (2026-08-24): CT1's live-DAT pin
|
||||
// (HeaderElements_AuthorExpectedFontsAndColors) confirmed all FOUR
|
||||
// header identity elements — Name, Heritage, PkStatus, Level — carry
|
||||
// their own authored FontColor (white/white/white/pale-gold with
|
||||
// Outline). The "runtime color, dat carries none" reasoning this
|
||||
// block used to justify a hand-picked Body/Gold constant per element
|
||||
// was FALSIFIED by that pin: every element below now sources its
|
||||
// color from the widget's own DAT-set DefaultColor
|
||||
// (LabelAuthoredColor), matching the "authored color/font wins"
|
||||
// pattern CT3's CharacterTitlesController already established for
|
||||
// its row/display text. Level's Outline is likewise already applied
|
||||
// at import time (DatWidgetFactory.BuildText reads dat property
|
||||
// 0x21) — no controller-side Outline flag needed.
|
||||
LabelAuthoredColor(layout, contentPage, NameId, null, () => data().Name);
|
||||
LabelAuthoredColor(layout, contentPage, HeritageId, null, () => CharacterIdentityText.StatHeaderLine(data()));
|
||||
LabelAuthoredColor(layout, contentPage, PkStatusId, null, () => data().PkStatus ?? string.Empty);
|
||||
|
||||
// ── Header captions (new — retail labels above/left of each number) ──────
|
||||
// LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font.
|
||||
|
|
@ -305,12 +330,13 @@ public static class CharacterStatController
|
|||
// Level number: retail renders this as large gold centered text in the 65×50 element.
|
||||
// Fix C: the dat FontDid for the level element (0x1000023B) is now applied at build
|
||||
// time when the font resolver is provided (studio path). We no longer force rowDatFont
|
||||
// here for the level — the dat's own FontDid drives the font. The Gold color is still
|
||||
// set via LinesProvider. SYNTHESIZED elements (the 9 attribute rows built in
|
||||
// BuildAttributeRows) continue to use datFont directly since they have no dat origin.
|
||||
// Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770.
|
||||
// runtime color, dat carries none.
|
||||
Label(layout, contentPage, LevelId, null, Gold, () => data().Level.ToString());
|
||||
// here for the level — the dat's own FontDid drives the font.
|
||||
// Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo
|
||||
// 0x004f0770. CT4 contract (PE-recovered 2026-08-24): InqInt(0x19) present formats
|
||||
// with "%d" semantics (a bare integer, data_7a0184); absent shows the literal "???"
|
||||
// (data_7b0f34) — CharacterSheet.Level is null in that case.
|
||||
LabelAuthoredColor(layout, contentPage, LevelId, null,
|
||||
() => data().Level is int lvl ? lvl.ToString(CultureInfo.InvariantCulture) : "???");
|
||||
|
||||
// TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font.
|
||||
LabelLeft(layout, contentPage, TotalXpLabelId, null, Body, static () => "Total Experience (XP):");
|
||||
|
|
@ -367,6 +393,32 @@ public static class CharacterStatController
|
|||
}
|
||||
}
|
||||
|
||||
// ── Luminance pair (0x100005C5/C6) — CT4 item 5 ───────────────────────
|
||||
// gmStatManagementUI::UpdateExperience (0x004f0a70): InqInt64(6)
|
||||
// (AvailableLuminance) and InqInt64(7) (MaximumLuminance) are read
|
||||
// unconditionally, but the pair is hidden — UIElement_Text::ClearAllText
|
||||
// on BOTH m_pLuminanceLabelText and m_pLuminanceText — whenever
|
||||
// "InqInt(0x19) < 0xc8 (200) || MaximumLuminance == 0". Only the
|
||||
// gate is ported this slice: the label's caption and the value's
|
||||
// composed "available / maximum" string both resolve through a
|
||||
// SetText call whose source string BN mislabels as a vftable slot
|
||||
// (not a StringTable key like the PK line) — recovering the exact
|
||||
// literal needs a PE-byte-decode pass this slice didn't budget for
|
||||
// (register row: AP-109 narrows to exactly this). Content is
|
||||
// intentionally left unbound (blank) rather than guessed; only
|
||||
// Visible is toggled, so a level-200+ character sees an empty
|
||||
// (not wrong) pair until a follow-up slice fills it in.
|
||||
UiElement? luminanceLabel = FindElementByDatId(layout, contentPage, LuminanceLabelId);
|
||||
UiElement? luminanceValue = FindElementByDatId(layout, contentPage, LuminanceValueId);
|
||||
void RefreshLuminanceVisibility()
|
||||
{
|
||||
var sheet = data();
|
||||
bool visible = sheet.Level is int lvl && lvl >= 200 && sheet.MaximumLuminance != 0;
|
||||
if (luminanceLabel is not null) luminanceLabel.Visible = visible;
|
||||
if (luminanceValue is not null) luminanceValue.Visible = visible;
|
||||
}
|
||||
RefreshLuminanceVisibility();
|
||||
|
||||
// The tab visuals are already retained in the imported LayoutDesc. Controllers
|
||||
// bind only click behavior and the active Open/Closed state below.
|
||||
|
||||
|
|
@ -656,6 +708,11 @@ public static class CharacterStatController
|
|||
}
|
||||
|
||||
RefreshActiveRaiseButtons();
|
||||
// CT4: the luminance gate reads Level/MaximumLuminance off the
|
||||
// CURRENT sheet, so it must re-run on every sheet-changed refresh
|
||||
// (level-up, a luminance-award quality change), not only at bind
|
||||
// time.
|
||||
RefreshLuminanceVisibility();
|
||||
}
|
||||
|
||||
return () => RefreshAfterRaise(null);
|
||||
|
|
@ -2009,6 +2066,31 @@ public static class CharacterStatController
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same binding shape as <see cref="Label"/>, but the per-line color is
|
||||
/// read from the widget's own <see cref="UiText.DefaultColor"/> — the
|
||||
/// value <c>DatWidgetFactory.BuildText</c> already seeded from the
|
||||
/// element's authored dat property 0x1B — instead of a caller-supplied
|
||||
/// constant. Campaign CT slice CT4 (2026-08-24): the header identity
|
||||
/// block's four elements (Name/Heritage/PkStatus/Level) all carry their
|
||||
/// own correct authored color (CT1's live-DAT pin), so "authored color
|
||||
/// wins" here is both simpler and more correct than hand-picking a
|
||||
/// runtime constant — the same precedent
|
||||
/// <see cref="CharacterTitlesController"/>'s row/display text already
|
||||
/// set (<c>rowText.DefaultColor</c>).
|
||||
/// </summary>
|
||||
private static void LabelAuthoredColor(ImportedLayout layout, UiElement? scope, uint id, UiDatFont? datFont, Func<string> text)
|
||||
{
|
||||
if (FindTextByDatId(layout, scope, id) is UiText t)
|
||||
{
|
||||
if (datFont is not null) t.DatFont = datFont;
|
||||
t.Centered = true;
|
||||
t.OneLine = true;
|
||||
t.ClickThrough = true;
|
||||
t.LinesProvider = () => new[] { new UiText.Line(text(), t.DefaultColor) };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Two-line centered label. Provides TWO lines from LinesProvider so both
|
||||
/// fit side-by-side in a narrow element without truncation. The scroll path in
|
||||
/// <see cref="UiText"/> renders multiple lines oldest-first (top-to-bottom), so
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue