namespace AcDream.App.UI.Layout; /// /// Caching row-template resolver for /// consumers whose row COUNT can change across a session (a live roster, /// not a fixed authored set) — Friends, Squelch, and (Campaign FA slice /// FA4) Fellowship all resolve their 0x64 template entries through /// one instance of this class. /// /// /// Originally a private local function inside /// (Campaign FA slice FA3 /// fix-round blast SF-2): the ORIGINAL shape re-ran /// LayoutImporter.ImportInfos — a full DAT tree walk — under the /// shared DAT lock on EVERY row, every revision, even while the panel was /// closed. The fix caches each template id pair's resolved /// tree the FIRST time it is imported and never /// re-imports for that pair again; still calls /// (constructor parameter) on every invocation /// because each row needs its OWN instance — only /// the expensive per-row DAT tree WALK is memoized, not the built widget. /// /// /// /// Extracted to its own class in FA4 (carry-forward 2, the FA3 re-review's /// non-blocking finding: "add a production-resolver test for the new /// template cache") so this caching behavior is unit-testable against fake /// / delegates — no /// live DAT access needed to prove the cache actually short-circuits a /// repeat import. Production callers still take the shared DAT lock /// AROUND at the call site (see /// RetailUiRuntime.MountSocialPanel) — this class has no lock of its /// own, matching the "controller has no DAT dependency of its own" shape /// every other hermetically-testable page controller in this codebase /// already follows. /// /// public sealed class RowTemplateResolver { private readonly Dictionary<(uint LayoutId, uint ElementId), ElementInfo?> _cache = new(); private readonly Func _importInfos; private readonly Func _build; /// Number of times (the constructor /// parameter) actually ran — i.e. cache MISSES. Exposed for the /// production-resolver conformance test; not used by any production /// code path. public int ImportCount { get; private set; } public RowTemplateResolver( Func importInfos, Func build) { _importInfos = importInfos ?? throw new ArgumentNullException(nameof(importInfos)); _build = build ?? throw new ArgumentNullException(nameof(build)); } /// Resolves one row template. Null if the (layoutId, elementId) /// pair does not import (cached as a miss — a permanently-unresolvable /// template is not retried on every call; see the class doc's cache /// contract). 's exact /// signature. public UiElement? Resolve(uint templateLayoutId, uint templateElementId) { var key = (templateLayoutId, templateElementId); if (!_cache.TryGetValue(key, out ElementInfo? info)) { info = _importInfos(templateLayoutId, templateElementId); _cache[key] = info; ImportCount++; } return info is null ? null : _build(info); } }