User finding 3 (retail screenshot): hovering a town on the Map tab turns its marker GREEN and shows the name on a special-font tooltip — clearly not our generic 0x10000395 popup skin, and we had no hover highlight at all. Re-derivation (live-DAT probe + raw ElementDesc dump + surface byte-decode; MapNoteLiveDatTests pins all of it): - m_pMap (0x100001EC)'s P0x47/P0x48 = 0x100001F0 @ 0x21000026 are the note CONSTRUCTION template (AddMapNote @0x004a1bb0's CreateChildElement args) — that part we had right. - The TEMPLATE's own DirectState authors the note's tooltip popup locator P0x47=0x10000398/P0x48=0x21000041 — the FOURTH popup skin, whose incorporated text child 0x10000396 fonts 0x40000015 where the other three skins font 0x40000002 (the user's "special font") — plus P0x50=0.0 (zero per-element tooltip delay: town tooltips fire the instant the dwell arms; UiRoot already honors it), P0x4B TooltipOn, and P0x13 RolloverEnabled. Batch C's "the template authors no locator of its own" claim was WRONG, and BuildTownMarkers' hardcoded shared-skin override was clobbering the authored values — removed. - The hover highlight: the template's Normal/Normal_rollover states are PassToChildren descriptors driving the swallowed highlight child 0x100001F1 (base 0x100002B7@0x21000042 — a four-piece frame all drawing 0x06004CC9, byte-decoded PURE GREEN A=FF R=00 G=FF B=00) via per-state P0x3B (Invisible): hidden at rest, green on rollover. Port: - UiButton.CascadeStateToChildren — retail UIElement::SetState @0x00464E70's PassToChildren cascade, keyed off the REQUESTED state id (properties commit unconditionally; only the sprite draw is art-gated, the existing #382/AP-222 distinction). - UiDatElement.TrySetRetailState honors per-state P0x3B for NAMED states (OnSetAttribute @0x00462d80 case 8: SetVisible(value==0)). The unnamed-DirectState case is explicitly excluded — honoring it would un-gate ISSUES #408 (1,083 authored-invisible elements) through BuildWidget's post-children state reapply; measured breaking the spell-favorite drag tests before the scoping (note added to #408). - MapPageController.BuildTownMarkers rebuilds the button-swallowed highlight child per marker through the AD-108 IconBuilder seam (Bindings.TemplateInfoResolver, backed by RowTemplateResolver.ResolveInfo — same cache) and arms it with the initial Normal cascade. Register TS-85's Batch C paragraph corrected; RetailTooltipPresenter's F10 shared-skin remark updated (MapPageController no longer a consumer). Tests: 3 installed-DAT pins (locator/delay/rollover; per-state P0x3B + green frame; the four-skin font sweep), UiButton cascade + UiDatElement P0x3B units, MapHousePanel marker no-clobber + hover-highlight fixture. App suite 5487 passed / 3 skips (5490 total, +11 over baseline); Runtime 1744/1744. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
93 lines
4.3 KiB
C#
93 lines
4.3 KiB
C#
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Caching row-template resolver for <see cref="UiTemplateListBox"/>
|
|
/// 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 <c>0x64</c> template entries through
|
|
/// one instance of this class.
|
|
///
|
|
/// <para>
|
|
/// Originally a private local function inside
|
|
/// <see cref="RetailUiRuntime.MountSocialPanel"/> (Campaign FA slice FA3
|
|
/// fix-round blast SF-2): the ORIGINAL shape re-ran
|
|
/// <c>LayoutImporter.ImportInfos</c> — 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
|
|
/// <see cref="ElementInfo"/> tree the FIRST time it is imported and never
|
|
/// re-imports for that pair again; <see cref="Resolve"/> still calls
|
|
/// <paramref name="build"/> (constructor parameter) on every invocation
|
|
/// because each row needs its OWN <see cref="UiElement"/> instance — only
|
|
/// the expensive per-row DAT tree WALK is memoized, not the built widget.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// 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
|
|
/// <paramref name="importInfos"/>/<paramref name="build"/> 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 <see cref="Resolve"/> at the call site (see
|
|
/// <c>RetailUiRuntime.MountSocialPanel</c>) — 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.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class RowTemplateResolver
|
|
{
|
|
private readonly Dictionary<(uint LayoutId, uint ElementId), ElementInfo?> _cache = new();
|
|
private readonly Func<uint, uint, ElementInfo?> _importInfos;
|
|
private readonly Func<ElementInfo, UiElement?> _build;
|
|
|
|
/// <summary>Number of times <paramref name="importInfos"/> (the constructor
|
|
/// parameter) actually ran — i.e. cache MISSES. Exposed for the
|
|
/// production-resolver conformance test; not used by any production
|
|
/// code path.</summary>
|
|
public int ImportCount { get; private set; }
|
|
|
|
public RowTemplateResolver(
|
|
Func<uint, uint, ElementInfo?> importInfos,
|
|
Func<ElementInfo, UiElement?> build)
|
|
{
|
|
_importInfos = importInfos ?? throw new ArgumentNullException(nameof(importInfos));
|
|
_build = build ?? throw new ArgumentNullException(nameof(build));
|
|
}
|
|
|
|
/// <summary>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). <see cref="UiTemplateListBox.TemplateResolver"/>'s exact
|
|
/// signature.</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>The cached import half alone — the template's resolved
|
|
/// <see cref="ElementInfo"/> without building a widget. Same cache, same
|
|
/// miss contract as <see cref="Resolve"/>. Consumers that need the
|
|
/// template's AUTHORED shape beyond what the built widget carries (the
|
|
/// map town markers' button-swallowed highlight child + its per-state
|
|
/// property bags — 2026-08-17 morning gate finding 3) read it here
|
|
/// instead of a second cold import.</summary>
|
|
public ElementInfo? ResolveInfo(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;
|
|
}
|
|
}
|