acdream/src/AcDream.App/Studio/LayoutSource.cs
Erik a0d33956f6 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>
2026-06-26 10:30:13 +02:00

123 lines
4.5 KiB
C#

using AcDream.App.UI;
using AcDream.App.UI.Layout;
using DatReaderWriter;
namespace AcDream.App.Studio;
/// <summary>Which kind of source the studio is currently previewing.</summary>
public enum LayoutSourceKind { DatLayout, Markup }
/// <summary>
/// Wraps the two ways the UI Studio can load a panel to preview:
/// a LayoutDesc dat id, or a KSML markup file path (Task 6 — unsupported now).
///
/// <para>Call <see cref="Load"/> with the current <see cref="StudioOptions"/> to
/// import the layout and get the root <see cref="UiElement"/>. The result is also
/// cached in <see cref="CurrentLayout"/> so <see cref="Reload"/> can re-run the same
/// source without re-reading the options.</para>
/// </summary>
public sealed class LayoutSource
{
private readonly DatCollection _dats;
private readonly Func<uint, (uint, int, int)> _resolve;
private readonly UiDatFont? _datFont;
private readonly Func<uint, UiDatFont?>? _fontResolve;
public LayoutSourceKind Kind { get; private set; }
public uint? LayoutId { get; private set; }
public string? MarkupPath { get; private set; }
public string? LastError { get; private set; }
public ImportedLayout? CurrentLayout { get; private set; }
/// <summary>
/// Create a LayoutSource.
/// </summary>
/// <param name="fontResolve">Optional per-element font resolver: FontDid →
/// <see cref="UiDatFont"/> (null when the font isn't in the dats). When supplied,
/// elements with a non-zero FontDid receive their own dat font at build time
/// instead of the shared <paramref name="datFont"/> global. Controllers that
/// explicitly set <see cref="UiText.DatFont"/> after
/// <see cref="ImportedLayout.FindElement"/> still override the build-time value.
/// Pass null (default) for the original single-font behavior — the live
/// <see cref="GameWindow"/> path passes null so it is provably unchanged.</param>
public LayoutSource(
DatCollection dats,
Func<uint, (uint, int, int)> resolve,
UiDatFont? datFont,
Func<uint, UiDatFont?>? fontResolve = null)
{
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
_resolve = resolve ?? throw new ArgumentNullException(nameof(resolve));
_datFont = datFont;
_fontResolve = fontResolve;
}
/// <summary>
/// Load the layout described by <paramref name="opts"/>. For a dat layout
/// (<see cref="StudioOptions.LayoutId"/> is non-null) calls
/// <see cref="LayoutImporter.Import"/>. For a markup path sets
/// <see cref="LastError"/> and returns null (Task 6, not yet implemented).
///
/// Returns the root <see cref="UiElement"/> on success, or null on failure
/// (check <see cref="LastError"/>).
/// </summary>
public UiElement? Load(StudioOptions opts)
{
LastError = null;
CurrentLayout = null;
if (opts.MarkupPath is not null)
{
Kind = LayoutSourceKind.Markup;
MarkupPath = opts.MarkupPath;
LastError = "markup unsupported (Task 6)";
return null;
}
if (opts.LayoutId is null)
{
LastError = "ui-studio: no layout id or markup path specified.";
return null;
}
Kind = LayoutSourceKind.DatLayout;
LayoutId = opts.LayoutId;
return LoadDat(opts.LayoutId.Value);
}
/// <summary>Re-run the most-recently-configured source without re-reading options.</summary>
public UiElement? Reload()
{
LastError = null;
CurrentLayout = null;
if (Kind == LayoutSourceKind.Markup)
{
LastError = "markup unsupported (Task 6)";
return null;
}
if (LayoutId is null)
{
LastError = "ui-studio: no layout id to reload.";
return null;
}
return LoadDat(LayoutId.Value);
}
// ── Private ──────────────────────────────────────────────────────────────────
private UiElement? LoadDat(uint layoutId)
{
var imported = LayoutImporter.Import(_dats, layoutId, _resolve, _datFont, _fontResolve);
if (imported is null)
{
LastError = $"ui-studio: LayoutDesc 0x{layoutId:X8} not found in dats.";
return null;
}
CurrentLayout = imported;
return imported.Root;
}
}