feat(ui): importer Fix C — per-element dat FontDid resolver (studio path); character level uses its retail font
DatWidgetFactory.Create now accepts an optional fontResolve: Func<uint,UiDatFont?> parameter. When supplied and the element has a non-zero FontDid, the element receives its own dat font instead of the shared global datFont fallback. Null = original single-font behavior (the live GameWindow path passes null — provably unchanged). LayoutImporter.Build/BuildFromInfos/Import all thread the optional resolver down to the factory. RenderStack gains a lazy font cache (ConcurrentDictionary, pre-seeded with VitalsDatFont + LargeDatFont) and a ResolveDatFont(uint) method. StudioWindow wires stack.ResolveDatFont into LayoutSource so every studio import gets per-element fonts. GameWindow import calls left passing null (follow-up todo). CharacterStatController font-hack cleanup (diagnosed via one-shot console dump then removed): - Name (0x10000231): dat FontDid = 18px font — remove datFont override (null) - Heritage/PkStatus: dat FontDid = 14px fonts — remove override - LevelCaption: dat FontDid = 16px — remove override (same font, no visual change) - Level (0x1000023B): dat FontDid = 36px (the big retail gold font) — was forced to rowDatFont/LargeDatFont (18px); now drops to null so the dat 36px font drives - TotalXpLabel/TotalXp: dat FontDid = 16px — remove override - FooterTitle (0x1000024E): dat FontDid = 20px — remove datFont override - KEEP: synthesized elements (XP meter overlays, 9 attribute rows, tab sprites) still use datFont directly since they have no dat origin All Label/LabelTwoLine/LabelLeft/LabelProvider helpers updated: null = keep build-time dat font; non-null = controller explicit override (backward-compat). 8 new tests in DatWidgetFactoryFontResolveTests: - null resolver → DatFont == global datFont - FontDid=0 → resolver not called - resolver returns null → fallback to global datFont - resolver called with element's FontDid - controller DatFont override wins after build - LayoutImporter.Build threads fontResolve to factory - meter element fires resolver for non-zero FontDid - BuildFromInfos without fontResolve param = original behavior Build + all 710 App tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6e0be4bd34
commit
a0d33956f6
7 changed files with 360 additions and 66 deletions
|
|
@ -181,35 +181,32 @@ public static class CharacterStatController
|
|||
// rowDatFont: larger font for attribute row name/value text (18px vs 16px default).
|
||||
// Falls back to datFont when null (tests, or dat missing).
|
||||
rowDatFont ??= datFont;
|
||||
// Name: retail "Horan" is WHITE, not gold. Heritage and PK status are also white.
|
||||
// runtime color, dat carries none (LayoutDesc 0x2100002E property 0x1B absent — FIX-B diagnosis 2026-06-26).
|
||||
Label(layout, NameId, datFont, Vector4.One, () => data().Name);
|
||||
// runtime color, dat carries none.
|
||||
Label(layout, HeritageId, datFont, Body, () => data().Heritage ?? data().Race ?? string.Empty);
|
||||
// runtime color, dat carries none.
|
||||
Label(layout, PkStatusId, datFont, Body, () => data().PkStatus ?? string.Empty);
|
||||
|
||||
// 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, NameId, null, Vector4.One, () => data().Name);
|
||||
Label(layout, HeritageId, null, Body, () => data().Heritage ?? data().Race ?? string.Empty);
|
||||
Label(layout, PkStatusId, null, Body, () => data().PkStatus ?? string.Empty);
|
||||
|
||||
// ── Header captions (new — retail labels above/left of each number) ──────
|
||||
// 0x1000023A = "Character Level" caption area above the level value (235,0,65×35).
|
||||
// Retail shows 2 lines: "Character" (top) / "Level" (bottom) so neither truncates.
|
||||
// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1).
|
||||
// runtime color, dat carries none.
|
||||
LabelTwoLine(layout, LevelCaptionId, datFont, Body, "Character", "Level");
|
||||
// LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font.
|
||||
LabelTwoLine(layout, LevelCaptionId, null, Body, "Character", "Level");
|
||||
|
||||
// Level number: retail renders this as large gold centered text in the 65×50 element.
|
||||
// Use the larger dat font (18px) for the level value so it fills more of the vertical
|
||||
// space in the 50px element and approaches the retail "large gold digit" look.
|
||||
// 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, LevelId, rowDatFont, Gold, () => data().Level.ToString());
|
||||
Label(layout, LevelId, null, Gold, () => data().Level.ToString());
|
||||
|
||||
// 0x10000234 = "Total Experience (XP):" caption, left of the XP value.
|
||||
// Source: dump idx=22; spec §Header element map.
|
||||
// runtime color, dat carries none.
|
||||
LabelLeft(layout, TotalXpLabelId, datFont, Body, static () => "Total Experience (XP):");
|
||||
|
||||
// runtime color, dat carries none.
|
||||
Label(layout, TotalXpId, datFont, Body, () => data().TotalXp.ToString("N0"));
|
||||
// TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font.
|
||||
LabelLeft(layout, TotalXpLabelId, null, Body, static () => "Total Experience (XP):");
|
||||
Label(layout, TotalXpId, null, Body, () => data().TotalXp.ToString("N0"));
|
||||
|
||||
// XP-to-level meter fill (gmStatManagementUI::UpdateExperience 0x004f0a70).
|
||||
// NOTE: child elements 0x10000237 (label) and 0x10000238 (value) are consumed by
|
||||
|
|
@ -763,10 +760,11 @@ public static class CharacterStatController
|
|||
titleEl.BackgroundSprite = 0;
|
||||
titleEl.VerticalJustify = VJustify.Top; // dat says Center; override to Top (see comment above)
|
||||
}
|
||||
// Title color: State A = body (parchment); State B = WHITE per retail (confirmed 2026-06-26 ref).
|
||||
// Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font.
|
||||
// Fix C: the dat has a 20px font for the footer title. Let it drive.
|
||||
if (titleEl is not null)
|
||||
{
|
||||
titleEl.DatFont = datFont;
|
||||
// DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game).
|
||||
// Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here.
|
||||
// RightAligned stays false (BuildText default for a Center element).
|
||||
titleEl.ClickThrough = true;
|
||||
|
|
@ -782,33 +780,26 @@ public static class CharacterStatController
|
|||
};
|
||||
}
|
||||
|
||||
// Line-1 label (Top=20, Left=5): State A = "Skill Credits Available:"; State B = "Experience To Raise:"
|
||||
// runtime color, dat carries none.
|
||||
// Footer lines (all dat-origin with their own font sizes): pass null → keep dat font.
|
||||
var l1L = ByPos(20f, 5f, FooterLine1Label);
|
||||
LabelProvider(l1L, datFont, Body, () =>
|
||||
LabelProvider(l1L, null, Body, () =>
|
||||
sel[0] < 0 ? "Skill Credits Available:" : "Experience To Raise:");
|
||||
|
||||
// Line-1 value (Top=20, Left=200): State A = SkillCredits; State B = raise cost
|
||||
// When attribute is maxed (cost == 0), retail shows "Infinity!" (confirmed 2026-06-26 ref).
|
||||
// runtime color, dat carries none.
|
||||
var l1V = ByPos(20f, 200f, FooterLine1Value);
|
||||
LabelProvider(l1V, datFont, Body, () =>
|
||||
LabelProvider(l1V, null, Body, () =>
|
||||
{
|
||||
if (sel[0] < 0) return data().SkillCredits.ToString();
|
||||
long cost = GetRaiseCost(data(), sel[0]);
|
||||
return cost > 0 ? cost.ToString("N0") : "Infinity!";
|
||||
});
|
||||
|
||||
// Line-2 label (Top=37, Left=5): "Unassigned Experience:" in both states
|
||||
// runtime color, dat carries none.
|
||||
// Line-2 elements: pass null → keep dat font.
|
||||
var l2L = ByPos(37f, 5f, FooterLine2Label);
|
||||
LabelProvider(l2L, datFont, Body,
|
||||
LabelProvider(l2L, null, Body,
|
||||
static () => "Unassigned Experience:");
|
||||
|
||||
// Line-2 value (Top=37, Left=200): UnassignedXp in both states
|
||||
// runtime color, dat carries none.
|
||||
var l2V = ByPos(37f, 200f, FooterLine2Value);
|
||||
LabelProvider(l2V, datFont, Body,
|
||||
LabelProvider(l2V, null, Body,
|
||||
() => data().UnassignedXp.ToString("N0"));
|
||||
}
|
||||
|
||||
|
|
@ -851,7 +842,9 @@ public static class CharacterStatController
|
|||
{
|
||||
if (layout.FindElement(id) is UiText t)
|
||||
{
|
||||
t.DatFont = datFont;
|
||||
// Null = keep whatever the importer (dat FontDid resolver) set at build time.
|
||||
// Non-null = controller explicit override.
|
||||
if (datFont is not null) t.DatFont = datFont;
|
||||
t.Centered = true;
|
||||
t.ClickThrough = true;
|
||||
t.LinesProvider = () => new[] { new UiText.Line(text(), color) };
|
||||
|
|
@ -869,7 +862,8 @@ public static class CharacterStatController
|
|||
{
|
||||
if (layout.FindElement(id) is UiText t)
|
||||
{
|
||||
t.DatFont = datFont;
|
||||
// Null = keep whatever the importer (dat FontDid resolver) set at build time.
|
||||
if (datFont is not null) t.DatFont = datFont;
|
||||
t.Centered = false; // non-Centered → scroll/multi-line path
|
||||
t.RightAligned = false;
|
||||
t.ClickThrough = true;
|
||||
|
|
@ -889,7 +883,8 @@ public static class CharacterStatController
|
|||
{
|
||||
if (layout.FindElement(id) is UiText t)
|
||||
{
|
||||
t.DatFont = datFont;
|
||||
// Null = keep whatever the importer (dat FontDid resolver) set at build time.
|
||||
if (datFont is not null) t.DatFont = datFont;
|
||||
t.Centered = false;
|
||||
t.RightAligned = false;
|
||||
t.ClickThrough = true;
|
||||
|
|
@ -906,7 +901,8 @@ public static class CharacterStatController
|
|||
private static void LabelProvider(UiText? t, UiDatFont? datFont, Vector4 color, Func<string> text)
|
||||
{
|
||||
if (t is null) return;
|
||||
t.DatFont = datFont;
|
||||
// Null = keep whatever the importer (dat FontDid resolver) set at build time.
|
||||
if (datFont is not null) t.DatFont = datFont;
|
||||
t.Centered = false;
|
||||
t.RightAligned = false;
|
||||
t.ClickThrough = true;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue