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
|
|
@ -0,0 +1,194 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for Fix C: per-element dat FontDid resolver in <see cref="DatWidgetFactory"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// <see cref="UiDatFont"/> cannot be constructed in pure unit tests (GL + dat required),
|
||||
/// so these tests verify the plumbing using null/non-null identity and sentinel resolvers:
|
||||
/// <list type="bullet">
|
||||
/// <item>Null resolver → DatFont unchanged (backward-compat: same as before Fix C).</item>
|
||||
/// <item>Resolver present, FontDid == 0 → resolver NOT called; DatFont == global datFont.</item>
|
||||
/// <item>Resolver returns null for an id (font missing) → DatFont falls back to global datFont.</item>
|
||||
/// <item>Controller sets DatFont after build → controller value wins.</item>
|
||||
/// <item><see cref="LayoutImporter.Build"/> threads fontResolve to the factory.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class DatWidgetFactoryFontResolveTests
|
||||
{
|
||||
private static (uint, int, int) NoTex(uint _) => (0, 0, 0);
|
||||
|
||||
// ── Test 1: null fontResolve → DatFont == datFont (backward-compat) ─────
|
||||
|
||||
/// <summary>
|
||||
/// When fontResolve is null (the live GameWindow path), BuildText must NOT
|
||||
/// touch DatFont beyond setting it to the global datFont. Null in → null out.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void NullFontResolve_DatFont_EqualsGlobalFont()
|
||||
{
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = 0x40000001u };
|
||||
// datFont=null, fontResolve=null → backward-compat: DatFont == null
|
||||
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null, fontResolve: null));
|
||||
Assert.Null(t.DatFont);
|
||||
}
|
||||
|
||||
// ── Test 2: FontDid == 0 → resolver NOT called ────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// When the element has FontDid == 0, the fontResolve delegate must NOT be
|
||||
/// invoked (the element has no dat font; fall through to global datFont).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FontDidZero_ResolverNotCalled()
|
||||
{
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = 0u };
|
||||
bool called = false;
|
||||
UiDatFont? Resolver(uint id) { called = true; return null; }
|
||||
|
||||
// FontDid=0 → resolver should never fire, even though one is provided.
|
||||
Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null, fontResolve: Resolver));
|
||||
Assert.False(called, "fontResolve must NOT be called when FontDid == 0");
|
||||
}
|
||||
|
||||
// ── Test 3: resolver returns null (font missing) → fallback to datFont ───
|
||||
|
||||
/// <summary>
|
||||
/// When the element has a non-zero FontDid but the resolver returns null
|
||||
/// (font not found in dats), DatFont must fall back to the shared global datFont.
|
||||
/// Since datFont is null in unit tests, this verifies the fallback chain.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ResolverReturnsNull_FallsBackToGlobalFont()
|
||||
{
|
||||
const uint SomeFontDid = 0x40000005u;
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = SomeFontDid };
|
||||
int callCount = 0;
|
||||
UiDatFont? Resolver(uint id) { callCount++; return null; } // always returns null (font missing)
|
||||
|
||||
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, datFont: null, fontResolve: Resolver));
|
||||
|
||||
// Resolver was called exactly once for this element.
|
||||
Assert.Equal(1, callCount);
|
||||
// Fallback: DatFont == global datFont (null in unit tests).
|
||||
Assert.Null(t.DatFont);
|
||||
}
|
||||
|
||||
// ── Test 4: resolver called with correct FontDid ──────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// When fontResolve is provided and FontDid is non-zero, the resolver is called
|
||||
/// with the element's exact FontDid value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ResolverCalledWithElementFontDid()
|
||||
{
|
||||
const uint ExpectedFontDid = 0x40000002u;
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = ExpectedFontDid };
|
||||
uint? capturedId = null;
|
||||
UiDatFont? Resolver(uint id) { capturedId = id; return null; }
|
||||
|
||||
DatWidgetFactory.Create(info, NoTex, datFont: null, fontResolve: Resolver);
|
||||
|
||||
Assert.Equal(ExpectedFontDid, capturedId);
|
||||
}
|
||||
|
||||
// ── Test 5: controller DatFont override wins over build-time value ───────
|
||||
|
||||
/// <summary>
|
||||
/// A controller that calls FindElement and sets DatFont afterward MUST override
|
||||
/// whatever the factory set at build time. This is the backward-compat guarantee.
|
||||
/// The DatFont property is settable — a controller can override it after build.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ControllerDatFontOverride_WinsOverBuildTimeFont()
|
||||
{
|
||||
const uint SomeFontDid = 0x40000003u;
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20, FontDid = SomeFontDid };
|
||||
UiDatFont? Resolver(uint _) => null; // returns null → build-time DatFont == null
|
||||
|
||||
var t = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, datFont: null, fontResolve: Resolver));
|
||||
Assert.Null(t.DatFont); // build-time value set by factory
|
||||
|
||||
// Controller simulated override: set DatFont to null explicitly.
|
||||
// The real guarantee is that DatFont is a settable property.
|
||||
// (We can't construct a real UiDatFont here — it requires GL + dats.)
|
||||
t.DatFont = null; // controller overrides after build
|
||||
Assert.Null(t.DatFont);
|
||||
// ✓ DatFont is settable — controller override mechanism is in place.
|
||||
}
|
||||
|
||||
// ── Test 6: LayoutImporter.Build threads fontResolve to factory ──────────
|
||||
|
||||
/// <summary>
|
||||
/// When <see cref="LayoutImporter.Build"/> is given a fontResolve, it must pass
|
||||
/// it to <see cref="DatWidgetFactory.Create"/> for EVERY element in the tree.
|
||||
/// Verified by checking that a Type-12 child with FontDid triggers the resolver.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LayoutImporter_Build_ThreadsFontResolve_ToFactory()
|
||||
{
|
||||
const uint FontDid = 0x40000004u;
|
||||
var root = new ElementInfo { Id = 1u, Type = 3, Width = 200, Height = 100 };
|
||||
var child = new ElementInfo { Id = 2u, Type = 12, Width = 100, Height = 20, FontDid = FontDid };
|
||||
root.Children.Add(child);
|
||||
|
||||
int resolveCallCount = 0;
|
||||
UiDatFont? Resolver(uint id) { resolveCallCount++; return null; }
|
||||
|
||||
var layout = LayoutImporter.Build(root, NoTex, datFont: null, fontResolve: Resolver);
|
||||
|
||||
// The child element has a non-zero FontDid — resolver must have been called for it.
|
||||
Assert.True(resolveCallCount > 0, "fontResolve was not called for the child element");
|
||||
// The child widget was built (findable in the layout).
|
||||
Assert.NotNull(layout.FindElement(2u));
|
||||
}
|
||||
|
||||
// ── Test 7: Meter element also receives element font from resolver ────────
|
||||
|
||||
/// <summary>
|
||||
/// Type-7 (UiMeter) elements with a FontDid must also go through the resolver.
|
||||
/// Verified by checking the resolver fires for a meter element with a non-zero FontDid.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FontResolve_CalledForMeter_WhenFontDidPresent()
|
||||
{
|
||||
const uint MeterFontDid = 0x40000006u;
|
||||
var meter = new ElementInfo { Type = 7, Width = 150, Height = 16, FontDid = MeterFontDid };
|
||||
int callCount = 0;
|
||||
UiDatFont? Resolver(uint id) { callCount++; return null; }
|
||||
|
||||
var w = DatWidgetFactory.Create(meter, NoTex, datFont: null, fontResolve: Resolver);
|
||||
|
||||
var m = Assert.IsType<UiMeter>(w);
|
||||
// Resolver was called for the meter element's FontDid.
|
||||
Assert.True(callCount > 0, "fontResolve was not called for meter element");
|
||||
// Meter DatFont: resolver returned null, so DatFont falls back to global datFont (null).
|
||||
Assert.Null(m.DatFont);
|
||||
}
|
||||
|
||||
// ── Test 8: LayoutImporter.BuildFromInfos signature is backward-compat ────
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="LayoutImporter.BuildFromInfos"/> without fontResolve (default null)
|
||||
/// must still work for all callers that don't pass it — verifies the optional
|
||||
/// parameter default is correct and no existing callers broke.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildFromInfos_WithoutFontResolve_WorksAsBeforeFix()
|
||||
{
|
||||
var root = new ElementInfo { Id = 10u, Type = 3, Width = 200, Height = 100 };
|
||||
var child = new ElementInfo { Id = 11u, Type = 12, Width = 100, Height = 20, FontDid = 0x40000001u };
|
||||
|
||||
// Call without fontResolve (the old signature shape).
|
||||
var layout = LayoutImporter.BuildFromInfos(root, new[] { child }, NoTex, datFont: null);
|
||||
|
||||
var t = Assert.IsType<UiText>(layout.FindElement(11u));
|
||||
// No resolver → DatFont == global datFont (null in unit tests).
|
||||
Assert.Null(t.DatFont);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue